feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
continuous-integration/drone/push Build is failing
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:
+237
-234
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
HoverCard,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */}
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user