feat: implement tenant OAuth flow and guest achievements
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Page } from './_util';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import { readGuestName } from '../context/GuestIdentityContext';
|
||||
|
||||
export default function LandingPage() {
|
||||
const nav = useNavigate();
|
||||
const [slug, setSlug] = React.useState('');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [slug, setSlug] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [scanner, setScanner] = useState<Html5Qrcode | null>(null);
|
||||
|
||||
async function join() {
|
||||
const s = slug.trim();
|
||||
async function join(eventSlug?: string) {
|
||||
const s = (eventSlug ?? slug).trim();
|
||||
if (!s) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -22,30 +27,131 @@ export default function LandingPage() {
|
||||
setError('Event nicht gefunden oder geschlossen.');
|
||||
return;
|
||||
}
|
||||
nav(`/e/${encodeURIComponent(s)}`);
|
||||
const storedName = readGuestName(s);
|
||||
if (!storedName) {
|
||||
nav(`/setup/${encodeURIComponent(s)}`);
|
||||
} else {
|
||||
nav(`/e/${encodeURIComponent(s)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setError('Netzwerkfehler. Bitte später erneut versuchen.');
|
||||
console.error('Join request failed', e);
|
||||
setError('Netzwerkfehler. Bitte spaeter erneut versuchen.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const qrConfig = { fps: 10, qrbox: { width: 250, height: 250 } } as const;
|
||||
|
||||
async function startScanner() {
|
||||
if (scanner) {
|
||||
try {
|
||||
await scanner.start({ facingMode: 'environment' }, qrConfig, onScanSuccess, () => undefined);
|
||||
setIsScanning(true);
|
||||
} catch (err) {
|
||||
console.error('Scanner start failed', err);
|
||||
setError('Kamera-Zugriff fehlgeschlagen. Bitte erlaube den Zugriff und versuche es erneut.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newScanner = new Html5Qrcode('qr-reader');
|
||||
setScanner(newScanner);
|
||||
await newScanner.start({ facingMode: 'environment' }, qrConfig, onScanSuccess, () => undefined);
|
||||
setIsScanning(true);
|
||||
} catch (err) {
|
||||
console.error('Scanner initialisation failed', err);
|
||||
setError('Kamera-Zugriff fehlgeschlagen. Bitte erlaube den Zugriff und versuche es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
if (!scanner) {
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
scanner
|
||||
.stop()
|
||||
.then(() => {
|
||||
setIsScanning(false);
|
||||
})
|
||||
.catch((err) => console.error('Scanner stop failed', err));
|
||||
}
|
||||
|
||||
async function onScanSuccess(decodedText: string) {
|
||||
const value = decodedText.trim();
|
||||
if (!value) return;
|
||||
await join(value);
|
||||
stopScanner();
|
||||
}
|
||||
|
||||
useEffect(() => () => {
|
||||
if (scanner) {
|
||||
scanner.stop().catch(() => undefined);
|
||||
}
|
||||
}, [scanner]);
|
||||
|
||||
return (
|
||||
<Page title="Willkommen bei Fotochallenge 🎉">
|
||||
<Page title="Willkommen bei der Fotobox!">
|
||||
{error && (
|
||||
<Alert className="mb-3" variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Input
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value)}
|
||||
placeholder="QR/PIN oder Event-Slug eingeben"
|
||||
/>
|
||||
<div className="h-3" />
|
||||
<Button disabled={loading || !slug.trim()} onClick={join}>
|
||||
{loading ? 'Prüfe…' : 'Event beitreten'}
|
||||
</Button>
|
||||
<div className="space-y-6 pb-20">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Willkommen bei der Fotobox!</h1>
|
||||
<p className="text-lg text-gray-600">Dein Schluessel zu unvergesslichen Momenten.</p>
|
||||
</div>
|
||||
|
||||
<Card className="mx-auto w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl font-semibold">Event beitreten</CardTitle>
|
||||
<CardDescription>Scanne den QR-Code oder gib den Code manuell ein.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<div className="flex h-24 w-24 items-center justify-center rounded-lg bg-gray-200">
|
||||
<svg className="h-16 w-16 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="qr-reader" className="w-full" hidden={!isScanning} />
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={isScanning ? stopScanner : startScanner}
|
||||
disabled={loading}
|
||||
>
|
||||
{isScanning ? 'Scanner stoppen' : 'QR-Code scannen'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 py-2 text-center text-sm text-gray-500">
|
||||
Oder manuell eingeben
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={slug}
|
||||
onChange={(event) => setSlug(event.target.value)}
|
||||
placeholder="Event-Code eingeben"
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-pink-500 to-pink-600 text-white hover:from-pink-600 hover:to-pink-700"
|
||||
disabled={loading || !slug.trim()}
|
||||
onClick={() => join()}
|
||||
>
|
||||
{loading ? 'Pruefe...' : 'Event beitreten'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user