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
+22 -7
View File
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award, Bot, CalendarOff } from 'lucide-react';
import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award, Bot, CalendarOff, Bell } from 'lucide-react';
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
@@ -15,6 +15,7 @@ import {
SidebarRail,
useSidebar,
SidebarTrigger,
SidebarSeparator,
} from "@/components/ui/sidebar"
import { Task, User } from '@shared/schema';
import ThemeToggle from './ThemeToggle';
@@ -44,8 +45,9 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
await fetch("/api/logout", { method: "POST" });
},
onSuccess: () => {
queryClient.setQueryData(["/api/user"], null);
toast({ title: "Logged out successfully" });
queryClient.clear();
// toast({ title: "Logged out successfully" });
window.location.href = "/auth?mode=login";
},
});
@@ -68,8 +70,8 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground h-14">
<div className="flex aspect-square size-9 items-center justify-center rounded-xl bg-gradient-to-br from-violet-600 to-indigo-600 text-white shadow-lg">
<CheckSquare className="size-5" />
<div className="flex aspect-square size-9 items-center justify-center rounded-xl overflow-hidden shadow-lg bg-transparent">
<img src="/favicon.png" alt="Logo" className="w-full h-full object-cover" />
</div>
{state !== 'collapsed' && (
<div className="grid flex-1 text-left text-sm leading-tight ml-2">
@@ -103,18 +105,31 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
))}
</SidebarMenu>
</SidebarContent>
<SidebarSeparator />
<SidebarFooter>
{state !== 'collapsed' && user && (
<GamificationBar xp={user.xp} level={user.level} streak={user.currentStreak} />
)}
<div className={`p-4 ${state === 'collapsed'
? 'flex flex-col items-center justify-center gap-4'
: 'grid grid-cols-3 items-center'
: 'grid grid-cols-4 items-center gap-2'
}`}>
<div className={state === 'collapsed' ? '' : 'justify-self-start'}>
<ThemeToggle />
</div>
<div className={state === 'collapsed' ? '' : 'justify-self-center'}>
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/notifications')}
className="bg-transparent hover:bg-sidebar-accent"
title={t('navigation.notifications', 'Notifications')}
>
<Bell className="size-4" />
</Button>
</div>
<div className={state === 'collapsed' ? '' : 'justify-self-center'}>
<Button
variant="ghost"
@@ -122,7 +137,7 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
onClick={() => logoutMutation.mutate()}
disabled={logoutMutation.isPending}
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
title="Logout"
title={t('auth.logout', 'Logout')}
>
<LogOut className="size-4" />
</Button>
-1
View File
@@ -21,7 +21,6 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
HoverCard,
+49
View File
@@ -0,0 +1,49 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-red-50 text-red-900">
<div className="max-w-xl p-8 bg-white rounded-lg shadow-xl border border-red-200">
<h1 className="text-2xl font-bold mb-4">Something went wrong</h1>
<p className="mb-4">The application crashed. Here is the error:</p>
<pre className="bg-red-100 p-4 rounded overflow-auto text-sm font-mono">
{this.state.error?.toString()}
</pre>
<button
className="mt-6 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={() => window.location.reload()}
>
Reload Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
+1
View File
@@ -30,6 +30,7 @@ interface KanbanBoardProps {
onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskClick?: (task: Task) => void;
onTaskEdit?: (task: Task) => void;
onTaskDelete?: (taskId: string) => void;
onStartTimer?: (taskId: string) => void;
onStopTimer?: (taskId: string) => void;
+1 -1
View File
@@ -27,7 +27,7 @@ export default function PomodoroOverlay({ taskId, taskTitle, isOpen, onClose, on
// Initialize Worker
useEffect(() => {
if (typeof Worker !== 'undefined') {
workerRef.current = new Worker(new URL('../workers/timer.worker.ts', import.meta.url));
workerRef.current = new Worker(new URL('../workers/timer.worker.ts', import.meta.url), { type: 'module' });
workerRef.current.onmessage = (e) => {
const { type, remaining } = e.data;
+18 -10
View File
@@ -55,8 +55,12 @@ export function useRoutineBlocker() {
const morningStartTime = parseTime(settings.morning_routine_time || "09:00");
// Block if it's morning time (>= start) AND < evening start (if evening enabled).
// Effectively: Morning Routine is mandatory from 9:00 AM until 5:00 PM.
const cutoffTime = eveningEnabled ? eveningStartTime : 24 * 60; // Up to evening or end of day
// FIX: Add a hard cutoff (e.g. 13:00 / 1 PM) so morning routine doesn't haunt users all day.
const HARD_MORNING_CUTOFF = 13 * 60;
const cutoffTime = eveningEnabled
? Math.min(eveningStartTime, HARD_MORNING_CUTOFF)
: HARD_MORNING_CUTOFF;
if (currentTimeVal >= morningStartTime && currentTimeVal < cutoffTime && !isToday(user.lastMorningRoutine)) {
return '/focus/routine/morning';
@@ -84,17 +88,21 @@ export function useRoutineBlocker() {
const currentTimeVal = now.getHours() * 60 + now.getMinutes();
const eveningEnabled = settings.evening_routine_enabled !== "false";
const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number);
const eveningStartVal = eveningStartTime[0] * 60 + eveningStartTime[1];
// Safe parse
const [eh, em] = (settings.evening_routine_time || "17:00").split(':').map(Number);
const eveningStartVal = (eh || 0) * 60 + (em || 0);
const morningEnabled = settings.morning_routine_enabled !== "false";
if (!morningEnabled) return false;
const morningStartTime = (settings.morning_routine_time || "09:00").split(':').map(Number);
const startVal = morningStartTime[0] * 60 + morningStartTime[1];
const [mh, mm] = (settings.morning_routine_time || "09:00").split(':').map(Number);
const startVal = (mh || 0) * 60 + (mm || 0);
// Cutoff: End of day OR Evening Start
const cutoffVal = eveningEnabled ? eveningStartVal : 24 * 60;
const HARD_MORNING_CUTOFF = 13 * 60; // 13:00
// Cutoff: Min of (End of day OR Evening Start) AND Hard Cutoff
const baseCutoff = eveningEnabled ? eveningStartVal : 24 * 60;
const cutoffVal = Math.min(baseCutoff, HARD_MORNING_CUTOFF);
if (currentTimeVal >= startVal && currentTimeVal < cutoffVal && !isSameDay(user.lastMorningRoutine, now)) return true;
@@ -110,8 +118,8 @@ export function useRoutineBlocker() {
const eveningEnabled = settings.evening_routine_enabled !== "false";
if (!eveningEnabled) return false;
const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number);
const startVal = eveningStartTime[0] * 60 + eveningStartTime[1];
const [eh, em] = (settings.evening_routine_time || "17:00").split(':').map(Number);
const startVal = (eh || 0) * 60 + (em || 0);
if (currentTimeVal >= startVal && !isSameDay(user.lastEveningRoutine, now)) return true;
+3 -6
View File
@@ -17,8 +17,9 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X, Lock, CornerDownRight } from "lucide-react";
import { Task, Label } from '@shared/schema';
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X, Lock, CornerDownRight, Share2, Users } from "lucide-react";
import { Task, Label, User } from '@shared/schema';
import { ShareTaskModal } from './ShareTaskModal';
import { useQuery } from '@tanstack/react-query';
import { Checkbox } from "@/components/ui/checkbox";
import { motion, PanInfo, useAnimation } from 'framer-motion';
@@ -47,10 +48,6 @@ interface TaskCardProps {
isDragging?: boolean;
}
import { ShareTaskModal } from './ShareTaskModal';
import { Share2, Users } from "lucide-react";
import { User } from "@shared/schema";
function SharedTaskIcon({ task }: { task: Task }) {
const { t } = useTranslation();
const { data: user } = useQuery<User>({ queryKey: ["/api/user"], retry: false });
+21 -4
View File
@@ -10,11 +10,12 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { CalendarIcon, Plus, Tag, Check, Link2 } from 'lucide-react';
import { CalendarIcon, Plus, Tag, Check, Link2, Info } from 'lucide-react';
import { Task, Label } from '@shared/schema';
import { useQuery } from '@tanstack/react-query';
import { parseTaskInput } from '../lib/nlp';
import { Sparkles } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
interface TaskCreationModalProps {
isOpen: boolean;
@@ -47,13 +48,15 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
const [error, setError] = useState<string | null>(null);
// Fetch labels
const { data: labels = [] } = useQuery<Label[]>({
const { data: labelsData } = useQuery<Label[]>({
queryKey: ['/api/labels'],
});
const labels = labelsData ?? [];
const { data: tasks = [] } = useQuery<Task[]>({
const { data: tasksData } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const tasks = tasksData ?? [];
const handleSave = () => {
if (!title.trim()) {
@@ -193,7 +196,21 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<Select value={energyLevel} onValueChange={(value: 'low' | 'medium' | 'high') => setEnergyLevel(value)}>
<SelectTrigger>
<SelectValue placeholder={t('taskCreation.energy')} />
<div className="flex items-center gap-2">
<SelectValue placeholder={t('taskCreation.energy')} />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-help" onClick={(e) => e.stopPropagation()}>
<p className="sr-only">Help</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">{t('taskCreation.energyHelp', 'Match tasks to your energy levels. Low energy tasks are good for slumps, High energy tasks require focus.')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="low">{t('gamification.energy.low')}</SelectItem>
+2 -1
View File
@@ -66,9 +66,10 @@ export default function TaskDetailsModal({
const [isCreatingSubtask, setIsCreatingSubtask] = useState(false);
const [isScheduling, setIsScheduling] = useState(false);
const { data: tasks = [] } = useQuery<Task[]>({
const { data: tasksData } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const tasks = tasksData ?? [];
// Notes state
const [notes, setNotes] = useState('');
@@ -51,7 +51,7 @@ export function SMTPSettingsCard() {
mutationFn: (data: Partial<typeof settings>) => apiRequest("POST", "/api/admin/settings", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/admin/settings"] });
toast({ title: t('smtp.saveSuccess', "Settings updated") });
toast({ title: t('settings.smtp.saveSuccess', "Settings updated") });
},
});
@@ -1,17 +0,0 @@
import BottomNavigation from '../BottomNavigation';
export default function BottomNavigationExample() {
return (
<div className="relative h-[400px] bg-muted/20">
<div className="p-4">
<h3 className="text-lg font-semibold mb-2">App Content Area</h3>
<p className="text-muted-foreground">
This is where your main app content would be displayed.
The bottom navigation is fixed at the bottom of the screen.
</p>
</div>
<BottomNavigation />
</div>
);
}
@@ -1,50 +0,0 @@
import CalendarView from '../CalendarView';
import { Task } from '../TaskCard';
import { addDays } from 'date-fns';
const mockTasks: Task[] = [
{
id: '1',
title: 'Team meeting',
status: 'todo',
priority: 'high',
dueDate: new Date(),
timeTracked: 0,
isTracking: false
},
{
id: '2',
title: 'Review design mockups',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 2),
timeTracked: 30,
isTracking: false
},
{
id: '3',
title: 'Client presentation',
status: 'todo',
priority: 'high',
dueDate: addDays(new Date(), 5),
timeTracked: 0,
isTracking: false
},
{
id: '4',
title: 'Code review',
status: 'todo',
priority: 'low',
dueDate: addDays(new Date(), 1),
timeTracked: 0,
isTracking: false
}
];
export default function CalendarViewExample() {
return (
<div className="p-4">
<CalendarView tasks={mockTasks} />
</div>
);
}
@@ -1,75 +0,0 @@
import KanbanBoard from '../KanbanBoard';
import { Task } from '../TaskCard';
import { addDays } from 'date-fns';
const mockTasks: Task[] = [
{
id: '1',
title: 'Design new landing page',
description: 'Create wireframes and mockups for the new product landing page',
status: 'todo',
priority: 'high',
dueDate: addDays(new Date(), 3),
timeTracked: 0,
isTracking: false,
projectId: 'project-1'
},
{
id: '2',
title: 'Implement user authentication',
description: 'Set up login, registration, and password reset functionality',
status: 'inProgress',
priority: 'high',
dueDate: addDays(new Date(), 1),
timeTracked: 120,
isTracking: true,
projectId: 'project-2'
},
{
id: '3',
title: 'Write API documentation',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 5),
timeTracked: 45,
isTracking: false,
projectId: 'project-1'
},
{
id: '4',
title: 'Set up CI/CD pipeline',
description: 'Configure automated testing and deployment',
status: 'done',
priority: 'medium',
dueDate: addDays(new Date(), -2),
timeTracked: 180,
isTracking: false,
projectId: 'project-2'
},
{
id: '5',
title: 'Review code changes',
status: 'todo',
priority: 'low',
dueDate: new Date(),
timeTracked: 0,
isTracking: false
},
{
id: '6',
title: 'Update dependencies',
status: 'done',
priority: 'low',
dueDate: addDays(new Date(), -1),
timeTracked: 30,
isTracking: false
}
];
export default function KanbanBoardExample() {
return (
<div className="p-4">
<KanbanBoard tasks={mockTasks} />
</div>
);
}
@@ -1,9 +0,0 @@
import ProjectTemplate from '../ProjectTemplate';
export default function ProjectTemplateExample() {
return (
<div className="p-4">
<ProjectTemplate />
</div>
);
}
@@ -1,34 +0,0 @@
import TaskCard from '../TaskCard';
import { Task } from '@shared/schema';
const mockTask: Task = {
id: '1',
title: 'Design mobile task interface',
description: 'Create wireframes and mockups for the mobile-first task management interface',
status: 'inProgress',
priority: 'high',
dueDate: new Date('2024-09-20'),
timeTracked: 45,
isTracking: false,
projectId: 'project-1',
notes: 'Focus on mobile-first design principles'
};
export default function TaskCardExample() {
return (
<div className="p-4 space-y-4">
<TaskCard task={mockTask} />
<TaskCard
task={{
...mockTask,
id: '2',
title: 'Quick task without details',
description: undefined,
status: 'todo',
priority: 'low',
timeTracked: 0
}}
/>
</div>
);
}
@@ -1,29 +0,0 @@
import { useState } from 'react';
import TaskCreationModal from '../TaskCreationModal';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
export default function TaskCreationModalExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="p-4">
<Button
onClick={() => setIsOpen(true)}
className="w-full flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Create New Task
</Button>
<TaskCreationModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSave={(task) => {
console.log('Task saved:', task);
setIsOpen(false);
}}
/>
</div>
);
}
@@ -1,77 +0,0 @@
import TaskList from '../TaskList';
import { Task } from '../TaskCard';
import { addDays, subDays } from 'date-fns';
const mockTasks: Task[] = [
{
id: '1',
title: 'Review quarterly reports',
description: 'Analyze Q3 performance metrics and prepare summary',
status: 'todo',
priority: 'high',
dueDate: addDays(new Date(), 2),
timeTracked: 0,
isTracking: false,
projectId: 'project-1'
},
{
id: '2',
title: 'Update website copy',
description: 'Revise landing page content based on user feedback',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 5),
timeTracked: 90,
isTracking: true,
projectId: 'project-2'
},
{
id: '3',
title: 'Team standup meeting',
status: 'todo',
priority: 'low',
dueDate: new Date(),
timeTracked: 0,
isTracking: false
},
{
id: '4',
title: 'Fix login bug',
description: 'Users unable to login with special characters in password',
status: 'todo',
priority: 'high',
dueDate: subDays(new Date(), 1), // Overdue
timeTracked: 30,
isTracking: false,
projectId: 'project-2'
},
{
id: '5',
title: 'Deploy new features',
status: 'done',
priority: 'medium',
dueDate: subDays(new Date(), 2),
timeTracked: 120,
isTracking: false,
projectId: 'project-1'
},
{
id: '6',
title: 'Write documentation',
description: 'Document new API endpoints for external developers',
status: 'inProgress',
priority: 'medium',
dueDate: addDays(new Date(), 7),
timeTracked: 60,
isTracking: false,
projectId: 'project-1'
}
];
export default function TaskListExample() {
return (
<div className="p-4">
<TaskList tasks={mockTasks} />
</div>
);
}
@@ -1,19 +0,0 @@
import ThemeToggle from '../ThemeToggle';
export default function ThemeToggleExample() {
return (
<div className="p-4 space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-semibold">Dark Mode</h3>
<p className="text-sm text-muted-foreground">Toggle between light and dark themes</p>
</div>
<ThemeToggle />
</div>
<div className="p-4 bg-card border rounded-lg">
<p className="text-sm">This card will change appearance when you toggle the theme.</p>
</div>
</div>
);
}
+1 -1
View File
@@ -375,7 +375,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto",
className
)}
{...props}