feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
continuous-integration/drone/push Build is failing

- implemented /notifications page with overdue/upcoming alerts
- Fixed sidebar scrolling in collapsed mode
- Moved notification button to sidebar footer
- Enhanced Achievements page with streak stats and tooltips
- Improved XP history to show task titles
- Added missing translations (en/de)
- Removed top bar header
- Fixed Docker environment routing
This commit is contained in:
2025-12-17 10:06:36 +01:00
parent 7b79015ac2
commit 9819d8db0b
47 changed files with 2309 additions and 1241 deletions
+237 -234
View File
@@ -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<Task[]>({
const { data: tasksData } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
enabled: !!user,
select: (data) => data.map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}))
});
const { data: labels = [] } = useQuery<Label[]>({
const tasks = (tasksData ?? []).map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}));
const { data: labelsData } = useQuery<Label[]>({
queryKey: ['/api/labels'],
enabled: !!user,
});
const labels = labelsData ?? [];
const handleCreateTask = async (newTask: Partial<Task>) => {
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 (
<Switch>
<Route path="/setup" component={SetupWizard} />
<Route component={SetupWizard} />
</Switch>
);
}
if (isBlocked) {
return (
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
{/* Catch all redirects to nothing, the hook handles the push to routine page */}
<Route component={() => <div />} />
</Switch>
<Toaster />
</div>
);
}
if (isBlocked) {
return (
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="min-h-screen bg-background flex flex-col">
{user && (
<header className="flex h-16 shrink-0 items-center gap-2 border-b pl-4 pr-6">
<SidebarTrigger />
<div className="flex-1" />
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => setLocation('/notifications')}>
{/* Bell icon would go here */}
</Button>
<div className="flex items-center gap-2">
<span className="text-sm text-yellow-500 font-bold"> {user.xp}</span>
<span className="text-sm text-orange-500 font-bold">🔥 {user.currentStreak}</span>
</div>
</div>
</header>
)}
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/tasks">
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/calendar">
<CalendarView
tasks={tasks}
onTaskDrop={handleTaskDrop}
onTaskClick={handleTaskClick}
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/kanban">
<KanbanBoard
tasks={tasks}
onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/weeklist">
<WeekListView
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
{user ? <AchievementsPage user={user} /> : <AuthPage />}
</Route>
<Route path="/unscheduled">
{user ? (
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
) : (
<AuthPage />
)}
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user.role === 'admin' && (
<>
<Route path="/admin/users" component={AdminUserManagement} />
<Route path="/admin/settings" component={AdminSettings} />
</>
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
<Route component={NotFound} />
</Switch>
</main>
{/* Floating Action Button */}
<div className="fixed bottom-8 right-8 z-[100]">
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
onClick={() => setIsCreateModalOpen(true)}
data-testid="fab-create-task"
>
<Plus className="h-6 w-6 text-white" />
</Button>
</div>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
<PomodoroOverlay
isOpen={showPomodoro}
onClose={() => setShowPomodoro(false)}
taskId={activePomodoroTaskId}
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
/>
<Toaster />
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
{/* Catch all redirects to nothing, the hook handles the push to routine page */}
<Route component={() => <div />} />
</Switch>
<Toaster />
</div>
);
}
if (!user) {
return (
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/auth" component={AuthPage} />
<Route path="/forgot-password" component={ForgotPasswordPage} />
<Route path="/reset-password" component={ResetPasswordPage} />
<Route component={AuthPage} />
</Switch>
<Toaster />
</div>
);
}
return (
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="min-h-screen bg-background flex flex-col">
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/focus">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/calendar">
<CalendarView tasks={tasks} />
</Route>
<Route path="/weeklist">
<WeekListView tasks={tasks} onTaskUpdate={handleTaskUpdate} />
</Route>
<Route path="/kanban">
<KanbanBoard
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
/>
</Route>
<Route path="/auth">
{() => {
setLocation('/');
return null;
}}
</Route>
<Route path="/tasks">
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
<AchievementsPage user={user} />
</Route>
<Route path="/unscheduled">
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/notifications">
<NotificationsPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user?.role === 'admin' && (
<>
<Route path="/admin/users" component={AdminUserManagement} />
<Route path="/admin/settings" component={AdminSettings} />
</>
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
<Route component={NotFound} />
</Switch>
</main>
{/* Floating Action Button */}
<div className="fixed bottom-8 right-8 z-[100]">
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
onClick={() => setIsCreateModalOpen(true)}
data-testid="fab-create-task"
>
<Plus className="h-6 w-6 text-white" />
</Button>
</div>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
<PomodoroOverlay
isOpen={showPomodoro}
onClose={() => setShowPomodoro(false)}
taskId={activePomodoroTaskId}
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
/>
<Toaster />
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
);
}
export default App;
+22 -7
View File
@@ -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) {
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground h-14">
<div className="flex aspect-square size-9 items-center justify-center rounded-xl bg-gradient-to-br from-violet-600 to-indigo-600 text-white shadow-lg">
<CheckSquare className="size-5" />
<div className="flex aspect-square size-9 items-center justify-center rounded-xl overflow-hidden shadow-lg bg-transparent">
<img src="/favicon.png" alt="Logo" className="w-full h-full object-cover" />
</div>
{state !== 'collapsed' && (
<div className="grid flex-1 text-left text-sm leading-tight ml-2">
@@ -103,18 +105,31 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
))}
</SidebarMenu>
</SidebarContent>
<SidebarSeparator />
<SidebarFooter>
{state !== 'collapsed' && user && (
<GamificationBar xp={user.xp} level={user.level} streak={user.currentStreak} />
)}
<div className={`p-4 ${state === 'collapsed'
? 'flex flex-col items-center justify-center gap-4'
: 'grid grid-cols-3 items-center'
: 'grid grid-cols-4 items-center gap-2'
}`}>
<div className={state === 'collapsed' ? '' : 'justify-self-start'}>
<ThemeToggle />
</div>
<div className={state === 'collapsed' ? '' : 'justify-self-center'}>
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/notifications')}
className="bg-transparent hover:bg-sidebar-accent"
title={t('navigation.notifications', 'Notifications')}
>
<Bell className="size-4" />
</Button>
</div>
<div className={state === 'collapsed' ? '' : 'justify-self-center'}>
<Button
variant="ghost"
@@ -122,7 +137,7 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
onClick={() => logoutMutation.mutate()}
disabled={logoutMutation.isPending}
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
title="Logout"
title={t('auth.logout', 'Logout')}
>
<LogOut className="size-4" />
</Button>
-1
View File
@@ -21,7 +21,6 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
HoverCard,
+49
View File
@@ -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<Props, State> {
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 (
<div className="min-h-screen flex items-center justify-center p-4 bg-red-50 text-red-900">
<div className="max-w-xl p-8 bg-white rounded-lg shadow-xl border border-red-200">
<h1 className="text-2xl font-bold mb-4">Something went wrong</h1>
<p className="mb-4">The application crashed. Here is the error:</p>
<pre className="bg-red-100 p-4 rounded overflow-auto text-sm font-mono">
{this.state.error?.toString()}
</pre>
<button
className="mt-6 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={() => window.location.reload()}
>
Reload Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
+1
View File
@@ -30,6 +30,7 @@ interface KanbanBoardProps {
onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskClick?: (task: Task) => void;
onTaskEdit?: (task: Task) => void;
onTaskDelete?: (taskId: string) => void;
onStartTimer?: (taskId: string) => void;
onStopTimer?: (taskId: string) => void;
+1 -1
View File
@@ -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;
+18 -10
View File
@@ -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;
+3 -6
View File
@@ -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<User>({ queryKey: ["/api/user"], retry: false });
+21 -4
View File
@@ -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<string | null>(null);
// Fetch labels
const { data: labels = [] } = useQuery<Label[]>({
const { data: labelsData } = useQuery<Label[]>({
queryKey: ['/api/labels'],
});
const labels = labelsData ?? [];
const { data: tasks = [] } = useQuery<Task[]>({
const { data: tasksData } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const tasks = tasksData ?? [];
const handleSave = () => {
if (!title.trim()) {
@@ -193,7 +196,21 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<Select value={energyLevel} onValueChange={(value: 'low' | 'medium' | 'high') => setEnergyLevel(value)}>
<SelectTrigger>
<SelectValue placeholder={t('taskCreation.energy')} />
<div className="flex items-center gap-2">
<SelectValue placeholder={t('taskCreation.energy')} />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-help" onClick={(e) => e.stopPropagation()}>
<p className="sr-only">Help</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">{t('taskCreation.energyHelp', 'Match tasks to your energy levels. Low energy tasks are good for slumps, High energy tasks require focus.')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="low">{t('gamification.energy.low')}</SelectItem>
+2 -1
View File
@@ -66,9 +66,10 @@ export default function TaskDetailsModal({
const [isCreatingSubtask, setIsCreatingSubtask] = useState(false);
const [isScheduling, setIsScheduling] = useState(false);
const { data: tasks = [] } = useQuery<Task[]>({
const { data: tasksData } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const tasks = tasksData ?? [];
// Notes state
const [notes, setNotes] = useState('');
@@ -51,7 +51,7 @@ export function SMTPSettingsCard() {
mutationFn: (data: Partial<typeof settings>) => apiRequest("POST", "/api/admin/settings", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/admin/settings"] });
toast({ title: t('smtp.saveSuccess', "Settings updated") });
toast({ title: t('settings.smtp.saveSuccess', "Settings updated") });
},
});
@@ -1,17 +0,0 @@
import BottomNavigation from '../BottomNavigation';
export default function BottomNavigationExample() {
return (
<div className="relative h-[400px] bg-muted/20">
<div className="p-4">
<h3 className="text-lg font-semibold mb-2">App Content Area</h3>
<p className="text-muted-foreground">
This is where your main app content would be displayed.
The bottom navigation is fixed at the bottom of the screen.
</p>
</div>
<BottomNavigation />
</div>
);
}
@@ -1,50 +0,0 @@
import CalendarView from '../CalendarView';
import { Task } from '../TaskCard';
import { addDays } from 'date-fns';
const mockTasks: Task[] = [
{
id: '1',
title: 'Team meeting',
status: 'todo',
priority: 'high',
dueDate: new Date(),
timeTracked: 0,
isTracking: false
},
{
id: '2',
title: 'Review design mockups',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 2),
timeTracked: 30,
isTracking: false
},
{
id: '3',
title: 'Client presentation',
status: 'todo',
priority: 'high',
dueDate: addDays(new Date(), 5),
timeTracked: 0,
isTracking: false
},
{
id: '4',
title: 'Code review',
status: 'todo',
priority: 'low',
dueDate: addDays(new Date(), 1),
timeTracked: 0,
isTracking: false
}
];
export default function CalendarViewExample() {
return (
<div className="p-4">
<CalendarView tasks={mockTasks} />
</div>
);
}
@@ -1,75 +0,0 @@
import KanbanBoard from '../KanbanBoard';
import { Task } from '../TaskCard';
import { addDays } from 'date-fns';
const mockTasks: Task[] = [
{
id: '1',
title: 'Design new landing page',
description: 'Create wireframes and mockups for the new product landing page',
status: 'todo',
priority: 'high',
dueDate: addDays(new Date(), 3),
timeTracked: 0,
isTracking: false,
projectId: 'project-1'
},
{
id: '2',
title: 'Implement user authentication',
description: 'Set up login, registration, and password reset functionality',
status: 'inProgress',
priority: 'high',
dueDate: addDays(new Date(), 1),
timeTracked: 120,
isTracking: true,
projectId: 'project-2'
},
{
id: '3',
title: 'Write API documentation',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 5),
timeTracked: 45,
isTracking: false,
projectId: 'project-1'
},
{
id: '4',
title: 'Set up CI/CD pipeline',
description: 'Configure automated testing and deployment',
status: 'done',
priority: 'medium',
dueDate: addDays(new Date(), -2),
timeTracked: 180,
isTracking: false,
projectId: 'project-2'
},
{
id: '5',
title: 'Review code changes',
status: 'todo',
priority: 'low',
dueDate: new Date(),
timeTracked: 0,
isTracking: false
},
{
id: '6',
title: 'Update dependencies',
status: 'done',
priority: 'low',
dueDate: addDays(new Date(), -1),
timeTracked: 30,
isTracking: false
}
];
export default function KanbanBoardExample() {
return (
<div className="p-4">
<KanbanBoard tasks={mockTasks} />
</div>
);
}
@@ -1,9 +0,0 @@
import ProjectTemplate from '../ProjectTemplate';
export default function ProjectTemplateExample() {
return (
<div className="p-4">
<ProjectTemplate />
</div>
);
}
@@ -1,34 +0,0 @@
import TaskCard from '../TaskCard';
import { Task } from '@shared/schema';
const mockTask: Task = {
id: '1',
title: 'Design mobile task interface',
description: 'Create wireframes and mockups for the mobile-first task management interface',
status: 'inProgress',
priority: 'high',
dueDate: new Date('2024-09-20'),
timeTracked: 45,
isTracking: false,
projectId: 'project-1',
notes: 'Focus on mobile-first design principles'
};
export default function TaskCardExample() {
return (
<div className="p-4 space-y-4">
<TaskCard task={mockTask} />
<TaskCard
task={{
...mockTask,
id: '2',
title: 'Quick task without details',
description: undefined,
status: 'todo',
priority: 'low',
timeTracked: 0
}}
/>
</div>
);
}
@@ -1,29 +0,0 @@
import { useState } from 'react';
import TaskCreationModal from '../TaskCreationModal';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
export default function TaskCreationModalExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="p-4">
<Button
onClick={() => setIsOpen(true)}
className="w-full flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Create New Task
</Button>
<TaskCreationModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSave={(task) => {
console.log('Task saved:', task);
setIsOpen(false);
}}
/>
</div>
);
}
@@ -1,77 +0,0 @@
import TaskList from '../TaskList';
import { Task } from '../TaskCard';
import { addDays, subDays } from 'date-fns';
const mockTasks: Task[] = [
{
id: '1',
title: 'Review quarterly reports',
description: 'Analyze Q3 performance metrics and prepare summary',
status: 'todo',
priority: 'high',
dueDate: addDays(new Date(), 2),
timeTracked: 0,
isTracking: false,
projectId: 'project-1'
},
{
id: '2',
title: 'Update website copy',
description: 'Revise landing page content based on user feedback',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 5),
timeTracked: 90,
isTracking: true,
projectId: 'project-2'
},
{
id: '3',
title: 'Team standup meeting',
status: 'todo',
priority: 'low',
dueDate: new Date(),
timeTracked: 0,
isTracking: false
},
{
id: '4',
title: 'Fix login bug',
description: 'Users unable to login with special characters in password',
status: 'todo',
priority: 'high',
dueDate: subDays(new Date(), 1), // Overdue
timeTracked: 30,
isTracking: false,
projectId: 'project-2'
},
{
id: '5',
title: 'Deploy new features',
status: 'done',
priority: 'medium',
dueDate: subDays(new Date(), 2),
timeTracked: 120,
isTracking: false,
projectId: 'project-1'
},
{
id: '6',
title: 'Write documentation',
description: 'Document new API endpoints for external developers',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 7),
timeTracked: 60,
isTracking: false,
projectId: 'project-1'
}
];
export default function TaskListExample() {
return (
<div className="p-4">
<TaskList tasks={mockTasks} />
</div>
);
}
@@ -1,19 +0,0 @@
import ThemeToggle from '../ThemeToggle';
export default function ThemeToggleExample() {
return (
<div className="p-4 space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-semibold">Dark Mode</h3>
<p className="text-sm text-muted-foreground">Toggle between light and dark themes</p>
</div>
<ThemeToggle />
</div>
<div className="p-4 bg-card border rounded-lg">
<p className="text-sm">This card will change appearance when you toggle the theme.</p>
</div>
</div>
);
}
+1 -1
View File
@@ -375,7 +375,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto",
className
)}
{...props}
+49 -36
View File
@@ -12,7 +12,15 @@
"upcomingTitle": "Demnächst: {{task}}",
"upcomingBody": "Fällig in {{time}}",
"overdueTitle": "Überfällig: {{task}}",
"overdueBody": "Diese Aufgabe ist jetzt überfällig!"
"overdueBody": "Diese Aufgabe ist jetzt überfällig!",
"settings": {
"title": "Browser-Benachrichtigungen",
"description": "Erhalten Sie Warnungen für bevorstehende und überfällige Aufgaben.",
"denied": "Zugriff verweigert. Bitte in den Browsereinstellungen aktivieren.",
"request": "Zugriff anfordern"
},
"recent": "Aktuelle Warnungen",
"empty": "Alles erledigt! Keine urgierten Warnungen."
},
"unscheduled": {
"title": "Ungeplante Aufgaben",
@@ -267,7 +275,9 @@
"publicLeaderboardDesc": "Mein Profil auf der globalen Bestenliste anzeigen",
"searchable": "Auffindbarkeit erlauben",
"searchableDesc": "Anderen Benutzern erlauben, mich zum Teilen von Aufgaben zu finden",
"shareAccess": "Zugriff auf alle Aufgaben teilen..."
"shareAccess": "Zugriff auf alle Aufgaben teilen...",
"2fa": "Zwei-Faktor-Authentifizierung",
"2faDesc": "Ihr Konto mit E-Mail-basierter 2FA sichern"
},
"admin": {
"title": "Administration",
@@ -317,6 +327,10 @@
"deletedDescription": "Ihr Label wurde erfolgreich gelöscht.",
"share": "Label teilen"
},
"security": {
"2fa": "Zwei-Faktor-Authentifizierung",
"2faDesc": "Sichern Sie Ihr Konto mit E-Mail-basierter 2FA"
},
"templates": {
"title": "Projektvorlagen",
"description": "Verwende Vorlagen zum schnellen Erstellen von Projekten",
@@ -354,8 +368,8 @@
"baseUrl": "Basis-URL",
"systemPrompt": "System-Prompt",
"systemPromptPlaceholder": "Definieren Sie die Persona und Regeln der KI...",
"enableUser": "KI-Assistent aktivieren",
"enableUserDesc": "KI-Chat-Widget anzeigen.",
"enableUser": "KI-Chat aktivieren",
"enableUserDesc": "Zugriff auf den KI-Chat-Assistenten erlauben",
"save": "KI-Einstellungen speichern",
"saved": "Einstellungen gespeichert",
"error": "Fehler beim Speichern",
@@ -563,7 +577,7 @@
"create_subtask": "Teilaufgabe erstellt",
"update_task": "Aufgabe aktualisiert",
"complete_task": "Aufgabe erledigt",
"complete_task_late": "Verspätet erledigt",
"complete_task_late": "Aufgabe erledigt",
"ai_action": "AI Aktion",
"daily_streak": "Täglicher Streak"
},
@@ -576,12 +590,14 @@
"actions": "XP Aktionen",
"createTask": "Aufgabe erstellen",
"createSubtask": "Unteraufgabe erstellen",
"updateTask": "Aufgabe aktualisieren",
"updateTask": "Aufgabe aktualisiert",
"completeTask": "Aufgabe erledigen",
"completeTaskLate": "Verspätet erledigen",
"aiAction": "AI Nutzung",
"dailyStreak": "Täglicher Streak",
"streakTooltip": "Melde dich täglich an, um deinen Streak zu erhöhen!",
"dailyStreak": "Tägliche Strähne",
"weeklyStreak": "Wochensträhne (7 Tage)",
"monthlyStreak": "Monatssträhne (30 Tage)",
"streakTooltip": "Melde dich täglich an, um deine Strähne zu erhöhen!",
"streakBonus": "Wöchentliche & Monatliche Boni verfügbar: 7 Tage (+300 XP), 30 Tage (+1000 XP)."
}
},
@@ -605,6 +621,10 @@
"oct": "Okt",
"nov": "Nov",
"dec": "Dez",
"week_1": "Woche 1",
"week_2": "Woche 2",
"week_3": "Woche 3",
"week_4": "Woche 4",
"cw": "KW"
},
"ranks": {
@@ -617,7 +637,17 @@
"grandmaster": "Großmeister",
"virtuoso": "Virtuose",
"legend": "Legende",
"mythic": "Mythisch"
"mythic": "Mythisch",
"titan": "Titan",
"sentinel": "Wächter",
"vanguard": "Vorhut",
"oracle": "Orakel",
"ascendant": "Aufgestiegener",
"ethereal": "Ätherisch",
"celestial": "Himmlisch",
"divine": "Göttlich",
"omnipotent": "Allmächtig",
"eternal": "Ewiger"
},
"leaderboardPage": {
"title": "Bestenliste",
@@ -651,6 +681,7 @@
"achievements": {
"title": "Erfolge",
"subtitle": "Verfolge deinen Fortschritt und deine Ziele",
"overview": "Übersicht",
"rewardsDescription": "Gib deine {{xp}} EP für exklusive Belohnungen aus!",
"addReward": "Belohnung hinzufügen",
"createCustomReward": "Eigene Belohnung erstellen",
@@ -659,6 +690,8 @@
"rewardCost": "Kosten (EP)",
"createRewardBtn": "Belohnung erstellen",
"currentStreak": "Aktuelle Serie",
"weeklyStreak": "Wochensträhne",
"monthlyStreak": "Monatssträhne",
"days": "{{count}} Tage",
"bestStreak": "Rekord: {{count}} Tage",
"xpActivity": "EP Aktivität",
@@ -718,32 +751,6 @@
}
}
},
"analytics": {
"mon": "Mo",
"tue": "Di",
"wed": "Mi",
"thu": "Do",
"fri": "Fr",
"sat": "Sa",
"sun": "So",
"jan": "Jan",
"feb": "Feb",
"mar": "Mär",
"apr": "Apr",
"may": "Mai",
"jun": "Jun",
"jul": "Jul",
"aug": "Aug",
"sep": "Sep",
"oct": "Okt",
"nov": "Nov",
"dec": "Dez",
"week_1": "Woche 1",
"week_2": "Woche 2",
"week_3": "Woche 3",
"week_4": "Woche 4",
"cw": "KW"
},
"energy": {
"low": "⚡ Wenig Energie",
"medium": "⚡⚡ Mittlere Energie",
@@ -813,6 +820,12 @@
"createAccount": "Konto erstellen",
"registrationDisabled": "Registrierung ist derzeit deaktiviert.",
"heroTitle": "TaskFlow",
"heroSubtitle": "Meistern Sie Ihre Produktivität mit KI-gesteuertem Aufgabenmanagement, gamifizierten Erfolgen und intelligenten Fokusmodi."
"heroSubtitle": "Meistern Sie Ihre Produktivität mit KI-gesteuertem Aufgabenmanagement, gamifizierten Erfolgen und intelligenten Fokusmodi.",
"2faVerification": "2FA Verifizierung",
"2faCodeSent": "2FA Code gesendet",
"checkEmail": "Bitte überprüfen Sie Ihre E-Mail auf den Bestätigungscode.",
"enterCodeSentTo": "Geben Sie den 6-stelligen Code ein, der an ... gesendet wurde",
"codeExpiresIn10": "Der Code läuft in 10 Minuten ab",
"verify": "Verifizieren"
}
}
+108 -77
View File
@@ -12,7 +12,15 @@
"upcomingTitle": "Upcoming: {{task}}",
"upcomingBody": "Due in {{time}}",
"overdueTitle": "Overdue: {{task}}",
"overdueBody": "This task is now overdue!"
"overdueBody": "This task is now overdue!",
"settings": {
"title": "Browser Notifications",
"description": "Receive alerts for upcoming and overdue tasks.",
"denied": "Permission denied. Please enable in browser settings.",
"request": "Request Permission"
},
"recent": "Recent Alerts",
"empty": "All caught up! No urgent alerts."
},
"unscheduled": {
"title": "Unscheduled Tasks",
@@ -267,7 +275,9 @@
"publicLeaderboardDesc": "Show my profile on the global leaderboard",
"searchable": "Allow others to find me",
"searchableDesc": "Allow users to search for me to share tasks",
"shareAccess": "Share access to all tasks..."
"shareAccess": "Share access to all tasks...",
"2fa": "Two-Factor Authentication",
"2faDesc": "Secure your account with email-based 2FA"
},
"admin": {
"title": "Admin Settings",
@@ -317,85 +327,85 @@
"deletedDescription": "Your label has been deleted successfully.",
"share": "Share Label"
},
"security": {
"2fa": "Two-Factor Authentication",
"2faDesc": "Secure your account with email-based 2FA"
},
"ai": {
"enableUser": "Enable AI Assistant",
"enableUserDesc": "Allow the AI assistant to help you with tasks and organization."
},
"mcp": {
"title": "MCP Server",
"description": "Configure the Model Context Protocol server.",
"enableLabel": "Enable MCP Server",
"enableDesc": "Turn on the MCP server to expose task data to AI agents.",
"status": "Status",
"running": "Running",
"url": "Server URL",
"apiKey": "API Key",
"noKey": "No API Key configured",
"generate": "Generate New Key",
"instructions": "The MCP server runs on the same port as the application (/api/mcp).",
"portLabel": "Port",
"portDesc": "The port is determined by the main application."
},
"templates": {
"title": "Project Templates",
"description": "Use templates to quickly create projects",
"manageTemplates": "Manage Templates"
},
"mcp": {
"title": "MCP Server Configuration",
"description": "Configure the Model Context Protocol (MCP) server settings.",
"descriptionDetail": "The MCP server runs on the same port as the application (/api/mcp).",
"enableLabel": "Enable MCP Server",
"enableDesc": "Allow external AI tools to connect via MCP protocol.",
"portLabel": "Port (Informational)",
"portDesc": "Currently runs on the main application port. Separate port configuration coming soon.",
"status": "Server Status",
"running": "Active",
"url": "Endpoint URL (SSE)",
"apiKey": "Access Token",
"generate": "Generate Token",
"revoke": "Revoke Token",
"generated": "Access Token generated",
"revoked": "Access Token revoked",
"noKey": "No token active. Generate one to connect.",
"copy": "Copy to Clipboard",
"copied": "Copied!",
"instructions": "Configure your MCP client (e.g. Claude Desktop) with this URL and Token."
},
"ai": {
"title": "AI Configuration",
"description": "Configure the global AI provider for the assistant.",
"provider": "AI Provider",
"apiKey": "API Key",
"model": "Model Name",
"baseUrl": "Base URL",
"systemPrompt": "System Prompt",
"systemPromptPlaceholder": "Define the AI's persona and rules...",
"save": "Save AI Settings",
"saved": "Settings saved",
"error": "Failed to save settings",
"ollama": {
"placeholder": "Optional for Ollama",
"pull": "Pull Model",
"pullPlaceholder": "e.g. llama3",
"fetch": "Fetch Models",
"found": "Found {{count}} models",
"pullSuccess": "Model pulled successfully",
"pullError": "Failed to pull model",
"connectError": "Could not connect to Ollama"
}
},
"routines": {
"title": "Routine Configuration",
"description": "Configure global morning and evening routine schedules.",
"morningLabel": "Morning Routine",
"eveningLabel": "Evening Routine",
"time": "Start Time",
"saved": "Routine settings saved",
"error": "Failed to save settings",
"save": "Save Routines",
"goodMorning": "Good Morning",
"goodEvening": "Good Evening",
"morningSubtitle": "Let's plan your day for success.",
"eveningSubtitle": "Time to reflect and unwind.",
"dayComplete": "Day Complete! Great job."
}
},
"smtp": {
"title": "Email Settings (SMTP)",
"description": "Configure outgoing email server",
"host": "Host",
"port": "Port",
"user": "User",
"password": "Password",
"from": "From Address",
"secure": "Secure (TLS)",
"save": "Save Email Settings",
"saving": "Saving...",
"saveSuccess": "Settings saved successfully"
"apiKey": "Access Token",
"generate": "Generate Token",
"revoke": "Revoke Token",
"generated": "Access Token generated",
"revoked": "Access Token revoked",
"noKey": "No token active. Generate one to connect.",
"copy": "Copy to Clipboard",
"copied": "Copied!",
"instructions": "Configure your MCP client (e.g. Claude Desktop) with this URL and Token."
},
"ai": {
"title": "AI Configuration",
"description": "Configure the global AI provider for the assistant.",
"provider": "AI Provider",
"apiKeyLabel": "API Key",
"model": "Model Name",
"baseUrl": "Base URL",
"systemPrompt": "System Prompt",
"systemPromptPlaceholder": "Define the AI's persona and rules...",
"enableUser": "Enable AI Chat",
"enableUserDesc": "Allow access to the AI chat assistant",
"save": "Save AI Settings",
"saved": "Settings saved",
"error": "Failed to save settings",
"ollama": {
"placeholder": "Optional for Ollama",
"pull": "Pull Model",
"pullPlaceholder": "e.g. llama3",
"fetch": "Fetch Models",
"found": "Found {{count}} models",
"pullSuccess": "Model pulled successfully",
"pullError": "Failed to pull model",
"connectError": "Could not connect to Ollama"
}
},
"routines": {
"title": "Routine Configuration",
"description": "Configure global morning and evening routine schedules.",
"morningLabel": "Morning Routine",
"eveningLabel": "Evening Routine",
"time": "Start Time",
"saved": "Routine settings saved",
"error": "Failed to save settings",
"save": "Save Routines",
"goodMorning": "Good Morning",
"goodEvening": "Good Evening",
"morningSubtitle": "Let's plan your day for success.",
"eveningSubtitle": "Time to reflect and unwind.",
"dayComplete": "Day Complete! Great job."
},
"aiChat": {
"title": "AI Assistant",
"welcome": "How can I help you manage your tasks today?",
"thinking": "Thinking...",
@@ -565,7 +575,17 @@
"grandmaster": "Grandmaster",
"virtuoso": "Virtuoso",
"legend": "Legend",
"mythic": "Mythic"
"mythic": "Mythic",
"titan": "Titan",
"sentinel": "Sentinel",
"vanguard": "Vanguard",
"oracle": "Oracle",
"ascendant": "Ascendant",
"ethereal": "Ethereal",
"celestial": "Celestial",
"divine": "Divine",
"omnipotent": "Omnipotent",
"eternal": "Eternal"
},
"leaderboardPage": {
"title": "Leaderboard",
@@ -599,6 +619,7 @@
"achievements": {
"title": "Achievements",
"subtitle": "Track your progress and goals",
"overview": "Overview",
"rewardsDescription": "Spend your {{xp}} XP on exclusive rewards!",
"addReward": "Add Reward",
"createCustomReward": "Create Custom Reward",
@@ -607,6 +628,8 @@
"rewardCost": "Cost (XP)",
"createRewardBtn": "Create Reward",
"currentStreak": "Current Streak",
"weeklyStreak": "Weekly Streak",
"monthlyStreak": "Monthly Streak",
"days": "{{count}} Days",
"bestStreak": "Best: {{count}} Days",
"xpActivity": "XP Activity",
@@ -653,7 +676,7 @@
"create_subtask": "Subtask Created",
"update_task": "Task Updated",
"complete_task": "Task Completed",
"complete_task_late": "Task Completed (Late)",
"complete_task_late": "Task Completed",
"ai_action": "AI Assistant Used",
"daily_streak": "Daily Streak Bonus",
"daily_clear_bonus": "Daily Clear Bonus",
@@ -675,6 +698,8 @@
"completeTaskLate": "Complete Task (Late)",
"aiAction": "AI Action",
"dailyStreak": "Daily Streak",
"weeklyStreak": "Weekly Streak (7 Days)",
"monthlyStreak": "Monthly Streak (30 Days)",
"streakTooltip": "Log in daily to increase your streak!",
"streakBonus": "Weekly & Monthly bonuses available: 7 days (+300 XP), 30 days (+1000 XP)."
}
@@ -800,6 +825,12 @@
"createAccount": "Create Account",
"registrationDisabled": "Registration is currently disabled.",
"heroTitle": "TaskFlow",
"heroSubtitle": "Master your productivity with AI-driven task management, gamified achievements, and intelligent focus modes."
"heroSubtitle": "Boost productivity with gamified task management and AI assistance.",
"2faVerification": "2FA Verification",
"2faCodeSent": "2FA Code Sent",
"checkEmail": "Please check your email for the verification code.",
"enterCodeSentTo": "Enter the 6-digit code sent to",
"codeExpiresIn10": "Code expires in 10 minutes",
"verify": "Verify"
}
}
+11 -11
View File
@@ -28,23 +28,23 @@ export const getQueryFn: <T>(options: {
on401: UnauthorizedBehavior;
}) => QueryFunction<T> =
({ on401: unauthorizedBehavior }) =>
async ({ queryKey }) => {
const res = await fetch(queryKey.join("/") as string, {
credentials: "include",
});
async ({ queryKey }) => {
const res = await fetch(queryKey.join("/") as string, {
credentials: "include",
});
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
return null;
}
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
return null;
}
await throwIfResNotOk(res);
return await res.json();
};
await throwIfResNotOk(res);
return await res.json();
};
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: getQueryFn({ on401: "throw" }),
queryFn: getQueryFn({ on401: "returnNull" }),
refetchInterval: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
+7 -3
View File
@@ -6,8 +6,12 @@ import "./i18n/config";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/queryClient";
import { ErrorBoundary } from "./components/ErrorBoundary";
createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ErrorBoundary>
);
File diff suppressed because it is too large Load Diff
+50
View File
@@ -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<User | null>(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 (
<div className="space-y-6 container mx-auto p-4 max-w-5xl">
<div className="flex justify-between items-center">
@@ -210,6 +232,14 @@ export default function AdminUserManagement() {
>
{user.isActive ? t('userManagement.table.deactivate') : t('userManagement.table.activate')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setResetXpUser(user)}
disabled={user.role === 'admin' && user.username === 'admin'}
>
{t('userManagement.resetXp', 'Reset XP')}
</Button>
<Button
variant="ghost"
size="sm"
@@ -255,6 +285,26 @@ export default function AdminUserManagement() {
</div>
</DialogContent>
</Dialog>
<AlertDialog open={!!resetXpUser} onOpenChange={(open) => !open && setResetXpUser(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('userManagement.resetXpTitle', 'Reset User XP')}</AlertDialogTitle>
<AlertDialogDescription>
{t('userManagement.resetXpConfirm', 'Are you sure you want to reset XP and Level for user "{username}"? This action cannot be undone.', { username: resetXpUser?.username })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setResetXpUser(null)}>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => resetXpUser && resetXpMutation.mutate(resetXpUser.id)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{resetXpMutation.isPending ? t('common.loading') : t('userManagement.resetXp', 'Reset XP')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
+32 -5
View File
@@ -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 (
<div className="prose prose-sm dark:prose-invert max-w-none break-words">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{displayedContent}</ReactMarkdown>
{/* <ReactMarkdown remarkPlugins={[remarkGfm]}>{displayedContent}</ReactMarkdown> */}
{displayedContent}
</div>
);
};
@@ -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() {
<TypewriterMessage content={msg.content} onComplete={() => scrollToBottom()} />
) : (
<div className="prose prose-sm dark:prose-invert max-w-none break-words">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
{/* <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown> */}
{msg.content}
</div>
)
) : (
+102 -11
View File
@@ -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<string | null>(null);
const [twoFAEmail, setTwoFAEmail] = useState<string | null>(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 (
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
<Card className="w-full max-w-md shadow-xl border-border/50">
<CardHeader className="text-center space-y-2">
<div className="mx-auto mb-4">
<img src="/favicon.png" alt="Logo" className="w-12 h-12 mx-auto" />
</div>
<CardTitle className="text-2xl font-bold">{t('auth.2faVerification')}</CardTitle>
<CardDescription>
{t('auth.enterCodeSentTo')} <span className="font-medium text-foreground">{twoFAUserId && twoFAEmail ? twoFAEmail : 'your email'}</span>
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleVerify2FA} className="space-y-4">
<div className="space-y-2">
<Input
placeholder="123456"
className="text-center text-2xl tracking-widest"
maxLength={6}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
/>
<p className="text-xs text-muted-foreground text-center">{t('auth.codeExpiresIn10')}</p>
</div>
<Button className="w-full" type="submit" disabled={verify2FAMutation.isPending || otpCode.length !== 6}>
{verify2FAMutation.isPending ? t('common.verifying') : t('auth.verify')}
</Button>
<Button variant="ghost" className="w-full" type="button" onClick={() => setIs2FARequired(false)}>
{t('common.cancel')}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}
return (
<div className="min-h-screen grid lg:grid-cols-2">
<div className="min-h-screen grid lg:grid-cols-2 dark">
<div className="hidden lg:flex flex-col justify-center items-center bg-zinc-900 p-12 text-white">
<div className="max-w-md space-y-4 text-center">
<div className="bg-white/10 p-4 rounded-2xl inline-block mb-4 backdrop-blur-sm">
<BrainCircuit className="w-16 h-16 text-primary-foreground" />
<div className="mb-8">
<img src="/favicon.png" alt="Logo" className="w-24 h-24" />
</div>
<h1 className="text-4xl font-bold tracking-tight">{t('auth.heroTitle')}</h1>
<p className="text-lg text-zinc-400">
@@ -107,8 +198,8 @@ export default function AuthPage() {
<div className="flex items-center justify-center p-4 bg-background">
<Card className="w-full max-w-md shadow-xl border-border/50">
<CardHeader className="text-center space-y-2">
<div className="lg:hidden mx-auto bg-primary/10 p-3 rounded-xl w-fit mb-2">
<BrainCircuit className="w-8 h-8 text-primary" />
<div className="lg:hidden mx-auto mb-4">
<img src="/favicon.png" alt="Logo" className="w-12 h-12 mx-auto" />
</div>
<CardTitle className="text-2xl font-bold">{t('auth.welcomeBack')}</CardTitle>
<CardDescription>
+2 -2
View File
@@ -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
+134
View File
@@ -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<Task[]>({
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 (
<div className="space-y-6 max-w-4xl mx-auto">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('notifications.title', 'Notifications')}</h1>
<p className="text-muted-foreground mt-2">
{t('notifications.description', 'Manage your alerts and view important updates.')}
</p>
</div>
</div>
{/* Settings Card */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-primary" />
<CardTitle>{t('notifications.settings.title', 'Browser Notifications')}</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notifications-toggle" className="text-base">
{t('notifications.enableBrowser', 'Enable Push Notifications')}
</Label>
<p className="text-sm text-muted-foreground">
{permission === 'denied'
? <span className="text-red-500">{t('notifications.settings.denied', 'Permission denied. Please enable in browser settings.')}</span>
: t('notifications.settings.description', 'Receive alerts for upcoming and overdue tasks.')}
</p>
</div>
<Switch
id="notifications-toggle"
checked={enabled}
onCheckedChange={toggleEnabled}
disabled={permission === 'denied'}
/>
</div>
{permission === 'default' && (
<div className="mt-4">
<Button variant="outline" size="sm" onClick={requestPermission}>
{t('notifications.settings.request', 'Request Permission')}
</Button>
</div>
)}
</CardContent>
</Card>
{/* Notification List */}
<div className="space-y-4">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Clock className="h-5 w-5" />
{t('notifications.recent', 'Recent Alerts')}
</h2>
{notifications.length === 0 ? (
<div className="text-center py-12 border rounded-lg bg-muted/10 border-dashed">
<CheckCircle2 className="h-10 w-10 text-muted-foreground mx-auto mb-3 opacity-50" />
<p className="text-muted-foreground font-medium">{t('notifications.empty', 'All caught up! No urgent alerts.')}</p>
</div>
) : (
notifications.map((notif) => (
<Card key={notif.id} className={`border-l-4 ${notif.type === 'overdue' ? 'border-l-red-500 bg-red-50/10 dark:bg-red-900/10' : 'border-l-blue-500'}`}>
<CardContent className="p-4 flex items-start gap-4">
<div className={`p-2 rounded-full shrink-0 ${notif.type === 'overdue' ? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' : 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'}`}>
{notif.type === 'overdue' ? <AlertCircle className="h-5 w-5" /> : <Calendar className="h-5 w-5" />}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<h3 className="font-semibold">{notif.task.title}</h3>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{formatDistanceToNow(notif.timestamp, { addSuffix: true, locale: i18n.language === 'de' ? de : enUS })}
</span>
</div>
<p className="text-sm text-muted-foreground mt-1">
{notif.type === 'overdue'
? t('notifications.overdueBody', 'This task is overdue!')
: t('notifications.upcomingBody', { time: formatDistanceToNow(notif.timestamp, { locale: i18n.language === 'de' ? de : enUS }) })
}
</p>
</div>
</CardContent>
</Card>
))
)}
</div>
</div>
);
}
+1
View File
@@ -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",
+40 -8
View File
@@ -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<User>({
const { data: user, isLoading: isLoadingUser } = useQuery<User>({
queryKey: ['/api/user']
});
if (isLoadingUser) {
return (
<div className="flex items-center justify-center min-h-[50vh]">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
if (!user) {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] space-y-4">
<h2 className="text-2xl font-bold">{t('common.loginRequired')}</h2>
<Button onClick={() => setLocation('/')}>
{t('auth.login')}
</Button>
</div>
);
}
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
const [editingLabel, setEditingLabel] = useState<Label | null>(null);
const [labelName, setLabelName] = useState('');
@@ -79,6 +98,8 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {
const handleLanguageChange = (value: string) => {
i18n.changeLanguage(value);
localStorage.setItem('taskflow-language', value);
// Persist to DB
privacyMutation.mutate({ language: value } as any);
console.log('Language changed to:', value);
};
@@ -98,9 +119,10 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {
};
// Fetch labels
const { data: labels = [], isLoading: labelsLoading } = useQuery<Label[]>({
const { data: labelsData, isLoading: labelsLoading } = useQuery<Label[]>({
queryKey: ['/api/labels']
});
const labels = labelsData ?? [];
// Create label mutation
const createLabelMutation = useMutation({
@@ -296,7 +318,7 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {
<p className="text-sm text-muted-foreground">{t('settings.social.publicLeaderboardDesc')}</p>
</div>
<Switch
checked={user?.showOnLeaderboard}
checked={!!user?.showOnLeaderboard}
onCheckedChange={(checked) => handlePrivacyUpdate({ showOnLeaderboard: checked })}
/>
</div>
@@ -306,7 +328,7 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {
<p className="text-sm text-muted-foreground">{t('settings.social.searchableDesc')}</p>
</div>
<Switch
checked={user?.isSearchable}
checked={!!user?.isSearchable}
onCheckedChange={(checked) => handlePrivacyUpdate({ isSearchable: checked })}
/>
</div>
@@ -316,10 +338,20 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {
<p className="text-sm text-muted-foreground">{t('settings.ai.enableUserDesc')}</p>
</div>
<Switch
checked={user?.aiEnabled}
checked={!!user?.aiEnabled}
onCheckedChange={(checked) => handlePrivacyUpdate({ aiEnabled: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<p className="font-medium">{t('settings.social.2fa')}</p>
<p className="text-sm text-muted-foreground">{t('settings.social.2faDesc')}</p>
</div>
<Switch
checked={!!user?.is2faEnabled}
onCheckedChange={(checked) => handlePrivacyUpdate({ is2faEnabled: checked } as any)}
/>
</div>
<div className="pt-2">
<Button variant="outline" onClick={() => setIsShareAccessOpen(true)}>
<Share2 className="w-4 h-4 mr-2" />
@@ -554,7 +586,7 @@ export default function Settings({ onNavigateToTemplates }: SettingsProps) {
</Card>
{/* Data Export */}
<DataExportCard user={user} />
{/* <DataExportCard user={user} /> */}
{/* Admin Section */}
{