2 Commits

Author SHA1 Message Date
MechaCat02
a33b2c4d41 fix: serialize UserRole as lowercase in JWT
Add #[serde(rename_all = "lowercase")] to UserRole enum so JWT payloads
contain lowercase role strings ("guest", "host", "admin") matching what
the frontend route guards and getRole() comparisons expect.

Without this, JWTs contained PascalCase ("Guest", "Host", "Admin")
causing hosts and admins to be redirected to /join instead of their
dashboards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 14:11:44 +02:00
MechaCat02
87b5aff478 feat: implement onboarding guide and HTML export guide
Add 4-step dismissible onboarding overlay shown on first feed visit
(welcome, upload, hashtags, PIN importance). Dismissed state persisted
in localStorage under eventsnap_guide_seen. Step indicator dots and
skip/continue buttons included.

Update HTML export guide modal to persist the eventsnap_html_guide_seen
flag: first download shows the instructions modal; subsequent clicks go
straight to download without interruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:13:14 +02:00
4 changed files with 98 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ use sqlx::PgPool;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[serde(rename_all = "lowercase")]
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
pub enum UserRole {
Guest,

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { browser } from '$app/environment';
const GUIDE_SEEN_KEY = 'eventsnap_guide_seen';
let visible = $state(false);
let step = $state(0);
const steps = [
{
icon: '📸',
title: 'Willkommen bei EventSnap!',
body: 'Hier kannst du Fotos und Videos mit allen Gästen teilen — in Echtzeit, ganz ohne App-Store.'
},
{
icon: '⬆️',
title: 'Fotos & Videos hochladen',
body: 'Tippe oben auf „Hochladen", um Fotos aus deiner Galerie oder direkt mit der Kamera aufzunehmen. Mehrere Dateien auf einmal sind kein Problem!'
},
{
icon: '#️⃣',
title: 'Hashtags nutzen',
body: 'Füge in deiner Bildunterschrift #hashtags ein, um Fotos zu gruppieren — z.B. #tanz, #buffet oder #reden. Du kannst danach filtern.'
},
{
icon: '🔑',
title: 'Deinen PIN merken!',
body: 'Du hast beim Registrieren einen 4-stelligen PIN erhalten. Speichere ihn — du brauchst ihn, um dein Konto auf einem anderen Gerät wiederherzustellen. Er ist immer unter „Mein Konto" zu finden.'
}
];
if (browser && !localStorage.getItem(GUIDE_SEEN_KEY)) {
visible = true;
}
function next() {
if (step < steps.length - 1) {
step++;
} else {
dismiss();
}
}
function dismiss() {
if (browser) localStorage.setItem(GUIDE_SEEN_KEY, '1');
visible = false;
}
</script>
{#if visible}
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-end justify-center bg-black/60 sm:items-center">
<div class="w-full max-w-sm rounded-t-3xl bg-white p-6 shadow-2xl sm:rounded-2xl">
<!-- Step indicator -->
<div class="mb-5 flex justify-center gap-1.5">
{#each steps as _, i}
<div class="h-1.5 rounded-full transition-all {i === step ? 'w-6 bg-blue-600' : 'w-1.5 bg-gray-200'}"></div>
{/each}
</div>
<!-- Content -->
<div class="mb-6 text-center">
<div class="mb-3 text-5xl">{steps[step].icon}</div>
<h2 class="mb-2 text-xl font-bold text-gray-900">{steps[step].title}</h2>
<p class="text-sm leading-relaxed text-gray-600">{steps[step].body}</p>
</div>
<!-- Buttons -->
<div class="flex gap-2">
<button
onclick={dismiss}
class="flex-1 rounded-xl border border-gray-200 py-3 text-sm text-gray-500 hover:bg-gray-50"
>
Überspringen
</button>
<button
onclick={next}
class="flex-1 rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white hover:bg-blue-700"
>
{step < steps.length - 1 ? 'Weiter' : 'Los geht\'s!'}
</button>
</div>
</div>
</div>
{/if}

View File

@@ -16,6 +16,8 @@
html: JobStatus;
}
const HTML_GUIDE_KEY = 'eventsnap_html_guide_seen';
let status = $state<ExportStatus | null>(null);
let showHtmlGuide = $state(false);
let loading = $state(true);
@@ -75,10 +77,15 @@
}
function downloadHtml() {
showHtmlGuide = true;
if (localStorage.getItem(HTML_GUIDE_KEY)) {
window.location.href = '/api/v1/export/html';
} else {
showHtmlGuide = true;
}
}
function confirmHtmlDownload() {
localStorage.setItem(HTML_GUIDE_KEY, '1');
showHtmlGuide = false;
window.location.href = '/api/v1/export/html';
}

View File

@@ -7,6 +7,7 @@
import FeedGrid from '$lib/components/FeedGrid.svelte';
import HashtagChips from '$lib/components/HashtagChips.svelte';
import LightboxModal from '$lib/components/LightboxModal.svelte';
import OnboardingGuide from '$lib/components/OnboardingGuide.svelte';
import type { FeedUpload, FeedResponse, HashtagCount } from '$lib/types';
let uploads = $state<FeedUpload[]>([]);
@@ -223,3 +224,6 @@
onlike={handleLike}
/>
{/if}
<!-- First-visit onboarding guide -->
<OnboardingGuide />