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
+56 -3
View File
@@ -1,10 +1,63 @@
"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="w-full max-w-md">
{children}
<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."
/>
)}
+4 -11
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 />
<TopBar />
{/* 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">
{children}
</main>
</div>
<main className="max-w-7xl mx-auto px-6 py-8 lg:py-12 pb-28 md:pb-12">
{children}
</main>
{/* Mobile bottom nav */}
<MobileNav />
</div>
</DataProviderProvider>
+371 -47
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 ─── */
export default function DashboardPage() {
const { data: residences, isLoading } = useResidences();
const user = useAuthStore((s) => s.user);
function getTimeGreeting() {
const h = new Date().getHours();
if (h < 12) return "Good morning";
if (h < 17) return "Good afternoon";
return "Good evening";
}
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);
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));
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`)}
/>
)}
+4 -12
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 />
<TopBar />
{/* Sidebar - hidden on mobile */}
<Sidebar />
<main className="max-w-7xl mx-auto px-6 py-8 lg:py-12 pb-28 md:pb-12">
{children}
</main>
{/* 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">
{children}
</main>
</div>
{/* Mobile bottom nav */}
<MobileNav />
</div>
</DataProviderProvider>
+38 -19
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">
Casera
</h1>
<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
</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>
);
}