Compare commits

..

No commits in common. "17d25da9d4c2262cfae8bd5ac8fe41737f66d3c4" and "65801464f14d27c5cdc2883439dac21e5063d9ca" have entirely different histories.

22 changed files with 1065 additions and 2371 deletions

4
.gitmodules vendored
View File

@ -1,4 +0,0 @@
[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,10 +14,6 @@ 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.GetVisible(website.Guestbook.ID) comments, err := app.guestbookComments.GetAll(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.GetVisibleSerialized(website.Guestbook.ID) comments, err := app.guestbookComments.GetAllSerialized(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.GetVisible(website.Guestbook.ID) comments, err := app.guestbookComments.GetAll(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.GetAll(website.Guestbook.ID) comments, err := app.guestbookComments.GetUnpublished(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,8 +315,6 @@ 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) {
@ -351,7 +349,6 @@ 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 'self'; style-src-elem 'self';") w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
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,7 +35,6 @@ 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)
@ -44,6 +43,7 @@ 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,12 +51,13 @@ 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/hidden", protected.ThenFunc(app.getCommentQueue)) mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/queue", 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,7 +7,6 @@ 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
@ -17,9 +16,4 @@ require (
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
) )
require ( require github.com/go-jose/go-jose/v4 v4.0.5 // indirect
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,41 +6,27 @@ 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)
GetVisible(guestbookId int64) ([]GuestbookComment, error)
GetVisibleSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error)
GetDeleted(guestbookId int64) ([]GuestbookComment, error)
GetAll(guestbookId int64) ([]GuestbookComment, error) GetAll(guestbookId int64) ([]GuestbookComment, error)
GetAllSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error)
GetDeleted(guestbookId int64) ([]GuestbookComment, error)
GetUnpublished(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) GetVisible(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
@ -100,7 +100,7 @@ func (m *GuestbookCommentModel) GetVisible(guestbookId int64) ([]GuestbookCommen
return comments, nil return comments, nil
} }
func (m *GuestbookCommentModel) GetVisibleSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error) { func (m *GuestbookCommentModel) GetAllSerialized(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) GetAll(guestbookId int64) ([]GuestbookComment, error) { func (m *GuestbookCommentModel) GetUnpublished(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 WHERE GuestbookId = ? AND Deleted IS NULL AND IsPublished = FALSE
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) GetVisible(guestbookId int64) ([]models.GuestbookComment, error) { func (m *GuestbookCommentModel) GetAll(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) GetVisible(guestbookId int64) ([]models.Guestboo
} }
} }
func (m *GuestbookCommentModel) GetVisibleSerialized(guestbookId int64) ([]models.GuestbookCommentSerialized, error) { func (m *GuestbookCommentModel) GetAllSerialized(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) GetAll(guestbookId int64) ([]models.GuestbookComment, error) { func (m *GuestbookCommentModel) GetUnpublished(guestbookId int64) ([]models.GuestbookComment, error) {
switch guestbookId { switch guestbookId {
default: default:
return []models.GuestbookComment{}, models.ErrNoRecord return []models.GuestbookComment{}, models.ErrNoRecord

View File

@ -12,6 +12,7 @@ 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
@ -316,30 +317,16 @@ 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 = ?`
tx, err := m.DB.Begin() r, err := m.DB.Exec(stmt, time.Now().UTC(), websiteId)
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
}
stmt = `UPDATE guestbooks SET Deleted = ? WHERE WebsiteId = ?`
_, err = tx.Exec(stmt, t, websiteId)
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return rbErr
}
return err
}
err = tx.Commit()
if err != nil { if err != nil {
return err return err
} }
if rows, err := r.RowsAffected(); rows != 1 {
if err != nil {
return err
}
return errors.New("Failed to update website")
}
return nil return nil
} }

View File

@ -1,886 +1,86 @@
/* CSS Reset and Base Styles */ /* html {
*, 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 {
font-size: var(--text-base); max-width: 1024px;
color: var(--color-text); margin: 1rem auto;
background-color: var(--color-background); padding: 1rem;
min-height: 100vh; /* background: white; */
display: flex; font-size: 1.2rem;
flex-direction: column; line-height: 1.5;
font-family: Arial, Helvetica, sans-serif;
} }
h1, h2, h3, h4, h5, h6 { header {
font-weight: 600; text-align: center;
line-height: 1.25;
margin-bottom: var(--space-md);
} }
h1 { font-size: var(--text-3xl); } body > nav {
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-lg); }
p {
margin-bottom: var(--space-md);
}
a {
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; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--space-md);
} }
.nav-welcome { body > nav ul {
font-weight: 500;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: var(--space-md);
list-style: none; list-style: none;
margin: 0; margin: 0 1rem;
padding: 0; padding: 0;
align-items: center;
} }
.nav-links li { body > nav li {
margin: 0; display: inline-block;
padding: 0 0.5rem;
} }
.nav-links a { nav form {
padding: var(--space-sm) var(--space-md); display: inline-block;
border-radius: var(--radius-md);
transition: background-color 0.2s ease;
white-space: nowrap;
} }
.nav-links a:hover { nav button {
background-color: var(--color-border-light); border: none;
text-decoration: none; background: none;
font-family: unset;
font-size: unset;
/* color: blue; */
/* text-decoration: underline; */
cursor: pointer;
} }
main { main {
flex: 1; padding: 1rem;
max-width: var(--max-width); }
margin: 0 auto;
padding: var(--space-2xl) var(--space-lg); div#dashboard {
width: 100%; 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 { footer {
background-color: var(--color-surface);
border-top: 1px solid var(--color-border);
padding: var(--space-lg);
text-align: center; text-align: center;
margin-top: auto;
} }
.footer-links { a {
padding: 0; /* color: blue; */
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 +0,0 @@
Subproject commit 5a85d8a93237e08d9d1f861aa5630f292424cfc0

View File

@ -41,37 +41,31 @@ 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 aria-label="Site navigation"> <nav>
<div class="nav-welcome"> <div>
<span>
if data.IsAuthenticated { if data.IsAuthenticated {
Welcome, { data.CurrentUser.Username } Welcome, { data.CurrentUser.Username }
} }
</span>
</div> </div>
<ul class="nav-links"> <div>
if data.IsAuthenticated { if data.IsAuthenticated {
<li><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>
} else { } else {
if data.LocalAuthEnabled { if data.LocalAuthEnabled {
<li><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>
} }
</ul> </div>
</nav> </nav>
} }
templ commonFooter() { templ commonFooter() {
<footer> <footer>
<p>A <a href="https://32bit.cafe" rel="noopener">32bit.cafe</a> Project</p> <p>A <a href="https://32bit.cafe">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>
} }
@ -82,19 +76,18 @@ 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"/>
<meta name="htmx-config" content={ `{"includeIndicatorStyles":false}` }/> <link href="/static/css/classless.min.css" rel="stylesheet"/>
<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 role="main"> <main>
if data.Flash != "" { if data.Flash != "" {
<div class="notice flash">{ data.Flash }</div> <div class="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 aria-label=\"Site navigation\"><div class=\"nav-welcome\"><span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<nav><div>")
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: 48, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 47, Col: 40}
} }
_, 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, "</span></div><ul class=\"nav-links\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div>")
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, "<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=\"") 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=\"")
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: 57, Col: 66} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 55, Col: 62}
} }
_, 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></li>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Logout</a>")
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, "<li><a href=\"/users/register\">Create an Account</a></li>| ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"/users/register\">Create an Account</a> | ")
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, " <li><a href=\"/users/login\">Login</a></li>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <a href=\"/users/login\">Login</a>")
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, "</ul></nav>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></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\" 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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><p>A <a href=\"https://32bit.cafe\">32bit.cafe</a> Project</p></footer>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -210,26 +210,13 @@ 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: 82, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 76, 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\"><meta name=\"htmx-config\" content=\"") 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>")
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
} }
@ -241,34 +228,51 @@ 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, 14, "<main role=\"main\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<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, 15, "<div class=\"notice flash\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"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(data.Flash) templ_7745c5c3_Var9, 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: 96, Col: 43} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 90, Col: 15}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, 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, 16, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h1>")
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, 17, "</main>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -276,7 +280,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, 18, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -10,140 +10,91 @@ templ GuestbookDashboardCommentsView(title string, data CommonData, website mode
<div id="dashboard"> <div id="dashboard">
@wSidebar(website) @wSidebar(website)
<div> <div>
<section aria-labelledby="comments-management-heading"> <h1>Comments on { website.Name }</h1>
<header class="section-header"> <hr/>
<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 i, c := range comments { for _, c := range comments {
@GuestbookDashboardCommentView(data, website, c, i) @GuestbookDashboardCommentView(data, website, c)
} }
</section>
</div> </div>
</div> </div>
} }
} }
templ GuestbookDashboardCommentDeletePart(text string) { templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models.GuestbookComment) {
<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) }}
{{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(w), shortIdToSlug(c.ShortId)) }} {{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(w), shortIdToSlug(c.ShortId)) }}
<button {{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
type="button" <div class="comment">
class="outline" <div>
hx-put={ commentUrl } if c.Deleted.IsZero() {
hx-headers={ hxHeaders } <button class="danger" hx-delete={ commentUrl } hx-target="closest div.comment" hx-headers={ hxHeaders }>Delete</button>
hx-swap="outerHTML" <button class="outline" hx-put={ commentUrl } hx-target="closest div.comment" hx-headers={ hxHeaders }>
>
if !c.IsPublished { if !c.IsPublished {
Publish Publish
} else { } else {
Hide Hide
} }
</button> </button>
} }
templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models.GuestbookComment, i int) {
{{ 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 {
<div class="comment-contact">
{{ email := "mailto:" + c.AuthorEmail }}
<i class="fa-solid fa-envelope"></i>
<a href={ templ.URL(email) } target="_blank">{ c.AuthorEmail }</a>
</div> </div>
<div>
<strong>{ c.AuthorName }</strong>
if len(c.AuthorEmail) > 0 {
{{ email := "mailto:" + c.AuthorEmail }}
| <a href={ templ.URL(email) } target="_blank">{ c.AuthorEmail }</a>
} }
if len(c.AuthorSite) > 0 { if len(c.AuthorSite) > 0 {
<div class="comment-contact"> | <a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorSite }</a>
<i class="fa-solid fa-house"></i>
<a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorSite }</a>
</div>
} }
<p>
{ c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM") }
</p>
</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) {
<fieldset> <div>
<legend>Leave a comment</legend> <label for="authorname">Name</label>
<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" required aria-describedby="authorname-help"/> <input type="text" name="authorname" id="authorname"/>
<small id="authorname-help">Your name or handle</small>
</div> </div>
<div class="form-group"> <div>
<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="email" name="authoremail" id="authoremail" aria-describedby="authoremail-help"/> <input type="text" name="authoremail" id="authoremail"/>
<small id="authoremail-help">Your email address will only be shared with the guestbook's owner</small>
</div> </div>
<div class="form-group"> <div>
<label for="authorsite">Website URL (Optional)</label> <label for="authorsite">Site 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="url" name="authorsite" id="authorsite" aria-describedby="authorsite-help"/> <input type="text" name="authorsite" id="authorsite"/>
<small id="authorsite-help">Link to your website or social profile</small>
</div> </div>
<div class="form-group"> <div>
<label for="content">Comment <span aria-label="required">*</span></label> <label for="content">Comment</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" required aria-describedby="content-help"></textarea> <textarea name="content" id="content"></textarea>
<small id="content-help">Share your thoughts, feedback, or just say hello!</small>
</div> </div>
<div class="form-group"> <div>
<button type="submit">Submit Comment</button> <input type="submit" value="Submit"/>
</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) {
@ -154,30 +105,26 @@ templ GuestbookView(title string, data CommonData, website models.Website, guest
<html> <html>
<head> <head>
<title>{ title }</title> <title>{ title }</title>
<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/main.js" defer></script> <script src="/static/js/main.js" defer></script>
</head> </head>
<body> <body>
<main role="main"> <main>
<section aria-labelledby="guestbook-heading"> <div>
<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>
</section> </div>
<section id="comments" aria-labelledby="comments-heading"> <div id="comments">
<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 i, c := range comments { for _, c := range comments {
{{ commentAuthorRole := fmt.Sprintf("comment-author-%d", i+1) }} <div>
<article class="comment" role="article" aria-labelledby={ commentAuthorRole }> <h3>
<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 {
@ -185,15 +132,12 @@ 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>
} }
</section> </div>
</main> </main>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,13 @@ package views
templ Home(title string, data CommonData) { templ Home(title string, data CommonData) {
@base(title, data) { @base(title, data) {
<section aria-labelledby="welcome-heading"> <h2>Welcome</h2>
<h2 id="welcome-heading">Welcome</h2> <p>
<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> Welcome to webweav.ing, a collection of webmastery tools created by the <a href="https://32bit.cafe">32-Bit Cafe</a>.
<aside class="notice" role="alert" aria-labelledby="service-status"> </p>
<h3 id="service-status" class="sr-only">Service Status Notice</h3> <p>
<p><strong>Important:</strong> This service is in a pre-alpha state. Your account and data can disappear at any time.</p> Note this service is in a pre-alpha state. Your account and data can disappear at any time.
</aside> </p>
</section>
} }
} }
@ -18,11 +17,3 @@ 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, "<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>") 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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -102,51 +102,4 @@ 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,12 +86,8 @@ 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 {
@ -102,11 +98,8 @@ templ UserSettingsView(data CommonData, timezones []string) {
} }
} }
</select> </select>
</div> <input type="submit" value="Submit"/>
</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><section aria-labelledby=\"user-settings-heading\"><form hx-put=\"/users/settings\"><input type=\"hidden\" name=\"csrf_token\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div><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: 91, Col: 66} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 90, Col: 65}
} }
_, 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, "\"><fieldset><legend id=\"user-settings-heading\">User Settings</legend><div class=\"form-group\"><label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"> <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: 99, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 25}
} }
_, 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: 99, Col: 51} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 48}
} }
_, 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: 101, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 97, Col: 25}
} }
_, 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: 101, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 97, Col: 32}
} }
_, 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></div></fieldset><button type=\"submit\">Save Settings</button></form></section></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</select> <input type=\"submit\" value=\"Submit\"></form></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -14,126 +14,91 @@ 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 aria-label="Dashboard navigation"> <nav>
<div> <div>
<section aria-labelledby="main-nav-heading"> <ul>
<h3 id="main-nav-heading">Website</h3> <li><a href={ templ.URL(dashUrl) }>Dashboard</a></li>
<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>
</section> <h3>Guestbook</h3>
</div> <ul>
<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>
</section> <ul>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments") }>Manage messages</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments/queue") }>Review message queue</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments/trash") }>Trash</a></li>
</ul>
<ul>
//<li><a href={ templ.URL(dashUrl + "/guestbook/themes") }>Themes</a></li>
//<li><a href={ templ.URL(dashUrl + "/guestbook/customize") }>Custom CSS</a></li>
</ul>
</div> </div>
<div> <div>
<section aria-labelledby="feeds-nav-heading"> <h3>Feeds</h3>
<h3 id="feeds-nav-heading">Feeds</h3>
<p>Coming Soon</p> <p>Coming Soon</p>
</section>
</div> </div>
<div> <div>
<section aria-labelledby="account-nav-heading"> <h3>Account</h3>
<h3 id="account-nav-heading">Account</h3> <ul>
<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 }/>
<fieldset> <div>
<legend id="website-create-heading">Website Settings</legend> {{ err, exists := form.FieldErrors["sitename"] }}
<div class="form-group"> <label for="sitename">Site Name: </label>
{{ 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="ws_name" id="sitename" value={ form.Name } required aria-describedby="sitename-help"/> <input type="text" name="sitename" id="sitename" value={ form.Name } required/>
<small id="sitename-help">The display name for your website</small>
</div>
<div class="form-group">
{{ err, exists = form.FieldErrors["ws_url"] }}
<label for="ws_url">Site URL <span aria-label="required">*</span></label>
if exists {
<label class="error">{ err }</label>
}
<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["ws_author"] }} {{ err, exists = form.FieldErrors["siteurl"] }}
<label for="ws_author">Site Author <span aria-label="required">*</span></label> <label for="siteurl">Site URL: </label>
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
<input type="text" name="ws_author" id="authorname" value={ form.AuthorName } required aria-describedby="authorname-help"/> <input type="text" name="siteurl" id="siteurl" value={ form.SiteUrl } required/>
<small id="authorname-help">Your name or the website owner's name</small>
</div> </div>
</fieldset>
<div> <div>
<button type="submit">Add Website</button> {{ err, exists = form.FieldErrors["authorname"] }}
<label for="authorname">Site Author: </label>
if exists {
<label class="error">{ err }</label>
}
<input type="text" name="authorname" id="authorname" value={ form.AuthorName } required/>
</div>
<div>
<button type="submit">Submit</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) {
<section aria-labelledby="websites-heading"> <div>
<header class="section-header"> <a href="/websites/create">Add Website</a>
<h1 id="websites-heading">My Websites</h1>
<a href="/websites/create" class="btn btn-primary" role="button" aria-label="Create a new website">Add Website</a>
</header>
<div class="websites-container">
if len(websites) == 0 {
<h2>No Websites yet.</h2>
<p>Create your first website to get started with webweav.ing tools.</p>
<a href="/websites/create" class="btn btn-primary">Create Your First Website</a>
} else {
<ul
id="websites"
role="list"
aria-label="Your websites"
hx-get="/websites"
hx-trigger="newWebsite from:body"
hx-swap="outerHTML"
>
for _, w := range websites {
<li role="listitem">
<article class="website-card">
<header class="website-header">
<h2 class="website-name">
<a href={ templ.URL(wUrl(w) + "/dashboard") } aria-label={ fmt.Sprintf("Manage %s website dashboard", w.Name) }>
{ w.Name }
</a>
</h2>
<span class="website-url" aria-label="Website URL">{ w.Url.String() }</span>
</header>
<div class="website-actions">
<a href={ templ.URL(wUrl(w) + "/dashboard") } class="btn btn-outline" aria-label={ fmt.Sprintf("Open %s dashboard", w.Name) }>Dashboard</a>
<a href={ templ.URL(wUrl(w) + "/guestbook") } target="_blank" rel="noopener" class="btn btn-outline" aria-label={ fmt.Sprintf("View %s website guestbook", w.Name) }>View Guestbook</a>
<a href={ templ.URL(externalUrl(w.Url.String())) } target="_blank" rel="noopener" class="btn btn-outline" aria-label={ fmt.Sprintf("View %s website", w.Name) }>Visit Site</a>
</div> </div>
</article> <div>
if len(websites) == 0 {
<p>No Websites yet. <a href="">Register a website.</a></p>
} else {
<ul id="websites" hx-get="/websites" hx-trigger="newWebsite from:body" hx-swap="outerHTML">
for _, w := range websites {
<li>
<a href={ templ.URL(wUrl(w) + "/dashboard") }>{ w.Name }</a>
</li> </li>
} }
</ul> </ul>
} }
</div> </div>
</section>
} }
} }
@ -142,142 +107,103 @@ templ WebsiteDashboard(title string, data CommonData, website models.Website) {
<div id="dashboard"> <div id="dashboard">
@wSidebar(website) @wSidebar(website)
<div> <div>
{{ gbUrl := fmt.Sprintf("https://%s/websites/%s/guestbook", data.RootUrl, shortIdToSlug(website.ShortId)) }} <h1>Embed your Guestbook</h1>
<section aria-labelledby="embed-instructions"> <p>
<h1 id="embed-instructions">Embed your Guestbook</h1> Upload <a href="/static/js/guestbook.js" download>this JavaScript WebComponent</a> to your site and include it in your <code>{ `<head>` }</code> tag.
<div class="instruction-overview"> </p>
<p>There are two ways to add your guestbook to your website: using our JavaScript component (recommended) or with an iframe.</p> <div>
</div> //<button>Copy to Clipboard</button>
<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" aria-label="HTML head script tag"> <code id="guestbookSnippet">
&lt;head&gt; {
&lt;script type=&#34;module&#34; src=&#34;js/guestbook.js&#34;&gt;&lt;/script&gt; `<head>
&lt;/head&gt; <script type="module" src="js/guestbook.js"></script>
</head>` }
</code> </code>
</pre> </pre>
</figure> <p>
</div> Then add the custom elements where you want your form and comments to show up
<div class="instruction-step"> </p>
<h3>Step 3: Add the custom elements</h3> {{ gbUrl := fmt.Sprintf("https://%s/websites/%s/guestbook", data.RootUrl, shortIdToSlug(website.ShortId)) }}
<figure class="code-example"> //<button>Copy to Clipboard</button>
<figcaption>Place these elements where you want your guestbook to appear:</figcaption>
<pre> <pre>
<code aria-label="Custom guestbook elements"> <code>
{ 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>
</article> <p>
<article class="instruction-method"> If your web host does not allow CORS requests, use an iframe instead
<h2>Method 2: iframe (Alternative)</h2> </p>
<details> <div>
<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 aria-label="iframe embedding code"> <code>
{ fmt.Sprintf(`<iframe src="%s" { fmt.Sprintf(`<iframe src="%s" title="Guestbook"></iframe>`, gbUrl) }
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) {
<legend id="website-settings-heading">Website Settings</legend> <h3>Website Settings</h3>
<div class="form-group"> <div>
{{ err, exists := form.FieldErrors["ws_name"] }} {{ err, exists := form.FieldErrors["ws_name"] }}
<label for="ws_name">Site Name <span aria-label="required">*</span></label> <label for="ws_name">Site Name: </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 aria-describedby="sitename-help"/> <input type="text" name="ws_name" id="sitename" value={ form.SiteName } required/>
} else { } else {
<input type="text" name="ws_name" id="sitename" value={ website.Name } required aria-describedby="sitename-help"/> <input type="text" name="ws_name" id="sitename" value={ website.Name } required/>
} }
<small id="sitename-help">The display name for your website</small>
</div> </div>
<div class="form-group"> <div>
{{ err, exists = form.FieldErrors["ws_url"] }} {{ err, exists = form.FieldErrors["ws_url"] }}
<label for="ws_url">Site URL <span aria-label="required">*</span></label> <label for="ws_url">Site URL: </label>
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
if form.SiteUrl != "" { if form.SiteUrl != "" {
<input type="url" name="ws_url" id="ws_url" value={ form.SiteUrl } required aria-describedby="siteurl-help"/> <input type="text" name="ws_url" id="ws_url" value={ form.SiteUrl } required/>
} else { } else {
<input type="url" name="ws_url" id="ws_url" value={ website.Url.String() } required aria-describedby="siteurl-help"/> <input type="text" name="ws_url" id="ws_url" value={ website.Url.String() } required/>
} }
<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 <span aria-label="required">*</span></label> <label for="ws_author">Site Author: </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 aria-describedby="authorname-help"/> <input type="text" name="ws_author" id="authorname" value={ form.AuthorName } required/>
} else { } else {
<input type="text" name="ws_author" id="authorname" value={ website.AuthorName } required aria-describedby="authorname-help"/> <input type="text" name="ws_author" id="authorname" value={ website.AuthorName } required/>
} }
<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) {
<legend>Guestbook Settings</legend> <h3>Guestbook Settings</h3>
<div class="form-group"> <div>
<fieldset class="radio-group"> <label>Guestbook Visibility</label>
<legend>Guestbook Visibility</legend> <label for="gb_visible_true">
<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> <label for="gb_visible_false">
<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 class="form-group"> <div>
<label for="gb-commenting">Guestbook Commenting</label> <label>Guestbook Commenting</label>
<select name="gb_commenting" id="gb-commenting" aria-describedby="commenting-help"> <select name="gb_commenting" id="gb-commenting">
<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>
@ -291,11 +217,9 @@ 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 class="form-group"> <div>
<fieldset class="radio-group"> <label>Enable Widgets</label>
<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
@ -304,8 +228,6 @@ 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>
} }
@ -317,36 +239,27 @@ 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 }/>
<fieldset> <div id="settings_form">
@websiteSettingsForm(data, website, form) @websiteSettingsForm(data, website, form)
</fieldset>
<fieldset>
@guestbookSettingsForm(data, website, gb, form) @guestbookSettingsForm(data, website, gb, form)
</fieldset> </div>
<button type="submit">Save Settings</button> <input type="submit" value="Submit"/>
</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" aria-label="Delete website"> <form hx-put={ putUrl } hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/> <input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<fieldset class="danger-zone"> <h3>Delete Website</h3>
<legend id="danger-zone-heading">Delete Website</legend> <p>Deleting a website is permanent. Be absolutely sure before proceeding.</p>
<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"] }}
<div class="form-group"> <label for="delete">Type your site name in the form.</label>
if exists { if exists {
<label class="error">{ err }</label> <label class="error">{ err }</label>
} }
<label for="delete">Type your site name to confirm deletion</label> <input type="text" name="delete" id="delete" required/>
<input type="text" name="delete" id="delete" required aria-describedby="delete-help" placeholder={ website.Name }/> <input type="submit" value="Delete" class="danger"/>
<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>
} }
@ -356,12 +269,8 @@ 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>
} }
@ -382,10 +291,8 @@ 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