add skeleton resolvers
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCollectionProducts } from '@/editor/hooks/use-shopify-collections';
|
import { useCollectionProducts } from '@/editor/hooks/use-shopify-collections';
|
||||||
import ProductCard from './product-card';
|
import ProductCard from './product-card';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
const CollectionDetail: React.FC<{ handle?: string }> = ({ handle: handleProp }) => {
|
const CollectionDetail: React.FC<{ handle?: string }> = ({ handle: handleProp }) => {
|
||||||
const 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())
|
? handle.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||||
: 'Collection';
|
: 'Collection';
|
||||||
|
|
||||||
if (loading) {
|
if (loading || !handle) {
|
||||||
return (
|
return (
|
||||||
<div className="pt-4 pb-16">
|
<div className="pt-4 pb-16">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
|
<div className="mb-16 flex justify-center">
|
||||||
{formattedTitle}
|
<Skeleton className="h-12 w-64" />
|
||||||
</h2>
|
</div>
|
||||||
|
|
||||||
{/* Loading Skeleton */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
<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) => (
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
|
<div key={index} className="flex flex-col gap-3">
|
||||||
<div className="aspect-square bg-gray-200"></div>
|
<Skeleton className="aspect-square w-full" />
|
||||||
<div className="p-6">
|
<Skeleton className="h-5 w-3/4" />
|
||||||
<div className="h-6 bg-gray-200 rounded mb-2"></div>
|
<Skeleton className="h-4 w-1/3" />
|
||||||
<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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ShopifyCollection } from "@reacteditor/field-shopify";
|
|||||||
import { useCollectionProducts } from "@/editor/hooks/use-shopify-collections";
|
import { useCollectionProducts } from "@/editor/hooks/use-shopify-collections";
|
||||||
import { ProductCard } from "./product-card";
|
import { ProductCard } from "./product-card";
|
||||||
import { Typography } from "@/editor/theme/Typography";
|
import { Typography } from "@/editor/theme/Typography";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export type CollectionProps = {
|
export type CollectionProps = {
|
||||||
collection: ShopifyCollection | null;
|
collection: ShopifyCollection | null;
|
||||||
@@ -17,10 +18,19 @@ export function CollectionView({
|
|||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return (
|
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="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">
|
<header className="mx-auto mb-14 flex max-w-2xl flex-col items-center gap-3 text-center">
|
||||||
Pick a collection to render this page.
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-3 lg:grid-cols-4">
|
||||||
{loading
|
{loading
|
||||||
? Array.from({ length: 8 }).map((_, i) => (
|
? Array.from({ length: 8 }).map((_, i) => (
|
||||||
<div
|
<Skeleton key={i} className="aspect-[4/5] w-full" />
|
||||||
key={i}
|
|
||||||
className="aspect-[4/5] w-full animate-pulse rounded-md bg-muted"
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
: products.map((p: any) => <ProductCard key={p.id} product={p} />)}
|
: products.map((p: any) => <ProductCard key={p.id} product={p} />)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { ShopifyProduct } from "@reacteditor/field-shopify";
|
|||||||
import { useProduct } from "@/editor/hooks/use-shopify-products";
|
import { useProduct } from "@/editor/hooks/use-shopify-products";
|
||||||
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
|
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
|
||||||
import { Typography } from "@/editor/theme/Typography";
|
import { Typography } from "@/editor/theme/Typography";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type FeaturedProductProps = {
|
export type FeaturedProductProps = {
|
||||||
product: ShopifyProduct | null;
|
product: ShopifyProduct | null;
|
||||||
@@ -23,21 +25,31 @@ export function FeaturedProductView({
|
|||||||
const product: any = full ?? selected;
|
const product: any = full ?? selected;
|
||||||
const cart = useShopifyCart();
|
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) {
|
if (!product) {
|
||||||
return (
|
return (
|
||||||
<section className="py-20">
|
<section
|
||||||
<div className="container mx-auto max-w-7xl px-6">
|
className={cn(
|
||||||
<div className="mx-auto max-w-md rounded-md border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
"py-20 md:py-28",
|
||||||
Pick a product to feature here.
|
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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ProductDetailInfo from './product-detail-info';
|
|||||||
import ProductRecommendations from './product-recommendations';
|
import ProductRecommendations from './product-recommendations';
|
||||||
import { Button } from '@/editor/components/ui/button';
|
import { Button } from '@/editor/components/ui/button';
|
||||||
import { Alert, AlertTitle, AlertDescription } from '@/editor/components/ui/alert';
|
import { Alert, AlertTitle, AlertDescription } from '@/editor/components/ui/alert';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbList,
|
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 (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||||
{/* Image Gallery Skeleton */}
|
|
||||||
<div>
|
<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">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Info Skeleton */}
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
<Skeleton className="h-8 w-3/4" />
|
||||||
<div className="h-8 bg-gray-200 rounded mb-4 animate-pulse"></div>
|
<Skeleton className="h-6 w-1/3" />
|
||||||
<div className="h-6 bg-gray-200 rounded mb-6 w-1/3 animate-pulse"></div>
|
<Skeleton className="h-24 w-full" />
|
||||||
<div className="h-24 bg-gray-200 rounded mb-6 animate-pulse"></div>
|
<Skeleton className="h-12 w-full" />
|
||||||
<div className="h-12 bg-gray-200 rounded mb-4 animate-pulse"></div>
|
<Skeleton className="h-12 w-full" />
|
||||||
<div className="h-12 bg-gray-200 rounded animate-pulse"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<div className="container mx-auto px-4 py-8">
|
<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 { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
|
||||||
import { Typography } from "@/editor/theme/Typography";
|
import { Typography } from "@/editor/theme/Typography";
|
||||||
import { cn } from "@/editor/lib/utils";
|
import { cn } from "@/editor/lib/utils";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export type ProductDetailsProps = {
|
export type ProductDetailsProps = {
|
||||||
product: ShopifyProduct | null;
|
product: ShopifyProduct | null;
|
||||||
@@ -26,42 +27,39 @@ export function ProductDetailsView({ product: selected }: ProductDetailsProps) {
|
|||||||
}
|
}
|
||||||
}, [product]);
|
}, [product]);
|
||||||
|
|
||||||
if (!handle) {
|
if (!handle || loading || !product) {
|
||||||
return (
|
return (
|
||||||
<section className="py-20">
|
<section className="bg-background py-12 md:py-20">
|
||||||
<div className="container mx-auto max-w-7xl px-6">
|
<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="mx-auto max-w-md rounded-md border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
<div className="flex flex-col gap-4">
|
||||||
Pick a product to render this page.
|
<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>
|
||||||
</div>
|
<div className="flex flex-col gap-6">
|
||||||
</section>
|
<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" />
|
||||||
if (loading) {
|
<div className="flex gap-2">
|
||||||
return (
|
<Skeleton className="h-10 w-16 rounded-full" />
|
||||||
<section className="py-20">
|
<Skeleton className="h-10 w-16 rounded-full" />
|
||||||
<div className="container mx-auto grid max-w-7xl grid-cols-1 gap-12 px-6 md:grid-cols-2">
|
<Skeleton className="h-10 w-16 rounded-full" />
|
||||||
<div className="aspect-[4/5] w-full animate-pulse rounded-md bg-muted" />
|
</div>
|
||||||
<div className="space-y-4">
|
</div>
|
||||||
<div className="h-8 w-3/4 animate-pulse rounded bg-muted" />
|
<div className="flex items-center gap-4 pt-2">
|
||||||
<div className="h-5 w-1/4 animate-pulse rounded bg-muted" />
|
<Skeleton className="h-11 w-32 rounded-full" />
|
||||||
<div className="h-32 w-full animate-pulse rounded bg-muted" />
|
<Skeleton className="h-11 flex-1 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="space-y-2 border-t border-border pt-6">
|
||||||
</section>
|
<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" />
|
||||||
if (!product) {
|
</div>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/editor/hooks/use-shopify-products";
|
} from "@/editor/hooks/use-shopify-products";
|
||||||
import { ProductCard } from "./product-card";
|
import { ProductCard } from "./product-card";
|
||||||
import { Typography } from "@/editor/theme/Typography";
|
import { Typography } from "@/editor/theme/Typography";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export type RecommendedProductsProps = {
|
export type RecommendedProductsProps = {
|
||||||
product: ShopifyProduct | null;
|
product: ShopifyProduct | null;
|
||||||
@@ -25,10 +26,16 @@ export function RecommendedProductsView({
|
|||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return (
|
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="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">
|
<div className="mb-12 flex max-w-xl flex-col gap-3">
|
||||||
Pick a product to load recommendations.
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -50,10 +57,7 @@ export function RecommendedProductsView({
|
|||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-4">
|
||||||
{items.length === 0
|
{items.length === 0
|
||||||
? Array.from({ length: limit }).map((_, i) => (
|
? Array.from({ length: limit }).map((_, i) => (
|
||||||
<div
|
<Skeleton key={i} className="aspect-[4/5] w-full" />
|
||||||
key={i}
|
|
||||||
className="aspect-[4/5] w-full animate-pulse rounded-md bg-muted"
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
: items.map((p: any) => <ProductCard key={p.id} product={p} />)}
|
: items.map((p: any) => <ProductCard key={p.id} product={p} />)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ export const navigationEditor: ComponentConfig<NavigationProps> = {
|
|||||||
{ label: "About", href: "/about" },
|
{ label: "About", href: "/about" },
|
||||||
],
|
],
|
||||||
showSearch: "yes",
|
showSearch: "yes",
|
||||||
showAccount: "yes",
|
|
||||||
showCart: "yes",
|
showCart: "yes",
|
||||||
sticky: "yes",
|
sticky: "yes",
|
||||||
tone: "default",
|
tone: "default",
|
||||||
|
bannerText: "",
|
||||||
|
bannerTone: "accent",
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
brand: { label: "Brand", type: "text", contentEditable: true },
|
brand: { label: "Brand", type: "text", contentEditable: true },
|
||||||
@@ -33,16 +34,22 @@ export const navigationEditor: ComponentConfig<NavigationProps> = {
|
|||||||
href: { label: "Link", type: "text" },
|
href: { label: "Link", type: "text" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showSearch: {
|
bannerText: {
|
||||||
label: "Search icon",
|
label: "Banner text",
|
||||||
type: "radio",
|
type: "text",
|
||||||
|
contentEditable: true,
|
||||||
|
},
|
||||||
|
bannerTone: {
|
||||||
|
label: "Banner tone",
|
||||||
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ label: "Show", value: "yes" },
|
{ label: "Default", value: "default" },
|
||||||
{ label: "Hide", value: "no" },
|
{ label: "Accent", value: "accent" },
|
||||||
|
{ label: "Inverse (dark)", value: "inverse" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
showAccount: {
|
showSearch: {
|
||||||
label: "Account icon",
|
label: "Search icon",
|
||||||
type: "radio",
|
type: "radio",
|
||||||
options: [
|
options: [
|
||||||
{ label: "Show", value: "yes" },
|
{ 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 { useState } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
|
import { useShopifyCart } from "@/editor/hooks/use-shopify-cart";
|
||||||
@@ -9,20 +9,22 @@ export type NavigationProps = {
|
|||||||
brand: string;
|
brand: string;
|
||||||
links: Array<{ label: string; href: string }>;
|
links: Array<{ label: string; href: string }>;
|
||||||
showSearch: "yes" | "no";
|
showSearch: "yes" | "no";
|
||||||
showAccount: "yes" | "no";
|
|
||||||
showCart: "yes" | "no";
|
showCart: "yes" | "no";
|
||||||
sticky: "yes" | "no";
|
sticky: "yes" | "no";
|
||||||
tone: "default" | "muted" | "inverse";
|
tone: "default" | "muted" | "inverse";
|
||||||
|
bannerText: string;
|
||||||
|
bannerTone: "default" | "accent" | "inverse";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Navigation({
|
export function Navigation({
|
||||||
brand,
|
brand,
|
||||||
links,
|
links,
|
||||||
showSearch,
|
showSearch,
|
||||||
showAccount,
|
|
||||||
showCart,
|
showCart,
|
||||||
sticky,
|
sticky,
|
||||||
tone,
|
tone,
|
||||||
|
bannerText,
|
||||||
|
bannerTone,
|
||||||
}: NavigationProps) {
|
}: NavigationProps) {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [cartOpen, setCartOpen] = useState(false);
|
const [cartOpen, setCartOpen] = useState(false);
|
||||||
@@ -35,12 +37,33 @@ export function Navigation({
|
|||||||
inverse: "bg-foreground text-background",
|
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 (
|
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
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full",
|
"w-full",
|
||||||
sticky === "yes" && "sticky top-0 z-40 backdrop-blur",
|
sticky === "yes" && "backdrop-blur",
|
||||||
toneClass[tone],
|
toneClass[tone],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -74,15 +97,6 @@ export function Navigation({
|
|||||||
<Search size={18} strokeWidth={1.5} />
|
<Search size={18} strokeWidth={1.5} />
|
||||||
</button>
|
</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" && (
|
{showCart === "yes" && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setCartOpen(true)}
|
onClick={() => setCartOpen(true)}
|
||||||
@@ -107,6 +121,7 @@ export function Navigation({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu */}
|
{/* Mobile menu */}
|
||||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Quote, ArrowLeft, ArrowRight } from "lucide-react";
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
import { Typography } from "@/editor/theme/Typography";
|
import { Typography } from "@/editor/theme/Typography";
|
||||||
|
|
||||||
export type TestimonialsProps = {
|
export type TestimonialsProps = {
|
||||||
@@ -30,16 +30,11 @@ export function Testimonials({ tagline, heading, items }: TestimonialsProps) {
|
|||||||
|
|
||||||
{item ? (
|
{item ? (
|
||||||
<figure className="mx-auto mt-12 flex max-w-2xl flex-col items-center">
|
<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
|
<blockquote
|
||||||
className="text-balance text-foreground"
|
className="text-balance text-foreground"
|
||||||
style={{ fontSize: "clamp(1.25rem, 2.4vw, 1.75rem)", lineHeight: 1.4 }}
|
style={{ fontSize: "clamp(1.25rem, 2.4vw, 1.75rem)", lineHeight: 1.4 }}
|
||||||
>
|
>
|
||||||
"{item.quote}"
|
{item.quote}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<figcaption className="mt-8 flex items-center gap-3">
|
<figcaption className="mt-8 flex items-center gap-3">
|
||||||
{item.avatar ? (
|
{item.avatar ? (
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ export const initialData: Record<string, UserData> = {
|
|||||||
],
|
],
|
||||||
cta: { label: "", href: "" },
|
cta: { label: "", href: "" },
|
||||||
showSearch: "yes",
|
showSearch: "yes",
|
||||||
showAccount: "yes",
|
|
||||||
showCart: "yes",
|
showCart: "yes",
|
||||||
sticky: "yes",
|
sticky: "yes",
|
||||||
tone: "default",
|
tone: "default",
|
||||||
|
bannerText: "",
|
||||||
|
bannerTone: "accent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user