Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/api/dashboard/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,60 @@
"messages_today": 0
}

@dashboard_main_router.get("/guilds/{guild_id}/mega-data")
async def get_mega_data(guild_id: int, user: dict = Depends(get_current_user)):
"""Consolidated endpoint for dashboard landing page (Settings, Stats, Metadata)."""
if bot_instance is None:
raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar")

guild = bot_instance.get_guild(guild_id)
if not guild:
raise HTTPException(status_code=404, detail="Guild not found or bot not in guild")

member = guild.get_member(int(user["id"]))
if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator):
raise HTTPException(status_code=403, detail="Insufficient permissions")

try:
# 1. Fetch Stats (reuse logic)
try:
stats = await get_guild_stats(guild_id, user)
except:

Check notice on line 347 in src/api/dashboard/routes.py

View check run for this annotation

codefactor.io / CodeFactor

src/api/dashboard/routes.py#L347

Do not use bare 'except'. (E722)
stats = {}

# 2. Fetch Settings
guild_lang = "de"
if hasattr(bot_instance, 'settings_db'):
guild_lang = bot_instance.settings_db.get_guild_language(guild_id)

# 3. Fetch Metadata
channels = [{"id": str(c.id), "name": c.name} for c in guild.text_channels]
roles = [{"id": str(r.id), "name": r.name, "color": str(r.color)} for r in guild.roles if not r.is_default() and not r.managed]
categories = [{"id": str(c.id), "name": c.name} for c in guild.categories]
voice_channels = [{"id": str(c.id), "name": c.name} for c in guild.voice_channels]

return {
"success": True,
"data": {
"settings": {
"bot_name": bot_instance.user.name,
"prefix": "!",
"auto_mod": True,
"welcome_message": False,
"language": guild_lang
},
"stats": stats,
"metadata": {
"channels": channels,
"roles": roles,
"categories": categories,
"voice_channels": voice_channels
}
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

dashboard_main_router.include_router(auth_router)
dashboard_main_router.include_router(settings_router)
dashboard_main_router.include_router(user_router)
Expand Down
15 changes: 11 additions & 4 deletions src/web/components/OverviewSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import OverviewCharts from "./OverviewCharts";

interface OverviewSettingsProps {
guildId: string | null;
initialStats?: any;
}

export default function OverviewSettings({ guildId }: OverviewSettingsProps) {
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
export default function OverviewSettings({ guildId, initialStats }: OverviewSettingsProps) {
const [stats, setStats] = useState<any>(initialStats || null);
const [loading, setLoading] = useState(!initialStats);

useEffect(() => {
if (initialStats) {
setStats(initialStats);
setLoading(false);
return;
}

const fetchStats = async () => {
if (!guildId) return;
const token = localStorage.getItem("token");
Expand All @@ -36,7 +43,7 @@ export default function OverviewSettings({ guildId }: OverviewSettingsProps) {
setLoading(true);
fetchStats();
}
}, [guildId]);
}, [guildId, initialStats]);

// Prepare chart data from API history
const messageData = (stats?.history || []).map((h: any) => ({
Expand Down
75 changes: 33 additions & 42 deletions src/web/dashboard/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,60 +69,51 @@
const [theme, setTheme] = useState("dark");
const [language, setLanguage] = useState("de");

const [stats, setStats] = useState<any>(null);

React.useEffect(() => {
const fetchSettings = async () => {
const fetchAllData = async () => {
if (!token || !guildId) return;
setIsLoading(true);
try {
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040';

// Fetch settings
const settingsRes = await fetch(`${baseUrl}/dashboard/settings/${guildId}`, {
// Fetch EVERYTHING in ONE call
const res = await fetch(`${baseUrl}/dashboard/guilds/${guildId}/mega-data`, {
headers: { "Authorization": `Bearer ${token}` }
});
if (settingsRes.ok) {
const data = await settingsRes.json();
if (data.success && data.data) {
setBotName(data.data.bot_name || "ManagerX");
setPrefix(data.data.prefix || "!");
setAutoMod(data.data.auto_mod ?? true);
setWelcomeMessage(data.data.welcome_message ?? false);
setLanguage(data.data.language || "de");

if (res.ok) {
const json = await res.json();
if (json.success && json.data) {
const { settings, metadata, stats: guildStats } = json.data;

// Set General Settings
setBotName(settings.bot_name || "ManagerX");
setPrefix(settings.prefix || "!");
setAutoMod(settings.auto_mod ?? true);
setWelcomeMessage(settings.welcome_message ?? false);
setLanguage(settings.language || "de");

// Set Guild Metadata
setGuildData({
channels: metadata.channels || [],
roles: metadata.roles || [],
categories: metadata.categories || [],
voiceChannels: metadata.voice_channels || []
});

// Set Stats
setStats(guildStats);
}
}

// Fetch Guild Data (Channels, Roles, Categories, VoiceChannels)
const [channelsRes, rolesRes, catsRes, vcsRes] = await Promise.all([
fetch(`${baseUrl}/dashboard/guilds/${guildId}/channels`, {
headers: { "Authorization": `Bearer ${token}` }
}),
fetch(`${baseUrl}/dashboard/guilds/${guildId}/roles`, {
headers: { "Authorization": `Bearer ${token}` }
}),
fetch(`${baseUrl}/dashboard/guilds/${guildId}/categories`, {
headers: { "Authorization": `Bearer ${token}` }
}),
fetch(`${baseUrl}/dashboard/guilds/${guildId}/voice_channels`, {
headers: { "Authorization": `Bearer ${token}` }
})
]);

if (channelsRes.ok && rolesRes.ok && catsRes.ok && vcsRes.ok) {
const channels = await channelsRes.json();
const roles = await rolesRes.json();
const categories = await catsRes.json();
const voiceChannels = await vcsRes.json();
setGuildData({
channels: channels.channels || [],
roles: roles.roles || [],
categories: categories.categories || [],
voiceChannels: voiceChannels.channels || []
});
}
} catch (e) {
console.error("Failed to fetch guild data", e);
console.error("Failed to fetch consolidated dashboard data", e);
} finally {
setIsLoading(false);
}
};

Check notice on line 115 in src/web/dashboard/SettingsPage.tsx

View check run for this annotation

codefactor.io / CodeFactor

src/web/dashboard/SettingsPage.tsx#L75-L115

Complex Method
fetchSettings();
fetchAllData();
}, [token, guildId]);

const handleSave = async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/web/dashboard/UserSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,443 +37,450 @@
import { Toaster, toast } from "sonner";
import { cn } from "../lib/utils";

export default function UserSettingsPage() {
const { token, user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);

// User Settings state
const [settings, setSettings] = useState({
username: "",
language: "de",
globalStats: null as any,
moderation: null as any,
globalChat: null as any,
topServers: [] as any[],
is_private: false
});

useEffect(() => {
const fetchUserSettings = async () => {
if (!token) return;
setIsLoading(true);
try {
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040';
const res = await fetch(`${baseUrl}/dashboard/user/settings`, {
headers: { "Authorization": `Bearer ${token}` }
});

if (res.ok) {
const data = await res.json();
if (data.success && data.data) {
setSettings({
username: data.data.username || "",
language: data.data.language || "de",
globalStats: data.data.global_stats,
moderation: data.data.moderation,
globalChat: data.data.global_chat,
topServers: data.data.top_servers || [],
is_private: data.data.is_private === 1
});
}
}
} catch (e) {
console.error("Failed to fetch user settings", e);
} finally {
setIsLoading(false);
}
};

fetchUserSettings();
}, [token]);

const handleSave = async () => {
if (!token) return;
setIsSaving(true);
try {
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040';
const res = await fetch(`${baseUrl}/dashboard/user/settings`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
language: settings.language,
is_private: settings.is_private
})
});

if (res.ok) {
toast.success("Einstellungen gespeichert!");
} else {
throw new Error("Save failed");
}
} catch (e) {
toast.error("Fehler beim Speichern der Einstellungen.");
} finally {
setIsSaving(false);
}
};

if (isLoading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin" />
</div>
);
}

return (
<div className="min-h-screen bg-background text-foreground p-4 md:p-8 relative overflow-hidden flex flex-col items-center">
<Toaster theme="dark" position="bottom-right" />

{/* Premium Background */}
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 blur-[150px] rounded-full pointer-events-none -mr-40 -mt-40" />
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-accent/5 blur-[120px] rounded-full pointer-events-none -ml-40 -mb-40" />

<div className="w-full max-w-4xl relative z-10 space-y-8 mt-4">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row md:items-center justify-between gap-6"
>
<div className="flex items-center gap-6">
<Link to="/dash/settings">
<Button variant="outline" size="icon" className="group rounded-2xl w-14 h-14 border-white/10 bg-white/5 hover:bg-white/10 hover:text-white transition-all shadow-xl shadow-black/20">
<ChevronLeft className="w-6 h-6 group-hover:-translate-x-1 transition-transform" />
</Button>
</Link>
<div className="space-y-1">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 w-fit">
<Settings className="w-3.5 h-3.5 text-primary" />
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary">Global Settings</span>
</div>
<h1 className="text-5xl font-extrabold tracking-tighter text-white">User <span className="text-primary">Settings</span></h1>
<p className="text-muted-foreground text-sm font-medium opacity-70">Verwalte deine persönlichen Präferenzen im ManagerX Network.</p>
</div>
</div>

<Button
onClick={handleSave}
disabled={isSaving}
className="rounded-2xl px-10 h-14 gap-2 bg-primary hover:bg-primary/90 text-primary-foreground shadow-2xl shadow-primary/30 transition-all hover:scale-[1.05] active:scale-[0.95] font-bold"
>
{isSaving ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<Save className="w-5 h-5" />
)}
Save Changes
</Button>
</motion.div>

<div className="grid grid-cols-1 gap-10">
{/* Settings Form */}
<div className="space-y-8">
{/* Account Info */}
<Card className="glass border-white/10 shadow-2xl rounded-[2.5rem] overflow-hidden">
<CardHeader className="p-10 pb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center border border-primary/20">
<User className="w-6 h-6 text-primary" />
</div>
<div>
<CardTitle className="text-2xl font-bold">Account</CardTitle>
<CardDescription>Angemeldet als {settings.username}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-10 pt-4">
<div className="flex items-center gap-6 p-6 rounded-3xl bg-white/5 border border-white/10">
<div className="w-20 h-20 rounded-2xl overflow-hidden shadow-2xl">
{user?.avatar ? (
<img src={`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=256`} alt="Avatar" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-primary/20 flex items-center justify-center text-primary text-2xl font-bold">
{settings.username?.substring(0, 1).toUpperCase()}
</div>
)}
</div>
<div className="space-y-1">
<h2 className="text-xl font-bold text-white leading-none">{settings.username}</h2>
<p className="text-muted-foreground text-sm font-medium opacity-70">Discord ID: {user?.id}</p>
</div>
</div>
</CardContent>
</Card>

{/* Global Statistics */}
{settings.globalStats && (
<Card className="glass border-white/10 shadow-2xl rounded-[2.5rem] overflow-hidden">
<CardHeader className="p-10 pb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center border border-primary/20">
<BarChart3 className="w-6 h-6 text-primary" />
</div>
<div>
<CardTitle className="text-2xl font-bold">Global Statistics</CardTitle>
<CardDescription>Deine Aktivitäten im gesamten ManagerX Network.</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-10 pt-4 space-y-8">
{/* Level Progress */}
<div className="p-6 rounded-3xl bg-white/5 border border-white/10 overflow-hidden relative">
<div className="absolute top-0 right-0 p-6 opacity-10">
<Sparkles className="w-16 h-16 text-primary" />
</div>
<div className="flex items-center justify-between mb-4">
<div className="space-y-1">
<span className="text-[10px] font-black uppercase tracking-widest text-primary">Global Level</span>
<h3 className="text-4xl font-black text-white italic">Level {settings.globalStats.level}</h3>
</div>
<div className="text-right">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500">Total XP</span>
<p className="text-xl font-bold text-white">{Math.floor(settings.globalStats.xp).toLocaleString()}</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs font-bold uppercase tracking-widest">
<span className="text-slate-500">Progress to Level {settings.globalStats.level + 1}</span>
<span className="text-primary">{Math.round((settings.globalStats.xp_progress / settings.globalStats.xp_needed) * 100)}%</span>
</div>
<div className="h-2 w-full bg-white/5 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(settings.globalStats.xp_progress / settings.globalStats.xp_needed) * 100}%` }}
className="h-full bg-gradient-to-r from-primary to-accent rounded-full"
/>
</div>
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest text-right">
{Math.floor(settings.globalStats.xp_needed - settings.globalStats.xp_progress).toLocaleString()} XP LEFT
</p>
</div>
</div>

{/* Simple Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-5 rounded-2xl bg-white/5 border border-white/10 flex flex-col gap-1 group hover:border-primary/30 transition-colors">
<MessageSquare className="w-4 h-4 text-primary mb-2 opacity-50 group-hover:opacity-100 transition-opacity" />
<span className="text-2xl font-black text-white">{settings.globalStats.total_messages.toLocaleString()}</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Messages</span>
</div>
<div className="p-5 rounded-2xl bg-white/5 border border-white/10 flex flex-col gap-1 group hover:border-primary/30 transition-colors">
<Mic className="w-4 h-4 text-primary mb-2 opacity-50 group-hover:opacity-100 transition-opacity" />
<span className="text-2xl font-black text-white">{Math.floor(settings.globalStats.total_voice_minutes).toLocaleString()}m</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Voice Time</span>
</div>
<div className="p-5 rounded-2xl bg-white/5 border border-white/10 flex flex-col gap-1 group hover:border-primary/30 transition-colors">
<Server className="w-4 h-4 text-primary mb-2 opacity-50 group-hover:opacity-100 transition-opacity" />
<span className="text-2xl font-black text-white">{settings.globalStats.total_servers}</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Servers</span>
</div>
<div className="p-5 rounded-2xl bg-white/5 border border-white/10 flex flex-col gap-1 group hover:border-primary/30 transition-colors">
<Flame className="w-4 h-4 text-primary mb-2 opacity-50 group-hover:opacity-100 transition-colors" />
<span className="text-2xl font-black text-white">{settings.globalStats.daily_streak}d</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Streak</span>
</div>
</div>

{/* Advanced Global Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
<Calendar className="w-5 h-5 text-primary" />
</div>
<div>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Mitglied seit</p>
<p className="text-sm font-bold text-white">
{settings.globalStats.first_seen ? new Date(settings.globalStats.first_seen).toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' }) : 'Unbekannt'}
</p>
</div>
</div>
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-accent/10 flex items-center justify-center">
<Award className="w-5 h-5 text-accent" />
</div>
<div>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Beste Streak</p>
<p className="text-sm font-bold text-white">{settings.globalStats.best_streak || settings.globalStats.daily_streak} Tage</p>
</div>
</div>
</div>


{/* Achievements Preview */}
{settings.globalStats.achievements && settings.globalStats.achievements.length > 0 && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Trophy className="w-4 h-4 text-primary" />
<span className="text-[10px] font-black uppercase tracking-widest text-white">Achievements ({settings.globalStats.achievements.length})</span>
</div>
<div className="flex flex-wrap gap-2">
{settings.globalStats.achievements.map((a: any, i: number) => (
<div key={i} className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white/5 border border-white/5 text-[10px] font-bold text-slate-300">
<span>{a.icon}</span>
<span>{a.name}</span>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
)}

{/* Moderation & Community */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
{/* Moderation Card */}
<Card className="glass border-white/10 shadow-2xl rounded-[2.5rem] overflow-hidden">
<CardHeader className="p-10 pb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-red-500/10 flex items-center justify-center border border-red-500/20">
<Shield className="w-6 h-6 text-red-500" />
</div>
<div>
<CardTitle className="text-2xl font-bold">Moderation</CardTitle>
<CardDescription>Deine globale History.</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-10 pt-4 space-y-6">
<div className="flex items-center justify-between p-6 rounded-3xl bg-white/5 border border-white/10">
<div className="space-y-1">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Gesamt Verwarnungen</p>
<h3 className={cn("text-4xl font-black", (settings.moderation?.total_warnings || 0) > 0 ? "text-red-500" : "text-green-500")}>
{settings.moderation?.total_warnings || 0}
</h3>
</div>
<div className="px-4 py-2 rounded-xl bg-white/5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
{(settings.moderation?.total_warnings || 0) === 0 ? "Clean Record ✓" : "Achtung"}
</div>
</div>

<div className="flex items-center gap-4 p-4 rounded-2xl bg-white/5 border border-white/10">
<div className="w-10 h-10 rounded-xl bg-blue-500/10 flex items-center justify-center">
<MessageSquare className="w-5 h-5 text-blue-500" />
</div>
<div>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Global-Chat Nachrichten</p>
<p className="text-lg font-bold text-white">{settings.globalChat?.total_messages?.toLocaleString() || 0}</p>
</div>
</div>
</CardContent>
</Card>

{/* Top Servers Card */}
<Card className="glass border-white/10 shadow-2xl rounded-[2.5rem] overflow-hidden">
<CardHeader className="p-10 pb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-yellow-500/10 flex items-center justify-center border border-yellow-500/20">
<History className="w-6 h-6 text-yellow-500" />
</div>
<div>
<CardTitle className="text-2xl font-bold">Top Server</CardTitle>
<CardDescription>Deine aktivsten Communities.</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-10 pt-4 space-y-4">
{settings.topServers && settings.topServers.length > 0 ? (
settings.topServers.map((srv: any, i: number) => (
<div key={i} className="flex items-center justify-between p-4 rounded-2xl bg-white/5 border border-white/10 group hover:border-yellow-500/30 transition-colors">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-white/5 flex items-center justify-center overflow-hidden border border-white/10 shadow-inner group-hover:border-yellow-500/20 transition-all">
{srv.icon_url ? (
<img src={srv.icon_url} alt={srv.name} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center font-black text-yellow-500/50 bg-yellow-500/5">
{srv.name?.substring(0, 1).toUpperCase() || srv.guild_id.substring(0, 1)}
</div>
)}
</div>
<div className="space-y-0.5">
<p className="text-sm font-bold text-white leading-tight group-hover:text-yellow-500 transition-colors">{srv.name}</p>
<div className="flex items-center gap-2">
<div className="px-1.5 py-0.5 rounded-md bg-white/5 border border-white/5">
<p className="text-[8px] font-black uppercase tracking-tighter text-slate-500">ID: {srv.guild_id}</p>
</div>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Level {srv.level}</p>
</div>
</div>
</div>
<div className="text-right">
<p className="text-xs font-bold text-yellow-500">{srv.xp.toLocaleString()} XP</p>
</div>
</div>
))
) : (
<div className="p-10 text-center space-y-2 opacity-50">
<Activity className="w-8 h-8 mx-auto text-slate-500" />
<p className="text-xs font-bold uppercase tracking-widest">Noch keine Level-Daten</p>
</div>
)}
</CardContent>
</Card>
</div>


{/* Preferences */}
<Card className="glass border-white/10 shadow-2xl rounded-[2.5rem] overflow-hidden">
<CardHeader className="p-10 pb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-accent/10 flex items-center justify-center border border-accent/20">
<Globe className="w-6 h-6 text-accent" />
</div>
<div>
<CardTitle className="text-2xl font-bold">Präferenzen</CardTitle>
<CardDescription>System-Einstellungen und Verhaltensweisen.</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-10 pt-4 space-y-10">
<div className="flex items-center justify-between group">
<div className="space-y-1">
<Label className="text-lg font-bold">System Sprache</Label>
<p className="text-sm text-muted-foreground group-hover:text-muted-foreground/80 transition-colors">Wähle deine bevorzugte Sprache für das Dashboard.</p>
</div>
<select
className="bg-white/5 border border-white/10 rounded-xl px-6 h-12 text-white outline-none cursor-pointer hover:bg-white/10 transition-all font-bold"
value={settings.language}
onChange={(e) => setSettings({ ...settings, language: e.target.value })}
>
<option value="de" className="bg-[#1a1c1e]">Deutsch 🇩🇪</option>
<option value="en" className="bg-[#1a1c1e]">English 🇬🇧</option>
</select>
</div>

<div className="p-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />

<div className="flex items-center justify-between group">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-primary" />
<Label className="text-lg font-bold">Privatsphäre bei Leaderboard</Label>
</div>
<p className="text-sm text-muted-foreground group-hover:text-muted-foreground/80 transition-colors">
Wenn aktiviert, werden dein Name und dein Profilbild auf dem globalen Leaderboard anonymisiert.
</p>
</div>
<div className="flex items-center gap-3">
<span className={cn("text-xs font-bold uppercase tracking-widest transition-colors", settings.is_private ? "text-primary" : "text-slate-500")}>
{settings.is_private ? "Anonym" : "Öffentlich"}
</span>
<Switch
checked={settings.is_private}
onCheckedChange={(val) => setSettings({ ...settings, is_private: val })}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>

{/* Footer */}
<div className="mt-20 py-10 w-full text-center opacity-30 border-t border-white/5 flex flex-col items-center gap-2">
<p className="text-[11px] font-bold uppercase tracking-[0.4em] text-white">© 2026 OPPRO.NET DEVELOPMENT</p>
<div className="flex items-center gap-2">
<Zap className="w-3 h-3 text-primary" />
<span className="text-[10px] uppercase font-bold tracking-widest">User Intelligence Module</span>
</div>
</div>
</div>
);
}

Check notice on line 486 in src/web/dashboard/UserSettingsPage.tsx

View check run for this annotation

codefactor.io / CodeFactor

src/web/dashboard/UserSettingsPage.tsx#L40-L486

Complex Method
Loading