345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
import React from 'react';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
/** Base pulsing block */
|
||
export function Skeleton({
|
||
className,
|
||
...props
|
||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||
return (
|
||
<div
|
||
className={cn('animate-pulse rounded-md bg-gray-200/80', className)}
|
||
{...props}
|
||
/>
|
||
);
|
||
}
|
||
|
||
export function SkeletonText({
|
||
lines = 3,
|
||
className,
|
||
}: {
|
||
lines?: number;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<div className={cn('space-y-2', className)}>
|
||
{Array.from({ length: lines }).map((_, i) => (
|
||
<Skeleton
|
||
key={i}
|
||
className={cn('h-3', i === lines - 1 ? 'w-2/3' : 'w-full')}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Product card placeholder */
|
||
export function ProductCardSkeleton({ className }: { className?: string }) {
|
||
return (
|
||
<div className={cn('p-4 md:p-5 flex flex-col min-h-[340px]', className)}>
|
||
<Skeleton className="w-full h-[150px] mb-4 rounded-lg" />
|
||
<Skeleton className="h-4 w-4/5 mb-2" />
|
||
<Skeleton className="h-4 w-3/5 mb-3" />
|
||
<Skeleton className="h-3 w-24 mb-2" />
|
||
<Skeleton className="h-4 w-20 mt-auto" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Horizontal featured mini-card */
|
||
export function FeaturedCardSkeleton() {
|
||
return (
|
||
<div className="flex items-center gap-3 p-3 border border-gray-100 rounded-md h-[122px]">
|
||
<Skeleton className="w-[76px] h-[76px] shrink-0 rounded" />
|
||
<div className="flex-1 space-y-2">
|
||
<Skeleton className="h-3.5 w-4/5" />
|
||
<Skeleton className="h-3 w-20" />
|
||
<Skeleton className="h-3.5 w-16" />
|
||
<Skeleton className="h-2.5 w-3/4" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Section header with title + action */
|
||
export function SectionHeaderSkeleton() {
|
||
return (
|
||
<div className="flex items-center justify-between pb-3.5 border-b border-[#e5e5e5] mb-5">
|
||
<Skeleton className="h-5 w-44" />
|
||
<Skeleton className="h-4 w-20" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Brand logo tile */
|
||
export function BrandTileSkeleton() {
|
||
return <Skeleton className="h-[72px] md:h-[80px] rounded-lg" />;
|
||
}
|
||
|
||
/** Category circle */
|
||
export function CategorySkeleton() {
|
||
return (
|
||
<div className="flex flex-col items-center gap-2">
|
||
<Skeleton className="w-16 h-16 rounded-full" />
|
||
<Skeleton className="h-3 w-14" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Promo banner pair */
|
||
export function PromoBannerSkeleton() {
|
||
return (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-5">
|
||
{[0, 1].map((i) => (
|
||
<Skeleton key={i} className="min-h-[200px] md:min-h-[220px] rounded-xl" />
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Shop / catalog grid with sidebar */
|
||
export function CatalogSkeleton() {
|
||
return (
|
||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||
<div className="flex gap-6">
|
||
<div className="hidden lg:block w-[240px] shrink-0 space-y-4">
|
||
<Skeleton className="h-6 w-32 mb-2" />
|
||
{Array.from({ length: 8 }).map((_, i) => (
|
||
<Skeleton key={i} className="h-4 w-full" />
|
||
))}
|
||
<Skeleton className="h-6 w-28 mt-6 mb-2" />
|
||
{Array.from({ length: 5 }).map((_, i) => (
|
||
<Skeleton key={i} className="h-4 w-3/4" />
|
||
))}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex justify-between mb-4">
|
||
<Skeleton className="h-5 w-40" />
|
||
<Skeleton className="h-9 w-36 rounded" />
|
||
</div>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 border border-[#e8e8e8]">
|
||
{Array.from({ length: 8 }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className="border-r border-b border-[#e8e8e8] last:border-r-0"
|
||
>
|
||
<ProductCardSkeleton />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** PDP skeleton */
|
||
export function ProductDetailSkeleton() {
|
||
return (
|
||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<Skeleton className="h-4 w-48 mb-6" />
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-10">
|
||
<div className="lg:col-span-6 flex gap-3">
|
||
<div className="hidden md:flex flex-col gap-2">
|
||
{[1, 2, 3, 4].map((i) => (
|
||
<Skeleton key={i} className="w-16 h-16 rounded-lg" />
|
||
))}
|
||
</div>
|
||
<Skeleton className="flex-1 h-[380px] md:h-[420px] rounded-xl" />
|
||
</div>
|
||
<div className="lg:col-span-6 space-y-4">
|
||
<Skeleton className="h-4 w-24" />
|
||
<Skeleton className="h-8 w-4/5" />
|
||
<Skeleton className="h-4 w-32" />
|
||
<Skeleton className="h-10 w-40" />
|
||
<Skeleton className="h-20 w-full rounded-lg" />
|
||
<div className="flex gap-3 pt-2">
|
||
<Skeleton className="h-12 w-28 rounded-md" />
|
||
<Skeleton className="h-12 flex-1 rounded-md" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Skeleton className="h-10 w-full max-w-lg mt-10 mb-4" />
|
||
<Skeleton className="h-48 w-full rounded-lg" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Cart / checkout style page */
|
||
export function CartPageSkeleton() {
|
||
return (
|
||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<Skeleton className="h-7 w-48 mb-6" />
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||
<div className="lg:col-span-8 space-y-3">
|
||
{Array.from({ length: 3 }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className="flex gap-4 p-4 bg-white border border-gray-100 rounded-lg"
|
||
>
|
||
<Skeleton className="w-20 h-20 rounded-md shrink-0" />
|
||
<div className="flex-1 space-y-2">
|
||
<Skeleton className="h-4 w-3/4" />
|
||
<Skeleton className="h-3 w-24" />
|
||
<Skeleton className="h-8 w-28" />
|
||
</div>
|
||
<Skeleton className="h-5 w-16" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="lg:col-span-4">
|
||
<div className="bg-white border border-gray-100 rounded-lg p-5 space-y-3">
|
||
<Skeleton className="h-5 w-32" />
|
||
<Skeleton className="h-4 w-full" />
|
||
<Skeleton className="h-4 w-full" />
|
||
<Skeleton className="h-10 w-full rounded-md mt-4" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Full homepage skeleton matching storefront sections */
|
||
export function HomePageSkeleton() {
|
||
return (
|
||
<div className="w-full min-h-screen bg-white">
|
||
{/* Announcement */}
|
||
<Skeleton className="h-8 w-full rounded-none" />
|
||
|
||
{/* Header */}
|
||
<div className="border-b border-gray-100">
|
||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between gap-4">
|
||
<Skeleton className="h-8 w-28" />
|
||
<Skeleton className="hidden md:block h-10 flex-1 max-w-xl rounded-md" />
|
||
<div className="flex gap-3">
|
||
<Skeleton className="h-8 w-8 rounded-full" />
|
||
<Skeleton className="h-8 w-8 rounded-full" />
|
||
<Skeleton className="h-8 w-8 rounded-full" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Nav */}
|
||
<div className="border-b border-gray-100">
|
||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-3 flex gap-4">
|
||
<Skeleton className="h-9 w-36 rounded-md" />
|
||
{Array.from({ length: 5 }).map((_, i) => (
|
||
<Skeleton key={i} className="hidden md:block h-4 w-16 self-center" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 space-y-8 py-6">
|
||
{/* Hero */}
|
||
<Skeleton className="w-full h-[220px] md:h-[360px] rounded-xl" />
|
||
|
||
{/* Promo cards ×3 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
{[0, 1, 2].map((i) => (
|
||
<Skeleton key={i} className="h-[200px] rounded-2xl" />
|
||
))}
|
||
</div>
|
||
|
||
{/* Categories */}
|
||
<div>
|
||
<SectionHeaderSkeleton />
|
||
<div className="flex gap-6 overflow-hidden">
|
||
{Array.from({ length: 8 }).map((_, i) => (
|
||
<CategorySkeleton key={i} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* New arrivals */}
|
||
<div>
|
||
<SectionHeaderSkeleton />
|
||
<div className="border border-[#e5e5e5] grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5">
|
||
{Array.from({ length: 5 }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className="border-r border-[#e5e5e5] last:border-r-0"
|
||
>
|
||
<ProductCardSkeleton />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Promo banners */}
|
||
<PromoBannerSkeleton />
|
||
|
||
{/* Hot deals */}
|
||
<div className="flex flex-col lg:flex-row gap-5">
|
||
<Skeleton className="w-full lg:w-[310px] h-[380px] rounded-md shrink-0" />
|
||
<div className="flex-1">
|
||
<SectionHeaderSkeleton />
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{Array.from({ length: 6 }).map((_, i) => (
|
||
<FeaturedCardSkeleton key={i} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Brands */}
|
||
<div>
|
||
<SectionHeaderSkeleton />
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 md:gap-4">
|
||
{Array.from({ length: 10 }).map((_, i) => (
|
||
<BrandTileSkeleton key={i} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Features */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<Skeleton key={i} className="h-[88px] rounded-lg" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Generic content page skeleton */
|
||
export function PageSkeleton({
|
||
variant = 'home',
|
||
}: {
|
||
variant?: 'home' | 'catalog' | 'product' | 'cart' | 'simple';
|
||
}) {
|
||
if (variant === 'catalog') {
|
||
return (
|
||
<div className="min-h-screen bg-white pt-4">
|
||
<CatalogSkeleton />
|
||
</div>
|
||
);
|
||
}
|
||
if (variant === 'product') {
|
||
return (
|
||
<div className="min-h-screen bg-white">
|
||
<ProductDetailSkeleton />
|
||
</div>
|
||
);
|
||
}
|
||
if (variant === 'cart') {
|
||
return (
|
||
<div className="min-h-screen bg-[#f4f7fe]">
|
||
<CartPageSkeleton />
|
||
</div>
|
||
);
|
||
}
|
||
if (variant === 'simple') {
|
||
return (
|
||
<div className="min-h-screen bg-white">
|
||
<div className="container mx-auto px-4 py-10 space-y-4">
|
||
<Skeleton className="h-8 w-56" />
|
||
<Skeleton className="h-4 w-96 max-w-full" />
|
||
<Skeleton className="h-64 w-full rounded-xl mt-6" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
return <HomePageSkeleton />;
|
||
}
|