diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 3060325..e1321e0 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -110,6 +110,7 @@ func (app *application) newCommonData(r *http.Request) views.CommonData { CSRFToken: nosurf.Token(r), CurrentUser: app.getCurrentUser(r), IsHtmx: r.Header.Get("Hx-Request") == "true", + RootUrl: app.rootUrl, } } diff --git a/cmd/web/main.go b/cmd/web/main.go index 7f22d07..5fe6dac 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -28,12 +28,14 @@ type application struct { formDecoder *schema.Decoder debug bool timezones []string + rootUrl string } func main() { addr := flag.String("addr", ":3000", "HTTP network address") dsn := flag.String("dsn", "guestbook.db", "data source name") debug := flag.Bool("debug", false, "enable debug mode") + root := flag.String("root", "localhost:3000", "root URL of application") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) @@ -62,6 +64,7 @@ func main() { formDecoder: formDecoder, debug: *debug, timezones: getAvailableTimezones(), + rootUrl: *root, } err = app.users.InitializeSettingsMap() diff --git a/ui/static/css/style.css b/ui/static/css/style.css index 0b1a576..1f291a6 100644 --- a/ui/static/css/style.css +++ b/ui/static/css/style.css @@ -58,10 +58,17 @@ div#dashboard { 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 { diff --git a/ui/static/js/guestbook.js b/ui/static/js/guestbook.js new file mode 100644 index 0000000..9af432e --- /dev/null +++ b/ui/static/js/guestbook.js @@ -0,0 +1,105 @@ +class CommentList extends HTMLElement { + static get observedAttributes() { + return ['src']; + } + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.comments = []; + this.loading = false; + this.error = null; + } + + connectedCallback() { + if (this.hasAttribute('src')) { + this.fetchComments(); + } + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'src' && oldValue !== newValue) { + this.fetchComments(); + } + } + + async fetchComments() { + const src = this.getAttribute('src'); + if (!src) return; + this.loading = true; + this.error = null; + this.render(); + + try { + const response = await fetch(src); + if (!response.ok) throw new Error(`HTTP error: ${response.status}`); + const data = await response.json(); + this.comments = Array.isArray(data) ? data : []; + this.loading = false; + this.render(); + } catch (err) { + this.error = err.message; + this.loading = false; + this.render(); + } + } + + formatDate(isoString) { + if (!isoString) return ''; + const date = new Date(isoString); + return date.toLocaleString(); + } + + render() { + this.shadowRoot.innerHTML = ` + +
+ ${this.loading + ? `
Loading comments...
` + : this.error + ? `
Error: ${this.error}
` + : this.comments.length === 0 + ? `
No comments found.
` + : this.comments.map(comment => ` +
+ ${comment.AuthorName || 'Unknown Author'} + ${this.formatDate(comment.Created)} +
${comment.CommentText || ''}
+
+ `).join('') + } +
+ `; + } +} + +customElements.define('comment-list', CommentList); diff --git a/ui/views/common_templ.go b/ui/views/common_templ.go index 8d07a52..e79039c 100644 --- a/ui/views/common_templ.go +++ b/ui/views/common_templ.go @@ -20,6 +20,7 @@ type CommonData struct { CSRFToken string CurrentUser *models.User IsHtmx bool + RootUrl string } func shortIdToSlug(shortId uint64) string { diff --git a/ui/views/websites.templ b/ui/views/websites.templ index d778676..1c7f1be 100644 --- a/ui/views/websites.templ +++ b/ui/views/websites.templ @@ -115,9 +115,41 @@ templ WebsiteDashboard(title string, data CommonData, website models.Website) { @wSidebar(website)

{ website.Name }

+

Embed your Guestbook

+

Comment form

- Stats and stuff will go here. + Use this form to allow readers of your website to comment on your guestbook!

+
+ // + @embeddableForm(data.RootUrl, website) +
+

Embed your comments

+

+ Upload this JavaScript WebComponent to your site and include it in your { `` } tag. +

+
+ // +
+						
+							{ 
+`
+    
+` }
+						
+					
+

+ Then add the custom element where you want the comments to show up +

+ {{ getUrl := fmt.Sprintf("https://%s/websites/%s/guestbook/comments", data.RootUrl, shortIdToSlug(website.ShortId)) }} + // +
+						
+							{ fmt.Sprintf(``, getUrl) }
+						
+					
+ @embedJavaScriptSnippet(data.RootUrl, website) +
} @@ -142,3 +174,38 @@ templ WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) @websiteCreateForm(data.CSRFToken, form) } + +templ embeddableForm(root string, website models.Website) { + {{ postUrl := fmt.Sprintf("https://%s/websites/%s/guestbook/comments/create/remote", root, shortIdToSlug(website.ShortId)) }} + {{formStr := + `
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
` + }} +
+		
+			{ fmt.Sprintf(formStr, postUrl) }
+		
+	
+} + +templ embedJavaScriptSnippet(root string, website models.Website) { +} diff --git a/ui/views/websites_templ.go b/ui/views/websites_templ.go index 2d6d6a6..a19e987 100644 --- a/ui/views/websites_templ.go +++ b/ui/views/websites_templ.go @@ -481,7 +481,70 @@ func WebsiteDashboard(title string, data CommonData, website models.Website) tem if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

Stats and stuff will go here.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

Embed your Guestbook

Comment form

Use this form to allow readers of your website to comment on your guestbook!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = embeddableForm(data.RootUrl, website).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Embed your comments

Upload this JavaScript WebComponent to your site and include it in your ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(``) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 129, Col: 140} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " tag.

")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var27 string
+			templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(
+				`
+    
+`)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 138, Col: 8}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

Then add the custom element where you want the comments to show up

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + getUrl := fmt.Sprintf("https://%s/websites/%s/guestbook/comments", data.RootUrl, shortIdToSlug(website.ShortId)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var28 string
+			templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(``, getUrl))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 148, Col: 70}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = embedJavaScriptSnippet(data.RootUrl, website).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -511,12 +574,12 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var26 := templ.GetChildren(ctx) - if templ_7745c5c3_Var26 == nil { - templ_7745c5c3_Var26 = templ.NopComponent + templ_7745c5c3_Var29 := templ.GetChildren(ctx) + if templ_7745c5c3_Var29 == nil { + templ_7745c5c3_Var29 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var30 := 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 { @@ -528,7 +591,7 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -536,26 +599,26 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 131, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 163, Col: 22} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

Coming Soon

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

Coming Soon

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var30), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -579,12 +642,12 @@ func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var29 := templ.GetChildren(ctx) - if templ_7745c5c3_Var29 == nil { - templ_7745c5c3_Var29 = templ.NopComponent + templ_7745c5c3_Var32 := templ.GetChildren(ctx) + if templ_7745c5c3_Var32 == nil { + templ_7745c5c3_Var32 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -592,7 +655,7 @@ func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -600,4 +663,94 @@ func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) }) } +func embeddableForm(root string, website models.Website) 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_Var33 := templ.GetChildren(ctx) + if templ_7745c5c3_Var33 == nil { + templ_7745c5c3_Var33 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + postUrl := fmt.Sprintf("https://%s/websites/%s/guestbook/comments/create/remote", root, shortIdToSlug(website.ShortId)) + formStr := + `
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
` + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var34 string
+		templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(formStr, postUrl))
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 205, Col: 34}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func embedJavaScriptSnippet(root string, website models.Website) 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_Var35 := templ.GetChildren(ctx) + if templ_7745c5c3_Var35 == nil { + templ_7745c5c3_Var35 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + return nil + }) +} + var _ = templruntime.GeneratedTemplate