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

- implemented /notifications page with overdue/upcoming alerts
- Fixed sidebar scrolling in collapsed mode
- Moved notification button to sidebar footer
- Enhanced Achievements page with streak stats and tooltips
- Improved XP history to show task titles
- Added missing translations (en/de)
- Removed top bar header
- Fixed Docker environment routing
This commit is contained in:
2025-12-17 10:06:36 +01:00
parent 7b79015ac2
commit 9819d8db0b
47 changed files with 2309 additions and 1241 deletions
+237 -234
View File
@@ -34,6 +34,7 @@ import { Plus } from 'lucide-react';
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "./components/AppSidebar"
import { useNotifications } from './hooks/use-notifications';
import { useToast } from "@/hooks/use-toast";
import { CommandPalette } from './components/CommandPalette';
import PomodoroOverlay from './components/PomodoroOverlay';
import AuthPage from "@/pages/AuthPage";
@@ -43,10 +44,11 @@ import SetupWizard from "@/pages/SetupWizard";
import AdminUserManagement from "@/pages/AdminUserManagement";
import AdminSettings from "@/pages/AdminSettings";
import NotFound from "@/pages/not-found";
import { AiChat } from "@/components/AiChat";
// import { AiChat } from "@/components/AiChat";
import ForgotPasswordPage from "@/pages/ForgotPasswordPage";
import ResetPasswordPage from "@/pages/ResetPasswordPage";
import AiChatPage from "@/pages/AiChatPage";
import NotificationsPage from "@/pages/NotificationsPage";
import FocusRoutinePage from "@/pages/FocusRoutinePage";
@@ -59,6 +61,7 @@ import { Loader2 } from "lucide-react";
function App() {
const { t } = useTranslation();
const { toast } = useToast();
const [, setLocation] = useLocation();
const isBlocked = useRoutineBlocker();
@@ -84,19 +87,21 @@ function App() {
});
// Fetch tasks and labels using React Query
const { data: tasks = [] } = useQuery<Task[]>({
const { data: tasksData } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
enabled: !!user,
select: (data) => data.map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}))
});
const { data: labels = [] } = useQuery<Label[]>({
const tasks = (tasksData ?? []).map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}));
const { data: labelsData } = useQuery<Label[]>({
queryKey: ['/api/labels'],
enabled: !!user,
});
const labels = labelsData ?? [];
const handleCreateTask = async (newTask: Partial<Task>) => {
try {
@@ -105,17 +110,17 @@ function App() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: newTask.title || '',
description: newTask.description || null,
description: newTask.description || undefined,
status: 'todo',
priority: newTask.priority || 'medium',
dueDate: newTask.dueDate || null,
dueDate: newTask.dueDate || null, // Explicitly handled as nullable in schema
timeTracked: 0,
isTracking: false,
projectId: newTask.projectId || null,
notes: newTask.notes || null,
labelId: newTask.labelId || null,
projectId: newTask.projectId || undefined,
notes: newTask.notes || undefined,
labelId: newTask.labelId || undefined,
energyLevel: newTask.energyLevel || 'medium',
estimatedDuration: newTask.estimatedDuration || null
estimatedDuration: newTask.estimatedDuration || undefined
})
});
@@ -134,8 +139,18 @@ function App() {
// Invalidate to be sure
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
toast({
title: t('task.created', 'Task Created'),
description: t('task.createdDescription', '"{title}" has been added.', { title: createdTask.title }),
});
} catch (error) {
console.error('Error creating task:', error);
toast({
title: t('error.createTask', 'Error'),
description: t('error.createTaskDescription', 'Failed to create task. Please try again.'),
variant: "destructive"
});
}
};
@@ -265,231 +280,219 @@ function App() {
);
}
if (!user) {
// Redirect to setup if no admin user exists
if (setupStatus && !setupStatus.isSetup) {
return (
<Switch>
<Route path="/setup" component={SetupWizard} />
<Route component={SetupWizard} />
</Switch>
);
}
if (isBlocked) {
return (
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
{/* Catch all redirects to nothing, the hook handles the push to routine page */}
<Route component={() => <div />} />
</Switch>
<Toaster />
</div>
);
}
if (isBlocked) {
return (
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="min-h-screen bg-background flex flex-col">
{user && (
<header className="flex h-16 shrink-0 items-center gap-2 border-b pl-4 pr-6">
<SidebarTrigger />
<div className="flex-1" />
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => setLocation('/notifications')}>
{/* Bell icon would go here */}
</Button>
<div className="flex items-center gap-2">
<span className="text-sm text-yellow-500 font-bold"> {user.xp}</span>
<span className="text-sm text-orange-500 font-bold">🔥 {user.currentStreak}</span>
</div>
</div>
</header>
)}
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/tasks">
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/calendar">
<CalendarView
tasks={tasks}
onTaskDrop={handleTaskDrop}
onTaskClick={handleTaskClick}
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/kanban">
<KanbanBoard
tasks={tasks}
onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/weeklist">
<WeekListView
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
{user ? <AchievementsPage user={user} /> : <AuthPage />}
</Route>
<Route path="/unscheduled">
{user ? (
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
) : (
<AuthPage />
)}
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user.role === 'admin' && (
<>
<Route path="/admin/users" component={AdminUserManagement} />
<Route path="/admin/settings" component={AdminSettings} />
</>
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
<Route component={NotFound} />
</Switch>
</main>
{/* Floating Action Button */}
<div className="fixed bottom-8 right-8 z-[100]">
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
onClick={() => setIsCreateModalOpen(true)}
data-testid="fab-create-task"
>
<Plus className="h-6 w-6 text-white" />
</Button>
</div>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
<PomodoroOverlay
isOpen={showPomodoro}
onClose={() => setShowPomodoro(false)}
taskId={activePomodoroTaskId}
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
/>
<Toaster />
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
{/* Catch all redirects to nothing, the hook handles the push to routine page */}
<Route component={() => <div />} />
</Switch>
<Toaster />
</div>
);
}
if (!user) {
return (
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/auth" component={AuthPage} />
<Route path="/forgot-password" component={ForgotPasswordPage} />
<Route path="/reset-password" component={ResetPasswordPage} />
<Route component={AuthPage} />
</Switch>
<Toaster />
</div>
);
}
return (
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="min-h-screen bg-background flex flex-col">
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/focus">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/calendar">
<CalendarView tasks={tasks} />
</Route>
<Route path="/weeklist">
<WeekListView tasks={tasks} onTaskUpdate={handleTaskUpdate} />
</Route>
<Route path="/kanban">
<KanbanBoard
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
/>
</Route>
<Route path="/auth">
{() => {
setLocation('/');
return null;
}}
</Route>
<Route path="/tasks">
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
<AchievementsPage user={user} />
</Route>
<Route path="/unscheduled">
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/notifications">
<NotificationsPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user?.role === 'admin' && (
<>
<Route path="/admin/users" component={AdminUserManagement} />
<Route path="/admin/settings" component={AdminSettings} />
</>
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
<Route component={NotFound} />
</Switch>
</main>
{/* Floating Action Button */}
<div className="fixed bottom-8 right-8 z-[100]">
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
onClick={() => setIsCreateModalOpen(true)}
data-testid="fab-create-task"
>
<Plus className="h-6 w-6 text-white" />
</Button>
</div>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
<PomodoroOverlay
isOpen={showPomodoro}
onClose={() => setShowPomodoro(false)}
taskId={activePomodoroTaskId}
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
/>
<Toaster />
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
);
}
export default App;