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
+32 -30
View File
@@ -12,6 +12,8 @@ import TimeCompletionModal from './TimeCompletionModal';
import { addDays, format, isSameDay, startOfWeek, endOfWeek, startOfDay, endOfDay, addWeeks } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useDateLocale } from '../hooks/use-date-locale';
interface WeekListViewProps {
tasks: Task[];
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
@@ -26,6 +28,7 @@ type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: WeekListViewProps) {
const { t } = useTranslation();
const dateLocale = useDateLocale();
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
const [filterBy, setFilterBy] = useState<FilterOption>('all');
@@ -64,10 +67,10 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
// Search filter
const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
(task.description?.toLowerCase()?.includes(searchQuery.toLowerCase()) ?? false);
(task.description?.toLowerCase()?.includes(searchQuery.toLowerCase()) ?? false);
if (!matchesSearch) return false;
// Status filter
switch (filterBy) {
case 'overdue':
@@ -85,18 +88,18 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return a.dueDate.getTime() - b.dueDate.getTime();
case 'priority':
const priorityOrder = { high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority as keyof typeof priorityOrder] - priorityOrder[a.priority as keyof typeof priorityOrder];
case 'title':
return a.title.localeCompare(b.title);
case 'status':
const statusOrder = { todo: 1, inProgress: 2, done: 3 };
return statusOrder[a.status as keyof typeof statusOrder] - statusOrder[b.status as keyof typeof statusOrder];
default:
return 0;
}
@@ -115,7 +118,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
};
const getTasksForDate = (date: Date) => {
return filteredAndSortedTasks.filter(task =>
return filteredAndSortedTasks.filter(task =>
task.dueDate && isSameDay(task.dueDate, date)
);
};
@@ -136,7 +139,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
const handleTimeLogged = (timeSpent: number) => {
if (completionModal.task) {
onTaskUpdate?.(completionModal.task.id, {
onTaskUpdate?.(completionModal.task.id, {
status: 'done',
timeTracked: (completionModal.task.timeTracked || 0) + timeSpent,
isTracking: false
@@ -148,7 +151,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
return (
<div className="space-y-4 pb-4">
{/* Header and Search Filters */}
<div className="sticky top-16 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b pb-3 mb-4">
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b pb-3 mb-4 pt-4">
<div className="space-y-3">
{/* Title Bar */}
<div className="flex items-center justify-between">
@@ -175,7 +178,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
data-testid="input-search-week-tasks"
/>
</div>
<div className="flex gap-3">
<Select value={filterBy} onValueChange={(value: FilterOption) => {
setFilterBy(value);
@@ -195,7 +198,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
<SelectItem value="overdue">{t('weekList.filter.overdue')} ({getFilterCount('overdue')})</SelectItem>
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={(value: SortOption) => {
setSortBy(value);
console.log('Sort changed to:', value);
@@ -223,13 +226,13 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-primary" />
<h3 className="text-sm font-semibold" data-testid="text-week-range">
{format(weekStart, 'MMM d')} - {format(weekEnd, 'MMM d, yyyy')}
{format(weekStart, 'MMM d', { locale: dateLocale })} - {format(weekEnd, 'MMM d, yyyy', { locale: dateLocale })}
</h3>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={() => navigateCalendar('prev')}
data-testid="button-prev-week-list"
@@ -237,18 +240,18 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
<ChevronLeft className="w-4 h-4" />
{t('weekList.previous')}
</Button>
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={goToCurrentWeek}
data-testid="button-today-week-list"
>
{t('weekList.today')}
</Button>
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={() => navigateCalendar('next')}
data-testid="button-next-week-list"
@@ -265,20 +268,19 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
const dayTasks = getTasksForDate(date);
const isToday = isSameDay(date, new Date());
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
return (
<div
key={index}
className={`p-2 rounded border-2 text-center ${
isToday ? 'border-primary bg-primary/5' : isWeekend ? 'bg-muted/50' : 'border-border'
}`}
className={`p-2 rounded border-2 text-center ${isToday ? 'border-primary bg-primary/5' : isWeekend ? 'bg-muted/50' : 'border-border'
}`}
data-testid={`week-calendar-date-${format(date, 'yyyy-MM-dd')}`}
>
<div className={`text-xs font-medium ${isWeekend ? 'text-muted-foreground' : 'text-muted-foreground'}`}>
{format(date, 'EEE')}
{format(date, 'EEE', { locale: dateLocale })}
</div>
<div className={`text-sm font-semibold ${isToday ? 'text-primary' : ''}`}>
{format(date, 'd')}
{format(date, 'd', { locale: dateLocale })}
</div>
{dayTasks.length > 0 && (
<Badge variant="secondary" className="mt-1 text-xs">
@@ -296,7 +298,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
{filteredAndSortedTasks.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground" data-testid="text-no-week-tasks">
{searchQuery || filterBy !== 'all'
{searchQuery || filterBy !== 'all'
? t('weekList.noMatchingTasks')
: t('weekList.noTasks')
}
@@ -305,7 +307,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
) : (
<>
<div className="text-sm font-medium text-muted-foreground mb-2">
{t('weekList.tasksFor', { start: format(weekStart, 'MMM d'), end: format(weekEnd, 'MMM d') })}
{t('weekList.tasksFor', { start: format(weekStart, 'MMM d', { locale: dateLocale }), end: format(weekEnd, 'MMM d', { locale: dateLocale }) })}
</div>
{filteredAndSortedTasks.map((task) => (
<div key={task.id}>