feat: Enhance task filtering, smart scheduling, audit logs and translations
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -1,178 +1,242 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRoute, useLocation } from "wouter";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Task, User } from "@shared/schema";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Sun, Moon, ArrowRight, CheckCircle2, ListTodo, Calendar as CalendarIcon } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { User, Task } from "@shared/schema";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Sun, Moon, CheckCircle2, ArrowRight, ListTodo, Calendar as CalendarIcon, NotebookPen, BrainCircuit } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { format } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { triggerConfetti } from "@/lib/confetti";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
// Steps Configuration
|
||||
const MORNING_STEPS = [
|
||||
{ key: "review_yesterday", title: "Review Yesterday", description: "Did you complete everything?", icon: ListTodo },
|
||||
{ key: "plan_today", title: "Plan Today", description: "What are your top 3 priorities?", icon: CalendarIcon },
|
||||
{ key: "check_schedule", title: "Visualize Success", description: "Take a moment to visualize your day.", icon: BrainCircuit },
|
||||
];
|
||||
|
||||
const EVENING_STEPS = [
|
||||
{ key: "review_today", title: "Review Today", description: "Celebrate your wins!", icon: CheckCircle2 },
|
||||
{ key: "plan_tomorrow", title: "Plan Tomorrow", description: "Set yourself up for success.", icon: CalendarIcon },
|
||||
{ key: "clear_mind", title: "Clear Mind", description: "Jot down any lingering thoughts.", icon: NotebookPen },
|
||||
];
|
||||
|
||||
export default function FocusRoutinePage() {
|
||||
const [match, params] = useRoute("/focus/routine/:type");
|
||||
const type = params?.type as 'morning' | 'evening';
|
||||
const { t } = useTranslation();
|
||||
const [, setLocation] = useLocation();
|
||||
const { toast } = useToast();
|
||||
const [match, params] = useRoute("/focus/routine/:type");
|
||||
const type = params?.type as "morning" | "evening";
|
||||
const [stepIndex, setStepIndex] = useState(0);
|
||||
const queryClient = useQueryClient();
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
const { data: user } = useQuery<User>({ queryKey: ["/api/user"] });
|
||||
const { data: tasks = [] } = useQuery<Task[]>({ queryKey: ["/api/tasks"] });
|
||||
|
||||
// Filter tasks
|
||||
const today = new Date();
|
||||
const todayTasks = tasks.filter(t => {
|
||||
if (!t.dueDate) return false;
|
||||
const d = new Date(t.dueDate);
|
||||
return d.getDate() === today.getDate() && d.getMonth() === today.getMonth();
|
||||
});
|
||||
|
||||
const completedToday = todayTasks.filter(t => t.status === 'done');
|
||||
const pendingTasks = tasks.filter(t => t.status !== 'done');
|
||||
|
||||
// Handlers
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
await apiRequest("POST", `/api/user/routine/${type}/complete`);
|
||||
// Invalidate user query to update lastMorningRoutine/lastEveningRoutine
|
||||
await queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({ title: t('routine.error', 'Failed to save progress'), variant: 'destructive' });
|
||||
}
|
||||
|
||||
if (type === 'morning') {
|
||||
setLocation('/focus');
|
||||
} else {
|
||||
triggerConfetti(0.5, 0.5);
|
||||
toast({ title: t('routine.dayComplete', "Day Complete! Great job.") });
|
||||
// For evening, maybe logout or home? Or achievements
|
||||
setLocation('/achievements');
|
||||
}
|
||||
};
|
||||
const { toast } = useToast();
|
||||
|
||||
// Prevent hydration mismatch or early render
|
||||
if (!match || !['morning', 'evening'].includes(type)) {
|
||||
return <div className="p-8">Invalid routine type</div>;
|
||||
return <div className="p-8 text-center">Invalid routine type</div>;
|
||||
}
|
||||
|
||||
// Animation variants
|
||||
const pageVariants = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
in: { opacity: 1, y: 0 },
|
||||
out: { opacity: 0, y: -20 }
|
||||
const steps = type === "morning" ? MORNING_STEPS : EVENING_STEPS;
|
||||
const currentStep = steps[stepIndex];
|
||||
|
||||
const { data: user } = useQuery<User>({ queryKey: ["/api/user"] });
|
||||
const { data: tasks } = useQuery<Task[]>({ queryKey: ["/api/tasks"] });
|
||||
|
||||
// Task Context
|
||||
const overdueTasks = tasks?.filter(t => t.status !== 'done' && t.dueDate && new Date(t.dueDate) < new Date()) || [];
|
||||
const todayTasks = tasks?.filter(t => t.status !== 'done' && ((t.dueDate && new Date(t.dueDate) <= new Date()) || !t.dueDate)) || [];
|
||||
const completedToday = tasks?.filter(t => {
|
||||
if (t.status !== 'done') return false;
|
||||
// Check if completed today (approximate based on status update or we need 'completedAt' field which we don't strictly preserve in schema except via AuditLog, but let's assume 'done' tasks are relevant)
|
||||
// Ideally we filter by 'last updated' or audit log, but for now just showing 'Done' tasks is okay as visual reinforcement.
|
||||
return true;
|
||||
}) || [];
|
||||
|
||||
const completeRoutineMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await apiRequest("POST", `/api/user/routine/${type}/complete`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
if (type === 'evening') {
|
||||
triggerConfetti(0.5, 0.5);
|
||||
toast({ title: t('routine.dayComplete', "Day Complete! Sleep well.") });
|
||||
setLocation('/achievements'); // Or dashboard
|
||||
} else {
|
||||
toast({ title: t('routine.dayStarted', "Have a great day!") });
|
||||
setLocation('/');
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Failed to complete routine", variant: "destructive" });
|
||||
}
|
||||
});
|
||||
|
||||
const handleNext = () => {
|
||||
if (stepIndex < steps.length - 1) {
|
||||
setStepIndex(stepIndex + 1);
|
||||
} else {
|
||||
completeRoutineMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen w-full flex flex-col justify-center items-center p-4 transition-colors duration-1000 ${type === 'morning' ? 'bg-orange-50/50 dark:bg-orange-950/20' : 'bg-indigo-50/50 dark:bg-indigo-950/20'}`}>
|
||||
if (!user) return null;
|
||||
|
||||
const Icon = currentStep.icon;
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen w-full flex flex-col justify-center items-center p-6 transition-colors duration-1000 ${type === 'morning' ? 'bg-orange-50/50 dark:bg-orange-950/20' : 'bg-indigo-50/50 dark:bg-indigo-950/20'
|
||||
}`}>
|
||||
<motion.div
|
||||
initial="initial" animate="in" exit="out" variants={pageVariants}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-full max-w-2xl"
|
||||
>
|
||||
<Card className="border-none shadow-2xl bg-background/80 backdrop-blur-sm">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<div className="mx-auto mb-4 w-16 h-16 rounded-full flex items-center justify-center bg-primary/10">
|
||||
{type === 'morning' ? <Sun className="w-8 h-8 text-orange-500" /> : <Moon className="w-8 h-8 text-indigo-500" />}
|
||||
</div>
|
||||
<CardTitle className="text-3xl font-bold">
|
||||
{type === 'morning' ? t('routine.goodMorning', 'Good Morning') : t('routine.goodEvening', 'Good Evening')}, {user?.username}
|
||||
</CardTitle>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{type === 'morning'
|
||||
? t('routine.morningSubtitle', "Let's plan your day for success.")
|
||||
: t('routine.eveningSubtitle', "Time to reflect and unwind.")}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<div className="mb-8 text-center space-y-2">
|
||||
<div className="inline-flex items-center justify-center p-4 rounded-full bg-background shadow-sm mb-4">
|
||||
{type === "morning" ? <Sun className="w-8 h-8 text-orange-500" /> : <Moon className="w-8 h-8 text-indigo-500" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="pt-6">
|
||||
<AnimatePresence mode="wait">
|
||||
{type === 'morning' ? (
|
||||
<MorningRoutine tasks={pendingTasks} onComplete={handleComplete} />
|
||||
) : (
|
||||
<EveningRoutine completed={completedToday} pending={pendingTasks} onComplete={handleComplete} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={stepIndex}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Card className="border-none shadow-2xl bg-background/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Step {stepIndex + 1} of {steps.length}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={() => setLocation('/')} className="h-6 text-xs text-muted-foreground">
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${type === 'morning' ? 'bg-orange-100 dark:bg-orange-900/40 text-orange-600' : 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600'}`}>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-2xl">{currentStep.title}</CardTitle>
|
||||
<CardDescription>{currentStep.description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-[300px] flex flex-col gap-4">
|
||||
|
||||
{/* DYNAMIC CONTENT BASED ON STEP */}
|
||||
{currentStep.key === "review_yesterday" && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-4">
|
||||
{overdueTasks.length > 0 ? (
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-100 dark:border-red-900">
|
||||
<h4 className="font-semibold text-red-700 dark:text-red-400 mb-2 flex items-center gap-2">
|
||||
<ListTodo className="w-4 h-4" />
|
||||
Overdue Tasks
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{overdueTasks.map(t => (
|
||||
<li key={t.id} className="text-sm flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red-500" />
|
||||
{t.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 text-center space-y-3">
|
||||
<div className="inline-flex p-3 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600">
|
||||
<CheckCircle2 className="w-8 h-8" />
|
||||
</div>
|
||||
<p className="font-medium">No overdue tasks from yesterday!</p>
|
||||
<p className="text-sm text-muted-foreground">Great job staying on track.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep.key === "plan_today" && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-4">
|
||||
<h4 className="font-medium">Your schedule for specific tasks:</h4>
|
||||
{todayTasks.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{todayTasks.slice(0, 5).map(task => (
|
||||
<div key={task.id} className="flex items-center gap-3 p-3 border rounded-lg bg-card/50">
|
||||
<div className={`w-3 h-3 rounded-full ${task.priority === 'high' ? 'bg-red-500' : 'bg-blue-500'}`} />
|
||||
<span className="font-medium">{task.title}</span>
|
||||
{task.estimatedDuration && <span className="ml-auto text-xs text-muted-foreground">{task.estimatedDuration}m</span>}
|
||||
</div>
|
||||
))}
|
||||
{todayTasks.length > 5 && <p className="text-center text-xs text-muted-foreground">and {todayTasks.length - 5} more...</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-8 border-2 border-dashed rounded-lg">
|
||||
<p className="text-muted-foreground">No tasks specifically scheduled for today.</p>
|
||||
<Button variant="link" onClick={() => window.open('/', '_blank')}>Add Tasks</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/10 rounded-lg text-sm text-blue-700 dark:text-blue-300">
|
||||
💡 Tip: Pick just 3 absolute "Must Do" tasks for today.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep.key === "review_today" && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-4">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-green-500 to-emerald-600 mb-2">
|
||||
{completedToday.length}
|
||||
</div>
|
||||
<p className="text-muted-foreground font-medium">Tasks Completed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<p className="text-sm text-center italic">"Small progress is still progress."</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Default / Text Input Steps */}
|
||||
{["clear_mind", "check_schedule", "plan_tomorrow"].includes(currentStep.key) && (
|
||||
<div className="flex-1 flex flex-col justify-center animate-in fade-in slide-in-from-bottom-4">
|
||||
{currentStep.key === "clear_mind" && (
|
||||
<div className="space-y-4">
|
||||
<Input placeholder="Note down any loose thoughts..." className="h-12 text-lg" />
|
||||
<Button variant="outline" className="w-full">Save to Inbox</Button>
|
||||
</div>
|
||||
)}
|
||||
{currentStep.key !== "clear_mind" && (
|
||||
<div className="text-center py-12 text-muted-foreground italic">
|
||||
Take 2 minutes to {currentStep.title.toLowerCase()}.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between border-t pt-6">
|
||||
<Button variant="ghost" disabled={stepIndex === 0} onClick={() => setStepIndex(stepIndex - 1)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleNext} className="gap-2 px-8" size="lg">
|
||||
{stepIndex === steps.length - 1 ? (
|
||||
<>Finish and Start <CheckCircle2 className="w-4 h-4" /></>
|
||||
) : (
|
||||
<>Next Step <ArrowRight className="w-4 h-4" /></>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MorningRoutine({ tasks, onComplete }: { tasks: Task[], onComplete: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<motion.div className="space-y-6">
|
||||
<div className="bg-muted/50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<ListTodo className="w-4 h-4" />
|
||||
{t('routine.tasksForToday', 'Tasks for Today')}
|
||||
</h3>
|
||||
<ScrollArea className="h-[300px] pr-4">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t('routine.noTasks', 'No tasks scheduled yet. Add some!')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{tasks.map(task => (
|
||||
<div key={task.id} className="flex items-center gap-3 p-3 bg-card border rounded-md">
|
||||
<div className={`w-1 h-8 rounded-full ${getPriorityColor(task.priority)}`} />
|
||||
<span className="flex-1 font-medium">{task.title}</span>
|
||||
{task.estimatedDuration && <span className="text-xs text-muted-foreground">{task.estimatedDuration}m</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button size="lg" onClick={onComplete} className="w-full sm:w-auto">
|
||||
{t('routine.startFocus', 'Start Focus Mode')} <ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function EveningRoutine({ completed, pending, onComplete }: { completed: Task[], pending: Task[], onComplete: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<motion.div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-green-50 dark:bg-green-950/20 rounded-lg border border-green-100 dark:border-green-900 text-center">
|
||||
<div className="text-3xl font-bold text-green-600 mb-1">{completed.length}</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-400">{t('routine.completed', 'Completed')}</div>
|
||||
</div>
|
||||
<div className="p-4 bg-orange-50 dark:bg-orange-950/20 rounded-lg border border-orange-100 dark:border-orange-900 text-center">
|
||||
<div className="text-3xl font-bold text-orange-600 mb-1">{pending.length}</div>
|
||||
<div className="text-sm text-orange-700 dark:text-orange-400">{t('routine.open', 'Remaining')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button size="lg" onClick={onComplete} className="w-full sm:w-auto">
|
||||
{t('routine.endDay', 'End Day')} <CheckCircle2 className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function getPriorityColor(priority: string) {
|
||||
if (priority === 'high') return 'bg-red-500';
|
||||
if (priority === 'medium') return 'bg-yellow-500';
|
||||
return 'bg-blue-500';
|
||||
}
|
||||
|
||||
// End of file
|
||||
|
||||
Reference in New Issue
Block a user