add skeleton resolvers
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,40 +112,14 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
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>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !product) {
|
||||
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 || "The requested product could not be found."}
|
||||
{error}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -158,6 +133,30 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<div>
|
||||
<Skeleton className="aspect-square w-full mb-4" />
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="aspect-square w-full" />
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
|
||||
@@ -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>
|
||||
</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 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>
|
||||
</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 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,10 +16,11 @@ export const navigationEditor: ComponentConfig<NavigationProps> = {
|
||||
{ label: "About", href: "/about" },
|
||||
],
|
||||
showSearch: "yes",
|
||||
showAccount: "yes",
|
||||
showCart: "yes",
|
||||
sticky: "yes",
|
||||
tone: "default",
|
||||
bannerText: "",
|
||||
bannerTone: "accent",
|
||||
},
|
||||
fields: {
|
||||
brand: { label: "Brand", type: "text", contentEditable: true },
|
||||
@@ -33,16 +34,22 @@ export const navigationEditor: ComponentConfig<NavigationProps> = {
|
||||
href: { label: "Link", type: "text" },
|
||||
},
|
||||
},
|
||||
showSearch: {
|
||||
label: "Search icon",
|
||||
type: "radio",
|
||||
bannerText: {
|
||||
label: "Banner text",
|
||||
type: "text",
|
||||
contentEditable: true,
|
||||
},
|
||||
bannerTone: {
|
||||
label: "Banner tone",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Show", value: "yes" },
|
||||
{ label: "Hide", value: "no" },
|
||||
{ label: "Default", value: "default" },
|
||||
{ label: "Accent", value: "accent" },
|
||||
{ label: "Inverse (dark)", value: "inverse" },
|
||||
],
|
||||
},
|
||||
showAccount: {
|
||||
label: "Account icon",
|
||||
showSearch: {
|
||||
label: "Search icon",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ label: "Show", value: "yes" },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Menu as MenuIcon, ShoppingBag, Search, User } from "lucide-react";
|
||||
import { Menu as MenuIcon, ShoppingBag, Search } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
|
||||
@@ -9,20 +9,22 @@ export type NavigationProps = {
|
||||
brand: string;
|
||||
links: Array<{ label: string; href: string }>;
|
||||
showSearch: "yes" | "no";
|
||||
showAccount: "yes" | "no";
|
||||
showCart: "yes" | "no";
|
||||
sticky: "yes" | "no";
|
||||
tone: "default" | "muted" | "inverse";
|
||||
bannerText: string;
|
||||
bannerTone: "default" | "accent" | "inverse";
|
||||
};
|
||||
|
||||
export function Navigation({
|
||||
brand,
|
||||
links,
|
||||
showSearch,
|
||||
showAccount,
|
||||
showCart,
|
||||
sticky,
|
||||
tone,
|
||||
bannerText,
|
||||
bannerTone,
|
||||
}: NavigationProps) {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [cartOpen, setCartOpen] = useState(false);
|
||||
@@ -35,12 +37,33 @@ export function Navigation({
|
||||
inverse: "bg-foreground text-background",
|
||||
};
|
||||
|
||||
const bannerToneClass: Record<NavigationProps["bannerTone"], string> = {
|
||||
default: "bg-muted text-foreground",
|
||||
accent: "bg-primary text-primary-foreground",
|
||||
inverse: "bg-foreground text-background",
|
||||
};
|
||||
|
||||
const hasBanner = typeof bannerText === "string" && bannerText.trim().length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full",
|
||||
sticky === "yes" && "sticky top-0 z-40",
|
||||
)}
|
||||
>
|
||||
{hasBanner && (
|
||||
<div className={cn("w-full", bannerToneClass[bannerTone])}>
|
||||
<div className="container mx-auto max-w-7xl px-6 py-2 text-center text-xs tracking-wide md:text-sm">
|
||||
{bannerText}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<header
|
||||
className={cn(
|
||||
"w-full",
|
||||
sticky === "yes" && "sticky top-0 z-40 backdrop-blur",
|
||||
sticky === "yes" && "backdrop-blur",
|
||||
toneClass[tone],
|
||||
)}
|
||||
>
|
||||
@@ -74,15 +97,6 @@ export function Navigation({
|
||||
<Search size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
)}
|
||||
{showAccount === "yes" && (
|
||||
<Link
|
||||
to="/account"
|
||||
aria-label="Account"
|
||||
className="hidden h-10 w-10 items-center justify-center rounded-full transition-colors hover:bg-foreground/5 md:inline-flex"
|
||||
>
|
||||
<User size={18} strokeWidth={1.5} />
|
||||
</Link>
|
||||
)}
|
||||
{showCart === "yes" && (
|
||||
<button
|
||||
onClick={() => setCartOpen(true)}
|
||||
@@ -107,6 +121,7 @@ export function Navigation({
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Quote, ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import { Typography } from "@/editor/theme/Typography";
|
||||
|
||||
export type TestimonialsProps = {
|
||||
@@ -30,16 +30,11 @@ export function Testimonials({ tagline, heading, items }: TestimonialsProps) {
|
||||
|
||||
{item ? (
|
||||
<figure className="mx-auto mt-12 flex max-w-2xl flex-col items-center">
|
||||
<Quote
|
||||
size={28}
|
||||
strokeWidth={1.25}
|
||||
className="mb-6 text-muted-foreground"
|
||||
/>
|
||||
<blockquote
|
||||
className="text-balance text-foreground"
|
||||
style={{ fontSize: "clamp(1.25rem, 2.4vw, 1.75rem)", lineHeight: 1.4 }}
|
||||
>
|
||||
"{item.quote}"
|
||||
{item.quote}
|
||||
</blockquote>
|
||||
<figcaption className="mt-8 flex items-center gap-3">
|
||||
{item.avatar ? (
|
||||
|
||||
@@ -26,10 +26,11 @@ export const initialData: Record<string, UserData> = {
|
||||
],
|
||||
cta: { label: "", href: "" },
|
||||
showSearch: "yes",
|
||||
showAccount: "yes",
|
||||
showCart: "yes",
|
||||
sticky: "yes",
|
||||
tone: "default",
|
||||
bannerText: "",
|
||||
bannerTone: "accent",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user