add remote client examples and webcomponent script

This commit is contained in:
yequari 2025-06-26 20:12:24 -07:00
parent 306053b1e3
commit c3b10ae239
7 changed files with 356 additions and 19 deletions

View File

@ -110,6 +110,7 @@ func (app *application) newCommonData(r *http.Request) views.CommonData {
CSRFToken: nosurf.Token(r), CSRFToken: nosurf.Token(r),
CurrentUser: app.getCurrentUser(r), CurrentUser: app.getCurrentUser(r),
IsHtmx: r.Header.Get("Hx-Request") == "true", IsHtmx: r.Header.Get("Hx-Request") == "true",
RootUrl: app.rootUrl,
} }
} }

View File

@ -28,12 +28,14 @@ type application struct {
formDecoder *schema.Decoder formDecoder *schema.Decoder
debug bool debug bool
timezones []string timezones []string
rootUrl string
} }
func main() { func main() {
addr := flag.String("addr", ":3000", "HTTP network address") addr := flag.String("addr", ":3000", "HTTP network address")
dsn := flag.String("dsn", "guestbook.db", "data source name") dsn := flag.String("dsn", "guestbook.db", "data source name")
debug := flag.Bool("debug", false, "enable debug mode") debug := flag.Bool("debug", false, "enable debug mode")
root := flag.String("root", "localhost:3000", "root URL of application")
flag.Parse() flag.Parse()
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
@ -62,6 +64,7 @@ func main() {
formDecoder: formDecoder, formDecoder: formDecoder,
debug: *debug, debug: *debug,
timezones: getAvailableTimezones(), timezones: getAvailableTimezones(),
rootUrl: *root,
} }
err = app.users.InitializeSettingsMap() err = app.users.InitializeSettingsMap()

View File

@ -58,10 +58,17 @@ div#dashboard {
div#dashboard nav { div#dashboard nav {
flex: 1 1 25%; flex: 1 1 25%;
margin-top: 2rem; margin-top: 2rem;
min-width: 0;
} }
div#dashboard > div { div#dashboard > div {
flex: 10 1 40%; flex: 10 1 40%;
min-width: 0;
}
div > pre {
max-width: 100%;
overflow: auto;
} }
main nav ul { main nav ul {

105
ui/static/js/guestbook.js Normal file
View File

@ -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 = `
<style>
.comment-list {
font-family: Arial, sans-serif;
border: 1px solid #ddd;
border-radius: 6px;
padding: 1em;
background: #fafafa;
}
.comment {
border-bottom: 1px solid #eee;
padding: 0.7em 0;
}
.comment:last-child {
border-bottom: none;
}
.author {
font-weight: bold;
margin-right: 1em;
}
.timestamp {
color: #888;
font-size: 0.85em;
}
.text {
margin: 0.2em 0 0 0;
}
.error {
color: red;
}
</style>
<div class="comment-list">
${this.loading
? `<div>Loading comments...</div>`
: this.error
? `<div class="error">Error: ${this.error}</div>`
: this.comments.length === 0
? `<div>No comments found.</div>`
: this.comments.map(comment => `
<div class="comment">
<span class="author">${comment.AuthorName || 'Unknown Author'}</span>
<span class="timestamp">${this.formatDate(comment.Created)}</span>
<div class="text">${comment.CommentText || ''}</div>
</div>
`).join('')
}
</div>
`;
}
}
customElements.define('comment-list', CommentList);

View File

@ -20,6 +20,7 @@ type CommonData struct {
CSRFToken string CSRFToken string
CurrentUser *models.User CurrentUser *models.User
IsHtmx bool IsHtmx bool
RootUrl string
} }
func shortIdToSlug(shortId uint64) string { func shortIdToSlug(shortId uint64) string {

View File

@ -115,9 +115,41 @@ templ WebsiteDashboard(title string, data CommonData, website models.Website) {
@wSidebar(website) @wSidebar(website)
<div> <div>
<h1>{ website.Name }</h1> <h1>{ website.Name }</h1>
<h2>Embed your Guestbook</h2>
<h3>Comment form</h3>
<p> <p>
Stats and stuff will go here. Use this form to allow readers of your website to comment on your guestbook!
</p> </p>
<div>
//<button>Copy to Clipboard</button>
@embeddableForm(data.RootUrl, website)
</div>
<h3>Embed your comments</h3>
<p>
Upload <a href="/static/js/guestbook.js" download>this JavaScript WebComponent</a> to your site and include it in your <code>{ `<head>` }</code> tag.
</p>
<div>
//<button>Copy to Clipboard</button>
<pre>
<code id="guestbookSnippet">
{
`<head>
<script type="module" src="js/guestbook.js"></script>
</head>` }
</code>
</pre>
<p>
Then add the custom element where you want the comments to show up
</p>
{{ getUrl := fmt.Sprintf("https://%s/websites/%s/guestbook/comments", data.RootUrl, shortIdToSlug(website.ShortId)) }}
//<button>Copy to Clipboard</button>
<pre>
<code>
{ fmt.Sprintf(`<comment-list src="%s"></comment-list>`, getUrl) }
</code>
</pre>
@embedJavaScriptSnippet(data.RootUrl, website)
</div>
</div> </div>
</div> </div>
} }
@ -142,3 +174,38 @@ templ WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm)
@websiteCreateForm(data.CSRFToken, form) @websiteCreateForm(data.CSRFToken, form)
</form> </form>
} }
templ embeddableForm(root string, website models.Website) {
{{ postUrl := fmt.Sprintf("https://%s/websites/%s/guestbook/comments/create/remote", root, shortIdToSlug(website.ShortId)) }}
{{formStr :=
`<form action="%s" method="post">
<div>
<label for="authorname">Name</label>
<input type="text" name="authorname" id="authorname"/>
</div>
<div>
<label for="authoremail">Email (Optional) </label>
<input type="text" name="authoremail" id="authoremail"/>
</div>
<div>
<label for="authorsite">Site Url (Optional) </label>
<input type="text" name="authorsite" id="authorsite"/>
</div>
<div>
<label for="content">Comment</label>
<textarea name="content" id="content"></textarea>
</div>
<div>
<input type="submit" value="Submit"/>
</div>
</form>`
}}
<pre>
<code>
{ fmt.Sprintf(formStr, postUrl) }
</code>
</pre>
}
templ embedJavaScriptSnippet(root string, website models.Website) {
}

View File

@ -481,7 +481,70 @@ func WebsiteDashboard(title string, data CommonData, website models.Website) tem
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, 38, "</h1><p>Stats and stuff will go here.</p></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</h1><h2>Embed your Guestbook</h2><h3>Comment form</h3><p>Use this form to allow readers of your website to comment on your guestbook!</p><div>")
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, "</div><h3>Embed your comments</h3><p>Upload <a href=\"/static/js/guestbook.js\" download>this JavaScript WebComponent</a> to your site and include it in your <code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(`<head>`)
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, "</code> tag.</p><div><pre><code id=\"guestbookSnippet\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(
`<head>
<script type="module" src="js/guestbook.js"></script>
</head>`)
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, "</code></pre><p>Then add the custom element where you want the comments to show up</p>")
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, "<pre><code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`<comment-list src="%s"></comment-list>`, 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, "</code></pre>")
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, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -511,12 +574,12 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var26 := templ.GetChildren(ctx) templ_7745c5c3_Var29 := templ.GetChildren(ctx)
if templ_7745c5c3_Var26 == nil { if templ_7745c5c3_Var29 == nil {
templ_7745c5c3_Var26 = templ.NopComponent templ_7745c5c3_Var29 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer { if !templ_7745c5c3_IsBuffer {
@ -528,7 +591,7 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div id=\"dashboard\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div id=\"dashboard\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -536,26 +599,26 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We
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, 40, "<div><h1>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<div><h1>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var28 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</h1><p>Coming Soon</p></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</h1><p>Coming Soon</p></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
return nil 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -579,12 +642,12 @@ func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm)
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var29 := templ.GetChildren(ctx) templ_7745c5c3_Var32 := templ.GetChildren(ctx)
if templ_7745c5c3_Var29 == nil { if templ_7745c5c3_Var32 == nil {
templ_7745c5c3_Var29 = templ.NopComponent templ_7745c5c3_Var32 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<form hx-post=\"/websites/create\" hx-target=\"closest div\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<form hx-post=\"/websites/create\" hx-target=\"closest div\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -592,7 +655,7 @@ func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm)
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, 43, "</form>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 :=
`<form action="%s" method="post">
<div>
<label for="authorname">Name</label>
<input type="text" name="authorname" id="authorname"/>
</div>
<div>
<label for="authoremail">Email (Optional) </label>
<input type="text" name="authoremail" id="authoremail"/>
</div>
<div>
<label for="authorsite">Site Url (Optional) </label>
<input type="text" name="authorsite" id="authorsite"/>
</div>
<div>
<label for="content">Comment</label>
<textarea name="content" id="content"></textarea>
</div>
<div>
<input type="submit" value="Submit"/>
</div>
</form>`
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<pre><code>")
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, "</code></pre>")
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 var _ = templruntime.GeneratedTemplate