v0.5.1 — Reminders, plant groups, location views, Wikipedia photos, Docker deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
158
src/app/(dashboard)/tasks/page.tsx
Normal file
158
src/app/(dashboard)/tasks/page.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { db } from "@/lib/db";
|
||||
import { isOverdue, isDueSoon, describeRecurrence } from "@/lib/tasks/schedule";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { AddTaskButton } from "@/components/tasks/add-task-button";
|
||||
import { CompleteTaskButton } from "@/components/tasks/complete-task-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { CalendarDays, Leaf, Home, Wrench, CheckCircle2, AlertCircle, Clock } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata = { title: "Reminders" };
|
||||
|
||||
const CATEGORY_ICONS = {
|
||||
GARDEN: Leaf,
|
||||
HOME: Home,
|
||||
EQUIPMENT: Wrench,
|
||||
OTHER: Clock,
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS = {
|
||||
GARDEN: "Garden",
|
||||
HOME: "Home",
|
||||
EQUIPMENT: "Equipment",
|
||||
OTHER: "Other",
|
||||
};
|
||||
|
||||
export default async function TasksPage() {
|
||||
const tasks = await db.task.findMany({
|
||||
where: { active: true },
|
||||
orderBy: { nextDue: "asc" },
|
||||
include: { plant: { select: { id: true, commonName: true, variety: true } } },
|
||||
});
|
||||
|
||||
const overdue = tasks.filter((t) => isOverdue(t.nextDue));
|
||||
const upcoming = tasks.filter((t) => !isOverdue(t.nextDue) && isDueSoon(t.nextDue, 60));
|
||||
const later = tasks.filter((t) => !isOverdue(t.nextDue) && !isDueSoon(t.nextDue, 60));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold">Reminders</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{overdue.length > 0
|
||||
? `${overdue.length} overdue · ${upcoming.length} coming up`
|
||||
: `${upcoming.length} coming up in the next 60 days`}
|
||||
</p>
|
||||
</div>
|
||||
<AddTaskButton />
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 && (
|
||||
<div className="text-center py-16 text-muted-foreground">
|
||||
<CalendarDays className="h-12 w-12 mx-auto mb-4 opacity-20" />
|
||||
<p className="font-medium">No reminders yet</p>
|
||||
<p className="text-sm mt-1">Add your first reminder to start tracking what needs doing.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{overdue.length > 0 && (
|
||||
<Section title="Overdue" icon={AlertCircle} iconClass="text-destructive">
|
||||
{overdue.map((task) => (
|
||||
<TaskCard key={task.id} task={task} overdue />
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{upcoming.length > 0 && (
|
||||
<Section title="Coming up" icon={CalendarDays} iconClass="text-[hsl(var(--leaf))]">
|
||||
{upcoming.map((task) => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{later.length > 0 && (
|
||||
<Section title="Later" icon={Clock} iconClass="text-muted-foreground">
|
||||
{later.map((task) => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ title, icon: Icon, iconClass, children }: {
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
iconClass: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Icon className={`h-4 w-4 ${iconClass}`} />
|
||||
<h2 className="font-display text-lg font-semibold">{title}</h2>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskCard({ task, overdue = false }: {
|
||||
task: Awaited<ReturnType<typeof db.task.findMany>>[0] & {
|
||||
plant: { id: string; commonName: string; variety: string | null } | null;
|
||||
};
|
||||
overdue?: boolean;
|
||||
}) {
|
||||
const Icon = CATEGORY_ICONS[task.category];
|
||||
const daysUntil = Math.ceil((new Date(task.nextDue).getTime() - Date.now()) / 86400000);
|
||||
|
||||
return (
|
||||
<Card className={overdue ? "border-destructive/40" : ""}>
|
||||
<CardContent className="py-3 px-4 flex items-start gap-3">
|
||||
<Icon className={`h-4 w-4 mt-0.5 shrink-0 ${overdue ? "text-destructive" : "text-muted-foreground"}`} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start gap-2 flex-wrap">
|
||||
<p className="font-medium text-sm">{task.title}</p>
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{CATEGORY_LABELS[task.category]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.plant && (
|
||||
<Link href={`/garden/${task.plant.id}`} className="text-xs text-[hsl(var(--leaf))] hover:underline">
|
||||
{task.plant.commonName}{task.plant.variety ? ` · ${task.plant.variety}` : ""}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{task.notes && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{task.notes}</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{describeRecurrence(task.recurrence, task.intervalDays, task.month)}
|
||||
{task.lastDone && ` · Last done ${formatDate(task.lastDone)}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="text-right">
|
||||
<p className={`text-xs font-medium ${overdue ? "text-destructive" : "text-muted-foreground"}`}>
|
||||
{overdue
|
||||
? `${Math.abs(daysUntil)}d overdue`
|
||||
: daysUntil === 0 ? "Today"
|
||||
: daysUntil === 1 ? "Tomorrow"
|
||||
: `In ${daysUntil} days`}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{formatDate(task.nextDue)}</p>
|
||||
</div>
|
||||
<CompleteTaskButton taskId={task.id} taskTitle={task.title} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user