Base app architecture:

    Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
This commit is contained in:
Stefan Lazic committed 2026-08-02 17:47:23 +01:00
1 parent 468d163299
commit 090646fdfd
201 files changed
+31984

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
import type { LucideIcon } from "lucide-react";
/**
* Placeholder for routes that exist to prove the layout/navigation/auth
* architecture works, but whose real feature is out of Phase 2's scope
* (booking, appointments, loyalty, staff/owner tools — see the Phase 2
* brief). Every nav destination renders one of these rather than 404ing.
*/
export function ComingSoon({
icon: Icon,
title,
phase,
}: {
icon: LucideIcon;
title: string;
phase: string;
}) {
return (
<div className="flex flex-col items-center justify-center px-6 py-24 text-center">
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full border border-line bg-white text-muted">
<Icon className="h-6 w-6" aria-hidden="true" />
</div>
<h1 className="mb-1 text-lg font-bold">{title}</h1>
<p className="max-w-xs text-sm text-muted">Coming in {phase}. This route exists today to prove the layout, navigation and auth gating around it.</p>
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
export interface EmptyStateProps {
icon: LucideIcon;
title: string;
description?: string;
action?: ReactNode;
}
/**
* Not represented anywhere in the templates (see
* docs/implementation-plan.md §6/§8) — every list screen needs one of
* these for the zero-items case.
*/
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
return (
<div className="flex flex-col items-center px-6 py-16 text-center">
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full border border-line bg-white text-muted">
<Icon className="h-6 w-6" aria-hidden="true" />
</div>
<h3 className="mb-1 text-base font-bold">{title}</h3>
{description && <p className="mb-6 text-sm text-muted">{description}</p>}
{action}
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { TriangleAlert } from "lucide-react";
import { Button } from "@/components/ui/button";
export interface ErrorStateProps {
title?: string;
description?: string;
onRetry?: () => void;
}
/** Not represented anywhere in the templates — the generic "something went wrong" state for a failed fetch/action. */
export function ErrorState({
title = "Something went wrong",
description = "Please try again in a moment.",
onRetry,
}: ErrorStateProps) {
return (
<div role="alert" className="flex flex-col items-center px-6 py-16 text-center">
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-red-50 text-red-500">
<TriangleAlert className="h-6 w-6" aria-hidden="true" />
</div>
<h3 className="mb-1 text-base font-bold">{title}</h3>
<p className="mb-6 text-sm text-muted">{description}</p>
{onRetry && (
<Button variant="secondary" size="md" onClick={onRetry}>
Try again
</Button>
)}
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
import { Loader2 } from "lucide-react";
/**
* Full-region loading indicator for cases a skeleton doesn't fit (route
* transitions, full-page Suspense fallbacks). Prefer `ListSkeleton`/
* `CardSkeleton` (skeleton.tsx) for known list/card shapes — a skeleton
* that mirrors the eventual layout reduces perceived load time more than
* a spinner does.
*/
export function LoadingState({ label = "Loading…" }: { label?: string }) {
return (
<div role="status" className="flex flex-col items-center justify-center gap-3 px-6 py-16 text-muted">
<Loader2 className="h-6 w-6 animate-spin" aria-hidden="true" />
<span className="text-sm">{label}</span>
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { cn } from "@/lib/utils/cn";
/** Base skeleton block — not represented anywhere in the templates. Compose into loading states for specific cards below. */
export function Skeleton({ className }: { className?: string }) {
return <div className={cn("animate-pulse rounded-lg bg-line/60", className)} />;
}
export function CardSkeleton() {
return (
<div className="rounded-jumbo border border-line bg-white p-5">
<div className="mb-4 flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-3.5 w-2/3" />
<Skeleton className="h-3 w-1/3" />
</div>
</div>
<Skeleton className="h-3 w-full" />
</div>
);
}
export function ListSkeleton({ count = 3 }: { count?: number }) {
return (
<div className="space-y-3" aria-hidden="true">
{Array.from({ length: count }, (_, i) => (
<CardSkeleton key={i} />
))}
</div>
);
}