UI Redesign #37
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							| @ -7,6 +7,7 @@ require ( | ||||
| 	github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c | ||||
| 	github.com/alexedwards/scs/v2 v2.8.0 | ||||
| 	github.com/coreos/go-oidc/v3 v3.14.1 | ||||
| 	github.com/golang-migrate/migrate/v4 v4.18.3 | ||||
| 	github.com/gorilla/schema v1.4.1 | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	github.com/justinas/alice v1.2.0 | ||||
| @ -16,4 +17,9 @@ require ( | ||||
| 	golang.org/x/oauth2 v0.30.0 | ||||
| ) | ||||
| 
 | ||||
| require github.com/go-jose/go-jose/v4 v4.0.5 // indirect | ||||
| require ( | ||||
| 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect | ||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||
| 	go.uber.org/atomic v1.7.0 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										14
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								go.sum
									
									
									
									
									
								
							| @ -6,27 +6,41 @@ github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZx | ||||
| github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= | ||||
| github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= | ||||
| github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= | ||||
| github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= | ||||
| github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= | ||||
| github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= | ||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= | ||||
| github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= | ||||
| github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= | ||||
| github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= | ||||
| github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= | ||||
| github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= | ||||
| github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= | ||||
| github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= | ||||
| golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= | ||||
| golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= | ||||
|  | ||||
| @ -1,86 +1,882 @@ | ||||
| /* html { | ||||
|   background: lightgray; | ||||
| } */ | ||||
| /* CSS Reset and Base Styles */ | ||||
| *, | ||||
| *::before, | ||||
| *::after { | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| * { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| } | ||||
| 
 | ||||
| html { | ||||
|   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | ||||
|   line-height: 1.6; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
| 
 | ||||
| /* CSS Custom Properties for Theming */ | ||||
| :root { | ||||
|   /* Light mode colors */ | ||||
|   --color-primary: #2563eb; | ||||
|   --color-primary-hover: #1d4ed8; | ||||
|   --color-danger: #dc2626; | ||||
|   --color-danger-hover: #b91c1c; | ||||
|   --color-warning: #d97706; | ||||
| 
 | ||||
|   --color-text: #1f2937; | ||||
|   --color-text-muted: #6b7280; | ||||
|   --color-background: #ffffff; | ||||
|   --color-surface: #f9fafb; | ||||
|   --color-border: #e5e7eb; | ||||
|   --color-border-light: #f3f4f6; | ||||
| 
 | ||||
|   --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | ||||
| 
 | ||||
|   /* Spacing scale */ | ||||
|   --space-xs: 0.25rem; | ||||
|   --space-sm: 0.5rem; | ||||
|   --space-md: 1rem; | ||||
|   --space-lg: 1.5rem; | ||||
|   --space-xl: 2rem; | ||||
|   --space-2xl: 3rem; | ||||
|   --space-3xl: 4rem; | ||||
| 
 | ||||
|   /* Typography scale */ | ||||
|   --text-xs: 0.75rem; | ||||
|   --text-sm: 0.875rem; | ||||
|   --text-base: 1rem; | ||||
|   --text-lg: 1.125rem; | ||||
|   --text-xl: 1.25rem; | ||||
|   --text-2xl: 1.5rem; | ||||
|   --text-3xl: 1.875rem; | ||||
|   --text-4xl: 2.25rem; | ||||
| 
 | ||||
|   /* Border radius */ | ||||
|   --radius-sm: 0.125rem; | ||||
|   --radius-md: 0.375rem; | ||||
|   --radius-lg: 0.5rem; | ||||
| 
 | ||||
|   /* Layout */ | ||||
|   --max-width: 1200px; | ||||
|   --header-height: 60px; | ||||
| } | ||||
| 
 | ||||
| /* Dark mode colors */ | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   :root { | ||||
|     --color-text: #f9fafb; | ||||
|     --color-text-muted: #9ca3af; | ||||
|     --color-background: #111827; | ||||
|     --color-surface: #1f2937; | ||||
|     --color-border: #374151; | ||||
|     --color-border-light: #4b5563; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* Base Typography */ | ||||
| body { | ||||
|   max-width: 1024px; | ||||
|   margin: 1rem auto; | ||||
|   padding: 1rem; | ||||
|   /* background: white; */ | ||||
|   font-size: 1.2rem; | ||||
|   line-height: 1.5; | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
| } | ||||
| 
 | ||||
| header { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| body > nav { | ||||
|   font-size: var(--text-base); | ||||
|   color: var(--color-text); | ||||
|   background-color: var(--color-background); | ||||
|   min-height: 100vh; | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   flex-direction: column; | ||||
| } | ||||
| 
 | ||||
| body > nav ul { | ||||
|   list-style: none; | ||||
|   margin: 0 1rem; | ||||
|   padding: 0; | ||||
| h1, h2, h3, h4, h5, h6 { | ||||
|   font-weight: 600; | ||||
|   line-height: 1.25; | ||||
|   margin-bottom: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| body > nav li { | ||||
|   display: inline-block; | ||||
|   padding: 0 0.5rem; | ||||
| } | ||||
| h1 { font-size: var(--text-3xl); } | ||||
| h2 { font-size: var(--text-2xl); } | ||||
| h3 { font-size: var(--text-xl); } | ||||
| h4 { font-size: var(--text-lg); } | ||||
| 
 | ||||
| nav form { | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| nav button { | ||||
|   border: none; | ||||
|   background: none; | ||||
|   font-family: unset; | ||||
|   font-size: unset; | ||||
|   /* color: blue; */ | ||||
|   /* text-decoration: underline; */ | ||||
|   cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
|   padding: 1rem; | ||||
| } | ||||
| 
 | ||||
| div#dashboard { | ||||
|   display: flex; | ||||
|   flex-flow: row wrap; | ||||
| } | ||||
| 
 | ||||
| div#dashboard nav { | ||||
|   flex: 1 1 25%; | ||||
|   /* margin-top: 2rem; */ | ||||
|   min-width: 0; | ||||
| } | ||||
| 
 | ||||
| div#dashboard > div { | ||||
|   flex: 10 1 40%; | ||||
|   min-width: 0; | ||||
| } | ||||
| 
 | ||||
| div > pre { | ||||
|     max-width: 100%; | ||||
|     overflow: auto; | ||||
| } | ||||
| 
 | ||||
| main nav ul { | ||||
|   list-style: none; | ||||
|   margin: 1rem; | ||||
|   padding: 0; | ||||
| } | ||||
| 
 | ||||
| footer { | ||||
|   text-align: center; | ||||
| p { | ||||
|   margin-bottom: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|   /* color: blue; */ | ||||
|   color: var(--color-primary); | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|   color: var(--color-primary-hover); | ||||
|   text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| /* Layout Components */ | ||||
| body > header { | ||||
|   background-color: var(--color-surface); | ||||
|   border-bottom: 1px solid var(--color-border); | ||||
|   height: var(--header-height); | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 0 var(--space-lg); | ||||
|   position: sticky; | ||||
|   top: 0; | ||||
|   z-index: 100; | ||||
| } | ||||
| 
 | ||||
| body > header h1 { | ||||
|   margin: 0; | ||||
|   font-size: var(--text-2xl); | ||||
| } | ||||
| 
 | ||||
| body > header h1 a { | ||||
|   color: var(--color-text); | ||||
|   font-weight: 700; | ||||
| } | ||||
| 
 | ||||
| body > header h1 a:hover { | ||||
|   text-decoration: none; | ||||
|   color: var(--color-primary); | ||||
| } | ||||
| 
 | ||||
| nav { | ||||
|   background-color: var(--color-surface); | ||||
|   border-bottom: 1px solid var(--color-border); | ||||
|   padding: var(--space-md) var(--space-lg); | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   flex-wrap: wrap; | ||||
|   gap: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| .nav-welcome { | ||||
|   font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| .nav-links { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   gap: var(--space-md); | ||||
|   list-style: none; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   align-items: center; | ||||
| } | ||||
| 
 | ||||
| .nav-links li { | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| .nav-links a { | ||||
|   padding: var(--space-sm) var(--space-md); | ||||
|   border-radius: var(--radius-md); | ||||
|   transition: background-color 0.2s ease; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .nav-links a:hover { | ||||
|   background-color: var(--color-border-light); | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
|   flex: 1; | ||||
|   max-width: var(--max-width); | ||||
|   margin: 0 auto; | ||||
|   padding: var(--space-2xl) var(--space-lg); | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| footer { | ||||
|   background-color: var(--color-surface); | ||||
|   border-top: 1px solid var(--color-border); | ||||
|   padding: var(--space-lg); | ||||
|   text-align: center; | ||||
|   margin-top: auto; | ||||
| } | ||||
| 
 | ||||
| /* Dashboard Layout */ | ||||
| #dashboard { | ||||
|   display: grid; | ||||
|   grid-template-columns: 280px 1fr; | ||||
|   gap: var(--space-2xl); | ||||
|   margin-top: var(--space-xl); | ||||
| } | ||||
| 
 | ||||
| #dashboard nav { | ||||
|   background: none; | ||||
|   border: none; | ||||
|   padding: 0; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| #dashboard nav > div { | ||||
|   margin-bottom: var(--space-xl); | ||||
|   padding: var(--space-lg); | ||||
|   background-color: var(--color-surface); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-lg); | ||||
| } | ||||
| 
 | ||||
| #dashboard nav h3 { | ||||
|   font-size: var(--text-lg); | ||||
|   margin-bottom: var(--space-md); | ||||
|   padding-bottom: var(--space-sm); | ||||
|   border-bottom: 1px solid var(--color-border); | ||||
| } | ||||
| 
 | ||||
| #dashboard nav ul { | ||||
|   list-style: none; | ||||
|   margin-bottom: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| #dashboard nav ul:last-child { | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| #dashboard nav li { | ||||
|   margin-bottom: var(--space-xs); | ||||
| } | ||||
| 
 | ||||
| #dashboard nav a { | ||||
|   display: block; | ||||
|   padding: var(--space-sm) var(--space-md); | ||||
|   border-radius: var(--radius-md); | ||||
|   transition: background-color 0.2s ease; | ||||
| } | ||||
| 
 | ||||
| #dashboard nav a:hover { | ||||
|   background-color: var(--color-border-light); | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| /* Forms */ | ||||
| form { | ||||
|   background-color: var(--color-surface); | ||||
|   padding: var(--space-xl); | ||||
|   border-radius: var(--radius-lg); | ||||
|   border: 1px solid var(--color-border); | ||||
|   margin-bottom: var(--space-xl); | ||||
| } | ||||
| 
 | ||||
| .form-group { | ||||
|   margin-bottom: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| .form-group small { | ||||
|   display: block; | ||||
|   margin-top: var(--space-xs); | ||||
|   font-size: var(--text-sm); | ||||
|   color: var(--color-text-muted); | ||||
| } | ||||
| 
 | ||||
| fieldset { | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-md); | ||||
|   padding: var(--space-lg); | ||||
|   margin-bottom: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| fieldset.radio-group { | ||||
|   border: none; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| fieldset.danger-zone { | ||||
|   border-color: var(--color-danger); | ||||
|   background-color: rgba(220, 38, 38, 0.05); | ||||
| } | ||||
| 
 | ||||
| legend { | ||||
|   padding: 0 var(--space-sm); | ||||
|   font-weight: 600; | ||||
|   color: var(--color-text); | ||||
| } | ||||
| 
 | ||||
| label { | ||||
|   display: block; | ||||
|   font-weight: 500; | ||||
|   margin-bottom: var(--space-sm); | ||||
|   color: var(--color-text); | ||||
| } | ||||
| 
 | ||||
| input[type="text"], | ||||
| input[type="email"], | ||||
| input[type="url"], | ||||
| textarea, | ||||
| select { | ||||
|   width: 100%; | ||||
|   padding: var(--space-md); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-md); | ||||
|   background-color: var(--color-background); | ||||
|   color: var(--color-text); | ||||
|   font-size: var(--text-base); | ||||
|   transition: border-color 0.2s ease, box-shadow 0.2s ease; | ||||
| } | ||||
| 
 | ||||
| input[type="text"]:focus, | ||||
| input[type="email"]:focus, | ||||
| input[type="url"]:focus, | ||||
| textarea:focus, | ||||
| select:focus { | ||||
|   outline: none; | ||||
|   border-color: var(--color-primary); | ||||
|   box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1); | ||||
| } | ||||
| 
 | ||||
| textarea { | ||||
|   min-height: 120px; | ||||
|   resize: vertical; | ||||
| } | ||||
| 
 | ||||
| /* Radio buttons */ | ||||
| input[type="radio"] { | ||||
|   margin-right: var(--space-sm); | ||||
| } | ||||
| 
 | ||||
| label:has(input[type="radio"]) { | ||||
|   display: inline-flex; | ||||
|   align-items: center; | ||||
|   margin-right: var(--space-lg); | ||||
|   margin-bottom: var(--space-sm); | ||||
|   font-weight: normal; | ||||
| } | ||||
| 
 | ||||
| /* Buttons */ | ||||
| button, | ||||
| input[type="submit"] { | ||||
|   display: inline-flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   padding: var(--space-md) var(--space-lg); | ||||
|   border: none; | ||||
|   border-radius: var(--radius-md); | ||||
|   background-color: var(--color-primary); | ||||
|   color: white; | ||||
|   font-size: var(--text-base); | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.2s ease, transform 0.1s ease; | ||||
|   min-height: 44px; | ||||
| } | ||||
| 
 | ||||
| button:hover, | ||||
| input[type="submit"]:hover { | ||||
|   background-color: var(--color-primary-hover); | ||||
|   transform: translateY(-1px); | ||||
| } | ||||
| 
 | ||||
| button:active, | ||||
| input[type="submit"]:active { | ||||
|   transform: translateY(0); | ||||
| } | ||||
| 
 | ||||
| button.danger { | ||||
|   background-color: var(--color-danger); | ||||
| } | ||||
| 
 | ||||
| button.danger:hover { | ||||
|   background-color: var(--color-danger-hover); | ||||
| } | ||||
| 
 | ||||
| button.outline { | ||||
|   background-color: transparent; | ||||
|   color: var(--color-text); | ||||
|   border: 1px solid var(--color-border); | ||||
| } | ||||
| 
 | ||||
| button.outline:hover { | ||||
|   background-color: var(--color-surface); | ||||
|   border-color: var(--color-text-muted); | ||||
| } | ||||
| 
 | ||||
| /* Button variants */ | ||||
| .btn { | ||||
|   display: inline-flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   padding: var(--space-md) var(--space-lg); | ||||
|   border: none; | ||||
|   border-radius: var(--radius-md); | ||||
|   font-size: var(--text-base); | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   text-decoration: none; | ||||
|   transition: all 0.2s ease; | ||||
|   min-height: 44px; | ||||
| } | ||||
| 
 | ||||
| .btn-primary { | ||||
|   background-color: var(--color-primary); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .btn-primary:hover { | ||||
|   background-color: var(--color-primary-hover); | ||||
|   color: white; | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .btn-outline { | ||||
|   background-color: transparent; | ||||
|   color: var(--color-text); | ||||
|   border: 1px solid var(--color-border); | ||||
| } | ||||
| 
 | ||||
| .btn-outline:hover { | ||||
|   background-color: var(--color-surface); | ||||
|   border-color: var(--color-text-muted); | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| /* Comments */ | ||||
| #comments { | ||||
|   margin-top: var(--space-2xl); | ||||
| } | ||||
| 
 | ||||
| .comment { | ||||
|   background-color: var(--color-surface); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-lg); | ||||
|   padding: var(--space-lg); | ||||
|   margin-bottom: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| .comment-header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: flex-start; | ||||
|   margin-bottom: var(--space-md); | ||||
|   gap: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| .comment-meta { | ||||
|   flex: 1; | ||||
| } | ||||
| 
 | ||||
| .comment-author { | ||||
|   margin: 0 0 var(--space-xs) 0; | ||||
|   font-size: var(--text-lg); | ||||
| } | ||||
| 
 | ||||
| .comment time { | ||||
|   font-size: var(--text-sm); | ||||
|   color: var(--color-text-muted); | ||||
| } | ||||
| 
 | ||||
| .comment-content p:last-of-type { | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .comment-actions { | ||||
|   display: flex; | ||||
|   gap: var(--space-sm); | ||||
|   flex-shrink: 0; | ||||
| } | ||||
| 
 | ||||
| .comments-list { | ||||
|   margin-top: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| /* Code blocks */ | ||||
| pre { | ||||
|   background-color: var(--color-surface); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-md); | ||||
|   padding: var(--space-lg); | ||||
|   overflow-x: auto; | ||||
|   margin-bottom: var(--space-lg); | ||||
|   white-space: pre-wrap; | ||||
|   word-break: break-all; | ||||
|   line-height: 1.4; | ||||
| } | ||||
| 
 | ||||
| code { | ||||
|   font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; | ||||
|   font-size: var(--text-sm); | ||||
|   background-color: var(--color-border-light); | ||||
|   padding: var(--space-xs) var(--space-sm); | ||||
|   border-radius: var(--radius-sm); | ||||
|   word-break: break-all; | ||||
|   white-space: pre-wrap; | ||||
| } | ||||
| 
 | ||||
| pre code { | ||||
|   background: none; | ||||
|   padding: 0; | ||||
|   white-space: pre-wrap; | ||||
|   word-break: break-all; | ||||
| } | ||||
| 
 | ||||
| .code-example { | ||||
|   margin: var(--space-lg) 0; | ||||
| } | ||||
| 
 | ||||
| .code-example figcaption { | ||||
|   font-size: var(--text-sm); | ||||
|   color: var(--color-text-muted); | ||||
|   margin-bottom: var(--space-sm); | ||||
|   font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| /* Lists */ | ||||
| ul, ol { | ||||
|   padding-left: var(--space-xl); | ||||
|   margin-bottom: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| li { | ||||
|   margin-bottom: var(--space-sm); | ||||
| } | ||||
| 
 | ||||
| ul#websites { | ||||
|   list-style: none; | ||||
|   padding: 0; | ||||
| } | ||||
| 
 | ||||
| ul#websites li { | ||||
|   margin-bottom: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| /* Website cards */ | ||||
| .website-card { | ||||
|   background-color: var(--color-surface); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-lg); | ||||
|   padding: var(--space-xl); | ||||
|   transition: box-shadow 0.2s ease; | ||||
| } | ||||
| 
 | ||||
| .website-card:hover { | ||||
|   box-shadow: var(--shadow-md); | ||||
| } | ||||
| 
 | ||||
| .website-header { | ||||
|   margin-bottom: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| .website-name { | ||||
|   margin: 0 0 var(--space-sm) 0; | ||||
| } | ||||
| 
 | ||||
| .website-name a { | ||||
|   color: var(--color-text); | ||||
|   font-weight: 600; | ||||
|   font-size: var(--text-xl); | ||||
| } | ||||
| 
 | ||||
| .website-url { | ||||
|   font-size: var(--text-sm); | ||||
|   color: var(--color-text-muted); | ||||
|   font-family: monospace; | ||||
| } | ||||
| 
 | ||||
| .website-actions { | ||||
|   display: flex; | ||||
|   gap: var(--space-sm); | ||||
|   flex-wrap: wrap; | ||||
| } | ||||
| 
 | ||||
| /* Screen reader only content */ | ||||
| .sr-only { | ||||
|   position: absolute; | ||||
|   width: 1px; | ||||
|   height: 1px; | ||||
|   padding: 0; | ||||
|   margin: -1px; | ||||
|   overflow: hidden; | ||||
|   clip: rect(0, 0, 0, 0); | ||||
|   white-space: nowrap; | ||||
|   border: 0; | ||||
| } | ||||
| 
 | ||||
| /* Notices and alerts */ | ||||
| .notice, | ||||
| .warning-notice { | ||||
|   padding: var(--space-md); | ||||
|   border-radius: var(--radius-md); | ||||
|   margin-bottom: var(--space-lg); | ||||
|   border-left: 4px solid var(--color-warning); | ||||
|   background-color: rgba(217, 119, 6, 0.1); | ||||
| } | ||||
| 
 | ||||
| .warning-notice { | ||||
|   border-left-color: var(--color-danger); | ||||
|   background-color: rgba(220, 38, 38, 0.1); | ||||
| } | ||||
| 
 | ||||
| /* Section headers */ | ||||
| .section-header { | ||||
|   margin-bottom: var(--space-xl); | ||||
| } | ||||
| 
 | ||||
| .section-header:has(.btn) { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: flex-start; | ||||
|   flex-wrap: wrap; | ||||
|   gap: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| .section-header h1 { | ||||
|   margin-bottom: var(--space-sm); | ||||
| } | ||||
| 
 | ||||
| .section-description { | ||||
|   color: var(--color-text-muted); | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| /* Hero section (index.html) */ | ||||
| .hero { | ||||
|   text-align: center; | ||||
|   padding: var(--space-3xl) 0; | ||||
|   background: linear-gradient(135deg, var(--color-surface) 0%, var(--color-background) 100%); | ||||
|   border-radius: var(--radius-lg); | ||||
|   margin-bottom: var(--space-2xl); | ||||
| } | ||||
| 
 | ||||
| .hero-header h1 { | ||||
|   font-size: var(--text-4xl); | ||||
|   margin-bottom: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| .hero-subtitle { | ||||
|   font-size: var(--text-xl); | ||||
|   color: var(--color-text-muted); | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| /* Instruction sections (dashboard_main.html) */ | ||||
| .instruction-overview { | ||||
|   background-color: var(--color-surface); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-md); | ||||
|   padding: var(--space-lg); | ||||
|   margin-bottom: var(--space-2xl); | ||||
| } | ||||
| 
 | ||||
| .instruction-method { | ||||
|   margin-bottom: var(--space-2xl); | ||||
|   padding-bottom: var(--space-2xl); | ||||
|   border-bottom: 1px solid var(--color-border); | ||||
| } | ||||
| 
 | ||||
| .instruction-method:last-child { | ||||
|   border-bottom: none; | ||||
| } | ||||
| 
 | ||||
| .instruction-step { | ||||
|   margin-bottom: var(--space-xl); | ||||
| } | ||||
| 
 | ||||
| .instruction-step h3 { | ||||
|   color: var(--color-primary); | ||||
|   margin-bottom: var(--space-md); | ||||
| } | ||||
| 
 | ||||
| .method-note { | ||||
|   background-color: rgba(59, 130, 246, 0.1); | ||||
|   border: 1px solid rgba(59, 130, 246, 0.2); | ||||
|   border-radius: var(--radius-md); | ||||
|   padding: var(--space-md); | ||||
|   margin-top: var(--space-lg); | ||||
| } | ||||
| 
 | ||||
| .help-section { | ||||
|   background-color: var(--color-surface); | ||||
|   border: 1px solid var(--color-border); | ||||
|   border-radius: var(--radius-lg); | ||||
|   padding: var(--space-xl); | ||||
|   margin-top: var(--space-2xl); | ||||
| } | ||||
| 
 | ||||
| /* Utilities */ | ||||
| hr { | ||||
|   border: none; | ||||
|   height: 1px; | ||||
|   background-color: var(--color-border); | ||||
|   margin: var(--space-lg) 0; | ||||
| } | ||||
| 
 | ||||
| .htmx-indicator { | ||||
|   opacity: 0; | ||||
|   transition: opacity 200ms ease-in; | ||||
| } | ||||
| 
 | ||||
| .htmx-request .htmx-indicator, | ||||
| .htmx-request.htmx-indicator { | ||||
|   opacity: 1; | ||||
| } | ||||
| 
 | ||||
| /* Responsive Design */ | ||||
| @media (max-width: 768px) { | ||||
|   :root { | ||||
|     --space-lg: 1rem; | ||||
|     --space-xl: 1.5rem; | ||||
|     --space-2xl: 2rem; | ||||
|   } | ||||
| 
 | ||||
|   body > header { | ||||
|     padding: 0 var(--space-md); | ||||
|   } | ||||
| 
 | ||||
|   nav { | ||||
|     padding: var(--space-md); | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|     gap: var(--space-sm); | ||||
|   } | ||||
| 
 | ||||
|   .nav-welcome { | ||||
|     text-align: center; | ||||
|     padding-bottom: var(--space-sm); | ||||
|     border-bottom: 1px solid var(--color-border); | ||||
|   } | ||||
| 
 | ||||
|   .nav-links { | ||||
|     justify-content: center; | ||||
|     gap: var(--space-sm); | ||||
|   } | ||||
| 
 | ||||
|   .nav-links a { | ||||
|     padding: var(--space-sm); | ||||
|     font-size: var(--text-sm); | ||||
|   } | ||||
| 
 | ||||
|   main { | ||||
|     padding: var(--space-lg) var(--space-md); | ||||
|   } | ||||
| 
 | ||||
|   #dashboard { | ||||
|     grid-template-columns: 1fr; | ||||
|     gap: var(--space-lg); | ||||
|   } | ||||
| 
 | ||||
|   #dashboard nav > div { | ||||
|     padding: var(--space-md); | ||||
|   } | ||||
| 
 | ||||
|   form { | ||||
|     padding: var(--space-lg); | ||||
|   } | ||||
| 
 | ||||
|   h1 { font-size: var(--text-2xl); } | ||||
|   h2 { font-size: var(--text-xl); } | ||||
| 
 | ||||
|   label:has(input[type="radio"]) { | ||||
|     display: block; | ||||
|     margin-bottom: var(--space-sm); | ||||
|   } | ||||
| 
 | ||||
|   .hero { | ||||
|     padding: var(--space-2xl) 0; | ||||
|   } | ||||
| 
 | ||||
|   .hero-header h1 { | ||||
|     font-size: var(--text-3xl); | ||||
|   } | ||||
| 
 | ||||
|   .hero-subtitle { | ||||
|     font-size: var(--text-lg); | ||||
|   } | ||||
| 
 | ||||
|   .section-header:has(.btn) { | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|   } | ||||
| 
 | ||||
|   .section-header .btn { | ||||
|     width: 100%; | ||||
|     text-align: center; | ||||
|   } | ||||
| 
 | ||||
|   .section-description { | ||||
|     font-size: var(--text-sm); | ||||
|   } | ||||
| 
 | ||||
|   .website-actions { | ||||
|     flex-direction: column; | ||||
|   } | ||||
| 
 | ||||
|   .website-actions .btn { | ||||
|     width: 100%; | ||||
|   } | ||||
| 
 | ||||
|   .comment-header { | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|   } | ||||
| 
 | ||||
|   .comment-actions { | ||||
|     margin-top: var(--space-sm); | ||||
|   } | ||||
| 
 | ||||
|   pre { | ||||
|     font-size: var(--text-xs); | ||||
|     padding: var(--space-md); | ||||
|     white-space: pre-wrap; | ||||
|     word-break: break-all; | ||||
|     overflow-x: hidden; | ||||
|   } | ||||
| 
 | ||||
|   code { | ||||
|     font-size: var(--text-xs); | ||||
|     word-break: break-all; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   body > header h1 { | ||||
|     font-size: var(--text-xl); | ||||
|   } | ||||
| 
 | ||||
|   .nav-links { | ||||
|     flex-direction: column; | ||||
|     gap: var(--space-xs); | ||||
|   } | ||||
| 
 | ||||
|   .nav-links a { | ||||
|     width: 100%; | ||||
|     text-align: center; | ||||
|   } | ||||
| 
 | ||||
|   button, | ||||
|   input[type="submit"] { | ||||
|     width: 100%; | ||||
|     margin-bottom: var(--space-sm); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* Focus styles for better accessibility */ | ||||
| button:focus-visible, | ||||
| input:focus-visible, | ||||
| textarea:focus-visible, | ||||
| select:focus-visible { | ||||
|   outline: 2px solid var(--color-primary); | ||||
|   outline-offset: 2px; | ||||
| } | ||||
| 
 | ||||
| /* Print styles */ | ||||
| @media print { | ||||
|   body > header, | ||||
|   nav, | ||||
|   footer, | ||||
|   button, | ||||
|   input[type="submit"] { | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   main { | ||||
|     max-width: none; | ||||
|     padding: 0; | ||||
|   } | ||||
| 
 | ||||
|   #dashboard { | ||||
|     grid-template-columns: 1fr; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -41,31 +41,33 @@ templ commonHeader() { | ||||
| 
 | ||||
| templ topNav(data CommonData) { | ||||
| 	{{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }} | ||||
| 	<nav> | ||||
| 		<div> | ||||
| 			if data.IsAuthenticated { | ||||
| 				Welcome, { data.CurrentUser.Username } | ||||
| 			} | ||||
| 	<nav aria-label="Site navigation"> | ||||
| 		<div class="nav-welcome"> | ||||
| 			<span> | ||||
| 				if data.IsAuthenticated { | ||||
| 					Welcome, { data.CurrentUser.Username } | ||||
| 				} | ||||
| 			</span> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 		<ul class="nav-links"> | ||||
| 			if data.IsAuthenticated { | ||||
| 				<a href="/guestbooks">All Guestbooks</a> | | ||||
| 				<a href="/websites">My Websites</a> |  | ||||
| 				<a href="/users/settings">Settings</a> |  | ||||
| 				<a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a> | ||||
| 				<li><a href="/guestbooks">All Guestbooks</a></li> | ||||
| 				<li><a href="/websites">My Websites</a></li> | ||||
| 				<li><a href="/users/settings">Settings</a></li> | ||||
| 				<li><a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a></li> | ||||
| 			} else { | ||||
| 				if data.LocalAuthEnabled { | ||||
| 					<a href="/users/register">Create an Account</a> |  | ||||
| 					<li><a href="/users/register">Create an Account</a></li> |  | ||||
| 				} | ||||
| 				<a href="/users/login">Login</a> | ||||
| 				<li><a href="/users/login">Login</a></li> | ||||
| 			} | ||||
| 		</div> | ||||
| 		</ul> | ||||
| 	</nav> | ||||
| } | ||||
| 
 | ||||
| templ commonFooter() { | ||||
| 	<footer> | ||||
| 		<p>A <a href="https://32bit.cafe">32bit.cafe</a> Project</p> | ||||
| 		<p>A <a href="https://32bit.cafe" rel="noopener">32bit.cafe</a> Project</p> | ||||
| 	</footer> | ||||
| } | ||||
| 
 | ||||
| @ -76,18 +78,16 @@ templ base(title string, data CommonData) { | ||||
| 			<title>{ title } - webweav.ing</title> | ||||
| 			<meta charset="UTF-8"/> | ||||
| 			<meta name="viewport" content="width=device-width, initial-scale=1"/> | ||||
| 			<link href="/static/css/classless.min.css" rel="stylesheet"/> | ||||
| 			<link href="/static/css/style.css" rel="stylesheet"/> | ||||
| 			<script src="/static/js/htmx.min.js"></script> | ||||
| 		</head> | ||||
| 		<body> | ||||
| 			@commonHeader() | ||||
| 			@topNav(data) | ||||
| 			<main> | ||||
| 			<main role="main"> | ||||
| 				if data.Flash != "" { | ||||
| 					<div class="flash">{ data.Flash }</div> | ||||
| 					<div class="notice flash">{ data.Flash }</div> | ||||
| 				} | ||||
| 				<h1>{ title }</h1> | ||||
| 				{ children... } | ||||
| 			</main> | ||||
| 			@commonFooter() | ||||
|  | ||||
| @ -92,7 +92,7 @@ func topNav(data CommonData) templ.Component { | ||||
| 		} | ||||
| 		ctx = templ.ClearChildren(ctx) | ||||
| 		hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<nav><div>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<nav aria-label=\"Site navigation\"><div class=\"nav-welcome\"><span>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| @ -104,48 +104,48 @@ func topNav(data CommonData) templ.Component { | ||||
| 			var templ_7745c5c3_Var3 string | ||||
| 			templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 47, Col: 40} | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 48, Col: 41} | ||||
| 			} | ||||
| 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></div><ul class=\"nav-links\">") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		if data.IsAuthenticated { | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"/guestbooks\">All Guestbooks</a> | <a href=\"/websites\">My Websites</a> |  <a href=\"/users/settings\">Settings</a> |  <a href=\"#\" hx-post=\"/users/logout\" hx-headers=\"") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li><a href=\"/guestbooks\">All Guestbooks</a></li><li><a href=\"/websites\">My Websites</a></li><li><a href=\"/users/settings\">Settings</a></li><li><a href=\"#\" hx-post=\"/users/logout\" hx-headers=\"") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 			var templ_7745c5c3_Var4 string | ||||
| 			templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 55, Col: 62} | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 57, Col: 66} | ||||
| 			} | ||||
| 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Logout</a>") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Logout</a></li>") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 		} else { | ||||
| 			if data.LocalAuthEnabled { | ||||
| 				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"/users/register\">Create an Account</a> | ") | ||||
| 				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<li><a href=\"/users/register\">Create an Account</a></li>| ") | ||||
| 				if templ_7745c5c3_Err != nil { | ||||
| 					return templ_7745c5c3_Err | ||||
| 				} | ||||
| 			} | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <a href=\"/users/login\">Login</a>") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <li><a href=\"/users/login\">Login</a></li>") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></nav>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</ul></nav>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| @ -174,7 +174,7 @@ func commonFooter() templ.Component { | ||||
| 			templ_7745c5c3_Var5 = templ.NopComponent | ||||
| 		} | ||||
| 		ctx = templ.ClearChildren(ctx) | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><p>A <a href=\"https://32bit.cafe\">32bit.cafe</a> Project</p></footer>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><p>A <a href=\"https://32bit.cafe\" rel=\"noopener\">32bit.cafe</a> Project</p></footer>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| @ -210,13 +210,13 @@ func base(title string, data CommonData) templ.Component { | ||||
| 		var templ_7745c5c3_Var7 string | ||||
| 		templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 76, Col: 17} | ||||
| 			return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 78, Col: 17} | ||||
| 		} | ||||
| 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/classless.min.css\" rel=\"stylesheet\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| @ -228,19 +228,19 @@ func base(title string, data CommonData) templ.Component { | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<main>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<main role=\"main\">") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		if data.Flash != "" { | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"flash\">") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"notice flash\">") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 			var templ_7745c5c3_Var8 string | ||||
| 			templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 88, Col: 36} | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 89, Col: 43} | ||||
| 			} | ||||
| 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| @ -251,28 +251,11 @@ func base(title string, data CommonData) templ.Component { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h1>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		var templ_7745c5c3_Var9 string | ||||
| 		templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(title) | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 90, Col: 15} | ||||
| 		} | ||||
| 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h1>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templ_7745c5c3_Var6.Render(ctx, templ_7745c5c3_Buffer) | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</main>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</main>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| @ -280,7 +263,7 @@ func base(title string, data CommonData) templ.Component { | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</body></html>") | ||||
| 		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</body></html>") | ||||
| 		if templ_7745c5c3_Err != nil { | ||||
| 			return templ_7745c5c3_Err | ||||
| 		} | ||||
|  | ||||
| @ -10,14 +10,19 @@ templ GuestbookDashboardCommentsView(title string, data CommonData, website mode | ||||
| 		<div id="dashboard"> | ||||
| 			@wSidebar(website) | ||||
| 			<div> | ||||
| 				<h1>Comments on { website.Name }</h1> | ||||
| 				<hr/> | ||||
| 				if len(comments) == 0 { | ||||
| 					<p>No comments yet!</p> | ||||
| 				} | ||||
| 				for  _, c := range comments { | ||||
| 					@GuestbookDashboardCommentView(data, website, c) | ||||
| 				} | ||||
| 				<section aria-labelledby="comments-management-heading"> | ||||
| 					<header class="section-header"> | ||||
| 						<h1 id="comments-management-heading">Comments on yq</h1> | ||||
| 						<p class="section-description">Manage, moderate, and organize comments on your guestbook</p> | ||||
| 					</header> | ||||
| 					<hr role="separator"/> | ||||
| 					if len(comments) == 0 { | ||||
| 						<p>No comments yet!</p> | ||||
| 					} | ||||
| 					for  _, c := range comments { | ||||
| 						@GuestbookDashboardCommentView(data, website, c) | ||||
| 					} | ||||
| 				</section> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	} | ||||
| @ -60,41 +65,48 @@ templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models. | ||||
| } | ||||
| 
 | ||||
| templ commentForm(form forms.CommentCreateForm) { | ||||
| 	<div> | ||||
| 		<label for="authorname">Name</label> | ||||
| 		{{ error, exists := form.FieldErrors["authorName"] }} | ||||
| 		if exists { | ||||
| 			<label class="error">{ error }</label> | ||||
| 		} | ||||
| 		<input type="text" name="authorname" id="authorname"/> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label for="authoremail">Email (Optional) </label> | ||||
| 		{{ error, exists = form.FieldErrors["authorEmail"] }} | ||||
| 		if exists { | ||||
| 			<label class="error">{ error }</label> | ||||
| 		} | ||||
| 		<input type="text" name="authoremail" id="authoremail"/> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label for="authorsite">Site Url (Optional) </label> | ||||
| 		{{ error, exists = form.FieldErrors["authorSite"] }} | ||||
| 		if exists { | ||||
| 			<label class="error">{ error }</label> | ||||
| 		} | ||||
| 		<input type="text" name="authorsite" id="authorsite"/> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label for="content">Comment</label> | ||||
| 		{{ error, exists = form.FieldErrors["content"] }} | ||||
| 		if exists { | ||||
| 			<label class="error">{ error }</label> | ||||
| 		} | ||||
| 		<textarea name="content" id="content"></textarea> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<input type="submit" value="Submit"/> | ||||
| 	</div> | ||||
| 	<fieldset> | ||||
| 		<legend>Leave a comment</legend> | ||||
| 		<div class="form-group"> | ||||
| 			<label for="authorname">Name <span aria-label="required">*</span></label> | ||||
| 			{{ error, exists := form.FieldErrors["authorName"] }} | ||||
| 			if exists { | ||||
| 				<label class="error">{ error }</label> | ||||
| 			} | ||||
| 			<input type="text" name="authorname" id="authorname" required aria-describedby="authorname-help"/> | ||||
| 			<small id="authorname-help">Your name or handle</small> | ||||
| 		</div> | ||||
| 		<div class="form-group"> | ||||
| 			<label for="authoremail">Email (Optional)</label> | ||||
| 			{{ error, exists = form.FieldErrors["authorEmail"] }} | ||||
| 			if exists { | ||||
| 				<label class="error">{ error }</label> | ||||
| 			} | ||||
| 			<input type="email" name="authoremail" id="authoremail" aria-describedby="authoremail-help"/> | ||||
| 			<small id="authoremail-help">We won't share your email address, except with the guestbook's owner</small> | ||||
| 		</div> | ||||
| 		<div class="form-group"> | ||||
| 			<label for="authorsite">Website URL (Optional)</label> | ||||
| 			{{ error, exists = form.FieldErrors["authorSite"] }} | ||||
| 			if exists { | ||||
| 				<label class="error">{ error }</label> | ||||
| 			} | ||||
| 			<input type="url" name="authorsite" id="authorsite" aria-describedby="authorsite-help"/> | ||||
| 			<small id="authorsite-help">Link to your website or social profile</small> | ||||
| 		</div> | ||||
| 		<div class="form-group"> | ||||
| 			<label for="content">Comment <span aria-label="required">*</span></label> | ||||
| 			{{ error, exists = form.FieldErrors["content"] }} | ||||
| 			if exists { | ||||
| 				<label class="error">{ error }</label> | ||||
| 			} | ||||
| 			<textarea name="content" id="content" required aria-describedby="content-help"></textarea> | ||||
| 			<small id="content-help">Share your thoughts, feedback, or just say hello!</small> | ||||
| 		</div> | ||||
| 		<div class="form-group"> | ||||
| 			<button type="submit">Submit Comment</button> | ||||
| 		</div> | ||||
| 	</fieldset> | ||||
| } | ||||
| 
 | ||||
| templ GuestbookView(title string, data CommonData, website models.Website, guestbook models.Guestbook, comments []models.GuestbookComment, form forms.CommentCreateForm) { | ||||
| @ -105,39 +117,46 @@ templ GuestbookView(title string, data CommonData, website models.Website, guest | ||||
| 		<html> | ||||
| 			<head> | ||||
| 				<title>{ title }</title> | ||||
| 				<link href="/static/css/classless.min.css" rel="stylesheet"/> | ||||
| 				<meta name="viewport" content="width=device-width, initial-scale=1"/> | ||||
| 				<link href="/static/css/style.css" rel="stylesheet"/> | ||||
| 				<script src="/static/js/main.js" defer></script> | ||||
| 			</head> | ||||
| 			<body> | ||||
| 				<main> | ||||
| 					<div> | ||||
| 				<main role="main"> | ||||
| 					<section aria-labelledby="guestbook-heading"> | ||||
| 						<h1>{ website.Name } Guestbook</h1> | ||||
| 						{ data.Flash } | ||||
| 						<form action={ templ.URL(postUrl) } method="post"> | ||||
| 							<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> | ||||
| 							@commentForm(form) | ||||
| 						</form> | ||||
| 					</div> | ||||
| 					<div id="comments"> | ||||
| 					</section> | ||||
| 					<section id="comments" aria-labelledby="comments-heading"> | ||||
| 						<h2 id="comments-heading">Comments</h2> | ||||
| 						if len(comments) == 0 { | ||||
| 							<p>No comments yet!</p> | ||||
| 						} | ||||
| 						for _, c := range comments { | ||||
| 							<div> | ||||
| 								<h3> | ||||
| 									if c.AuthorSite != "" { | ||||
| 										<a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorName }</a> | ||||
| 									} else { | ||||
| 										{ c.AuthorName } | ||||
| 									} | ||||
| 								</h3> | ||||
| 								<time datetime={ c.Created.Format(time.RFC3339) }>{ c.Created.Format("01-02-2006 03:04PM") }</time> | ||||
| 								<p> | ||||
| 									{ c.CommentText } | ||||
| 								</p> | ||||
| 							</div> | ||||
| 						for i, c := range comments { | ||||
| 							{{ commentAuthorRole := fmt.Sprintf("comment-author-%d", i+1) }} | ||||
| 							<article class="comment" role="article" aria-labelledby={ commentAuthorRole }> | ||||
| 								<header class="comment-header"> | ||||
| 									<h3 id={ commentAuthorRole } class="comment-author"> | ||||
| 										if c.AuthorSite != "" { | ||||
| 											<a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorName }</a> | ||||
| 										} else { | ||||
| 											{ c.AuthorName } | ||||
| 										} | ||||
| 									</h3> | ||||
| 									<time datetime={ c.Created.Format(time.RFC3339) }>{ c.Created.Format("01-02-2006 03:04PM") }</time> | ||||
| 								</header> | ||||
| 								<div class="comment-content"> | ||||
| 									<p> | ||||
| 										{ c.CommentText } | ||||
| 									</p> | ||||
| 								</div> | ||||
| 							</article> | ||||
| 						} | ||||
| 					</div> | ||||
| 					</section> | ||||
| 				</main> | ||||
| 			</body> | ||||
| 		</html> | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -2,13 +2,14 @@ package views | ||||
| 
 | ||||
| templ Home(title string, data CommonData) { | ||||
| 	@base(title, data) { | ||||
| 		<h2>Welcome</h2> | ||||
| 		<p> | ||||
| 			Welcome to webweav.ing, a collection of webmastery tools created by the <a href="https://32bit.cafe">32-Bit Cafe</a>. | ||||
| 		</p> | ||||
| 		<p> | ||||
| 			Note this service is in a pre-alpha state. Your account and data can disappear at any time.  | ||||
| 		</p> | ||||
| 		<section aria-labelledby="welcome-heading"> | ||||
| 			<h2 id="welcome-heading">Welcome</h2> | ||||
| 			<p>Welcome to webweav.ing, a collection of webmastery tools created by the <a href="https://32bit.cafe" rel="noopener">32-Bit Cafe</a>.</p> | ||||
| 			<aside class="notice" role="alert" aria-labelledby="service-status"> | ||||
| 				<h3 id="service-status" class="sr-only">Service Status Notice</h3> | ||||
| 				<p><strong>Important:</strong> This service is in a pre-alpha state. Your account and data can disappear at any time.</p> | ||||
| 			</aside> | ||||
| 		</section> | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -41,7 +41,7 @@ func Home(title string, data CommonData) templ.Component { | ||||
| 				}() | ||||
| 			} | ||||
| 			ctx = templ.InitializeContext(ctx) | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h2>Welcome</h2><p>Welcome to webweav.ing, a collection of webmastery tools created by the <a href=\"https://32bit.cafe\">32-Bit Cafe</a>.</p><p>Note this service is in a pre-alpha state. Your account and data can disappear at any time. </p>") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section aria-labelledby=\"welcome-heading\"><h2 id=\"welcome-heading\">Welcome</h2><p>Welcome to webweav.ing, a collection of webmastery tools created by the <a href=\"https://32bit.cafe\" rel=\"noopener\">32-Bit Cafe</a>.</p><aside class=\"notice\" role=\"alert\" aria-labelledby=\"service-status\"><h3 id=\"service-status\" class=\"sr-only\">Service Status Notice</h3><p><strong>Important:</strong> This service is in a pre-alpha state. Your account and data can disappear at any time.</p></aside></section>") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
|  | ||||
| @ -86,20 +86,27 @@ templ UserSettingsView(data CommonData, timezones []string) { | ||||
| 	{{ user := data.CurrentUser }} | ||||
| 	@base("User Settings", data) { | ||||
| 		<div> | ||||
| 			<form hx-put="/users/settings"> | ||||
| 				<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> | ||||
| 				<label>Local Timezone</label> | ||||
| 				<select name="timezones" id="timezone-select"> | ||||
| 					for _, tz := range timezones { | ||||
| 						if tz == user.Settings.LocalTimezone.String() { | ||||
| 							<option value={ tz } selected="true">{ tz }</option> | ||||
| 						} else { | ||||
| 							<option value={ tz }>{ tz }</option> | ||||
| 						} | ||||
| 					} | ||||
| 				</select> | ||||
| 				<input type="submit" value="Submit"/> | ||||
| 			</form> | ||||
| 			<section aria-labelledby="user-settings-heading"> | ||||
| 				<form hx-put="/users/settings"> | ||||
| 					<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> | ||||
| 					<fieldset> | ||||
| 						<legend id="user-settings-heading">User Settings</legend> | ||||
| 						<div class="form-group"> | ||||
| 							<label>Local Timezone</label> | ||||
| 							<select name="timezones" id="timezone-select"> | ||||
| 								for _, tz := range timezones { | ||||
| 									if tz == user.Settings.LocalTimezone.String() { | ||||
| 										<option value={ tz } selected="true">{ tz }</option> | ||||
| 									} else { | ||||
| 										<option value={ tz }>{ tz }</option> | ||||
| 									} | ||||
| 								} | ||||
| 							</select> | ||||
| 						</div> | ||||
| 					</fieldset> | ||||
| 					<button type="submit">Save Settings</button> | ||||
| 				</form> | ||||
| 			</section> | ||||
| 		</div> | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -444,20 +444,20 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component { | ||||
| 				}() | ||||
| 			} | ||||
| 			ctx = templ.InitializeContext(ctx) | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div><form hx-put=\"/users/settings\"><input type=\"hidden\" name=\"csrf_token\" value=\"") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div><section aria-labelledby=\"user-settings-heading\"><form hx-put=\"/users/settings\"><input type=\"hidden\" name=\"csrf_token\" value=\"") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 			var templ_7745c5c3_Var22 string | ||||
| 			templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 90, Col: 65} | ||||
| 				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 91, Col: 66} | ||||
| 			} | ||||
| 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"> <label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"><fieldset><legend id=\"user-settings-heading\">User Settings</legend><div class=\"form-group\"><label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
| @ -470,7 +470,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component { | ||||
| 					var templ_7745c5c3_Var23 string | ||||
| 					templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 25} | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 99, Col: 28} | ||||
| 					} | ||||
| 					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| @ -483,7 +483,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component { | ||||
| 					var templ_7745c5c3_Var24 string | ||||
| 					templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 48} | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 99, Col: 51} | ||||
| 					} | ||||
| 					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| @ -501,7 +501,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component { | ||||
| 					var templ_7745c5c3_Var25 string | ||||
| 					templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 97, Col: 25} | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 101, Col: 28} | ||||
| 					} | ||||
| 					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| @ -514,7 +514,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component { | ||||
| 					var templ_7745c5c3_Var26 string | ||||
| 					templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 97, Col: 32} | ||||
| 						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 101, Col: 35} | ||||
| 					} | ||||
| 					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | ||||
| 					if templ_7745c5c3_Err != nil { | ||||
| @ -526,7 +526,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component { | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</select> <input type=\"submit\" value=\"Submit\"></form></div>") | ||||
| 			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</select></div></fieldset><button type=\"submit\">Save Settings</button></form></section></div>") | ||||
| 			if templ_7745c5c3_Err != nil { | ||||
| 				return templ_7745c5c3_Err | ||||
| 			} | ||||
|  | ||||
| @ -14,38 +14,47 @@ func wUrl(w models.Website) string { | ||||
| templ wSidebar(website models.Website) { | ||||
| 	{{ dashUrl := wUrl(website) + "/dashboard" }} | ||||
| 	{{ gbUrl := wUrl(website) + "/guestbook" }} | ||||
| 	<nav> | ||||
| 	<nav aria-label="Dashboard navigation"> | ||||
| 		<div> | ||||
| 			<ul> | ||||
| 				<li><a href={ templ.URL(dashUrl) }>Dashboard</a></li> | ||||
| 				<li><a href={ templ.URL(dashUrl + "/settings") }>Settings</a></li> | ||||
| 				<li><a href={ templ.URL(externalUrl(website.Url.String())) } target="_blank">View Website</a></li> | ||||
| 			</ul> | ||||
| 			<h3>Guestbook</h3> | ||||
| 			<ul> | ||||
| 				<li><a href={ templ.URL(gbUrl) } target="_blank">View Guestbook</a></li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
| 				<li><a href={ templ.URL(dashUrl + "/guestbook/comments") }>Manage messages</a></li> | ||||
| 				<li><a href={ templ.URL(dashUrl + "/guestbook/comments/queue") }>Review message queue</a></li> | ||||
| 				<li><a href={ templ.URL(dashUrl + "/guestbook/comments/trash") }>Trash</a></li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
| 				//<li><a href={ templ.URL(dashUrl + "/guestbook/themes") }>Themes</a></li> | ||||
| 				//<li><a href={ templ.URL(dashUrl + "/guestbook/customize") }>Custom CSS</a></li> | ||||
| 			</ul> | ||||
| 			<section aria-labelledby="main-nav-heading"> | ||||
| 				<h3 id="main-nav-heading">Website</h3> | ||||
| 				<ul role="list"> | ||||
| 					<li><a href={ templ.URL(dashUrl) } aria-current="page">Dashboard</a></li> | ||||
| 					<li><a href={ templ.URL(dashUrl + "/settings") }>Settings</a></li> | ||||
| 					<li><a href={ templ.URL(externalUrl(website.Url.String())) } target="_blank">View Website</a></li> | ||||
| 				</ul> | ||||
| 			</section> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<h3>Feeds</h3> | ||||
| 			<p>Coming Soon</p> | ||||
| 			<section aria-labelledby="guestbook-nav-heading"> | ||||
| 				<h3 id="guestbook-nav-heading">Guestbook</h3> | ||||
| 				<ul role="list"> | ||||
| 					<li><a href={ templ.URL(gbUrl) } target="_blank">View Guestbook</a></li> | ||||
| 					<li><a href={ templ.URL(dashUrl + "/guestbook/comments") }>Manage messages</a></li> | ||||
| 					<li><a href={ templ.URL(dashUrl + "/guestbook/comments/queue") }>Review message queue</a></li> | ||||
| 					<li><a href={ templ.URL(dashUrl + "/guestbook/comments/trash") }>Trash</a></li> | ||||
| 				</ul> | ||||
| 			</section> | ||||
| 			//<ul> | ||||
| 			//<li><a href={ templ.URL(dashUrl + "/guestbook/themes") }>Themes</a></li> | ||||
| 			//<li><a href={ templ.URL(dashUrl + "/guestbook/customize") }>Custom CSS</a></li> | ||||
| 			//</ul> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<h3>Account</h3> | ||||
| 			<ul> | ||||
| 				<li><a href="/users/settings">Settings</a></li> | ||||
| 				<li><a href="/users/privacy">Privacy</a></li> | ||||
| 				<li><a href="/help">Help</a></li> | ||||
| 			</ul> | ||||
| 			<section aria-labelledby="feeds-nav-heading"> | ||||
| 				<h3 id="feeds-nav-heading">Feeds</h3> | ||||
| 				<p>Coming Soon</p> | ||||
| 			</section> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<section aria-labelledby="account-nav-heading"> | ||||
| 				<h3 id="account-nav-heading">Account</h3> | ||||
| 				<ul role="list"> | ||||
| 					<li><a href="/users/settings">Settings</a></li> | ||||
| 					<li><a href="/users/privacy">Privacy</a></li> | ||||
| 					<li><a href="/help">Help</a></li> | ||||
| 				</ul> | ||||
| 			</section> | ||||
| 		</div> | ||||
| 	</nav> | ||||
| } | ||||
| @ -83,22 +92,48 @@ templ websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) { | ||||
| 
 | ||||
| templ WebsiteList(title string, data CommonData, websites []models.Website) { | ||||
| 	@base(title, data) { | ||||
| 		<div> | ||||
| 			<a href="/websites/create">Add Website</a> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			if len(websites) == 0 { | ||||
| 				<p>No Websites yet. <a href="">Register a website.</a></p> | ||||
| 			} else { | ||||
| 				<ul id="websites" hx-get="/websites" hx-trigger="newWebsite from:body" hx-swap="outerHTML"> | ||||
| 					for _, w := range websites { | ||||
| 						<li> | ||||
| 							<a href={ templ.URL(wUrl(w) + "/dashboard") }>{ w.Name }</a> | ||||
| 						</li> | ||||
| 					} | ||||
| 				</ul> | ||||
| 			} | ||||
| 		</div> | ||||
| 		<section aria-labelledby="websites-heading"> | ||||
| 			<header class="section-header"> | ||||
| 				<h1 id="websites-heading">My Websites</h1> | ||||
| 				<a href="/websites/create" class="btn btn-primary" role="button" aria-label="Create a new website">Add Website</a> | ||||
| 			</header> | ||||
| 			<div class="websites-container"> | ||||
| 				if len(websites) == 0 { | ||||
| 					<h2>No Websites yet.</h2> | ||||
| 					<p>Create your first website to get started with webweav.ing tools.</p> | ||||
| 					<a href="/websites/create" class="btn btn-primary">Create Your First Website</a> | ||||
| 				} else { | ||||
| 					<ul | ||||
| 						id="websites" | ||||
| 						role="list" | ||||
| 						aria-label="Your websites" | ||||
| 						hx-get="/websites" | ||||
| 						hx-trigger="newWebsite from:body" | ||||
| 						hx-swap="outerHTML" | ||||
| 					> | ||||
| 						for _, w := range websites { | ||||
| 							<li role="listitem"> | ||||
| 								<article class="website-card"> | ||||
| 									<header class="website-header"> | ||||
| 										<h2 class="website-name"> | ||||
| 											<a href={ templ.URL(wUrl(w) + "/dashboard") } aria-label={ fmt.Sprintf("Manage %s website dashboard", w.Name) }> | ||||
| 												{ w.Name } | ||||
| 											</a> | ||||
| 										</h2> | ||||
| 										<span class="website-url" aria-label="Website URL">{ w.Url.String() }</span> | ||||
| 									</header> | ||||
| 									<div class="website-actions"> | ||||
| 										<a href={ templ.URL(wUrl(w) + "/dashboard") } class="btn btn-outline" aria-label={ fmt.Sprintf("Open %s dashboard", w.Name) }>Dashboard</a> | ||||
| 										<a href={ templ.URL(wUrl(w) + "/guestbook") } target="_blank" rel="noopener" class="btn btn-outline" aria-label={ fmt.Sprintf("View %s website guestbook", w.Name) }>View Guestbook</a> | ||||
| 										<a href={ templ.URL(externalUrl(w.Url.String())) } target="_blank" rel="noopener" class="btn btn-outline" aria-label={ fmt.Sprintf("View %s website", w.Name) }>Visit Site</a> | ||||
| 									</div> | ||||
| 								</article> | ||||
| 							</li> | ||||
| 						} | ||||
| 					</ul> | ||||
| 				} | ||||
| 			</div> | ||||
| 		</section> | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -107,103 +142,142 @@ templ WebsiteDashboard(title string, data CommonData, website models.Website) { | ||||
| 		<div id="dashboard"> | ||||
| 			@wSidebar(website) | ||||
| 			<div> | ||||
| 				<h1>Embed your Guestbook</h1> | ||||
| 				<p> | ||||
| 					Upload <a href="/static/js/guestbook.js" download>this JavaScript WebComponent</a> to your site and include it in your <code>{ `<head>` }</code> tag. | ||||
| 				</p> | ||||
| 				<div> | ||||
| 					//<button>Copy to Clipboard</button> | ||||
| 					<pre> | ||||
| 						<code id="guestbookSnippet"> | ||||
| 							{  | ||||
| `<head> | ||||
|     <script type="module" src="js/guestbook.js"></script> | ||||
| </head>` } | ||||
| 						</code> | ||||
| 					</pre> | ||||
| 					<p> | ||||
| 						Then add the custom elements where you want your form and comments to show up | ||||
| 					</p> | ||||
| 					{{ gbUrl := fmt.Sprintf("https://%s/websites/%s/guestbook", data.RootUrl, shortIdToSlug(website.ShortId)) }} | ||||
| 					//<button>Copy to Clipboard</button> | ||||
| 					<pre> | ||||
| 						<code> | ||||
| 							{ fmt.Sprintf(`<guestbook-form guestbook="%s"></guestbook-form> | ||||
| 				{{ gbUrl := fmt.Sprintf("https://%s/websites/%s/guestbook", data.RootUrl, shortIdToSlug(website.ShortId)) }} | ||||
| 				<section aria-labelledby="embed-instructions"> | ||||
| 					<h1 id="embed-instructions">Embed your Guestbook</h1> | ||||
| 					<div class="instruction-overview"> | ||||
| 						<p>There are two ways to add your guestbook to your website: using our JavaScript component (recommended) or with an iframe.</p> | ||||
| 					</div> | ||||
| 					<article class="instruction-method"> | ||||
| 						<h2>Method 1: JavaScript Component (Recommended)</h2> | ||||
| 						<div class="instruction-step"> | ||||
| 							<h3>Step 1: Download and upload the component</h3> | ||||
| 							<p>Download <a href="/static/js/guestbook.js" download>this JavaScript WebComponent</a> and upload it to your website's JavaScript folder.</p> | ||||
| 						</div> | ||||
| 						<div class="instruction-step"> | ||||
| 							<h3>Step 2: Include in your HTML head</h3> | ||||
| 							<figure class="code-example"> | ||||
| 								<figcaption>Add this script tag to your <head> section:</figcaption> | ||||
| 								<pre> | ||||
| 									<code id="guestbookSnippet" aria-label="HTML head script tag"> | ||||
| 										<head> | ||||
| 										<script type="module" src="js/guestbook.js"></script> | ||||
| 										</head> | ||||
| 									</code> | ||||
| 								</pre> | ||||
| 							</figure> | ||||
| 						</div> | ||||
| 						<div class="instruction-step"> | ||||
| 							<h3>Step 3: Add the custom elements</h3> | ||||
| 							<figure class="code-example"> | ||||
| 								<figcaption>Place these elements where you want your guestbook to appear:</figcaption> | ||||
| 								<pre> | ||||
| 									<code aria-label="Custom guestbook elements"> | ||||
| 										{ fmt.Sprintf(`<guestbook-form guestbook="%s"></guestbook-form> | ||||
| <guestbook-comments guestbook="%s"></guestbook-comments>`, gbUrl, gbUrl) } | ||||
| 						</code> | ||||
| 					</pre> | ||||
| 				</div> | ||||
| 				<p> | ||||
| 					If your web host does not allow CORS requests, use an iframe instead | ||||
| 				</p> | ||||
| 				<div> | ||||
| 					<pre> | ||||
| 						<code> | ||||
| 							{ fmt.Sprintf(`<iframe src="%s" title="Guestbook"></iframe>`, gbUrl) } | ||||
| 						</code> | ||||
| 					</pre> | ||||
| 				</div> | ||||
| 									</code> | ||||
| 								</pre> | ||||
| 							</figure> | ||||
| 						</div> | ||||
| 					</article> | ||||
| 					<article class="instruction-method"> | ||||
| 						<h2>Method 2: iframe (Alternative)</h2> | ||||
| 						<details> | ||||
| 							<summary>Use this method if your web host doesn't allow CORS requests</summary> | ||||
| 							<div class="instruction-step"> | ||||
| 								<p>If your hosting provider blocks cross-origin requests, you can embed the guestbook using an iframe instead:</p> | ||||
| 								<figure class="code-example"> | ||||
| 									<figcaption>iframe embedding code:</figcaption> | ||||
| 									<pre> | ||||
| 										<code aria-label="iframe embedding code"> | ||||
| 											{ fmt.Sprintf(`<iframe src="%s"  | ||||
|     title="Guestbook" | ||||
|     width="100%%" | ||||
|     height="600" | ||||
|     style="border: 1px solid #ccc; border-radius: 8px;" | ||||
|     > | ||||
|         <p>Your browser does not support iframes. <a href="%s">View the guestbook directly.</a></p> | ||||
| </iframe>`, gbUrl, gbUrl) } | ||||
| 										</code> | ||||
| 									</pre> | ||||
| 								</figure> | ||||
| 								<aside role="note" class="method-note"> | ||||
| 									<p><strong>Note:</strong> The iframe method may have styling limitations and won't integrate as seamlessly with your site's design.</p> | ||||
| 								</aside> | ||||
| 							</div> | ||||
| 						</details> | ||||
| 					</article> | ||||
| 					<aside role="complementary" class="help-section"> | ||||
| 						<h2>Need Help?</h2> | ||||
| 						<p>If you're having trouble embedding your guestbook, check out our <a href="/help">help documentation</a> or contact support.</p> | ||||
| 					</aside> | ||||
| 				</section> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| templ websiteSettingsForm(data CommonData, website models.Website, form forms.WebsiteSettingsForm) { | ||||
| 	<h3>Website Settings</h3> | ||||
| 	<div> | ||||
| 	<legend id="website-settings-heading">Website Settings</legend> | ||||
| 	<div class="form-group"> | ||||
| 		{{ err, exists := form.FieldErrors["ws_name"] }} | ||||
| 		<label for="ws_name">Site Name: </label> | ||||
| 		<label for="ws_name">Site Name <span aria-label="required">*</span></label> | ||||
| 		if exists { | ||||
| 			<label class="error">{ err }</label> | ||||
| 		} | ||||
| 		if form.SiteName != "" { | ||||
| 			<input type="text" name="ws_name" id="sitename" value={ form.SiteName } required/> | ||||
| 			<input type="text" name="ws_name" id="sitename" value={ form.SiteName } required aria-describedby="sitename-help"/> | ||||
| 		} else { | ||||
| 			<input type="text" name="ws_name" id="sitename" value={ website.Name } required/> | ||||
| 			<input type="text" name="ws_name" id="sitename" value={ website.Name } required aria-describedby="sitename-help"/> | ||||
| 		} | ||||
| 		<small id="sitename-help">The display name for your website</small> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 	<div class="form-group"> | ||||
| 		{{ err, exists = form.FieldErrors["ws_url"] }} | ||||
| 		<label for="ws_url">Site URL: </label> | ||||
| 		<label for="ws_url">Site URL <span aria-label="required">*</span></label> | ||||
| 		if exists { | ||||
| 			<label class="error">{ err }</label> | ||||
| 		} | ||||
| 		if form.SiteUrl != "" { | ||||
| 			<input type="text" name="ws_url" id="ws_url" value={ form.SiteUrl } required/> | ||||
| 			<input type="url" name="ws_url" id="ws_url" value={ form.SiteUrl } required aria-describedby="siteurl-help"/> | ||||
| 		} else { | ||||
| 			<input type="text" name="ws_url" id="ws_url" value={ website.Url.String() } required/> | ||||
| 			<input type="url" name="ws_url" id="ws_url" value={ website.Url.String() } required aria-describedby="siteurl-help"/> | ||||
| 		} | ||||
| 		<small id="siteurl-help">The full URL where your website can be accessed</small> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		{{ err, exists = form.FieldErrors["ws_author"] }} | ||||
| 		<label for="ws_author">Site Author: </label> | ||||
| 		<label for="ws_author">Site Author <span aria-label="required">*</span></label> | ||||
| 		if exists { | ||||
| 			<label class="error">{ err }</label> | ||||
| 		} | ||||
| 		if form.AuthorName != "" { | ||||
| 			<input type="text" name="ws_author" id="authorname" value={ form.AuthorName } required/> | ||||
| 			<input type="text" name="ws_author" id="authorname" value={ form.AuthorName } required aria-describedby="authorname-help"/> | ||||
| 		} else { | ||||
| 			<input type="text" name="ws_author" id="authorname" value={ website.AuthorName } required/> | ||||
| 			<input type="text" name="ws_author" id="authorname" value={ website.AuthorName } required aria-describedby="authorname-help"/> | ||||
| 		} | ||||
| 		<small id="authorname-help">Your name or the website owner's name</small> | ||||
| 	</div> | ||||
| } | ||||
| 
 | ||||
| templ guestbookSettingsForm(data CommonData, website models.Website, gb models.Guestbook, form forms.WebsiteSettingsForm) { | ||||
| 	<h3>Guestbook Settings</h3> | ||||
| 	<div> | ||||
| 		<label>Guestbook Visibility</label> | ||||
| 		<label for="gb_visible_true"> | ||||
| 			<input type="radio" name="gb_visible" id="gb_visible_true" value="true" checked?={ gb.Settings.IsVisible }/> | ||||
| 			Public | ||||
| 		</label> | ||||
| 		<label for="gb_visible_false"> | ||||
| 			<input type="radio" name="gb_visible" id="gb_visible_false" value="false" checked?={ !gb.Settings.IsVisible }/> | ||||
| 			Private | ||||
| 		</label> | ||||
| 	<legend>Guestbook Settings</legend> | ||||
| 	<div class="form-group"> | ||||
| 		<fieldset class="radio-group"> | ||||
| 			<legend>Guestbook Visibility</legend> | ||||
| 			<label> | ||||
| 				<input type="radio" name="gb_visible" id="gb_visible_true" value="true" checked?={ gb.Settings.IsVisible }/> | ||||
| 				Public | ||||
| 			</label> | ||||
| 			<label> | ||||
| 				<input type="radio" name="gb_visible" id="gb_visible_false" value="false" checked?={ !gb.Settings.IsVisible }/> | ||||
| 				Private | ||||
| 			</label> | ||||
| 		</fieldset> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label>Guestbook Commenting</label> | ||||
| 		<select name="gb_commenting" id="gb-commenting"> | ||||
| 	<div class="form-group"> | ||||
| 		<label for="gb-commenting">Guestbook Commenting</label> | ||||
| 		<select name="gb_commenting" id="gb-commenting" aria-describedby="commenting-help"> | ||||
| 			<option value="true" selected?={ gb.Settings.IsCommentingEnabled }>Enabled</option> | ||||
| 			<option value="1h">Disabled for 1 Hour</option> | ||||
| 			<option value="4h">Disabled for 4 Hours</option> | ||||
| @ -217,17 +291,21 @@ templ guestbookSettingsForm(data CommonData, website models.Website, gb models.G | ||||
| 			{{ localtime := gb.Settings.ReenableCommenting.In(data.CurrentUser.Settings.LocalTimezone) }} | ||||
| 			<label>Commenting re-enabled on <time value={ localtime.Format(time.RFC3339) }>{ localtime.Format("2 January 2006") } at { localtime.Format("3:04PM MST") }</time></label> | ||||
| 		} | ||||
| 		<small id="commenting-help">Control when users can post new comments</small> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label>Enable Widgets</label> | ||||
| 		<label for="gb_remote_true"> | ||||
| 			<input type="radio" name="gb_remote" id="gb_remote_true" value="true" checked?={ gb.Settings.AllowRemoteHostAccess }/> | ||||
| 			Yes | ||||
| 		</label> | ||||
| 		<label for="gb_remote_false"> | ||||
| 			<input type="radio" name="gb_remote" id="gb_remote_false" value="false" checked?={ !gb.Settings.AllowRemoteHostAccess }/> | ||||
| 			No | ||||
| 		</label> | ||||
| 	<div class="form-group"> | ||||
| 		<fieldset class="radio-group"> | ||||
| 			<legend>Enable Widgets</legend> | ||||
| 			<label for="gb_remote_true"> | ||||
| 				<input type="radio" name="gb_remote" id="gb_remote_true" value="true" checked?={ gb.Settings.AllowRemoteHostAccess }/> | ||||
| 				Yes | ||||
| 			</label> | ||||
| 			<label for="gb_remote_false"> | ||||
| 				<input type="radio" name="gb_remote" id="gb_remote_false" value="false" checked?={ !gb.Settings.AllowRemoteHostAccess }/> | ||||
| 				No | ||||
| 			</label> | ||||
| 		</fieldset> | ||||
| 		<small>Allow embedding guestbook on external websites</small> | ||||
| 	</div> | ||||
| } | ||||
| 
 | ||||
| @ -239,27 +317,36 @@ templ SettingsForm(data CommonData, website models.Website, form forms.WebsiteSe | ||||
| 			{ msg } | ||||
| 		</p> | ||||
| 		<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> | ||||
| 		<div id="settings_form"> | ||||
| 		<fieldset> | ||||
| 			@websiteSettingsForm(data, website, form) | ||||
| 		</fieldset> | ||||
| 		<fieldset> | ||||
| 			@guestbookSettingsForm(data, website, gb, form) | ||||
| 		</div> | ||||
| 		<input type="submit" value="Submit"/> | ||||
| 		</fieldset> | ||||
| 		<button type="submit">Save Settings</button> | ||||
| 	</form> | ||||
| } | ||||
| 
 | ||||
| templ DeleteForm(data CommonData, website models.Website, form forms.WebsiteDeleteForm) { | ||||
| 	{{ putUrl := fmt.Sprintf("/websites/%s", shortIdToSlug(website.ShortId)) }} | ||||
| 	<form hx-put={ putUrl } hx-swap="outerHTML"> | ||||
| 	<form hx-put={ putUrl } hx-swap="outerHTML" aria-label="Delete website"> | ||||
| 		<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> | ||||
| 		<h3>Delete Website</h3> | ||||
| 		<p>Deleting a website is permanent. Be absolutely sure before proceeding.</p> | ||||
| 		{{ err, exists := form.FieldErrors["delete"] }} | ||||
| 		<label for="delete">Type your site name in the form.</label> | ||||
| 		if exists { | ||||
| 			<label class="error">{ err }</label> | ||||
| 		} | ||||
| 		<input type="text" name="delete" id="delete" required/> | ||||
| 		<input type="submit" value="Delete" class="danger"/> | ||||
| 		<fieldset class="danger-zone"> | ||||
| 			<legend id="danger-zone-heading">Delete Website</legend> | ||||
| 			<aside role="alert" class="warning-notice"> | ||||
| 				<p><strong>Warning:</strong> Deleting a website is permanent. Be absolutely sure before proceeding.</p> | ||||
| 			</aside> | ||||
| 			{{ err, exists := form.FieldErrors["delete"] }} | ||||
| 			<div class="form-group"> | ||||
| 				if exists { | ||||
| 					<label class="error">{ err }</label> | ||||
| 				} | ||||
| 				<label for="delete">Type your site name to confirm deletion</label> | ||||
| 				<input type="text" name="delete" id="delete" required aria-describedby="delete-help" placeholder={ website.Name }/> | ||||
| 				<small id="delete-help">Type { website.Name } exactly as shown to confirm deletion</small> | ||||
| 			</div> | ||||
| 			<button type="submit" class="danger">Delete Website</button> | ||||
| 		</fieldset> | ||||
| 	</form> | ||||
| } | ||||
| 
 | ||||
| @ -269,8 +356,12 @@ templ WebsiteDashboardSettings(data CommonData, website models.Website, form for | ||||
| 		<div id="dashboard"> | ||||
| 			@wSidebar(website) | ||||
| 			<div> | ||||
| 				@SettingsForm(data, website, form, "") | ||||
| 				@DeleteForm(data, website, forms.WebsiteDeleteForm{}) | ||||
| 				<section aria-labelledby="website-settings-heading"> | ||||
| 					@SettingsForm(data, website, form, "") | ||||
| 				</section> | ||||
| 				<section aria-labelledby="danger-zone-heading"> | ||||
| 					@DeleteForm(data, website, forms.WebsiteDeleteForm{}) | ||||
| 				</section> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	} | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user