Files
ebooks/views/pages.templ
T
arnefandCopilot be3dfaeb47 Allow selecting and uploading multiple EPUB files at once
- views/pages.templ: upload input now has "multiple" attribute; page
  title/heading updated to plural ("Bücher hochladen"); templ regenerated
- internal/web/handlers.go: uploadSubmit now reads all files from
  r.MultipartForm.File["book"] instead of a single r.FormFile, saving
  each via a new saveUpload helper; partial failures are reported per
  filename while successfully saved files are kept

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-15 14:23:05 +02:00

178 lines
5.0 KiB
Templ

package views
import (
"strings"
"github.com/arnef/ebooks/internal/library"
)
templ Layout(title string, backHref string) {
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{ title }</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<header class="header">
<div class="header-bar">
if backHref != "" {
<a class="header-back" href={ backHref } aria-label="Zurück"></a>
}
<h1><a href="/">eBook Library</a></h1>
<form method="post" action="/logout" style="margin-left:auto">
<button type="submit" class="btn-logout">Abmelden</button>
</form>
</div>
</header>
<main class="container">
{ children... }
</main>
</body>
</html>
}
templ LoginLayout(title string) {
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{ title }</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<main class="container">
{ children... }
</main>
</body>
</html>
}
templ LoginPage(errMsg string) {
@LoginLayout("Anmelden") {
<section class="login-box">
<h2>Anmelden</h2>
if errMsg != "" {
<p class="error">{ errMsg }</p>
}
<form method="post" action="/login" class="login-form">
<label for="username">Benutzername</label>
<input id="username" type="text" name="username" required autofocus />
<label for="password">Passwort</label>
<input id="password" type="password" name="password" required />
<button type="submit" class="btn">Anmelden</button>
</form>
</section>
}
}
templ UploadPage(errMsg string) {
@Layout("Bücher hochladen", "/") {
<section class="page-head">
<h2>Bücher hochladen</h2>
</section>
if errMsg != "" {
<p class="error">{ errMsg }</p>
}
<form method="post" action="/upload" enctype="multipart/form-data" class="upload-form">
<label for="book">EPUB(s) auswählen</label>
<input id="book" type="file" name="book" accept=".epub" multiple required />
<button type="submit" class="btn">Hochladen</button>
</form>
}
}
templ BookCover(book library.Book, detail bool) {
if book.HasCover {
<img class={ coverClass(detail) } src={ "/cover/" + book.ID } alt={ "Cover von " + book.Title } loading="lazy" />
} else {
<div class={ placeholderClass(detail) } aria-hidden="true">
<span>{ strings.ToUpper(book.Format) }</span>
</div>
}
}
templ IndexPage(books []library.Book, canUpload bool) {
@Layout("Bibliothek", "") {
<section class="page-head">
<h2>Meine Bücher</h2>
<p class="page-subtitle">Titel, Autor und Cover auf einen Blick.</p>
if canUpload {
<a class="btn" href="/upload">Buch hochladen</a>
}
</section>
if len(books) == 0 {
<p>Keine eBooks gefunden. Lege EPUB-Dateien im Ordner <code>books/</code> ab.</p>
} else {
<ul class="book-list">
for _, b := range books {
<li class="book-item">
<a class="book-card" href={ "/book/" + b.ID }>
<div class="book-card-cover">
@BookCover(b, false)
</div>
<div class="book-card-body">
<strong class="book-title" title={ b.Title }>{ truncateTitle(b.Title, 60) }</strong>
<span class="book-author">{ truncateTitle(b.Author, 40) }</span>
<span class="format">{ b.Format }</span>
</div>
</a>
</li>
}
</ul>
}
}
}
templ BookPage(book library.Book) {
@Layout(book.Title, "/") {
<article class="book-detail">
<div class="book-detail-cover">
@BookCover(book, true)
</div>
<div class="book-detail-body">
<h2>{ book.Title }</h2>
<p class="book-author-detail">{ book.Author }</p>
<dl class="book-meta">
<div>
<dt>Datei</dt>
<dd>{ book.Filename }</dd>
</div>
<div>
<dt>Format</dt>
<dd>{ book.Format }</dd>
</div>
</dl>
<p><a class="btn" href={ "/download/" + book.ID }>Auf Tolino herunterladen</a></p>
</div>
</article>
}
}
func coverClass(detail bool) string {
if detail {
return "book-cover book-cover-detail"
}
return "book-cover"
}
func placeholderClass(detail bool) string {
if detail {
return "book-cover-placeholder book-cover-placeholder-detail"
}
return "book-cover-placeholder"
}
// truncateTitle kürzt lange Titel für die Kartenansicht, damit lange,
// nicht umbrechbare Titel das Grid-Layout nicht sprengen. Der volle Titel
// bleibt im Detail-Seiten-Titel und im alt-Text des Covers erhalten.
func truncateTitle(title string, maxLen int) string {
r := []rune(title)
if len(r) <= maxLen {
return title
}
return string(r[:maxLen]) + "…"
}