feat: add social features, leaderboard, auth enhancements, and admin fixes
continuous-integration/drone/push Build is passing

- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable).
- Add Leaderboard Page and API.
- Enhance Auth: Support Email/Username login, explicit duplicate registration errors.
- Fix: Admin login password hash regression.
- Refactor: Move to wouter for routing, add Admin Dashboard and User Management.
- Add Setup Wizard.
- Update UI with Sidebar and Gamification elements.
This commit is contained in:
2025-12-10 14:04:26 +01:00
parent d5b045158a
commit ccfb674318
52 changed files with 9206 additions and 1505 deletions
+235 -300
View File
@@ -1,11 +1,8 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
AlertDialog,
AlertDialogAction,
@@ -16,122 +13,75 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import logoImage from '@assets/generated_images/TaskFlow_app_logo_icon_3a0e77e9.png';
import { Switch, Route, useLocation } from "wouter";
// Components
import BottomNavigation from './components/BottomNavigation';
import TaskCreationModal from './components/TaskCreationModal';
import TaskDetailsModal from './components/TaskDetailsModal';
import TasksWithCalendar from './components/TasksWithCalendar';
import FocusMode from './components/FocusMode';
import CalendarView from './components/CalendarView';
import KanbanBoard from './components/KanbanBoard';
import ProjectTemplate from './components/ProjectTemplate';
import WeekListView from './components/WeekListView';
import ThemeToggle from './components/ThemeToggle';
import Settings from './pages/settings';
import AchievementsPage from './pages/AchievementsPage';
import { Task, Label } from '@shared/schema';
import { addDays, subDays } from 'date-fns';
import { useTimer } from './hooks/useTimer';
// Mock data for prototype
const initialTasks: 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',
notes: null,
labelId: '2e8c3aa0-278f-4220-b2c5-aa109b4279b7' // Urgent label
},
{
id: '2',
title: 'Update mobile app interface',
description: 'Implement new design system and improve user experience',
status: 'inProgress',
priority: 'high',
dueDate: addDays(new Date(), 1),
timeTracked: 120,
isTracking: true,
projectId: 'project-2',
notes: null,
labelId: 'cb44bed1-8ba3-43fe-9498-bb28e483ed1f' // Work label
},
{
id: '3',
title: 'Team standup meeting',
description: null,
status: 'todo',
priority: 'low',
dueDate: null,
timeTracked: 0,
isTracking: false,
projectId: null,
notes: null,
labelId: '274f0ba4-a133-471a-bbe9-8189aa3b0106' // Personal label
},
{
id: '4',
title: 'Fix critical login bug',
description: 'Users unable to login with special characters in password',
status: 'todo',
priority: 'high',
dueDate: null,
timeTracked: 30,
isTracking: false,
projectId: 'project-2',
notes: null,
labelId: 'c5eccac9-7919-4eb1-bb50-badacad6b1c1' // Study label
},
{
id: '5',
title: 'Deploy new features',
description: null,
status: 'done',
priority: 'medium',
dueDate: subDays(new Date(), 2),
timeTracked: 120,
isTracking: false,
projectId: 'project-1',
notes: null,
labelId: null
}
];
import { Plus } from 'lucide-react';
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "./components/AppSidebar"
import { CommandPalette } from './components/CommandPalette';
import PomodoroOverlay from './components/PomodoroOverlay';
import AuthPage from "@/pages/AuthPage";
import SettingsPage from "@/pages/settings";
import LeaderboardPage from "@/pages/LeaderboardPage";
import SetupWizard from "@/pages/SetupWizard";
import AdminUserManagement from "@/pages/AdminUserManagement";
import NotFound from "@/pages/not-found";
import { useQuery } from "@tanstack/react-query";
import { User } from "@shared/schema";
import { Loader2 } from "lucide-react";
function App() {
const { t } = useTranslation();
const [currentTab, setCurrentTab] = useState('tasks');
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [, setLocation] = useLocation();
const [tasks, setTasks] = useState<Task[]>([]);
const [labels, setLabels] = useState<Label[]>([]);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null);
const [showPomodoro, setShowPomodoro] = useState(false);
const [activePomodoroTaskId, setActivePomodoroTaskId] = useState<string | null>(null);
// Fetch tasks and labels from server on mount
const { data: user, isLoading: isLoadingUser } = useQuery<User>({
queryKey: ["/api/user"],
retry: false,
});
const { data: setupStatus } = useQuery<{ isSetup: boolean }>({
queryKey: ['/api/setup/status'],
});
// Fetch tasks and labels
useEffect(() => {
if (!user) return;
const fetchData = async () => {
try {
// Fetch tasks
const tasksResponse = await fetch('/api/tasks');
if (tasksResponse.ok) {
const serverTasks: Task[] = await tasksResponse.json();
// Normalize server tasks - convert date strings to Date objects
const normalizedTasks = serverTasks.map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}));
// Merge server tasks with initial tasks (avoiding duplicates)
const existingIds = new Set(normalizedTasks.map(t => t.id));
const uniqueInitialTasks = initialTasks.filter(t => !existingIds.has(t.id));
setTasks([...normalizedTasks, ...uniqueInitialTasks]);
setTasks(normalizedTasks);
}
// Fetch labels
const labelsResponse = await fetch('/api/labels');
if (labelsResponse.ok) {
const serverLabels: Label[] = await labelsResponse.json();
@@ -139,11 +89,10 @@ function App() {
}
} catch (error) {
console.error('Error fetching data:', error);
// Keep initial data on error
}
};
fetchData();
}, []);
}, [user]);
const handleCreateTask = async (newTask: Partial<Task>) => {
try {
@@ -160,22 +109,20 @@ function App() {
isTracking: false,
projectId: newTask.projectId || null,
notes: newTask.notes || null,
labelId: newTask.labelId || null
labelId: newTask.labelId || null,
energyLevel: newTask.energyLevel || 'medium',
estimatedDuration: newTask.estimatedDuration || null
})
});
if (!response.ok) {
throw new Error('Failed to create task');
}
if (!response.ok) throw new Error('Failed to create task');
const createdTask: Task = await response.json();
// Normalize the created task's date
const normalizedTask = {
...createdTask,
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
};
setTasks(prev => [...prev, normalizedTask]);
console.log('Task created:', normalizedTask);
} catch (error) {
console.error('Error creating task:', error);
}
@@ -188,36 +135,36 @@ function App() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error('Failed to update task');
}
if (!response.ok) throw new Error('Failed to update task');
const updatedTask: Task = await response.json();
// Normalize the updated task's date
const normalizedTask = {
...updatedTask,
dueDate: updatedTask.dueDate ? new Date(updatedTask.dueDate) : null
};
setTasks(prev => prev.map(task =>
task.id === taskId ? normalizedTask : task
));
console.log('Task updated:', taskId, updates);
setTasks(prev => prev.map(task => task.id === taskId ? normalizedTask : task));
} catch (error) {
console.error('Error updating task:', error);
// Optimistic update fallback
setTasks(prev => prev.map(task =>
task.id === taskId ? { ...task, ...updates } : task
));
setTasks(prev => prev.map(task => task.id === taskId ? { ...task, ...updates } : task));
}
};
// Initialize the timer hook after handleTaskUpdate is defined
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
console.log('Creating project from template:', { templateId, startDate, projectName });
};
const { startTimer, stopTimer } = useTimer({
tasks,
onTaskUpdate: handleTaskUpdate
});
const handleStartPomodoro = (taskId: string) => {
setActivePomodoroTaskId(taskId);
setShowPomodoro(true);
startTimer(taskId);
};
const handleTaskStatusChange = (taskId: string, newStatus: Task['status']) => {
handleTaskUpdate(taskId, { status: newStatus });
};
@@ -229,7 +176,6 @@ function App() {
const handleTaskClick = (task: Task) => {
setSelectedTask(task);
setIsTaskDetailsOpen(true);
console.log('Task clicked:', task.title);
};
const handleTaskDetailsClose = () => {
@@ -239,7 +185,6 @@ function App() {
const handleTaskDetailsSave = (updatedTask: Task) => {
handleTaskUpdate(updatedTask.id, updatedTask);
console.log('Task details saved:', updatedTask.title);
};
const handleTaskDelete = async (taskId: string) => {
@@ -248,215 +193,205 @@ function App() {
const confirmTaskDelete = async () => {
if (!deleteTaskId) return;
// Optimistic deletion - remove from UI immediately
const taskToDelete = tasks.find(task => task.id === deleteTaskId);
setTasks(prev => prev.filter(task => task.id !== deleteTaskId));
// Close task details modal if the deleted task was open
if (selectedTask?.id === deleteTaskId) {
handleTaskDetailsClose();
}
setDeleteTaskId(null);
try {
const response = await fetch(`/api/tasks/${deleteTaskId}`, {
method: 'DELETE'
});
// 404 is acceptable - task doesn't exist on backend (seeded task)
if (!response.ok && response.status !== 404) {
throw new Error('Failed to delete task');
}
console.log('Task deleted:', deleteTaskId);
const response = await fetch(`/api/tasks/${deleteTaskId}`, { method: 'DELETE' });
if (!response.ok && response.status !== 404) throw new Error('Failed to delete task');
} catch (error) {
console.error('Error deleting task:', error);
// Restore task on error
if (taskToDelete) {
setTasks(prev => [...prev, taskToDelete]);
}
if (taskToDelete) setTasks(prev => [...prev, taskToDelete]);
}
};
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
console.log('Creating project from template:', { templateId, startDate, projectName });
// In a real app, this would create multiple tasks based on the template
};
if (isLoadingUser) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
const renderContent = () => {
switch (currentTab) {
case 'tasks':
return (
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
);
case 'calendar':
return (
<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}
/>
);
case 'kanban':
return (
<KanbanBoard
tasks={tasks}
onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
);
case 'weeklist':
return (
<WeekListView
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
);
case 'templates':
return (
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => setTasks(prev => [...prev, ...newTasks])}
onNavigateToSettings={() => setCurrentTab('settings')}
/>
);
case 'settings':
return <Settings onNavigateToTemplates={() => setCurrentTab('templates')} />;
default:
return (
<div className="text-center py-8">
<p className="text-muted-foreground">Page not found</p>
</div>
);
}
};
// Setup Redirect
if (setupStatus && !setupStatus.isSetup) {
return (
<Switch>
<Route path="/setup" component={SetupWizard} />
<Route component={() => <SetupWizard />} />
</Switch>
);
}
if (!user) {
return <AuthPage />;
}
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-16 items-center justify-between px-4">
<div className="flex items-center gap-2">
<img
src={logoImage}
alt="TaskFlow Logo"
className="w-8 h-8 object-contain rounded-lg"
data-testid="img-app-logo"
/>
<h1 className="text-lg font-semibold" data-testid="text-app-title">
{t('app.title')}
</h1>
</div>
<div className="flex items-center gap-2">
{(currentTab === 'settings' || currentTab === 'templates') && (
<Button
variant="outline"
size="sm"
onClick={() => setCurrentTab('tasks')}
data-testid="button-back-to-tasks"
>
{t('app.backToTasks')}
</Button>
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="flex flex-col min-h-screen bg-background">
{/* Mobile Header trigger */}
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 md:hidden">
<SidebarTrigger className="-ml-1" />
<div className="font-semibold">{t('app.title')}</div>
</header>
{/* Main Content */}
<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) => setTasks(prev => [...prev, ...newTasks])}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
<AchievementsPage user={user} />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user.role === 'admin' && (
<Route path="/admin/users" component={AdminUserManagement} />
)}
<ThemeToggle />
</div>
<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>
</header>
</div>
</SidebarInset>
</SidebarProvider>
{/* Main Content */}
<main className="pb-20 px-3 py-4 sm:px-4 sm:py-6 max-w-screen-2xl mx-auto">
{renderContent()}
</main>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
{/* Bottom Navigation */}
<BottomNavigation
activeTab={currentTab}
onTabChange={(tab) => {
if (tab === 'settings') {
setCurrentTab('settings');
} else {
setCurrentTab(tab);
}
}}
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 })}
/>
{/* Task Creation Modal */}
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<Toaster />
{/* Task Details Modal */}
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
/>
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
{/* Delete Confirmation Dialog */}
<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>
<Toaster />
</TooltipProvider>
</QueryClientProvider>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
/>
<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>
</TooltipProvider>
);
}