mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 05:56:59 +00:00
Merge branch 'poc/react-nextjs' into feat/cart
This commit is contained in:
commit
59b9a9d1b7
@ -4,3 +4,4 @@ SITE_NAME="Next.js Commerce with Shopware Composable Frontends"
|
|||||||
SHOPWARE_STORE_DOMAIN=""
|
SHOPWARE_STORE_DOMAIN=""
|
||||||
SHOPWARE_API_TYPE="store-api"
|
SHOPWARE_API_TYPE="store-api"
|
||||||
SHOPWARE_ACCESS_TOKEN=""
|
SHOPWARE_ACCESS_TOKEN=""
|
||||||
|
SHOPWARE_USE_SEO_URLS="false"
|
||||||
|
0
.husky/pre-commit
Normal file → Executable file
0
.husky/pre-commit
Normal file → Executable file
@ -8,11 +8,7 @@ export const runtime = 'edge';
|
|||||||
|
|
||||||
export const revalidate = 43200; // 12 hours in seconds
|
export const revalidate = 43200; // 12 hours in seconds
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: { params: { cms: string } }): Promise<Metadata> {
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: { cms: string };
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
const page = await getPage(params.cms);
|
const page = await getPage(params.cms);
|
||||||
|
|
||||||
if (!page) return notFound();
|
if (!page) return notFound();
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { getCollection, getCollectionProducts } from 'lib/shopware';
|
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import Grid from 'components/grid';
|
import Grid from 'components/grid';
|
||||||
|
import Collections from 'components/layout/search/collections';
|
||||||
|
import FilterList from 'components/layout/search/filter';
|
||||||
import ProductGridItems from 'components/layout/product-grid-items';
|
import ProductGridItems from 'components/layout/product-grid-items';
|
||||||
import Pagination from 'components/collection/pagination';
|
import Pagination from 'components/collection/pagination';
|
||||||
|
|
||||||
|
import { getCollection, getCollectionProducts } from 'lib/shopware';
|
||||||
import { defaultSort, sorting } from 'lib/constants';
|
import { defaultSort, sorting } from 'lib/constants';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
@ -47,14 +50,26 @@ export default async function CategoryPage({
|
|||||||
{products.length === 0 ? (
|
{products.length === 0 ? (
|
||||||
<p className="py-3 text-lg">{`No products found in this collection`}</p>
|
<p className="py-3 text-lg">{`No products found in this collection`}</p>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="mx-auto flex max-w-7xl flex-col bg-white py-6 text-black dark:bg-black dark:text-white md:flex-row">
|
||||||
|
<div className="order-first flex-none md:w-1/6">
|
||||||
|
<Collections collection={params.collection} />
|
||||||
|
</div>
|
||||||
|
<div className="order-last min-h-screen w-full md:order-none">
|
||||||
<Grid className="grid-cols-2 lg:grid-cols-3">
|
<Grid className="grid-cols-2 lg:grid-cols-3">
|
||||||
<ProductGridItems products={products} />
|
<ProductGridItems products={products} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<nav aria-label="Collection pagination" className='block sm:flex items-center'>
|
<nav aria-label="Collection pagination" className="block items-center sm:flex">
|
||||||
<Pagination itemsPerPage={limit} itemsTotal={total} currentPage={page ? parseInt(page) - 1 : 0} />
|
<Pagination
|
||||||
|
itemsPerPage={limit}
|
||||||
|
itemsTotal={total}
|
||||||
|
currentPage={page ? parseInt(page) - 1 : 0}
|
||||||
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="order-none md:order-last md:w-1/6 md:flex-none">
|
||||||
|
<FilterList list={sorting} title="Sort by" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
import Footer from 'components/layout/footer';
|
import Footer from 'components/layout/footer';
|
||||||
import Collections from 'components/layout/search/collections';
|
|
||||||
import FilterList from 'components/layout/search/filter';
|
|
||||||
import { sorting } from 'lib/constants';
|
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
// @ToDo: We could use dynamic Layout per page, see https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#with-typescript
|
||||||
export default function SearchLayout({ children }: { children: React.ReactNode }) {
|
export default function SearchLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<div className="mx-auto flex max-w-7xl flex-col bg-white py-6 text-black dark:bg-black dark:text-white md:flex-row">
|
{children}
|
||||||
<div className="order-first flex-none md:w-1/6">
|
|
||||||
<Collections />
|
|
||||||
</div>
|
|
||||||
<div className="order-last min-h-screen w-full md:order-none">{children}</div>
|
|
||||||
<div className="order-none md:order-last md:w-1/6 md:flex-none">
|
|
||||||
<FilterList list={sorting} title="Sort by" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
import Grid from 'components/grid';
|
import Grid from 'components/grid';
|
||||||
|
import FilterList from 'components/layout/search/filter';
|
||||||
|
import { sorting } from 'lib/constants';
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col bg-white py-6 text-black dark:bg-black dark:text-white md:flex-row">
|
||||||
|
<div className="order-first flex-none md:w-1/6"></div>
|
||||||
|
<div className="order-last min-h-screen w-full md:order-none">
|
||||||
<Grid className="grid-cols-2 lg:grid-cols-3">
|
<Grid className="grid-cols-2 lg:grid-cols-3">
|
||||||
{Array(12)
|
{Array(12)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, index) => {
|
.map((_, index) => {
|
||||||
return <Grid.Item key={index} className="animate-pulse bg-gray-100 dark:bg-gray-900" />;
|
return (
|
||||||
|
<Grid.Item key={index} className="animate-pulse bg-gray-100 dark:bg-gray-900" />
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<div className="order-none md:order-last md:w-1/6 md:flex-none">
|
||||||
|
<FilterList list={sorting} title="Sort by" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Grid from 'components/grid';
|
import Grid from 'components/grid';
|
||||||
import ProductGridItems from 'components/layout/product-grid-items';
|
import ProductGridItems from 'components/layout/product-grid-items';
|
||||||
|
import FilterList from 'components/layout/search/filter';
|
||||||
import { defaultSort, sorting } from 'lib/constants';
|
import { defaultSort, sorting } from 'lib/constants';
|
||||||
import { getSearchCollectionProducts } from 'lib/shopware';
|
import { getSearchCollectionProducts } from 'lib/shopware';
|
||||||
|
|
||||||
@ -23,18 +24,32 @@ export default async function SearchPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{searchValue && products.length === 0 ? (
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col bg-white py-6 text-black dark:bg-black dark:text-white md:flex-row">
|
||||||
|
<p>
|
||||||
|
{'There are no products that match '}
|
||||||
|
<span className="font-bold">"{searchValue}"</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{products.length > 0 ? (
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col bg-white py-6 text-black dark:bg-black dark:text-white md:flex-row">
|
||||||
|
<div className="order-first flex-none md:w-1/6">
|
||||||
{searchValue ? (
|
{searchValue ? (
|
||||||
<p>
|
<p>
|
||||||
{products.length === 0
|
{`Showing ${products.length} ${resultsText} for `}
|
||||||
? 'There are no products that match '
|
|
||||||
: `Showing ${products.length} ${resultsText} for `}
|
|
||||||
<span className="font-bold">"{searchValue}"</span>
|
<span className="font-bold">"{searchValue}"</span>
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{products.length > 0 ? (
|
<p className="pt-4">Good place to add other suggest search terms ;)</p>
|
||||||
|
</div>
|
||||||
<Grid className="grid-cols-2 lg:grid-cols-3">
|
<Grid className="grid-cols-2 lg:grid-cols-3">
|
||||||
<ProductGridItems products={products} />
|
<ProductGridItems products={products} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<div className="order-none md:order-last md:w-1/6 md:flex-none">
|
||||||
|
<FilterList list={sorting} title="Sort by" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -19,9 +19,7 @@ export default async function sitemap(): Promise<Promise<Promise<MetadataRoute.S
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchedRoutes = (
|
const fetchedRoutes = (await Promise.all([productsPromise])).flat();
|
||||||
await Promise.all([productsPromise])
|
|
||||||
).flat();
|
|
||||||
|
|
||||||
return [...routesMap, ...fetchedRoutes];
|
return [...routesMap, ...fetchedRoutes];
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,44 @@
|
|||||||
import { getCollectionProducts } from 'lib/shopware';
|
import { getCollectionProducts } from 'lib/shopware';
|
||||||
|
import { isSeoUrls } from 'lib/shopware/helpers';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export async function Carousel() {
|
export async function Carousel() {
|
||||||
// Collections that start with `hidden-*` are hidden from the search page.
|
const collectionName = isSeoUrls()
|
||||||
// const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' });
|
? 'Summer-BBQ/Hidden-Carousel-Category'
|
||||||
|
: 'ff7bf3c59f1342a685844fbf8fdf9dc8';
|
||||||
|
const { products } = await getCollectionProducts({
|
||||||
|
collection: collectionName
|
||||||
|
});
|
||||||
|
|
||||||
// if (!products?.length) return null;
|
if (!products?.length) return null;
|
||||||
|
|
||||||
return null;
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden bg-white dark:bg-black">
|
||||||
// return (
|
<div className="flex animate-carousel">
|
||||||
// <div className="relative w-full overflow-hidden bg-black dark:bg-white">
|
{[...products, ...products].map((product, i) => (
|
||||||
// <div className="flex animate-carousel">
|
<Link
|
||||||
// {[...products, ...products].map((product, i) => (
|
key={`${product.path}${i}`}
|
||||||
// <Link
|
href={`/product/${product.path}`}
|
||||||
// key={`${product.handle}${i}`}
|
className="relative h-[30vh] w-1/2 flex-none md:w-1/3"
|
||||||
// href={`/product/${product.handle}`}
|
>
|
||||||
// className="relative h-[30vh] w-1/2 flex-none md:w-1/3"
|
{product.featuredImage ? (
|
||||||
// >
|
<Image
|
||||||
// {product.featuredImage ? (
|
alt={product.title}
|
||||||
// <Image
|
className="h-full object-contain"
|
||||||
// alt={product.title}
|
fill
|
||||||
// className="h-full object-contain"
|
sizes="33vw"
|
||||||
// fill
|
src={product.featuredImage.url}
|
||||||
// sizes="33vw"
|
/>
|
||||||
// src={product.featuredImage.url}
|
) : null}
|
||||||
// />
|
<div className="absolute inset-y-0 right-0 flex items-center justify-center">
|
||||||
// ) : null}
|
<div className="inline-flex bg-white p-4 text-xl font-semibold text-black dark:bg-black dark:text-white">
|
||||||
// <div className="absolute inset-y-0 right-0 flex items-center justify-center">
|
{product.title}
|
||||||
// <div className="inline-flex bg-white p-4 text-xl font-semibold text-black dark:bg-black dark:text-white">
|
</div>
|
||||||
// {product.title}
|
</div>
|
||||||
// </div>
|
</Link>
|
||||||
// </div>
|
))}
|
||||||
// </Link>
|
</div>
|
||||||
// ))}
|
</div>
|
||||||
// </div>
|
);
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
@ -59,4 +59,3 @@ export const updateItemQuantity = async ({
|
|||||||
return new Error('Error updating item quantity', { cause: e });
|
return new Error('Error updating item quantity', { cause: e });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,17 +6,17 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
|||||||
import { useEffect, useState, useTransition } from 'react';
|
import { useEffect, useState, useTransition } from 'react';
|
||||||
|
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import { ProductVariant } from 'lib/shopify/types';
|
|
||||||
import { Product } from 'lib/shopware/types';
|
import { Product } from 'lib/shopware/types';
|
||||||
|
import { ProductVariant } from 'lib/shopware/types';
|
||||||
|
|
||||||
export function AddToCart({
|
export function AddToCart({
|
||||||
product,
|
product,
|
||||||
variants,
|
variants,
|
||||||
availableForSale,
|
availableForSale
|
||||||
}: {
|
}: {
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
availableForSale: boolean;
|
availableForSale: boolean;
|
||||||
product: Product
|
product: Product;
|
||||||
}) {
|
}) {
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState(variants[0]?.id);
|
const [selectedVariantId, setSelectedVariantId] = useState(variants[0]?.id);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -3,7 +3,7 @@ import LoadingDots from 'components/loading-dots';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { CartItem } from 'lib/shopware/types';
|
||||||
import { useTransition } from 'react';
|
import { useTransition } from 'react';
|
||||||
import { removeItem } from 'components/cart/actions';
|
import { removeItem } from 'components/cart/actions';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { removeItem, updateItemQuantity } from 'components/cart/actions';
|
|||||||
import MinusIcon from 'components/icons/minus';
|
import MinusIcon from 'components/icons/minus';
|
||||||
import PlusIcon from 'components/icons/plus';
|
import PlusIcon from 'components/icons/plus';
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { CartItem } from 'lib/shopware/types';
|
||||||
|
|
||||||
export default function EditItemQuantityButton({
|
export default function EditItemQuantityButton({
|
||||||
item,
|
item,
|
||||||
|
@ -9,6 +9,10 @@ export default async function Cart() {
|
|||||||
let cartIdUpdated = false;
|
let cartIdUpdated = false;
|
||||||
const cart = await getCart(cartId);
|
const cart = await getCart(cartId);
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (cartId !== cart.id) {
|
if (cartId !== cart.id) {
|
||||||
cartIdUpdated = true;
|
cartIdUpdated = true;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import CloseIcon from 'components/icons/close';
|
|||||||
import ShoppingBagIcon from 'components/icons/shopping-bag';
|
import ShoppingBagIcon from 'components/icons/shopping-bag';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import { DEFAULT_OPTION } from 'lib/constants';
|
import { DEFAULT_OPTION } from 'lib/constants';
|
||||||
import type { Cart } from 'lib/shopify/types';
|
import type { Cart } from 'lib/shopware/types';
|
||||||
import { createUrl } from 'lib/utils';
|
import { createUrl } from 'lib/utils';
|
||||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
import { useCookies } from 'react-cookie';
|
import { useCookies } from 'react-cookie';
|
||||||
@ -110,7 +110,7 @@ export default function CartModal({ cart, cartIdUpdated }: { cart: Cart; cartIdU
|
|||||||
});
|
});
|
||||||
|
|
||||||
const merchandiseUrl = createUrl(
|
const merchandiseUrl = createUrl(
|
||||||
`/product/${item.merchandise.product.handle}`,
|
`/product/${item.merchandise.product.path}`,
|
||||||
new URLSearchParams(merchandiseSearchParams)
|
new URLSearchParams(merchandiseSearchParams)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import ReactPaginate from 'react-paginate';
|
import ReactPaginate from 'react-paginate';
|
||||||
import { createUrl } from 'lib/utils';
|
import { createUrl } from 'lib/utils';
|
||||||
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
|
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function Pagination({ itemsPerPage, itemsTotal, currentPage }: { itemsPerPage: number, itemsTotal: number, currentPage: number }) {
|
export default function Pagination({
|
||||||
|
itemsPerPage,
|
||||||
|
itemsTotal,
|
||||||
|
currentPage
|
||||||
|
}: {
|
||||||
|
itemsPerPage: number;
|
||||||
|
itemsTotal: number;
|
||||||
|
currentPage: number;
|
||||||
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const currentParams = useSearchParams();
|
const currentParams = useSearchParams();
|
||||||
@ -12,20 +20,26 @@ export default function Pagination({ itemsPerPage, itemsTotal, currentPage }: {
|
|||||||
const sort = currentParams.get('sort');
|
const sort = currentParams.get('sort');
|
||||||
const pageCount = Math.ceil(itemsTotal / itemsPerPage);
|
const pageCount = Math.ceil(itemsTotal / itemsPerPage);
|
||||||
|
|
||||||
// Invoke when user click to request another page.
|
// Invoke when user click to request another page. test
|
||||||
const handlePageClick = (event: clickEvent) => {
|
const handlePageClick = (event: clickEvent) => {
|
||||||
const page = event.selected;
|
const page = event.selected;
|
||||||
const newPage = page + 1;
|
const newPage = page + 1;
|
||||||
let newUrl = createUrl(pathname, new URLSearchParams({
|
let newUrl = createUrl(
|
||||||
|
pathname,
|
||||||
|
new URLSearchParams({
|
||||||
...(q && { q }),
|
...(q && { q }),
|
||||||
...(sort && { sort }),
|
...(sort && { sort })
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
if (page !== 0) {
|
if (page !== 0) {
|
||||||
newUrl = createUrl(pathname, new URLSearchParams({
|
newUrl = createUrl(
|
||||||
|
pathname,
|
||||||
|
new URLSearchParams({
|
||||||
...(q && { q }),
|
...(q && { q }),
|
||||||
...(sort && { sort }),
|
...(sort && { sort }),
|
||||||
page: newPage.toString(),
|
page: newPage.toString()
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
router.replace(newUrl);
|
router.replace(newUrl);
|
||||||
};
|
};
|
||||||
@ -65,4 +79,4 @@ type clickEvent = {
|
|||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
isBreak: boolean;
|
isBreak: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { GridTileImage } from 'components/grid/tile';
|
import { GridTileImage } from 'components/grid/tile';
|
||||||
import { getCollectionProducts } from 'lib/shopware';
|
import { getCollectionProducts } from 'lib/shopware';
|
||||||
|
import { isSeoUrls } from 'lib/shopware/helpers';
|
||||||
import type { Product } from 'lib/shopware/types';
|
import type { Product } from 'lib/shopware/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@ -37,8 +38,11 @@ function ThreeItemGridItem({
|
|||||||
|
|
||||||
export async function ThreeItemGrid() {
|
export async function ThreeItemGrid() {
|
||||||
// Collections that start with `hidden-*` are hidden from the search page.
|
// Collections that start with `hidden-*` are hidden from the search page.
|
||||||
|
const collectionName = isSeoUrls()
|
||||||
|
? 'Summer-BBQ/Hidden-Category'
|
||||||
|
: '4ab73c06d90d4a5cb312209a64480d87';
|
||||||
const { products: homepageItems } = await getCollectionProducts({
|
const { products: homepageItems } = await getCollectionProducts({
|
||||||
collection: 'Summer-BBQ/Hidden-Category'
|
collection: collectionName
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
|
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
|
||||||
|
@ -41,7 +41,8 @@ export function GridTileImage({
|
|||||||
{props.src ? (
|
{props.src ? (
|
||||||
<Image
|
<Image
|
||||||
className={clsx('relative h-full w-full object-contain', {
|
className={clsx('relative h-full w-full object-contain', {
|
||||||
'transition duration-300 ease-in-out hover:scale-105': isInteractive
|
'transition duration-300 ease-in-out hover:scale-105': isInteractive,
|
||||||
|
'm-4 max-h-[8rem] min-h-[8rem]': props.width === 200 && props.height === 200 // this styling is for the thumbnails below gallery on product detail page
|
||||||
})}
|
})}
|
||||||
{...props}
|
{...props}
|
||||||
alt={props.title || ''}
|
alt={props.title || ''}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
import GitHubIcon from 'components/icons/github';
|
import GitHubIcon from 'components/icons/github';
|
||||||
import LogoIcon from 'components/icons/logo';
|
import LogoIcon from 'components/icons/logo';
|
||||||
@ -26,11 +27,7 @@ export default async function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
{menu.map((item: Menu) => (
|
{menu.map((item: Menu) => (
|
||||||
<nav className="col-span-1 lg:col-span-3" key={item.title + item.type}>
|
<nav className="col-span-1 lg:col-span-3" key={item.title + item.type}>
|
||||||
{
|
{item.type === 'headline' ? <span className="font-bold">{item.title}</span> : null}
|
||||||
item.type === "headline" ? (
|
|
||||||
<span className='font-bold'>{item.title}</span>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{item.children.length > 0 ? (
|
{item.children.length > 0 ? (
|
||||||
<ul className="py-3 md:py-0 md:pt-4" key={item.title}>
|
<ul className="py-3 md:py-0 md:pt-4" key={item.title}>
|
||||||
{item.children.map((item: Menu) => (
|
{item.children.map((item: Menu) => (
|
||||||
@ -44,7 +41,7 @@ export default async function Footer() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) :
|
) : (
|
||||||
// if there are no children, at least display a link
|
// if there are no children, at least display a link
|
||||||
<Link
|
<Link
|
||||||
key={item.title}
|
key={item.title}
|
||||||
@ -52,11 +49,17 @@ export default async function Footer() {
|
|||||||
className="text-gray-800 transition duration-150 ease-in-out hover:text-gray-300 dark:text-gray-100"
|
className="text-gray-800 transition duration-150 ease-in-out hover:text-gray-300 dark:text-gray-100"
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>}
|
</Link>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
))}
|
))}
|
||||||
<div className="col-span-1 text-black dark:text-white lg:col-span-2 inline-grid justify-items-end">
|
<div className="col-span-1 inline-grid justify-items-end text-black dark:text-white lg:col-span-2">
|
||||||
<a aria-label="Github Repository" href="https://github.com/shopware/frontends" target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
aria-label="Github Repository"
|
||||||
|
href="https://github.com/shopware/frontends"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<GitHubIcon className="h-6" />
|
<GitHubIcon className="h-6" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -77,10 +80,12 @@ export default async function Footer() {
|
|||||||
className="text-black dark:text-white"
|
className="text-black dark:text-white"
|
||||||
>
|
>
|
||||||
<div className="ml-4 h-auto w-10">
|
<div className="ml-4 h-auto w-10">
|
||||||
<img
|
<Image
|
||||||
src="https://www.shopware.com/media/pages/solutions/shopware-frontends/shopware-frontends-intro-graphic-base.svg"
|
src="https://www.shopware.com/media/pages/solutions/shopware-frontends/shopware-frontends-intro-graphic-base.svg"
|
||||||
alt="Shopware Composable Frontends Logo"
|
alt="Shopware Composable Frontends Logo"
|
||||||
></img>
|
width={40}
|
||||||
|
height={40}
|
||||||
|
></Image>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ import { Fragment, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import CloseIcon from 'components/icons/close';
|
import CloseIcon from 'components/icons/close';
|
||||||
import MenuIcon from 'components/icons/menu';
|
import MenuIcon from 'components/icons/menu';
|
||||||
import { Menu } from 'lib/shopify/types';
|
import { Menu } from 'lib/shopware/types';
|
||||||
import Search from './search';
|
import Search from './search';
|
||||||
|
|
||||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { getStaticCollections } from 'lib/shopware';
|
import { getSubCollections } from 'lib/shopware';
|
||||||
import FilterList from './filter';
|
import FilterList from './filter';
|
||||||
import { transformStaticCollectionToList } from 'lib/shopware/transform';
|
import { transformCollectionToList } from 'lib/shopware/transform';
|
||||||
|
|
||||||
async function CollectionList() {
|
async function CollectionList({ collection }: { collection: string }) {
|
||||||
const collections = await getStaticCollections();
|
const collections = await getSubCollections(collection);
|
||||||
if (collections) {
|
if (collections) {
|
||||||
const list = transformStaticCollectionToList(collections);
|
const list = transformCollectionToList(collections);
|
||||||
return <FilterList list={list} title="Collections" />;
|
if (list.length > 0) return <FilterList list={list} title="Sub-Collections" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const skeleton = 'mb-3 h-4 w-5/6 animate-pulse rounded';
|
|||||||
const activeAndTitles = 'bg-gray-800 dark:bg-gray-300';
|
const activeAndTitles = 'bg-gray-800 dark:bg-gray-300';
|
||||||
const items = 'bg-gray-400 dark:bg-gray-700';
|
const items = 'bg-gray-400 dark:bg-gray-700';
|
||||||
|
|
||||||
export default function Collections() {
|
export default function Collections({ collection }: { collection: string }) {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
@ -35,7 +35,7 @@ export default function Collections() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CollectionList />
|
<CollectionList collection={collection} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -84,8 +84,8 @@ export function Gallery({
|
|||||||
<GridTileImage
|
<GridTileImage
|
||||||
alt={image?.altText}
|
alt={image?.altText}
|
||||||
src={image.src}
|
src={image.src}
|
||||||
width={600}
|
width={200}
|
||||||
height={600}
|
height={200}
|
||||||
background="purple-dark"
|
background="purple-dark"
|
||||||
active={isActive}
|
active={isActive}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ProductOption, ProductVariant } from 'lib/shopify/types';
|
import { ProductOption, ProductVariant } from 'lib/shopware/types';
|
||||||
import { createUrl } from 'lib/utils';
|
import { createUrl } from 'lib/utils';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
@ -57,7 +57,8 @@ export type ExtendedProductListingResult = Omit<schemas['ProductListingResult'],
|
|||||||
export type ExtendedCrossSellingElementCollection = Omit<
|
export type ExtendedCrossSellingElementCollection = Omit<
|
||||||
schemas['CrossSellingElementCollection'],
|
schemas['CrossSellingElementCollection'],
|
||||||
'products'
|
'products'
|
||||||
> & {
|
> &
|
||||||
|
{
|
||||||
products?: ExtendedProduct[];
|
products?: ExtendedProduct[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ type extendedReadProductCrossSellings = {
|
|||||||
/** Found cross sellings */
|
/** Found cross sellings */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
'application/json': ExtendedCrossSellingElementCollection
|
'application/json': ExtendedCrossSellingElementCollection;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createAPIClient, RequestReturnType } from '@shopware/api-client';
|
import { createAPIClient, RequestReturnType, ApiClientError } from '@shopware/api-client';
|
||||||
import { operations } from '@shopware/api-client/api-types';
|
import { operations } from '@shopware/api-client/api-types';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import {
|
import {
|
||||||
@ -16,15 +16,13 @@ import {
|
|||||||
SeoURLResultSW,
|
SeoURLResultSW,
|
||||||
StoreNavigationTypeSW
|
StoreNavigationTypeSW
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { getStoreDomainWithApiType, getAccessToken, getApiType } from 'lib/shopware/helpers';
|
||||||
const domainSW = `https://${process.env.SHOPWARE_STORE_DOMAIN!}/${process.env.SHOPWARE_API_TYPE!}`;
|
|
||||||
const accessTokenSW = `${process.env.SHOPWARE_ACCESS_TOKEN}`;
|
|
||||||
|
|
||||||
function getApiClient(cartId?: string) {
|
function getApiClient(cartId?: string) {
|
||||||
const apiInstance = createAPIClient<extendedOperations, extendedPaths>({
|
const apiInstance = createAPIClient<extendedOperations, extendedPaths>({
|
||||||
baseURL: domainSW,
|
baseURL: getStoreDomainWithApiType(),
|
||||||
accessToken: accessTokenSW,
|
accessToken: getAccessToken(),
|
||||||
apiType: 'store-api',
|
apiType: getApiType(),
|
||||||
contextToken: cartId,
|
contextToken: cartId,
|
||||||
onContextChanged(newContextToken: string) {
|
onContextChanged(newContextToken: string) {
|
||||||
//cookies().set('sw-context-token', newContextToken);
|
//cookies().set('sw-context-token', newContextToken);
|
||||||
@ -43,8 +41,9 @@ export type ApiReturnType<OPERATION_NAME extends keyof operations> = RequestRetu
|
|||||||
export async function requestNavigation(
|
export async function requestNavigation(
|
||||||
type: StoreNavigationTypeSW,
|
type: StoreNavigationTypeSW,
|
||||||
depth: number
|
depth: number
|
||||||
): Promise<ExtendedCategory[]> {
|
): Promise<ExtendedCategory[] | undefined> {
|
||||||
return await getApiClient(cookies().get('sw-context-token')).invoke(
|
try {
|
||||||
|
return await getApiClient().invoke(
|
||||||
'readNavigation post /navigation/{activeId}/{rootId} sw-include-seo-urls',
|
'readNavigation post /navigation/{activeId}/{rootId} sw-include-seo-urls',
|
||||||
{
|
{
|
||||||
activeId: type,
|
activeId: type,
|
||||||
@ -52,50 +51,104 @@ export async function requestNavigation(
|
|||||||
depth: depth
|
depth: depth
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestCategory(
|
export async function requestCategory(
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
criteria?: Partial<ProductListingCriteria>
|
criteria?: Partial<ProductListingCriteria>
|
||||||
): Promise<ExtendedCategory> {
|
): Promise<ExtendedCategory | undefined> {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke('readCategory post /category/{navigationId}?slots', {
|
return await getApiClient().invoke('readCategory post /category/{navigationId}?slots', {
|
||||||
navigationId: categoryId,
|
navigationId: categoryId,
|
||||||
criteria
|
criteria
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestCategoryList(
|
export async function requestCategoryList(
|
||||||
criteria: Partial<ExtendedCriteria>
|
criteria: Partial<ExtendedCriteria>
|
||||||
): Promise<CategoryListingResultSW> {
|
): Promise<CategoryListingResultSW | undefined> {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke('readCategoryList post /category', criteria);
|
return await getApiClient().invoke('readCategoryList post /category', criteria);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestProductsCollection(
|
export async function requestProductsCollection(
|
||||||
criteria: Partial<ProductListingCriteria>
|
criteria: Partial<ProductListingCriteria>
|
||||||
): Promise<ExtendedProductListingResult> {
|
): Promise<ExtendedProductListingResult | undefined> {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke('readProduct post /product', criteria);
|
return await getApiClient().invoke('readProduct post /product', criteria);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestCategoryProductsCollection(
|
export async function requestCategoryProductsCollection(
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
criteria: Partial<ProductListingCriteria>
|
criteria: Partial<ProductListingCriteria>
|
||||||
): Promise<ExtendedProductListingResult> {
|
): Promise<ExtendedProductListingResult | undefined> {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke('readProductListing post /product-listing/{categoryId}', {
|
return await getApiClient().invoke('readProductListing post /product-listing/{categoryId}', {
|
||||||
...criteria,
|
...criteria,
|
||||||
categoryId: categoryId
|
categoryId: categoryId
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestSearchCollectionProducts(
|
export async function requestSearchCollectionProducts(
|
||||||
criteria?: Partial<ProductListingCriteria>
|
criteria?: Partial<ProductListingCriteria>
|
||||||
): Promise<ExtendedProductListingResult> {
|
): Promise<ExtendedProductListingResult | undefined> {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke('searchPage post /search', {
|
return await getApiClient().invoke('searchPage post /search', {
|
||||||
search: encodeURIComponent(criteria?.query || ''),
|
search: encodeURIComponent(criteria?.query || ''),
|
||||||
...criteria
|
...criteria
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestSeoUrls(routeName: RouteNames, page: number = 1, limit: number = 100) {
|
export async function requestSeoUrls(routeName: RouteNames, page: number = 1, limit: number = 100) {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke('readSeoUrl post /seo-url', {
|
return await getApiClient().invoke('readSeoUrl post /seo-url', {
|
||||||
page: page,
|
page: page,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
@ -107,20 +160,28 @@ export async function requestSeoUrls(routeName: RouteNames, page: number = 1, li
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestSeoUrl(
|
export async function requestSeoUrl(
|
||||||
handle: string,
|
handle: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 1
|
limit: number = 1
|
||||||
): Promise<SeoURLResultSW> {
|
): Promise<SeoURLResultSW | undefined> {
|
||||||
return await getApiClient().invoke('readSeoUrl post /seo-url', {
|
try {
|
||||||
|
const criteriaSeoUrls = {
|
||||||
page: page,
|
page: page,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
filter: [
|
filter: [
|
||||||
{
|
{
|
||||||
type: 'multi',
|
type: 'multi',
|
||||||
// @ts-ignore
|
|
||||||
operator: 'or',
|
operator: 'or',
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
@ -136,13 +197,24 @@ export async function requestSeoUrl(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
return await getApiClient().invoke('readSeoUrl post /seo-url', criteriaSeoUrls);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestCrossSell(
|
export async function requestCrossSell(
|
||||||
productId: string,
|
productId: string,
|
||||||
criteria?: Partial<ProductListingCriteria>
|
criteria?: Partial<ProductListingCriteria>
|
||||||
): Promise<ExtendedCrossSellingElementCollection> {
|
): Promise<ExtendedCrossSellingElementCollection | undefined> {
|
||||||
|
try {
|
||||||
return await getApiClient().invoke(
|
return await getApiClient().invoke(
|
||||||
'readProductCrossSellings post /product/{productId}/cross-selling',
|
'readProductCrossSellings post /product/{productId}/cross-selling',
|
||||||
{
|
{
|
||||||
@ -150,10 +222,27 @@ export async function requestCrossSell(
|
|||||||
...criteria
|
...criteria
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestCart(cartId?: string) {
|
export async function requestCart(cartId?: string) {
|
||||||
|
try {
|
||||||
return getApiClient(cartId).invoke('readCart get /checkout/cart?name', {});
|
return getApiClient(cartId).invoke('readCart get /checkout/cart?name', {});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiClientError) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Details:', error.details);
|
||||||
|
} else {
|
||||||
|
console.error('==>', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestContext(cartId?: string) {
|
export async function requestContext(cartId?: string) {
|
||||||
|
@ -165,7 +165,7 @@ export function getStaticCollectionCriteria(page: number = 1, limit: number = 20
|
|||||||
export function getDefaultSubCategoriesCriteria(
|
export function getDefaultSubCategoriesCriteria(
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 10
|
limit: number = 1
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
page: page,
|
page: page,
|
||||||
|
25
lib/shopware/helpers.ts
Normal file
25
lib/shopware/helpers.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export function getAccessToken(): string {
|
||||||
|
return `${process.env.SHOPWARE_ACCESS_TOKEN}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoreDomainWithApiType(): string {
|
||||||
|
return getStoreDomain() + '/' + getApiType();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoreDomain(protocol: boolean = true): string {
|
||||||
|
return protocol
|
||||||
|
? `https://${process.env.SHOPWARE_STORE_DOMAIN!}`
|
||||||
|
: `${process.env.SHOPWARE_STORE_DOMAIN!}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApiType(): 'store-api' | 'admin-api' {
|
||||||
|
if (`${process.env.SHOPWARE_API_TYPE!}` === 'admin-api') {
|
||||||
|
return 'admin-api';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'store-api';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSeoUrls(): boolean {
|
||||||
|
return `${process.env.SHOPWARE_USE_SEO_URLS}` === 'true';
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { Cart } from 'lib/shopify/types';
|
|
||||||
import {
|
import {
|
||||||
requestCart,
|
requestCart,
|
||||||
requestCategory,
|
requestCategory,
|
||||||
@ -19,8 +18,8 @@ import {
|
|||||||
getDefaultProductCriteria,
|
getDefaultProductCriteria,
|
||||||
getDefaultProductsCriteria,
|
getDefaultProductsCriteria,
|
||||||
getDefaultSearchProductsCriteria,
|
getDefaultSearchProductsCriteria,
|
||||||
getSortingCriteria,
|
getDefaultSubCategoriesCriteria,
|
||||||
getStaticCollectionCriteria
|
getSortingCriteria
|
||||||
} from './criteria';
|
} from './criteria';
|
||||||
import {
|
import {
|
||||||
transformCollection,
|
transformCollection,
|
||||||
@ -29,16 +28,19 @@ import {
|
|||||||
transformPage,
|
transformPage,
|
||||||
transformProduct,
|
transformProduct,
|
||||||
transformProducts,
|
transformProducts,
|
||||||
transformStaticCollection
|
transformSubCollection
|
||||||
} from './transform';
|
} from './transform';
|
||||||
import {
|
import {
|
||||||
ApiSchemas,
|
ApiSchemas,
|
||||||
|
Cart,
|
||||||
|
CategoryListingResultSW,
|
||||||
Menu,
|
Menu,
|
||||||
Page,
|
Page,
|
||||||
Product,
|
Product,
|
||||||
ProductListingCriteria,
|
ProductListingCriteria,
|
||||||
StoreNavigationTypeSW
|
StoreNavigationTypeSW
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { isSeoUrls } from 'lib/shopware/helpers';
|
||||||
|
|
||||||
export async function getMenu(params?: {
|
export async function getMenu(params?: {
|
||||||
type?: StoreNavigationTypeSW;
|
type?: StoreNavigationTypeSW;
|
||||||
@ -52,39 +54,66 @@ export async function getMenu(params?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPage(handle: string | []): Promise<Page | undefined> {
|
export async function getPage(handle: string | []): Promise<Page | undefined> {
|
||||||
const pageHandle = transformHandle(handle).replace('cms/', '');
|
let seoUrlElement;
|
||||||
const seoUrlElement = await getFirstSeoUrlElement(pageHandle);
|
let pageIdOrHandle = decodeURIComponent(transformHandle(handle)).replace('cms/', '');
|
||||||
if (seoUrlElement) {
|
|
||||||
const resCategory = await getCategory(seoUrlElement);
|
|
||||||
|
|
||||||
return resCategory ? transformPage(seoUrlElement, resCategory) : undefined;
|
if (isSeoUrls()) {
|
||||||
|
seoUrlElement = await getFirstSeoUrlElement(pageIdOrHandle);
|
||||||
|
if (seoUrlElement) {
|
||||||
|
pageIdOrHandle = seoUrlElement.foreignKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!seoUrlElement) {
|
||||||
|
console.log('[getPage] Did not found any seoUrl element with page handle:', pageIdOrHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await getCategory(pageIdOrHandle);
|
||||||
|
if (!category) {
|
||||||
|
console.log('[getPage] Did not found any category with handle:', pageIdOrHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return category ? transformPage(category, seoUrlElement) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFirstSeoUrlElement(
|
export async function getFirstSeoUrlElement(
|
||||||
handle: string
|
handle: string
|
||||||
): Promise<ApiSchemas['SeoUrl'] | undefined> {
|
): Promise<ApiSchemas['SeoUrl'] | undefined> {
|
||||||
const resSeoUrl = await requestSeoUrl(handle);
|
const seoURL = await requestSeoUrl(handle);
|
||||||
if (resSeoUrl.elements && resSeoUrl.elements.length > 0 && resSeoUrl.elements[0]) {
|
if (seoURL && seoURL.elements && seoURL.elements.length > 0 && seoURL.elements[0]) {
|
||||||
return resSeoUrl.elements[0];
|
return seoURL.elements[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFirstProduct(productId: string): Promise<ExtendedProduct | undefined> {
|
export async function getFirstProduct(productId: string): Promise<ExtendedProduct | undefined> {
|
||||||
const productCriteria = getDefaultProductCriteria(productId);
|
const productCriteria = getDefaultProductCriteria(productId);
|
||||||
const res: ExtendedProductListingResult = await requestProductsCollection(productCriteria);
|
const listing: ExtendedProductListingResult | undefined = await requestProductsCollection(
|
||||||
if (res.elements && res.elements.length > 0 && res.elements[0]) {
|
productCriteria
|
||||||
return res.elements[0];
|
);
|
||||||
|
if (listing && listing.elements && listing.elements.length > 0 && listing.elements[0]) {
|
||||||
|
return listing.elements[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToDo: should be more dynamic (depending on handle), should work with server and not client see generateStaticParams from next.js
|
// ToDo: should be more dynamic (depending on handle), should work with server and not client see generateStaticParams from next.js
|
||||||
export async function getStaticCollections() {
|
export async function getSubCollections(collection: string) {
|
||||||
// @ToDo: This is an example about multi-filter with new store API client
|
const collectionName = decodeURIComponent(transformHandle(collection ?? ''));
|
||||||
// @ts-ignore
|
let criteria = getDefaultSubCategoriesCriteria(collectionName);
|
||||||
const resCategory = await requestCategoryList(getStaticCollectionCriteria());
|
let res: CategoryListingResultSW | undefined = undefined;
|
||||||
|
const parentCollectionName =
|
||||||
|
Array.isArray(collection) && collection[0] ? collection[0] : undefined;
|
||||||
|
|
||||||
return resCategory ? transformStaticCollection(resCategory) : [];
|
if (isSeoUrls()) {
|
||||||
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
||||||
|
if (seoUrlElement) {
|
||||||
|
criteria = getDefaultSubCategoriesCriteria(seoUrlElement.foreignKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
res = await requestCategoryList(criteria);
|
||||||
|
|
||||||
|
return res ? transformSubCollection(res, parentCollectionName) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSearchCollectionProducts(params?: {
|
export async function getSearchCollectionProducts(params?: {
|
||||||
@ -99,9 +128,34 @@ export async function getSearchCollectionProducts(params?: {
|
|||||||
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
||||||
const searchCriteria = { ...criteria, ...sorting };
|
const searchCriteria = { ...criteria, ...sorting };
|
||||||
|
|
||||||
const res = await requestSearchCollectionProducts(searchCriteria);
|
const search = await requestSearchCollectionProducts(searchCriteria);
|
||||||
|
if (isSeoUrls() && search) {
|
||||||
|
search.elements = await changeVariantUrlToParentUrl(search);
|
||||||
|
}
|
||||||
|
|
||||||
return res ? transformProducts(res) : [];
|
return search ? transformProducts(search) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeVariantUrlToParentUrl(
|
||||||
|
collection: ExtendedProductListingResult
|
||||||
|
): Promise<ExtendedProduct[]> {
|
||||||
|
const newElements: ExtendedProduct[] = [];
|
||||||
|
if (collection.elements && collection.elements.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
collection.elements.map(async (item) => {
|
||||||
|
if (item.parentId && item.seoUrls && item.seoUrls[0]) {
|
||||||
|
const parentProduct = await getFirstProduct(item.parentId);
|
||||||
|
if (parentProduct && parentProduct.seoUrls && parentProduct.seoUrls[0]) {
|
||||||
|
item.seoUrls[0].seoPathInfo = parentProduct.seoUrls[0].seoPathInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newElements.push(item);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCollectionProducts(params?: {
|
export async function getCollectionProducts(params?: {
|
||||||
@ -112,12 +166,12 @@ export async function getCollectionProducts(params?: {
|
|||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
defaultSearchCriteria?: Partial<ProductListingCriteria>;
|
defaultSearchCriteria?: Partial<ProductListingCriteria>;
|
||||||
}): Promise<{ products: Product[]; total: number; limit: number }> {
|
}): Promise<{ products: Product[]; total: number; limit: number }> {
|
||||||
let res;
|
let products;
|
||||||
let category = params?.categoryId;
|
let category = params?.categoryId;
|
||||||
const collectionName = transformHandle(params?.collection ?? '');
|
const collectionName = decodeURIComponent(transformHandle(params?.collection ?? ''));
|
||||||
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
||||||
|
|
||||||
if (!category && collectionName !== '') {
|
if (isSeoUrls() && !category && collectionName !== '') {
|
||||||
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
||||||
if (seoUrlElement) {
|
if (seoUrlElement) {
|
||||||
category = seoUrlElement.foreignKey;
|
category = seoUrlElement.foreignKey;
|
||||||
@ -130,37 +184,56 @@ export async function getCollectionProducts(params?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSeoUrls()) {
|
||||||
|
category = collectionName ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
const criteria = !params?.defaultSearchCriteria
|
const criteria = !params?.defaultSearchCriteria
|
||||||
? getDefaultProductsCriteria(params?.page)
|
? getDefaultProductsCriteria(params?.page)
|
||||||
: params?.defaultSearchCriteria;
|
: params?.defaultSearchCriteria;
|
||||||
const productsCriteria = { ...criteria, ...sorting };
|
const productsCriteria = { ...criteria, ...sorting };
|
||||||
res = await requestCategoryProductsCollection(category, productsCriteria);
|
products = await requestCategoryProductsCollection(category, productsCriteria);
|
||||||
|
if (products) {
|
||||||
|
products.elements = await changeVariantUrlToParentUrl(products);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return products
|
||||||
? { products: transformProducts(res), total: res.total ?? 0, limit: res.limit ?? 0 }
|
? {
|
||||||
|
products: transformProducts(products),
|
||||||
|
total: products.total ?? 0,
|
||||||
|
limit: products.limit ?? 0
|
||||||
|
}
|
||||||
: { products: [], total: 0, limit: 0 };
|
: { products: [], total: 0, limit: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCategory(
|
export async function getCategory(
|
||||||
seoUrl: ApiSchemas['SeoUrl'],
|
categoryId: string,
|
||||||
cms: boolean = false
|
cms: boolean = false
|
||||||
): Promise<ExtendedCategory> {
|
): Promise<ExtendedCategory | undefined> {
|
||||||
const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria();
|
const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria();
|
||||||
const resCategory = await requestCategory(seoUrl.foreignKey, criteria);
|
return await requestCategory(categoryId, criteria);
|
||||||
|
|
||||||
return resCategory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is only used for generateMetadata at app/search/(collection)/[...collection]/page.tsx
|
// This function is only used for generateMetadata at app/search/(collection)/[...collection]/page.tsx
|
||||||
export async function getCollection(handle: string | []) {
|
export async function getCollection(handle: string | []) {
|
||||||
const collectionName = transformHandle(handle);
|
let path;
|
||||||
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
let seoUrlElement;
|
||||||
|
let categoryIdOrHandle = decodeURIComponent(transformHandle(handle));
|
||||||
|
|
||||||
|
if (isSeoUrls()) {
|
||||||
|
seoUrlElement = await getFirstSeoUrlElement(categoryIdOrHandle);
|
||||||
if (seoUrlElement) {
|
if (seoUrlElement) {
|
||||||
const resCategory = await getCategory(seoUrlElement);
|
categoryIdOrHandle = seoUrlElement.foreignKey;
|
||||||
const path = seoUrlElement.seoPathInfo ?? '';
|
path = seoUrlElement.seoPathInfo ?? '';
|
||||||
const collection = transformCollection(seoUrlElement, resCategory);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await getCategory(categoryIdOrHandle);
|
||||||
|
if (category) {
|
||||||
|
const collection = transformCollection(category, seoUrlElement);
|
||||||
|
path = path ?? category.id ?? '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
@ -171,10 +244,10 @@ export async function getCollection(handle: string | []) {
|
|||||||
|
|
||||||
export async function getProductSeoUrls() {
|
export async function getProductSeoUrls() {
|
||||||
const productSeoUrls: { path: string; updatedAt: string }[] = [];
|
const productSeoUrls: { path: string; updatedAt: string }[] = [];
|
||||||
const res = await requestSeoUrls('frontend.detail.page');
|
const seoUrls = await requestSeoUrls('frontend.detail.page');
|
||||||
|
|
||||||
if (res.elements && res.elements.length > 0) {
|
if (seoUrls && seoUrls.elements && seoUrls.elements.length > 0) {
|
||||||
res.elements.map((item) =>
|
seoUrls.elements.map((item) =>
|
||||||
productSeoUrls.push({ path: item.seoPathInfo, updatedAt: item.updatedAt ?? item.createdAt })
|
productSeoUrls.push({ path: item.seoPathInfo, updatedAt: item.updatedAt ?? item.createdAt })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -185,16 +258,20 @@ export async function getProductSeoUrls() {
|
|||||||
export async function getProduct(handle: string | []): Promise<Product | undefined> {
|
export async function getProduct(handle: string | []): Promise<Product | undefined> {
|
||||||
let productSW: ExtendedProduct | undefined;
|
let productSW: ExtendedProduct | undefined;
|
||||||
let productId: string | undefined;
|
let productId: string | undefined;
|
||||||
const productHandle = transformHandle(handle);
|
const productHandle = decodeURIComponent(transformHandle(handle));
|
||||||
|
productId = productHandle; // if we do not use seoUrls the handle should be the product id
|
||||||
|
|
||||||
|
if (isSeoUrls()) {
|
||||||
const seoUrlElement = await getFirstSeoUrlElement(productHandle);
|
const seoUrlElement = await getFirstSeoUrlElement(productHandle);
|
||||||
if (seoUrlElement) {
|
if (seoUrlElement) {
|
||||||
productId = seoUrlElement.foreignKey;
|
productId = seoUrlElement.foreignKey;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
console.log('[getProduct][search] Did not found any product with handle:', productHandle);
|
console.log('[getProduct][search] Did not found any product with handle:', handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (productId) {
|
if (productId) {
|
||||||
const firstProduct = await getFirstProduct(productId);
|
const firstProduct = await getFirstProduct(productId);
|
||||||
if (firstProduct) {
|
if (firstProduct) {
|
||||||
@ -217,10 +294,11 @@ export async function getProductRecommendations(productId: string): Promise<Prod
|
|||||||
return products ? transformProducts(products) : [];
|
return products ? transformProducts(products) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCart(cartId?: string): Promise<Cart> {
|
export async function getCart(cartId?: string): Promise<Cart | undefined> {
|
||||||
const cartData = await requestCart(cartId);
|
const cartData = await requestCart(cartId);
|
||||||
|
if (cartData) {
|
||||||
let cart: Cart = {
|
// @ToDo: should be moved to transformCart function
|
||||||
|
const cart: Cart = {
|
||||||
checkoutUrl: 'https://frontends-demo.vercel.app',
|
checkoutUrl: 'https://frontends-demo.vercel.app',
|
||||||
cost: {
|
cost: {
|
||||||
subtotalAmount: {
|
subtotalAmount: {
|
||||||
@ -240,42 +318,50 @@ export async function getCart(cartId?: string): Promise<Cart> {
|
|||||||
lines:
|
lines:
|
||||||
cartData.lineItems?.map((lineItem) => ({
|
cartData.lineItems?.map((lineItem) => ({
|
||||||
id: lineItem.referencedId || '',
|
id: lineItem.referencedId || '',
|
||||||
quantity: lineItem.quantity,
|
quantity: lineItem.quantity ?? 0,
|
||||||
cost: {
|
cost: {
|
||||||
totalAmount: {
|
totalAmount: {
|
||||||
amount: (lineItem as any)?.price?.totalPrice || ''
|
amount: (lineItem as any)?.price?.totalPrice || '',
|
||||||
|
currencyCode: 'EUR'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
merchandise: {
|
merchandise: {
|
||||||
id: lineItem.referencedId,
|
id: lineItem.referencedId ?? '',
|
||||||
title: lineItem.label,
|
title: lineItem.label ?? '',
|
||||||
selectedOptions: [],
|
selectedOptions: [],
|
||||||
product: {
|
product: {
|
||||||
description: lineItem.description,
|
description: lineItem.description ?? '',
|
||||||
descriptionHtml: lineItem.description,
|
descriptionHtml: lineItem.description ?? '',
|
||||||
id: lineItem.referencedId,
|
id: lineItem.referencedId ?? '',
|
||||||
images: [],
|
images: [],
|
||||||
|
path: '',
|
||||||
seo: {
|
seo: {
|
||||||
description: lineItem.description,
|
description: lineItem.description ?? '',
|
||||||
title: lineItem.label
|
title: lineItem.label ?? ''
|
||||||
},
|
},
|
||||||
availableForSale: true,
|
availableForSale: true,
|
||||||
featuredImage: {
|
featuredImage: (lineItem as any).cover?.url,
|
||||||
altText: 'Cover image of ' + lineItem.label,
|
|
||||||
url: (lineItem as any).cover?.url
|
|
||||||
},
|
|
||||||
handle: '',
|
handle: '',
|
||||||
options: [],
|
options: [],
|
||||||
variants: [],
|
variants: [],
|
||||||
priceRange: {},
|
priceRange: {
|
||||||
|
minVariantPrice: {
|
||||||
|
amount: '', // @ToDo: should be correct value
|
||||||
|
currencyCode: 'EUR'
|
||||||
|
},
|
||||||
|
maxVariantPrice: {
|
||||||
|
amount: '', // @ToDo: should be correct value
|
||||||
|
currencyCode: 'EUR'
|
||||||
|
}
|
||||||
|
},
|
||||||
tags: [],
|
tags: [],
|
||||||
title: lineItem.label,
|
title: lineItem.label ?? '',
|
||||||
updatedAt: (lineItem as any)?.payload?.updatedAt
|
updatedAt: (lineItem as any)?.payload?.updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})) || [],
|
})) || [],
|
||||||
totalQuantity: cartData.lineItems?.length || 0
|
totalQuantity: cartData.lineItems?.length || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
ExtendedProductListingResult
|
ExtendedProductListingResult
|
||||||
} from './api-extended';
|
} from './api-extended';
|
||||||
import { ListItem } from 'components/layout/search/filter';
|
import { ListItem } from 'components/layout/search/filter';
|
||||||
|
import { isSeoUrls } from 'lib/shopware/helpers';
|
||||||
|
|
||||||
export function transformMenu(res: ExtendedCategory[], type: string) {
|
export function transformMenu(res: ExtendedCategory[], type: string) {
|
||||||
const menu: Menu[] = [];
|
const menu: Menu[] = [];
|
||||||
@ -25,24 +26,29 @@ export function transformMenu(res: ExtendedCategory[], type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformMenuItem(item: ExtendedCategory, type: string): Menu {
|
function transformMenuItem(item: ExtendedCategory, type: string): Menu {
|
||||||
|
const path = isSeoUrls()
|
||||||
|
? item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo
|
||||||
|
? type === 'footer-navigation'
|
||||||
|
? '/cms/' + item.seoUrls[0].seoPathInfo
|
||||||
|
: '/search/' + item.seoUrls[0].seoPathInfo
|
||||||
|
: ''
|
||||||
|
: type === 'footer-navigation'
|
||||||
|
? '/cms/' + item.id ?? ''
|
||||||
|
: '/search/' + item.id ?? '';
|
||||||
|
|
||||||
// @ToDo: currently only footer-navigation is used for cms pages, this need to be more dynamic (shoud depending on the item)
|
// @ToDo: currently only footer-navigation is used for cms pages, this need to be more dynamic (shoud depending on the item)
|
||||||
return {
|
return {
|
||||||
id: item.id ?? '',
|
id: item.id ?? '',
|
||||||
title: item.name,
|
title: item.name,
|
||||||
children: item.children?.map((item) => transformMenuItem(item, type)) ?? [],
|
children: item.children?.map((item) => transformMenuItem(item, type)) ?? [],
|
||||||
path:
|
path: path,
|
||||||
item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo
|
|
||||||
? type === 'footer-navigation'
|
|
||||||
? '/cms/' + item.seoUrls[0].seoPathInfo
|
|
||||||
: '/search/' + item.seoUrls[0].seoPathInfo
|
|
||||||
: '',
|
|
||||||
type: item.children && item.children.length > 0 ? 'headline' : 'link'
|
type: item.children && item.children.length > 0 ? 'headline' : 'link'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformPage(
|
export function transformPage(
|
||||||
seoUrlElement: ApiSchemas['SeoUrl'],
|
category: ExtendedCategory,
|
||||||
category: ExtendedCategory
|
seoUrlElement?: ApiSchemas['SeoUrl']
|
||||||
): Page {
|
): Page {
|
||||||
let plainHtmlContent;
|
let plainHtmlContent;
|
||||||
if (category.cmsPage) {
|
if (category.cmsPage) {
|
||||||
@ -51,20 +57,20 @@ export function transformPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: seoUrlElement.id ?? '',
|
id: seoUrlElement?.id ?? category.id ?? '',
|
||||||
title: category.translated?.metaTitle ?? category.name ?? '',
|
title: category.translated?.metaTitle ?? category.name ?? '',
|
||||||
handle: seoUrlElement.seoPathInfo,
|
handle: seoUrlElement?.seoPathInfo ?? category.id ?? '',
|
||||||
body: plainHtmlContent ?? category.description ?? '',
|
body: plainHtmlContent ?? category.description ?? '',
|
||||||
bodySummary: category.translated?.metaDescription ?? category.description ?? '',
|
bodySummary: category.translated?.metaDescription ?? category.description ?? '',
|
||||||
seo: {
|
seo: {
|
||||||
title: category.translated?.metaTitle ?? category.name ?? '',
|
title: category.translated?.metaTitle ?? category.name ?? '',
|
||||||
description: category.translated?.metaDescription ?? category.description ?? ''
|
description: category.translated?.metaDescription ?? category.description ?? ''
|
||||||
},
|
},
|
||||||
createdAt: seoUrlElement.createdAt ?? '',
|
createdAt: seoUrlElement?.createdAt ?? category.createdAt ?? '',
|
||||||
updatedAt: seoUrlElement.updatedAt ?? '',
|
updatedAt: seoUrlElement?.updatedAt ?? category.updatedAt ?? '',
|
||||||
routeName: seoUrlElement.routeName,
|
routeName: seoUrlElement?.routeName,
|
||||||
originalCmsPage: category.cmsPage,
|
originalCmsPage: category.cmsPage,
|
||||||
foreignKey: seoUrlElement.foreignKey
|
foreignKey: seoUrlElement?.foreignKey ?? category.id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,55 +95,89 @@ export function transformToPlainHtmlContent(cmsPage: ExtendedCmsPage): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function transformCollection(
|
export function transformCollection(
|
||||||
seoUrlElement: ApiSchemas['SeoUrl'],
|
resCategory: ExtendedCategory,
|
||||||
resCategory: ExtendedCategory
|
seoUrlElement?: ApiSchemas['SeoUrl']
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
handle: seoUrlElement.seoPathInfo,
|
handle: seoUrlElement?.seoPathInfo ?? resCategory.id ?? '',
|
||||||
title: resCategory.translated?.metaTitle ?? resCategory.name ?? '',
|
title: resCategory.translated?.metaTitle ?? resCategory.name ?? '',
|
||||||
description: resCategory.description ?? '',
|
description: resCategory.description ?? '',
|
||||||
seo: {
|
seo: {
|
||||||
title: resCategory.translated?.metaTitle ?? resCategory.name ?? '',
|
title: resCategory.translated?.metaTitle ?? resCategory.name ?? '',
|
||||||
description: resCategory.translated?.metaDescription ?? resCategory.description ?? ''
|
description: resCategory.translated?.metaDescription ?? resCategory.description ?? ''
|
||||||
},
|
},
|
||||||
updatedAt: seoUrlElement.updatedAt ?? seoUrlElement.createdAt ?? ''
|
updatedAt:
|
||||||
|
seoUrlElement?.updatedAt ??
|
||||||
|
seoUrlElement?.createdAt ??
|
||||||
|
resCategory.updatedAt ??
|
||||||
|
resCategory.createdAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformStaticCollection(resCategory: CategoryListingResultSW): Collection[] {
|
export function transformSubCollection(
|
||||||
|
category: CategoryListingResultSW,
|
||||||
|
parentCollectionName?: string
|
||||||
|
): Collection[] {
|
||||||
const collection: Collection[] = [];
|
const collection: Collection[] = [];
|
||||||
|
|
||||||
if (resCategory.elements && resCategory.elements.length > 0) {
|
if (category.elements && category.elements[0] && category.elements[0].children) {
|
||||||
resCategory.elements.map((item) =>
|
// we do not support type links at the moment and show only visible categories
|
||||||
|
category.elements[0].children
|
||||||
|
.filter((item) => item.visible)
|
||||||
|
.filter((item) => item.type !== 'link')
|
||||||
|
.map((item) => {
|
||||||
|
const handle =
|
||||||
|
isSeoUrls() && item.seoUrls ? findHandle(item.seoUrls, parentCollectionName) : item.id;
|
||||||
|
if (handle) {
|
||||||
collection.push({
|
collection.push({
|
||||||
handle:
|
handle: handle,
|
||||||
item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo
|
|
||||||
? item.seoUrls[0].seoPathInfo
|
|
||||||
: '',
|
|
||||||
title: item.translated?.metaTitle ?? item.name ?? '',
|
title: item.translated?.metaTitle ?? item.name ?? '',
|
||||||
description: item.description ?? '',
|
description: item.description ?? '',
|
||||||
seo: {
|
seo: {
|
||||||
title: item.translated?.metaTitle ?? item.name ?? '',
|
title: item.translated?.metaTitle ?? item.name ?? '',
|
||||||
description: item.translated?.metaDescription ?? item.description ?? ''
|
description: item.translated?.metaDescription ?? item.description ?? ''
|
||||||
},
|
},
|
||||||
|
childCount: item.childCount ?? 0,
|
||||||
updatedAt: item.updatedAt ?? item.createdAt ?? ''
|
updatedAt: item.updatedAt ?? item.createdAt ?? ''
|
||||||
})
|
});
|
||||||
);
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformStaticCollectionToList(collection: Collection[]): ListItem[] {
|
// small function to find longest handle and to make sure parent collection name is in the path
|
||||||
|
function findHandle(seoUrls: ApiSchemas['SeoUrl'][], parentCollectionName?: string): string {
|
||||||
|
let handle: string = '';
|
||||||
|
seoUrls.map((item) => {
|
||||||
|
if (
|
||||||
|
!item.isDeleted &&
|
||||||
|
item.isCanonical &&
|
||||||
|
item.seoPathInfo &&
|
||||||
|
item.seoPathInfo.length > handle.length &&
|
||||||
|
item.seoPathInfo.includes(parentCollectionName ?? '')
|
||||||
|
) {
|
||||||
|
handle = item.seoPathInfo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformCollectionToList(collection: Collection[]): ListItem[] {
|
||||||
const listItem: ListItem[] = [];
|
const listItem: ListItem[] = [];
|
||||||
|
|
||||||
if (collection && collection.length > 0) {
|
if (collection && collection.length > 0) {
|
||||||
collection.map((item) =>
|
collection.map((item) => {
|
||||||
|
// we asume that when there is not product child count it must be a cms page
|
||||||
|
const pagePrefix = item.childCount === 0 ? '/cms' : '/search';
|
||||||
|
const newHandle = item.handle.replace('Main-navigation/', '');
|
||||||
listItem.push({
|
listItem.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
path: `/search/${item.handle}`
|
path: `${pagePrefix}/${newHandle}`
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return listItem;
|
return listItem;
|
||||||
@ -157,12 +197,17 @@ export function transformProduct(item: ExtendedProduct): Product {
|
|||||||
const productOptions = transformOptions(item);
|
const productOptions = transformOptions(item);
|
||||||
const productVariants = transformVariants(item);
|
const productVariants = transformVariants(item);
|
||||||
|
|
||||||
return {
|
let path = item.parentId ?? item.id ?? '';
|
||||||
id: item.id ?? '',
|
if (isSeoUrls()) {
|
||||||
path:
|
path =
|
||||||
item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo
|
item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo
|
||||||
? item.seoUrls[0].seoPathInfo
|
? item.seoUrls[0].seoPathInfo
|
||||||
: '',
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id ?? '',
|
||||||
|
path: path,
|
||||||
availableForSale: item.available ?? false,
|
availableForSale: item.available ?? false,
|
||||||
title: item.translated ? item.translated.name ?? '' : item.name,
|
title: item.translated ? item.translated.name ?? '' : item.name,
|
||||||
description: item.translated?.metaDescription
|
description: item.translated?.metaDescription
|
||||||
@ -213,9 +258,11 @@ function transformOptions(parent: ExtendedProduct): ProductOption[] {
|
|||||||
const productOptions: ProductOption[] = [];
|
const productOptions: ProductOption[] = [];
|
||||||
if (parent.children && parent.parentId === null && parent.children.length > 0) {
|
if (parent.children && parent.parentId === null && parent.children.length > 0) {
|
||||||
const group: { [key: string]: string[] } = {};
|
const group: { [key: string]: string[] } = {};
|
||||||
|
const groupId: { [key: string]: string } = {};
|
||||||
parent.children.map((child) => {
|
parent.children.map((child) => {
|
||||||
child.options?.map((option) => {
|
child.options?.map((option) => {
|
||||||
if (option && option.group) {
|
if (option && option.group) {
|
||||||
|
groupId[option.group.name] = option.groupId;
|
||||||
group[option.group.name] = group[option.group.name]
|
group[option.group.name] = group[option.group.name]
|
||||||
? [...new Set([...(group[option.group.name] as []), ...[option.name]])]
|
? [...new Set([...(group[option.group.name] as []), ...[option.name]])]
|
||||||
: [option.name];
|
: [option.name];
|
||||||
@ -223,16 +270,18 @@ function transformOptions(parent: ExtendedProduct): ProductOption[] {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parent.id) {
|
|
||||||
for (const [key, value] of Object.entries(group)) {
|
for (const [key, value] of Object.entries(group)) {
|
||||||
|
for (const [currentGroupName, currentGroupId] of Object.entries(groupId)) {
|
||||||
|
if (key === currentGroupName) {
|
||||||
productOptions.push({
|
productOptions.push({
|
||||||
id: parent.id,
|
id: currentGroupId,
|
||||||
name: key,
|
name: key,
|
||||||
values: value
|
values: value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return productOptions;
|
return productOptions;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,11 @@ export type ProductListingCriteria = {
|
|||||||
query: string;
|
query: string;
|
||||||
} & Omit<ApiSchemas['ProductListingCriteria'], 'filter'> &
|
} & Omit<ApiSchemas['ProductListingCriteria'], 'filter'> &
|
||||||
ExtendedCriteria;
|
ExtendedCriteria;
|
||||||
export type RouteNames = 'frontend.navigation.page' | 'frontend.detail.page' | 'frontend.account.customer-group-registration.page' | 'frontend.landing.page'
|
export type RouteNames =
|
||||||
|
| 'frontend.navigation.page'
|
||||||
|
| 'frontend.detail.page'
|
||||||
|
| 'frontend.account.customer-group-registration.page'
|
||||||
|
| 'frontend.landing.page';
|
||||||
|
|
||||||
/** Return Types */
|
/** Return Types */
|
||||||
export type CategoryListingResultSW = {
|
export type CategoryListingResultSW = {
|
||||||
@ -96,5 +100,35 @@ export type Collection = {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
seo: SEO;
|
seo: SEO;
|
||||||
|
childCount: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Cart = {
|
||||||
|
id: string;
|
||||||
|
checkoutUrl: string;
|
||||||
|
cost: {
|
||||||
|
subtotalAmount: Money;
|
||||||
|
totalAmount: Money;
|
||||||
|
totalTaxAmount: Money;
|
||||||
|
};
|
||||||
|
lines: CartItem[];
|
||||||
|
totalQuantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CartItem = {
|
||||||
|
id: string;
|
||||||
|
quantity: number;
|
||||||
|
cost: {
|
||||||
|
totalAmount: Money;
|
||||||
|
};
|
||||||
|
merchandise: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
selectedOptions: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
product: Product;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
33
package.json
33
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16",
|
"node": ">=18",
|
||||||
"pnpm": ">=7"
|
"pnpm": ">=8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@ -15,42 +15,39 @@
|
|||||||
"test": "pnpm lint && pnpm prettier:check",
|
"test": "pnpm lint && pnpm prettier:check",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"git": {
|
|
||||||
"pre-commit": "lint-staged"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": "prettier --write --ignore-unknown"
|
"*": "prettier --write --ignore-unknown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.7.15",
|
"@headlessui/react": "^1.7.15",
|
||||||
"@shopware/api-client": "0.0.0-canary-20230706101754",
|
"@shopware/api-client": "0.0.0-canary-20230713092547",
|
||||||
"@vercel/og": "^0.5.8",
|
"@vercel/og": "^0.5.8",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"is-empty-iterable": "^3.0.0",
|
"is-empty-iterable": "^3.0.0",
|
||||||
"next": "13.4.9",
|
"next": "13.4.10",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-paginate": "^8.2.0"
|
"react-paginate": "^8.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.35.1",
|
"@playwright/test": "^1.36.1",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/node": "^20.4.1",
|
"@types/node": "^20.4.2",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.15",
|
||||||
"@types/react-dom": "18.2.6",
|
"@types/react-dom": "18.2.7",
|
||||||
"@vercel/git-hooks": "^1.0.0",
|
"@vercel/git-hooks": "^1.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-config-next": "^13.4.9",
|
"eslint-config-next": "^13.4.10",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-unicorn": "^47.0.0",
|
"eslint-plugin-unicorn": "^48.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"postcss": "^8.4.25",
|
"postcss": "^8.4.26",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
519
pnpm-lock.yaml
generated
519
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user