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
190 changes: 8 additions & 182 deletions app/(shop)/[...category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,75 +85,16 @@ type CategoryOrProductProps = {
searchParams: Promise<SearchParams>;
};

export async function generateMetadata(props: CategoryOrProductProps): Promise<Metadata> {
const searchParams = await props.searchParams;
const params = await props.params;
const url = `/${params.category.join('/')}`;
const { page, priceRange, sort = 'popular', parentPath, inStock } = await props.searchParams;
const currentPage = Number(page ?? 1);
const limit = ITEMS_PER_PAGE;
const skip = currentPage ? (currentPage - 1) * limit : 0;
const itemShape = await fetchItemShape(url);

if (itemShape === 'product') {
const { meta, variants } = await fetchProductData({ path: url });
const currentVariant = findSuitableVariant({ variants: variants, searchParams });
const title = currentVariant?.name ?? '';
const description = meta?.description?.[0]?.textContent;
const image = currentVariant?.images?.[0];
const ogImage = image?.ogVariants?.[0];
const attributesQueryParams = new URLSearchParams(currentVariant?.attributes ?? {});

return {
title: `${title}`,
description,
openGraph: {
title: `${title} | Furnitut`,
description,
url: `${url}?${attributesQueryParams.toString()}`,
images: [
{
url: ogImage?.url ?? '',
alt: image?.altText ?? '',
height: ogImage?.height ?? 0,
width: ogImage?.width ?? 0,
},
],
},
};
}

const { meta, name } = await searchCategory({
path: url,
limit,
skip,
filters: buildFilterCriteria({ priceRange, parentPath, inStock: !!inStock }),
sorting: SORTING_CONFIGS[sort] as TenantSort,
});
const { title, description, image } = meta ?? {};

return {
title: title || name,
description: description?.[0]?.textContent ?? '',
openGraph: {
title: `${title} | Furnitut`,
description: description?.[0]?.textContent ?? '',
url: `/${url}`,
images: [
{
url: image?.[0]?.url ?? '',
alt: image?.[0]?.altText ?? '',
height: image?.[0]?.height ?? 0,
width: image?.[0]?.width ?? 0,
},
],
},
};
export async function generateStaticParams() {
return [{ category: ['products', 'plants', 'golden-pothos'] }];
}

export const revalidate = 3600;

export default async function CategoryOrProduct(props: CategoryOrProductProps) {
const params = await props.params;
const { page, priceRange, sort = 'popular', parentPath, preview, inStock } = await props.searchParams;
const searchParams = await props.searchParams;
const { page, priceRange, sort = 'popular', parentPath, preview, inStock } = searchParams;
const currentPage = Number(page ?? 1);
const limit = ITEMS_PER_PAGE;
const skip = currentPage ? (currentPage - 1) * limit : 0;
Expand All @@ -166,123 +107,8 @@ export default async function CategoryOrProduct(props: CategoryOrProductProps) {
}

if (itemShape === 'product') {
return <ProductPage params={props.params} searchParams={props.searchParams} />;
return <ProductPage params={params} searchParams={searchParams} />;
}

const { breadcrumbs, name, categories, blocks, products, summary } = await searchCategory({
path,
limit,
skip,
filters: buildFilterCriteria({ priceRange, parentPath, inStock: !!inStock }),
sorting: SORTING_CONFIGS[sort] as TenantSort,
isPreview: !!preview,
});
const { totalHits, hasPreviousHits, hasMoreHits, price, parentPaths } = summary ?? {};

const priceCounts = Object.values(price) as { count: number }[];

const pairs = createAdjacentPairs(
path.includes('entertainment') ? ENTERTAINMENT_PRICE_RANGE : PRODUCTS_PRICE_RANGE,
);

const priceRangeOptions: FilterOption[] = pairs.map((pair, index) => ({
value: pair.value,
label: pair.label,
count: priceCounts[index].count,
checked: isChecked({ filterValue: priceRange, value: pair.value }),
}));

const paths: FilterOption[] = Object.entries(parentPaths as ParentPathFacet)
.map(([key, value]) => ({
value: key,
label: value.label.split(' > ').at(-1) as string,
count: value.count,
checked: isChecked({
filterValue: parentPath,
value: key,
}),
}))
.sort((a, b) => a.label.localeCompare(b.label));

return (
<main>
<div>
<div className="page pb-2 ">
<Breadcrumbs breadcrumbs={breadcrumbs} />
<h1 className="text-6xl font-bold py-4 ">{name}</h1>
</div>
{/* Categories List */}
<div className={classNames('flex flex-wrap mx-auto gap-2 max-w-(--breakpoint-2xl) empty:pb-0 pb-4 ')}>
{categories?.map((child) => {
if (!child) {
return null;
}

return (
<Link
className={classNames(
'group w-28 pt-2 text-center text-dark divide divide-black divide-solid hover:border-dark transition-all',
'bg-light border-muted border border-solid rounded-lg flex flex-col gap-1 justify-start items-center',
)}
href={(child as Category).path ?? '#'}
key={(child as Category).id}
>
<div className="w-24 h-24 text-center rounded-lg overflow-hidden border border-muted relative ">
{(child as Category).image?.map((img) => {
return <Image {...img} key={img?.url} sizes="200px" />;
})}
</div>
<span className="py-2 text-sm text-wrap max-w-full">{(child as Category).name}</span>
</Link>
);
})}
</div>
</div>

{/* Blocks */}
{blocks && (
<div className={classNames('flex flex-col items-center')}>
<Blocks blocks={blocks} />
</div>
)}

{/* Products List */}
<div
className={classNames(
'grid mt-2 grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 lg:gap-4 max-w-(--breakpoint-2xl) mx-auto mb-8 relative',
)}
>
<div className="col-span-1 sm:col-span-2 md:col-span-3 lg:col-span-4 pb-4 mt-4">
{/* Filters */}
<Suspense fallback={null}>
<Filters
priceRange={priceRangeOptions}
sorting={sort}
totalHits={totalHits ?? 0}
paths={paths}
inStock={!!inStock}
/>
</Suspense>
</div>
{products?.map((child) => {
if (!child) {
return null;
}

return <Product key={(child as ProductShape).id} product={child} />;
})}
</div>

{/* Pagination */}
{totalHits && totalHits > ITEMS_PER_PAGE && (
<Pagination
totalItems={totalHits ?? 0}
currentPage={currentPage}
hasPreviousPage={hasPreviousHits ?? false}
hasNextPage={hasMoreHits ?? false}
url={path}
/>
)}
</main>
);
return null;
}
36 changes: 7 additions & 29 deletions components/ProductPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ import downloadIcon from '@/assets/icon-download.svg';
import Image from 'next/image';
import { getPrice } from '@/utils/price';
import classNames from 'classnames';
import { ProductPrice } from './product-price';

import { getCustomerPrices } from './get-customer-prices';
import { getTranslations } from 'next-intl/server';
import { getPromotionValues } from '@/utils/topics';
import { Suspense } from 'react';

const { CRYSTALLIZE_FALLBACK_PRICE, CRYSTALLIZE_SELECTED_PRICE, CRYSTALLIZE_COMPARE_AT_PRICE } = process.env;

type ProductsProps = {
searchParams: Promise<SearchParams>;
params: Promise<{ slug: string; category: string[] }>;
searchParams: SearchParams;
params: { slug: string; category: string[] };
};

export const fetchProductData = async ({ path, isPreview = false }: { path: string; isPreview?: boolean }) => {
Expand All @@ -55,9 +57,7 @@ export const fetchProductData = async ({ path, isPreview = false }: { path: stri

export default async function CategoryProduct(props: ProductsProps) {
const t = await getTranslations('Product');

const searchParams = await props.searchParams;
const params = await props.params;
const { searchParams, params } = props;
const url = `/${params.category.join('/')}`;
const product = await fetchProductData({ path: url, isPreview: !!searchParams.preview });
const currentVariant = findSuitableVariant({ variants: product.variants, searchParams });
Expand Down Expand Up @@ -316,30 +316,8 @@ export default async function CategoryProduct(props: ProductsProps) {
)}

<div className="text-4xl flex items-center font-bold py-4 justify-between w-full">
<div className="flex flex-col">
{/* Lowest price */}
<span>
<Price
price={{
price: currentVariantPrice.lowest,
currency: currentVariantPrice.currency,
}}
/>
</span>
{/* Compared at price */}
<span
className={classNames('block text-sm line-through font-normal', {
hidden: !currentVariantPrice.hasBestPrice,
})}
>
<Price
price={{
price: currentVariantPrice.highest,
currency: currentVariantPrice.currency,
}}
/>
</span>
</div>
{new Date().toISOString()}
<Suspense fallback={<div className="bg-red w-10 h-10" />}></Suspense>

{!!currentVariant && !!currentVariant.sku && (
<AddToCartButton
Expand Down
68 changes: 68 additions & 0 deletions components/ProductPage/new-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { print } from 'graphql';

import { TypedDocumentNode } from '@graphql-typed-document-node/core';

export const camelCaseHyphens = (string: string): string => string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());

const normalizeForGraphQLName = (name: string): string => {
const startsWithValidChars = /^[_a-zA-Z]/;
if (!startsWithValidChars.test(name)) {
return normalizeForGraphQLName(`_${name}`);
}
const validChars = /^[_a-zA-Z0-9]+$/;
if (!validChars.test(name)) {
const chars = name.split('');
const replacedChars = chars.map((char) => {
if (validChars.test(char)) {
return char;
} else {
return '_';
}
});
return replacedChars.join('');
}
return name;
};

export const normalizeForGraphQL = (string: string): string => normalizeForGraphQLName(camelCaseHyphens(string));

const apiEndpoint = `https://api.crystallize.com/${process.env.NEXT_PUBLIC_CRYSTALLIZE_TENANT_IDENTIFIER}/discovery`;

const apiLanguage: string = normalizeForGraphQL(process.env.NEXT_PUBLIC_CRYSTALLIZE_TENANT_LANGUAGE || 'en');

const selectedPrice: string = process.env.CRYSTALLIZE_SELECTED_PRICE || 'default';
const fallbackPrice: string = process.env.CRYSTALLIZE_FALLBACK_PRICE || 'default';

export const newFetch = async <TResult, TVariables = {}>(
query: TypedDocumentNode<TResult, TVariables>,
...[variables]: TVariables extends Record<string, never> ? [] : [TVariables]
) => {
const response = await fetch(apiEndpoint, {
method: 'POST',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: print(query),
variables: {
...variables,
language: apiLanguage,
selectedPriceVariant: selectedPrice,
fallbackPriceVariant: fallbackPrice,
},
}),
});

if (!response.ok) {
throw new Error(response.statusText);
}

const result = await response.json();

if ('errors' in result) {
throw new Error();
}

return result as { data: TResult };
};
Loading