der tenant admin hat eine neue, mobil unterstützende UI, login redirect funktioniert, typescript fehler wurden bereinigt. Neue Blog Posts von ChatGPT eingebaut, übersetzt von Gemini 2.5

This commit is contained in:
Codex Agent
2025-11-05 19:27:10 +01:00
parent adb93b5f9d
commit c6ac04eb15
44 changed files with 1995 additions and 1949 deletions

View File

@@ -4,11 +4,12 @@ import { useNavigate } from 'react-router-dom';
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AdminLayout } from '../components/AdminLayout';
import { TenantHeroCard, FrostedCard, FrostedSurface } from '../components/tenant';
import { useAuth } from '../auth/context';
import { ADMIN_EVENTS_PATH, ADMIN_PROFILE_PATH } from '../constants';
import { buildAdminOAuthStartPath, buildMarketingLoginUrl } from '../lib/returnTo';
@@ -33,6 +34,50 @@ export default function SettingsPage() {
const [notificationError, setNotificationError] = React.useState<string | null>(null);
const [notificationMeta, setNotificationMeta] = React.useState<NotificationPreferencesMeta | null>(null);
const heroDescription = t('settings.hero.description', { defaultValue: 'Gestalte das Erlebnis für dein Admin-Team Darstellung, Benachrichtigungen und Session-Sicherheit.' });
const heroSupporting = [
t('settings.hero.summary.appearance', { defaultValue: 'Synchronisiere den Look & Feel mit dem Gästeportal oder schalte den Dark Mode frei.' }),
t('settings.hero.summary.notifications', { defaultValue: 'Stimme Benachrichtigungen auf Aufgaben, Pakete und Live-Events ab.' })
];
const accountName = user?.name ?? user?.email ?? 'Tenant Admin';
const heroPrimaryAction = (
<Button
size="sm"
className="rounded-full bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] px-6 text-white shadow-md shadow-rose-400/30 transition hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5]"
onClick={() => navigate(ADMIN_PROFILE_PATH)}
>
{t('settings.hero.actions.profile', 'Profil bearbeiten')}
</Button>
);
const heroSecondaryAction = (
<Button
size="sm"
variant="outline"
className="rounded-full border-white/70 bg-white/80 px-6 text-slate-900 shadow-sm hover:bg-white"
onClick={() => navigate(ADMIN_EVENTS_PATH)}
>
{t('settings.hero.actions.events', 'Zur Event-Übersicht')}
</Button>
);
const heroAside = (
<FrostedSurface className="space-y-3 border-white/25 p-5 text-slate-900 shadow-lg shadow-rose-300/20 dark:border-slate-800/70 dark:bg-slate-950/80">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400">{t('settings.hero.accountLabel', { defaultValue: 'Angemeldeter Account' })}</p>
<p className="mt-2 text-lg font-semibold text-slate-900 dark:text-slate-100">{accountName}</p>
{user?.tenant_id ? (
<p className="text-xs text-slate-600 dark:text-slate-400">Tenant #{user.tenant_id}</p>
) : null}
</div>
<p className="text-xs text-slate-600 dark:text-slate-400">{t('settings.hero.support', { defaultValue: 'Passe Einstellungen für dich und dein Team an Änderungen wirken sofort im Admin.' })}</p>
</FrostedSurface>
);
const translateNotification = React.useCallback(
(key: string, fallback?: string, options?: Record<string, unknown>) =>
t(key, { defaultValue: fallback, ...(options ?? {}) }),
[t],
);
function handleLogout() {
const targetPath = buildAdminOAuthStartPath(ADMIN_EVENTS_PATH);
let marketingUrl = buildMarketingLoginUrl(targetPath);
@@ -55,23 +100,22 @@ export default function SettingsPage() {
})();
}, [t]);
const actions = (
<Button
variant="outline"
onClick={() => navigate(ADMIN_EVENTS_PATH)}
className="border-pink-200 text-pink-600 hover:bg-pink-50"
>
Zurück zur Übersicht
</Button>
);
return (
<AdminLayout
title="Einstellungen"
subtitle="Passe das Erscheinungsbild deines Dashboards an und verwalte deine Session."
actions={actions}
>
<Card className="max-w-2xl border-0 bg-white/85 shadow-xl shadow-amber-100/60">
<TenantHeroCard
badge={t('settings.hero.badge', { defaultValue: 'Administration' })}
title={t('settings.title', { defaultValue: 'Einstellungen' })}
description={heroDescription}
supporting={heroSupporting}
primaryAction={heroPrimaryAction}
secondaryAction={heroSecondaryAction}
aside={heroAside}
/>
<FrostedCard className="mt-6 max-w-2xl border border-white/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
<Palette className="h-5 w-5 text-amber-500" /> Darstellung & Account
@@ -114,9 +158,9 @@ export default function SettingsPage() {
</div>
</section>
</CardContent>
</Card>
</FrostedCard>
<Card className="mt-8 max-w-3xl border-0 bg-white/85 shadow-xl shadow-pink-100/60">
<FrostedCard className="max-w-3xl border border-white/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
<AlertTriangle className="h-5 w-5 text-pink-500" />
@@ -136,9 +180,9 @@ export default function SettingsPage() {
{loadingNotifications ? (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, index) => (
<div
<FrostedSurface
key={index}
className="h-12 animate-pulse rounded-xl bg-gradient-to-r from-white/30 via-white/60 to-white/30"
className="h-12 animate-pulse border border-white/20 bg-gradient-to-r from-white/30 via-white/60 to-white/30 shadow-inner dark:border-slate-800/60 dark:from-slate-800 dark:via-slate-700 dark:to-slate-800"
/>
))}
</div>
@@ -173,11 +217,11 @@ export default function SettingsPage() {
}
}}
saving={savingNotifications}
translate={t}
translate={translateNotification}
/>
) : null}
</CardContent>
</Card>
</FrostedCard>
</AdminLayout>
);
}
@@ -199,7 +243,7 @@ function NotificationPreferencesForm({
onReset: () => void;
onSave: () => Promise<void>;
saving: boolean;
translate: (key: string, options?: Record<string, unknown>) => string;
translate: (key: string, fallback?: string, options?: Record<string, unknown>) => string;
}) {
const items = React.useMemo(() => buildPreferenceMeta(translate), [translate]);
const locale = typeof window !== 'undefined' ? window.navigator.language : 'de-DE';
@@ -226,37 +270,37 @@ function NotificationPreferencesForm({
const checked = preferences[item.key] ?? defaults[item.key] ?? true;
return (
<div key={item.key} className="flex items-start justify-between gap-4 rounded-xl border border-pink-100 bg-white/70 p-4 shadow-sm">
<FrostedSurface key={item.key} className="flex items-start justify-between gap-4 border border-pink-100/60 p-4 text-slate-900 shadow-sm shadow-rose-200/20 dark:border-pink-500/20 dark:bg-slate-950/80">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-slate-900">{item.label}</h3>
<p className="text-sm text-slate-600">{item.description}</p>
<h3 className="text-sm font-semibold text-slate-900 dark:text-slate-100">{item.label}</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">{item.description}</p>
</div>
<Switch
checked={checked}
onCheckedChange={(value) => onChange({ ...preferences, [item.key]: Boolean(value) })}
/>
</div>
</FrostedSurface>
);
})}
</div>
<div className="flex flex-wrap items-center gap-3">
<Button onClick={onSave} disabled={saving} className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white">
<Button onClick={onSave} disabled={saving} className="bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-white shadow-md shadow-rose-400/30">
{saving ? 'Speichern...' : translate('settings.notifications.actions.save', 'Speichern')}
</Button>
<Button variant="ghost" onClick={onReset} disabled={saving}>
{translate('settings.notifications.actions.reset', 'Auf Standard setzen')}
</Button>
<span className="text-xs text-slate-500">
<span className="text-xs text-slate-500 dark:text-slate-400">
{translate('settings.notifications.hint', 'Du kannst Benachrichtigungen jederzeit wieder aktivieren.')}
</span>
</div>
{creditText && <p className="text-xs text-slate-500">{creditText}</p>}
{creditText ? <p className="text-xs text-slate-500 dark:text-slate-400">{creditText}</p> : null}
</div>
);
}
function buildPreferenceMeta(
translate: (key: string, options?: Record<string, unknown>) => string
translate: (key: string, fallback?: string, options?: Record<string, unknown>) => string
): Array<{ key: keyof NotificationPreferences; label: string; description: string }> {
const map = [
{