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
334 lines
7.1 KiB
Svelte
334 lines
7.1 KiB
Svelte
<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>
|