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:
Bonna Moon
2026-06-15 20:53:03 -05:00
parent e3f6a8ec92
commit 04dec2a198
32 changed files with 1988 additions and 181 deletions

View 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>
);
}