1 Commits
1.0.0 ... 1.0.1

Author SHA1 Message Date
44ebdcb8ee fix language selection and missing locales
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m7s
Build & Push Docker Image / build (release) Successful in 1m10s
2026-02-24 18:40:22 +01:00
6 changed files with 98 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "redlight", "name": "redlight",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently -n client,server -c blue,green \"vite\" \"node --watch server/index.js\"", "dev": "concurrently -n client,server -c blue,green \"vite\" \"node --watch server/index.js\"",

View File

@@ -1,5 +1,7 @@
import { useEffect } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuth } from './contexts/AuthContext'; import { useAuth } from './contexts/AuthContext';
import { useLanguage } from './contexts/LanguageContext';
import Layout from './components/Layout'; import Layout from './components/Layout';
import ProtectedRoute from './components/ProtectedRoute'; import ProtectedRoute from './components/ProtectedRoute';
import Home from './pages/Home'; import Home from './pages/Home';
@@ -13,6 +15,14 @@ import GuestJoin from './pages/GuestJoin';
export default function App() { export default function App() {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
const { setLanguage } = useLanguage();
// Sync language from server when user loads
useEffect(() => {
if (user?.language) {
setLanguage(user.language);
}
}, [user?.language, setLanguage]);
if (loading) { if (loading) {
return ( return (

View File

@@ -157,7 +157,34 @@
"joinFailed": "Beitritt fehlgeschlagen", "joinFailed": "Beitritt fehlgeschlagen",
"endConfirm": "Meeting wirklich beenden?", "endConfirm": "Meeting wirklich beenden?",
"enterAccessCode": "Zugangscode eingeben:", "enterAccessCode": "Zugangscode eingeben:",
"notFound": "Raum nicht gefunden" "notFound": "Raum nicht gefunden",
"guestAccessTitle": "Gastzugang",
"guestAccess": "Gastzugang aktivieren",
"guestAccessHint": "Ermöglicht nicht angemeldeten Benutzern, dem Meeting beizutreten.",
"moderatorCode": "Moderator-Code",
"moderatorCodeHint": "Optionaler Code für Moderator-Rechte",
"moderatorCodeDesc": "Gäste, die diesen Code eingeben, erhalten Moderator-Rechte.",
"guestLink": "Gast-Einladungslink",
"guestLinkCopied": "Gast-Link kopiert!",
"guestJoinTitle": "Meeting beitreten",
"guestCreatedBy": "Erstellt von",
"guestMeetingRunning": "Meeting läuft",
"guestMeetingNotStarted": "Noch nicht gestartet",
"guestYourName": "Ihr Name",
"guestNamePlaceholder": "Max Mustermann",
"guestAccessCode": "Zugangscode",
"guestAccessCodePlaceholder": "Code eingeben",
"guestModeratorCode": "Moderator-Code",
"guestModeratorOptional": "(optional)",
"guestModeratorPlaceholder": "Nur wenn Sie Moderator sind",
"guestJoinButton": "Meeting beitreten",
"guestWaitingMessage": "Das Meeting wurde noch nicht gestartet. Bitte warten Sie, bis der Moderator es startet.",
"guestAccessDenied": "Zugang nicht möglich",
"guestNameRequired": "Name ist erforderlich",
"guestJoinFailed": "Beitritt fehlgeschlagen",
"guestHasAccount": "Haben Sie ein Konto?",
"guestSignIn": "Anmelden",
"guestRoomNotFound": "Raum nicht gefunden"
}, },
"recordings": { "recordings": {
"title": "Aufnahmen", "title": "Aufnahmen",

View File

@@ -157,7 +157,34 @@
"joinFailed": "Join failed", "joinFailed": "Join failed",
"endConfirm": "Really end meeting?", "endConfirm": "Really end meeting?",
"enterAccessCode": "Enter access code:", "enterAccessCode": "Enter access code:",
"notFound": "Room not found" "notFound": "Room not found",
"guestAccessTitle": "Guest Access",
"guestAccess": "Enable guest access",
"guestAccessHint": "Allows unauthenticated users to join the meeting.",
"moderatorCode": "Moderator Code",
"moderatorCodeHint": "Optional code for moderator rights",
"moderatorCodeDesc": "Guests who enter this code will receive moderator rights.",
"guestLink": "Guest Invite Link",
"guestLinkCopied": "Guest link copied!",
"guestJoinTitle": "Join Meeting",
"guestCreatedBy": "Created by",
"guestMeetingRunning": "Meeting in progress",
"guestMeetingNotStarted": "Not started yet",
"guestYourName": "Your Name",
"guestNamePlaceholder": "John Doe",
"guestAccessCode": "Access Code",
"guestAccessCodePlaceholder": "Enter code",
"guestModeratorCode": "Moderator Code",
"guestModeratorOptional": "(optional)",
"guestModeratorPlaceholder": "Only if you are a moderator",
"guestJoinButton": "Join meeting",
"guestWaitingMessage": "The meeting has not started yet. Please wait for the moderator to start it.",
"guestAccessDenied": "Access denied",
"guestNameRequired": "Name is required",
"guestJoinFailed": "Join failed",
"guestHasAccount": "Have an account?",
"guestSignIn": "Sign in",
"guestRoomNotFound": "Room not found"
}, },
"recordings": { "recordings": {
"title": "Recordings", "title": "Recordings",

View File

@@ -3,9 +3,11 @@ import { useParams, Link } from 'react-router-dom';
import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio } from 'lucide-react'; import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio } from 'lucide-react';
import api from '../services/api'; import api from '../services/api';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useLanguage } from '../contexts/LanguageContext';
export default function GuestJoin() { export default function GuestJoin() {
const { uid } = useParams(); const { uid } = useParams();
const { t } = useLanguage();
const [roomInfo, setRoomInfo] = useState(null); const [roomInfo, setRoomInfo] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -22,7 +24,7 @@ export default function GuestJoin() {
setRoomInfo(res.data.room); setRoomInfo(res.data.room);
setStatus({ running: res.data.running }); setStatus({ running: res.data.running });
} catch (err) { } catch (err) {
setError(err.response?.data?.error || 'Raum nicht gefunden'); setError(err.response?.data?.error || t('room.guestRoomNotFound'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -44,7 +46,7 @@ export default function GuestJoin() {
const handleJoin = async (e) => { const handleJoin = async (e) => {
e.preventDefault(); e.preventDefault();
if (!name.trim()) { if (!name.trim()) {
toast.error('Name ist erforderlich'); toast.error(t('room.guestNameRequired'));
return; return;
} }
@@ -59,7 +61,7 @@ export default function GuestJoin() {
window.location.href = res.data.joinUrl; window.location.href = res.data.joinUrl;
} }
} catch (err) { } catch (err) {
toast.error(err.response?.data?.error || 'Beitritt fehlgeschlagen'); toast.error(err.response?.data?.error || t('room.guestJoinFailed'));
} finally { } finally {
setJoining(false); setJoining(false);
} }
@@ -87,10 +89,10 @@ export default function GuestJoin() {
<div className="w-16 h-16 bg-th-error/15 rounded-full flex items-center justify-center mx-auto mb-4"> <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" /> <Video size={28} className="text-th-error" />
</div> </div>
<h2 className="text-xl font-bold text-th-text mb-2">Zugang nicht möglich</h2> <h2 className="text-xl font-bold text-th-text mb-2">{t('room.guestAccessDenied')}</h2>
<p className="text-sm text-th-text-s mb-6">{error}</p> <p className="text-sm text-th-text-s mb-6">{error}</p>
<Link to="/login" className="btn-primary inline-flex"> <Link to="/login" className="btn-primary inline-flex">
Zum Login {t('auth.login')}
</Link> </Link>
</div> </div>
</div> </div>
@@ -124,7 +126,7 @@ export default function GuestJoin() {
<div className="text-center mb-6"> <div className="text-center mb-6">
<h2 className="text-xl font-bold text-th-text mb-1">{roomInfo.name}</h2> <h2 className="text-xl font-bold text-th-text mb-1">{roomInfo.name}</h2>
<p className="text-sm text-th-text-s"> <p className="text-sm text-th-text-s">
Erstellt von <span className="font-medium text-th-text">{roomInfo.owner_name}</span> {t('room.guestCreatedBy')} <span className="font-medium text-th-text">{roomInfo.owner_name}</span>
</p> </p>
<div className="mt-3 inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium" <div className="mt-3 inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium"
style={{ style={{
@@ -133,14 +135,14 @@ export default function GuestJoin() {
}} }}
> >
{status.running ? <Radio size={10} className="animate-pulse" /> : <Users size={12} />} {status.running ? <Radio size={10} className="animate-pulse" /> : <Users size={12} />}
{status.running ? 'Meeting läuft' : 'Noch nicht gestartet'} {status.running ? t('room.guestMeetingRunning') : t('room.guestMeetingNotStarted')}
</div> </div>
</div> </div>
{/* Join form */} {/* Join form */}
<form onSubmit={handleJoin} className="space-y-4"> <form onSubmit={handleJoin} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-th-text mb-1.5">Ihr Name *</label> <label className="block text-sm font-medium text-th-text mb-1.5">{t('room.guestYourName')} *</label>
<div className="relative"> <div className="relative">
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" /> <User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
<input <input
@@ -148,7 +150,7 @@ export default function GuestJoin() {
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
className="input-field pl-11" className="input-field pl-11"
placeholder="Max Mustermann" placeholder={t('room.guestNamePlaceholder')}
required required
autoFocus autoFocus
/> />
@@ -157,7 +159,7 @@ export default function GuestJoin() {
{roomInfo.has_access_code && ( {roomInfo.has_access_code && (
<div> <div>
<label className="block text-sm font-medium text-th-text mb-1.5">Zugangscode</label> <label className="block text-sm font-medium text-th-text mb-1.5">{t('room.guestAccessCode')}</label>
<div className="relative"> <div className="relative">
<Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" /> <Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
<input <input
@@ -165,7 +167,7 @@ export default function GuestJoin() {
value={accessCode} value={accessCode}
onChange={e => setAccessCode(e.target.value)} onChange={e => setAccessCode(e.target.value)}
className="input-field pl-11" className="input-field pl-11"
placeholder="Code eingeben" placeholder={t('room.guestAccessCodePlaceholder')}
/> />
</div> </div>
</div> </div>
@@ -173,8 +175,8 @@ export default function GuestJoin() {
<div> <div>
<label className="block text-sm font-medium text-th-text mb-1.5"> <label className="block text-sm font-medium text-th-text mb-1.5">
Moderator-Code {t('room.guestModeratorCode')}
<span className="text-th-text-s font-normal ml-1">(optional)</span> <span className="text-th-text-s font-normal ml-1">{t('room.guestModeratorOptional')}</span>
</label> </label>
<div className="relative"> <div className="relative">
<Shield size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" /> <Shield size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
@@ -183,7 +185,7 @@ export default function GuestJoin() {
value={moderatorCode} value={moderatorCode}
onChange={e => setModeratorCode(e.target.value)} onChange={e => setModeratorCode(e.target.value)}
className="input-field pl-11" className="input-field pl-11"
placeholder="Nur wenn Sie Moderator sind" placeholder={t('room.guestModeratorPlaceholder')}
/> />
</div> </div>
</div> </div>
@@ -197,7 +199,7 @@ export default function GuestJoin() {
<Loader2 size={18} className="animate-spin" /> <Loader2 size={18} className="animate-spin" />
) : ( ) : (
<> <>
Meeting beitreten {t('room.guestJoinButton')}
<ArrowRight size={18} /> <ArrowRight size={18} />
</> </>
)} )}
@@ -205,14 +207,14 @@ export default function GuestJoin() {
{!status.running && ( {!status.running && (
<p className="text-xs text-th-text-s text-center"> <p className="text-xs text-th-text-s text-center">
Das Meeting wurde noch nicht gestartet. Bitte warten Sie, bis der Moderator es startet. {t('room.guestWaitingMessage')}
</p> </p>
)} )}
</form> </form>
<div className="mt-6 pt-4 border-t border-th-border text-center"> <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"> <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> {t('room.guestHasAccount')} <span className="text-th-accent font-medium">{t('room.guestSignIn')}</span>
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -23,6 +23,16 @@ export default function Settings() {
}); });
const [savingProfile, setSavingProfile] = useState(false); const [savingProfile, setSavingProfile] = useState(false);
const [savingPassword, setSavingPassword] = useState(false); const [savingPassword, setSavingPassword] = useState(false);
const handleLanguageChange = async (lang) => {
setLanguage(lang);
try {
const res = await api.put('/auth/profile', { language: lang });
updateUser(res.data.user);
} catch {
// Language is still saved locally even if API fails
}
};
const [activeSection, setActiveSection] = useState('profile'); const [activeSection, setActiveSection] = useState('profile');
const [uploadingAvatar, setUploadingAvatar] = useState(false); const [uploadingAvatar, setUploadingAvatar] = useState(false);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
@@ -44,6 +54,7 @@ export default function Settings() {
name: profile.name, name: profile.name,
email: profile.email, email: profile.email,
theme, theme,
language,
avatar_color: user?.avatar_color, avatar_color: user?.avatar_color,
}); });
updateUser(res.data.user); updateUser(res.data.user);
@@ -349,7 +360,7 @@ export default function Settings() {
].map(lang => ( ].map(lang => (
<button <button
key={lang.code} key={lang.code}
onClick={() => setLanguage(lang.code)} onClick={() => handleLanguageChange(lang.code)}
className={`flex items-center gap-3 p-4 rounded-xl border-2 transition-all ${ className={`flex items-center gap-3 p-4 rounded-xl border-2 transition-all ${
language === lang.code language === lang.code
? 'border-th-accent shadow-md bg-th-accent/5' ? 'border-th-accent shadow-md bg-th-accent/5'