Add TypeScript migration, image resizing, media upload UX, and multimedia support
All checks were successful
Deploy to NAS / deploy (push) Successful in 2m20s

- Migrate static JS to TypeScript (static-ts/ → compiled to internal/api/static/)
- Add image resizing on upload: JPEG/PNG/WebP scaled to max 1920px at quality 80
- Extract shared upload logic into upload.go (saveUpload, saveResizedImage, saveResizedWebP)
- Add POST /media endpoint for drag-drop/paste media uploads with markdown ref return
- Add background music player with video/audio coordination (autoplay.ts)
- Add global nav, public feed, hashtags, visibility, Markdown rendering for entries
- Add Dockerfile stage for TypeScript compilation (static-ts-builder)
- Add goldmark, disintegration/imaging, golang.org/x/image dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-04-09 23:03:04 +02:00
parent 8eef933573
commit 17186e7b64
24 changed files with 890 additions and 179 deletions

View File

@@ -20,6 +20,7 @@
</nav>
{{block "content" .}}{{end}}
{{block "scripts" .}}{{end}}
<script src="/static/autoplay.js"></script>
</body>
</html>
{{end}}

View File

@@ -15,7 +15,14 @@
</select>
</div>
<input type="text" name="title" placeholder="Überschrift">
<textarea name="description" rows="4" placeholder="Beschreibung"></textarea>
<div class="editor-wrap">
<textarea name="description" rows="6" placeholder="Beschreibung — Markdown unterstützt&#10;Medien: per Drag & Drop oder Einfügen (Strg+V)"></textarea>
<div class="editor-bar">
<button type="button" class="media-picker">&#128206; Datei anhängen</button>
<span class="upload-status"></span>
<input type="file" class="media-file-input" multiple accept="image/*,video/*,audio/*" style="display:none">
</div>
</div>
<div class="gps-row">
<input type="number" name="lat" id="entry-lat" step="any" placeholder="Breite">
<input type="number" name="lon" id="entry-lon" step="any" placeholder="Länge">
@@ -23,8 +30,6 @@
</div>
<small id="gps-status"></small>
<input type="text" name="hashtags" placeholder="Hashtags (kommagetrennt)">
<input type="file" name="images" multiple accept="image/*" id="image-input">
<div id="image-preview" class="image-preview"></div>
<button type="submit">Speichern</button>
</form>
@@ -38,15 +43,21 @@
<a href="/entries/{{.EntryID}}/edit" class="entry-edit">bearbeiten</a>
</div>
{{if .Title}}<div class="entry-title">{{.Title}}</div>{{end}}
{{if .Description}}<div class="entry-desc">{{.Description}}</div>{{end}}
{{if .Description}}<div class="entry-desc">{{markdown .Description}}</div>{{end}}
{{if .Hashtags}}<div class="hashtags">{{range .Hashtags}}<span class="tag">#{{.}}</span> {{end}}</div>{{end}}
{{if .Images}}
<div class="entry-images">
{{range .Images}}
{{if isVideo .MimeType}}
<video src="/uploads/{{.Filename}}" controls class="media-embed"></video>
{{else if isAudio .MimeType}}
<audio src="/uploads/{{.Filename}}" controls class="media-audio"></audio>
{{else}}
<a href="/uploads/{{.Filename}}" target="_blank">
<img src="/uploads/{{.Filename}}" alt="{{.OriginalName}}" class="thumb">
</a>
{{end}}
{{end}}
</div>
{{end}}
</div>
@@ -96,6 +107,7 @@
{{define "scripts"}}
<script src="/static/day.js"></script>
<script src="/static/editor.js"></script>
{{end}}
{{template "base" .}}

View File

@@ -14,7 +14,14 @@
</select>
</div>
<input type="text" name="title" placeholder="Überschrift" value="{{.Entry.Title}}">
<textarea name="description" rows="4" placeholder="Beschreibung">{{.Entry.Description}}</textarea>
<div class="editor-wrap">
<textarea name="description" rows="6" placeholder="Beschreibung — Markdown unterstützt&#10;Medien: per Drag & Drop oder Einfügen (Strg+V)">{{.Entry.Description}}</textarea>
<div class="editor-bar">
<button type="button" class="media-picker">&#128206; Datei anhängen</button>
<span class="upload-status"></span>
<input type="file" class="media-file-input" multiple accept="image/*,video/*,audio/*" style="display:none">
</div>
</div>
<div class="gps-row">
<input type="number" name="lat" id="entry-lat" step="any" placeholder="Breite"{{if .Entry.Lat}} value="{{printf "%.6f" (deref .Entry.Lat)}}"{{end}}>
<input type="number" name="lon" id="entry-lon" step="any" placeholder="Länge"{{if .Entry.Lon}} value="{{printf "%.6f" (deref .Entry.Lon)}}"{{end}}>
@@ -23,16 +30,22 @@
<small id="gps-status"></small>
<input type="text" name="hashtags" placeholder="Hashtags (kommagetrennt)" value="{{join .Entry.Hashtags ", "}}">
{{if .Entry.Images}}
<div class="entry-images" style="margin-bottom:.5rem">
<div class="media-refs">
{{range .Entry.Images}}
<a href="/uploads/{{.Filename}}" target="_blank">
<div class="media-ref-row">
{{if isVideo .MimeType}}
<code class="media-ref-code">![{{.OriginalName}}](/uploads/{{.Filename}})</code>
{{else if isAudio .MimeType}}
<code class="media-ref-code">[{{.OriginalName}}](/uploads/{{.Filename}})</code>
{{else}}
<img src="/uploads/{{.Filename}}" alt="{{.OriginalName}}" class="thumb">
</a>
<code class="media-ref-code">![{{.OriginalName}}](/uploads/{{.Filename}})</code>
{{end}}
<button type="button" class="btn-insert" data-ref="{{if isVideo .MimeType}}![{{.OriginalName}}](/uploads/{{.Filename}}){{else if isAudio .MimeType}}[{{.OriginalName}}](/uploads/{{.Filename}}){{else}}![{{.OriginalName}}](/uploads/{{.Filename}}){{end}}">↩ einfügen</button>
</div>
{{end}}
</div>
{{end}}
<input type="file" name="images" multiple accept="image/*" id="image-input">
<div id="image-preview" class="image-preview"></div>
<button type="submit">Speichern</button>
</form>
</main>
@@ -40,6 +53,7 @@
{{define "scripts"}}
<script src="/static/day.js"></script>
<script src="/static/editor.js"></script>
{{end}}
{{template "base" .}}

View File

@@ -12,14 +12,22 @@
{{range .Entries}}
<article class="pub-card">
{{if .Images}}
<a href="/uploads/{{(index .Images 0).Filename}}" target="_blank">
<img class="pub-cover" src="/uploads/{{(index .Images 0).Filename}}" alt="">
{{with (index .Images 0)}}
{{if isVideo .MimeType}}
<video src="/uploads/{{.Filename}}" controls class="media-embed"></video>
{{else if isAudio .MimeType}}
<audio src="/uploads/{{.Filename}}" controls class="media-audio"></audio>
{{else}}
<a href="/uploads/{{.Filename}}" target="_blank">
<img class="pub-cover" src="/uploads/{{.Filename}}" alt="">
</a>
{{end}}
{{end}}
{{end}}
<div class="pub-body">
<small class="pub-meta">{{.EntryDate}} · {{.EntryTime}}</small>
{{if .Title}}<strong class="pub-title">{{.Title}}</strong>{{end}}
{{if .Description}}<p class="pub-desc">{{.Description}}</p>{{end}}
{{if .Description}}<div class="pub-desc">{{markdown .Description}}</div>{{end}}
{{if .Hashtags}}<div class="pub-tags">{{range .Hashtags}}<span class="tag">#{{.}}</span> {{end}}</div>{{end}}
</div>
</article>