feat: redesign app UI — top nav, clean dashboard, warm branding

- Replace sidebar with top navigation bar (like Airbnb/Nextdoor)
- Redesign dashboard: home cards, coming up tasks, quick action pills
- Remove widget-heavy layout (charts, stats, activity feed)
- Add landing page with hero, features, how-it-works, CTA sections
- Update auth pages with split layout
- Clean white theme with neutral grays, brand orange/teal accents
- Friendly copy across all empty states and page headers
- Add Bricolage Grotesque + Outfit fonts
- Default to light mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
treyt
2026-03-03 13:06:13 -06:00
parent a0e38e5ae5
commit db89ddb861
37 changed files with 1622 additions and 498 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

+54 -1
View File
@@ -1,11 +1,64 @@
"use client";
import Image from "next/image";
import Link from "next/link";
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<div className="min-h-screen flex bg-[#FAFAF7]">
{/* Left brand panel — hidden on mobile */}
<div className="hidden lg:flex lg:w-[480px] xl:w-[540px] relative flex-col justify-between bg-[#1C1917] p-10 overflow-hidden">
{/* Decorative blurs */}
<div className="absolute top-0 right-0 w-80 h-80 rounded-full bg-[#E07A3A]/15 blur-[100px] pointer-events-none" />
<div className="absolute bottom-0 left-0 w-64 h-64 rounded-full bg-[#0D7C66]/10 blur-[80px] pointer-events-none" />
{/* Subtle grid */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage:
"linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)",
backgroundSize: "48px 48px",
}}
/>
<div className="relative">
<Link href="/" className="flex items-center gap-2.5">
<Image
src="/logo.png"
alt="Casera"
width={36}
height={36}
className="rounded-lg"
/>
<span className="font-heading text-xl font-bold text-white">
Casera
</span>
</Link>
</div>
<div className="relative">
<h2 className="font-heading text-3xl font-bold text-white leading-snug mb-4">
Home maintenance,<br />
<span className="text-[#E07A3A]">simplified.</span>
</h2>
<p className="text-[#A8A29E] leading-relaxed max-w-sm">
Track tasks, organize contractors, and store important
documents all in one place built for homeowners.
</p>
</div>
<p className="relative text-xs text-[#78716C]">
&copy; {new Date().getFullYear()} Casera
</p>
</div>
{/* Right form area */}
<div className="flex-1 flex items-center justify-center p-6 sm:p-8">
<div className="w-full max-w-md">
{children}
</div>
</div>
</div>
);
}
+5 -5
View File
@@ -104,9 +104,9 @@ export default function ContractorsPage() {
return (
<div className="space-y-6">
<PageHeader
title="Contractors"
description="Manage your trusted contractors and service providers"
actionLabel="Add Contractor"
title="Your Pros"
description="The people you trust to get the job done"
actionLabel="Add a Pro"
onAction={() => router.push(`${basePath}/contractors/new`)}
>
<Button
@@ -145,10 +145,10 @@ export default function ContractorsPage() {
{filtered.length === 0 ? (
<EmptyState
icon={Wrench}
title="No contractors found"
title="No pros saved yet"
description={
(contractors?.length ?? 0) === 0
? "Add your first contractor to keep track of service providers."
? "Save your go-to plumber, electrician, or handyman so you always know who to call."
: "Try adjusting your search or filters."
}
actionLabel={contractors.length === 0 ? "Add Contractor" : undefined}
+7 -7
View File
@@ -32,8 +32,8 @@ export default function DocumentsPage() {
<div className="space-y-6">
<PageHeader
title="Documents"
description="Manage your property documents and warranties"
actionLabel="Add Document"
description="Warranties, manuals, receipts — all in one place"
actionLabel="Save a Document"
onAction={() => router.push(`${basePath}/documents/new`)}
/>
@@ -59,9 +59,9 @@ export default function DocumentsPage() {
documents.length === 0 && (
<EmptyState
icon={FileText}
title="No documents yet"
description="Add your first document to start organizing your property records."
actionLabel="Add Document"
title="No documents saved yet"
description="Store warranties, manuals, receipts, and more — so they're easy to find when you need them."
actionLabel="Save a Document"
onAction={() => router.push(`${basePath}/documents/new`)}
/>
)}
@@ -94,8 +94,8 @@ export default function DocumentsPage() {
warranties.length === 0 && (
<EmptyState
icon={FileText}
title="No warranties yet"
description="Documents with type 'warranty' will appear here."
title="No warranties saved yet"
description="When you save a document as a warranty, it'll show up here for easy access."
/>
)}
+2 -9
View File
@@ -1,6 +1,5 @@
"use client";
import { Sidebar } from '@/components/layout/sidebar';
import { TopBar } from '@/components/layout/top-bar';
import { MobileNav } from '@/components/layout/mobile-nav';
import { DataProviderProvider } from '@/lib/demo/data-provider-context';
@@ -10,18 +9,12 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<DataProviderProvider value={realProvider}>
<div className="min-h-screen bg-background">
{/* Sidebar - hidden on mobile */}
<Sidebar />
{/* Main content area */}
<div className="md:ml-16 lg:ml-64 flex flex-col min-h-screen">
<TopBar />
<main className="flex-1 p-4 lg:p-6 pb-20 md:pb-6">
<main className="max-w-7xl mx-auto px-6 py-8 lg:py-12 pb-28 md:pb-12">
{children}
</main>
</div>
{/* Mobile bottom nav */}
<MobileNav />
</div>
</DataProviderProvider>
+369 -45
View File
@@ -1,61 +1,385 @@
"use client";
import dynamic from "next/dynamic";
import Link from "next/link";
import {
Home,
CheckSquare,
HardHat,
FileText,
MapPin,
ArrowRight,
Plus,
CalendarClock,
Sparkles,
CircleAlert,
} from "lucide-react";
import { useResidences } from "@/lib/hooks/use-residences";
import { useTasks } from "@/lib/hooks/use-tasks";
import { useAuthStore } from "@/stores/auth";
import { LoadingSkeleton } from "@/components/shared/loading-skeleton";
import { StatsCards } from "@/components/dashboard/stats-cards";
import { RecentActivity } from "@/components/dashboard/recent-activity";
import { useDataProvider } from "@/lib/demo/data-provider-context";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import type { MyResidenceResponse } from "@/lib/api/residences";
import type { TaskResponse } from "@/lib/api/tasks";
const TaskCompletionChart = dynamic(
() => import("@/components/dashboard/task-completion-chart").then((mod) => ({ default: mod.TaskCompletionChart })),
{
loading: () => (
<div className="rounded-lg border p-6 space-y-4">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-[300px] w-full" />
</div>
),
/* ─── Helpers ─── */
function getTimeGreeting() {
const h = new Date().getHours();
if (h < 12) return "Good morning";
if (h < 17) return "Good afternoon";
return "Good evening";
}
);
export default function DashboardPage() {
const { data: residences, isLoading } = useResidences();
const user = useAuthStore((s) => s.user);
function getRelativeDate(dateStr: string) {
const date = new Date(dateStr);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const diff = Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
const list = Array.isArray(residences) ? residences : [];
const totalOverdue =
list.reduce((sum, r) => sum + (r.task_summary?.overdue ?? 0), 0);
const totalDueSoon =
list.reduce((sum, r) => sum + (r.task_summary?.due_soon ?? 0), 0);
const totalActive =
list.reduce((sum, r) => sum + (r.task_summary?.in_progress ?? 0), 0);
const totalCompleted =
list.reduce((sum, r) => sum + (r.task_summary?.completed ?? 0), 0);
if (diff < 0) return "Overdue";
if (diff === 0) return "Today";
if (diff === 1) return "Tomorrow";
if (diff < 7) return date.toLocaleDateString("en-US", { weekday: "long" });
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
/* ─── Home Card ─── */
function HomeCard({
data,
basePath,
}: {
data: MyResidenceResponse;
basePath: string;
}) {
const r = data.residence;
const overdue = data.task_summary?.overdue ?? 0;
const dueSoon = data.task_summary?.due_soon ?? 0;
const total = data.task_summary?.total ?? 0;
const address = [r.street_address, r.city, r.state_province]
.filter(Boolean)
.join(", ");
const isGood = overdue === 0 && dueSoon === 0;
return (
<div className="space-y-8">
<h1 className="text-2xl font-bold tracking-tight">
{user?.first_name
? `Welcome back, ${user.first_name}`
: "Dashboard"}
</h1>
<Link
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>
{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>
{isLoading ? (
<LoadingSkeleton variant="card-grid" count={4} />
) : (
<>
<StatsCards
overdue={totalOverdue}
dueSoon={totalDueSoon}
active={totalActive}
completed={totalCompleted}
/>
<TaskCompletionChart data={[]} />
<RecentActivity />
</>
{/* 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>
</div>
</div>
</Link>
);
}
/* ─── Coming Up (task list) ─── */
function ComingUp({
tasks,
basePath,
}: {
tasks: TaskResponse[];
basePath: string;
}) {
if (tasks.length === 0) return null;
return (
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="font-heading text-lg font-semibold">Coming Up</h2>
<Link
href={`${basePath}/tasks`}
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
>
All tasks
<ArrowRight className="size-3.5" />
</Link>
</div>
<div className="space-y-1">
{tasks.map((task) => {
const dateLabel = task.next_due_date || task.due_date;
const isOverdue =
dateLabel && new Date(dateLabel) < new Date() && getRelativeDate(dateLabel) === "Overdue";
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"
>
<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"
}`}
>
{getRelativeDate(dateLabel)}
</span>
)}
</Link>
);
})}
</div>
</div>
);
}
/* ─── 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() {
return (
<div className="space-y-10">
<div className="space-y-2">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-5 w-48" />
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-2xl border bg-card p-6 space-y-4">
<Skeleton className="size-11 rounded-xl" />
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
))}
</div>
</div>
);
}
/* ─── Dashboard ─── */
export default function DashboardPage() {
const { data: residences, isLoading: homesLoading } = useResidences();
const { data: kanban, isLoading: tasksLoading } = useTasks();
const user = useAuthStore((s) => s.user);
const { basePath } = useDataProvider();
const homes = Array.isArray(residences) ? residences : [];
const name = user?.first_name || "";
const greeting = `${getTimeGreeting()}${name ? `, ${name}` : ""}`;
// Flatten all tasks from kanban columns, sort by due date, take upcoming ones
const allTasks: TaskResponse[] = kanban?.columns
?.flatMap((col) => col.tasks)
?.filter((t) => !t.is_cancelled && !t.is_archived) ?? [];
const upcomingTasks = allTasks
.filter((t) => t.next_due_date || t.due_date || t.in_progress)
.sort((a, b) => {
const dateA = a.next_due_date || a.due_date || "";
const dateB = b.next_due_date || b.due_date || "";
if (!dateA) return 1;
if (!dateB) return -1;
return new Date(dateA).getTime() - new Date(dateB).getTime();
})
.slice(0, 6);
const totalOverdue = homes.reduce(
(sum, r) => sum + (r.task_summary?.overdue ?? 0), 0
);
// Status line under greeting
const statusMsg = homes.length === 0
? ""
: totalOverdue > 0
? totalOverdue === 1
? "One thing needs your attention."
: `${totalOverdue} things need your attention.`
: allTasks.length > 0
? "Everything\u2019s looking good."
: "";
if (homesLoading || tasksLoading) {
return <DashboardSkeleton />;
}
/* ─── Empty state: no homes yet ─── */
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>
<h1 className="font-heading text-3xl font-bold tracking-tight">
Welcome to Casera{name ? `, ${name}` : ""}
</h1>
<p className="text-muted-foreground mt-3 max-w-md text-base leading-relaxed">
The easiest way to keep your home running smoothly.
Add your first home to get started.
</p>
<Button
asChild
className="mt-8 rounded-full px-8 h-12 text-base"
>
<Link href={`${basePath}/residences/new`}>
<Plus className="size-5 mr-2" />
Add Your Home
</Link>
</Button>
<div className="grid sm:grid-cols-3 gap-5 mt-16 max-w-xl w-full">
{[
{
icon: CheckSquare,
title: "Track tasks",
body: "Repairs, maintenance, projects — all in one place.",
},
{
icon: HardHat,
title: "Save your pros",
body: "Never lose a good contractor\u2019s number again.",
},
{
icon: FileText,
title: "Store documents",
body: "Warranties, manuals, receipts — easy to find.",
},
].map((tip) => (
<div key={tip.title} className="text-center">
<tip.icon className="size-5 text-muted-foreground mx-auto mb-2" />
<p className="text-sm font-medium">{tip.title}</p>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
{tip.body}
</p>
</div>
))}
</div>
</div>
);
}
/* ─── Main dashboard ─── */
return (
<div className="space-y-10">
{/* Greeting */}
<div>
<h1 className="font-heading text-2xl sm:text-3xl font-bold tracking-tight">
{greeting}
</h1>
{statusMsg && (
<p className="text-muted-foreground mt-1.5 text-[15px]">{statusMsg}</p>
)}
</div>
{/* Your Homes — the main content */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="font-heading text-lg font-semibold">Your Homes</h2>
<Link
href={`${basePath}/residences/new`}
className="inline-flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<Plus className="size-3.5" />
Add
</Link>
</div>
<div className={`grid gap-4 ${
homes.length === 1
? "grid-cols-1 max-w-lg"
: "sm:grid-cols-2 lg:grid-cols-3"
}`}>
{homes.map((home) => (
<HomeCard key={home.residence.id} data={home} basePath={basePath} />
))}
</div>
</div>
{/* Coming Up — clean task list */}
<ComingUp tasks={upcomingTasks} basePath={basePath} />
{/* Quick actions — subtle pills at the bottom */}
<QuickActions basePath={basePath} />
</div>
);
}
+6 -6
View File
@@ -19,9 +19,9 @@ export default function ResidencesPage() {
return (
<div className="space-y-6">
<PageHeader
title="Residences"
description="Manage your properties"
actionLabel="Add Residence"
title="Your Homes"
description="All the places you look after"
actionLabel="Add Home"
onAction={() => router.push(`${basePath}/residences/new`)}
/>
@@ -37,9 +37,9 @@ export default function ResidencesPage() {
{!isLoading && !error && Array.isArray(residences) && residences.length === 0 && (
<EmptyState
icon={Home}
title="No residences yet"
description="Add your first property to start tracking tasks and maintenance."
actionLabel="Add Residence"
title="No homes added yet"
description="Add your home to start keeping track of everything tasks, documents, contractors, and more."
actionLabel="Add Your Home"
onAction={() => router.push(`${basePath}/residences/new`)}
/>
)}
+10 -6
View File
@@ -21,17 +21,21 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold tracking-tight">Settings</h1>
<div>
<h1 className="font-heading text-2xl font-bold tracking-tight">Settings</h1>
<p className="text-sm text-muted-foreground mt-1">Manage your account preferences.</p>
</div>
<div className="flex flex-col sm:flex-row gap-6">
<nav className="flex sm:flex-col gap-1 sm:w-48 shrink-0">
<nav className="flex sm:flex-col gap-1 sm:w-52 shrink-0">
{settingsNav.map((item) => (
<Link key={item.href} href={item.href}
className={cn(
"flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"hover:bg-accent hover:text-accent-foreground",
pathname === item.href ? "bg-accent text-accent-foreground" : "text-muted-foreground"
"flex items-center gap-2.5 rounded-xl px-3.5 py-2.5 text-sm font-medium transition-all duration-200",
pathname === item.href
? "bg-primary/10 text-primary shadow-sm"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
)}>
<item.icon className="size-4" />
<item.icon className={cn("size-4", pathname === item.href ? "text-primary" : "text-muted-foreground")} />
{item.label}
</Link>
))}
+4 -4
View File
@@ -44,7 +44,7 @@ export default function TasksPage() {
<div className="space-y-6">
<PageHeader
title="Tasks"
description="Manage your home maintenance tasks"
description="Everything on your to-do list"
actionLabel="New Task"
onAction={() => router.push(`${basePath}/tasks/new`)}
>
@@ -72,9 +72,9 @@ export default function TasksPage() {
{!isLoading && !isError && isEmpty && (
<EmptyState
icon={ClipboardList}
title="No tasks yet"
description="Create your first task to start tracking home maintenance."
actionLabel="New Task"
title="Nothing on the list yet"
description="When something around the house needs attention, add it here and we'll help you stay on top of it."
actionLabel="Add Your First Task"
onAction={() => router.push(`${basePath}/tasks/new`)}
/>
)}
+2 -10
View File
@@ -1,6 +1,5 @@
"use client";
import { Sidebar } from '@/components/layout/sidebar';
import { TopBar } from '@/components/layout/top-bar';
import { MobileNav } from '@/components/layout/mobile-nav';
import { DemoBanner } from '@/components/demo/demo-banner';
@@ -12,19 +11,12 @@ export default function DemoAppLayout({ children }: { children: React.ReactNode
<DataProviderProvider value={demoProvider}>
<div className="min-h-screen bg-background">
<DemoBanner />
{/* Sidebar - hidden on mobile */}
<Sidebar />
{/* Main content area */}
<div className="md:ml-16 lg:ml-64 flex flex-col min-h-screen">
<TopBar />
<main className="flex-1 p-4 lg:p-6 pb-20 md:pb-6">
<main className="max-w-7xl mx-auto px-6 py-8 lg:py-12 pb-28 md:pb-12">
{children}
</main>
</div>
{/* Mobile bottom nav */}
<MobileNav />
</div>
</DataProviderProvider>
+37 -18
View File
@@ -1,38 +1,57 @@
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import { ArrowRight, Play } from "lucide-react";
export default function DemoLandingPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background px-4">
<div className="mx-auto max-w-md text-center">
<div className="flex min-h-screen flex-col items-center justify-center bg-[#FAFAF7] px-6 relative overflow-hidden">
{/* Decorative background */}
<div className="absolute top-20 right-[-10%] w-[500px] h-[500px] rounded-full bg-[#E07A3A]/[0.04] blur-3xl pointer-events-none" />
<div className="absolute bottom-0 left-[-5%] w-[300px] h-[300px] rounded-full bg-[#0D7C66]/[0.03] blur-3xl pointer-events-none" />
<div className="relative mx-auto max-w-lg text-center">
{/* Logo */}
<h1 className="mb-8 text-2xl font-bold tracking-tight text-primary">
<Link href="/" className="inline-flex items-center gap-2.5 mb-10">
<Image
src="/logo.png"
alt="Casera"
width={36}
height={36}
className="rounded-lg"
/>
<span className="font-heading text-2xl font-bold tracking-tight text-[#1C1917]">
Casera
</h1>
</span>
</Link>
{/* Hero */}
<h2 className="text-3xl font-bold tracking-tight">
Try Casera &mdash; No Account Needed
</h2>
<p className="mt-3 text-muted-foreground">
Manage your home maintenance, track tasks, organize contractors, and
store documents.
<h1 className="font-heading text-4xl font-bold tracking-tight text-[#1C1917]">
See Casera in action
</h1>
<p className="mt-4 text-lg text-[#78716C] leading-relaxed">
Explore the full app with sample data. No account needed
just click and start exploring.
</p>
{/* Actions */}
<div className="mt-8 flex flex-col gap-3">
<Button size="lg" asChild>
<Link href="/demo/app">Start Demo</Link>
</Button>
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/demo/app"
className="group inline-flex items-center gap-2.5 rounded-full bg-[#E07A3A] px-8 py-4 text-base font-semibold text-white shadow-lg shadow-[#E07A3A]/20 hover:bg-[#C4632A] transition-all"
>
<Play className="size-4" />
Launch Demo
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
</Link>
</div>
{/* Login link */}
<p className="mt-6 text-sm text-muted-foreground">
<p className="mt-8 text-sm text-[#A8A29E]">
Already have an account?{" "}
<Link href="/login" className="text-primary hover:underline">
Log In
<Link href="/login" className="text-[#E07A3A] font-medium hover:underline">
Sign In
</Link>
</p>
</div>
+89 -6
View File
@@ -8,8 +8,9 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-sans: var(--font-outfit);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-bricolage);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -48,15 +49,30 @@
--radius-4xl: calc(var(--radius) + 16px);
/* App-specific theme-aware Tailwind utilities */
--color-bg-primary: var(--color-bg-primary);
--color-bg-secondary: var(--color-bg-secondary);
--color-text-primary: var(--color-text-primary);
--color-text-secondary: var(--color-text-secondary);
--color-text-on-primary: var(--color-text-on-primary);
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F7F7F7;
--color-text-primary: #1C1917;
--color-text-secondary: #78716C;
--color-text-on-primary: #FFFFFF;
--color-app-primary: var(--color-primary);
--color-app-secondary: var(--color-secondary);
--color-app-accent: var(--color-accent);
--color-app-error: var(--color-error);
/* Landing page brand colors (theme-independent) */
--color-brand-orange: #E07A3A;
--color-brand-orange-dark: #C4632A;
--color-brand-orange-light: #FFF3EB;
--color-brand-teal: #0D7C66;
--color-brand-teal-light: #ECFDF5;
--color-brand-slate: #1C1917;
--color-brand-warm: #FFFFFF;
/* Animation tokens */
--animate-fade-up: fade-up 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-fade-in: fade-in 0.6s ease both;
--animate-slide-in-right: slide-in-right 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-scale-in: scale-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
}
:root {
@@ -71,3 +87,70 @@
@apply bg-background text-foreground;
}
}
/* Keyframes */
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-in-right {
from {
opacity: 0;
transform: translateX(40px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-12px); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Utility classes for staggered animations */
.stagger-1 { animation-delay: 0ms; }
.stagger-2 { animation-delay: 100ms; }
.stagger-3 { animation-delay: 200ms; }
.stagger-4 { animation-delay: 300ms; }
.stagger-5 { animation-delay: 400ms; }
.stagger-6 { animation-delay: 500ms; }
/* Noise texture overlay for backgrounds */
.noise-overlay::before {
content: "";
position: absolute;
inset: 0;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
pointer-events: none;
}
+11 -4
View File
@@ -1,15 +1,22 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { Geist, Geist_Mono } from "next/font/google";
import { Bricolage_Grotesque, Outfit, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "@/lib/themes/theme-provider";
import { QueryProvider } from "@/lib/query/query-provider";
import { PostHogProvider } from "@/lib/analytics/posthog-provider";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
const bricolage = Bricolage_Grotesque({
variable: "--font-bricolage",
subsets: ["latin"],
display: "swap",
});
const outfit = Outfit({
variable: "--font-outfit",
subsets: ["latin"],
display: "swap",
});
const geistMono = Geist_Mono({
@@ -46,7 +53,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${bricolage.variable} ${outfit.variable} ${geistMono.variable} antialiased`}
>
<Suspense fallback={null}>
<PostHogProvider>
+496 -3
View File
@@ -1,5 +1,498 @@
import { redirect } from 'next/navigation';
import Link from "next/link";
import Image from "next/image";
import {
CheckSquare,
HardHat,
FileText,
ArrowRight,
Shield,
Users,
Bell,
ChevronRight,
Home,
} from "lucide-react";
export default function Home() {
redirect('/app');
const features = [
{
icon: CheckSquare,
title: "Smart Task Tracking",
description:
"Kanban boards, recurring schedules, and due date reminders keep every repair and project on track.",
color: "bg-[#FFF3EB] text-[#E07A3A]",
},
{
icon: HardHat,
title: "Contractor Rolodex",
description:
"Store contact details, specialties, and notes for every service provider. Never lose a plumber's number again.",
color: "bg-[#ECFDF5] text-[#0D7C66]",
},
{
icon: FileText,
title: "Document Vault",
description:
"Warranties, manuals, leases — organized by property and always at your fingertips when you need them.",
color: "bg-[#FEF3C7] text-[#92400E]",
},
];
const steps = [
{
number: "01",
title: "Add your home",
description:
"Create a residence and invite household members to collaborate on maintenance together.",
},
{
number: "02",
title: "Track everything",
description:
"Add tasks, log contractors, upload documents. Build your home's complete history over time.",
},
{
number: "03",
title: "Stay ahead",
description:
"Get reminders for upcoming maintenance. Never miss a filter change, inspection, or warranty deadline.",
},
];
const highlights = [
{
icon: Shield,
title: "Private & Secure",
description: "Your data stays yours. We never share or sell your information.",
},
{
icon: Users,
title: "Household Sharing",
description: "Invite family members with simple invite codes. Everyone stays in sync.",
},
{
icon: Bell,
title: "Smart Reminders",
description: "Recurring tasks and due dates keep you ahead of every maintenance need.",
},
{
icon: Home,
title: "Multi-Property",
description: "Manage one home or many. Each property gets its own organized space.",
},
];
export default function HomePage() {
return (
<div className="min-h-screen bg-white text-[#1C1917] font-sans selection:bg-[#E07A3A]/20">
{/* ─── Navigation ─── */}
<nav className="fixed top-0 w-full z-50 bg-white/80 backdrop-blur-xl border-b border-[#E7E5E4]/60">
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between h-16">
<Link href="/" className="flex items-center gap-2.5">
<Image
src="/logo.png"
alt="Casera"
width={32}
height={32}
className="rounded-lg"
/>
<span className="font-heading text-xl font-bold tracking-tight text-[#1C1917]">
Casera
</span>
</Link>
<div className="hidden md:flex items-center gap-8">
<a
href="#features"
className="text-sm font-medium text-[#78716C] hover:text-[#1C1917] transition-colors"
>
Features
</a>
<a
href="#how-it-works"
className="text-sm font-medium text-[#78716C] hover:text-[#1C1917] transition-colors"
>
How It Works
</a>
<Link
href="/demo"
className="text-sm font-medium text-[#78716C] hover:text-[#1C1917] transition-colors"
>
Demo
</Link>
</div>
<div className="flex items-center gap-3">
<Link
href="/login"
className="text-sm font-medium text-[#78716C] hover:text-[#1C1917] transition-colors hidden sm:inline-flex"
>
Sign In
</Link>
<Link
href="/register"
className="inline-flex items-center gap-1.5 rounded-full bg-[#1C1917] px-5 py-2 text-sm font-semibold text-white hover:bg-[#292524] transition-colors"
>
Get Started
<ArrowRight className="size-3.5" />
</Link>
</div>
</div>
</nav>
{/* ─── Hero ─── */}
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden">
{/* Decorative background */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-20 right-[-10%] w-[600px] h-[600px] rounded-full bg-[#E07A3A]/[0.04] blur-3xl" />
<div className="absolute bottom-0 left-[-5%] w-[400px] h-[400px] rounded-full bg-[#0D7C66]/[0.03] blur-3xl" />
{/* Subtle grid pattern */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage:
"linear-gradient(#1C1917 1px, transparent 1px), linear-gradient(90deg, #1C1917 1px, transparent 1px)",
backgroundSize: "64px 64px",
}}
/>
</div>
<div className="max-w-7xl mx-auto px-6 relative">
<div className="max-w-3xl">
{/* Badge */}
<div className="animate-fade-up stagger-1 inline-flex items-center gap-2 rounded-full border border-[#E7E5E4] bg-white px-4 py-1.5 text-sm text-[#78716C] mb-8 shadow-sm">
<span className="inline-block size-2 rounded-full bg-[#0D7C66] animate-pulse" />
Now available for homeowners
</div>
<h1 className="animate-fade-up stagger-2 font-heading text-5xl sm:text-6xl md:text-7xl font-bold tracking-tight leading-[1.08]">
Your home,{" "}
<span className="text-[#E07A3A]">perfectly maintained.</span>
</h1>
<p className="animate-fade-up stagger-3 mt-6 text-lg md:text-xl text-[#78716C] max-w-xl leading-relaxed">
Track tasks, organize contractors, and store important
documents. Everything you need to keep your home running
smoothly in one place.
</p>
<div className="animate-fade-up stagger-4 mt-10 flex flex-wrap gap-4">
<Link
href="/register"
className="group inline-flex items-center gap-2 rounded-full bg-[#E07A3A] px-7 py-3.5 text-base font-semibold text-white shadow-lg shadow-[#E07A3A]/20 hover:bg-[#C4632A] transition-all hover:shadow-xl hover:shadow-[#E07A3A]/25"
>
Get Started Free
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
</Link>
<Link
href="/demo"
className="group inline-flex items-center gap-2 rounded-full border-2 border-[#E7E5E4] bg-white px-7 py-3.5 text-base font-semibold text-[#1C1917] hover:border-[#D6D3D1] hover:bg-[#F5F5F4] transition-all"
>
Try the Demo
<ChevronRight className="size-4 text-[#A8A29E] transition-transform group-hover:translate-x-0.5" />
</Link>
</div>
</div>
{/* Hero visual — abstract app preview */}
<div className="animate-fade-up stagger-5 hidden lg:block absolute top-12 right-6 w-[420px]">
<div className="relative">
{/* Main card */}
<div className="rounded-2xl bg-white border border-[#E7E5E4] shadow-xl shadow-black/[0.04] p-5">
<div className="flex items-center gap-3 mb-4">
<div className="size-8 rounded-lg bg-[#FFF3EB] flex items-center justify-center">
<CheckSquare className="size-4 text-[#E07A3A]" />
</div>
<div>
<div className="text-sm font-semibold">Upcoming Tasks</div>
<div className="text-xs text-[#A8A29E]">3 due this week</div>
</div>
</div>
<div className="space-y-2.5">
{[
{ task: "Replace HVAC filter", due: "Tomorrow", status: "bg-[#FEF3C7] text-[#92400E]" },
{ task: "Schedule gutter cleaning", due: "Wed", status: "bg-[#ECFDF5] text-[#0D7C66]" },
{ task: "Check smoke detectors", due: "Fri", status: "bg-[#FFF3EB] text-[#E07A3A]" },
].map((item) => (
<div
key={item.task}
className="flex items-center justify-between rounded-lg bg-white px-3.5 py-2.5"
>
<span className="text-sm font-medium">{item.task}</span>
<span
className={`text-xs font-medium px-2 py-0.5 rounded-full ${item.status}`}
>
{item.due}
</span>
</div>
))}
</div>
</div>
{/* Floating stat card */}
<div
className="absolute -bottom-8 -left-8 rounded-xl bg-white border border-[#E7E5E4] shadow-lg shadow-black/[0.04] p-4 w-48"
style={{ animation: "float 6s ease-in-out infinite" }}
>
<div className="text-xs text-[#A8A29E] mb-1">This Month</div>
<div className="text-2xl font-bold font-heading text-[#0D7C66]">12 Done</div>
<div className="mt-2 h-1.5 rounded-full bg-[#F7F7F7] overflow-hidden">
<div className="h-full w-3/4 rounded-full bg-[#0D7C66]" />
</div>
</div>
</div>
</div>
</div>
</section>
{/* ─── Social proof strip ─── */}
<section className="border-y border-[#E7E5E4] bg-white/50">
<div className="max-w-7xl mx-auto px-6 py-8 flex flex-wrap items-center justify-center gap-x-12 gap-y-4 text-center">
{[
{ value: "Free", label: "to get started" },
{ value: "100%", label: "private & secure" },
{ value: "All-in-one", label: "home management" },
].map((stat) => (
<div key={stat.label} className="flex items-baseline gap-2">
<span className="text-lg font-bold font-heading text-[#1C1917]">
{stat.value}
</span>
<span className="text-sm text-[#A8A29E]">{stat.label}</span>
</div>
))}
</div>
</section>
{/* ─── Features ─── */}
<section id="features" className="py-24 md:py-32">
<div className="max-w-7xl mx-auto px-6">
<div className="max-w-2xl mb-16">
<p className="text-sm font-semibold text-[#E07A3A] uppercase tracking-wider mb-3">
Features
</p>
<h2 className="font-heading text-3xl md:text-4xl font-bold tracking-tight">
Everything your home needs, nothing it doesn&apos;t.
</h2>
<p className="mt-4 text-lg text-[#78716C] leading-relaxed">
Casera brings all your home maintenance into one clear,
organized space. No bloat, no learning curve.
</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
{features.map((feature, i) => (
<div
key={feature.title}
className={`group relative rounded-2xl border border-[#E7E5E4] bg-white p-8 transition-all hover:shadow-lg hover:shadow-black/[0.04] hover:-translate-y-0.5 stagger-${i + 1}`}
>
<div
className={`inline-flex items-center justify-center size-12 rounded-xl ${feature.color} mb-5`}
>
<feature.icon className="size-5" />
</div>
<h3 className="font-heading text-xl font-bold mb-2">
{feature.title}
</h3>
<p className="text-[#78716C] leading-relaxed">
{feature.description}
</p>
</div>
))}
</div>
</div>
</section>
{/* ─── How It Works ─── */}
<section
id="how-it-works"
className="py-24 md:py-32 bg-[#F7F7F7] relative noise-overlay"
>
<div className="max-w-7xl mx-auto px-6 relative">
<div className="text-center max-w-2xl mx-auto mb-16">
<p className="text-sm font-semibold text-[#0D7C66] uppercase tracking-wider mb-3">
How It Works
</p>
<h2 className="font-heading text-3xl md:text-4xl font-bold tracking-tight">
Up and running in minutes
</h2>
</div>
<div className="grid md:grid-cols-3 gap-8 md:gap-12">
{steps.map((step) => (
<div key={step.number} className="text-center md:text-left">
<div className="inline-flex items-center justify-center size-14 rounded-2xl bg-[#1C1917] text-white font-heading font-bold text-lg mb-5">
{step.number}
</div>
<h3 className="font-heading text-xl font-bold mb-2">
{step.title}
</h3>
<p className="text-[#78716C] leading-relaxed">
{step.description}
</p>
</div>
))}
</div>
</div>
</section>
{/* ─── Highlights Grid ─── */}
<section className="py-24 md:py-32">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center max-w-2xl mx-auto mb-16">
<h2 className="font-heading text-3xl md:text-4xl font-bold tracking-tight">
Built for real homeowners
</h2>
<p className="mt-4 text-lg text-[#78716C]">
Thoughtful details that make home maintenance feel manageable.
</p>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{highlights.map((item) => (
<div
key={item.title}
className="rounded-2xl border border-[#E7E5E4] bg-white p-6 hover:shadow-md hover:shadow-black/[0.03] transition-all"
>
<item.icon className="size-6 text-[#E07A3A] mb-4" />
<h3 className="font-heading font-bold text-base mb-1.5">
{item.title}
</h3>
<p className="text-sm text-[#78716C] leading-relaxed">
{item.description}
</p>
</div>
))}
</div>
</div>
</section>
{/* ─── CTA Section ─── */}
<section className="py-24 md:py-32 relative overflow-hidden">
<div className="absolute inset-0 bg-[#1C1917]" />
{/* Orange glow */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-[#E07A3A]/10 blur-[120px] pointer-events-none" />
<div className="absolute bottom-0 left-0 w-[400px] h-[400px] rounded-full bg-[#0D7C66]/10 blur-[100px] pointer-events-none" />
<div className="max-w-3xl mx-auto px-6 text-center relative">
<h2 className="font-heading text-3xl md:text-5xl font-bold tracking-tight text-white">
Ready to take control of your home?
</h2>
<p className="mt-5 text-lg text-[#A8A29E] max-w-xl mx-auto leading-relaxed">
Join homeowners who&apos;ve simplified their maintenance routine.
Free to start, no credit card required.
</p>
<div className="mt-10 flex flex-wrap justify-center gap-4">
<Link
href="/register"
className="group inline-flex items-center gap-2 rounded-full bg-[#E07A3A] px-8 py-4 text-base font-semibold text-white shadow-lg shadow-[#E07A3A]/20 hover:bg-[#C4632A] transition-all"
>
Get Started Free
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
</Link>
<Link
href="/demo"
className="inline-flex items-center gap-2 rounded-full border-2 border-white/15 px-8 py-4 text-base font-semibold text-white hover:border-white/30 hover:bg-white/5 transition-all"
>
Try the Demo
</Link>
</div>
</div>
</section>
{/* ─── Footer ─── */}
<footer className="bg-[#1C1917] border-t border-white/5 text-[#A8A29E]">
<div className="max-w-7xl mx-auto px-6 py-16">
<div className="grid md:grid-cols-4 gap-10">
{/* Brand */}
<div className="md:col-span-1">
<Link href="/" className="flex items-center gap-2.5 mb-4">
<Image
src="/logo.png"
alt="Casera"
width={28}
height={28}
className="rounded-md"
/>
<span className="font-heading text-lg font-bold text-white">
Casera
</span>
</Link>
<p className="text-sm leading-relaxed">
Home maintenance made simple. Track tasks, organize
contractors, store documents.
</p>
</div>
{/* Product */}
<div>
<h4 className="text-sm font-semibold text-white mb-4">
Product
</h4>
<ul className="space-y-2.5 text-sm">
<li>
<a href="#features" className="hover:text-white transition-colors">
Features
</a>
</li>
<li>
<Link href="/demo" className="hover:text-white transition-colors">
Demo
</Link>
</li>
<li>
<a href="#how-it-works" className="hover:text-white transition-colors">
How It Works
</a>
</li>
</ul>
</div>
{/* Account */}
<div>
<h4 className="text-sm font-semibold text-white mb-4">
Account
</h4>
<ul className="space-y-2.5 text-sm">
<li>
<Link href="/login" className="hover:text-white transition-colors">
Sign In
</Link>
</li>
<li>
<Link href="/register" className="hover:text-white transition-colors">
Create Account
</Link>
</li>
<li>
<Link href="/forgot-password" className="hover:text-white transition-colors">
Reset Password
</Link>
</li>
</ul>
</div>
{/* Legal */}
<div>
<h4 className="text-sm font-semibold text-white mb-4">Legal</h4>
<ul className="space-y-2.5 text-sm">
<li>
<span className="cursor-default">Privacy Policy</span>
</li>
<li>
<span className="cursor-default">Terms of Service</span>
</li>
</ul>
</div>
</div>
<div className="mt-16 pt-8 border-t border-white/5 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs">
&copy; {new Date().getFullYear()} Casera. All rights reserved.
</p>
<p className="text-xs">
Made for homeowners, by homeowners.
</p>
</div>
</div>
</footer>
</div>
);
}
+23 -21
View File
@@ -2,7 +2,6 @@
import Link from "next/link";
import { Phone, Mail, Star } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardAction } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useDataProvider } from "@/lib/demo/data-provider-context";
@@ -16,19 +15,23 @@ interface ContractorCardProps {
export function ContractorCard({ contractor, onToggleFavorite }: ContractorCardProps) {
const { basePath } = useDataProvider();
return (
<Card className="transition-shadow hover:shadow-md">
<CardHeader>
<Link href={`${basePath}/contractors/${contractor.id}`} className="hover:underline">
<CardTitle>{contractor.name}</CardTitle>
<div className="group rounded-2xl border border-border bg-card p-5 transition-all duration-200 hover:shadow-md hover:shadow-black/[0.04] hover:-translate-y-0.5">
<div className="flex items-start justify-between mb-3">
<div className="min-w-0 flex-1">
<Link
href={`${basePath}/contractors/${contractor.id}`}
className="font-heading font-bold text-base leading-tight hover:text-primary transition-colors line-clamp-1"
>
{contractor.name}
</Link>
{contractor.company && (
<CardDescription>{contractor.company}</CardDescription>
<p className="text-sm text-muted-foreground mt-0.5 truncate">{contractor.company}</p>
)}
<CardAction>
</div>
<Button
variant="ghost"
size="icon"
className="size-8"
className="size-8 shrink-0 -mr-1 -mt-1"
aria-label={contractor.is_favorite ? "Remove from favorites" : "Add to favorites"}
onClick={(e) => {
e.preventDefault();
@@ -39,41 +42,40 @@ export function ContractorCard({ contractor, onToggleFavorite }: ContractorCardP
aria-hidden="true"
className={
contractor.is_favorite
? "size-4 fill-yellow-400 text-yellow-400"
: "size-4 text-muted-foreground"
? "size-4 fill-amber-400 text-amber-400"
: "size-4 text-muted-foreground hover:text-amber-400 transition-colors"
}
/>
</Button>
</CardAction>
</CardHeader>
<CardContent>
</div>
{contractor.specialties.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
<div className="flex flex-wrap gap-1.5 mb-4">
{contractor.specialties.map((s) => (
<Badge key={s.id} variant="secondary">
<Badge key={s.id} variant="secondary" className="rounded-lg text-xs">
{s.icon && <span className="mr-1">{s.icon}</span>}
{s.name}
</Badge>
))}
</div>
)}
<div className="flex items-center gap-2">
{contractor.phone && (
<Button variant="outline" size="icon" className="size-8" asChild>
<Button variant="outline" size="icon" className="size-8 rounded-lg" asChild>
<a href={`tel:${contractor.phone}`} aria-label={`Call ${contractor.name}`} onClick={(e) => e.stopPropagation()}>
<Phone className="size-4" aria-hidden="true" />
<Phone className="size-3.5" aria-hidden="true" />
</a>
</Button>
)}
{contractor.email && (
<Button variant="outline" size="icon" className="size-8" asChild>
<Button variant="outline" size="icon" className="size-8 rounded-lg" asChild>
<a href={`mailto:${contractor.email}`} aria-label={`Email ${contractor.name}`} onClick={(e) => e.stopPropagation()}>
<Mail className="size-4" aria-hidden="true" />
<Mail className="size-3.5" aria-hidden="true" />
</a>
</Button>
)}
</div>
</CardContent>
</Card>
</div>
);
}
+33 -26
View File
@@ -1,9 +1,8 @@
"use client";
import Link from "next/link";
import { Bell } from "lucide-react";
import { Bell, Coffee } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { useNotifications } from "@/lib/hooks/use-notifications";
import { useDataProvider } from "@/lib/demo/data-provider-context";
@@ -14,52 +13,60 @@ export function RecentActivity() {
const notifications = data?.results ?? [];
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Recent Activity</span>
<div className="rounded-2xl border border-border bg-card overflow-hidden h-full">
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<h3 className="font-heading font-semibold text-base">What&apos;s Been Happening</h3>
<Link
href={`${basePath}/settings/notifications`}
className="text-sm font-normal text-primary hover:underline"
className="text-xs font-medium text-primary hover:text-primary/80 transition-colors"
>
View all
</Link>
</CardTitle>
</CardHeader>
<CardContent>
</div>
<div className="p-4">
{isLoading ? (
<div className="space-y-4">
<div className="space-y-4 p-2">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex gap-3 animate-pulse">
<div className="size-8 rounded-full bg-muted" />
<div className="flex-1 space-y-2">
<div className="h-4 w-1/2 rounded bg-muted" />
<div className="h-3 w-3/4 rounded bg-muted" />
<div className="size-9 rounded-xl bg-muted shrink-0" />
<div className="flex-1 space-y-2 py-0.5">
<div className="h-3.5 w-3/5 rounded bg-muted" />
<div className="h-3 w-4/5 rounded bg-muted" />
</div>
</div>
))}
</div>
) : notifications.length === 0 ? (
<div className="flex items-center justify-center py-8 text-muted-foreground">
No recent activity
<div className="flex flex-col items-center justify-center py-10 text-center">
<div className="size-11 rounded-xl bg-[#FFF3EB] flex items-center justify-center mb-3">
<Coffee className="size-5 text-[#E07A3A]" />
</div>
<p className="text-sm font-medium text-foreground">Nothing yet all quiet!</p>
<p className="text-xs text-muted-foreground mt-1">
Activity will show up here as you manage your home.
</p>
</div>
) : (
<div className="space-y-4">
<div className="space-y-1">
{notifications.map((notification) => (
<div key={notification.id} className="flex gap-3 items-start">
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
<div
key={notification.id}
className="flex gap-3 items-start rounded-xl p-2.5 hover:bg-accent/50 transition-colors"
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-xl bg-primary/10">
<Bell className="size-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium leading-tight">
<div className="flex-1 min-w-0 py-0.5">
<p className="text-sm font-medium leading-snug">
{notification.title}
</p>
{notification.body && (
<p className="text-xs text-muted-foreground mt-0.5 truncate">
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">
{notification.body}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
<p className="text-[11px] text-muted-foreground/70 mt-1">
{formatDistanceToNow(new Date(notification.created_at), {
addSuffix: true,
})}
@@ -69,7 +76,7 @@ export function RecentActivity() {
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
+18 -15
View File
@@ -2,7 +2,6 @@
import Link from "next/link";
import { AlertTriangle, Clock, ClipboardList, CheckCircle2 } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { useDataProvider } from "@/lib/demo/data-provider-context";
interface StatsCardsProps {
@@ -17,28 +16,32 @@ const stats = [
key: "overdue",
label: "Overdue",
icon: AlertTriangle,
color: "text-red-500",
iconColor: "text-red-500",
bgColor: "bg-red-50 dark:bg-red-500/10",
prop: "overdue" as const,
},
{
key: "dueSoon",
label: "Due Soon",
icon: Clock,
color: "text-orange-500",
iconColor: "text-amber-500",
bgColor: "bg-amber-50 dark:bg-amber-500/10",
prop: "dueSoon" as const,
},
{
key: "active",
label: "Active",
icon: ClipboardList,
color: "text-blue-500",
iconColor: "text-[#0D7C66]",
bgColor: "bg-emerald-50 dark:bg-emerald-500/10",
prop: "active" as const,
},
{
key: "completed",
label: "Completed",
icon: CheckCircle2,
color: "text-green-500",
iconColor: "text-[#E07A3A]",
bgColor: "bg-orange-50 dark:bg-orange-500/10",
prop: "completed" as const,
},
] as const;
@@ -51,17 +54,17 @@ export function StatsCards({ overdue, dueSoon, active, completed }: StatsCardsPr
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat) => (
<Link key={stat.key} href={`${basePath}/tasks`}>
<Card className="hover:shadow-md transition-shadow cursor-pointer">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
<stat.icon className={`size-4 ${stat.color}`} />
<div className="group rounded-2xl border border-border bg-card p-5 hover:shadow-md hover:shadow-black/[0.03] transition-all hover:-translate-y-0.5 cursor-pointer">
<div className="flex items-center gap-3 mb-3">
<div className={`inline-flex items-center justify-center size-9 rounded-xl ${stat.bgColor}`}>
<stat.icon className={`size-4 ${stat.iconColor}`} />
</div>
<span className="text-sm font-medium text-muted-foreground">
{stat.label}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{values[stat.prop]}</p>
</CardContent>
</Card>
</span>
</div>
<p className="text-3xl font-bold font-heading tracking-tight">{values[stat.prop]}</p>
</div>
</Link>
))}
</div>
@@ -9,7 +9,7 @@ import {
Tooltip,
CartesianGrid,
} from "recharts";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { TrendingUp } from "lucide-react";
interface TaskCompletionChartProps {
data: { date: string; count: number }[];
@@ -19,11 +19,12 @@ export function TaskCompletionChart({ data }: TaskCompletionChartProps) {
const hasData = data && data.length > 0;
return (
<Card>
<CardHeader>
<CardTitle>Task Completions</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-2xl border border-border bg-card overflow-hidden h-full">
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<h3 className="font-heading font-semibold text-base">Your Progress</h3>
</div>
<div className="p-6">
{hasData ? (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data}>
@@ -42,19 +43,25 @@ export function TaskCompletionChart({ data }: TaskCompletionChartProps) {
<Area
type="monotone"
dataKey="count"
stroke="hsl(var(--primary))"
fill="hsl(var(--primary))"
fillOpacity={0.2}
stroke="#E07A3A"
fill="#E07A3A"
fillOpacity={0.1}
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
No completion data yet
<div className="flex flex-col items-center justify-center h-[300px] text-center">
<div className="size-12 rounded-xl bg-[#ECFDF5] flex items-center justify-center mb-3">
<TrendingUp className="size-5 text-[#0D7C66]" />
</div>
<p className="text-sm font-medium text-foreground">Your progress chart lives here</p>
<p className="text-xs text-muted-foreground mt-1 max-w-[260px] leading-relaxed">
As you complete tasks, you&apos;ll see a visual history of everything you&apos;ve accomplished.
</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
+4 -4
View File
@@ -11,17 +11,17 @@ export function DemoBanner() {
if (dismissed) return null;
return (
<div className="sticky top-0 z-50 flex items-center justify-center gap-3 border-b bg-muted/60 px-4 py-2 text-sm text-muted-foreground backdrop-blur-sm">
<p>
<div className="sticky top-0 z-50 flex items-center justify-center gap-3 border-b border-[#E07A3A]/20 bg-[#FFF3EB]/80 px-4 py-2.5 text-sm text-[#92400E] backdrop-blur-xl">
<p className="font-medium">
You&apos;re exploring Casera in demo mode. Changes aren&apos;t saved.
</p>
<Button size="xs" asChild>
<Button size="xs" className="rounded-full" asChild>
<Link href="/register">Sign Up Free</Link>
</Button>
<Button
variant="ghost"
size="icon-xs"
className="absolute right-2"
className="absolute right-2 text-[#92400E] hover:bg-[#E07A3A]/10"
onClick={() => setDismissed(true)}
aria-label="Dismiss banner"
>
+23 -15
View File
@@ -4,7 +4,6 @@ import Link from "next/link";
import { FileText, FileImage, File, FileSpreadsheet } from "lucide-react";
import { format } from "date-fns";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { WarrantyStatus } from "@/components/documents/warranty-status";
import { useDataProvider } from "@/lib/demo/data-provider-context";
@@ -30,40 +29,49 @@ const typeLabels: Record<string, string> = {
manual: "Manual",
};
const typeColors: Record<string, string> = {
general: "bg-muted text-muted-foreground",
warranty: "bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
receipt: "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400",
contract: "bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400",
insurance: "bg-purple-50 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400",
manual: "bg-orange-50 text-orange-700 dark:bg-orange-500/10 dark:text-orange-400",
};
export function DocumentCard({ document: doc }: DocumentCardProps) {
const Icon = getFileIcon(doc.mime_type);
const { basePath } = useDataProvider();
return (
<Link href={`${basePath}/documents/${doc.id}`} className="block">
<Card className="transition-colors hover:border-primary/40">
<CardHeader>
<div className="flex items-start gap-3">
<div className="rounded-md bg-muted p-2 shrink-0" aria-hidden="true">
<Icon className="size-5 text-muted-foreground" />
<Link href={`${basePath}/documents/${doc.id}`} className="block group">
<div className="rounded-2xl border border-border bg-card p-5 transition-all duration-200 hover:shadow-md hover:shadow-black/[0.04] hover:-translate-y-0.5 hover:border-primary/30">
<div className="flex items-start gap-3 mb-4">
<div className={`rounded-xl p-2.5 shrink-0 ${typeColors[doc.document_type] ?? typeColors.general}`} aria-hidden="true">
<Icon className="size-5" />
</div>
<div className="min-w-0 flex-1">
<CardTitle className="text-base truncate">{doc.title}</CardTitle>
<h3 className="font-heading font-bold text-base leading-tight truncate group-hover:text-primary transition-colors">
{doc.title}
</h3>
<p className="text-sm text-muted-foreground truncate mt-0.5">
{doc.residence_name}
</p>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">
<Badge variant="outline" className="rounded-lg">
{typeLabels[doc.document_type] ?? doc.document_type}
</Badge>
{doc.document_type === "warranty" && (
<WarrantyStatus expiry_date={doc.expiry_date} />
)}
</div>
<p className="text-xs text-muted-foreground mt-3">
Created {format(new Date(doc.created_at), "MMM d, yyyy")}
<p className="text-xs text-muted-foreground/70 mt-3">
{format(new Date(doc.created_at), "MMM d, yyyy")}
</p>
</CardContent>
</Card>
</div>
</Link>
);
}
+32 -17
View File
@@ -1,12 +1,7 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import Image from "next/image";
import Link from "next/link";
interface AuthFormWrapperProps {
title: string;
@@ -22,19 +17,39 @@ export function AuthFormWrapper({
footer,
}: AuthFormWrapperProps) {
return (
<div className="flex flex-col items-center gap-6">
<div className="text-center">
<h1 className="text-2xl font-bold tracking-tight">Casera</h1>
<div className="flex flex-col gap-8">
{/* Mobile logo — hidden on desktop since it's in the side panel */}
<div className="lg:hidden flex items-center gap-2.5">
<Link href="/" className="flex items-center gap-2.5">
<Image
src="/logo.png"
alt="Casera"
width={32}
height={32}
className="rounded-lg"
/>
<span className="font-heading text-xl font-bold text-[#1C1917]">
Casera
</span>
</Link>
</div>
<Card className="w-full">
<CardHeader>
<CardTitle className="text-xl">{title}</CardTitle>
{subtitle && <CardDescription>{subtitle}</CardDescription>}
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
{/* Header */}
<div>
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground">
{title}
</h1>
{subtitle && (
<p className="mt-1.5 text-sm text-muted-foreground">{subtitle}</p>
)}
</div>
{/* Form content */}
<div className="rounded-2xl border border-border bg-card p-6 shadow-sm">
{children}
</div>
{/* Footer */}
{footer && (
<div className="text-center text-sm text-muted-foreground">
{footer}
+14 -13
View File
@@ -9,18 +9,19 @@ import { useDataProvider } from '@/lib/demo/data-provider-context';
export function MobileNav() {
const pathname = usePathname();
const { basePath } = useDataProvider();
const navItems = getNavItems(basePath);
// Show the first 5 nav items on mobile (exclude Settings)
const mobileNavItems = navItems.filter((item) => item.label !== 'Settings');
const navItems = getNavItems(basePath).filter((item) => item.label !== 'Settings');
return (
<nav role="navigation" aria-label="Main navigation" className="md:hidden fixed bottom-0 left-0 right-0 z-30 bg-card border-t border-border">
<div className="flex items-center justify-around px-2 py-2">
{mobileNavItems.map((item) => {
<nav
role="navigation"
aria-label="Main navigation"
className="md:hidden fixed bottom-0 left-0 right-0 z-30 bg-background/80 backdrop-blur-xl border-t border-border/60"
>
<div className="flex items-center justify-around px-1 py-1.5 pb-[calc(0.375rem+env(safe-area-inset-bottom))]">
{navItems.map((item) => {
const isActive =
item.href === basePath
? pathname === basePath
? pathname === basePath || pathname === basePath + '/'
: pathname.startsWith(item.href);
return (
@@ -30,14 +31,14 @@ export function MobileNav() {
aria-label={item.label}
aria-current={isActive ? 'page' : undefined}
className={cn(
'flex flex-col items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors',
'flex flex-col items-center gap-0.5 px-3 py-1.5 rounded-lg text-[10px] transition-colors',
isActive
? 'text-primary'
: 'text-muted-foreground hover:text-foreground'
? 'text-foreground'
: 'text-muted-foreground'
)}
>
<item.icon className="size-5" aria-hidden="true" />
<span>{item.label}</span>
<item.icon className={cn("size-[22px]", isActive && "text-primary")} aria-hidden="true" />
<span className="font-medium">{item.label}</span>
</Link>
);
})}
+4 -4
View File
@@ -1,4 +1,4 @@
import { Home, Building2, CheckSquare, HardHat, FileText, Settings } from 'lucide-react';
import { Home, CheckSquare, Building2, HardHat, FileText, Settings } from 'lucide-react';
export interface NavItem {
label: string;
@@ -9,10 +9,10 @@ export interface NavItem {
export function getNavItems(basePath: string): NavItem[] {
return [
{ label: 'Home', href: basePath, icon: Home },
{ label: 'Residences', href: `${basePath}/residences`, icon: Building2 },
{ label: 'Tasks', href: `${basePath}/tasks`, icon: CheckSquare },
{ label: 'Contractors', href: `${basePath}/contractors`, icon: HardHat },
{ label: 'Documents', href: `${basePath}/documents`, icon: FileText },
{ label: 'Homes', href: `${basePath}/residences`, icon: Building2 },
{ label: 'Pros', href: `${basePath}/contractors`, icon: HardHat },
{ label: 'Docs', href: `${basePath}/documents`, icon: FileText },
{ label: 'Settings', href: `${basePath}/settings`, icon: Settings },
];
}
+59 -14
View File
@@ -1,9 +1,9 @@
"use client";
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import { Separator } from '@/components/ui/separator';
import { getNavItems } from './nav-items';
import { useDataProvider } from '@/lib/demo/data-provider-context';
@@ -12,23 +12,32 @@ export function Sidebar() {
const { basePath } = useDataProvider();
const navItems = getNavItems(basePath);
const mainNavItems = navItems.filter((item) => item.label !== 'Settings');
const settingsItem = navItems.find((item) => item.label === 'Settings');
return (
<aside className="hidden md:flex md:flex-col md:fixed md:inset-y-0 md:left-0 md:z-30 w-16 lg:w-64 bg-card border-r border-border">
{/* Logo */}
<div className="flex items-center h-16 px-4 lg:px-6">
<Link href={basePath} className="flex items-center gap-2">
<span className="text-xl font-bold text-primary">C</span>
<span className="hidden lg:inline text-xl font-bold text-foreground">
<div className="flex items-center h-16 px-3 lg:px-5">
<Link href={basePath} className="flex items-center gap-2.5 group">
<Image
src="/logo.png"
alt="Casera"
width={32}
height={32}
className="rounded-lg transition-transform group-hover:scale-105"
/>
<span className="hidden lg:inline font-heading text-lg font-bold text-foreground tracking-tight">
Casera
</span>
</Link>
</div>
<Separator />
<div className="mx-3 lg:mx-4 h-px bg-border" />
{/* Navigation */}
<nav role="navigation" aria-label="Main navigation" className="flex-1 flex flex-col gap-1 p-2 lg:p-3">
{navItems.map((item) => {
{/* Main navigation */}
<nav role="navigation" aria-label="Main navigation" className="flex-1 flex flex-col gap-0.5 p-2 lg:p-3 mt-2">
{mainNavItems.map((item) => {
const isActive =
item.href === basePath
? pathname === basePath
@@ -41,19 +50,55 @@ export function Sidebar() {
aria-label={item.label}
aria-current={isActive ? 'page' : undefined}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
'hover:bg-accent hover:text-accent-foreground',
'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200',
isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'
? 'bg-primary/10 text-primary shadow-sm'
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
)}
>
<item.icon className="size-5 shrink-0" aria-hidden="true" />
<item.icon
className={cn(
'size-[18px] shrink-0 transition-colors',
isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'
)}
aria-hidden="true"
/>
<span className="hidden lg:inline">{item.label}</span>
</Link>
);
})}
</nav>
{/* Settings at bottom */}
{settingsItem && (
<div className="p-2 lg:p-3 border-t border-border">
{(() => {
const isActive = pathname.startsWith(settingsItem.href);
return (
<Link
href={settingsItem.href}
aria-label={settingsItem.label}
aria-current={isActive ? 'page' : undefined}
className={cn(
'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200',
isActive
? 'bg-primary/10 text-primary shadow-sm'
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
)}
>
<settingsItem.icon
className={cn(
'size-[18px] shrink-0 transition-colors',
isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'
)}
aria-hidden="true"
/>
<span className="hidden lg:inline">{settingsItem.label}</span>
</Link>
);
})()}
</div>
)}
</aside>
);
}
+69 -15
View File
@@ -1,7 +1,10 @@
"use client";
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { LogOut, Settings, User } from 'lucide-react';
import { cn } from '@/lib/utils';
import { NotificationBell } from '@/components/notifications/notification-bell';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import {
@@ -12,10 +15,19 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useDataProvider } from '@/lib/demo/data-provider-context';
import { useAuthStore } from '@/stores/auth';
import { getNavItems } from './nav-items';
export function TopBar() {
const router = useRouter();
const pathname = usePathname();
const { basePath } = useDataProvider();
const user = useAuthStore((s) => s.user);
const navItems = getNavItems(basePath).filter((item) => item.label !== 'Settings');
const initials = user
? `${user.first_name?.[0] ?? ''}${user.last_name?.[0] ?? ''}`.toUpperCase() || 'U'
: 'U';
const handleLogout = async () => {
try {
@@ -31,28 +43,69 @@ export function TopBar() {
};
return (
<header className="sticky top-0 z-20 flex items-center justify-between h-16 px-4 lg:px-6 bg-card border-b border-border">
{/* Mobile logo - hidden on desktop since sidebar has it */}
<div className="md:hidden">
<span className="text-xl font-bold text-foreground">Casera</span>
</div>
<header className="sticky top-0 z-30 bg-background/80 backdrop-blur-xl border-b border-border/60">
<div className="max-w-7xl mx-auto flex items-center justify-between h-16 px-6">
{/* Logo */}
<Link href={basePath} className="flex items-center gap-2.5 shrink-0 group">
<Image
src="/logo.png"
alt="Casera"
width={32}
height={32}
className="rounded-lg transition-transform group-hover:scale-105"
/>
<span className="font-heading text-xl font-bold text-foreground tracking-tight">
Casera
</span>
</Link>
{/* Spacer for desktop (logo is in sidebar) */}
<div className="hidden md:block" />
{/* Desktop nav links */}
<nav className="hidden md:flex items-center gap-1" role="navigation" aria-label="Main navigation">
{navItems.map((item) => {
const isActive =
item.href === basePath
? pathname === basePath || pathname === basePath + '/'
: pathname.startsWith(item.href);
{/* Notifications + Profile */}
<div className="flex items-center gap-2">
return (
<Link
key={item.href}
href={item.href}
aria-current={isActive ? 'page' : undefined}
className={cn(
'relative px-3.5 py-1.5 text-sm font-medium rounded-lg transition-colors',
isActive
? 'text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
{item.label}
{isActive && (
<span className="absolute bottom-0 left-3.5 right-3.5 h-[2px] bg-foreground rounded-full" />
)}
</Link>
);
})}
</nav>
{/* Right section */}
<div className="flex items-center gap-1.5">
<NotificationBell />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button aria-label="User menu" className="flex items-center gap-2 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
<Avatar>
<AvatarFallback>U</AvatarFallback>
<button
aria-label="User menu"
className="flex items-center gap-2 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<Avatar className="size-8 border border-border/60">
<AvatarFallback className="bg-primary/10 text-primary text-xs font-semibold">
{initials}
</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => router.push(`${basePath}/settings`)}>
<DropdownMenuItem onClick={() => router.push(`${basePath}/settings/profile`)}>
<User className="size-4" />
Profile
</DropdownMenuItem>
@@ -63,11 +116,12 @@ export function TopBar() {
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} variant="destructive">
<LogOut className="size-4" />
Logout
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</header>
);
}
+12 -13
View File
@@ -3,7 +3,6 @@
import Link from "next/link";
import { MapPin } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useDataProvider } from "@/lib/demo/data-provider-context";
import type { MyResidenceResponse } from "@/lib/api/residences";
@@ -21,36 +20,36 @@ export function ResidenceCard({ data }: ResidenceCardProps) {
.join(", ");
return (
<Link href={`${basePath}/residences/${residence.id}`} className="block">
<Card className="transition-colors hover:border-primary/40">
<CardHeader>
<CardTitle className="text-base">{residence.name}</CardTitle>
<Link href={`${basePath}/residences/${residence.id}`} className="block group">
<div className="rounded-2xl border border-border bg-card p-5 transition-all duration-200 hover:shadow-md hover:shadow-black/[0.04] hover:-translate-y-0.5 hover:border-primary/30">
<div className="mb-3">
<h3 className="font-heading font-bold text-base leading-tight group-hover:text-primary transition-colors">
{residence.name}
</h3>
{address && (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<div className="flex items-center gap-1.5 text-sm text-muted-foreground mt-1.5">
<MapPin className="size-3.5 shrink-0" aria-hidden="true" />
<span className="sr-only">Address:</span>
<span className="truncate">{address}</span>
</div>
)}
</CardHeader>
<CardContent>
</div>
<div className="flex flex-wrap gap-2">
{task_summary.overdue > 0 && (
<Badge variant="destructive">
<Badge variant="destructive" className="rounded-lg">
{task_summary.overdue} overdue
</Badge>
)}
{task_summary.due_soon > 0 && (
<Badge variant="secondary">
<Badge variant="secondary" className="rounded-lg">
{task_summary.due_soon} due soon
</Badge>
)}
<Badge variant="outline">
<Badge variant="outline" className="rounded-lg">
{task_summary.total} {task_summary.total === 1 ? "task" : "tasks"}
</Badge>
</div>
</CardContent>
</Card>
</div>
</Link>
);
}
+9 -5
View File
@@ -11,12 +11,16 @@ interface EmptyStateProps {
export function EmptyState({ icon: Icon, title, description, actionLabel, onAction }: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-full bg-muted p-4 mb-4"><Icon className="size-8 text-muted-foreground" /></div>
<h3 className="text-lg font-semibold">{title}</h3>
<p className="text-muted-foreground mt-1 max-w-sm">{description}</p>
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="size-16 rounded-2xl bg-[#FFF3EB] flex items-center justify-center mb-5">
<Icon className="size-7 text-[#E07A3A]" />
</div>
<h3 className="font-heading text-lg font-bold">{title}</h3>
<p className="text-sm text-muted-foreground mt-2 max-w-sm leading-relaxed">{description}</p>
{actionLabel && onAction && (
<Button onClick={onAction} className="mt-4"><Plus className="size-4 mr-2" />{actionLabel}</Button>
<Button onClick={onAction} className="mt-6 rounded-xl shadow-sm shadow-primary/20">
<Plus className="size-4 mr-2" />{actionLabel}
</Button>
)}
</div>
);
+5 -3
View File
@@ -9,10 +9,12 @@ interface ErrorBannerProps {
export function ErrorBanner({ message = "Something went wrong. Please try again.", onRetry }: ErrorBannerProps) {
return (
<div role="alert" aria-live="assertive" className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 flex items-center gap-3">
<AlertTriangle className="size-5 text-destructive shrink-0" aria-hidden="true" />
<div role="alert" aria-live="assertive" className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 flex items-center gap-3">
<div className="size-9 rounded-xl bg-destructive/10 flex items-center justify-center shrink-0">
<AlertTriangle className="size-4 text-destructive" aria-hidden="true" />
</div>
<p className="text-sm text-destructive flex-1">{message}</p>
{onRetry && <Button variant="outline" size="sm" onClick={onRetry}>Retry</Button>}
{onRetry && <Button variant="outline" size="sm" onClick={onRetry} className="rounded-lg shrink-0">Retry</Button>}
</div>
);
}
+11 -11
View File
@@ -10,9 +10,9 @@ export function LoadingSkeleton({ variant, count = 4 }: LoadingSkeletonProps) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="rounded-lg border p-4 space-y-3">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<div key={i} className="rounded-2xl border bg-card p-5 space-y-3">
<Skeleton className="h-5 w-3/4 rounded-lg" />
<Skeleton className="h-4 w-1/2 rounded-lg" />
<div className="flex gap-2 pt-2"><Skeleton className="h-6 w-16 rounded-full" /><Skeleton className="h-6 w-16 rounded-full" /></div>
</div>
))}
@@ -23,10 +23,10 @@ export function LoadingSkeleton({ variant, count = 4 }: LoadingSkeletonProps) {
return (
<div className="space-y-3">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="flex items-center gap-4 rounded-lg border p-4">
<Skeleton className="size-10 rounded-full" />
<div className="flex-1 space-y-2"><Skeleton className="h-4 w-1/3" /><Skeleton className="h-3 w-1/2" /></div>
<Skeleton className="h-8 w-20" />
<div key={i} className="flex items-center gap-4 rounded-2xl border bg-card p-4">
<Skeleton className="size-10 rounded-xl" />
<div className="flex-1 space-y-2"><Skeleton className="h-4 w-1/3 rounded-lg" /><Skeleton className="h-3 w-1/2 rounded-lg" /></div>
<Skeleton className="h-8 w-20 rounded-lg" />
</div>
))}
</div>
@@ -35,10 +35,10 @@ export function LoadingSkeleton({ variant, count = 4 }: LoadingSkeletonProps) {
if (variant === "detail") {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-1/3" />
<Skeleton className="h-8 w-1/3 rounded-lg" />
<div className="grid grid-cols-2 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2"><Skeleton className="h-3 w-20" /><Skeleton className="h-5 w-full" /></div>
<div key={i} className="space-y-2"><Skeleton className="h-3 w-20 rounded-lg" /><Skeleton className="h-5 w-full rounded-lg" /></div>
))}
</div>
</div>
@@ -47,8 +47,8 @@ export function LoadingSkeleton({ variant, count = 4 }: LoadingSkeletonProps) {
return (
<div className="flex gap-4 overflow-x-auto pb-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="min-w-[280px] rounded-lg border p-4 space-y-3">
<Skeleton className="h-5 w-24" /><Skeleton className="h-24 w-full rounded-md" /><Skeleton className="h-24 w-full rounded-md" />
<div key={i} className="min-w-[280px] rounded-2xl border bg-card p-4 space-y-3">
<Skeleton className="h-5 w-24 rounded-lg" /><Skeleton className="h-24 w-full rounded-xl" /><Skeleton className="h-24 w-full rounded-xl" />
</div>
))}
</div>
+5 -3
View File
@@ -14,13 +14,15 @@ export function PageHeader({ title, description, actionLabel, onAction, children
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
{description && <p className="text-muted-foreground mt-1">{description}</p>}
<h1 className="font-heading text-2xl font-bold tracking-tight">{title}</h1>
{description && <p className="text-sm text-muted-foreground mt-1">{description}</p>}
</div>
<div className="flex items-center gap-2">
{children}
{actionLabel && onAction && (
<Button onClick={onAction}><Plus className="size-4 mr-2" />{actionLabel}</Button>
<Button onClick={onAction} className="rounded-xl shadow-sm">
<Plus className="size-4 mr-2" />{actionLabel}
</Button>
)}
</div>
</div>
+22 -22
View File
@@ -8,30 +8,30 @@ import { TaskCard } from "./task-card";
import type { KanbanColumn as KanbanColumnType } from "@/lib/api/tasks";
const COLUMN_COLORS: Record<string, string> = {
overdue_tasks: "border-red-500/50 bg-red-50/50 dark:bg-red-950/20",
due_soon_tasks: "border-yellow-500/50 bg-yellow-50/50 dark:bg-yellow-950/20",
upcoming_tasks: "border-blue-500/50 bg-blue-50/50 dark:bg-blue-950/20",
in_progress_tasks: "border-green-500/50 bg-green-50/50 dark:bg-green-950/20",
completed_tasks: "border-gray-500/50 bg-gray-50/50 dark:bg-gray-950/20",
cancelled_tasks: "border-slate-500/50 bg-slate-50/50 dark:bg-slate-950/20",
overdue_tasks: "border-red-200 bg-red-50/40 dark:border-red-500/20 dark:bg-red-950/20",
due_soon_tasks: "border-amber-200 bg-amber-50/40 dark:border-amber-500/20 dark:bg-amber-950/20",
upcoming_tasks: "border-blue-200 bg-blue-50/40 dark:border-blue-500/20 dark:bg-blue-950/20",
in_progress_tasks: "border-emerald-200 bg-emerald-50/40 dark:border-emerald-500/20 dark:bg-emerald-950/20",
completed_tasks: "border-stone-200 bg-stone-50/40 dark:border-stone-500/20 dark:bg-stone-950/20",
cancelled_tasks: "border-slate-200 bg-slate-50/40 dark:border-slate-500/20 dark:bg-slate-950/20",
};
const COLUMN_HEADER_COLORS: Record<string, string> = {
overdue_tasks: "text-red-700 dark:text-red-400",
due_soon_tasks: "text-yellow-700 dark:text-yellow-400",
due_soon_tasks: "text-amber-700 dark:text-amber-400",
upcoming_tasks: "text-blue-700 dark:text-blue-400",
in_progress_tasks: "text-green-700 dark:text-green-400",
completed_tasks: "text-gray-700 dark:text-gray-400",
cancelled_tasks: "text-slate-700 dark:text-slate-400",
in_progress_tasks: "text-emerald-700 dark:text-emerald-400",
completed_tasks: "text-stone-600 dark:text-stone-400",
cancelled_tasks: "text-slate-600 dark:text-slate-400",
};
const COUNT_BADGE_COLORS: Record<string, string> = {
overdue_tasks: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
due_soon_tasks: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
upcoming_tasks: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
in_progress_tasks: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
completed_tasks: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
cancelled_tasks: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-200",
overdue_tasks: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
due_soon_tasks: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
upcoming_tasks: "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300",
in_progress_tasks: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300",
completed_tasks: "bg-stone-100 text-stone-700 dark:bg-stone-900/50 dark:text-stone-300",
cancelled_tasks: "bg-slate-100 text-slate-700 dark:bg-slate-900/50 dark:text-slate-300",
};
interface KanbanColumnProps {
@@ -94,15 +94,15 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
return (
<div
className={cn(
"flex flex-col min-w-[280px] sm:min-w-0 sm:flex-1 max-w-[320px] sm:max-w-none snap-center sm:snap-align-none rounded-lg border-2 p-3",
"flex flex-col min-w-[280px] sm:min-w-0 sm:flex-1 max-w-[320px] sm:max-w-none snap-center sm:snap-align-none rounded-2xl border p-3",
COLUMN_COLORS[column.name] ?? "border-border bg-muted/30",
isOver && "ring-2 ring-primary"
isOver && "ring-2 ring-primary ring-offset-2"
)}
>
<div className="flex items-center gap-2 mb-3">
<div className="flex items-center gap-2 mb-3 px-1">
<h3
className={cn(
"font-semibold text-sm",
"font-heading font-semibold text-sm",
COLUMN_HEADER_COLORS[column.name]
)}
>
@@ -110,7 +110,7 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
</h3>
<Badge
variant="secondary"
className={cn("text-xs", COUNT_BADGE_COLORS[column.name])}
className={cn("text-xs rounded-md", COUNT_BADGE_COLORS[column.name])}
>
{column.count}
</Badge>
@@ -121,7 +121,7 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
<DraggableTask key={task.id} task={task} />
))}
{column.tasks.length === 0 && (
<div className="flex items-center justify-center h-[60px] text-xs text-muted-foreground rounded-md border border-dashed">
<div className="flex items-center justify-center h-[60px] text-xs text-muted-foreground rounded-xl border border-dashed border-border/60">
No tasks
</div>
)}
+7 -7
View File
@@ -18,23 +18,23 @@ export function TaskCard({ task, isDragging }: TaskCardProps) {
<Link href={`${basePath}/tasks/${task.id}`}>
<div
className={cn(
"rounded-lg border bg-card p-3 space-y-2 transition-shadow hover:shadow-md cursor-grab",
isDragging && "shadow-lg ring-2 ring-primary"
"rounded-xl border bg-card p-3.5 space-y-2 transition-all duration-200 hover:shadow-md hover:shadow-black/[0.04] cursor-grab",
isDragging && "shadow-lg ring-2 ring-primary rotate-[1deg] scale-[1.02]"
)}
>
<div className="font-medium text-sm leading-tight line-clamp-2">
<div className="font-medium text-sm leading-snug line-clamp-2">
{task.title}
</div>
{task.residence_name && (
<p className="text-xs text-muted-foreground truncate">
<p className="text-xs text-muted-foreground/70 truncate">
{task.residence_name}
</p>
)}
<div className="flex flex-wrap gap-1.5">
{task.priority && (
<Badge variant="outline" className="text-xs px-1.5 py-0">
<Badge variant="outline" className="text-xs px-1.5 py-0 rounded-md">
{task.priority.icon && (
<span className="mr-0.5">{task.priority.icon}</span>
)}
@@ -42,7 +42,7 @@ export function TaskCard({ task, isDragging }: TaskCardProps) {
</Badge>
)}
{task.category && (
<Badge variant="secondary" className="text-xs px-1.5 py-0">
<Badge variant="secondary" className="text-xs px-1.5 py-0 rounded-md">
{task.category.icon && (
<span className="mr-0.5">{task.category.icon}</span>
)}
@@ -51,7 +51,7 @@ export function TaskCard({ task, isDragging }: TaskCardProps) {
)}
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<div className="flex items-center gap-3 text-xs text-muted-foreground/70">
{task.due_date && (
<span className="flex items-center gap-1">
<Calendar className="size-3" aria-hidden="true" />
+1 -1
View File
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
"flex flex-col gap-6 rounded-2xl border bg-card py-6 text-card-foreground shadow-sm transition-shadow",
className
)}
{...props}
+17 -17
View File
@@ -26,28 +26,28 @@ export const themes: ThemeDefinition[] = [
{
id: "default",
name: "Default",
description: "Vibrant iOS system colors",
description: "Warm orange with teal accents",
light: {
primary: "#0079FF",
secondary: "#5AC7F9",
accent: "#FF9400",
error: "#FF3A2F",
primary: "#E07A3A",
secondary: "#0D7C66",
accent: "#D4A574",
error: "#DC2626",
bgPrimary: "#FFFFFF",
bgSecondary: "#F1F7F7",
textPrimary: "#111111",
textSecondary: "#3C3C3C",
bgSecondary: "#F7F7F7",
textPrimary: "#1C1917",
textSecondary: "#78716C",
textOnPrimary: "#FFFFFF",
},
dark: {
primary: "#0984FF",
secondary: "#63D2FF",
accent: "#FF9F09",
error: "#FF4539",
bgPrimary: "#1C1C1C",
bgSecondary: "#2C2C2C",
textPrimary: "#FFFFFF",
textSecondary: "#EBEBEB",
textOnPrimary: "#FFFFFF",
primary: "#F0A070",
secondary: "#4FC9AF",
accent: "#DDB892",
error: "#EF4444",
bgPrimary: "#0A0A0A",
bgSecondary: "#161616",
textPrimary: "#FAFAFA",
textSecondary: "#A1A1A1",
textOnPrimary: "#0A0A0A",
},
},
{
+17 -10
View File
@@ -3,16 +3,6 @@
import { useEffect } from "react";
import { useThemeStore } from "@/stores/theme";
/**
* ThemeProvider syncs the Zustand theme store with the DOM.
*
* It sets:
* - `data-theme` attribute on <html> for CSS theme selection
* - `dark` class on <html> for dark mode (respects system preference when mode is "system")
*
* Render this component once, near the root of the app (e.g., in layout.tsx).
* It renders no visible DOM — it only produces side effects.
*/
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const themeId = useThemeStore((s) => s.themeId);
const mode = useThemeStore((s) => s.mode);
@@ -51,5 +41,22 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
return () => mql.removeEventListener("change", handler);
}, [mode]);
// On mount, migrate any stale persisted state to light mode
useEffect(() => {
const stored = localStorage.getItem("casera-theme");
if (stored) {
try {
const parsed = JSON.parse(stored);
if (parsed?.state?.mode === "system") {
parsed.state.mode = "light";
localStorage.setItem("casera-theme", JSON.stringify(parsed));
useThemeStore.getState().setMode("light");
}
} catch {
// ignore
}
}
}, []);
return <>{children}</>;
}
+1 -1
View File
@@ -15,7 +15,7 @@ export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
themeId: DEFAULT_THEME_ID,
mode: "system",
mode: "light",
setTheme: (themeId: string) => set({ themeId }),
setMode: (mode: ColorMode) => set({ mode }),
}),
+30 -30
View File
@@ -32,19 +32,19 @@
/* ------------------------------------------------------------------ */
/* ================================================================== */
/* THEME: Default — Vibrant iOS system colors */
/* THEME: Default — Warm orange brand with teal accents */
/* ================================================================== */
[data-theme="default"],
:root {
/* App color tokens light */
--color-primary: #0079FF;
--color-secondary: #5AC7F9;
--color-accent: #FF9400;
--color-error: #FF3A2F;
--color-primary: #E07A3A;
--color-secondary: #0D7C66;
--color-accent: #D4A574;
--color-error: #DC2626;
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F1F7F7;
--color-text-primary: #111111;
--color-text-secondary: #3C3C3C;
--color-bg-secondary: #F7F7F7;
--color-text-primary: #1C1917;
--color-text-secondary: #78716C;
--color-text-on-primary: #FFFFFF;
/* shadcn overrides */
@@ -56,38 +56,38 @@
--accent-foreground: var(--color-text-primary);
--background: var(--color-bg-primary);
--foreground: var(--color-text-primary);
--card: var(--color-bg-secondary);
--card: #FFFFFF;
--card-foreground: var(--color-text-primary);
--popover: var(--color-bg-primary);
--popover: #FFFFFF;
--popover-foreground: var(--color-text-primary);
--muted: var(--color-bg-secondary);
--muted-foreground: var(--color-text-secondary);
--destructive: var(--color-error);
--border: color-mix(in srgb, var(--color-text-primary) 15%, transparent);
--input: color-mix(in srgb, var(--color-text-primary) 15%, transparent);
--border: #E8E8E8;
--input: #E8E8E8;
--ring: var(--color-primary);
--sidebar: var(--color-bg-secondary);
--sidebar: #FFFFFF;
--sidebar-foreground: var(--color-text-primary);
--sidebar-primary: var(--color-primary);
--sidebar-primary-foreground: var(--color-text-on-primary);
--sidebar-accent: var(--color-bg-primary);
--sidebar-accent: var(--color-bg-secondary);
--sidebar-accent-foreground: var(--color-text-primary);
--sidebar-border: color-mix(in srgb, var(--color-text-primary) 15%, transparent);
--sidebar-border: #E7E5E4;
--sidebar-ring: var(--color-primary);
}
[data-theme="default"].dark,
.dark[data-theme="default"] {
--color-primary: #0984FF;
--color-secondary: #63D2FF;
--color-accent: #FF9F09;
--color-error: #FF4539;
--color-bg-primary: #1C1C1C;
--color-bg-secondary: #2C2C2C;
--color-text-primary: #FFFFFF;
--color-text-secondary: #EBEBEB;
--color-text-on-primary: #FFFFFF;
--color-primary: #F0A070;
--color-secondary: #4FC9AF;
--color-accent: #DDB892;
--color-error: #EF4444;
--color-bg-primary: #0A0A0A;
--color-bg-secondary: #161616;
--color-text-primary: #FAFAFA;
--color-text-secondary: #A1A1A1;
--color-text-on-primary: #0A0A0A;
--primary: var(--color-primary);
--primary-foreground: var(--color-text-on-primary);
@@ -97,24 +97,24 @@
--accent-foreground: var(--color-text-primary);
--background: var(--color-bg-primary);
--foreground: var(--color-text-primary);
--card: var(--color-bg-secondary);
--card: #141414;
--card-foreground: var(--color-text-primary);
--popover: var(--color-bg-secondary);
--popover: #141414;
--popover-foreground: var(--color-text-primary);
--muted: var(--color-bg-secondary);
--muted-foreground: var(--color-text-secondary);
--destructive: var(--color-error);
--border: color-mix(in srgb, var(--color-text-primary) 12%, transparent);
--input: color-mix(in srgb, var(--color-text-primary) 15%, transparent);
--border: rgba(255, 255, 255, 0.1);
--input: rgba(255, 255, 255, 0.12);
--ring: var(--color-primary);
--sidebar: var(--color-bg-secondary);
--sidebar: #141414;
--sidebar-foreground: var(--color-text-primary);
--sidebar-primary: var(--color-primary);
--sidebar-primary-foreground: var(--color-text-on-primary);
--sidebar-accent: var(--color-bg-primary);
--sidebar-accent-foreground: var(--color-text-primary);
--sidebar-border: color-mix(in srgb, var(--color-text-primary) 12%, transparent);
--sidebar-border: rgba(255, 255, 255, 0.1);
--sidebar-ring: var(--color-primary);
}