Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,4 @@ Potential future additions to version system:
## Questions?

For questions about the versioning system, see the About page or contact via the FAQ section.
| STS-3.0.1 | 2026-07-01 | Variant groups count & list as one item across all pages (shared lib/variants.ts helper) |
5 changes: 3 additions & 2 deletions app/collections/CollectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from 'next/link';
import { useEffect, useState, useMemo } from 'react';
import FadeImage from '@/components/FadeImage';
import { countDistinctProducts } from '@/lib/variants';

interface CollectionCardProps {
collection: {
Expand Down Expand Up @@ -92,8 +93,8 @@ export default function CollectionCard({ collection, design, tick }: CollectionC
fontFamily: design.fonts.bodyFont,
}}
>
{collection.products.length}{' '}
{collection.products.length === 1 ? 'product' : 'products'}
{countDistinctProducts(collection.products)}{' '}
{countDistinctProducts(collection.products) === 1 ? 'product' : 'products'}
</p>
<div className="mt-4">
<span
Expand Down
8 changes: 3 additions & 5 deletions app/collections/[collectionId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link';
import { notFound } from 'next/navigation';
import { useEffect, useState } from 'react';
import FadeImage from '@/components/FadeImage';
import { dedupeVariantGroups } from '@/lib/variants';

type SortOption = 'name-asc' | 'name-desc' | 'price-asc' | 'price-desc' | 'units-asc' | 'units-desc' | 'total-asc' | 'total-desc';

Expand Down Expand Up @@ -113,11 +114,8 @@ export default function CollectionPage({ params }: { params: Promise<{ collectio
}
});

// One card per variant group — keep only the first variant (index 0 in sorted variants array)
const displayProducts = sortedProducts.filter((product: Product) => {
if (!product.variants || product.variants.length === 0) return true;
return product.variants[0].id === product.id;
});
// One card per variant group (shared helper — see lib/variants.ts)
const displayProducts = dedupeVariantGroups(sortedProducts);

return (
<div>
Expand Down
13 changes: 8 additions & 5 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import Link from 'next/link';
import { useEffect, useState } from 'react';
import FadeImage from '@/components/FadeImage';
import VariantChips from '@/components/VariantChips';
import { countDistinctProducts, dedupeVariantGroups, variantDisplayName } from '@/lib/variants';

export default function Home() {
const [design, setDesign] = useState<any>(null);
Expand Down Expand Up @@ -33,7 +35,7 @@ export default function Home() {
products.push(product);
});
});
setAllProducts(products);
setAllProducts(dedupeVariantGroups(products));

try {
const inventoryResponse = await fetch('/api/inventory');
Expand Down Expand Up @@ -215,7 +217,7 @@ export default function Home() {
<div className="aspect-square bg-gray-100 relative border-b" style={{ borderColor: design.colors.border }}>
<FadeImage
src={product.images[0]}
alt={product.name}
alt={variantDisplayName(product)}
className="w-full h-full object-contain p-4"
/>
</div>
Expand All @@ -231,8 +233,9 @@ export default function Home() {
className="font-bold text-lg mb-1"
style={{ color: design.colors.primary, fontFamily: design.fonts.titleFont }}
>
{product.name}
{variantDisplayName(product)}
</h3>
<VariantChips product={product} design={design} stockMap={stockMap} />
<p className="text-sm mb-2" style={{ color: design.colors.textLight, fontFamily: design.fonts.bodyFont }}>
SKU: {product.sku}
</p>
Expand Down Expand Up @@ -299,8 +302,8 @@ export default function Home() {
fontFamily: design.fonts.bodyFont,
}}
>
{collection.products.length}{' '}
{collection.products.length === 1 ? 'product' : 'products'}
{countDistinctProducts(collection.products)}{' '}
{countDistinctProducts(collection.products) === 1 ? 'product' : 'products'}
</p>
<div className="mt-4">
<span
Expand Down
9 changes: 6 additions & 3 deletions app/shop-all/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import Link from 'next/link';
import { useEffect, useState } from 'react';
import FadeImage from '@/components/FadeImage';
import VariantChips from '@/components/VariantChips';
import { dedupeVariantGroups, variantDisplayName } from '@/lib/variants';

type SortOption = 'name-asc' | 'name-desc' | 'price-asc' | 'price-desc' | 'units-asc' | 'units-desc' | 'total-asc' | 'total-desc';

Expand Down Expand Up @@ -42,7 +44,7 @@ export default function ShopAllPage() {
});
});

setProducts(allProducts);
setProducts(dedupeVariantGroups(allProducts));
}
loadData();
}, []);
Expand Down Expand Up @@ -151,7 +153,7 @@ export default function ShopAllPage() {
<div className="aspect-square bg-gray-100 relative border-b" style={{ borderColor: design.colors.border }}>
<FadeImage
src={product.images[0]}
alt={product.name}
alt={variantDisplayName(product)}
className="w-full h-full object-contain p-4"
/>
</div>
Expand Down Expand Up @@ -181,8 +183,9 @@ export default function ShopAllPage() {
fontFamily: design.fonts.titleFont,
}}
>
{product.name}
{variantDisplayName(product)}
</h3>
<VariantChips product={product} design={design} stockMap={stockMap} />
<p
className="text-xs mb-2 italic"
style={{
Expand Down
59 changes: 59 additions & 0 deletions components/VariantChips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ReactElement } from 'react';

interface Variant { id: string; values: string[] }
interface ChipProduct {
variants?: Variant[];
variantDimensions?: string[];
variantValues?: string[];
}

// Renders first-dimension variant values (e.g. sizes/colors) as chips beneath a
// product card, matching the collection detail page. Returns null for
// non-variant products. Shared so every listing renders variants identically.
export default function VariantChips({
product,
design,
stockMap = {},
}: {
product: ChipProduct;
design: any;
stockMap?: Record<string, number>;
}): ReactElement | null {
if (!product.variants || product.variants.length === 0) return null;
const seen = new Set<string>();
const uniqueValues = product.variants
.map((v) => v.values[0])
.filter((val) => val && !seen.has(val) && !!seen.add(val));

return (
<div className="mb-2">
<p className="text-xs mb-1" style={{ color: design.colors.textLight, fontFamily: design.fonts.bodyFont }}>
{product.variantDimensions?.[0] ?? 'Options'}:
</p>
<div className="flex flex-wrap gap-1">
{uniqueValues.map((value) => {
const withValue = product.variants!.filter((v) => v.values[0] === value);
const allOOS = withValue.length > 0 && withValue.every(
(v) => stockMap[v.id] !== undefined && stockMap[v.id] <= 0
);
const isActive = product.variantValues?.[0] === value;
return (
<span
key={value}
className="px-2 py-0.5 text-xs border rounded-full"
style={{
borderColor: isActive ? design.colors.secondary : design.colors.border,
color: isActive ? design.colors.secondary : allOOS ? design.colors.border : design.colors.textLight,
opacity: allOOS ? 0.45 : 1,
textDecoration: allOOS ? 'line-through' : 'none',
fontFamily: design.fonts.bodyFont,
}}
>
{value}
</span>
);
})}
</div>
</div>
);
}
31 changes: 31 additions & 0 deletions lib/variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// lib/variants.ts — Variant grouping helpers (STS-3.0.0+)
//
// A "variant group" is 2+ sibling products that share a base name (e.g. the same
// product in several sizes or colors). The catalog engine stamps a `variants`
// array onto every member of a group. These helpers collapse a group down to a
// single representative product so that product LISTINGS and product COUNTS treat
// one variant set as one item (instead of one per size/color).

export interface VariantLike {
id: string;
variants?: { id: string }[];
}

// Returns one representative per variant group (the first variant in the group),
// leaving non-variant products untouched. Input order is preserved.
export function dedupeVariantGroups<T extends VariantLike>(products: T[]): T[] {
return products.filter(
(p) => !p.variants || p.variants.length === 0 || p.variants[0].id === p.id
);
}

// Count of distinct products, treating each variant group as a single item.
export function countDistinctProducts(products: VariantLike[]): number {
return dedupeVariantGroups(products).length;
}

// Display name for a card: the variant group base name (e.g. "Branded Sneakers")
// rather than the folder name of one variant ("Branded Sneakers (10)").
export function variantDisplayName(product: { name: string; variantGroup?: string }): string {
return product.variantGroup || product.name;
}
2 changes: 1 addition & 1 deletion lib/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* 4. Commit with version number in commit message
*/

export const VERSION = 'STS-2.6.3';
export const VERSION = 'STS-3.0.1';

export const VERSION_INFO = {
name: 'Shop Template System',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shop-template-system",
"version": "3.0.0",
"version": "3.0.1",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down