fix language selection and missing locales
This commit is contained in:
@@ -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\"",
|
||||||
|
|||||||
10
src/App.jsx
10
src/App.jsx
@@ -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 (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user