UI Redesign #37

Merged
yequari merged 4 commits from redesign into dev 2025-08-17 03:16:53 +00:00
22 changed files with 2451 additions and 1145 deletions

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "ui/static/fontawesome"]
path = ui/static/fontawesome
url = https://github.com/FortAwesome/Font-Awesome.git
branch = fa-release-7.0.0

View File

@ -14,6 +14,10 @@ func (app *application) home(w http.ResponseWriter, r *http.Request) {
views.Home("Home", app.newCommonData(r)).Render(r.Context(), w) views.Home("Home", app.newCommonData(r)).Render(r.Context(), w)
} }
func (app *application) about(w http.ResponseWriter, r *http.Request) {
views.AboutPage("About Webweav.ing", app.newCommonData(r)).Render(r.Context(), w)
}
func (app *application) notImplemented(w http.ResponseWriter, r *http.Request) { func (app *application) notImplemented(w http.ResponseWriter, r *http.Request) {
views.ComingSoon("Coming Soon", app.newCommonData(r)).Render(r.Context(), w) views.ComingSoon("Coming Soon", app.newCommonData(r)).Render(r.Context(), w)
} }

View File

@ -36,7 +36,7 @@ func (app *application) getGuestbook(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
comments, err := app.guestbookComments.GetAll(website.Guestbook.ID) comments, err := app.guestbookComments.GetVisible(website.Guestbook.ID)
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
@ -79,7 +79,7 @@ func (app *application) getGuestbookCommentsSerialized(w http.ResponseWriter, r
if !website.Guestbook.Settings.IsVisible || !website.Guestbook.Settings.AllowRemoteHostAccess { if !website.Guestbook.Settings.IsVisible || !website.Guestbook.Settings.AllowRemoteHostAccess {
app.clientError(w, http.StatusForbidden) app.clientError(w, http.StatusForbidden)
} }
comments, err := app.guestbookComments.GetAllSerialized(website.Guestbook.ID) comments, err := app.guestbookComments.GetVisibleSerialized(website.Guestbook.ID)
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
@ -151,7 +151,7 @@ func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *htt
views.EmbeddableGuestbookCommentForm(data, website, form).Render(r.Context(), w) views.EmbeddableGuestbookCommentForm(data, website, form).Render(r.Context(), w)
} }
// TODO: use htmx to avoid getting comments again // TODO: use htmx to avoid getting comments again
comments, err := app.guestbookComments.GetAll(website.Guestbook.ID) comments, err := app.guestbookComments.GetVisible(website.Guestbook.ID)
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
@ -243,7 +243,7 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request)
return return
} }
comments, err := app.guestbookComments.GetUnpublished(website.Guestbook.ID) comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
if err != nil { if err != nil {
if errors.Is(err, models.ErrNoRecord) { if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r) http.NotFound(w, r)
@ -315,6 +315,8 @@ func (app *application) putHideGuestbookComment(w http.ResponseWriter, r *http.R
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
} }
data := app.newCommonData(r)
views.GuestbookDashboardUpdateButtonPart(data, website, comment).Render(r.Context(), w)
} }
func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) { func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
@ -349,6 +351,7 @@ func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Re
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
} }
views.GuestbookDashboardCommentDeletePart("Comment was successfully deleted").Render(r.Context(), w)
} }
func (app *application) getAllGuestbooks(w http.ResponseWriter, r *http.Request) { func (app *application) getAllGuestbooks(w http.ResponseWriter, r *http.Request) {

View File

@ -23,7 +23,7 @@ func (app *application) logRequest(next http.Handler) http.Handler {
func commonHeaders(next http.Handler) http.Handler { func commonHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com") w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com 'self'; style-src-elem 'self';")
w.Header().Set("Referrer-Policy", "origin-when-cross-origin") w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
// w.Header().Set("X-Frame-Options", "deny") // w.Header().Set("X-Frame-Options", "deny")

View File

@ -35,6 +35,7 @@ func (app *application) routes() http.Handler {
mux.Handle("/users/login/oidc", dynamic.ThenFunc(app.userLoginOIDC)) mux.Handle("/users/login/oidc", dynamic.ThenFunc(app.userLoginOIDC))
mux.Handle("/users/login/oidc/callback", dynamic.ThenFunc(app.userLoginOIDCCallback)) mux.Handle("/users/login/oidc/callback", dynamic.ThenFunc(app.userLoginOIDCCallback))
mux.Handle("GET /help", dynamic.ThenFunc(app.notImplemented)) mux.Handle("GET /help", dynamic.ThenFunc(app.notImplemented))
mux.Handle("GET /about", dynamic.ThenFunc(app.about))
protected := dynamic.Append(app.requireAuthentication) protected := dynamic.Append(app.requireAuthentication)
@ -43,7 +44,6 @@ func (app *application) routes() http.Handler {
mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout)) mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout))
mux.Handle("GET /users/settings", protected.ThenFunc(app.getUserSettings)) mux.Handle("GET /users/settings", protected.ThenFunc(app.getUserSettings))
mux.Handle("PUT /users/settings", protected.ThenFunc(app.putUserSettings)) mux.Handle("PUT /users/settings", protected.ThenFunc(app.putUserSettings))
mux.Handle("GET /users/privacy", protected.ThenFunc(app.notImplemented))
mux.Handle("GET /guestbooks", protected.ThenFunc(app.getAllGuestbooks)) mux.Handle("GET /guestbooks", protected.ThenFunc(app.getAllGuestbooks))
mux.Handle("GET /websites", protected.ThenFunc(app.getWebsiteList)) mux.Handle("GET /websites", protected.ThenFunc(app.getWebsiteList))
@ -51,13 +51,12 @@ func (app *application) routes() http.Handler {
mux.Handle("POST /websites/create", protected.ThenFunc(app.postWebsiteCreate)) mux.Handle("POST /websites/create", protected.ThenFunc(app.postWebsiteCreate))
mux.Handle("GET /websites/{id}/dashboard", protected.ThenFunc(app.getWebsiteDashboard)) mux.Handle("GET /websites/{id}/dashboard", protected.ThenFunc(app.getWebsiteDashboard))
mux.Handle("GET /websites/{id}/dashboard/guestbook/comments", protected.ThenFunc(app.getGuestbookComments)) mux.Handle("GET /websites/{id}/dashboard/guestbook/comments", protected.ThenFunc(app.getGuestbookComments))
mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/queue", protected.ThenFunc(app.getCommentQueue)) mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/hidden", protected.ThenFunc(app.getCommentQueue))
mux.Handle("DELETE /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.deleteGuestbookComment)) mux.Handle("DELETE /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.deleteGuestbookComment))
mux.Handle("PUT /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.putHideGuestbookComment)) mux.Handle("PUT /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.putHideGuestbookComment))
mux.Handle("GET /websites/{id}/dashboard/settings", protected.ThenFunc(app.getWebsiteSettings)) mux.Handle("GET /websites/{id}/dashboard/settings", protected.ThenFunc(app.getWebsiteSettings))
mux.Handle("PUT /websites/{id}/settings", protected.ThenFunc(app.putWebsiteSettings)) mux.Handle("PUT /websites/{id}/settings", protected.ThenFunc(app.putWebsiteSettings))
mux.Handle("PUT /websites/{id}", protected.ThenFunc(app.deleteWebsite)) mux.Handle("PUT /websites/{id}", protected.ThenFunc(app.deleteWebsite))
mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/trash", protected.ThenFunc(app.getCommentTrash))
mux.Handle("GET /websites/{id}/dashboard/guestbook/themes", protected.ThenFunc(app.getComingSoon)) mux.Handle("GET /websites/{id}/dashboard/guestbook/themes", protected.ThenFunc(app.getComingSoon))
mux.Handle("GET /websites/{id}/dashboard/guestbook/customize", protected.ThenFunc(app.getComingSoon)) mux.Handle("GET /websites/{id}/dashboard/guestbook/customize", protected.ThenFunc(app.getComingSoon))

8
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c
github.com/alexedwards/scs/v2 v2.8.0 github.com/alexedwards/scs/v2 v2.8.0
github.com/coreos/go-oidc/v3 v3.14.1 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/gorilla/schema v1.4.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/justinas/alice v1.2.0 github.com/justinas/alice v1.2.0
@ -16,4 +17,9 @@ require (
golang.org/x/oauth2 v0.30.0 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
View File

@ -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/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 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= 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 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= 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.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 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=

View File

@ -33,10 +33,10 @@ type GuestbookCommentModel struct {
type GuestbookCommentModelInterface interface { type GuestbookCommentModelInterface interface {
Insert(shortId uint64, guestbookId, parentId int64, authorName, authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error) Insert(shortId uint64, guestbookId, parentId int64, authorName, authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error)
Get(shortId uint64) (GuestbookComment, error) Get(shortId uint64) (GuestbookComment, error)
GetAll(guestbookId int64) ([]GuestbookComment, error) GetVisible(guestbookId int64) ([]GuestbookComment, error)
GetAllSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error) GetVisibleSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error)
GetDeleted(guestbookId int64) ([]GuestbookComment, error) GetDeleted(guestbookId int64) ([]GuestbookComment, error)
GetUnpublished(guestbookId int64) ([]GuestbookComment, error) GetAll(guestbookId int64) ([]GuestbookComment, error)
UpdateComment(comment *GuestbookComment) error UpdateComment(comment *GuestbookComment) error
} }
@ -74,7 +74,7 @@ func (m *GuestbookCommentModel) Get(shortId uint64) (GuestbookComment, error) {
return c, nil return c, nil
} }
func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, error) { func (m *GuestbookCommentModel) GetVisible(guestbookId int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite, stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished CommentText, PageUrl, Created, IsPublished
FROM guestbook_comments FROM guestbook_comments
@ -100,7 +100,7 @@ func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, e
return comments, nil return comments, nil
} }
func (m *GuestbookCommentModel) GetAllSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error) { func (m *GuestbookCommentModel) GetVisibleSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error) {
stmt := `SELECT AuthorName, CommentText, Created stmt := `SELECT AuthorName, CommentText, Created
FROM guestbook_comments FROM guestbook_comments
WHERE GuestbookId = ? AND IsPublished = TRUE AND DELETED IS NULL WHERE GuestbookId = ? AND IsPublished = TRUE AND DELETED IS NULL
@ -154,11 +154,11 @@ func (m *GuestbookCommentModel) GetDeleted(guestbookId int64) ([]GuestbookCommen
return comments, nil return comments, nil
} }
func (m *GuestbookCommentModel) GetUnpublished(guestbookId int64) ([]GuestbookComment, error) { func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite, stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished CommentText, PageUrl, Created, IsPublished
FROM guestbook_comments FROM guestbook_comments
WHERE GuestbookId = ? AND Deleted IS NULL AND IsPublished = FALSE WHERE GuestbookId = ? AND Deleted IS NULL
ORDER BY Created DESC` ORDER BY Created DESC`
rows, err := m.DB.Query(stmt, guestbookId) rows, err := m.DB.Query(stmt, guestbookId)
if err != nil { if err != nil {

View File

@ -40,7 +40,7 @@ func (m *GuestbookCommentModel) Get(shortId uint64) (models.GuestbookComment, er
} }
} }
func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]models.GuestbookComment, error) { func (m *GuestbookCommentModel) GetVisible(guestbookId int64) ([]models.GuestbookComment, error) {
switch guestbookId { switch guestbookId {
case 1: case 1:
return []models.GuestbookComment{mockGuestbookComment}, nil return []models.GuestbookComment{mockGuestbookComment}, nil
@ -51,7 +51,7 @@ func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]models.GuestbookCom
} }
} }
func (m *GuestbookCommentModel) GetAllSerialized(guestbookId int64) ([]models.GuestbookCommentSerialized, error) { func (m *GuestbookCommentModel) GetVisibleSerialized(guestbookId int64) ([]models.GuestbookCommentSerialized, error) {
switch guestbookId { switch guestbookId {
case 1: case 1:
return []models.GuestbookCommentSerialized{mockSerializedGuestbookComment}, nil return []models.GuestbookCommentSerialized{mockSerializedGuestbookComment}, nil
@ -69,7 +69,7 @@ func (m *GuestbookCommentModel) GetDeleted(guestbookId int64) ([]models.Guestboo
} }
} }
func (m *GuestbookCommentModel) GetUnpublished(guestbookId int64) ([]models.GuestbookComment, error) { func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]models.GuestbookComment, error) {
switch guestbookId { switch guestbookId {
default: default:
return []models.GuestbookComment{}, models.ErrNoRecord return []models.GuestbookComment{}, models.ErrNoRecord

View File

@ -12,7 +12,6 @@ type Website struct {
ID int64 ID int64
ShortId uint64 ShortId uint64
Name string Name string
// SiteUrl string
Url *url.URL Url *url.URL
AuthorName string AuthorName string
UserId int64 UserId int64
@ -317,15 +316,29 @@ func (m *WebsiteModel) Update(w Website) error {
func (m *WebsiteModel) Delete(websiteId int64) error { func (m *WebsiteModel) Delete(websiteId int64) error {
stmt := `UPDATE websites SET Deleted = ? WHERE ID = ?` stmt := `UPDATE websites SET Deleted = ? WHERE ID = ?`
r, err := m.DB.Exec(stmt, time.Now().UTC(), websiteId) tx, err := m.DB.Begin()
if err != nil { if err != nil {
return nil
}
t := time.Now().UTC()
_, err = tx.Exec(stmt, t, websiteId)
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return rbErr
}
return err return err
} }
if rows, err := r.RowsAffected(); rows != 1 { stmt = `UPDATE guestbooks SET Deleted = ? WHERE WebsiteId = ?`
_, err = tx.Exec(stmt, t, websiteId)
if err != nil { if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return rbErr
}
return err return err
} }
return errors.New("Failed to update website") err = tx.Commit()
if err != nil {
return err
} }
return nil return nil
} }

View File

@ -1,86 +1,886 @@
/* html { /* CSS Reset and Base Styles */
background: lightgray; *,
} */ *::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 { body {
max-width: 1024px; font-size: var(--text-base);
margin: 1rem auto; color: var(--color-text);
padding: 1rem; background-color: var(--color-background);
/* background: white; */ min-height: 100vh;
font-size: 1.2rem;
line-height: 1.5;
font-family: Arial, Helvetica, sans-serif;
}
header {
text-align: center;
}
body > nav {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
} }
body > nav ul { h1, h2, h3, h4, h5, h6 {
list-style: none; font-weight: 600;
margin: 0 1rem; line-height: 1.25;
padding: 0; margin-bottom: var(--space-md);
} }
body > nav li { h1 { font-size: var(--text-3xl); }
display: inline-block; h2 { font-size: var(--text-2xl); }
padding: 0 0.5rem; h3 { font-size: var(--text-xl); }
} h4 { font-size: var(--text-lg); }
nav form { p {
display: inline-block; margin-bottom: var(--space-md);
}
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;
} }
a { 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;
}
.footer-links {
padding: 0;
list-style: none;
}
.footer-links li {
display: inline-block;
padding: 0 1rem;
}
/* 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}
.htmx-request .htmx-indicator{opacity:1; transition: opacity 200ms ease-in;}
.htmx-request.htmx-indicator{opacity:1; transition: opacity 200ms ease-in;}
/* 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;
}
}

1
ui/static/fontawesome Submodule

@ -0,0 +1 @@
Subproject commit 5a85d8a93237e08d9d1f861aa5630f292424cfc0

View File

@ -41,31 +41,37 @@ templ commonHeader() {
templ topNav(data CommonData) { templ topNav(data CommonData) {
{{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }} {{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
<nav> <nav aria-label="Site navigation">
<div> <div class="nav-welcome">
<span>
if data.IsAuthenticated { if data.IsAuthenticated {
Welcome, { data.CurrentUser.Username } Welcome, { data.CurrentUser.Username }
} }
</span>
</div> </div>
<div> <ul class="nav-links">
if data.IsAuthenticated { if data.IsAuthenticated {
<a href="/guestbooks">All Guestbooks</a> | <li><a href="/guestbooks">All Guestbooks</a></li>
<a href="/websites">My Websites</a> | <li><a href="/websites">My Websites</a></li>
<a href="/users/settings">Settings</a> | <li><a href="/users/settings">Settings</a></li>
<a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a> <li><a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a></li>
} else { } else {
if data.LocalAuthEnabled { 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> </nav>
} }
templ commonFooter() { templ commonFooter() {
<footer> <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>
<ul class="footer-links">
<li><a href="/about">About</a></li>
<li><a href="/help">Help</a></li>
</ul>
</footer> </footer>
} }
@ -76,18 +82,19 @@ templ base(title string, data CommonData) {
<title>{ title } - webweav.ing</title> <title>{ title } - webweav.ing</title>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<link href="/static/css/classless.min.css" rel="stylesheet"/> <meta name="htmx-config" content={ `{"includeIndicatorStyles":false}` }/>
<link href="/static/css/style.css" rel="stylesheet"/> <link href="/static/css/style.css" rel="stylesheet"/>
<link href="/static/fontawesome/css/fontawesome.css" rel="stylesheet"/>
<link href="/static/fontawesome/css/solid.css" rel="stylesheet"/>
<script src="/static/js/htmx.min.js"></script> <script src="/static/js/htmx.min.js"></script>
</head> </head>
<body> <body>
@commonHeader() @commonHeader()
@topNav(data) @topNav(data)
<main> <main role="main">
if data.Flash != "" { if data.Flash != "" {
<div class="flash">{ data.Flash }</div> <div class="notice flash">{ data.Flash }</div>
} }
<h1>{ title }</h1>
{ children... } { children... }
</main> </main>
@commonFooter() @commonFooter()

View File

@ -92,7 +92,7 @@ func topNav(data CommonData) templ.Component {
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -104,48 +104,48 @@ func topNav(data CommonData) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if data.IsAuthenticated { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
if data.LocalAuthEnabled { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -174,7 +174,7 @@ func commonFooter() templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent templ_7745c5c3_Var5 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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><ul class=\"footer-links\"><li><a href=\"/about\">About</a></li><li><a href=\"/help\">Help</a></li></ul></footer>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -210,13 +210,26 @@ func base(title string, data CommonData) templ.Component {
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { 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: 82, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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\"><meta name=\"htmx-config\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(`{"includeIndicatorStyles":false}`)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 85, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><link href=\"/static/fontawesome/css/fontawesome.css\" rel=\"stylesheet\"><link href=\"/static/fontawesome/css/solid.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -228,51 +241,34 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<main>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<main role=\"main\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if data.Flash != "" { if data.Flash != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"flash\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<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}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h1>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 90, Col: 15} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 96, Col: 43}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h1>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
}
templ_7745c5c3_Err = templ_7745c5c3_Var6.Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templ_7745c5c3_Var6.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</main>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -280,7 +276,7 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -10,91 +10,140 @@ templ GuestbookDashboardCommentsView(title string, data CommonData, website mode
<div id="dashboard"> <div id="dashboard">
@wSidebar(website) @wSidebar(website)
<div> <div>
<h1>Comments on { website.Name }</h1> <section aria-labelledby="comments-management-heading">
<hr/> <header class="section-header">
<h1 id="comments-management-heading">Comments on { website.Name }</h1>
<p class="section-description">Manage, moderate, and organize comments on your guestbook</p>
</header>
<hr role="separator"/>
if len(comments) == 0 { if len(comments) == 0 {
<p>No comments yet!</p> <p>No comments yet!</p>
} }
for _, c := range comments { for i, c := range comments {
@GuestbookDashboardCommentView(data, website, c) @GuestbookDashboardCommentView(data, website, c, i)
} }
</section>
</div> </div>
</div> </div>
} }
} }
templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models.GuestbookComment) { templ GuestbookDashboardCommentDeletePart(text string) {
{{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(w), shortIdToSlug(c.ShortId)) }} <div class="comment-update-msg">
<p>{ text }</p>
</div>
}
templ GuestbookDashboardUpdateButtonPart(data CommonData, w models.Website, c models.GuestbookComment) {
{{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }} {{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
<div class="comment"> {{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(w), shortIdToSlug(c.ShortId)) }}
<div> <button
if c.Deleted.IsZero() { type="button"
<button class="danger" hx-delete={ commentUrl } hx-target="closest div.comment" hx-headers={ hxHeaders }>Delete</button> class="outline"
<button class="outline" hx-put={ commentUrl } hx-target="closest div.comment" hx-headers={ hxHeaders }> hx-put={ commentUrl }
hx-headers={ hxHeaders }
hx-swap="outerHTML"
>
if !c.IsPublished { if !c.IsPublished {
Publish Publish
} else { } else {
Hide Hide
} }
</button> </button>
} }
</div>
<div> templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models.GuestbookComment, i int) {
<strong>{ c.AuthorName }</strong> {{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(w), shortIdToSlug(c.ShortId)) }}
{{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
{{ authorClass := fmt.Sprintf("comment-author-%d", i) }}
<article class="comment" role="article" aria-labelledby={ authorClass }>
<div class="comment-update-msg"></div>
<header class="comment-header">
<div class="comment-meta">
<h3 id={ authorClass } class="comment-author">{ c.AuthorName }</h3>
<time datetime={ c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM") }>{ c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM") }</time>
if len(c.AuthorEmail) > 0 { if len(c.AuthorEmail) > 0 {
<div class="comment-contact">
{{ email := "mailto:" + c.AuthorEmail }} {{ email := "mailto:" + c.AuthorEmail }}
| <a href={ templ.URL(email) } target="_blank">{ c.AuthorEmail }</a> <i class="fa-solid fa-envelope"></i>
<a href={ templ.URL(email) } target="_blank">{ c.AuthorEmail }</a>
</div>
} }
if len(c.AuthorSite) > 0 { if len(c.AuthorSite) > 0 {
| <a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorSite }</a> <div class="comment-contact">
} <i class="fa-solid fa-house"></i>
<p> <a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorSite }</a>
{ c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM") }
</p>
</div> </div>
}
</div>
<div class="comment-actions" role="group" aria-label={ fmt.Sprintf("Actions for comment by %s", c.AuthorName) }>
if c.Deleted.IsZero() {
<button
type="button"
class="danger"
hx-delete={ commentUrl }
hx-target="closest article.comment"
hx-confirm="Are you sure you want to delete this comment? This action cannot be undone."
hx-headers={ hxHeaders }
>
Delete
</button>
@GuestbookDashboardUpdateButtonPart(data, w, c)
}
</div>
</header>
<div class="comment-content">
<p> <p>
{ c.CommentText } { c.CommentText }
</p> </p>
<hr/>
</div> </div>
<hr role="separator"/>
</article>
} }
templ commentForm(form forms.CommentCreateForm) { templ commentForm(form forms.CommentCreateForm) {
<div> <fieldset>
<label for="authorname">Name</label> <legend>Leave a comment</legend>
<div class="form-group">
<label for="authorname">Name <span aria-label="required">*</span></label>
{{ error, exists := form.FieldErrors["authorName"] }} {{ error, exists := form.FieldErrors["authorName"] }}
if exists { if exists {
<label class="error">{ error }</label> <label class="error">{ error }</label>
} }
<input type="text" name="authorname" id="authorname"/> <input type="text" name="authorname" id="authorname" required aria-describedby="authorname-help"/>
<small id="authorname-help">Your name or handle</small>
</div> </div>
<div> <div class="form-group">
<label for="authoremail">Email (Optional) </label> <label for="authoremail">Email (Optional)</label>
{{ error, exists = form.FieldErrors["authorEmail"] }} {{ error, exists = form.FieldErrors["authorEmail"] }}
if exists { if exists {
<label class="error">{ error }</label> <label class="error">{ error }</label>
} }
<input type="text" name="authoremail" id="authoremail"/> <input type="email" name="authoremail" id="authoremail" aria-describedby="authoremail-help"/>
<small id="authoremail-help">Your email address will only be shared with the guestbook's owner</small>
</div> </div>
<div> <div class="form-group">
<label for="authorsite">Site Url (Optional) </label> <label for="authorsite">Website URL (Optional)</label>
{{ error, exists = form.FieldErrors["authorSite"] }} {{ error, exists = form.FieldErrors["authorSite"] }}
if exists { if exists {
<label class="error">{ error }</label> <label class="error">{ error }</label>
} }
<input type="text" name="authorsite" id="authorsite"/> <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>
<div> <div class="form-group">
<label for="content">Comment</label> <label for="content">Comment <span aria-label="required">*</span></label>
{{ error, exists = form.FieldErrors["content"] }} {{ error, exists = form.FieldErrors["content"] }}
if exists { if exists {
<label class="error">{ error }</label> <label class="error">{ error }</label>
} }
<textarea name="content" id="content"></textarea> <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>
<div> <div class="form-group">
<input type="submit" value="Submit"/> <button type="submit">Submit Comment</button>
</div> </div>
</fieldset>
} }
templ GuestbookView(title string, data CommonData, website models.Website, guestbook models.Guestbook, comments []models.GuestbookComment, form forms.CommentCreateForm) { templ GuestbookView(title string, data CommonData, website models.Website, guestbook models.Guestbook, comments []models.GuestbookComment, form forms.CommentCreateForm) {
@ -105,26 +154,30 @@ templ GuestbookView(title string, data CommonData, website models.Website, guest
<html> <html>
<head> <head>
<title>{ title }</title> <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> <script src="/static/js/main.js" defer></script>
</head> </head>
<body> <body>
<main> <main role="main">
<div> <section aria-labelledby="guestbook-heading">
<h1>{ website.Name } Guestbook</h1> <h1>{ website.Name } Guestbook</h1>
{ data.Flash } { data.Flash }
<form action={ templ.URL(postUrl) } method="post"> <form action={ templ.URL(postUrl) } method="post">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> <input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
@commentForm(form) @commentForm(form)
</form> </form>
</div> </section>
<div id="comments"> <section id="comments" aria-labelledby="comments-heading">
<h2 id="comments-heading">Comments</h2>
if len(comments) == 0 { if len(comments) == 0 {
<p>No comments yet!</p> <p>No comments yet!</p>
} }
for _, c := range comments { for i, c := range comments {
<div> {{ commentAuthorRole := fmt.Sprintf("comment-author-%d", i+1) }}
<h3> <article class="comment" role="article" aria-labelledby={ commentAuthorRole }>
<header class="comment-header">
<h3 id={ commentAuthorRole } class="comment-author">
if c.AuthorSite != "" { if c.AuthorSite != "" {
<a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorName }</a> <a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorName }</a>
} else { } else {
@ -132,12 +185,15 @@ templ GuestbookView(title string, data CommonData, website models.Website, guest
} }
</h3> </h3>
<time datetime={ c.Created.Format(time.RFC3339) }>{ c.Created.Format("01-02-2006 03:04PM") }</time> <time datetime={ c.Created.Format(time.RFC3339) }>{ c.Created.Format("01-02-2006 03:04PM") }</time>
</header>
<div class="comment-content">
<p> <p>
{ c.CommentText } { c.CommentText }
</p> </p>
</div> </div>
</article>
} }
</div> </section>
</main> </main>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,14 @@ package views
templ Home(title string, data CommonData) { templ Home(title string, data CommonData) {
@base(title, data) { @base(title, data) {
<h2>Welcome</h2> <section aria-labelledby="welcome-heading">
<p> <h2 id="welcome-heading">Welcome</h2>
Welcome to webweav.ing, a collection of webmastery tools created by the <a href="https://32bit.cafe">32-Bit Cafe</a>. <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>
</p> <aside class="notice" role="alert" aria-labelledby="service-status">
<p> <h3 id="service-status" class="sr-only">Service Status Notice</h3>
Note this service is in a pre-alpha state. Your account and data can disappear at any time. <p><strong>Important:</strong> This service is in a pre-alpha state. Your account and data can disappear at any time.</p>
</p> </aside>
</section>
} }
} }
@ -17,3 +18,11 @@ templ ComingSoon(title string, data CommonData) {
<h2>Coming Soon</h2> <h2>Coming Soon</h2>
} }
} }
templ AboutPage(title string, data CommonData) {
@base(title, data) {
<section aria-labelledby="about-heading">
<h2 id="about-heading">About Webweav.ing</h2>
</section>
}
}

View File

@ -41,7 +41,7 @@ func Home(title string, data CommonData) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -102,4 +102,51 @@ func ComingSoon(title string, data CommonData) templ.Component {
}) })
} }
func AboutPage(title string, data CommonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<section aria-labelledby=\"about-heading\"><h2 id=\"about-heading\">About Webweav.ing</h2></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate var _ = templruntime.GeneratedTemplate

View File

@ -86,8 +86,12 @@ templ UserSettingsView(data CommonData, timezones []string) {
{{ user := data.CurrentUser }} {{ user := data.CurrentUser }}
@base("User Settings", data) { @base("User Settings", data) {
<div> <div>
<section aria-labelledby="user-settings-heading">
<form hx-put="/users/settings"> <form hx-put="/users/settings">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> <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> <label>Local Timezone</label>
<select name="timezones" id="timezone-select"> <select name="timezones" id="timezone-select">
for _, tz := range timezones { for _, tz := range timezones {
@ -98,8 +102,11 @@ templ UserSettingsView(data CommonData, timezones []string) {
} }
} }
</select> </select>
<input type="submit" value="Submit"/> </div>
</fieldset>
<button type="submit">Save Settings</button>
</form> </form>
</section>
</div> </div>
} }
} }

View File

@ -444,20 +444,20 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -470,7 +470,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -483,7 +483,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -501,7 +501,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -514,7 +514,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -14,91 +14,126 @@ func wUrl(w models.Website) string {
templ wSidebar(website models.Website) { templ wSidebar(website models.Website) {
{{ dashUrl := wUrl(website) + "/dashboard" }} {{ dashUrl := wUrl(website) + "/dashboard" }}
{{ gbUrl := wUrl(website) + "/guestbook" }} {{ gbUrl := wUrl(website) + "/guestbook" }}
<nav> <nav aria-label="Dashboard navigation">
<div> <div>
<ul> <section aria-labelledby="main-nav-heading">
<li><a href={ templ.URL(dashUrl) }>Dashboard</a></li> <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(dashUrl + "/settings") }>Settings</a></li>
<li><a href={ templ.URL(externalUrl(website.Url.String())) } target="_blank">View Website</a></li> <li><a href={ templ.URL(externalUrl(website.Url.String())) } target="_blank">View Website</a></li>
</ul> </ul>
<h3>Guestbook</h3> </section>
<ul> </div>
<div>
<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(gbUrl) } target="_blank">View Guestbook</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments") }>Manage Comments</a></li>
</ul> </ul>
<ul> </section>
<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>
</div> </div>
<div> <div>
<h3>Feeds</h3> <section aria-labelledby="feeds-nav-heading">
<h3 id="feeds-nav-heading">Feeds</h3>
<p>Coming Soon</p> <p>Coming Soon</p>
</section>
</div> </div>
<div> <div>
<h3>Account</h3> <section aria-labelledby="account-nav-heading">
<ul> <h3 id="account-nav-heading">Account</h3>
<ul role="list">
<li><a href="/users/settings">Settings</a></li> <li><a href="/users/settings">Settings</a></li>
<li><a href="/users/privacy">Privacy</a></li> <li><a href="/users/privacy">Privacy</a></li>
<li><a href="/help">Help</a></li> <li><a href="/help">Help</a></li>
</ul> </ul>
</section>
</div> </div>
</nav> </nav>
} }
templ websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) { templ websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) {
<input type="hidden" name="csrf_token" value={ csrfToken }/> <input type="hidden" name="csrf_token" value={ csrfToken }/>
<div> <fieldset>
{{ err, exists := form.FieldErrors["sitename"] }} <legend id="website-create-heading">Website Settings</legend>
<label for="sitename">Site Name: </label> <div class="form-group">
{{ err, exists := form.FieldErrors["ws_name"] }}
<label for="ws_name">Site Name <span aria-label="required">*</span></label>
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
<input type="text" name="sitename" id="sitename" value={ form.Name } required/> <input type="text" name="ws_name" id="sitename" value={ form.Name } required aria-describedby="sitename-help"/>
<small id="sitename-help">The display name for your website</small>
</div> </div>
<div> <div class="form-group">
{{ err, exists = form.FieldErrors["siteurl"] }} {{ err, exists = form.FieldErrors["ws_url"] }}
<label for="siteurl">Site URL: </label> <label for="ws_url">Site URL <span aria-label="required">*</span></label>
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
<input type="text" name="siteurl" id="siteurl" value={ form.SiteUrl } required/> <input type="url" name="ws_url" id="ws_url" value={ form.SiteUrl } required aria-describedby="siteurl-help"/>
<small id="siteurl-help">The full URL where your website can be accessed</small>
</div> </div>
<div> <div>
{{ err, exists = form.FieldErrors["authorname"] }} {{ err, exists = form.FieldErrors["ws_author"] }}
<label for="authorname">Site Author: </label> <label for="ws_author">Site Author <span aria-label="required">*</span></label>
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
<input type="text" name="authorname" id="authorname" value={ form.AuthorName } required/> <input type="text" name="ws_author" id="authorname" value={ form.AuthorName } required aria-describedby="authorname-help"/>
<small id="authorname-help">Your name or the website owner's name</small>
</div> </div>
</fieldset>
<div> <div>
<button type="submit">Submit</button> <button type="submit">Add Website</button>
</div> </div>
} }
templ WebsiteList(title string, data CommonData, websites []models.Website) { templ WebsiteList(title string, data CommonData, websites []models.Website) {
@base(title, data) { @base(title, data) {
<div> <section aria-labelledby="websites-heading">
<a href="/websites/create">Add Website</a> <header class="section-header">
</div> <h1 id="websites-heading">My Websites</h1>
<div> <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 { if len(websites) == 0 {
<p>No Websites yet. <a href="">Register a website.</a></p> <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 { } else {
<ul id="websites" hx-get="/websites" hx-trigger="newWebsite from:body" hx-swap="outerHTML"> <ul
id="websites"
role="list"
aria-label="Your websites"
hx-get="/websites"
hx-trigger="newWebsite from:body"
hx-swap="outerHTML"
>
for _, w := range websites { for _, w := range websites {
<li> <li role="listitem">
<a href={ templ.URL(wUrl(w) + "/dashboard") }>{ w.Name }</a> <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> </li>
} }
</ul> </ul>
} }
</div> </div>
</section>
} }
} }
@ -107,103 +142,142 @@ templ WebsiteDashboard(title string, data CommonData, website models.Website) {
<div id="dashboard"> <div id="dashboard">
@wSidebar(website) @wSidebar(website)
<div> <div>
<h1>Embed your Guestbook</h1> {{ gbUrl := fmt.Sprintf("https://%s/websites/%s/guestbook", data.RootUrl, shortIdToSlug(website.ShortId)) }}
<p> <section aria-labelledby="embed-instructions">
Upload <a href="/static/js/guestbook.js" download>this JavaScript WebComponent</a> to your site and include it in your <code>{ `<head>` }</code> tag. <h1 id="embed-instructions">Embed your Guestbook</h1>
</p> <div class="instruction-overview">
<div> <p>There are two ways to add your guestbook to your website: using our JavaScript component (recommended) or with an iframe.</p>
//<button>Copy to Clipboard</button> </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 &lt;head&gt; section:</figcaption>
<pre> <pre>
<code id="guestbookSnippet"> <code id="guestbookSnippet" aria-label="HTML head script tag">
{ &lt;head&gt;
`<head> &lt;script type=&#34;module&#34; src=&#34;js/guestbook.js&#34;&gt;&lt;/script&gt;
<script type="module" src="js/guestbook.js"></script> &lt;/head&gt;
</head>` }
</code> </code>
</pre> </pre>
<p> </figure>
Then add the custom elements where you want your form and comments to show up </div>
</p> <div class="instruction-step">
{{ gbUrl := fmt.Sprintf("https://%s/websites/%s/guestbook", data.RootUrl, shortIdToSlug(website.ShortId)) }} <h3>Step 3: Add the custom elements</h3>
//<button>Copy to Clipboard</button> <figure class="code-example">
<figcaption>Place these elements where you want your guestbook to appear:</figcaption>
<pre> <pre>
<code> <code aria-label="Custom guestbook elements">
{ fmt.Sprintf(`<guestbook-form guestbook="%s"></guestbook-form> { fmt.Sprintf(`<guestbook-form guestbook="%s"></guestbook-form>
<guestbook-comments guestbook="%s"></guestbook-comments>`, gbUrl, gbUrl) } <guestbook-comments guestbook="%s"></guestbook-comments>`, gbUrl, gbUrl) }
</code> </code>
</pre> </pre>
</figure>
</div> </div>
<p> </article>
If your web host does not allow CORS requests, use an iframe instead <article class="instruction-method">
</p> <h2>Method 2: iframe (Alternative)</h2>
<div> <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> <pre>
<code> <code aria-label="iframe embedding code">
{ fmt.Sprintf(`<iframe src="%s" title="Guestbook"></iframe>`, gbUrl) } { 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> </code>
</pre> </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> </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>
</div> </div>
} }
} }
templ websiteSettingsForm(data CommonData, website models.Website, form forms.WebsiteSettingsForm) { templ websiteSettingsForm(data CommonData, website models.Website, form forms.WebsiteSettingsForm) {
<h3>Website Settings</h3> <legend id="website-settings-heading">Website Settings</legend>
<div> <div class="form-group">
{{ err, exists := form.FieldErrors["ws_name"] }} {{ 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 { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
if form.SiteName != "" { 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 { } 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> <div class="form-group">
{{ err, exists = form.FieldErrors["ws_url"] }} {{ 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 { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
if form.SiteUrl != "" { 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 { } 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>
<div> <div>
{{ err, exists = form.FieldErrors["ws_author"] }} {{ 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 { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
if form.AuthorName != "" { 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 { } 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> </div>
} }
templ guestbookSettingsForm(data CommonData, website models.Website, gb models.Guestbook, form forms.WebsiteSettingsForm) { templ guestbookSettingsForm(data CommonData, website models.Website, gb models.Guestbook, form forms.WebsiteSettingsForm) {
<h3>Guestbook Settings</h3> <legend>Guestbook Settings</legend>
<div> <div class="form-group">
<label>Guestbook Visibility</label> <fieldset class="radio-group">
<label for="gb_visible_true"> <legend>Guestbook Visibility</legend>
<label>
<input type="radio" name="gb_visible" id="gb_visible_true" value="true" checked?={ gb.Settings.IsVisible }/> <input type="radio" name="gb_visible" id="gb_visible_true" value="true" checked?={ gb.Settings.IsVisible }/>
Public Public
</label> </label>
<label for="gb_visible_false"> <label>
<input type="radio" name="gb_visible" id="gb_visible_false" value="false" checked?={ !gb.Settings.IsVisible }/> <input type="radio" name="gb_visible" id="gb_visible_false" value="false" checked?={ !gb.Settings.IsVisible }/>
Private Private
</label> </label>
</fieldset>
</div> </div>
<div> <div class="form-group">
<label>Guestbook Commenting</label> <label for="gb-commenting">Guestbook Commenting</label>
<select name="gb_commenting" id="gb-commenting"> <select name="gb_commenting" id="gb-commenting" aria-describedby="commenting-help">
<option value="true" selected?={ gb.Settings.IsCommentingEnabled }>Enabled</option> <option value="true" selected?={ gb.Settings.IsCommentingEnabled }>Enabled</option>
<option value="1h">Disabled for 1 Hour</option> <option value="1h">Disabled for 1 Hour</option>
<option value="4h">Disabled for 4 Hours</option> <option value="4h">Disabled for 4 Hours</option>
@ -217,9 +291,11 @@ templ guestbookSettingsForm(data CommonData, website models.Website, gb models.G
{{ localtime := gb.Settings.ReenableCommenting.In(data.CurrentUser.Settings.LocalTimezone) }} {{ 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> <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>
<div> <div class="form-group">
<label>Enable Widgets</label> <fieldset class="radio-group">
<legend>Enable Widgets</legend>
<label for="gb_remote_true"> <label for="gb_remote_true">
<input type="radio" name="gb_remote" id="gb_remote_true" value="true" checked?={ gb.Settings.AllowRemoteHostAccess }/> <input type="radio" name="gb_remote" id="gb_remote_true" value="true" checked?={ gb.Settings.AllowRemoteHostAccess }/>
Yes Yes
@ -228,6 +304,8 @@ templ guestbookSettingsForm(data CommonData, website models.Website, gb models.G
<input type="radio" name="gb_remote" id="gb_remote_false" value="false" checked?={ !gb.Settings.AllowRemoteHostAccess }/> <input type="radio" name="gb_remote" id="gb_remote_false" value="false" checked?={ !gb.Settings.AllowRemoteHostAccess }/>
No No
</label> </label>
</fieldset>
<small>Allow embedding guestbook on external websites</small>
</div> </div>
} }
@ -239,27 +317,36 @@ templ SettingsForm(data CommonData, website models.Website, form forms.WebsiteSe
{ msg } { msg }
</p> </p>
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> <input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<div id="settings_form"> <fieldset>
@websiteSettingsForm(data, website, form) @websiteSettingsForm(data, website, form)
</fieldset>
<fieldset>
@guestbookSettingsForm(data, website, gb, form) @guestbookSettingsForm(data, website, gb, form)
</div> </fieldset>
<input type="submit" value="Submit"/> <button type="submit">Save Settings</button>
</form> </form>
} }
templ DeleteForm(data CommonData, website models.Website, form forms.WebsiteDeleteForm) { templ DeleteForm(data CommonData, website models.Website, form forms.WebsiteDeleteForm) {
{{ putUrl := fmt.Sprintf("/websites/%s", shortIdToSlug(website.ShortId)) }} {{ 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 }/> <input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<h3>Delete Website</h3> <fieldset class="danger-zone">
<p>Deleting a website is permanent. Be absolutely sure before proceeding.</p> <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"] }} {{ err, exists := form.FieldErrors["delete"] }}
<label for="delete">Type your site name in the form.</label> <div class="form-group">
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
<input type="text" name="delete" id="delete" required/> <label for="delete">Type your site name to confirm deletion</label>
<input type="submit" value="Delete" class="danger"/> <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> </form>
} }
@ -269,8 +356,12 @@ templ WebsiteDashboardSettings(data CommonData, website models.Website, form for
<div id="dashboard"> <div id="dashboard">
@wSidebar(website) @wSidebar(website)
<div> <div>
<section aria-labelledby="website-settings-heading">
@SettingsForm(data, website, form, "") @SettingsForm(data, website, form, "")
</section>
<section aria-labelledby="danger-zone-heading">
@DeleteForm(data, website, forms.WebsiteDeleteForm{}) @DeleteForm(data, website, forms.WebsiteDeleteForm{})
</section>
</div> </div>
</div> </div>
} }
@ -291,8 +382,10 @@ templ WebsiteDashboardComingSoon(title string, data CommonData, website models.W
templ WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) { templ WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) {
@base(title, data) { @base(title, data) {
<section aria-labelledby="website-create-heading">
<form action="/websites/create" method="post"> <form action="/websites/create" method="post">
@websiteCreateForm(data.CSRFToken, form) @websiteCreateForm(data.CSRFToken, form)
</form> </form>
</section>
} }
} }

File diff suppressed because it is too large Load Diff