vereinshaus/app/src/routes/onboarding/+page.svelte
rene 39981c0d17 Migrate: PocketBase → SvelteKit + better-sqlite3 + JWT
Vollständige Migration weg von PocketBase. Neuer Stack:
- better-sqlite3 (WAL-Mode, direkte SQLite-Abfragen)
- jose (JWT HS256, 30 Tage Laufzeit)
- bcryptjs (Passwort-Hashing, cost 12)

Neue Dateien:
- src/lib/server/db.ts    → SQLite-Singleton + Schema + Helpers
- src/lib/server/auth.ts  → JWT sign/verify, bcrypt, Bearer-Token
- src/lib/user.ts         → Svelte-Store (ersetzt pb.authStore)
- src/lib/api.ts          → fetch()-Wrapper (ersetzt pb.collection())
- src/app.d.ts            → App.Locals TypeScript-Deklaration
- 30 neue API-Routes unter src/routes/api/

Entfernt:
- Abhängigkeit von pocketbase npm-Paket (bleibt im package.json bis
  alle Referenzen bereinigt sind)
- PocketBase-Container aus docker-compose.yml
- Migrations und Hooks aus Deploy-Pipeline

Docker: Ein einziger Container, SQLite-Volume unter /data/
Makefile: PocketBase-spezifische Targets entfernt
seed.js: Komplett neu für neue REST-API
2026-05-21 21:55:04 +02:00

334 lines
7.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { api } from '$lib/api';
import { user } from '$lib/user';
let schritt = $state(1);
let loading = $state(false);
let error = $state('');
// Formular
let vereinsname = $state('');
let ort = $state('');
let fertigName = $state('');
onMount(() => {
const u = get(user);
if (!u) {
goto('/login');
return;
}
if (u.verein_id) {
goto('/');
return;
}
// Vereinsname aus Registration vorbelegen
vereinsname = u.name ?? '';
});
async function vereinAnlegen() {
if (!vereinsname.trim()) return;
error = ''; loading = true;
try {
const updated = await api.post<any>('/onboarding/verein', {
name: vereinsname.trim(),
ort: ort.trim() || null,
});
// user store mit aktualisiertem verein_id updaten
const u = get(user);
if (u) user.set({ ...u, verein_id: updated.verein_id });
fertigName = vereinsname.trim();
schritt = 3;
} catch (e: unknown) {
error = e instanceof Error ? e.message : 'Fehler beim Anlegen.';
} finally {
loading = false;
}
}
</script>
<svelte:head><title>Einrichtung — vereins.haus</title></svelte:head>
<div class="shell">
<div class="logo">
<img src="/favicon.svg" alt="" width="36" height="36" />
vereins.haus
</div>
<div class="card">
<!-- Fortschritt -->
<div class="dots">
{#each [1, 2, 3] as s}
<span class="dot" class:active={schritt >= s}></span>
{/each}
</div>
{#if schritt === 1}
<!-- Willkommen -->
<div class="step">
<div class="icon-ring">👋</div>
<h1>Schön, dass du dabei bist!</h1>
<p class="beschr">
vereins.haus hilft deinem Vorstand, Mitglieder, Termine und
Beiträge zu verwalten übersichtlich, mobil und ohne Excel.
</p>
<p class="beschr">
Die Einrichtung dauert weniger als eine Minute.
</p>
<button class="btn-primary" onclick={() => (schritt = 2)}>
Los geht's →
</button>
</div>
{:else if schritt === 2}
<!-- Verein anlegen -->
<div class="step">
<h1>Dein Verein</h1>
<p class="beschr">Wie heißt dein Verein? Du kannst alle Angaben später noch ändern.</p>
<form onsubmit={(e) => { e.preventDefault(); vereinAnlegen(); }}>
<div class="field">
<label for="vname">Name des Vereins *</label>
<input
id="vname"
type="text"
bind:value={vereinsname}
placeholder="z. B. TSV Musterstadt 1923"
required
/>
</div>
<div class="field">
<label for="vort">Ort</label>
<input
id="vort"
type="text"
bind:value={ort}
placeholder="z. B. Musterstadt"
/>
</div>
{#if error}
<p class="error">{error}</p>
{/if}
<div class="btn-row">
<button type="button" class="btn-ghost" onclick={() => (schritt = 1)}>
← Zurück
</button>
<button type="submit" class="btn-primary" disabled={loading || !vereinsname.trim()}>
{loading ? 'Anlegen…' : 'Weiter →'}
</button>
</div>
</form>
</div>
{:else}
<!-- Fertig -->
<div class="step">
<div class="icon-ring success"></div>
<h1>Alles bereit!</h1>
<p class="beschr">
<strong>{fertigName}"</strong> wurde eingerichtet. Du kannst jetzt loslegen.
</p>
<ul class="naechstes">
<li>
<span class="naechstes-icon">👤</span>
<div>
<strong>Mitglieder anlegen</strong>
<span>Namen, E-Mail und IBAN für den SEPA-Einzug</span>
</div>
</li>
<li>
<span class="naechstes-icon">💶</span>
<div>
<strong>Beitragsarten definieren</strong>
<span>Jahresbeitrag, Sonderumlage mit SEPA-Export</span>
</div>
</li>
<li>
<span class="naechstes-icon">📅</span>
<div>
<strong>Termine eintragen</strong>
<span>Versammlung, Training, Ausflug</span>
</div>
</li>
</ul>
<button class="btn-primary" onclick={() => goto('/')}>
Zum Dashboard →
</button>
</div>
{/if}
</div>
</div>
<style>
.shell {
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem 1rem;
background: #f8fafc;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.2rem;
font-weight: 700;
color: #0f172a;
letter-spacing: -0.02em;
margin-bottom: 1.5rem;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 2rem 1.5rem;
width: 100%;
max-width: 420px;
}
.dots {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-bottom: 1.75rem;
}
.dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #e2e8f0;
transition: background 0.3s;
}
.dot.active { background: #1e40af; }
.step {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.icon-ring {
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
background: #e0e7ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin: 0 auto 0.25rem;
}
.icon-ring.success {
background: #dcfce7;
color: #16a34a;
font-size: 1.6rem;
font-weight: 700;
}
h1 {
font-size: 1.3rem;
font-weight: 700;
color: #1e293b;
text-align: center;
margin: 0;
}
.beschr {
font-size: 0.9rem;
color: #64748b;
text-align: center;
line-height: 1.55;
margin: 0;
}
.field {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-bottom: 0.85rem;
}
label { font-size: 0.875rem; font-weight: 500; color: #475569; }
input {
padding: 0.65rem 0.85rem;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
background: #fff;
width: 100%;
box-sizing: border-box;
transition: border-color 0.15s;
}
input:focus { outline: none; border-color: #1e40af; }
.error { color: #dc2626; font-size: 0.875rem; margin: 0; }
.btn-row {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
}
.btn-primary {
flex: 1;
padding: 0.75rem;
background: #1e40af;
color: #fff;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover:not(:disabled) { background: #1d3a9e; }
.btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-ghost {
padding: 0.75rem 1rem;
background: none;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
color: #64748b;
cursor: pointer;
}
.naechstes {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
margin: 0.25rem 0;
}
.naechstes li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid #f1f5f9;
}
.naechstes li:last-child { border-bottom: none; }
.naechstes-icon { font-size: 1.1rem; flex-shrink: 0; }
.naechstes div {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.naechstes strong { font-size: 0.875rem; color: #1e293b; }
.naechstes span { font-size: 0.78rem; color: #94a3b8; }
</style>