diff --git a/client/src/App.tsx b/client/src/App.tsx index aa29cfe..32b363e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -34,6 +34,7 @@ import { Plus } from 'lucide-react'; import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar" import { AppSidebar } from "./components/AppSidebar" import { useNotifications } from './hooks/use-notifications'; +import { useToast } from "@/hooks/use-toast"; import { CommandPalette } from './components/CommandPalette'; import PomodoroOverlay from './components/PomodoroOverlay'; import AuthPage from "@/pages/AuthPage"; @@ -43,10 +44,11 @@ import SetupWizard from "@/pages/SetupWizard"; import AdminUserManagement from "@/pages/AdminUserManagement"; import AdminSettings from "@/pages/AdminSettings"; import NotFound from "@/pages/not-found"; -import { AiChat } from "@/components/AiChat"; +// import { AiChat } from "@/components/AiChat"; import ForgotPasswordPage from "@/pages/ForgotPasswordPage"; import ResetPasswordPage from "@/pages/ResetPasswordPage"; import AiChatPage from "@/pages/AiChatPage"; +import NotificationsPage from "@/pages/NotificationsPage"; import FocusRoutinePage from "@/pages/FocusRoutinePage"; @@ -59,6 +61,7 @@ import { Loader2 } from "lucide-react"; function App() { const { t } = useTranslation(); + const { toast } = useToast(); const [, setLocation] = useLocation(); const isBlocked = useRoutineBlocker(); @@ -84,19 +87,21 @@ function App() { }); // Fetch tasks and labels using React Query - const { data: tasks = [] } = useQuery({ + const { data: tasksData } = useQuery({ queryKey: ['/api/tasks'], enabled: !!user, - select: (data) => data.map(task => ({ - ...task, - dueDate: task.dueDate ? new Date(task.dueDate) : null - })) }); - const { data: labels = [] } = useQuery({ + const tasks = (tasksData ?? []).map(task => ({ + ...task, + dueDate: task.dueDate ? new Date(task.dueDate) : null + })); + + const { data: labelsData } = useQuery({ queryKey: ['/api/labels'], enabled: !!user, }); + const labels = labelsData ?? []; const handleCreateTask = async (newTask: Partial) => { try { @@ -105,17 +110,17 @@ function App() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTask.title || '', - description: newTask.description || null, + description: newTask.description || undefined, status: 'todo', priority: newTask.priority || 'medium', - dueDate: newTask.dueDate || null, + dueDate: newTask.dueDate || null, // Explicitly handled as nullable in schema timeTracked: 0, isTracking: false, - projectId: newTask.projectId || null, - notes: newTask.notes || null, - labelId: newTask.labelId || null, + projectId: newTask.projectId || undefined, + notes: newTask.notes || undefined, + labelId: newTask.labelId || undefined, energyLevel: newTask.energyLevel || 'medium', - estimatedDuration: newTask.estimatedDuration || null + estimatedDuration: newTask.estimatedDuration || undefined }) }); @@ -134,8 +139,18 @@ function App() { // Invalidate to be sure queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); + toast({ + title: t('task.created', 'Task Created'), + description: t('task.createdDescription', '"{title}" has been added.', { title: createdTask.title }), + }); + } catch (error) { console.error('Error creating task:', error); + toast({ + title: t('error.createTask', 'Error'), + description: t('error.createTaskDescription', 'Failed to create task. Please try again.'), + variant: "destructive" + }); } }; @@ -265,231 +280,219 @@ function App() { ); } - if (!user) { - // Redirect to setup if no admin user exists - if (setupStatus && !setupStatus.isSetup) { - return ( - - - - - ); - } - - if (isBlocked) { - return ( -
- - - {/* Catch all redirects to nothing, the hook handles the push to routine page */} -
} /> - - -
- ); - } + if (isBlocked) { return ( - - - - -
- {user && ( -
- -
-
- -
- ★ {user.xp} - đŸ”„ {user.currentStreak} -
-
-
- )} - -
- - - setLocation('/tasks')} - /> - - - - - - console.log('Date selected:', date.toLocaleDateString())} - onTaskUpdate={handleTaskUpdate} - onTaskEdit={handleTaskClick} - onTaskDelete={handleTaskDelete} - onStartTimer={startTimer} - onStopTimer={stopTimer} - /> - - - - - - - - - { - queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]); - queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); - }} - onNavigateToSettings={() => setLocation('/settings')} - /> - - - {user ? : } - - - - {user ? ( - handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')} - onDelete={(id) => setDeleteTaskId(id)} - onUpdate={handleTaskUpdate} - onSelect={setSelectedTask} - /> - ) : ( - - )} - - - - - - - - - setLocation('/templates')} /> - - - {/* Admin Route */} - {user.role === 'admin' && ( - <> - - - - )} - - - - -
- - {/* Floating Action Button */} -
- -
- setLocation(path.startsWith('/') ? path : `/${path}`)} - onCreateTask={() => setIsCreateModalOpen(true)} - /> - - setShowPomodoro(false)} - taskId={activePomodoroTaskId} - taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'} - onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })} - /> - - - - setIsCreateModalOpen(false)} - onSave={handleCreateTask} - /> - - { - const t = tasks.find(x => x.id === id); - if (t) setSelectedTask(t); - }} - /> - - !open && setDeleteTaskId(null)}> - - - {t('deleteConfirmation.title')} - - {t('deleteConfirmation.description')} - - - - - {t('deleteConfirmation.cancel')} - - - {t('deleteConfirmation.confirm')} - - - - -
-
-
-
+
+ + + {/* Catch all redirects to nothing, the hook handles the push to routine page */} +
} /> + + +
); } + + if (!user) { + return ( +
+ + + + + + + +
+ ); + } + + return ( + + + + +
+
+ + + setLocation('/tasks')} + /> + + + setLocation('/tasks')} + /> + + + + + + + + + + + + {() => { + setLocation('/'); + return null; + }} + + + + + + { + queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]); + queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); + }} + onNavigateToSettings={() => setLocation('/settings')} + /> + + + + + + + handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')} + onDelete={(id) => setDeleteTaskId(id)} + onUpdate={handleTaskUpdate} + onSelect={setSelectedTask} + /> + + + + + + + + + + + + + + + setLocation('/templates')} /> + + + {/* Admin Route */} + {user?.role === 'admin' && ( + <> + + + + )} + + + + +
+ + {/* Floating Action Button */} +
+ +
+ setLocation(path.startsWith('/') ? path : `/${path}`)} + onCreateTask={() => setIsCreateModalOpen(true)} + /> + + setShowPomodoro(false)} + taskId={activePomodoroTaskId} + taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'} + onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })} + /> + + + + setIsCreateModalOpen(false)} + onSave={handleCreateTask} + /> + + { + const t = tasks.find(x => x.id === id); + if (t) setSelectedTask(t); + }} + /> + + !open && setDeleteTaskId(null)}> + + + {t('deleteConfirmation.title')} + + {t('deleteConfirmation.description')} + + + + + {t('deleteConfirmation.cancel')} + + + {t('deleteConfirmation.confirm')} + + + + +
+
+
+
+ ); + + } export default App; diff --git a/client/src/components/AppSidebar.tsx b/client/src/components/AppSidebar.tsx index 149b95c..466b9f2 100644 --- a/client/src/components/AppSidebar.tsx +++ b/client/src/components/AppSidebar.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award, Bot, CalendarOff } from 'lucide-react'; +import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award, Bot, CalendarOff, Bell } from 'lucide-react'; import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'; import { useToast } from "@/hooks/use-toast"; import { Button } from "@/components/ui/button"; @@ -15,6 +15,7 @@ import { SidebarRail, useSidebar, SidebarTrigger, + SidebarSeparator, } from "@/components/ui/sidebar" import { Task, User } from '@shared/schema'; import ThemeToggle from './ThemeToggle'; @@ -44,8 +45,9 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) { await fetch("/api/logout", { method: "POST" }); }, onSuccess: () => { - queryClient.setQueryData(["/api/user"], null); - toast({ title: "Logged out successfully" }); + queryClient.clear(); + // toast({ title: "Logged out successfully" }); + window.location.href = "/auth?mode=login"; }, }); @@ -68,8 +70,8 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) { -
- +
+ Logo
{state !== 'collapsed' && (
@@ -103,18 +105,31 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) { ))} + {state !== 'collapsed' && user && ( )}
+
+ +
+
diff --git a/client/src/components/CalendarView.tsx b/client/src/components/CalendarView.tsx index 6cdb908..5b2042a 100644 --- a/client/src/components/CalendarView.tsx +++ b/client/src/components/CalendarView.tsx @@ -21,7 +21,6 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { HoverCard, diff --git a/client/src/components/ErrorBoundary.tsx b/client/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..7ef9267 --- /dev/null +++ b/client/src/components/ErrorBoundary.tsx @@ -0,0 +1,49 @@ +import React, { Component, ErrorInfo, ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return ( +
+
+

Something went wrong

+

The application crashed. Here is the error:

+
+                            {this.state.error?.toString()}
+                        
+ +
+
+ ); + } + + return this.props.children; + } +} diff --git a/client/src/components/KanbanBoard.tsx b/client/src/components/KanbanBoard.tsx index e525655..0e01367 100644 --- a/client/src/components/KanbanBoard.tsx +++ b/client/src/components/KanbanBoard.tsx @@ -30,6 +30,7 @@ interface KanbanBoardProps { onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void; onTaskUpdate?: (taskId: string, updates: Partial) => void; onTaskClick?: (task: Task) => void; + onTaskEdit?: (task: Task) => void; onTaskDelete?: (taskId: string) => void; onStartTimer?: (taskId: string) => void; onStopTimer?: (taskId: string) => void; diff --git a/client/src/components/PomodoroOverlay.tsx b/client/src/components/PomodoroOverlay.tsx index e2a6009..e343f13 100644 --- a/client/src/components/PomodoroOverlay.tsx +++ b/client/src/components/PomodoroOverlay.tsx @@ -27,7 +27,7 @@ export default function PomodoroOverlay({ taskId, taskTitle, isOpen, onClose, on // Initialize Worker useEffect(() => { if (typeof Worker !== 'undefined') { - workerRef.current = new Worker(new URL('../workers/timer.worker.ts', import.meta.url)); + workerRef.current = new Worker(new URL('../workers/timer.worker.ts', import.meta.url), { type: 'module' }); workerRef.current.onmessage = (e) => { const { type, remaining } = e.data; diff --git a/client/src/components/RoutineBlocker.tsx b/client/src/components/RoutineBlocker.tsx index 7214275..dc16aa8 100644 --- a/client/src/components/RoutineBlocker.tsx +++ b/client/src/components/RoutineBlocker.tsx @@ -55,8 +55,12 @@ export function useRoutineBlocker() { const morningStartTime = parseTime(settings.morning_routine_time || "09:00"); // Block if it's morning time (>= start) AND < evening start (if evening enabled). - // Effectively: Morning Routine is mandatory from 9:00 AM until 5:00 PM. - const cutoffTime = eveningEnabled ? eveningStartTime : 24 * 60; // Up to evening or end of day + // FIX: Add a hard cutoff (e.g. 13:00 / 1 PM) so morning routine doesn't haunt users all day. + const HARD_MORNING_CUTOFF = 13 * 60; + + const cutoffTime = eveningEnabled + ? Math.min(eveningStartTime, HARD_MORNING_CUTOFF) + : HARD_MORNING_CUTOFF; if (currentTimeVal >= morningStartTime && currentTimeVal < cutoffTime && !isToday(user.lastMorningRoutine)) { return '/focus/routine/morning'; @@ -84,17 +88,21 @@ export function useRoutineBlocker() { const currentTimeVal = now.getHours() * 60 + now.getMinutes(); const eveningEnabled = settings.evening_routine_enabled !== "false"; - const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number); - const eveningStartVal = eveningStartTime[0] * 60 + eveningStartTime[1]; + // Safe parse + const [eh, em] = (settings.evening_routine_time || "17:00").split(':').map(Number); + const eveningStartVal = (eh || 0) * 60 + (em || 0); const morningEnabled = settings.morning_routine_enabled !== "false"; if (!morningEnabled) return false; - const morningStartTime = (settings.morning_routine_time || "09:00").split(':').map(Number); - const startVal = morningStartTime[0] * 60 + morningStartTime[1]; + const [mh, mm] = (settings.morning_routine_time || "09:00").split(':').map(Number); + const startVal = (mh || 0) * 60 + (mm || 0); - // Cutoff: End of day OR Evening Start - const cutoffVal = eveningEnabled ? eveningStartVal : 24 * 60; + const HARD_MORNING_CUTOFF = 13 * 60; // 13:00 + + // Cutoff: Min of (End of day OR Evening Start) AND Hard Cutoff + const baseCutoff = eveningEnabled ? eveningStartVal : 24 * 60; + const cutoffVal = Math.min(baseCutoff, HARD_MORNING_CUTOFF); if (currentTimeVal >= startVal && currentTimeVal < cutoffVal && !isSameDay(user.lastMorningRoutine, now)) return true; @@ -110,8 +118,8 @@ export function useRoutineBlocker() { const eveningEnabled = settings.evening_routine_enabled !== "false"; if (!eveningEnabled) return false; - const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number); - const startVal = eveningStartTime[0] * 60 + eveningStartTime[1]; + const [eh, em] = (settings.evening_routine_time || "17:00").split(':').map(Number); + const startVal = (eh || 0) * 60 + (em || 0); if (currentTimeVal >= startVal && !isSameDay(user.lastEveningRoutine, now)) return true; diff --git a/client/src/components/TaskCard.tsx b/client/src/components/TaskCard.tsx index 301861c..5cc2fe7 100644 --- a/client/src/components/TaskCard.tsx +++ b/client/src/components/TaskCard.tsx @@ -17,8 +17,9 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; -import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X, Lock, CornerDownRight } from "lucide-react"; -import { Task, Label } from '@shared/schema'; +import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X, Lock, CornerDownRight, Share2, Users } from "lucide-react"; +import { Task, Label, User } from '@shared/schema'; +import { ShareTaskModal } from './ShareTaskModal'; import { useQuery } from '@tanstack/react-query'; import { Checkbox } from "@/components/ui/checkbox"; import { motion, PanInfo, useAnimation } from 'framer-motion'; @@ -47,10 +48,6 @@ interface TaskCardProps { isDragging?: boolean; } -import { ShareTaskModal } from './ShareTaskModal'; -import { Share2, Users } from "lucide-react"; -import { User } from "@shared/schema"; - function SharedTaskIcon({ task }: { task: Task }) { const { t } = useTranslation(); const { data: user } = useQuery({ queryKey: ["/api/user"], retry: false }); diff --git a/client/src/components/TaskCreationModal.tsx b/client/src/components/TaskCreationModal.tsx index 808699c..f6ef4b4 100644 --- a/client/src/components/TaskCreationModal.tsx +++ b/client/src/components/TaskCreationModal.tsx @@ -10,11 +10,12 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { CalendarIcon, Plus, Tag, Check, Link2 } from 'lucide-react'; +import { CalendarIcon, Plus, Tag, Check, Link2, Info } from 'lucide-react'; import { Task, Label } from '@shared/schema'; import { useQuery } from '@tanstack/react-query'; import { parseTaskInput } from '../lib/nlp'; import { Sparkles } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; interface TaskCreationModalProps { isOpen: boolean; @@ -47,13 +48,15 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat const [error, setError] = useState(null); // Fetch labels - const { data: labels = [] } = useQuery({ + const { data: labelsData } = useQuery({ queryKey: ['/api/labels'], }); + const labels = labelsData ?? []; - const { data: tasks = [] } = useQuery({ + const { data: tasksData } = useQuery({ queryKey: ['/api/tasks'], }); + const tasks = tasksData ?? []; const handleSave = () => { if (!title.trim()) { @@ -193,7 +196,21 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat +
+
+
+ {t('achievements.weeklyStreak')} + + +
?
+
+ +

{t('gamification.rules.streakBonus')}

+
+
-
-
- - -
-
- - -
-
- - - - - - -
- {goals.length === 0 ? ( -
- -

{t('achievements.noGoals')}

+
0
- ) : ( - goals.map((goal) => ( -
-
-
- {goal.completed ? ( - - ) : ( - - )} - - {goal.title} +
+
+ {t('achievements.monthlyStreak')} + + +
?
+
+ +

{t('gamification.rules.streakBonus')}

+
+
+
+
0
+
+
+ + + {/* More stats placeholder */} +
+ +
+ + + {t('achievements.xpActivity')} + {t('achievements.xpDescription')} + + + +
+ + {t('achievements.weekly')} + {t('achievements.monthly')} + {t('achievements.yearly')} + +
+ + + + { + const translationKey = `analytics.${val}`; + const translated = t(translationKey); + return translated !== translationKey ? translated : `${t('analytics.cw')} ${val}`; + }} + /> + `${value}`} + /> + } cursor={{ fill: 'rgba(255,255,255,0.05)' }} /> + + {monthlyData?.map((entry: any, index: number) => ( + 300 ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'} opacity={0.8} /> + ))} + + + + + + + + t(`analytics.${val}`)} + /> + `${value}`} + /> + } cursor={{ fill: 'rgba(255,255,255,0.05)' }} /> + + {weeklyData?.map((entry: any, index: number) => ( + 300 ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'} opacity={0.8} /> + ))} + + + + + + + + t(`analytics.${val}`)} + /> + + } cursor={{ fill: 'rgba(255,255,255,0.05)' }} /> + + + + +
+
+
+ + + +
+ {t('achievements.goals')} + {t('achievements.goalsDescription')} +
+ + + + + + + {t('achievements.createGoal')} + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+ {goals.length === 0 ? ( +
+ +

{t('achievements.noGoals')}

+
+ ) : ( + goals.map((goal) => ( +
+
+
+ {goal.completed ? ( + + ) : ( + + )} + + {goal.title} + +
+ + {goal.current} / {goal.target}
- - {goal.current} / {goal.target} - +
- + )) + )} +
+
+
+
+
+ )} + + {activeTab === 'rewards' && ( +
+
+ + +
+
+ + {t('rewards.shopTitle')} +
+ {t('achievements.rewardsDescription', { xp: user.xp })} +
+ + + + + + + {t('achievements.createCustomReward')} + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+ + {rewards.map(reward => ( +
+ +
+ ))} +
+
+ )} + + + {activeTab === 'inventory' && ( +
+
+ {inventory.length === 0 ? ( +
+ +

{t('achievements.noInventory')}

+ +
+ ) : ( + inventory.map((item) => ( + +
+ {/* Quick icon mapping or default */} + {item.reward.icon === 'coffee' ? '☕' : + item.reward.icon === 'gamepad-2' ? '🎼' : + item.reward.icon === 'palette' ? '🎹' : '🎁'} +
+ + {item.reward.title.startsWith('rewards.') ? t(item.reward.title) : item.reward.title} + + +

+ {item.reward.description?.startsWith('rewards.') ? t(item.reward.description) : item.reward.description} +

+

+ {t('achievements.purchasedAt', { date: new Date(item.purchasedAt).toLocaleDateString() })} +

+
+
+ )) + )} +
+
+ )} + + {activeTab === 'history' && ( +
+ + + {t('achievements.xpHistory')} + + +
+ {history.length === 0 ? ( +

{t('achievements.noHistory')}

+ ) : ( + history.map((event) => ( +
+
+
+ {event.source === 'complete_task_late' ? : + event.source.includes('complete') ? : + event.source === 'daily_streak' ? : + event.source === 'ai_action' ? : + } +
+
+

+ {event.details?.taskTitle ? event.details.taskTitle : (t(`gamification.source.${event.source}`, { defaultValue: event.source }) as string)} + {event.source === 'complete_task_late' && ({t('gamification.late', 'Late')})} + {(event.source === 'complete_task' || event.source === 'task_completion') && ({t('gamification.onTime', 'On Time')})} +

+
+ {new Date(event.createdAt).toLocaleString()} + {event.details?.taskTitle && ( + <> + ‱ + {t(`gamification.source.${event.source}`, { defaultValue: event.source })} + + )} +
+
+
+ + +{event.amount} XP +
)) )} @@ -384,265 +588,155 @@ export default function AchievementsPage({ user }: { user: User }) {
- - - -
- - -
+ )} + {activeTab === 'rules' && ( +
+
+ {/* Level Requirements */} + +
- {t('rewards.shopTitle')} +
+ {t('gamification.rules.levelRequirements')} + {t('gamification.rules.xpSystem')} +
- {t('achievements.rewardsDescription', { xp: user.xp })} -
- - - - - - - {t('achievements.createCustomReward')} - -
-
- - -
-
- - -
-
- - -
- -
-
-
- - + + +
+ {LEVEL_THRESHOLDS.slice(0, 20).map((threshold, index) => { + const level = index + 1; + const rankKey = getRankKey(level); + const isCurrentLevel = getLevelFromXP(user.xp) === level; - {rewards.map(reward => ( -
- -
- ))} -
- + // Visual Configuration for Ranks + const getRankStyle = (l: number) => { + if (l >= 20) return { icon: Sparkles, color: "text-rose-500", bg: "bg-rose-500/10", border: "border-rose-500/20" }; + if (l >= 19) return { icon: Sun, color: "text-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/20" }; + if (l >= 18) return { icon: Crown, color: "text-yellow-600", bg: "bg-yellow-600/10", border: "border-yellow-600/20" }; + if (l >= 17) return { icon: Zap, color: "text-violet-500", bg: "bg-violet-500/10", border: "border-violet-500/20" }; + if (l >= 16) return { icon: Star, color: "text-cyan-500", bg: "bg-cyan-500/10", border: "border-cyan-500/20" }; + if (l >= 15) return { icon: Award, color: "text-blue-500", bg: "bg-blue-500/10", border: "border-blue-500/20" }; + if (l >= 14) return { icon: BookOpen, color: "text-indigo-500", bg: "bg-indigo-500/10", border: "border-indigo-500/20" }; + if (l >= 13) return { icon: Scroll, color: "text-emerald-500", bg: "bg-emerald-500/10", border: "border-emerald-500/20" }; + if (l >= 12) return { icon: Hammer, color: "text-slate-500", bg: "bg-slate-500/10", border: "border-slate-500/20" }; + if (l >= 11) return { icon: Medal, color: "text-orange-500", bg: "bg-orange-500/10", border: "border-orange-500/20" }; + // 1-10 + if (l >= 10) return { icon: Sun, color: "text-rose-500", bg: "bg-rose-500/10", border: "border-rose-500/20" }; + if (l >= 9) return { icon: Sparkles, color: "text-purple-500", bg: "bg-purple-500/10", border: "border-purple-500/20" }; + if (l >= 8) return { icon: Zap, color: "text-violet-500", bg: "bg-violet-500/10", border: "border-violet-500/20" }; + if (l >= 7) return { icon: Crown, color: "text-yellow-600", bg: "bg-yellow-600/10", border: "border-yellow-600/20" }; + if (l >= 6) return { icon: Star, color: "text-yellow-500", bg: "bg-yellow-500/10", border: "border-yellow-500/20" }; + if (l >= 5) return { icon: Medal, color: "text-orange-500", bg: "bg-orange-500/10", border: "border-orange-500/20" }; + if (l >= 4) return { icon: Award, color: "text-blue-500", bg: "bg-blue-500/10", border: "border-blue-500/20" }; + if (l >= 3) return { icon: Hammer, color: "text-cyan-500", bg: "bg-cyan-500/10", border: "border-cyan-500/20" }; + if (l >= 2) return { icon: BookOpen, color: "text-green-500", bg: "bg-green-500/10", border: "border-green-500/20" }; + return { icon: Scroll, color: "text-slate-500", bg: "bg-slate-500/10", border: "border-slate-500/20" }; + }; - -
- {inventory.length === 0 ? ( -
- -

{t('achievements.noInventory')}

- -
- ) : ( - inventory.map((item) => ( - -
- {/* Quick icon mapping or default */} - {item.reward.icon === 'coffee' ? '☕' : - item.reward.icon === 'gamepad-2' ? '🎼' : - item.reward.icon === 'palette' ? '🎹' : '🎁'} -
- - {item.reward.title.startsWith('rewards.') ? t(item.reward.title) : item.reward.title} - - -

- {item.reward.description?.startsWith('rewards.') ? t(item.reward.description) : item.reward.description} -

-

- {t('achievements.purchasedAt', { date: new Date(item.purchasedAt).toLocaleDateString() })} -

-
-
- )) - )} -
-
+ const style = getRankStyle(level); + const RankIcon = style.icon; - - - - {t('achievements.xpHistory')} - - -
- {history.length === 0 ? ( -

{t('achievements.noHistory')}

- ) : ( - history.map((event) => ( -
-
-
- {event.source === 'complete_task_late' ? : - event.source.includes('complete') ? : - event.source === 'daily_streak' ? : - } -
-
-

- {event.details?.taskTitle ? event.details.taskTitle : (t(`gamification.source.${event.source}`, { defaultValue: event.source }) as string)} - {event.source === 'complete_task_late' && ({t('gamification.source.complete_task_late')})} -

-
- {new Date(event.createdAt).toLocaleString()} - {event.details?.taskTitle && ( - <> - ‱ - {t(`gamification.source.${event.source}`, { defaultValue: event.source })} - + return ( +
+
+
+ +
+
+
+ {t(`ranks.${rankKey}`)} + {isCurrentLevel && {t('gamification.level', { level })}} +
+
+ {t('gamification.rules.level', { level })} +
+
+
+
+
+ {t('gamification.rules.xp', { xp: threshold })} +
+ {isCurrentLevel && ( +
+ Current +
)}
-
- - +{event.amount} XP - + ); + })} +
+ + + + {/* XP Rewards */} + + +
+ +
+ {t('gamification.rules.actions')} + {t('gamification.rules.xpSystem')}
- )) - )} -
- -
- - -
- {/* Level Requirements */} - - -
- -
- {t('gamification.rules.levelRequirements')} - {t('gamification.rules.xpSystem')}
-
-
- -
- {LEVEL_THRESHOLDS.slice(0, 10).map((threshold, index) => { - const level = index + 1; - const rankKey = getRankKey(level); - const isCurrentLevel = getLevelFromXP(user.xp) === level; - - // Visual Configuration for Ranks - const getRankStyle = (l: number) => { - if (l >= 10) return { icon: Sun, color: "text-rose-500", bg: "bg-rose-500/10", border: "border-rose-500/20" }; - if (l >= 9) return { icon: Sparkles, color: "text-purple-500", bg: "bg-purple-500/10", border: "border-purple-500/20" }; - if (l >= 8) return { icon: Zap, color: "text-violet-500", bg: "bg-violet-500/10", border: "border-violet-500/20" }; - if (l >= 7) return { icon: Crown, color: "text-yellow-600", bg: "bg-yellow-600/10", border: "border-yellow-600/20" }; - if (l >= 6) return { icon: Star, color: "text-yellow-500", bg: "bg-yellow-500/10", border: "border-yellow-500/20" }; - if (l >= 5) return { icon: Medal, color: "text-orange-500", bg: "bg-orange-500/10", border: "border-orange-500/20" }; - if (l >= 4) return { icon: Award, color: "text-blue-500", bg: "bg-blue-500/10", border: "border-blue-500/20" }; - if (l >= 3) return { icon: Hammer, color: "text-cyan-500", bg: "bg-cyan-500/10", border: "border-cyan-500/20" }; - if (l >= 2) return { icon: BookOpen, color: "text-green-500", bg: "bg-green-500/10", border: "border-green-500/20" }; - return { icon: Scroll, color: "text-slate-500", bg: "bg-slate-500/10", border: "border-slate-500/20" }; - }; - - const style = getRankStyle(level); - const RankIcon = style.icon; - - return ( -
-
-
- + + +
+ {[ + { action: 'createTask', points: 10, icon: Plus, color: 'text-blue-500', bg: 'bg-blue-500/10' }, + { action: 'createSubtask', points: 5, icon: Plus, color: 'text-cyan-500', bg: 'bg-cyan-500/10' }, + { action: 'updateTask', points: 2, icon: CheckCircle2, color: 'text-slate-500', bg: 'bg-slate-500/10' }, + { action: 'completeTask', points: 50, icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10' }, + { action: 'completeTaskLate', points: 20, icon: CheckCircle2, color: 'text-yellow-500', bg: 'bg-yellow-500/10' }, + { action: 'aiAction', points: 5, icon: Sparkles, color: 'text-purple-500', bg: 'bg-purple-500/10' }, + { action: 'dailyStreak', points: 100, icon: Flame, color: 'text-orange-500', bg: 'bg-orange-500/10', tooltip: 'streakTooltip' }, + { action: 'weeklyStreak', points: 300, icon: Flame, color: 'text-orange-600', bg: 'bg-orange-600/10', tooltip: 'streakBonus' }, + { action: 'monthlyStreak', points: 1000, icon: Flame, color: 'text-red-500', bg: 'bg-red-500/10', tooltip: 'streakBonus' }, + ].map((item, index) => { + const ActionIcon = item.icon; + const content = ( +
+
+
+ +
+ {t(`gamification.rules.${item.action}`)}
-
-
- {t(`ranks.${rankKey}`)} - {isCurrentLevel && {t('gamification.level', { level })}} -
-
- {t('gamification.rules.level', { level })} -
+
+ +{item.points} XP
-
-
- {t('gamification.rules.xp', { xp: threshold })} -
- {isCurrentLevel && ( -
- Current -
- )} -
-
- ); - })} -
-
- + ); - {/* XP Rewards */} - - -
- -
- {t('gamification.rules.actions')} - {t('gamification.rules.xpSystem')} + if (item.tooltip) { + return ( + + + {content} + + +

{t(`gamification.rules.${item.tooltip}`)}

+
+
+ ); + } + + return content; + })}
-
-
- -
- {[ - { action: 'createTask', points: 10, icon: Plus, color: 'text-blue-500', bg: 'bg-blue-500/10' }, - { action: 'createSubtask', points: 5, icon: Plus, color: 'text-cyan-500', bg: 'bg-cyan-500/10' }, - { action: 'updateTask', points: 2, icon: CheckCircle2, color: 'text-slate-500', bg: 'bg-slate-500/10' }, - { action: 'completeTask', points: 50, icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10' }, - { action: 'completeTaskLate', points: 20, icon: CheckCircle2, color: 'text-yellow-500', bg: 'bg-yellow-500/10' }, - { action: 'aiAction', points: 5, icon: Sparkles, color: 'text-purple-500', bg: 'bg-purple-500/10' }, - { action: 'dailyStreak', points: 100, icon: Flame, color: 'text-orange-500', bg: 'bg-orange-500/10' }, - ].map((item, index) => { - const ActionIcon = item.icon; - return ( -
-
-
- -
- {t(`gamification.rules.${item.action}`)} -
-
- +{item.points} XP -
-
- ); - })} -
-
-
+ + +
- - -
+ )} +
+
); } diff --git a/client/src/pages/AdminUserManagement.tsx b/client/src/pages/AdminUserManagement.tsx index ebb6c7a..0583459 100644 --- a/client/src/pages/AdminUserManagement.tsx +++ b/client/src/pages/AdminUserManagement.tsx @@ -20,6 +20,16 @@ import { useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; export default function AdminUserManagement() { const { t } = useTranslation(); @@ -80,6 +90,18 @@ export default function AdminUserManagement() { onError: (e: Error) => toast({ title: "Failed to create", description: e.message, variant: "destructive" }), }); + const [resetXpUser, setResetXpUser] = useState(null); + + const resetXpMutation = useMutation({ + mutationFn: (userId: string) => apiRequest("POST", `/api/admin/users/${userId}/reset-xp`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/admin/users"] }); + setResetXpUser(null); + toast({ title: t('userManagement.resetXpSuccess', 'User XP reset successfully') }); + }, + onError: (e: Error) => toast({ title: "Failed to reset XP", description: e.message, variant: "destructive" }), + }); + return (
@@ -210,6 +232,14 @@ export default function AdminUserManagement() { > {user.isActive ? t('userManagement.table.deactivate') : t('userManagement.table.activate')} +
); } diff --git a/client/src/pages/AiChatPage.tsx b/client/src/pages/AiChatPage.tsx index f17f796..937fcd4 100644 --- a/client/src/pages/AiChatPage.tsx +++ b/client/src/pages/AiChatPage.tsx @@ -46,8 +46,8 @@ import { cn } from "@/lib/utils"; import { formatDistanceToNow } from "date-fns"; import { de } from "date-fns/locale"; import { useToast } from "@/hooks/use-toast"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; +// import ReactMarkdown from "react-markdown"; +// import remarkGfm from "remark-gfm"; // Types type Conversation = { @@ -93,7 +93,8 @@ const TypewriterMessage = ({ content, onComplete }: { content: string, onComplet return (
- {displayedContent} + {/* {displayedContent} */} + {displayedContent}
); }; @@ -168,7 +169,32 @@ export default function AiChatPage() { }, }); - // ... (Delete/Rename skipped for brevity in prompt, keeping existing) ... + // Delete Conversation + const deleteConversationMutation = useMutation({ + mutationFn: async (id: string) => { + const res = await apiRequest("DELETE", `/api/ai/conversations/${id}`); + if (!res.ok) throw new Error("Failed to delete conversation"); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations"] }); + if (selectedConversationId === deleteId) { + setSelectedConversationId(null); + } + toast({ title: t('common.deleted', 'Deleted') }); + }, + }); + + // Rename Conversation + const renameConversationMutation = useMutation({ + mutationFn: async ({ id, title }: { id: string, title: string }) => { + const res = await apiRequest("PATCH", `/api/ai/conversations/${id}`, { title }); + if (!res.ok) throw new Error("Failed to rename conversation"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations"] }); + }, + }); const sendMessageMutation = useMutation({ mutationFn: async ({ conversationId, content }: { conversationId: string, content: string }) => { @@ -592,7 +618,8 @@ export default function AiChatPage() { scrollToBottom()} /> ) : (
- {msg.content} + {/* {msg.content} */} + {msg.content}
) ) : ( diff --git a/client/src/pages/AuthPage.tsx b/client/src/pages/AuthPage.tsx index b9b4084..65f3ef9 100644 --- a/client/src/pages/AuthPage.tsx +++ b/client/src/pages/AuthPage.tsx @@ -31,9 +31,16 @@ import { BrainCircuit } from "lucide-react"; export default function AuthPage() { const { t } = useTranslation(); const { toast } = useToast(); + const [, setLocation] = useLocation(); const queryClient = useQueryClient(); const [activeTab, setActiveTab] = useState("login"); + // 2FA State + const [is2FARequired, setIs2FARequired] = useState(false); + const [twoFAUserId, setTwoFAUserId] = useState(null); + const [twoFAEmail, setTwoFAEmail] = useState(null); + const [otpCode, setOtpCode] = useState(""); + const { data: settings } = useQuery<{ registration_enabled: boolean }>({ queryKey: ["/api/settings/public"], queryFn: async () => { @@ -52,14 +59,26 @@ export default function AuthPage() { headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); + // Handle 200 OK could be user or 2fa_required + const json = await res.json(); + if (!res.ok) { - throw new Error("Invalid username or password"); + throw new Error(json.message || "Invalid username or password"); } - return res.json(); + return json; }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["/api/user"] }); - toast({ title: "Welcome back!" }); + onSuccess: (data) => { + if (data.message === "2fa_required") { + setIs2FARequired(true); + setTwoFAUserId(data.userId); + setTwoFAEmail(data.email); + toast({ title: t('auth.2faCodeSent'), description: t('auth.checkEmail') }); + } else { + queryClient.setQueryData(["/api/user"], data); + queryClient.invalidateQueries({ queryKey: ["/api/user"] }); + toast({ title: "Welcome back!" }); + setLocation('/'); + } }, onError: (error: Error) => { toast({ @@ -70,6 +89,38 @@ export default function AuthPage() { }, }); + const verify2FAMutation = useMutation({ + mutationFn: async (data: { userId: string, code: string }) => { + const res = await fetch("/api/auth/verify-2fa", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); + }, + onSuccess: (user) => { + queryClient.setQueryData(["/api/user"], user); + queryClient.invalidateQueries({ queryKey: ["/api/user"] }); + toast({ title: "Verification Successful", description: "Welcome back!" }); + setLocation('/'); + }, + onError: (error: Error) => { + toast({ + title: "Verification Failed", + description: error.message, + variant: "destructive", + }); + } + }); + + const handleVerify2FA = (e: React.FormEvent) => { + e.preventDefault(); + if (twoFAUserId && otpCode) { + verify2FAMutation.mutate({ userId: twoFAUserId, code: otpCode }); + } + }; + const registerMutation = useMutation({ mutationFn: async (data: InsertUser) => { const res = await fetch("/api/register", { @@ -83,19 +134,59 @@ export default function AuthPage() { } return res.json(); }, - onSuccess: () => { + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ["/api/user"] }); + queryClient.setQueryData(["/api/user"], data); toast({ title: "Account created!" }); + setLocation('/'); }, // Error handling is done in the form submission handler to set field errors }); + if (is2FARequired) { + return ( +
+ + +
+ Logo +
+ {t('auth.2faVerification')} + + {t('auth.enterCodeSentTo')} {twoFAUserId && twoFAEmail ? twoFAEmail : 'your email'} + +
+ +
+
+ setOtpCode(e.target.value.replace(/\D/g, ''))} + /> +

{t('auth.codeExpiresIn10')}

+
+ + +
+
+
+
+ ); + } + return ( -
+
-
- +
+ Logo

{t('auth.heroTitle')}

@@ -107,8 +198,8 @@ export default function AuthPage() {

-
- +
+ Logo
{t('auth.welcomeBack')} diff --git a/client/src/pages/FocusRoutinePage.tsx b/client/src/pages/FocusRoutinePage.tsx index 2c4b775..8b90fc8 100644 --- a/client/src/pages/FocusRoutinePage.tsx +++ b/client/src/pages/FocusRoutinePage.tsx @@ -12,6 +12,7 @@ import { apiRequest } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { format } from "date-fns"; import { useState } from "react"; +import { triggerConfetti } from "@/lib/confetti"; export default function FocusRoutinePage() { const [match, params] = useRoute("/focus/routine/:type"); @@ -174,5 +175,4 @@ function getPriorityColor(priority: string) { return 'bg-blue-500'; } -// Temporary import fix if confetti not available in module scope -import { triggerConfetti } from "@/lib/confetti"; +// End of file diff --git a/client/src/pages/NotificationsPage.tsx b/client/src/pages/NotificationsPage.tsx new file mode 100644 index 0000000..0f782fd --- /dev/null +++ b/client/src/pages/NotificationsPage.tsx @@ -0,0 +1,134 @@ +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { Task } from '@shared/schema'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Bell, BellOff, Calendar, AlertCircle, CheckCircle2, Clock } from 'lucide-react'; +import { useNotifications } from '@/hooks/use-notifications'; +import { formatDistanceToNow, isPast, isToday, addDays, isBefore } from 'date-fns'; +import { de, enUS } from 'date-fns/locale'; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; + +export default function NotificationsPage() { + const { t, i18n } = useTranslation(); + const { enabled, toggleEnabled, permission, requestPermission } = useNotifications({ poll: false }); // Don't double poll here + + const { data: tasks = [] } = useQuery({ + queryKey: ['/api/tasks'], + }); + + const now = new Date(); + + // Derive notifications from tasks + const notifications = tasks.flatMap(task => { + if (!task.dueDate || task.status === 'done') return []; + + const dueDate = new Date(task.dueDate); + const isOverdue = isBefore(dueDate, now); + // Upcoming: due within next 24 hours + const isUpcoming = !isOverdue && isBefore(dueDate, addDays(now, 1)); + + if (!isOverdue && !isUpcoming) return []; + + return [{ + id: task.id, + type: isOverdue ? 'overdue' : 'upcoming', + task, + timestamp: dueDate + }]; + }).sort((a, b) => { + // Sort: Overdue first, then by time + if (a.type !== b.type) return a.type === 'overdue' ? -1 : 1; + return a.timestamp.getTime() - b.timestamp.getTime(); + }); + + return ( +
+
+
+

{t('notifications.title', 'Notifications')}

+

+ {t('notifications.description', 'Manage your alerts and view important updates.')} +

+
+
+ + {/* Settings Card */} + + +
+ + {t('notifications.settings.title', 'Browser Notifications')} +
+
+ +
+
+ +

+ {permission === 'denied' + ? {t('notifications.settings.denied', 'Permission denied. Please enable in browser settings.')} + : t('notifications.settings.description', 'Receive alerts for upcoming and overdue tasks.')} +

+
+ +
+ {permission === 'default' && ( +
+ +
+ )} +
+
+ + {/* Notification List */} +
+

+ + {t('notifications.recent', 'Recent Alerts')} +

+ + {notifications.length === 0 ? ( +
+ +

{t('notifications.empty', 'All caught up! No urgent alerts.')}

+
+ ) : ( + notifications.map((notif) => ( + + +
+ {notif.type === 'overdue' ? : } +
+
+
+

{notif.task.title}

+ + {formatDistanceToNow(notif.timestamp, { addSuffix: true, locale: i18n.language === 'de' ? de : enUS })} + +
+

+ {notif.type === 'overdue' + ? t('notifications.overdueBody', 'This task is overdue!') + : t('notifications.upcomingBody', { time: formatDistanceToNow(notif.timestamp, { locale: i18n.language === 'de' ? de : enUS }) }) + } +

+
+
+
+ )) + )} +
+
+ ); +} diff --git a/client/src/pages/SetupWizard.tsx b/client/src/pages/SetupWizard.tsx index 241b9c2..3660800 100644 --- a/client/src/pages/SetupWizard.tsx +++ b/client/src/pages/SetupWizard.tsx @@ -31,6 +31,7 @@ export default function SetupWizard() { }, onSuccess: (user) => { queryClient.setQueryData(["/api/user"], user); + queryClient.invalidateQueries({ queryKey: ["/api/setup/status"] }); setLocation("/"); toast({ title: "Setup Complete", diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index f2af71b..344f254 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; -import { Globe, Tag, Plus, Edit, Trash2, Layers, User as UserIcon, ShieldCheck, Share2, Server, Copy, Settings as SettingsIcon, Bell } from 'lucide-react'; +import { Globe, Tag, Plus, Edit, Trash2, Layers, User as UserIcon, ShieldCheck, Share2, Server, Copy, Settings as SettingsIcon, Bell, Loader2 } from 'lucide-react'; import { Switch } from '@/components/ui/switch'; import { ShareAccessModal } from '@/components/ShareAccessModal'; import { ChangePasswordModal } from '@/components/ChangePasswordModal'; @@ -18,7 +18,7 @@ import { queryClient, apiRequest } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { useLocation } from "wouter"; import { useNotifications } from '@/hooks/use-notifications'; -import { DataExportCard } from '@/components/user/DataExportCard'; +// import { DataExportCard } from '@/components/user/DataExportCard'; const NotificationSettings = () => { const { t } = useTranslation(); @@ -62,10 +62,29 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) { const [, setLocation] = useLocation(); // Fetch user - const { data: user } = useQuery({ + const { data: user, isLoading: isLoadingUser } = useQuery({ queryKey: ['/api/user'] }); + if (isLoadingUser) { + return ( +
+ +
+ ); + } + + if (!user) { + return ( +
+

{t('common.loginRequired')}

+ +
+ ); + } + const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false); const [editingLabel, setEditingLabel] = useState
handlePrivacyUpdate({ showOnLeaderboard: checked })} />
@@ -306,7 +328,7 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {

{t('settings.social.searchableDesc')}

handlePrivacyUpdate({ isSearchable: checked })} />
@@ -316,10 +338,20 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {

{t('settings.ai.enableUserDesc')}

handlePrivacyUpdate({ aiEnabled: checked })} />
+
+
+

{t('settings.social.2fa')}

+

{t('settings.social.2faDesc')}

+
+ handlePrivacyUpdate({ is2faEnabled: checked } as any)} + /> +