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:
@@ -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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user