Phase 2
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:
201 files changed
+31984
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user