To gitea and beyond, let's go(-yco)

This commit is contained in:
2025-11-10 19:12:09 +01:00
parent 8f6133392d
commit 71a031342b
245 changed files with 83994 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
{{define "layout"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{if .Title}}{{.Title}}{{end}}</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico" />
<link rel="stylesheet" href="/static/css/main.css" />
</head>
<body>
<header class="site-header">
<div class="container header-bar">
<a class="brand" href="/">{{.SiteTitle}}</a>
<div class="header-search">
<form class="search-form" action="/search" method="get">
<input type="search" name="q" placeholder="Search posts..." value="{{.SearchQuery}}" class="search-input" />
<button type="submit" class="search-button" aria-label="Search">
<span aria-hidden="true">🔍</span>
</button>
</form>
</div>
<nav class="site-nav">
<a href="/">Home</a>
{{if .User}}
<a href="/posts/new">Share</a>
<a href="/settings">Settings</a>
<form class="nav-action" action="/logout" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<button type="submit">Sign out</button>
</form>
{{else}}
<a href="/register">Create account</a>
<a href="/login">Sign in</a>
{{end}}
</nav>
</div>
</header>
<main class="container content-stack">
{{if .Errors}}
<div class="alert alert-error">
{{range .Errors}}
<p>{{.}}</p>
{{end}}
</div>
{{end}}
{{if .Flash}}
<div class="alert alert-success">{{.Flash}}</div>
{{end}}
{{block "content" .}}{{end}}
</main>
<footer class="site-footer">
<div class="container">
<small>Powered with ❤️ by <a href="https://github.com/sandrocazzaniga/goyco">Goyco</a></small>
</div>
</footer>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,56 @@
{{define "content"}}
<section class="auth-card">
<h1 class="auth-card__message">Account deletion</h1>
{{if .Flash}}
<p class="auth-card__message">{{.Flash}}</p>
<div class="auth-card__actions">
<a class="button" href="/login">Go to sign in</a>
<a class="button button-ghost muted" href="/">Back home</a>
</div>
{{else if .Errors}}
{{range .Errors}}
<p class="auth-card__message" data-state="error">{{.}}</p>
{{end}}
<div class="auth-card__actions">
<a class="button" href="/settings">Return to settings</a>
<a class="button button-ghost muted" href="/">Back home</a>
</div>
{{else if .HasPosts}}
<div class="deletion-warning">
<p class="auth-card__message" data-state="error">
<strong>Warning:</strong> You have {{.PostCount}} post{{if ne .PostCount 1}}s{{end}} on this platform.
</p>
<p class="auth-card__message">
What would you like to do with your posts?
</p>
<form method="post" action="/settings/delete/confirm" class="deletion-form">
<input type="hidden" name="token" value="{{.Token}}" />
<div class="deletion-options">
<label class="deletion-option">
<input type="radio" name="delete_posts" value="false" checked />
<span>Keep my posts (they will be attributed to "(deleted)")</span>
</label>
<label class="deletion-option">
<input type="radio" name="delete_posts" value="true" />
<span>Delete all my posts permanently</span>
</label>
</div>
<div class="auth-card__actions">
<button type="submit" class="button button-danger">Confirm deletion</button>
<a class="button button-ghost muted" href="/settings">Cancel</a>
</div>
</form>
</div>
{{else}}
<p class="auth-card__message">Are you sure you want to delete your account? This action cannot be undone.</p>
<form method="post" action="/settings/delete/confirm" class="deletion-form">
<input type="hidden" name="token" value="{{.Token}}" />
<input type="hidden" name="delete_posts" value="false" />
<div class="auth-card__actions">
<button type="submit" class="button button-danger">Confirm deletion</button>
<a class="button button-ghost muted" href="/settings">Cancel</a>
</div>
</form>
{{end}}
</section>
{{end}}

View File

@@ -0,0 +1,57 @@
{{define "content"}}
<section class="auth-card confirmation-card">
{{if .VerificationSuccess}}
<div class="confirmation-success">
<div class="confirmation-icon success-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#10b981" stroke="#10b981" stroke-width="2"/>
<path d="M9 12l2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="confirmation-title">Email Confirmed! 🎉</h1>
<p class="confirmation-message">Your account has been successfully verified. You can now sign in and start using {{.SiteTitle}}.</p>
<div class="confirmation-actions">
<a class="button button-primary" href="/login?verified=1">
<svg class="svg-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M21 12H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Continue to Sign In
</a>
</div>
</div>
{{else}}
<div class="confirmation-error">
<div class="confirmation-icon error-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444" stroke="#ef4444" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="confirmation-title">Verification Failed</h1>
<p class="confirmation-message">We couldn't confirm this account with the link provided. The link may be invalid, expired, or already used.</p>
<div class="confirmation-help">
<h3>What can you do?</h3>
<ul>
<li>Check if you clicked the correct link from your email</li>
<li>Request a new verification email</li>
<li>Contact support if the problem persists</li>
</ul>
</div>
<div class="confirmation-actions">
<a class="button button-primary" href="/login">
<svg class="svg-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 12l2 2 4-4M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9 9 4.03 9 9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to Sign In
</a>
<a class="button button-secondary" href="/resend-verification">
<svg class="svg-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M3 3v5h5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M21 21v-5h-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Resend Email
</a>
</div>
</div>
{{end}}
</section>
{{end}}

View File

@@ -0,0 +1,15 @@
{{define "content"}}
<section class="form-card">
<h1>{{if .Title}}{{.Title}}{{else}}Something went wrong{{end}}</h1>
{{if .Errors}}
<ul class="error-list">
{{range .Errors}}
<li>{{.}}</li>
{{end}}
</ul>
{{else}}
<p>We couldn't handle that request this time.</p>
{{end}}
<p><a href="/">Back to home</a></p>
</section>
{{end}}

View File

@@ -0,0 +1,23 @@
{{define "content"}}
<section class="auth-card">
<h1>Reset your password</h1>
<p class="auth-card__message">Enter your username or email address and we'll send you a link to reset your password.</p>
<form method="post" action="/forgot-password" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="username_or_email">Username or email address</label>
<input
type="text"
id="username_or_email"
name="username_or_email"
value="{{index .FormValues "username_or_email"}}"
required
autofocus
placeholder="username or you@example.com"
/>
<button type="submit">Send reset link</button>
<p class="hint"><a href="/login">Back to sign in</a></p>
</form>
</section>
{{end}}

View File

@@ -0,0 +1,21 @@
{{define "content"}}
<section class="page-header">
<div class="page-heading">
<div class="feed-toggle" role="tablist" aria-label="Sort posts">
<a class="feed-toggle__link{{if ne .PostsSort "new"}} is-active{{end}}" href="{{.PostsSortTopURL}}" role="tab" aria-selected="{{if ne .PostsSort "new"}}true{{else}}false{{end}}">Top voted</a>
<a class="feed-toggle__link{{if eq .PostsSort "new"}} is-active{{end}}" href="{{.PostsSortNewURL}}" role="tab" aria-selected="{{if eq .PostsSort "new"}}true{{else}}false{{end}}">Newest</a>
</div>
</div>
<div class="page-actions">
{{if .User}}
<a class="button" href="/posts/new">Share a link</a>
{{else}}
<a class="button" href="/login">Share a link</a>
{{end}}
</div>
</section>
<section class="post-feed">
{{template "post-list" .}}
</section>
{{end}}

View File

@@ -0,0 +1,17 @@
{{define "content"}}
<section class="auth-card">
<h1>Sign in</h1>
<form method="post" action="/login" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{index .FormValues "username"}}" required autofocus />
<label for="password">Password</label>
<input type="password" id="password" name="password" required />
<button type="submit">Continue</button>
<p class="hint">Forgot your password? <a href="/forgot-password">Reset it</a>.</p>
</form>
</section>
{{end}}

View File

@@ -0,0 +1,91 @@
{{define "content"}}
<section class="form-card">
<h1>Share a link</h1>
<form method="post" action="/posts">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="title">Title</label>
<div class="field-with-action">
<input type="text" id="title" name="title" value="{{index .FormValues "title"}}" required />
<button type="button" id="autofill-title" class="button-secondary">Autofill</button>
</div>
<p class="hint" id="autofill-status" aria-live="polite"></p>
<label for="url">Link</label>
<input type="url" id="url" name="url" value="{{index .FormValues "url"}}" placeholder="https://example.com" required />
<label for="content">Notes</label>
<textarea id="content" name="content" rows="4" placeholder="Optional context">{{index .FormValues "content"}}</textarea>
<button type="submit">Publish</button>
</form>
<script nonce="{{.CSPNonce}}">
(function () {
const autofillButton = document.getElementById('autofill-title');
const urlInput = document.getElementById('url');
const titleInput = document.getElementById('title');
const statusEl = document.getElementById('autofill-status');
if (!autofillButton || !urlInput || !titleInput || !statusEl) {
return;
}
const setStatus = (message, isError) => {
statusEl.textContent = message || '';
if (!message) {
delete statusEl.dataset.state;
return;
}
statusEl.dataset.state = isError ? 'error' : 'info';
};
autofillButton.addEventListener('click', async () => {
const urlValue = urlInput.value.trim();
if (!urlValue) {
setStatus('Add a link first so we can pull its title.', true);
urlInput.focus();
return;
}
setStatus('Looking up the page title…', false);
autofillButton.disabled = true;
try {
const response = await fetch(`/api/posts/title?url=${encodeURIComponent(urlValue)}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
},
credentials: 'same-origin'
});
let body;
try {
body = await response.json();
} catch (_) {
body = {};
}
if (!response.ok) {
const message = body && body.error ? String(body.error) : 'Failed to fetch a title for that link.';
throw new Error(message);
}
const data = body && body.data ? body.data : {};
const fetchedTitle = data && typeof data.title === 'string' ? data.title.trim() : '';
if (!fetchedTitle) {
throw new Error('No title found on that page.');
}
titleInput.value = fetchedTitle;
setStatus('Title copied from the link.', false);
} catch (err) {
setStatus(err.message || 'Unable to fetch the title right now.', true);
} finally {
autofillButton.disabled = false;
}
});
})();
</script>
</section>
{{end}}

View File

@@ -0,0 +1,38 @@
{{define "post-list"}}
{{range .Posts}}
<article class="post-card">
<header>
{{if .URL}}
<h2><a href="{{.URL}}" target="_blank" rel="noopener">{{.Title}}</a></h2>
{{else}}
<h2><a href="/posts/{{.ID}}">{{.Title}}</a></h2>
{{end}}
</header>
<p class="post-meta">Shared by {{if .AuthorName}}{{if eq .AuthorName "(deleted)"}}(deleted){{else}}{{.AuthorName}}{{end}}{{else if and .AuthorID .Author.Username}}{{.Author.Username}}{{else}}unauthenticated user{{end}} · {{formatTime .CreatedAt}}</p>
<div class="post-stats">
<form action="/posts/{{.ID}}/vote" method="post" class="vote-form">
{{if $.CSRFToken}}<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}" />{{end}}
<input type="hidden" name="action" value="{{if eq .CurrentVote "up"}}clear{{else}}up{{end}}" />
<input type="hidden" name="redirect" value="{{$.CurrentPath}}" />
<button type="submit" class="vote-arrow{{if eq .CurrentVote "up"}} is-active{{end}}" aria-label="{{if eq .CurrentVote "up"}}Remove upvote{{else}}Upvote{{end}} post {{.Title}}" title="{{if eq .CurrentVote "up"}}Remove upvote{{else}}Upvote{{end}}">
<span aria-hidden="true">▲</span>
</button>
</form>
<div class="vote-totals">
<span class="vote-score">Score {{.Score}}</span>
<span class="vote-breakdown">▲ {{.UpVotes}} · ▼ {{.DownVotes}}</span>
</div>
<form action="/posts/{{.ID}}/vote" method="post" class="vote-form">
{{if $.CSRFToken}}<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}" />{{end}}
<input type="hidden" name="action" value="{{if eq .CurrentVote "down"}}clear{{else}}down{{end}}" />
<input type="hidden" name="redirect" value="{{$.CurrentPath}}" />
<button type="submit" class="vote-arrow{{if eq .CurrentVote "down"}} is-active{{end}}" aria-label="{{if eq .CurrentVote "down"}}Remove downvote{{else}}Downvote{{end}} post {{.Title}}" title="{{if eq .CurrentVote "down"}}Remove downvote{{else}}Downvote{{end}}">
<span aria-hidden="true">▼</span>
</button>
</form>
</div>
</article>
{{else}}
<p>No posts yet. Be the first to <a href="/posts/new">share something</a>.</p>
{{end}}
{{end}}

View File

@@ -0,0 +1,43 @@
{{define "content"}}
<article class="post-detail">
<header>
{{if .Post.URL}}
<h1><a href="{{.Post.URL}}" target="_blank" rel="noopener">{{.Post.Title}}</a></h1>
{{else}}
<h1>{{.Post.Title}}</h1>
{{end}}
<p class="post-meta">Shared by {{if .Post.AuthorName}}{{if eq .Post.AuthorName "(deleted)"}}(deleted){{else}}{{.Post.AuthorName}}{{end}}{{else if and .Post.AuthorID .Post.Author.Username}}{{.Post.Author.Username}}{{else}}unauthenticated user{{end}} · {{formatTime .Post.CreatedAt}}</p>
</header>
{{if .Post.Content}}
<section class="post-body">
<p>{{.Post.Content}}</p>
</section>
{{end}}
<section class="post-votes">
<div class="vote-strip">
<form action="/posts/{{.Post.ID}}/vote" method="post" class="vote-form">
{{if .CSRFToken}}<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />{{end}}
<input type="hidden" name="redirect" value="/posts/{{.Post.ID}}" />
<input type="hidden" name="action" value="{{if eq .CurrentVote "up"}}clear{{else}}up{{end}}" />
<button type="submit" class="vote-arrow{{if eq .CurrentVote "up"}} is-active{{end}}" aria-label="{{if eq .CurrentVote "up"}}Remove upvote{{else}}Upvote{{end}}">
<span aria-hidden="true">▲</span>
</button>
</form>
<div class="vote-totals">
<span class="vote-score">Score {{.Post.Score}}</span>
<span class="vote-breakdown">▲ {{.Post.UpVotes}} · ▼ {{.Post.DownVotes}}</span>
</div>
<form action="/posts/{{.Post.ID}}/vote" method="post" class="vote-form">
{{if .CSRFToken}}<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />{{end}}
<input type="hidden" name="redirect" value="/posts/{{.Post.ID}}" />
<input type="hidden" name="action" value="{{if eq .CurrentVote "down"}}clear{{else}}down{{end}}" />
<button type="submit" class="vote-arrow{{if eq .CurrentVote "down"}} is-active{{end}}" aria-label="{{if eq .CurrentVote "down"}}Remove downvote{{else}}Downvote{{end}}">
<span aria-hidden="true">▼</span>
</button>
</form>
</div>
</section>
</article>
{{end}}

View File

@@ -0,0 +1,23 @@
{{define "content"}}
<section class="auth-card">
<h1>Create account</h1>
<form method="post" action="/register" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{index .FormValues "username"}}" placeholder="choose-a-name" required autofocus />
<label for="email">Email</label>
<input type="email" id="email" name="email" value="{{index .FormValues "email"}}" placeholder="you@example.com" required />
<label for="password">Password</label>
<input type="password" id="password" name="password" minlength="8" required />
<p class="hint">Use at least 8 characters. Passwords are case-sensitive.</p>
<label for="password_confirm">Confirm password</label>
<input type="password" id="password_confirm" name="password_confirm" minlength="8" required />
<button type="submit">Create account</button>
</form>
<p class="hint">Already have an account? <a href="/login">Sign in</a>.</p>
</section>
{{end}}

View File

@@ -0,0 +1,69 @@
{{define "content"}}
<section class="auth-card">
<div class="resend-verification">
<div class="resend-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M3 3v5h5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M21 21v-5h-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="resend-title">Resend Verification Email</h1>
<p class="resend-message">Enter your email address and we'll send you a new verification link.</p>
{{if .Flash}}
<div class="alert alert-success">
{{.Flash}}
</div>
{{end}}
{{if .Errors}}
<div class="alert alert-error">
{{range .Errors}}
<div>{{.}}</div>
{{end}}
</div>
{{end}}
<form class="resend-form" method="post" action="/resend-verification" autocomplete="email">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="resend-email">Email address</label>
<input
id="resend-email"
type="email"
name="email"
value="{{index .FormValues "email"}}"
required
placeholder="you@example.com"
autofocus
/>
<button type="submit" class="button button-primary">
<svg class="svg-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8l7.89 4.26a2 2 0 0 0 2.22 0L21 8M5 19h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Send Verification Email
</button>
</form>
<div class="resend-help">
<h3>Need help?</h3>
<ul>
<li>Check your spam/junk folder for the verification email</li>
<li>Make sure you're using the email address you registered with</li>
<li>Wait a few minutes for the email to arrive</li>
<li>Contact support if you continue having issues</li>
</ul>
</div>
<div class="resend-actions">
<a class="button button-secondary" href="/login">
<svg class="svg-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M21 12H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to Sign In
</a>
</div>
</div>
</section>
{{end}}

View File

@@ -0,0 +1,47 @@
{{define "content"}}
<section class="auth-card">
<h1>Set new password</h1>
<p class="auth-card__message">Enter your new password below.</p>
{{if .Flash}}
<div class="alert alert-success">{{.Flash}}</div>
{{end}}
{{if .Errors}}
<div class="alert alert-error">
{{range .Errors}}
<p>{{.}}</p>
{{end}}
</div>
{{end}}
<form method="post" action="/reset-password" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<input type="hidden" name="token" value="{{.Token}}" />
<label for="password">New password</label>
<input
type="password"
id="password"
name="password"
minlength="8"
required
autofocus
placeholder="Enter your new password"
/>
<label for="confirm_password">Confirm new password</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
minlength="8"
required
placeholder="Confirm your new password"
/>
<button type="submit">Reset password</button>
<p class="hint"><a href="/login">Back to sign in</a></p>
</form>
</section>
{{end}}

View File

@@ -0,0 +1,27 @@
{{define "content"}}
<section class="page-header">
<div class="page-heading">
<h1 class="page-title">Search results</h1>
{{if .SearchQuery}}
<p class="search-query">Results for "{{.SearchQuery}}"</p>
{{end}}
</div>
</section>
<section class="post-feed">
{{if .SearchQuery}}
{{if .Posts}}
{{template "post-list" .}}
{{else}}
<div class="no-results">
<p>No posts found matching "{{.SearchQuery}}".</p>
<p>Try different keywords or <a href="/posts/new">share something new</a>.</p>
</div>
{{end}}
{{else}}
<div class="no-results">
<p>Enter a search term to find posts.</p>
</div>
{{end}}
</section>
{{end}}

View File

@@ -0,0 +1,151 @@
{{define "content"}}
<section class="settings">
<div class="page-header">
<div class="page-heading">
<h1 class="page-title">Account settings</h1>
<p class="page-subtitle">Manage how you sign in and maintain your presence on {{.SiteTitle}}.</p>
</div>
</div>
<div class="settings-stack">
<div class="settings-row">
<article class="form-card settings-card">
<header class="settings-card__header">
<h2>Change email address</h2>
<p class="settings-lead">Current email:&nbsp;<span class="settings-email">{{.User.Email}}</span></p>
</header>
<form class="settings-card__form" method="post" action="/settings/email" autocomplete="email">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="settings-email">New email</label>
<input
id="settings-email"
type="email"
name="email"
value="{{index .FormValues "email"}}"
required
placeholder="you@example.com"
/>
{{with index .FormErrors "email"}}
<ul class="error-list">
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
{{end}}
<p class="hint">We'll send a confirmation link to the new address. You'll need to verify it before signing in again.</p>
<button type="submit">Update email</button>
</form>
</article>
<article class="form-card settings-card">
<header class="settings-card__header">
<h2>Change username</h2>
<p class="settings-lead">Current username: <strong>{{.User.Username}}</strong></p>
</header>
<form class="settings-card__form" method="post" action="/settings/username" autocomplete="username">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="settings-username">New username</label>
<input
id="settings-username"
type="text"
name="username"
value="{{index .FormValues "username"}}"
minlength="3"
maxlength="50"
required
/>
<p class="hint">Usernames are unique. Pick something between 3 and 50 characters.</p>
<button type="submit">Update username</button>
</form>
</article>
</div>
<article class="form-card settings-card settings-card--full">
<header class="settings-card__header">
<h2>Change password</h2>
<p class="settings-lead">Update your account password to keep your account secure.</p>
</header>
<form class="settings-card__form" method="post" action="/settings/password" autocomplete="current-password">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="settings-current-password">Current password</label>
<input
id="settings-current-password"
type="password"
name="current_password"
required
placeholder="Enter your current password"
/>
{{with index .FormErrors "current_password"}}
<ul class="error-list">
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
{{end}}
<label for="settings-new-password">New password</label>
<input
id="settings-new-password"
type="password"
name="new_password"
minlength="8"
required
placeholder="Enter your new password"
/>
{{with index .FormErrors "new_password"}}
<ul class="error-list">
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
{{end}}
<label for="settings-confirm-password">Confirm new password</label>
<input
id="settings-confirm-password"
type="password"
name="confirm_password"
minlength="8"
required
placeholder="Confirm your new password"
/>
{{with index .FormErrors "confirm_password"}}
<ul class="error-list">
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
{{end}}
<p class="hint">Password must be at least 8 characters long. Use a combination of letters, numbers, and symbols for better security.</p>
<button type="submit">Update password</button>
</form>
</article>
<article class="form-card settings-card settings-card--danger settings-card--full settings-card--deletion">
<header class="settings-card__header">
<h2 class="settings-card__title--danger">Request account deletion</h2>
<p class="settings-lead">We'll send a confirmation link by email. Your account stays active until you confirm from that message.</p>
</header>
<form class="settings-card__form" method="post" action="/settings/delete">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<label for="settings-delete">Type DELETE to confirm</label>
<input
id="settings-delete"
type="text"
name="confirmation"
placeholder="DELETE"
autocomplete="off"
required
/>
{{with index .FormErrors "delete"}}
<ul class="error-list">
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
{{end}}
<p class="hint hint--danger">We will send the confirmation email to {{.User.Email}}. The account is deleted only after you click the link.</p>
<button type="submit" class="button button-danger">Send deletion email</button>
</form>
</article>
</div>
</section>
{{end}}

View File

@@ -0,0 +1,86 @@
package templates
import (
"html/template"
"io/fs"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTemplateParsing(t *testing.T) {
templateDir := "./"
var templateFiles []string
err := filepath.WalkDir(templateDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(path) == ".gohtml" {
templateFiles = append(templateFiles, path)
}
return nil
})
require.NoError(t, err)
tmpl := template.New("test")
tmpl = tmpl.Funcs(template.FuncMap{
"formatTime": func(any) string { return "2024-01-01" },
"eq": func(a, b any) bool { return a == b },
"ne": func(a, b any) bool { return a != b },
"len": func(s any) int { return 0 },
"range": func(s any) any { return s },
})
for _, file := range templateFiles {
t.Run(file, func(t *testing.T) {
_, err := tmpl.ParseFiles(file)
assert.NoError(t, err, "Template %s should parse without errors", file)
})
}
}
func TestTemplateSyntax(t *testing.T) {
tests := []struct {
name string
template string
shouldFail bool
}{
{
name: "valid template",
template: `{{define "test"}}<h1>{{.Title}}</h1>{{end}}`,
shouldFail: false,
},
{
name: "invalid define inside content",
template: `<div>{{define "invalid"}}content{{end}}</div>{{define "test"}}<h1>{{.Title}}</h1>{{end}}`,
shouldFail: true,
},
{
name: "unclosed template tag",
template: `{{define "test"}}<h1>{{.Title}}</h1>`,
shouldFail: true,
},
{
name: "valid nested template",
template: `{{define "parent"}}<div>{{template "child" .}}</div>{{end}}{{define "child"}}<span>{{.Content}}</span>{{end}}`,
shouldFail: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpl := template.New("test")
_, err := tmpl.Parse(tt.template)
if tt.shouldFail {
assert.Error(t, err, "Template should fail to parse")
} else {
assert.NoError(t, err, "Template should parse successfully")
}
})
}
}