Init v1.0.0
Some checks failed
Build & Push Docker Image / build (push) Failing after 53s

This commit is contained in:
2026-02-24 18:14:16 +01:00
commit 54d6ee553a
49 changed files with 10410 additions and 0 deletions

222
src/pages/GuestJoin.jsx Normal file
View File

@@ -0,0 +1,222 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio } from 'lucide-react';
import api from '../services/api';
import toast from 'react-hot-toast';
export default function GuestJoin() {
const { uid } = useParams();
const [roomInfo, setRoomInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [joining, setJoining] = useState(false);
const [name, setName] = useState('');
const [accessCode, setAccessCode] = useState('');
const [moderatorCode, setModeratorCode] = useState('');
const [status, setStatus] = useState({ running: false });
useEffect(() => {
const fetchRoom = async () => {
try {
const res = await api.get(`/rooms/${uid}/public`);
setRoomInfo(res.data.room);
setStatus({ running: res.data.running });
} catch (err) {
setError(err.response?.data?.error || 'Raum nicht gefunden');
} finally {
setLoading(false);
}
};
fetchRoom();
const interval = setInterval(async () => {
try {
const res = await api.get(`/rooms/${uid}/status`);
setStatus(res.data);
} catch {
// ignore
}
}, 10000);
return () => clearInterval(interval);
}, [uid]);
const handleJoin = async (e) => {
e.preventDefault();
if (!name.trim()) {
toast.error('Name ist erforderlich');
return;
}
setJoining(true);
try {
const res = await api.post(`/rooms/${uid}/guest-join`, {
name: name.trim(),
access_code: accessCode || undefined,
moderator_code: moderatorCode || undefined,
});
if (res.data.joinUrl) {
window.location.href = res.data.joinUrl;
}
} catch (err) {
toast.error(err.response?.data?.error || 'Beitritt fehlgeschlagen');
} finally {
setJoining(false);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-th-bg">
<Loader2 size={32} className="animate-spin text-th-accent" />
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center p-6 relative overflow-hidden">
<div className="absolute inset-0 bg-th-bg">
<div className="absolute inset-0 opacity-30">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-th-accent rounded-full blur-[128px] animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '2s' }} />
</div>
</div>
<div className="relative w-full max-w-md">
<div className="card p-8 backdrop-blur-xl bg-th-card/80 border border-th-border shadow-2xl rounded-2xl text-center">
<div className="w-16 h-16 bg-th-error/15 rounded-full flex items-center justify-center mx-auto mb-4">
<Video size={28} className="text-th-error" />
</div>
<h2 className="text-xl font-bold text-th-text mb-2">Zugang nicht möglich</h2>
<p className="text-sm text-th-text-s mb-6">{error}</p>
<Link to="/login" className="btn-primary inline-flex">
Zum Login
</Link>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center p-6 relative overflow-hidden">
{/* Animated background */}
<div className="absolute inset-0 bg-th-bg">
<div className="absolute inset-0 opacity-30">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-th-accent rounded-full blur-[128px] animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '2s' }} />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-pink-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '4s' }} />
</div>
</div>
{/* Join card */}
<div className="relative w-full max-w-md">
<div className="card p-8 backdrop-blur-xl bg-th-card/80 border border-th-border shadow-2xl rounded-2xl">
{/* Logo */}
<div className="flex items-center justify-center gap-2.5 mb-6">
<div className="w-10 h-10 gradient-bg rounded-xl flex items-center justify-center">
<Video size={22} className="text-white" />
</div>
<span className="text-2xl font-bold gradient-text">Redlight</span>
</div>
{/* Room info */}
<div className="text-center mb-6">
<h2 className="text-xl font-bold text-th-text mb-1">{roomInfo.name}</h2>
<p className="text-sm text-th-text-s">
Erstellt von <span className="font-medium text-th-text">{roomInfo.owner_name}</span>
</p>
<div className="mt-3 inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: status.running ? 'rgba(34, 197, 94, 0.15)' : 'rgba(100, 116, 139, 0.15)',
color: status.running ? '#22c55e' : '#94a3b8',
}}
>
{status.running ? <Radio size={10} className="animate-pulse" /> : <Users size={12} />}
{status.running ? 'Meeting läuft' : 'Noch nicht gestartet'}
</div>
</div>
{/* Join form */}
<form onSubmit={handleJoin} className="space-y-4">
<div>
<label className="block text-sm font-medium text-th-text mb-1.5">Ihr Name *</label>
<div className="relative">
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="input-field pl-11"
placeholder="Max Mustermann"
required
autoFocus
/>
</div>
</div>
{roomInfo.has_access_code && (
<div>
<label className="block text-sm font-medium text-th-text mb-1.5">Zugangscode</label>
<div className="relative">
<Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
<input
type="text"
value={accessCode}
onChange={e => setAccessCode(e.target.value)}
className="input-field pl-11"
placeholder="Code eingeben"
/>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-th-text mb-1.5">
Moderator-Code
<span className="text-th-text-s font-normal ml-1">(optional)</span>
</label>
<div className="relative">
<Shield size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
<input
type="text"
value={moderatorCode}
onChange={e => setModeratorCode(e.target.value)}
className="input-field pl-11"
placeholder="Nur wenn Sie Moderator sind"
/>
</div>
</div>
<button
type="submit"
disabled={joining || (!status.running && !roomInfo.anyone_can_start)}
className="btn-primary w-full py-3"
>
{joining ? (
<Loader2 size={18} className="animate-spin" />
) : (
<>
Meeting beitreten
<ArrowRight size={18} />
</>
)}
</button>
{!status.running && (
<p className="text-xs text-th-text-s text-center">
Das Meeting wurde noch nicht gestartet. Bitte warten Sie, bis der Moderator es startet.
</p>
)}
</form>
<div className="mt-6 pt-4 border-t border-th-border text-center">
<Link to="/login" className="text-sm text-th-text-s hover:text-th-accent transition-colors">
Haben Sie ein Konto? <span className="text-th-accent font-medium">Anmelden</span>
</Link>
</div>
</div>
</div>
</div>
);
}