add skeleton resolvers

This commit is contained in:
Rami Bitar
2026-05-03 19:31:41 -04:00
parent a8b94c99c0
commit 0e3c6f058a
10 changed files with 168 additions and 134 deletions

View File

@@ -3,6 +3,7 @@
import React from 'react';
import { useCollectionProducts } from '@/editor/hooks/use-shopify-collections';
import ProductCard from './product-card';
import { Skeleton } from '@/components/ui/skeleton';
const CollectionDetail: React.FC<{ handle?: string }> = ({ handle: handleProp }) => {
const handle = handleProp ?? '';
@@ -14,25 +15,20 @@ const CollectionDetail: React.FC<{ handle?: string }> = ({ handle: handleProp })
? handle.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
: 'Collection';
if (loading) {
if (loading || !handle) {
return (
<div className="pt-4 pb-16">
<div className="container mx-auto px-4">
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
{formattedTitle}
</h2>
<div className="mb-16 flex justify-center">
<Skeleton className="h-12 w-64" />
</div>
{/* Loading Skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
<div className="aspect-square bg-gray-200"></div>
<div className="p-6">
<div className="h-6 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded mb-4"></div>
<div className="h-8 bg-gray-200 rounded mb-4"></div>
<div className="h-12 bg-gray-200 rounded"></div>
</div>
<div key={index} className="flex flex-col gap-3">
<Skeleton className="aspect-square w-full" />
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-1/3" />
</div>
))}
</div>

View File

@@ -2,6 +2,7 @@ import type { ShopifyCollection } from "@reacteditor/field-shopify";
import { useCollectionProducts } from "@/editor/hooks/use-shopify-collections";
import { ProductCard } from "./product-card";
import { Typography } from "@/editor/theme/Typography";
import { Skeleton } from "@/components/ui/skeleton";
export type CollectionProps = {
collection: ShopifyCollection | null;
@@ -17,10 +18,19 @@ export function CollectionView({
if (!selected) {
return (
<section className="py-20">
<section className="bg-background pb-24 pt-12 md:pt-20">
<div className="container mx-auto max-w-7xl px-6">
<div className="mx-auto max-w-md rounded-md border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
Pick a collection to render this page.
<header className="mx-auto mb-14 flex max-w-2xl flex-col items-center gap-3 text-center">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-10 w-3/4" />
{showDescription === "yes" ? (
<Skeleton className="mt-1 h-5 w-2/3" />
) : null}
</header>
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="aspect-[4/5] w-full" />
))}
</div>
</div>
</section>
@@ -50,10 +60,7 @@ export function CollectionView({
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-3 lg:grid-cols-4">
{loading
? Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="aspect-[4/5] w-full animate-pulse rounded-md bg-muted"
/>
<Skeleton key={i} className="aspect-[4/5] w-full" />
))
: products.map((p: any) => <ProductCard key={p.id} product={p} />)}
</div>

View File

@@ -3,6 +3,8 @@ import type { ShopifyProduct } from "@reacteditor/field-shopify";
import { useProduct } from "@/editor/hooks/use-shopify-products";
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
import { Typography } from "@/editor/theme/Typography";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
export type FeaturedProductProps = {
product: ShopifyProduct | null;
@@ -23,21 +25,31 @@ export function FeaturedProductView({
const product: any = full ?? selected;
const cart = useShopifyCart();
if (!selected && loading) {
return (
<section className="py-20 md:py-28">
<div className="container mx-auto max-w-7xl px-6">
<div className="aspect-[2/1] w-full animate-pulse rounded-lg bg-muted" />
</div>
</section>
);
}
if (!product) {
return (
<section className="py-20">
<div className="container mx-auto max-w-7xl px-6">
<div className="mx-auto max-w-md rounded-md border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
Pick a product to feature here.
<section
className={cn(
"py-20 md:py-28",
tone === "muted" ? "bg-muted/40" : "bg-background",
)}
>
<div className="container mx-auto grid max-w-7xl grid-cols-1 items-center gap-10 px-6 md:grid-cols-2 md:gap-16">
<div className={cn(align === "right" && "md:order-2")}>
<Skeleton className="aspect-[4/5] w-full" />
</div>
<div className="flex w-full flex-col items-start gap-5">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-10 w-3/4" />
<Skeleton className="h-6 w-32" />
<div className="w-full max-w-md space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
<div className="mt-2 flex flex-wrap gap-3">
<Skeleton className="h-11 w-32 rounded-full" />
<Skeleton className="h-11 w-32 rounded-full" />
</div>
</div>
</div>
</section>

View File

@@ -9,6 +9,7 @@ import ProductDetailInfo from './product-detail-info';
import ProductRecommendations from './product-recommendations';
import { Button } from '@/editor/components/ui/button';
import { Alert, AlertTitle, AlertDescription } from '@/editor/components/ui/alert';
import { Skeleton } from '@/components/ui/skeleton';
import {
Breadcrumb,
BreadcrumbList,
@@ -111,53 +112,51 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
}
};
if (loading) {
if (loading || !handle || !product) {
if (error && handle && !loading) {
return (
<div className="container mx-auto px-4 py-12">
<div className="mx-auto flex max-w-md flex-col items-start gap-3 rounded-lg border border-border bg-foreground/[0.02] p-6">
<p className="text-sm font-medium">Product not found</p>
<p className="font-mono text-xs leading-relaxed text-muted-foreground line-clamp-3">
{error}
</p>
<Button
size="sm"
variant="outline"
onClick={() => window.history.back()}
>
Go back
</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Image Gallery Skeleton */}
<div>
<div className="aspect-square bg-gray-200 rounded-lg animate-pulse mb-4"></div>
<Skeleton className="aspect-square w-full mb-4" />
<div className="grid grid-cols-4 gap-2">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="aspect-square bg-gray-200 rounded animate-pulse"></div>
<Skeleton key={i} className="aspect-square w-full" />
))}
</div>
</div>
{/* Product Info Skeleton */}
<div>
<div className="h-8 bg-gray-200 rounded mb-4 animate-pulse"></div>
<div className="h-6 bg-gray-200 rounded mb-6 w-1/3 animate-pulse"></div>
<div className="h-24 bg-gray-200 rounded mb-6 animate-pulse"></div>
<div className="h-12 bg-gray-200 rounded mb-4 animate-pulse"></div>
<div className="h-12 bg-gray-200 rounded animate-pulse"></div>
<div className="flex flex-col gap-4">
<Skeleton className="h-8 w-3/4" />
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
</div>
</div>
);
}
if (error || !product) {
return (
<div className="container mx-auto px-4 py-12">
<div className="mx-auto flex max-w-md flex-col items-start gap-3 rounded-lg border border-border bg-foreground/[0.02] p-6">
<p className="text-sm font-medium">Product not found</p>
<p className="font-mono text-xs leading-relaxed text-muted-foreground line-clamp-3">
{error || "The requested product could not be found."}
</p>
<Button
size="sm"
variant="outline"
onClick={() => window.history.back()}
>
Go back
</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-white">
<div className="container mx-auto px-4 py-8">

View File

@@ -5,6 +5,7 @@ import { useProduct } from "@/editor/hooks/use-shopify-products";
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
import { Typography } from "@/editor/theme/Typography";
import { cn } from "@/editor/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
export type ProductDetailsProps = {
product: ShopifyProduct | null;
@@ -26,42 +27,39 @@ export function ProductDetailsView({ product: selected }: ProductDetailsProps) {
}
}, [product]);
if (!handle) {
if (!handle || loading || !product) {
return (
<section className="py-20">
<div className="container mx-auto max-w-7xl px-6">
<div className="mx-auto max-w-md rounded-md border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
Pick a product to render this page.
<section className="bg-background py-12 md:py-20">
<div className="container mx-auto grid max-w-7xl grid-cols-1 gap-10 px-6 md:grid-cols-2 md:gap-16">
<div className="flex flex-col gap-4">
<Skeleton className="aspect-[4/5] w-full" />
<div className="flex gap-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="aspect-square w-20 flex-shrink-0" />
))}
</div>
</div>
</div>
</section>
);
}
if (loading) {
return (
<section className="py-20">
<div className="container mx-auto grid max-w-7xl grid-cols-1 gap-12 px-6 md:grid-cols-2">
<div className="aspect-[4/5] w-full animate-pulse rounded-md bg-muted" />
<div className="space-y-4">
<div className="h-8 w-3/4 animate-pulse rounded bg-muted" />
<div className="h-5 w-1/4 animate-pulse rounded bg-muted" />
<div className="h-32 w-full animate-pulse rounded bg-muted" />
</div>
</div>
</section>
);
}
if (!product) {
return (
<section className="py-20">
<div className="container mx-auto max-w-7xl px-6">
<div className="mx-auto max-w-md rounded-md border border-border p-6 text-sm">
<p className="font-medium">Product not found</p>
<p className="mt-2 text-muted-foreground">
The requested product could not be found.
</p>
<div className="flex flex-col gap-6">
<Skeleton className="h-10 w-3/4" />
<Skeleton className="h-6 w-1/4" />
<div className="flex flex-col gap-3">
<Skeleton className="h-3 w-16" />
<div className="flex gap-2">
<Skeleton className="h-10 w-16 rounded-full" />
<Skeleton className="h-10 w-16 rounded-full" />
<Skeleton className="h-10 w-16 rounded-full" />
</div>
</div>
<div className="flex items-center gap-4 pt-2">
<Skeleton className="h-11 w-32 rounded-full" />
<Skeleton className="h-11 flex-1 rounded-full" />
</div>
<div className="space-y-2 border-t border-border pt-6">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
</div>
</div>
</section>

View File

@@ -5,6 +5,7 @@ import {
} from "@/editor/hooks/use-shopify-products";
import { ProductCard } from "./product-card";
import { Typography } from "@/editor/theme/Typography";
import { Skeleton } from "@/components/ui/skeleton";
export type RecommendedProductsProps = {
product: ShopifyProduct | null;
@@ -25,10 +26,16 @@ export function RecommendedProductsView({
if (!selected) {
return (
<section className="py-20">
<section className="bg-background py-20 md:py-28">
<div className="container mx-auto max-w-7xl px-6">
<div className="mx-auto max-w-md rounded-md border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
Pick a product to load recommendations.
<div className="mb-12 flex max-w-xl flex-col gap-3">
{tagline ? <Skeleton className="h-3 w-24" /> : null}
<Skeleton className="h-8 w-2/3" />
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-4">
{Array.from({ length: limit }).map((_, i) => (
<Skeleton key={i} className="aspect-[4/5] w-full" />
))}
</div>
</div>
</section>
@@ -50,10 +57,7 @@ export function RecommendedProductsView({
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-4">
{items.length === 0
? Array.from({ length: limit }).map((_, i) => (
<div
key={i}
className="aspect-[4/5] w-full animate-pulse rounded-md bg-muted"
/>
<Skeleton key={i} className="aspect-[4/5] w-full" />
))
: items.map((p: any) => <ProductCard key={p.id} product={p} />)}
</div>