fix(frontend): surface upload errors, push ban/lock, route guards (WS8)
H11: UploadSheet now honors the modal contract — role=dialog/aria-modal + accessible name, a focus-trap/Escape/focus-restore $effect mirroring ContextSheet, and inert + pointer-events-none when closed so its controls leave the tab order and AT tree. H12: a layout-level $effect toasts each upload-queue item the first time it transitions to 'error', so a banned/locked/over-quota guest is actually told why their photo didn't post instead of the composer silently closing. M10: api.ts now redirects to /join after clearAuth() on a 401 (guarded against loops on the auth screens), so a 401 no longer strands the user on a dead page. M11: ban and event-lock are pushed to guests. A new uploadsLocked store is seeded from /me/context (now exposes uploads_locked) and kept live by event-closed/opened SSE; the feed shows a banner and the FAB is disabled when locked. A user-banned SSE event force-logs-out the targeted user. M19: feed and export pages track a distinct loadError and render an "Erneut versuchen" retry card instead of collapsing a fetch failure into "empty" / "not released". Light-mode empty-state text bumped to gray-500 for contrast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import '../app.css';
|
||||
import { initAuth, getToken, getUserId, clearPin } from '$lib/auth';
|
||||
import { initAuth, getToken, getUserId, clearPin, clearAuth } from '$lib/auth';
|
||||
import { initTheme } from '$lib/theme-store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import BottomNav from '$lib/components/BottomNav.svelte';
|
||||
import UploadSheet from '$lib/components/UploadSheet.svelte';
|
||||
@@ -14,12 +15,28 @@
|
||||
import { refreshQuota } from '$lib/quota-store';
|
||||
import { onSseEvent } from '$lib/sse';
|
||||
import { api } from '$lib/api';
|
||||
import { toast } from '$lib/toast-store';
|
||||
import { uploadsLocked } from '$lib/event-state-store';
|
||||
import type { MeContextDto } from '$lib/types';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let unsubs: Array<() => void> = [];
|
||||
|
||||
// H12: surface upload failures. The queue marks failed items 'error' in
|
||||
// IndexedDB but nothing rendered them, so a banned/locked/over-quota guest saw
|
||||
// the composer close to a normal feed and assumed success. Toast each item the
|
||||
// first time it transitions to 'error'.
|
||||
let toastedErrors = new Set<string>();
|
||||
$effect(() => {
|
||||
for (const item of $queueItems) {
|
||||
if (item.status === 'error' && !toastedErrors.has(item.id)) {
|
||||
toastedErrors.add(item.id);
|
||||
toast(item.error ?? 'Upload fehlgeschlagen.', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Slim progress bar: ratio of completed items to total, shown while processing.
|
||||
let progressPct = $derived.by(() => {
|
||||
const total = $queueItems.length;
|
||||
@@ -39,6 +56,7 @@
|
||||
try {
|
||||
const ctx = await api.get<MeContextDto>('/me/context');
|
||||
privacyNote.set(ctx.privacy_note);
|
||||
uploadsLocked.set(ctx.uploads_locked);
|
||||
} catch {
|
||||
// Cross-cutting hydration on boot — failure is non-fatal; users without
|
||||
// a session land on /join anyway, and the per-page mount will retry.
|
||||
@@ -61,7 +79,26 @@
|
||||
} catch {
|
||||
// Malformed payload — discard; nothing actionable for the user.
|
||||
}
|
||||
})
|
||||
}),
|
||||
// M11: a host banned someone. If it's us, the session has already been
|
||||
// revoked server-side — drop local auth and send us to /join so the UI
|
||||
// doesn't keep pretending we're signed in.
|
||||
onSseEvent('user-banned', (data) => {
|
||||
try {
|
||||
const payload = JSON.parse(data) as { user_id: string };
|
||||
if (payload.user_id === getUserId()) {
|
||||
clearAuth();
|
||||
toast('Du wurdest vom Event entfernt.', 'error');
|
||||
void goto('/join');
|
||||
}
|
||||
} catch {
|
||||
/* malformed — ignore */
|
||||
}
|
||||
}),
|
||||
// M11: reflect event lock state live so the feed/upload pages can toggle
|
||||
// their "Uploads gesperrt" banner and disable the FAB.
|
||||
onSseEvent('event-closed', () => uploadsLocked.set(true)),
|
||||
onSseEvent('event-opened', () => uploadsLocked.set(false))
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user