feat: show manga covers everywhere a manga is referenced

Every place that surfaced a manga title used to show *just* the
title — the home page, the reader's back-to-manga link, and the
chapter upload form's manga selector. Adding the cover image
alongside makes the app feel like an actual manga library.

- Home (`/`): manga list switched from a one-line `<a>` per item to
  a responsive grid of cards (`auto-fill, minmax(140px, 1fr)`),
  each card showing the cover (with 📖 placeholder when no cover is
  set), the title (line-clamped to 2 rows), and the author.
- Reader (`/manga/[id]/chapter/[n]`): the back-to-manga link in the
  reader header now shows a 28×42 thumbnail of the manga's cover next
  to the title. Reuses the placeholder pattern for cover-less mangas.
- Upload (`/upload`): the chapter form's manga `<select>` still uses
  a native dropdown (covers don't fit in `<option>`), but a preview
  pane below the select now shows the currently-selected manga's
  cover + title + author so the user can visually confirm which
  manga they're attaching the chapter to.

No backend changes — `cover_image_path` was already in the Manga
JSON; only the frontend needed to read it.

Lockstep version bump to 0.12.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-17 10:52:57 +02:00
parent dee7f1d160
commit f0e57b0615
5 changed files with 177 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "mangalord"
version = "0.11.1"
version = "0.12.0"
edition = "2021"
[lib]

View File

@@ -1,6 +1,6 @@
{
"name": "mangalord-frontend",
"version": "0.11.1",
"version": "0.12.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { listMangas, type Manga, type MangaSort } from '$lib/api/mangas';
import { fileUrl } from '$lib/api/client';
let mangas: Manga[] = $state([]);
let search = $state('');
@@ -71,11 +72,25 @@
Showing {mangas.length} of {total}
</p>
{/if}
<ul data-testid="manga-list">
<ul class="manga-grid" data-testid="manga-list">
{#each mangas as m (m.id)}
<li>
<a href="/manga/{m.id}">{m.title}</a>
{#if m.author}<span>{m.author}</span>{/if}
<li class="manga-card">
<a href="/manga/{m.id}" class="cover-link" aria-hidden="true" tabindex="-1">
{#if m.cover_image_path}
<img
src={fileUrl(m.cover_image_path)}
alt=""
class="cover"
loading="lazy"
/>
{:else}
<div class="cover cover-placeholder">📖</div>
{/if}
</a>
<div class="meta">
<a href="/manga/{m.id}" class="title">{m.title}</a>
{#if m.author}<span class="author">{m.author}</span>{/if}
</div>
</li>
{/each}
</ul>
@@ -97,4 +112,58 @@
color: #777;
margin: 0.5rem 0;
}
.manga-grid {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1rem;
}
.manga-card {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cover-link {
display: block;
line-height: 0;
}
.cover {
width: 100%;
aspect-ratio: 2 / 3;
object-fit: cover;
border-radius: 4px;
background: #f0f0f0;
}
.cover-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
color: #999;
user-select: none;
}
.meta {
display: flex;
flex-direction: column;
min-width: 0;
}
.title {
font-weight: 600;
font-size: 0.95rem;
line-height: 1.3;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.author {
color: #777;
font-size: 0.85rem;
margin-top: 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -70,7 +70,19 @@
</svelte:head>
<nav class="reader-nav" aria-label="reader">
<a href="/manga/{manga.id}" data-testid="back-to-manga">{manga.title}</a>
<a href="/manga/{manga.id}" class="back" data-testid="back-to-manga">
{#if manga.cover_image_path}
<img
src={fileUrl(manga.cover_image_path)}
alt=""
class="back-cover"
loading="lazy"
/>
{:else}
<span class="back-cover back-cover-placeholder" aria-hidden="true">📖</span>
{/if}
<span class="back-text">{manga.title}</span>
</a>
<span class="indicator" data-testid="page-indicator">
Page {index + 1} / {pages.length}
</span>
@@ -133,9 +145,37 @@
padding-bottom: 0.5rem;
border-bottom: 1px solid #eee;
margin-bottom: 0.75rem;
gap: 0.75rem;
}
.back {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.back-cover {
width: 28px;
height: 42px;
object-fit: cover;
border-radius: 3px;
background: #f0f0f0;
flex-shrink: 0;
}
.back-cover-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: #999;
}
.back-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.indicator {
color: #555;
flex-shrink: 0;
}
.page-wrap {
display: grid;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ApiError } from '$lib/api/client';
import { ApiError, fileUrl } from '$lib/api/client';
import { createManga } from '$lib/api/mangas';
import { request } from '$lib/api/client';
import { session } from '$lib/session.svelte';
@@ -74,6 +74,7 @@
let chapterSuccess = $state<string | null>(null);
let isDragOver = $state(false);
const selectedManga = $derived(mangas.find((m) => m.id === chapterMangaId) ?? null);
const allChapterPagesValid = $derived(chapterPages.every((p) => !p.error));
const canSubmitChapter = $derived(
Boolean(chapterMangaId) &&
@@ -276,10 +277,32 @@
>
<option value="">Choose…</option>
{#each mangas as m (m.id)}
<option value={m.id}>{m.title}</option>
<option value={m.id}>
{m.title}{#if m.author}{m.author}{/if}
</option>
{/each}
</select>
</label>
{#if selectedManga}
<div class="manga-preview" data-testid="chapter-manga-preview">
{#if selectedManga.cover_image_path}
<img
src={fileUrl(selectedManga.cover_image_path)}
alt=""
class="preview-cover"
loading="lazy"
/>
{:else}
<span class="preview-cover preview-cover-placeholder" aria-hidden="true">📖</span>
{/if}
<div class="preview-meta">
<span class="preview-title">{selectedManga.title}</span>
{#if selectedManga.author}
<span class="preview-author">{selectedManga.author}</span>
{/if}
</div>
</div>
{/if}
<label>
Chapter number <span aria-hidden="true">*</span>
<input
@@ -415,6 +438,42 @@
.success {
color: #0a7d2c;
}
.manga-preview {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
border: 1px solid #eee;
border-radius: 4px;
background: #fafafa;
}
.preview-cover {
width: 48px;
height: 72px;
object-fit: cover;
border-radius: 3px;
background: #f0f0f0;
flex-shrink: 0;
}
.preview-cover-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: #999;
}
.preview-meta {
display: flex;
flex-direction: column;
min-width: 0;
}
.preview-title {
font-weight: 600;
}
.preview-author {
color: #777;
font-size: 0.85rem;
}
.drop-zone {
border: 2px dashed #aaa;
border-radius: 6px;