chore: initial project scaffold
Set up Mangalord with a Rust/axum backend, SvelteKit frontend, Postgres, and Docker Compose deployment. Establishes the architecture and TDD patterns the project will extend: - Hexagonal-ish backend layering (domain / repo / storage / api) with a pluggable Storage trait (LocalStorage today, S3 as a future impl). - Initial migration: users, mangas, chapters, bookmarks. - Vertical slice for mangas (list, search, create, get) with #[sqlx::test] integration coverage and storage unit tests. - SvelteKit frontend using Svelte 5 runes, typed API client, Vitest unit tests and Playwright e2e with route mocking. - CLAUDE.md documenting layering, TDD/git/SemVer workflow rules, and extension points (tags, fulltext search, OCR, S3, auth). - Project-scoped .claude/settings.json with permission allowlist for the toolchain (git, cargo, npm/vite, docker, psql, gh, doc fetches). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
30
frontend/src/routes/+layout.svelte
Normal file
30
frontend/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Mangalord</a>
|
||||
<a href="/upload">Upload</a>
|
||||
<a href="/bookmarks">Bookmarks</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
nav a {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
main {
|
||||
padding: 1rem;
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
57
frontend/src/routes/+page.svelte
Normal file
57
frontend/src/routes/+page.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { listMangas, type Manga } from '$lib/api/mangas';
|
||||
|
||||
let mangas: Manga[] = $state([]);
|
||||
let search = $state('');
|
||||
let loading = $state(true);
|
||||
let error: string | null = $state(null);
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
mangas = await listMangas({ search: search.trim() || undefined });
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
<h1>Mangas</h1>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
load();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={search}
|
||||
placeholder="Search by title or author"
|
||||
data-testid="search-input"
|
||||
/>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
{#if loading}
|
||||
<p data-testid="loading">Loading…</p>
|
||||
{:else if error}
|
||||
<p data-testid="error" role="alert">{error}</p>
|
||||
{:else if mangas.length === 0}
|
||||
<p data-testid="empty">No mangas yet. <a href="/upload">Upload one</a>.</p>
|
||||
{:else}
|
||||
<ul 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>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user