feat: consumer home app layout overhaul + residence kanban board
Layout & Navigation: - Tighten max-width to 6xl, adjust padding, add warm gradient overlay - Add icons to desktop nav links, responsive header height, stronger blur - Active pill highlight on mobile nav icons - Fix middleware blocking static assets (logo.png) behind auth Dashboard restructure: - Merge quick actions into hero area as inline pills - Rename "Coming Up" to "Needs Attention", exclude completed tasks - Promote task cards to #2 with richer card design (2-col grid, colored date badges) - Drop "Your Homes" to #3 with accent bars and larger icons Card redesigns: - Residence cards: accent bar, home icon, warm hover shadow - Contractor cards: letter avatar, text contact links, separator - Document cards: type-colored accent bar, restructured footer - Task cards: warm hover shadow - Empty states: larger icon container, gradient bg, rounded CTA Residence detail page: - Add full kanban board with drag-and-drop for task management - Add "Add Task" button pre-filling residence on task form - Replace broken Users stat with Overdue/Completed stats - Compute task summary from kanban columns (always accurate) Data accuracy fixes: - Fix getMyResidences() to fetch kanban data in parallel and compute real per-residence task counts instead of hardcoding zeros - Task form accepts defaultResidenceId prop for pre-filling - New task page reads residence_id from URL, redirects back after create Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -155,7 +155,7 @@ export default function ContractorsPage() {
|
||||
onAction={contractors.length === 0 ? () => router.push(`${basePath}/contractors/new`) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-5">
|
||||
{filtered.map((c) => (
|
||||
<ContractorCard
|
||||
key={c.id}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function DocumentsPage() {
|
||||
!documentsError &&
|
||||
Array.isArray(documents) &&
|
||||
documents.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{documents.map((doc) => (
|
||||
<DocumentCard key={doc.id} document={doc} />
|
||||
))}
|
||||
@@ -103,7 +103,7 @@ export default function DocumentsPage() {
|
||||
!warrantiesError &&
|
||||
Array.isArray(warranties) &&
|
||||
warranties.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{warranties.map((doc) => (
|
||||
<DocumentCard key={doc.id} document={doc} />
|
||||
))}
|
||||
|
||||
@@ -9,9 +9,10 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<DataProviderProvider value={realProvider}>
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="pointer-events-none fixed inset-0 bg-gradient-to-br from-primary/[0.02] via-transparent to-brand-clay/[0.02]" />
|
||||
<TopBar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-6 py-8 lg:py-12 pb-28 md:pb-12">
|
||||
<main className="max-w-6xl mx-auto px-4 sm:px-8 py-6 lg:py-10 pb-28 md:pb-12">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
|
||||
+121
-111
@@ -18,6 +18,7 @@ import { useTasks } from "@/lib/hooks/use-tasks";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useDataProvider } from "@/lib/demo/data-provider-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TemplateSuggestions } from "@/components/dashboard/template-suggestions";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { MyResidenceResponse } from "@/lib/api/residences";
|
||||
import type { TaskResponse } from "@/lib/api/tasks";
|
||||
@@ -68,59 +69,62 @@ function HomeCard({
|
||||
href={`${basePath}/residences/${r.id}`}
|
||||
className="group block"
|
||||
>
|
||||
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6 transition-all duration-200 hover:shadow-lg hover:shadow-black/[0.04] hover:-translate-y-0.5 hover:border-border/80">
|
||||
{/* Status badge */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="size-11 rounded-xl bg-[#FFF3EB] flex items-center justify-center">
|
||||
<Home className="size-5 text-[#E07A3A]" />
|
||||
<div className="rounded-2xl border border-border bg-card overflow-hidden transition-all duration-200 hover:shadow-[var(--shadow-warm-md)] hover:-translate-y-0.5 hover:border-border/80">
|
||||
<div className="h-1 bg-gradient-to-r from-primary/60 to-primary/20" />
|
||||
<div className="p-5 sm:p-6">
|
||||
{/* Status badge */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="size-12 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Home className="size-5 text-primary" />
|
||||
</div>
|
||||
{overdue > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-500/10 px-2.5 py-1 rounded-full">
|
||||
<CircleAlert className="size-3" />
|
||||
{overdue} overdue
|
||||
</span>
|
||||
) : !isGood && dueSoon > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-amber-600 bg-amber-50 dark:text-amber-400 dark:bg-amber-500/10 px-2.5 py-1 rounded-full">
|
||||
<CalendarClock className="size-3" />
|
||||
{dueSoon} coming up
|
||||
</span>
|
||||
) : isGood && total > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-primary bg-primary/10 px-2.5 py-1 rounded-full">
|
||||
<Sparkles className="size-3" />
|
||||
All good
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{overdue > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-500/10 px-2.5 py-1 rounded-full">
|
||||
<CircleAlert className="size-3" />
|
||||
{overdue} overdue
|
||||
</span>
|
||||
) : !isGood && dueSoon > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-amber-600 bg-amber-50 dark:text-amber-400 dark:bg-amber-500/10 px-2.5 py-1 rounded-full">
|
||||
<CalendarClock className="size-3" />
|
||||
{dueSoon} coming up
|
||||
</span>
|
||||
) : isGood && total > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-[#0D7C66] bg-emerald-50 dark:text-emerald-400 dark:bg-emerald-500/10 px-2.5 py-1 rounded-full">
|
||||
<Sparkles className="size-3" />
|
||||
All good
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Name and address */}
|
||||
<h3 className="font-heading text-lg font-semibold leading-tight group-hover:text-primary transition-colors">
|
||||
{r.name}
|
||||
</h3>
|
||||
{address && (
|
||||
<p className="text-sm text-muted-foreground mt-1 flex items-center gap-1.5">
|
||||
<MapPin className="size-3.5 shrink-0" />
|
||||
<span className="truncate">{address}</span>
|
||||
</p>
|
||||
)}
|
||||
{/* Name and address */}
|
||||
<h3 className="font-heading text-lg font-semibold leading-tight group-hover:text-primary transition-colors">
|
||||
{r.name}
|
||||
</h3>
|
||||
{address && (
|
||||
<p className="text-sm text-muted-foreground mt-1 flex items-center gap-1.5">
|
||||
<MapPin className="size-3.5 shrink-0" />
|
||||
<span className="truncate">{address}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Quick stats */}
|
||||
<div className="flex items-center gap-4 mt-4 pt-4 border-t border-border/60">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{total} {total === 1 ? "task" : "tasks"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
View home
|
||||
<ArrowRight className="size-3 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
{/* Quick stats */}
|
||||
<div className="flex items-center gap-4 mt-4 pt-4 border-t border-border/60">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{total} {total === 1 ? "task" : "tasks"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
View home
|
||||
<ArrowRight className="size-3 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Coming Up (task list) ─── */
|
||||
/* ─── Needs Attention (task cards) ─── */
|
||||
|
||||
function ComingUp({
|
||||
function NeedsAttention({
|
||||
tasks,
|
||||
basePath,
|
||||
}: {
|
||||
@@ -132,7 +136,7 @@ function ComingUp({
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-heading text-lg font-semibold">Coming Up</h2>
|
||||
<h2 className="font-heading text-lg font-semibold">Needs Attention</h2>
|
||||
<Link
|
||||
href={`${basePath}/tasks`}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||
@@ -142,44 +146,51 @@ function ComingUp({
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
{tasks.map((task) => {
|
||||
const dateLabel = task.next_due_date || task.due_date;
|
||||
const isOverdue =
|
||||
dateLabel && new Date(dateLabel) < new Date() && getRelativeDate(dateLabel) === "Overdue";
|
||||
const relative = dateLabel ? getRelativeDate(dateLabel) : null;
|
||||
const isOverdue = relative === "Overdue";
|
||||
const isToday = relative === "Today";
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={task.id}
|
||||
href={`${basePath}/tasks/${task.id}`}
|
||||
className="flex items-center gap-3 rounded-xl px-3 py-3 -mx-3 hover:bg-accent/50 transition-colors group"
|
||||
className="group"
|
||||
>
|
||||
<div
|
||||
className={`size-2 rounded-full shrink-0 ${
|
||||
isOverdue
|
||||
? "bg-red-500"
|
||||
: task.in_progress
|
||||
? "bg-[#0D7C66]"
|
||||
: "bg-border"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium leading-snug truncate group-hover:text-primary transition-colors">
|
||||
{task.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{task.residence_name}
|
||||
</p>
|
||||
</div>
|
||||
{dateLabel && (
|
||||
<span
|
||||
className={`text-xs font-medium shrink-0 ${
|
||||
isOverdue ? "text-red-500" : "text-muted-foreground"
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-all duration-200 hover:shadow-[var(--shadow-warm-sm)] hover:-translate-y-0.5">
|
||||
<div
|
||||
className={`size-2.5 rounded-full shrink-0 ${
|
||||
isOverdue
|
||||
? "bg-red-500"
|
||||
: task.in_progress
|
||||
? "bg-primary"
|
||||
: "bg-border"
|
||||
}`}
|
||||
>
|
||||
{getRelativeDate(dateLabel)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold leading-snug truncate group-hover:text-primary transition-colors">
|
||||
{task.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{task.residence_name}
|
||||
</p>
|
||||
</div>
|
||||
{relative && (
|
||||
<span
|
||||
className={`text-xs font-medium shrink-0 px-2.5 py-1 rounded-full ${
|
||||
isOverdue
|
||||
? "bg-red-50 text-red-600 dark:bg-red-500/10 dark:text-red-400"
|
||||
: isToday
|
||||
? "bg-amber-50 text-amber-600 dark:bg-amber-500/10 dark:text-amber-400"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{relative}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -188,32 +199,6 @@ function ComingUp({
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Quick Actions (subtle, not prominent) ─── */
|
||||
|
||||
function QuickActions({ basePath }: { basePath: string }) {
|
||||
const actions = [
|
||||
{ label: "Add task", href: `${basePath}/tasks/new`, icon: CheckSquare },
|
||||
{ label: "Add pro", href: `${basePath}/contractors/new`, icon: HardHat },
|
||||
{ label: "Save doc", href: `${basePath}/documents/new`, icon: FileText },
|
||||
{ label: "Add home", href: `${basePath}/residences/new`, icon: Home },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{actions.map((a) => (
|
||||
<Link
|
||||
key={a.href}
|
||||
href={a.href}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3.5 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
||||
>
|
||||
<a.icon className="size-3.5" />
|
||||
{a.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Loading State ─── */
|
||||
|
||||
function DashboardSkeleton() {
|
||||
@@ -248,12 +233,17 @@ export default function DashboardPage() {
|
||||
const name = user?.first_name || "";
|
||||
const greeting = `${getTimeGreeting()}${name ? `, ${name}` : ""}`;
|
||||
|
||||
// Flatten all tasks from kanban columns, sort by due date, take upcoming ones
|
||||
// Flatten all tasks from kanban columns (exclude completed/cancelled)
|
||||
const allTasks: TaskResponse[] = kanban?.columns
|
||||
?.flatMap((col) => col.tasks)
|
||||
?.filter((t) => !t.is_cancelled && !t.is_archived) ?? [];
|
||||
|
||||
const upcomingTasks = allTasks
|
||||
// Active tasks that need attention — overdue first, then due soonest
|
||||
const activeTasks = allTasks
|
||||
.filter((t) => {
|
||||
const col = kanban?.columns.find((c) => c.tasks.some((ct) => ct.id === t.id));
|
||||
return col?.name !== "completed_tasks";
|
||||
})
|
||||
.filter((t) => t.next_due_date || t.due_date || t.in_progress)
|
||||
.sort((a, b) => {
|
||||
const dateA = a.next_due_date || a.due_date || "";
|
||||
@@ -287,8 +277,8 @@ export default function DashboardPage() {
|
||||
if (homes.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center">
|
||||
<div className="size-20 rounded-3xl bg-gradient-to-br from-[#FFF3EB] to-[#ECFDF5] flex items-center justify-center mb-6">
|
||||
<Home className="size-9 text-[#E07A3A]" />
|
||||
<div className="size-20 rounded-3xl bg-gradient-to-br from-brand-clay-light to-brand-sage-light flex items-center justify-center mb-6">
|
||||
<Home className="size-9 text-primary" />
|
||||
</div>
|
||||
<h1 className="font-heading text-3xl font-bold tracking-tight">
|
||||
Welcome to Casera{name ? `, ${name}` : ""}
|
||||
@@ -338,10 +328,18 @@ export default function DashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Quick action pill data ─── */
|
||||
const quickActions = [
|
||||
{ label: "Add task", href: `${basePath}/tasks/new`, icon: CheckSquare },
|
||||
{ label: "Add pro", href: `${basePath}/contractors/new`, icon: HardHat },
|
||||
{ label: "Save doc", href: `${basePath}/documents/new`, icon: FileText },
|
||||
{ label: "Add home", href: `${basePath}/residences/new`, icon: Home },
|
||||
];
|
||||
|
||||
/* ─── Main dashboard ─── */
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
{/* Greeting */}
|
||||
{/* Hero — Greeting + inline quick actions */}
|
||||
<div>
|
||||
<h1 className="font-heading text-2xl sm:text-3xl font-bold tracking-tight">
|
||||
{greeting}
|
||||
@@ -349,9 +347,24 @@ export default function DashboardPage() {
|
||||
{statusMsg && (
|
||||
<p className="text-muted-foreground mt-1.5 text-[15px]">{statusMsg}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{quickActions.map((a) => (
|
||||
<Link
|
||||
key={a.href}
|
||||
href={a.href}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-4 py-2.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-primary/30 hover:shadow-[var(--shadow-warm-sm)] transition-all"
|
||||
>
|
||||
<a.icon className="size-3.5" />
|
||||
{a.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Your Homes — the main content */}
|
||||
{/* Needs Attention — overdue and upcoming tasks */}
|
||||
<NeedsAttention tasks={activeTasks} basePath={basePath} />
|
||||
|
||||
{/* Your Homes — dropped to #3 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-heading text-lg font-semibold">Your Homes</h2>
|
||||
@@ -364,7 +377,7 @@ export default function DashboardPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={`grid gap-4 ${
|
||||
<div className={`grid gap-5 ${
|
||||
homes.length === 1
|
||||
? "grid-cols-1 max-w-lg"
|
||||
: "sm:grid-cols-2 lg:grid-cols-3"
|
||||
@@ -375,11 +388,8 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coming Up — clean task list */}
|
||||
<ComingUp tasks={upcomingTasks} basePath={basePath} />
|
||||
|
||||
{/* Quick actions — subtle pills at the bottom */}
|
||||
<QuickActions basePath={basePath} />
|
||||
{/* Template suggestions — at the bottom */}
|
||||
<TemplateSuggestions hasResidences={homes.length > 0} activeTaskCount={allTasks.length} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { MapPin, Pencil, Share2, Trash2, FileDown } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { MapPin, Pencil, Share2, Trash2, FileDown, Plus, ClipboardList, ArrowRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useDataProvider } from "@/lib/demo/data-provider-context";
|
||||
|
||||
@@ -13,7 +15,13 @@ import { LoadingSkeleton } from "@/components/shared/loading-skeleton";
|
||||
import { ErrorBanner } from "@/components/shared/error-banner";
|
||||
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
||||
import { ResidenceSummary } from "@/components/residences/residence-summary";
|
||||
import { useResidence, useResidences, useDeleteResidence } from "@/lib/hooks/use-residences";
|
||||
import { useResidence, useDeleteResidence } from "@/lib/hooks/use-residences";
|
||||
import { useTasksByResidence } from "@/lib/hooks/use-tasks";
|
||||
|
||||
const KanbanBoard = dynamic(
|
||||
() => import("@/components/tasks/kanban-board").then((mod) => ({ default: mod.KanbanBoard })),
|
||||
{ loading: () => <LoadingSkeleton variant="kanban" /> }
|
||||
);
|
||||
|
||||
interface ResidenceDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -26,8 +34,8 @@ export default function ResidenceDetailPage({ params }: ResidenceDetailPageProps
|
||||
const { basePath, sharing } = useDataProvider();
|
||||
|
||||
const { data: residence, isLoading, error, refetch } = useResidence(id);
|
||||
const { data: residences } = useResidences();
|
||||
const deleteResidence = useDeleteResidence();
|
||||
const { data: kanbanData, isLoading: tasksLoading } = useTasksByResidence(id);
|
||||
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [reportLoading, setReportLoading] = useState(false);
|
||||
@@ -49,10 +57,15 @@ export default function ResidenceDetailPage({ params }: ResidenceDetailPageProps
|
||||
}
|
||||
};
|
||||
|
||||
// Find the task summary from the residences list
|
||||
const resList = Array.isArray(residences) ? residences : [];
|
||||
const myResidence = resList.find((r) => r.residence.id === id);
|
||||
const taskSummary = myResidence?.task_summary;
|
||||
// Compute task summary directly from kanban columns (always accurate)
|
||||
const taskSummary = kanbanData
|
||||
? {
|
||||
total: kanbanData.columns.reduce((sum, col) => sum + col.tasks.length, 0),
|
||||
in_progress: kanbanData.columns.find((c) => c.name === "in_progress_tasks")?.tasks.length ?? 0,
|
||||
overdue: kanbanData.columns.find((c) => c.name === "overdue_tasks")?.tasks.length ?? 0,
|
||||
completed: kanbanData.columns.find((c) => c.name === "completed_tasks")?.tasks.length ?? 0,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -152,10 +165,55 @@ export default function ResidenceDetailPage({ params }: ResidenceDetailPageProps
|
||||
<ResidenceSummary
|
||||
totalTasks={taskSummary.total}
|
||||
inProgress={taskSummary.in_progress}
|
||||
userCount={residence.user_count}
|
||||
overdue={taskSummary.overdue}
|
||||
completed={taskSummary.completed}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tasks Kanban Board */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-heading text-lg font-semibold">Tasks</h2>
|
||||
<Link
|
||||
href={`${basePath}/tasks?residence_id=${id}`}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||
>
|
||||
View all
|
||||
<ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<Button asChild size="sm" className="rounded-xl shadow-[var(--shadow-warm-sm)]">
|
||||
<Link href={`${basePath}/tasks/new?residence_id=${id}`}>
|
||||
<Plus className="size-4 mr-1.5" />
|
||||
Add Task
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{tasksLoading && <LoadingSkeleton variant="kanban" />}
|
||||
|
||||
{!tasksLoading && kanbanData && kanbanData.columns.every((col) => col.tasks.length === 0) && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center rounded-2xl border border-dashed border-border/60 bg-muted/20">
|
||||
<ClipboardList className="size-8 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm font-medium text-muted-foreground">No tasks yet</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1 max-w-xs">
|
||||
Add your first task to start tracking what needs to get done around this home.
|
||||
</p>
|
||||
<Button asChild size="sm" className="mt-4 rounded-full">
|
||||
<Link href={`${basePath}/tasks/new?residence_id=${id}`}>
|
||||
<Plus className="size-4 mr-1.5" />
|
||||
Add Task
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!tasksLoading && kanbanData && !kanbanData.columns.every((col) => col.tasks.length === 0) && (
|
||||
<KanbanBoard data={kanbanData} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{residence.description && (
|
||||
<Card>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ResidencesPage() {
|
||||
)}
|
||||
|
||||
{!isLoading && !error && Array.isArray(residences) && residences.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{residences.map((item) => (
|
||||
<ResidenceCard key={item.residence.id} data={item} />
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { PageHeader } from "@/components/shared/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
@@ -10,9 +10,13 @@ import { useDataProvider } from "@/lib/demo/data-provider-context";
|
||||
|
||||
export default function NewTaskPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { basePath } = useDataProvider();
|
||||
const createTask = useCreateTask();
|
||||
|
||||
const residenceId = searchParams.get("residence_id");
|
||||
const defaultResidenceId = residenceId ? Number(residenceId) : undefined;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-2xl mx-auto">
|
||||
<PageHeader title="New Task" />
|
||||
@@ -20,11 +24,16 @@ export default function NewTaskPage() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<TaskForm
|
||||
defaultResidenceId={defaultResidenceId}
|
||||
onSubmit={(data) => {
|
||||
createTask.mutate(data, {
|
||||
onSuccess: () => {
|
||||
toast.success("Task created");
|
||||
router.push(`${basePath}/tasks`);
|
||||
if (defaultResidenceId) {
|
||||
router.push(`${basePath}/residences/${defaultResidenceId}`);
|
||||
} else {
|
||||
router.push(`${basePath}/tasks`);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to create task");
|
||||
|
||||
@@ -10,10 +10,11 @@ export default function DemoAppLayout({ children }: { children: React.ReactNode
|
||||
return (
|
||||
<DataProviderProvider value={demoProvider}>
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="pointer-events-none fixed inset-0 bg-gradient-to-br from-primary/[0.02] via-transparent to-brand-clay/[0.02]" />
|
||||
<DemoBanner />
|
||||
<TopBar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-6 py-8 lg:py-12 pb-28 md:pb-12">
|
||||
<main className="max-w-6xl mx-auto px-4 sm:px-8 py-6 lg:py-10 pb-28 md:pb-12">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user