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;
|
||||
|
||||
Reference in New Issue
Block a user