@86d-app/products 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +65 -0
- package/COMPONENTS.md +231 -0
- package/README.md +201 -0
- package/package.json +46 -0
- package/src/__tests__/controllers.test.ts +2227 -0
- package/src/__tests__/state.test.ts +138 -0
- package/src/admin/components/categories-admin.mdx +3 -0
- package/src/admin/components/categories-admin.tsx +449 -0
- package/src/admin/components/category-form.mdx +9 -0
- package/src/admin/components/category-form.tsx +490 -0
- package/src/admin/components/category-list.mdx +75 -0
- package/src/admin/components/category-list.tsx +168 -0
- package/src/admin/components/collections-admin.mdx +3 -0
- package/src/admin/components/collections-admin.tsx +771 -0
- package/src/admin/components/index.tsx +8 -0
- package/src/admin/components/product-detail.mdx +12 -0
- package/src/admin/components/product-detail.tsx +790 -0
- package/src/admin/components/product-edit.tsx +60 -0
- package/src/admin/components/product-form.tsx +793 -0
- package/src/admin/components/product-list.mdx +3 -0
- package/src/admin/components/product-list.tsx +1125 -0
- package/src/admin/components/product-new.tsx +38 -0
- package/src/admin/endpoints/add-collection-product.ts +17 -0
- package/src/admin/endpoints/bulk-action.ts +43 -0
- package/src/admin/endpoints/create-category.ts +52 -0
- package/src/admin/endpoints/create-collection.ts +35 -0
- package/src/admin/endpoints/create-product.ts +50 -0
- package/src/admin/endpoints/create-variant.ts +45 -0
- package/src/admin/endpoints/delete-category.ts +27 -0
- package/src/admin/endpoints/delete-collection.ts +12 -0
- package/src/admin/endpoints/delete-product.ts +27 -0
- package/src/admin/endpoints/delete-variant.ts +27 -0
- package/src/admin/endpoints/get-product.ts +23 -0
- package/src/admin/endpoints/import-products.ts +47 -0
- package/src/admin/endpoints/index.ts +43 -0
- package/src/admin/endpoints/list-categories.ts +21 -0
- package/src/admin/endpoints/list-collections.ts +20 -0
- package/src/admin/endpoints/list-products.ts +25 -0
- package/src/admin/endpoints/remove-collection-product.ts +15 -0
- package/src/admin/endpoints/update-category.ts +82 -0
- package/src/admin/endpoints/update-collection.ts +22 -0
- package/src/admin/endpoints/update-product.ts +67 -0
- package/src/admin/endpoints/update-variant.ts +41 -0
- package/src/controllers.ts +1410 -0
- package/src/index.ts +120 -0
- package/src/markdown.ts +150 -0
- package/src/mdx.d.ts +5 -0
- package/src/schema.ts +352 -0
- package/src/state.ts +84 -0
- package/src/store/components/_hooks.ts +78 -0
- package/src/store/components/_types.ts +73 -0
- package/src/store/components/_utils.ts +14 -0
- package/src/store/components/back-in-stock-notify.tsx +97 -0
- package/src/store/components/collection-card.mdx +42 -0
- package/src/store/components/collection-card.tsx +12 -0
- package/src/store/components/collection-detail.mdx +12 -0
- package/src/store/components/collection-detail.tsx +149 -0
- package/src/store/components/collection-grid.mdx +9 -0
- package/src/store/components/collection-grid.tsx +80 -0
- package/src/store/components/featured-products.mdx +9 -0
- package/src/store/components/featured-products.tsx +75 -0
- package/src/store/components/filter-chip.mdx +25 -0
- package/src/store/components/filter-chip.tsx +12 -0
- package/src/store/components/index.tsx +39 -0
- package/src/store/components/product-card.mdx +69 -0
- package/src/store/components/product-card.tsx +71 -0
- package/src/store/components/product-detail.mdx +30 -0
- package/src/store/components/product-detail.tsx +488 -0
- package/src/store/components/product-listing.mdx +7 -0
- package/src/store/components/product-listing.tsx +423 -0
- package/src/store/components/product-reviews-section.mdx +21 -0
- package/src/store/components/product-reviews-section.tsx +372 -0
- package/src/store/components/recently-viewed.tsx +100 -0
- package/src/store/components/related-products.mdx +6 -0
- package/src/store/components/related-products.tsx +62 -0
- package/src/store/components/star-display.mdx +18 -0
- package/src/store/components/star-display.tsx +27 -0
- package/src/store/components/star-picker.mdx +21 -0
- package/src/store/components/star-picker.tsx +21 -0
- package/src/store/components/stock-badge.mdx +12 -0
- package/src/store/components/stock-badge.tsx +19 -0
- package/src/store/endpoints/get-category.ts +61 -0
- package/src/store/endpoints/get-collection.ts +46 -0
- package/src/store/endpoints/get-featured.ts +18 -0
- package/src/store/endpoints/get-product.ts +52 -0
- package/src/store/endpoints/get-related.ts +20 -0
- package/src/store/endpoints/index.ts +23 -0
- package/src/store/endpoints/list-categories.ts +13 -0
- package/src/store/endpoints/list-collections.ts +22 -0
- package/src/store/endpoints/list-products.ts +28 -0
- package/src/store/endpoints/search-products.ts +18 -0
- package/src/store/endpoints/store-search.ts +111 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import FilterChipTemplate from "./filter-chip.mdx";
|
|
4
|
+
|
|
5
|
+
export interface FilterChipProps {
|
|
6
|
+
label: string;
|
|
7
|
+
onRemove: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function FilterChip({ label, onRemove }: FilterChipProps) {
|
|
11
|
+
return <FilterChipTemplate label={label} onRemove={onRemove} />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { MDXComponents } from "mdx/types";
|
|
4
|
+
import type { CollectionCardData, Product } from "./_types";
|
|
5
|
+
import { CollectionCard } from "./collection-card";
|
|
6
|
+
import { CollectionDetail } from "./collection-detail";
|
|
7
|
+
import { CollectionGrid } from "./collection-grid";
|
|
8
|
+
import { FeaturedProducts } from "./featured-products";
|
|
9
|
+
import { FilterChip } from "./filter-chip";
|
|
10
|
+
import { ProductCard } from "./product-card";
|
|
11
|
+
import { ProductDetail } from "./product-detail";
|
|
12
|
+
import { ProductListing } from "./product-listing";
|
|
13
|
+
import { ProductReviewsSection } from "./product-reviews-section";
|
|
14
|
+
import { RecentlyViewedProducts } from "./recently-viewed";
|
|
15
|
+
import { RelatedProducts } from "./related-products";
|
|
16
|
+
import { StarDisplay } from "./star-display";
|
|
17
|
+
import { StarPicker } from "./star-picker";
|
|
18
|
+
import { StockBadge } from "./stock-badge";
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
ProductCard: ({ product }: { product: Product }) => (
|
|
22
|
+
<ProductCard product={product} />
|
|
23
|
+
),
|
|
24
|
+
FeaturedProducts,
|
|
25
|
+
ProductListing,
|
|
26
|
+
ProductDetail,
|
|
27
|
+
RelatedProducts,
|
|
28
|
+
CollectionCard: ({ collection }: { collection: CollectionCardData }) => (
|
|
29
|
+
<CollectionCard collection={collection} />
|
|
30
|
+
),
|
|
31
|
+
CollectionGrid,
|
|
32
|
+
CollectionDetail,
|
|
33
|
+
FilterChip,
|
|
34
|
+
StarDisplay,
|
|
35
|
+
StarPicker,
|
|
36
|
+
StockBadge,
|
|
37
|
+
ProductReviewsSection,
|
|
38
|
+
RecentlyViewedProducts,
|
|
39
|
+
} satisfies MDXComponents;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<a
|
|
2
|
+
href={`/products/${props.product.slug}`}
|
|
3
|
+
className="group relative flex flex-col"
|
|
4
|
+
>
|
|
5
|
+
<div className="relative aspect-[3/4] overflow-hidden rounded-lg bg-muted">
|
|
6
|
+
{props.image ? (
|
|
7
|
+
<img
|
|
8
|
+
src={props.image}
|
|
9
|
+
alt={props.product.name}
|
|
10
|
+
className="h-full w-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
|
11
|
+
/>
|
|
12
|
+
) : (
|
|
13
|
+
<div className="flex h-full w-full items-center justify-center text-muted-foreground/30">
|
|
14
|
+
<svg
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
width="32"
|
|
17
|
+
height="32"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
strokeWidth="1.5"
|
|
22
|
+
strokeLinecap="round"
|
|
23
|
+
strokeLinejoin="round"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
>
|
|
26
|
+
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
27
|
+
<circle cx="9" cy="9" r="2" />
|
|
28
|
+
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
{props.hasDiscount && (
|
|
33
|
+
<span className="absolute top-2.5 left-2.5 rounded-full bg-foreground px-2 py-0.5 font-medium text-background text-xs tabular-nums">
|
|
34
|
+
−{props.discountPct}%
|
|
35
|
+
</span>
|
|
36
|
+
)}
|
|
37
|
+
{props.product.inventory === 0 && (
|
|
38
|
+
<span className="absolute top-2.5 right-2.5 rounded-full bg-background/80 px-2 py-0.5 text-muted-foreground text-xs backdrop-blur-sm">
|
|
39
|
+
Sold out
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
{props.showAddToCart && (
|
|
43
|
+
<div className="absolute inset-x-2.5 bottom-2.5 translate-y-1 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={props.onAddToCart}
|
|
47
|
+
disabled={props.isAddingToCart}
|
|
48
|
+
className="w-full rounded-md bg-foreground/95 py-2 font-medium text-background text-xs backdrop-blur-sm transition-colors hover:bg-foreground disabled:opacity-50"
|
|
49
|
+
aria-label={`Add ${props.product.name} to cart`}
|
|
50
|
+
>
|
|
51
|
+
{props.isAddingToCart ? "Adding…" : "Add to cart"}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="mt-3 flex flex-col gap-0.5">
|
|
57
|
+
<p className="line-clamp-1 text-foreground text-sm">{props.product.name}</p>
|
|
58
|
+
<div className="flex items-baseline gap-1.5">
|
|
59
|
+
<span className="text-foreground text-sm tabular-nums">
|
|
60
|
+
{props.priceFormatted}
|
|
61
|
+
</span>
|
|
62
|
+
{props.compareAtPriceFormatted && (
|
|
63
|
+
<span className="text-muted-foreground/60 text-xs tabular-nums line-through">
|
|
64
|
+
{props.compareAtPriceFormatted}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</a>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useStoreContext } from "@86d-app/core/client";
|
|
4
|
+
import { memo } from "react";
|
|
5
|
+
import { useCartMutation, useTrack } from "./_hooks";
|
|
6
|
+
import type { Product } from "./_types";
|
|
7
|
+
import { formatPrice } from "./_utils";
|
|
8
|
+
import ProductCardTemplate from "./product-card.mdx";
|
|
9
|
+
|
|
10
|
+
export interface ProductCardProps {
|
|
11
|
+
product: Product;
|
|
12
|
+
showAddToCart?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ProductCard = memo(function ProductCard({
|
|
16
|
+
product,
|
|
17
|
+
showAddToCart = true,
|
|
18
|
+
}: ProductCardProps) {
|
|
19
|
+
const cartApi = useCartMutation();
|
|
20
|
+
const track = useTrack();
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: store context shape varies per app
|
|
22
|
+
const store = useStoreContext<{ cart: any }>();
|
|
23
|
+
|
|
24
|
+
const addToCartMutation = cartApi.addToCart.useMutation({
|
|
25
|
+
onSuccess: () => {
|
|
26
|
+
void cartApi.getCart.invalidate();
|
|
27
|
+
store.cart.openDrawer();
|
|
28
|
+
track({
|
|
29
|
+
type: "addToCart",
|
|
30
|
+
productId: product.id,
|
|
31
|
+
value: product.price,
|
|
32
|
+
data: { name: product.name, quantity: 1 },
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const image = product.images[0];
|
|
38
|
+
const hasDiscount =
|
|
39
|
+
product.compareAtPrice != null && product.compareAtPrice > product.price;
|
|
40
|
+
const discountPct = hasDiscount
|
|
41
|
+
? Math.round((1 - product.price / (product.compareAtPrice as number)) * 100)
|
|
42
|
+
: 0;
|
|
43
|
+
|
|
44
|
+
const handleAddToCart = (e: React.MouseEvent) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
addToCartMutation.mutate({
|
|
47
|
+
productId: product.id,
|
|
48
|
+
quantity: 1,
|
|
49
|
+
price: product.price,
|
|
50
|
+
productName: product.name,
|
|
51
|
+
productSlug: product.slug,
|
|
52
|
+
productImage: image,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ProductCardTemplate
|
|
58
|
+
product={product}
|
|
59
|
+
image={image}
|
|
60
|
+
showAddToCart={showAddToCart && product.inventory > 0}
|
|
61
|
+
hasDiscount={hasDiscount}
|
|
62
|
+
discountPct={discountPct}
|
|
63
|
+
priceFormatted={formatPrice(product.price)}
|
|
64
|
+
compareAtPriceFormatted={
|
|
65
|
+
hasDiscount ? formatPrice(product.compareAtPrice as number) : null
|
|
66
|
+
}
|
|
67
|
+
isAddingToCart={addToCartMutation.isPending}
|
|
68
|
+
onAddToCart={handleAddToCart}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div className="py-4 sm:py-6">
|
|
2
|
+
{/* Breadcrumbs */}
|
|
3
|
+
{props.breadcrumbs}
|
|
4
|
+
|
|
5
|
+
<div className="grid gap-8 lg:grid-cols-2 lg:gap-12">
|
|
6
|
+
{/* Image gallery */}
|
|
7
|
+
{props.imageGallery}
|
|
8
|
+
|
|
9
|
+
{/* Product info */}
|
|
10
|
+
<div className="flex flex-col gap-4 lg:sticky lg:top-20 lg:self-start">
|
|
11
|
+
{props.categoryLink}
|
|
12
|
+
<h1 className="font-display font-semibold text-foreground text-xl tracking-tight sm:text-2xl">
|
|
13
|
+
{props.name}
|
|
14
|
+
</h1>
|
|
15
|
+
{props.reviewSummaryLink}
|
|
16
|
+
{props.priceBlock}
|
|
17
|
+
{props.stockBadge}
|
|
18
|
+
{props.shortDescription}
|
|
19
|
+
{props.variantSelector}
|
|
20
|
+
{props.addToCartBlock}
|
|
21
|
+
{props.outOfStockNotice}
|
|
22
|
+
{props.descriptionBlock}
|
|
23
|
+
{props.tagsBlock}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{props.reviewsSection}
|
|
28
|
+
{props.relatedProducts}
|
|
29
|
+
{props.recentlyViewed}
|
|
30
|
+
</div>
|