This updates gallery handling to support video playback with generated poster thumbnails, adds authenticated admin upload/delete flows, and improves dev/runtime behavior including reliable thumbnail generation and media-safe response handling. Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
4.4 KiB
Plaintext
157 lines
4.4 KiB
Plaintext
package templates
|
|
|
|
import (
|
|
"fmt"
|
|
"technical.kiwi/website/internal/gallery"
|
|
)
|
|
|
|
templ AdminLogin(errMsg string) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>Admin login — Technical Kiwi</title>
|
|
<link rel="stylesheet" href="/static/style.css"/>
|
|
</head>
|
|
<body class="admin-body">
|
|
<main class="admin-login">
|
|
<h1>Gallery admin</h1>
|
|
if errMsg != "" {
|
|
<div class="alert alert-error" role="alert"><p>{ errMsg }</p></div>
|
|
}
|
|
<form method="post" action="/admin/login" class="contact-form">
|
|
<div class="form-row">
|
|
<label for="username">Username</label>
|
|
<input type="text" id="username" name="username" required autocomplete="username"/>
|
|
</div>
|
|
<div class="form-row">
|
|
<label for="password">Password</label>
|
|
<input type="password" id="password" name="password" required autocomplete="current-password"/>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Sign in</button>
|
|
</form>
|
|
<p class="admin-back"><a href="/">← Back to site</a></p>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
templ AdminDashboard(images []gallery.Image, albums []string, flash string) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>Gallery admin — Technical Kiwi</title>
|
|
<link rel="stylesheet" href="/static/style.css"/>
|
|
<script src="/static/htmx.min.js" defer></script>
|
|
</head>
|
|
<body class="admin-body" hx-boost="false">
|
|
<header class="admin-header">
|
|
<h1>Gallery admin</h1>
|
|
<form method="post" action="/admin/logout">
|
|
<button type="submit" class="btn btn-ghost">Sign out</button>
|
|
</form>
|
|
</header>
|
|
<main class="admin-main">
|
|
<div id="admin-flash">
|
|
if flash != "" {
|
|
@AdminFlash(flash)
|
|
}
|
|
</div>
|
|
<section class="admin-panel">
|
|
<h2>Upload image</h2>
|
|
<form
|
|
class="contact-form"
|
|
method="post"
|
|
action="/admin/upload"
|
|
enctype="multipart/form-data"
|
|
hx-post="/admin/upload"
|
|
hx-target="#admin-images"
|
|
hx-swap="innerHTML"
|
|
hx-encoding="multipart/form-data"
|
|
>
|
|
<div class="form-row">
|
|
<label for="album">Album</label>
|
|
<select id="album" name="album" required>
|
|
for _, a := range albums {
|
|
<option value={ a }>{ gallery.AlbumLabel(a) }</option>
|
|
}
|
|
<option value="new">+ New album</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row">
|
|
<label for="album_new">New album name</label>
|
|
<input type="text" id="album_new" name="album_new" placeholder="e.g. my-project" pattern="[a-z0-9][a-z0-9_-]{0,63}"/>
|
|
</div>
|
|
<div class="form-row">
|
|
<label for="image">JPEG image</label>
|
|
<input type="file" id="image" name="image" accept="image/jpeg,.jpg,.jpeg" required/>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Upload</button>
|
|
</form>
|
|
</section>
|
|
<section class="admin-panel">
|
|
<h2>Images ({ fmt.Sprintf("%d", len(images)) })</h2>
|
|
<div id="admin-images">
|
|
@AdminImageTable(images)
|
|
</div>
|
|
</section>
|
|
<p class="admin-back"><a href="/">← View public site</a></p>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
templ AdminFlash(msg string) {
|
|
if msg != "" {
|
|
<div class="alert alert-error" role="alert"><p>{ msg }</p></div>
|
|
}
|
|
}
|
|
|
|
templ AdminImageTable(images []gallery.Image) {
|
|
if len(images) == 0 {
|
|
<p class="gallery-empty">No images yet.</p>
|
|
} else {
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Preview</th>
|
|
<th>Path</th>
|
|
<th>Album</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, img := range images {
|
|
<tr>
|
|
<td class="admin-thumb">
|
|
if img.ThumbURL != "" {
|
|
<img src={ img.ThumbURL } alt="" loading="lazy"/>
|
|
}
|
|
</td>
|
|
<td><code>{ img.RelPath }</code></td>
|
|
<td>{ gallery.AlbumLabel(img.Album) }</td>
|
|
<td>
|
|
<form
|
|
method="post"
|
|
action="/admin/delete"
|
|
hx-post="/admin/delete"
|
|
hx-target="#admin-images"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="Delete this image and its thumbnails?"
|
|
>
|
|
<input type="hidden" name="path" value={ img.RelPath }/>
|
|
<button type="submit" class="btn btn-ghost admin-delete">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}
|