// Freunde / Admin / Settings pages — Clean Rail aesthetic // =================================================================== // SHARED PRIMITIVES // =================================================================== const PageHeader = ({ title, subtitle, actions }) => (

{title}

{subtitle &&

{subtitle}

}
{actions &&
{actions}
}
); const Section = ({ title, children, action }) => (

{title}

{action}
{children}
); const Toggle = ({ checked, onChange, label, hint, disabled }) => ( ); const SegmentedControl = ({ options, value, onChange }) => (
{options.map(opt => ( ))}
); const Row = ({ label, value, mono }) => (
{label} {value}
); const Stat = ({ label, value }) => (
{label}
{value}
); const Banner = ({ kind = 'info', children }) => (
{kind === 'error' && } {kind === 'success' && } {children}
); const Empty = ({ icon: IconComp = Icon.Friends, children }) => (

{children}

); // =================================================================== // FRIENDS PAGE // =================================================================== const FRIENDS_DATA = [ { id: 'max', userId: 'max', displayName: 'Max', username: 'maxx', initial: 'M', color: 'teal', status: 'accepted', direction: null, presence: 'online' }, { id: 'lena', userId: 'lena', displayName: 'Lena', username: 'lena_k', initial: 'L', color: 'amber', status: 'accepted', direction: null, presence: 'online' }, { id: 'tom', userId: 'tom', displayName: 'Tom', username: 'tomtom', initial: 'T', color: 'violet', status: 'accepted', direction: null, presence: 'idle' }, { id: 'nico', userId: 'nico', displayName: 'nico', username: 'niklas', initial: 'N', color: 'teal', status: 'accepted', direction: null, presence: 'offline' }, { id: 'julia', userId: 'julia', displayName: 'Julia', username: 'jul', initial: 'J', color: 'rose', status: 'accepted', direction: null, presence: 'dnd' }, { id: 'ben', userId: 'ben', displayName: 'Ben', username: 'benny', initial: 'B', color: 'amber', status: 'pending', direction: 'outgoing' }, { id: 'anna', userId: 'anna', displayName: 'Anna', username: 'anna_m', initial: 'A', color: 'violet', status: 'pending', direction: 'outgoing' }, { id: 'kai', userId: 'kai', displayName: 'Kai', username: 'kaiser', initial: 'K', color: 'teal', status: 'pending', direction: 'incoming' }, { id: 'sara', userId: 'sara', displayName: 'Sara', username: 'sari', initial: 'S', color: 'rose', status: 'pending', direction: 'incoming' }, ]; const SEARCH_RESULTS = [ { id: 'felix', userId: 'felix', displayName: 'Felix', username: 'felix_x', initial: 'F', color: 'violet' }, { id: 'mia', userId: 'mia', displayName: 'Mia', username: 'mia_w', initial: 'M', color: 'amber' }, { id: 'paul', userId: 'paul', displayName: 'Paul', username: 'paulk', initial: 'P', color: 'teal' }, ]; const FriendRow = ({ profile, children }) => (
{profile.displayName}
@{profile.username}
{children}
); const FriendsPageContent = () => { const [tab, setTab] = React.useState('friends'); const [query, setQuery] = React.useState(''); const accepted = FRIENDS_DATA.filter(f => f.status === 'accepted'); const outgoing = FRIENDS_DATA.filter(f => f.status === 'pending' && f.direction === 'outgoing'); const incoming = FRIENDS_DATA.filter(f => f.status === 'pending' && f.direction === 'incoming'); const showSearch = query.trim().length >= 2; const tabs = [ { key: 'friends', label: 'Freunde', count: accepted.length }, { key: 'pending', label: 'Ausgehend', count: outgoing.length }, { key: 'requests', label: 'Anfragen', count: incoming.length, highlight: incoming.length > 0 }, ]; const currentList = tab === 'friends' ? accepted : tab === 'pending' ? outgoing : incoming; return (
setQuery(e.target.value)} />
{showSearch && (

Suchergebnisse

{SEARCH_RESULTS.filter(r => r.displayName.toLowerCase().includes(query.toLowerCase()) || r.username.toLowerCase().includes(query.toLowerCase()) ).map(p => ( ))} {SEARCH_RESULTS.filter(r => r.displayName.toLowerCase().includes(query.toLowerCase()) || r.username.toLowerCase().includes(query.toLowerCase()) ).length === 0 && ( Keine Ergebnisse für „{query}" )}
)} {!showSearch && ( <>
{tabs.map(t => ( ))}
{currentList.length === 0 && ( {tab === 'friends' && 'Noch keine Freunde. Suche oben nach Benutzern um anzufangen.'} {tab === 'pending' && 'Keine ausgehenden Anfragen.'} {tab === 'requests' && 'Keine eingehenden Anfragen.'} )} {currentList.map(f => ( {tab === 'friends' && ( <> )} {tab === 'pending' && ( <> Wartet auf Antwort… )} {tab === 'requests' && ( <> )} ))}
)}
); }; // =================================================================== // ADMIN PAGE // =================================================================== const INVITES_DATA = [ { code: 'SQUAD-2k26', usesCount: 3, usesLimit: 10, expiresAt: '2026-05-01', disabled: false }, { code: 'FRIENDS-OF-DENNIS', usesCount: 1, usesLimit: null, expiresAt: null, disabled: false }, { code: 'BETA-ROUND-1', usesCount: 12, usesLimit: 12, expiresAt: null, disabled: false }, { code: 'OLD-UNUSED', usesCount: 0, usesLimit: 5, expiresAt: '2026-03-01', disabled: true }, ]; const USERS_DATA = [ { userId: 'dennis', displayName: 'dennis', username: 'dennis', initial: 'D', color: 'teal', isAdmin: true, blockedFromInviting: false, banned: false, joinedAt: '14. Mär 2026' }, { userId: 'max', displayName: 'Max', username: 'maxx', initial: 'M', color: 'teal', isAdmin: false, blockedFromInviting: false, banned: false, joinedAt: '16. Mär 2026' }, { userId: 'lena', displayName: 'Lena', username: 'lena_k', initial: 'L', color: 'amber', isAdmin: false, blockedFromInviting: false, banned: false, joinedAt: '02. Apr 2026' }, { userId: 'tom', displayName: 'Tom', username: 'tomtom', initial: 'T', color: 'violet', isAdmin: false, blockedFromInviting: true, banned: false, joinedAt: '08. Apr 2026' }, { userId: 'spam', displayName: 'spam_user_42', username: 'spam42', initial: 'S', color: 'rose', isAdmin: false, blockedFromInviting: false, banned: true, joinedAt: '11. Apr 2026' }, ]; const FlagChip = ({ active, tone, onToggle, children }) => ( ); const AdminPageContent = () => { const [invitesEnabled, setInvitesEnabled] = React.useState(true); const [invites, setInvites] = React.useState(INVITES_DATA); const [users, setUsers] = React.useState(USERS_DATA); const [copiedCode, setCopiedCode] = React.useState(null); const handleCopy = (code) => { setCopiedCode(code); setTimeout(() => setCopiedCode(null), 1400); }; const toggleUserFlag = (userId, flag) => { setUsers(us => us.map(u => u.userId === userId ? {...u, [flag]: !u[flag]} : u)); }; const toggleInvite = (code) => { setInvites(is => is.map(i => i.code === code ? {...i, disabled: !i.disabled} : i)); }; const deleteInvite = (code) => { setInvites(is => is.filter(i => i.code !== code)); }; return (
!i.disabled).length} aktive Invites`} />
Gesamt User
{users.length}
Admins
{users.filter(u => u.isAdmin).length}
Gebannt
{users.filter(u => u.banned).length}
Aktive Invites
{invites.filter(i => !i.disabled).length}
Neuer Code } >
{invites.map(inv => { const expired = inv.expiresAt && new Date(inv.expiresAt) < new Date('2026-04-19'); const status = inv.disabled ? 'disabled' : expired ? 'expired' : 'active'; const usesText = inv.usesLimit ? `${inv.usesCount}/${inv.usesLimit}` : `${inv.usesCount}/∞`; return ( ); })}
Code Nutzungen Ablauf Status
{inv.code} {usesText} {inv.expiresAt || 'nie'} {status === 'active' ? 'Aktiv' : status === 'expired' ? 'Abgelaufen' : 'Deaktiviert'}
{users.map(u => (
{u.displayName} {u.isAdmin && Admin}
@{u.username} · beigetreten {u.joinedAt}
toggleUserFlag(u.userId, 'isAdmin')}>Admin toggleUserFlag(u.userId, 'blockedFromInviting')}>Invite-Block toggleUserFlag(u.userId, 'banned')}>Bann
))}
); }; // =================================================================== // SETTINGS PAGE // =================================================================== const SettingsPageContent = ({ isDark, onToggleDark }) => { const [language, setLanguage] = React.useState('de'); const [readReceipts, setReadReceipts] = React.useState(true); const [allowDms, setAllowDms] = React.useState(true); const [audioQuality, setAudioQuality] = React.useState('voice'); const [ptt, setPtt] = React.useState(false); const [pttKey, setPttKey] = React.useState('Space'); const [capturingKey, setCapturingKey] = React.useState(false); const [e2ee, setE2ee] = React.useState(true); const [screenShareQuality, setScreenShareQuality] = React.useState('balanced'); const audioParams = audioQuality === 'hifi' ? { bitrate: '510 kbps', channels: 'Stereo', sample: '48 kHz', dsp: 'Off' } : { bitrate: '48 kbps', channels: 'Mono', sample: '48 kHz', dsp: 'On' }; const screenParams = { smooth: { bitrate: '3 Mbps', resolution: '1280×720', framerate: '60 fps' }, balanced: { bitrate: '6 Mbps', resolution: '1920×1080', framerate: '30 fps' }, crisp: { bitrate: '12 Mbps', resolution: '2560×1440', framerate: '30 fps' }, }[screenShareQuality]; return (
D
Avatar
Quadratisch, max 512×512, WebP. Sichtbar für deine Freunde.
Sprache
Sprache der App-Oberfläche
Theme
Dark Mode ist dezenter für späte Gaming-Sessions.
{ if ((v === 'dark') !== isDark) onToggleDark(); }} options={[{value: 'light', label: '☀ Light'}, {value: 'dark', label: '☾ Dark'}]} />
Audio-Qualität
{audioQuality === 'hifi' ? 'Stereo 510 kbps Opus ohne Noise-Suppression — bester Musik/Broadcast-Sound.' : 'Mono 48 kbps mit Noise-Suppression, Echo-Cancellation und Auto-Gain — optimiert für Sprache.'}
{ptt && (
Hotkey
Klick zum Ändern. Esc zum Abbrechen.
)}
Qualität
WebRTC passt Bitrate + Auflösung dynamisch an die Netzwerkqualität an. Die Werte sind Obergrenzen.
Dieses Gerät
Geräteschlüssel-Backup
Sichere deinen privaten Schlüssel passwortgeschützt, damit du auf neuen Geräten alte Nachrichten weiter lesen kannst.
); }; Object.assign(window, { FriendsPageContent, AdminPageContent, SettingsPageContent });