Initialize repo and add session changes (2025-09-08)

This commit is contained in:
Auto Commit
2025-09-08 14:03:43 +02:00
commit 44ab0a534b
327 changed files with 40952 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { NavLink, useParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { GalleryHorizontal, Home, Trophy } from 'lucide-react';
function TabLink({ to, children }: { to: string; children: React.ReactNode }) {
return (
<NavLink to={to} className={({ isActive }) => (isActive ? 'text-foreground' : 'text-muted-foreground')}>
{children}
</NavLink>
);
}
export default function BottomNav() {
const { slug } = useParams();
const base = `/e/${encodeURIComponent(slug ?? 'demo')}`;
return (
<div className="fixed inset-x-0 bottom-0 z-20 border-t bg-white/90 px-3 py-2 backdrop-blur dark:bg-black/40">
<div className="mx-auto flex max-w-md items-center justify-between">
<TabLink to={`${base}`}>
<Button variant="ghost" size="sm" className="flex flex-col gap-1">
<Home className="h-5 w-5" /> <span className="text-xs">Start</span>
</Button>
</TabLink>
<TabLink to={`${base}/gallery`}>
<Button variant="ghost" size="sm" className="flex flex-col gap-1">
<GalleryHorizontal className="h-5 w-5" /> <span className="text-xs">Galerie</span>
</Button>
</TabLink>
<TabLink to={`${base}/achievements`}>
<Button variant="ghost" size="sm" className="flex flex-col gap-1">
<Trophy className="h-5 w-5" /> <span className="text-xs">Erfolge</span>
</Button>
</TabLink>
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
export type GalleryFilter = 'latest' | 'popular' | 'mine';
export default function FiltersBar({ value, onChange }: { value: GalleryFilter; onChange: (v: GalleryFilter) => void }) {
return (
<div className="mb-3 flex items-center justify-between">
<ToggleGroup type="single" value={value} onValueChange={(v) => v && onChange(v as GalleryFilter)}>
<ToggleGroupItem value="latest">Neueste</ToggleGroupItem>
<ToggleGroupItem value="popular">Beliebt</ToggleGroupItem>
<ToggleGroupItem value="mine">Meine</ToggleGroupItem>
</ToggleGroup>
</div>
);
}

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
import { Settings } from 'lucide-react';
export default function Header({ title = '' }: { title?: string }) {
return (
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40">
<div className="font-semibold">{title}</div>
<div className="flex items-center gap-2">
<AppearanceToggleDropdown />
<SettingsSheet />
</div>
</div>
);
}
function SettingsSheet() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="h-9 w-9 rounded-md">
<Settings className="h-5 w-5" />
<span className="sr-only">Einstellungen öffnen</span>
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-80 sm:w-96">
<SheetHeader>
<SheetTitle>Einstellungen</SheetTitle>
</SheetHeader>
<div className="mt-4 space-y-4">
<div>
<div className="text-sm font-medium">Darstellung</div>
<div className="text-sm text-muted-foreground">Hell, Dunkel oder System</div>
<div className="mt-2">
<AppearanceToggleDropdown />
</div>
</div>
<div>
<div className="text-sm font-medium">Cache</div>
<ClearCacheButton />
</div>
<div>
<div className="text-sm font-medium">Rechtliches</div>
<ul className="mt-2 list-disc pl-5 text-sm">
<li><a href="/legal/imprint" className="underline">Impressum</a></li>
<li><a href="/legal/privacy" className="underline">Datenschutz</a></li>
<li><a href="/legal/terms" className="underline">AGB</a></li>
</ul>
</div>
</div>
</SheetContent>
</Sheet>
);
}
function ClearCacheButton() {
const [busy, setBusy] = React.useState(false);
const [done, setDone] = React.useState(false);
async function clearAll() {
setBusy(true); setDone(false);
try {
// Clear CacheStorage
if ('caches' in window) {
const keys = await caches.keys();
await Promise.all(keys.map((k) => caches.delete(k)));
}
// Clear known IndexedDB dbs (best-effort)
if ('indexedDB' in window) {
try { await new Promise((res, rej) => { const r = indexedDB.deleteDatabase('upload-queue'); r.onsuccess=()=>res(null); r.onerror=()=>res(null); }); } catch {}
}
setDone(true);
} finally {
setBusy(false);
setTimeout(() => setDone(false), 2500);
}
}
return (
<div className="mt-2">
<Button variant="secondary" onClick={clearAll} disabled={busy}>
{busy ? 'Leere Cache…' : 'Cache leeren'}
</Button>
{done && <div className="mt-2 text-xs text-muted-foreground">Cache gelöscht.</div>}
</div>
);
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
type Toast = { id: number; text: string; type?: 'success'|'error' };
const Ctx = React.createContext<{ push: (t: Omit<Toast,'id'>) => void } | null>(null);
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [list, setList] = React.useState<Toast[]>([]);
const push = React.useCallback((t: Omit<Toast,'id'>) => {
const id = Date.now() + Math.random();
setList((arr) => [...arr, { id, ...t }]);
setTimeout(() => setList((arr) => arr.filter((x) => x.id !== id)), 3000);
}, []);
React.useEffect(() => {
const onEvt = (e: any) => push(e.detail);
window.addEventListener('guest-toast', onEvt);
return () => window.removeEventListener('guest-toast', onEvt);
}, [push]);
return (
<Ctx.Provider value={{ push }}>
{children}
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-50 flex justify-center px-4">
<div className="flex w-full max-w-sm flex-col gap-2">
{list.map((t) => (
<div key={t.id} className={`pointer-events-auto rounded-md border p-3 shadow-sm ${t.type==='error'?'border-red-300 bg-red-50 text-red-700':'border-green-300 bg-green-50 text-green-700'}`}>
{t.text}
</div>
))}
</div>
</div>
</Ctx.Provider>
);
}
export function useToast() {
const ctx = React.useContext(Ctx);
if (!ctx) throw new Error('ToastProvider missing');
return ctx;
}