166 lines
5.2 KiB
TypeScript
Raw Normal View History

import type { ReactNode } from "react";
import Link from "next/link";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { ErrorState } from "@/components/atoms/error-state";
interface PageLayoutProps {
icon?: ReactNode | undefined;
title: string;
description?: string | undefined;
actions?: ReactNode | undefined;
backLink?: { label: string; href: string } | undefined;
statusPill?: ReactNode | undefined;
loading?: boolean | undefined;
loadingFallback?: ReactNode | undefined;
error?: Error | string | null | undefined;
onRetry?: (() => void) | undefined;
children: ReactNode;
}
export function PageLayout({
icon,
title,
description,
actions,
backLink,
statusPill,
loading = false,
loadingFallback,
error = null,
onRetry,
children,
}: PageLayoutProps) {
return (
<div>
{/* Page header */}
<div className="bg-muted/40 border-b border-border">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)]">
{backLink && (
<div className="mb-[var(--cp-space-sm)] sm:mb-[var(--cp-space-md)]">
<Link
href={backLink.href}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors duration-200"
>
<ArrowLeftIcon className="h-4 w-4" />
{backLink.label}
</Link>
</div>
)}
<div className="flex flex-col gap-[var(--cp-space-md)] sm:gap-[var(--cp-space-lg)]">
<div className="flex items-start justify-between gap-4 min-w-0">
<div className="flex items-start min-w-0 flex-1">
{icon && (
<div className="h-7 w-7 sm:h-8 sm:w-8 text-primary mr-[var(--cp-space-md)] sm:mr-[var(--cp-space-lg)] flex-shrink-0 mt-0.5">
{icon}
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground leading-tight">
{title}
</h1>
{statusPill}
</div>
{description && (
<p className="text-sm text-muted-foreground mt-1 leading-relaxed line-clamp-2 sm:line-clamp-none">
{description}
</p>
)}
</div>
</div>
{actions && (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 flex-shrink-0">
{actions}
</div>
)}
</div>
</div>
</div>
</div>
{/* Content */}
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)] md:py-[var(--cp-space-2xl)]">
<div className="space-y-[var(--cp-space-xl)] sm:space-y-[var(--cp-space-2xl)]">
{renderPageContent({
loading,
error: error ?? undefined,
children,
onRetry,
loadingFallback,
})}
</div>
</div>
</div>
);
}
function renderPageContent({
loading,
error,
children,
onRetry,
loadingFallback,
}: {
loading: boolean | undefined;
error: Error | string | undefined;
children: React.ReactNode;
onRetry: (() => void) | undefined;
loadingFallback: React.ReactNode;
}): React.ReactNode {
if (loading) {
return loadingFallback ?? <PageLoadingState />;
}
if (error) {
return <PageErrorState error={error} onRetry={onRetry} />;
}
return children;
}
function PageLoadingState() {
return (
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-3xl)]">
<div className="space-y-4 sm:space-y-6">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-6 w-36 sm:w-48" />
<Skeleton className="h-4 w-48 sm:w-64" />
</div>
</div>
<div className="space-y-3 sm:space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="bg-card border border-border rounded-lg p-3 sm:p-4 shadow-[var(--cp-shadow-1)]"
>
<Skeleton className="h-4 w-1/2 mb-2" />
<Skeleton className="h-3 w-3/4" />
</div>
))}
</div>
</div>
</div>
);
}
interface PageErrorStateProps {
error: Error | string;
onRetry?: (() => void) | undefined;
}
function PageErrorState({ error, onRetry }: PageErrorStateProps) {
const errorMessage = typeof error === "string" ? error : error.message;
return (
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-3xl)]">
<ErrorState
title="Unable to load page"
message={errorMessage}
onRetry={onRetry}
variant="page"
/>
</div>
);
}