mirror of
https://github.com/vercel/commerce.git
synced 2025-04-27 13:27:50 +00:00
parent
3a26bae429
commit
db63db1331
22
.env.example
22
.env.example
@ -1,7 +1,15 @@
|
||||
COMPANY_NAME="Vercel Inc."
|
||||
TWITTER_CREATOR="@vercel"
|
||||
TWITTER_SITE="https://nextjs.org/commerce"
|
||||
SITE_NAME="Next.js Commerce"
|
||||
SHOPIFY_REVALIDATION_SECRET=""
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
|
||||
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
|
||||
COMPANY_NAME= # Company name for eg. Geins
|
||||
SITE_NAME= # Site name for eg. Geins Store
|
||||
GEINS_API_KEY= # API key from Geins for eg. XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX
|
||||
GEINS_ACCOUNT_NAME= # Account name from geins for eg. acme
|
||||
GEINS_CHANNEL= # Channel Id from Geins for eg. 1
|
||||
GEINS_TLD= # Top-level domain for eg. com
|
||||
GEINS_LOCALE= # Locale for eg. en-US
|
||||
GEINS_MARKET= # Market for eg. us
|
||||
GEINS_ENVIRONMENT= # Environment for eg. qa, prod
|
||||
GEINS_PRODUCT_DESCRIPTION_SHORT_TEXT= # Property for Product description from Geins for eg. text1, text2, text3 default is text2
|
||||
GEINS_CURRENCY_CODE= # Currency code for eg. USD
|
||||
GEINS_PAYMENT_ID= # Payment ID from Geins for desired checkout for eg. 23
|
||||
GEINS_USE_CATEGORY_FOR_RECOMMENDATIONS_BACKUP= # Use category for recommendations backup on PDP true/false defalut is true
|
||||
GEINS_SKU_DEFAULT_VARIATION= # Default variation for SKU for eg. Size
|
||||
GEINS_REVALIDATION_SECRET= # Revalidation secret from your Geins webhook for eg. XYZ123-123-123
|
95
README.md
95
README.md
@ -1,73 +1,72 @@
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&env=COMPANY_NAME,SHOPIFY_REVALIDATION_SECRET,SHOPIFY_STORE_DOMAIN,SHOPIFY_STOREFRONT_ACCESS_TOKEN,SITE_NAME,TWITTER_CREATOR,TWITTER_SITE)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgeins-io%2Fvercel-nextjs-commerce&env=GEINS_API_KEY,GEINS_ACCOUNT_NAME,GEINS_CHANNEL,GEINS_TLD,GEINS_LOCALE,GEINS_MARKET,GEINS_IMAGE_URL,GEINS_CURRENCY_CODE,GEINS_CHECKOUT_ID&envDescription=Read%20more%20about%20environment%20varibles%20in%20the%20example%20file&envLink=https%3A%2F%2Fgithub.com%2Fgeins-io%2Fvercel-nextjs-commerce%2Fblob%2Fmain%2F.env.example&project-name=geins-nextjs-commerce-starter&repository-name=geins-nextjs-commerce-starter&demo-title=Geins%20Next.js%20Commerce%20Starter&demo-description=Commerce%20website%20created%20with%20Next.js&demo-url=http%3A%2F%2Fgeins.io&demo-image=avatars.githubusercontent.com%2Fu%2F123540473)
|
||||
|
||||
# Next.js Commerce
|
||||
# Next.js Commerce x Geins
|
||||
|
||||
A high-performance, server-rendered Next.js App Router ecommerce application.
|
||||
A high-performance, server-rendered Next.js (15 RC) App Router ecommerce application.
|
||||
|
||||
This template uses React Server Components, Server Actions, `Suspense`, `useOptimistic`, and more.
|
||||
This template showcases the integration of [Geins Commerce API](https://docs.geins.io) with [Next.js Commerce](https://github.com/vercel/commerce), leveraging the open-source [Geins SDK](https://github.com/geins-io/geins).
|
||||
|
||||
<h3 id="v1-note"></h3>
|
||||
## Features
|
||||
|
||||
> Note: Looking for Next.js Commerce v1? View the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store), and [release notes](https://github.com/vercel/commerce/releases/tag/v1).
|
||||
- **React Server Components**: Build fast and scalable UIs with Next.js's server-first approach.
|
||||
- **Server Actions**: Simplify backend logic and data fetching.
|
||||
- **Modern React APIs**: Including `Suspense` and `useOptimistic`.
|
||||
- **Integration with Geins**: Harness the power of Geins for exceptional ecommerce capabilities.
|
||||
|
||||
## Providers
|
||||
## What is Geins?
|
||||
|
||||
Vercel will only be actively maintaining a Shopify version [as outlined in our vision and strategy for Next.js Commerce](https://github.com/vercel/commerce/pull/966).
|
||||
[Geins](https://geins.io/) is the ultimate toolkit for modern commerce. With Geins, developers and agencies can craft unique, tailored shopping experiences using:
|
||||
|
||||
Vercel is happy to partner and work with any commerce provider to help them get a similar template up and running and listed below. Alternative providers should be able to fork this repository and swap out the `lib/shopify` file with their own implementation while leaving the rest of the template mostly unchanged.
|
||||
- A hybrid model combining the reliability of a managed platform with open-source flexibility.
|
||||
- A robust API-first approach enabling precise customization.
|
||||
- Features for managing channels, content, CRM, events, and more.
|
||||
|
||||
- Shopify (this repository)
|
||||
- [BigCommerce](https://github.com/bigcommerce/nextjs-commerce) ([Demo](https://next-commerce-v2.vercel.app/))
|
||||
- [Ecwid by Lightspeed](https://github.com/Ecwid/ecwid-nextjs-commerce/) ([Demo](https://ecwid-nextjs-commerce.vercel.app/))
|
||||
- [Medusa](https://github.com/medusajs/vercel-commerce) ([Demo](https://medusa-nextjs-commerce.vercel.app/))
|
||||
- [Saleor](https://github.com/saleor/nextjs-commerce) ([Demo](https://saleor-commerce.vercel.app/))
|
||||
- [Shopware](https://github.com/shopwareLabs/vercel-commerce) ([Demo](https://shopware-vercel-commerce-react.vercel.app/))
|
||||
- [Swell](https://github.com/swellstores/verswell-commerce) ([Demo](https://verswell-commerce.vercel.app/))
|
||||
- [Umbraco](https://github.com/umbraco/Umbraco.VercelCommerce.Demo) ([Demo](https://vercel-commerce-demo.umbraco.com/))
|
||||
- [Wix](https://github.com/wix/nextjs-commerce) ([Demo](https://wix-nextjs-commerce.vercel.app/))
|
||||
- [Fourthwall](https://github.com/FourthwallHQ/vercel-commerce) ([Demo](https://vercel-storefront.fourthwall.app/))
|
||||
Explore the [Geins Commerce API documentation](https://docs.geins.io) for detailed usage instructions and capabilities.
|
||||
|
||||
> Note: Providers, if you are looking to use similar products for your demo, you can [download these assets](https://drive.google.com/file/d/1q_bKerjrwZgHwCw0ovfUMW6He9VtepO_/view?usp=sharing).
|
||||
## Getting Started
|
||||
|
||||
## Integrations
|
||||
To run this application locally, follow these steps:
|
||||
|
||||
Integrations enable upgraded or additional functionality for Next.js Commerce
|
||||
### Prerequisites
|
||||
|
||||
- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/))
|
||||
Ensure you have the following installed:
|
||||
|
||||
- Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based configuration.
|
||||
- Search runs entirely in the browser for smaller catalogs or on a CDN for larger.
|
||||
- [Node.js](https://nodejs.org/) (v20 or later)
|
||||
- [Geins API-Key](https://geins.io/)
|
||||
|
||||
- [React Bricks](https://github.com/ReactBricks/nextjs-commerce-rb) ([Demo](https://nextjs-commerce.reactbricks.com/))
|
||||
- Edit pages, product details, and footer content visually using [React Bricks](https://www.reactbricks.com) visual headless CMS.
|
||||
### Environment Variables
|
||||
|
||||
## Running locally
|
||||
Set up your environment variables as defined in `.env.example`. It's recommended to use [Vercel's Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for secure storage.
|
||||
|
||||
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
|
||||
> ⚠️ **Important**: Never commit your `.env` file to version control.
|
||||
|
||||
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Shopify store.
|
||||
### Steps to Run Locally
|
||||
|
||||
1. Install Vercel CLI: `npm i -g vercel`
|
||||
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
|
||||
3. Download your environment variables: `vercel env pull`
|
||||
1. Clone this repository:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/geins-io/vercel-nextjs-commerce.git
|
||||
cd <your-repo>
|
||||
```
|
||||
|
||||
Your app should now be running on [localhost:3000](http://localhost:3000/).
|
||||
2. Install dependencies:
|
||||
|
||||
<details>
|
||||
<summary>Expand if you work at Vercel and want to run locally and / or contribute</summary>
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
1. Run `vc link`.
|
||||
1. Select the `Vercel Solutions` scope.
|
||||
1. Connect to the existing `commerce-shopify` project.
|
||||
1. Run `vc env pull` to get environment variables.
|
||||
1. Run `pnpm dev` to ensure everything is working correctly.
|
||||
</details>
|
||||
3. Link your local instance with Vercel and pull environment variables:
|
||||
|
||||
## Vercel, Next.js Commerce, and Shopify Integration Guide
|
||||
```bash
|
||||
npm i -g vercel
|
||||
vercel link
|
||||
vercel env pull
|
||||
```
|
||||
|
||||
You can use this comprehensive [integration guide](https://vercel.com/docs/integrations/ecommerce/shopify) with step-by-step instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on Vercel.
|
||||
4. Start the development server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
5. Access the app on [http://localhost:3000](http://localhost:3000).
|
||||
|
@ -1,5 +1,5 @@
|
||||
import OpengraphImage from 'components/opengraph-image';
|
||||
import { getPage } from 'lib/shopify';
|
||||
import { getPage } from 'lib/geins';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import Prose from 'components/prose';
|
||||
import { getPage } from 'lib/shopify';
|
||||
import { getPage } from 'lib/geins';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
@ -33,6 +32,7 @@ export default async function Page(props: { params: Promise<{ page: string }> })
|
||||
<>
|
||||
<h1 className="mb-8 text-5xl font-bold">{page.title}</h1>
|
||||
<Prose className="mb-8" html={page.body} />
|
||||
|
||||
<p className="text-sm italic">
|
||||
{`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { revalidate } from 'lib/shopify';
|
||||
import { revalidate } from 'lib/geins';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
|
@ -2,7 +2,7 @@ import { CartProvider } from 'components/cart/cart-context';
|
||||
import { Navbar } from 'components/layout/navbar';
|
||||
import { WelcomeToast } from 'components/welcome-toast';
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { getCart } from 'lib/geins';
|
||||
import { ensureStartsWith } from 'lib/utils';
|
||||
import { cookies } from 'next/headers';
|
||||
import { ReactNode } from 'react';
|
||||
|
@ -7,8 +7,8 @@ import { Gallery } from 'components/product/gallery';
|
||||
import { ProductProvider } from 'components/product/product-context';
|
||||
import { ProductDescription } from 'components/product/product-description';
|
||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||
import { getProduct, getProductRecommendations } from 'lib/shopify';
|
||||
import { Image } from 'lib/shopify/types';
|
||||
import { getProduct, getProductRecommendations } from 'lib/geins';
|
||||
import { ProductImageType, ProductType } from 'lib/geins/types';
|
||||
import Link from 'next/link';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
@ -89,7 +89,7 @@ export default async function ProductPage(props: { params: Promise<{ handle: str
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
images={product.images.slice(0, 5).map((image: Image) => ({
|
||||
images={product.images.slice(0, 5).map((image: ProductImageType) => ({
|
||||
src: image.url,
|
||||
altText: image.altText
|
||||
}))}
|
||||
@ -103,15 +103,15 @@ export default async function ProductPage(props: { params: Promise<{ handle: str
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<RelatedProducts id={product.id} />
|
||||
<RelatedProducts product={product} />
|
||||
</div>
|
||||
<Footer />
|
||||
</ProductProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function RelatedProducts({ id }: { id: string }) {
|
||||
const relatedProducts = await getProductRecommendations(id);
|
||||
async function RelatedProducts({ product }: { product: ProductType }) {
|
||||
const relatedProducts = await getProductRecommendations(product);
|
||||
|
||||
if (!relatedProducts.length) return null;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import OpengraphImage from 'components/opengraph-image';
|
||||
import { getCollection } from 'lib/shopify';
|
||||
import { getCollection } from 'lib/geins';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCollection, getCollectionProducts } from 'lib/shopify';
|
||||
import { getCollection, getCollectionProducts } from 'lib/geins';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -11,7 +11,6 @@ export async function generateMetadata(props: {
|
||||
}): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const collection = await getCollection(params.collection);
|
||||
|
||||
if (!collection) return notFound();
|
||||
|
||||
return {
|
||||
@ -29,6 +28,7 @@ export default async function CategoryPage(props: {
|
||||
const params = await props.params;
|
||||
const { sort } = searchParams as { [key: string]: string };
|
||||
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
|
||||
|
||||
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Grid from 'components/grid';
|
||||
import ProductGridItems from 'components/layout/product-grid-items';
|
||||
import { defaultSort, sorting } from 'lib/constants';
|
||||
import { getProducts } from 'lib/shopify';
|
||||
import { getProducts } from 'lib/geins';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Search',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { getCollections, getPages, getProducts } from 'lib/shopify';
|
||||
import { validateEnvironmentVariables } from 'lib/utils';
|
||||
import { getCollections, getPages, getProducts } from 'lib/geins';
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
type Route = {
|
||||
@ -14,7 +13,7 @@ const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
validateEnvironmentVariables();
|
||||
// validateEnvironmentVariables();
|
||||
|
||||
const routesMap = [''].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
@ -24,21 +23,21 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const collectionsPromise = getCollections().then((collections) =>
|
||||
collections.map((collection) => ({
|
||||
url: `${baseUrl}${collection.path}`,
|
||||
lastModified: collection.updatedAt
|
||||
lastModified: collection.updatedAt ?? new Date().toISOString()
|
||||
}))
|
||||
);
|
||||
|
||||
const productsPromise = getProducts({}).then((products) =>
|
||||
products.map((product) => ({
|
||||
url: `${baseUrl}/product/${product.handle}`,
|
||||
lastModified: product.updatedAt
|
||||
lastModified: product.updatedAt ?? new Date().toISOString()
|
||||
}))
|
||||
);
|
||||
|
||||
const pagesPromise = getPages().then((pages) =>
|
||||
pages.map((page) => ({
|
||||
url: `${baseUrl}/${page.handle}`,
|
||||
lastModified: page.updatedAt
|
||||
lastModified: page.updatedAt ?? new Date().toISOString()
|
||||
}))
|
||||
);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import { getCollectionProducts } from 'lib/geins';
|
||||
import Link from 'next/link';
|
||||
import { GridTileImage } from './grid/tile';
|
||||
|
||||
@ -27,7 +27,7 @@ export async function Carousel() {
|
||||
amount: product.priceRange.maxVariantPrice.amount,
|
||||
currencyCode: product.priceRange.maxVariantPrice.currencyCode
|
||||
}}
|
||||
src={product.featuredImage?.url}
|
||||
src={product.featuredImage?.url || ''}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw"
|
||||
/>
|
||||
|
@ -1,14 +1,14 @@
|
||||
'use server';
|
||||
|
||||
import { TAGS } from 'lib/constants';
|
||||
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
|
||||
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/geins';
|
||||
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
|
||||
let cartId = (await cookies()).get('cartId')?.value;
|
||||
|
||||
if (!cartId || !selectedVariantId) {
|
||||
return 'Error adding item to cart';
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { PlusIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
import { addItem } from 'components/cart/actions';
|
||||
import { useProduct } from 'components/product/product-context';
|
||||
import { Product, ProductVariant } from 'lib/shopify/types';
|
||||
import { ProductType, ProductVariantType } from 'lib/geins/types';
|
||||
import { useActionState } from 'react';
|
||||
import { useCart } from './cart-context';
|
||||
|
||||
@ -27,7 +27,6 @@ function SubmitButton({
|
||||
);
|
||||
}
|
||||
|
||||
console.log(selectedVariantId);
|
||||
if (!selectedVariantId) {
|
||||
return (
|
||||
<button
|
||||
@ -58,20 +57,19 @@ function SubmitButton({
|
||||
);
|
||||
}
|
||||
|
||||
export function AddToCart({ product }: { product: Product }) {
|
||||
export function AddToCart({ product }: { product: ProductType }) {
|
||||
const { variants, availableForSale } = product;
|
||||
const { addCartItem } = useCart();
|
||||
const { state } = useProduct();
|
||||
const [message, formAction] = useActionState(addItem, null);
|
||||
|
||||
const variant = variants.find((variant: ProductVariant) =>
|
||||
const variant = variants.find((variant: ProductVariantType) =>
|
||||
variant.selectedOptions.every((option) => option.value === state[option.name.toLowerCase()])
|
||||
);
|
||||
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
|
||||
const selectedVariantId = variant?.id || defaultVariantId;
|
||||
const actionWithVariant = formAction.bind(null, selectedVariantId);
|
||||
const finalVariant = variants.find((variant) => variant.id === selectedVariantId)!;
|
||||
|
||||
return (
|
||||
<form
|
||||
action={async () => {
|
||||
|
@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import type { Cart, CartItem, Product, ProductVariant } from 'lib/shopify/types';
|
||||
import type { CartItemType, CartType, ProductType, ProductVariantType } from 'lib/geins/types';
|
||||
import React, { createContext, use, useContext, useMemo, useOptimistic } from 'react';
|
||||
|
||||
type UpdateType = 'plus' | 'minus' | 'delete';
|
||||
|
||||
type CartAction =
|
||||
| { type: 'UPDATE_ITEM'; payload: { merchandiseId: string; updateType: UpdateType } }
|
||||
| { type: 'ADD_ITEM'; payload: { variant: ProductVariant; product: Product } };
|
||||
| { type: 'ADD_ITEM'; payload: { variant: ProductVariantType; product: ProductType } };
|
||||
|
||||
type CartContextType = {
|
||||
cart: Cart | undefined;
|
||||
cart: CartType | undefined;
|
||||
updateCartItem: (merchandiseId: string, updateType: UpdateType) => void;
|
||||
addCartItem: (variant: ProductVariant, product: Product) => void;
|
||||
addCartItem: (variant: ProductVariantType, product: ProductType) => void;
|
||||
};
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
@ -21,7 +21,7 @@ function calculateItemCost(quantity: number, price: string): string {
|
||||
return (Number(price) * quantity).toString();
|
||||
}
|
||||
|
||||
function updateCartItem(item: CartItem, updateType: UpdateType): CartItem | null {
|
||||
function updateCartItem(item: CartItemType, updateType: UpdateType): CartItemType | null {
|
||||
if (updateType === 'delete') return null;
|
||||
|
||||
const newQuantity = updateType === 'plus' ? item.quantity + 1 : item.quantity - 1;
|
||||
@ -44,10 +44,10 @@ function updateCartItem(item: CartItem, updateType: UpdateType): CartItem | null
|
||||
}
|
||||
|
||||
function createOrUpdateCartItem(
|
||||
existingItem: CartItem | undefined,
|
||||
variant: ProductVariant,
|
||||
product: Product
|
||||
): CartItem {
|
||||
existingItem: CartItemType | undefined,
|
||||
variant: ProductVariantType,
|
||||
product: ProductType
|
||||
): CartItemType {
|
||||
const quantity = existingItem ? existingItem.quantity + 1 : 1;
|
||||
const totalAmount = calculateItemCost(quantity, variant.price.amount);
|
||||
|
||||
@ -74,7 +74,7 @@ function createOrUpdateCartItem(
|
||||
};
|
||||
}
|
||||
|
||||
function updateCartTotals(lines: CartItem[]): Pick<Cart, 'totalQuantity' | 'cost'> {
|
||||
function updateCartTotals(lines: CartItemType[]): Pick<CartType, 'totalQuantity' | 'cost'> {
|
||||
const totalQuantity = lines.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = lines.reduce((sum, item) => sum + Number(item.cost.totalAmount.amount), 0);
|
||||
const currencyCode = lines[0]?.cost.totalAmount.currencyCode ?? 'USD';
|
||||
@ -89,7 +89,7 @@ function updateCartTotals(lines: CartItem[]): Pick<Cart, 'totalQuantity' | 'cost
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptyCart(): Cart {
|
||||
function createEmptyCart(): CartType {
|
||||
return {
|
||||
id: undefined,
|
||||
checkoutUrl: '',
|
||||
@ -103,7 +103,7 @@ function createEmptyCart(): Cart {
|
||||
};
|
||||
}
|
||||
|
||||
function cartReducer(state: Cart | undefined, action: CartAction): Cart {
|
||||
function cartReducer(state: CartType | undefined, action: CartAction): CartType {
|
||||
const currentCart = state || createEmptyCart();
|
||||
|
||||
switch (action.type) {
|
||||
@ -113,7 +113,7 @@ function cartReducer(state: Cart | undefined, action: CartAction): Cart {
|
||||
.map((item) =>
|
||||
item.merchandise.id === merchandiseId ? updateCartItem(item, updateType) : item
|
||||
)
|
||||
.filter(Boolean) as CartItem[];
|
||||
.filter(Boolean) as CartItemType[];
|
||||
|
||||
if (updatedLines.length === 0) {
|
||||
return {
|
||||
@ -150,7 +150,7 @@ export function CartProvider({
|
||||
cartPromise
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
cartPromise: Promise<Cart | undefined>;
|
||||
cartPromise: Promise<CartType | undefined>;
|
||||
}) {
|
||||
const initialCart = use(cartPromise);
|
||||
const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer);
|
||||
@ -159,7 +159,7 @@ export function CartProvider({
|
||||
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } });
|
||||
};
|
||||
|
||||
const addCartItem = (variant: ProductVariant, product: Product) => {
|
||||
const addCartItem = (variant: ProductVariantType, product: ProductType) => {
|
||||
updateOptimisticCart({ type: 'ADD_ITEM', payload: { variant, product } });
|
||||
};
|
||||
|
||||
@ -180,5 +180,6 @@ export function useCart() {
|
||||
if (context === undefined) {
|
||||
throw new Error('useCart must be used within a CartProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
@ -2,14 +2,14 @@
|
||||
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { removeItem } from 'components/cart/actions';
|
||||
import type { CartItem } from 'lib/shopify/types';
|
||||
import type { CartItemType } from 'lib/geins/types';
|
||||
import { useActionState } from 'react';
|
||||
|
||||
export function DeleteItemButton({
|
||||
item,
|
||||
optimisticUpdate
|
||||
}: {
|
||||
item: CartItem;
|
||||
item: CartItemType;
|
||||
optimisticUpdate: any;
|
||||
}) {
|
||||
const [message, formAction] = useActionState(removeItem, null);
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
import { updateItemQuantity } from 'components/cart/actions';
|
||||
import type { CartItem } from 'lib/shopify/types';
|
||||
import type { CartItemType } from 'lib/geins/types';
|
||||
import { useActionState } from 'react';
|
||||
|
||||
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
|
||||
@ -32,7 +32,7 @@ export function EditItemQuantityButton({
|
||||
type,
|
||||
optimisticUpdate
|
||||
}: {
|
||||
item: CartItem;
|
||||
item: CartItemType;
|
||||
type: 'plus' | 'minus';
|
||||
optimisticUpdate: any;
|
||||
}) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import type { Product } from 'lib/shopify/types';
|
||||
import { getCollectionProducts } from 'lib/geins';
|
||||
import type { ProductType } from 'lib/geins/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
function ThreeItemGridItem({
|
||||
@ -8,7 +8,7 @@ function ThreeItemGridItem({
|
||||
size,
|
||||
priority
|
||||
}: {
|
||||
item: Product;
|
||||
item: ProductType;
|
||||
size: 'full' | 'half';
|
||||
priority?: boolean;
|
||||
}) {
|
||||
|
81
components/icons/geins.tsx
Normal file
81
components/icons/geins.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
export default function GeinsIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 726.5 271.2"
|
||||
xmlSpace="preserve"
|
||||
className={className}
|
||||
style={{ color: 'black' }}
|
||||
>
|
||||
<path
|
||||
d="M292.6,133.4c-10,0-18.1,8.1-18.1,18.1v0.2c0.3,4.1-0.7,7.4-2.9,9.7c-3.4,3.6-8.9,4.4-10.9,4.5c-0.3,0-33.3,0-33.3,0
|
||||
c-0.1,0-7.5-0.1-11.7-4.5c-2.2-2.3-3.2-5.6-2.9-9.7V87v-0.2c-0.3-4.1,0.7-7.4,2.9-9.7c4.2-4.5,11.6-4.5,11.7-4.5h30.1l0,0
|
||||
c1.5,0,3.1,0,4.2,0c5.9,0.9,11.9,4.6,12.3,13.3V86c0.9,12.6-6.4,15-12,15.3c-0.9,0-17.4-0.1-17.4-0.1c-10,0-18.1,8.1-18.1,18.1
|
||||
s8.1,18.2,18.1,18.2h15.3c10,0,18.1-8.1,18.1-18.2c0-0.4,0.1-1.5,0.1-1.8c1.2-12.4,10.9-11.8,14.1-12.3h0.2c10,0,18.1-8.1,18.1-18.1
|
||||
s-8.1-18.1-18.1-18.1h-0.2c-2.7,0-13.2-0.5-14.2-12.6c0-0.7,0-1.8,0-2.2c0-10-8.1-17.8-18.1-17.8h-32.5c-10,0-18.1,8.1-18.1,18
|
||||
C207.7,69.1,196,69,194.7,68.9h-0.2c-10,0-18.1,8.1-18.1,18.1v64.5c0,10,8.1,18.1,18.1,18.1h0.2c1.3-0.1,13-0.2,14.4,14.6
|
||||
c0.1,8.4,5.8,15.4,13.6,17.4c0,0,0,0,0,0c0.2,0,0.4,0.1,0.5,0.1c0.3,0.1,0.6,0.1,0.9,0.2c1.1,0.2,2.1,0.4,3.3,0.4h32.5
|
||||
c10,0,18.1-8.1,18.1-18c1.4-14.8,13.1-14.7,14.4-14.6h0.2c10,0,18.1-8.1,18.1-18.1S302.6,133.4,292.6,133.4L292.6,133.4z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M536.9,68.5h-0.2c-1.3,0.1-13,0.2-14.4-14.6c-0.1-7.3-4.5-13.6-10.7-16.4c0,0-2.7-1.6-7.2-1.6H472c-10,0-18.1,8.1-18.1,18
|
||||
c-1.4,14.8-13.1,14.7-14.4,14.6h-0.2c-10,0-18.1,8.1-18.1,18.1v97.2c0,10,8.1,18.1,18.1,18.1s18.1-8.1,18.1-18.1V86.5
|
||||
c-0.3-4.1,0.7-7.4,2.9-9.7c4.2-4.5,11.6-4.5,11.7-4.5h32.2c0.1,0,7.5,0.1,11.7,4.5c2.2,2.3,3.2,5.6,2.9,9.7v12.1l0,0v52.9
|
||||
c0,0.1-0.1,7.5-4.5,11.7c-2.3,2.2-5.6,3.2-9.7,2.9h-0.2c-10,0-18.1,8.1-18.1,18.1s8.1,18.1,18.1,18.1s18.1-8.1,18.1-18.1v-0.2
|
||||
c-0.1-1.3-0.2-13,14.6-14.4c9.9-0.1,18-8.2,18-18.1v-20.4l0,0V86.7C555.1,76.7,546.9,68.5,536.9,68.5L536.9,68.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M365.7,82.1c-10,0-18.1,8.1-18.1,18.1v84c0,10,8.1,18.1,18.1,18.1s18.1-8.1,18.1-18.1v-84C383.9,90.2,375.7,82.1,365.7,82.1
|
||||
z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M708.4,133.8h-0.2c-0.5,0-13,0.6-14.4-14.7c0-0.1,0-0.2,0-0.2c-0.3-9.7-8.4-17.6-18.1-17.6h-32.6c-0.8,0-7.6-0.3-11.6-4.5
|
||||
c-2.2-2.3-2.9-5.6-2.9-9.7v-0.4c0-4.1,0.7-7.4,2.9-9.7c4.2-4.5,11.6-4.5,11.7-4.5l32.3,0c10,0,18.2-8.1,18.2-18.2c0-0.1,0-0.2,0-0.3
|
||||
c0,0,0.1,0,0.1,0c-0.2-9.8-8.2-17.8-18.1-17.8h-32.5c-10,0-18.1,8.1-18.1,18c-1.4,14.8-13.1,14.7-14.4,14.6h-0.2
|
||||
c-10,0-18.1,8.1-18.1,18.1s8.1,18.1,18.1,18.1h0.2c0.5,0,12.9-0.6,14.4,14.6c0.1,9.9,8.2,18,18.1,18h32.5c0.5,0,7.5,0.1,11.7,4.5
|
||||
c2.2,2.3,2.9,5,2.9,9.3v0.6c0,4.1-0.7,7.5-2.9,9.9c-4.1,4.4-11.4,4.6-11.7,4.6h-32.3c-0.1,0-7.5-0.1-11.7-4.5
|
||||
c-2.2-2.3-3.2-5.5-2.9-9.6l0-0.4c0-10-8.1-18.1-18.1-18.1s-18.1,8.1-18.1,18.1s8.1,18.1,18.1,18.1h0.3c1.6,0,12.9-0.2,14.3,14.6
|
||||
c0.1,9.9,8.2,18,18.1,18h32.5c10,0,18.1-8.1,18.1-18.1c0-0.5,0-0.9,0-1.3c2-13.3,13-13.2,14.2-13.1h0.2c10,0,18.1-8.1,18.1-18.1
|
||||
S718.4,133.7,708.4,133.8L708.4,133.8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M365.7,36c-10.1,0-18.2,8.2-18.2,18.2s8.2,18.2,18.2,18.2S384,64.3,384,54.2S375.8,36,365.7,36z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M120.8,68.5h-0.2c-1.1,0.1-8.9,0.1-12.6-7.7c0,0,0-0.1-0.1-0.1c-0.1-0.2-0.2-0.5-0.3-0.7c0-0.1-0.1-0.2-0.1-0.3
|
||||
c-0.1-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.1-0.3-0.2-0.5s-0.1-0.3-0.1-0.5c-0.1-0.2-0.1-0.4-0.2-0.7c0-0.1-0.1-0.2-0.1-0.4
|
||||
c-0.1-0.3-0.1-0.6-0.2-0.9c0-0.1,0-0.1,0-0.2c-0.1-0.4-0.1-0.7-0.2-1.1c0-0.3-0.1-0.7-0.1-1c-0.1-7.3-4.4-13.6-10.7-16.4
|
||||
c0,0-2.9-1.4-5.6-1.5c-0.4,0-1.4-0.1-1.6-0.1H55.8c-10,0-18.1,8.1-18.1,18c-1.4,14.8-13.1,14.7-14.4,14.6h-0.2
|
||||
c-10,0-18.1,8.1-18.1,18.1v65.2c0,10,8.1,18.1,18.1,18.1h0.2c1.7,0,12-0.2,14.2,12.8l0,1.8C36,199.5,25.2,198.7,22.9,199h-0.2
|
||||
c-10.7,0.3-18.1,8.1-18.1,18.1s8.1,18.1,18.1,18.1c0.4,0,0.6,0,1.1,0c3,0.1,12.3,1.6,13.5,14.6c0.1,8.4,5.8,15.4,13.6,17.4
|
||||
c0,0,0,0,0,0c0.2,0,0.4,0.1,0.5,0.1c0.3,0.1,0.6,0.1,0.9,0.2c1.1,0.2,2.2,0.4,3.3,0.4H88c9.9,0,18.1-8.1,18.1-18
|
||||
c1.4-14.8,13.1-14.7,14.4-14.6h0.2c10,0,18.1-8.1,18.1-18.1s-8.1-18.1-18.1-18.1s-18.1,8.1-18.1,18.1v0.2c0.3,4.1-0.7,7.4-2.9,9.7
|
||||
c-3.4,3.6-8.9,4.4-10.9,4.5c-0.3,0-33.3,0-33.3,0c-0.1,0-7.5-0.1-11.7-4.5c-2.2-2.3-2.9-5.6-2.9-9.7v-2.7c0.2-3,1.2-5.5,2.9-7.3
|
||||
c3.4-3.6,8.9-4.8,10.9-5c0.2,0,33.3,0,33.3,0c10,0,18.1-7.7,18.1-17.6c1.4-14.8,13.1-14.7,14.4-14.6h0.2c10,0,18.1-8.1,18.1-18.1
|
||||
V86.7C138.9,76.7,130.8,68.5,120.8,68.5L120.8,68.5z M99.7,161.8c-4.2,4.5-11.6,4.5-11.7,4.5H55.8c-0.1,0-7.5-0.1-11.7-4.5
|
||||
c-2.2-2.3-3.2-5.6-2.9-9.7v-0.2V86.7v-0.2v0c0-0.5,0-1,0-1.5c0-3.4,1-6.2,2.9-8.2c3.5-3.7,9.2-4.4,11.1-4.5c0.1,0,0.2,0,0.4,0h32.5
|
||||
c0.2,0,0.3,0,0.5,0c1.7,0.1,7.6,0.7,11.2,4.5c1.9,2,2.9,4.8,2.9,8.2c0,0.5,0,1,0,1.5v0.2v65.2v0.2
|
||||
C102.9,156.2,101.9,159.5,99.7,161.8L99.7,161.8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M120.9,39.9c10.1,0,18.2-8.2,18.2-18.2S131,3.4,120.9,3.4s-18.2,8.2-18.2,18.2S110.8,39.9,120.9,39.9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M708.3,104.8c10.1,0,18.2-8.2,18.2-18.2s-8.2-18.2-18.2-18.2s-18.2,8.2-18.2,18.2S698.3,104.8,708.3,104.8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { MenuItemType } from 'lib/geins/types';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function FooterMenuItem({ item }: { item: Menu }) {
|
||||
export function FooterMenuItem({ item }: { item: MenuItemType }) {
|
||||
const pathname = usePathname();
|
||||
const [active, setActive] = useState(pathname === item.path);
|
||||
|
||||
@ -31,13 +31,13 @@ export function FooterMenuItem({ item }: { item: Menu }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function FooterMenu({ menu }: { menu: Menu[] }) {
|
||||
export default function FooterMenu({ menu }: { menu: MenuItemType[] }) {
|
||||
if (!menu.length) return null;
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<ul>
|
||||
{menu.map((item: Menu) => {
|
||||
{menu.map((item: MenuItemType) => {
|
||||
return <FooterMenuItem key={item.title} item={item} />;
|
||||
})}
|
||||
</ul>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import GeinsIcon from 'components/icons/geins';
|
||||
import FooterMenu from 'components/layout/footer-menu';
|
||||
import LogoSquare from 'components/logo-square';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { getMenu } from 'lib/geins';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
const { COMPANY_NAME, SITE_NAME } = process.env;
|
||||
@ -41,7 +42,7 @@ export default async function Footer() {
|
||||
<a
|
||||
className="flex h-8 w-max flex-none items-center justify-center rounded-md border border-neutral-200 bg-white text-xs text-black dark:border-neutral-700 dark:bg-black dark:text-white"
|
||||
aria-label="Deploy on Vercel"
|
||||
href="https://vercel.com/templates/next.js/nextjs-commerce"
|
||||
href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgeins-io%2Fvercel-nextjs-commerce&env=GEINS_API_KEY,GEINS_ACCOUNT_NAME,GEINS_CHANNEL,GEINS_TLD,GEINS_LOCALE,GEINS_MARKET,GEINS_IMAGE_URL,GEINS_CURRENCY_CODE,GEINS_CHECKOUT_ID&envDescription=Read%20more%20about%20environment%20varibles%20in%20the%20example%20file&envLink=https%3A%2F%2Fgithub.com%2Fgeins-io%2Fvercel-nextjs-commerce%2Fblob%2Fmain%2F.env.example&project-name=geins-nextjs-commerce-starter&repository-name=geins-nextjs-commerce-starter&demo-title=Geins%20Next.js%20Commerce%20Starter&demo-description=Commerce%20website%20created%20with%20Next.js&demo-url=http%3A%2F%2Fgeins.io&demo-image=avatars.githubusercontent.com%2Fu%2F123540473"
|
||||
>
|
||||
<span className="px-3">▲</span>
|
||||
<hr className="h-full border-r border-neutral-200 dark:border-neutral-700" />
|
||||
@ -57,11 +58,11 @@ export default async function Footer() {
|
||||
</p>
|
||||
<hr className="mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block" />
|
||||
<p>
|
||||
<a href="https://github.com/vercel/commerce">View the source</a>
|
||||
<a href="https://github.com/geins-io/vercel-nextjs-commerce">View the source</a>
|
||||
</p>
|
||||
<p className="md:ml-auto">
|
||||
<a href="https://vercel.com" className="text-black dark:text-white">
|
||||
Created by ▲ Vercel
|
||||
<a href="https://geins.io" className="flex items-center text-black dark:text-white">
|
||||
Powered by <GeinsIcon className="ml-1 h-5 fill-current align-middle" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import CartModal from 'components/cart/modal';
|
||||
import LogoSquare from 'components/logo-square';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { getMenu } from 'lib/geins';
|
||||
import { MenuItemType } from 'lib/geins/types';
|
||||
import Link from 'next/link';
|
||||
import { Suspense } from 'react';
|
||||
import MobileMenu from './mobile-menu';
|
||||
@ -33,7 +33,7 @@ export async function Navbar() {
|
||||
</Link>
|
||||
{menu.length ? (
|
||||
<ul className="hidden gap-6 text-sm md:flex md:items-center">
|
||||
{menu.map((item: Menu) => (
|
||||
{menu.map((item: MenuItemType) => (
|
||||
<li key={item.title}>
|
||||
<Link
|
||||
href={item.path}
|
||||
|
@ -6,10 +6,10 @@ import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { MenuItemType } from 'lib/geins/types';
|
||||
import Search, { SearchSkeleton } from './search';
|
||||
|
||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
export default function MobileMenu({ menu }: { menu: MenuItemType[] }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -78,7 +78,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
</div>
|
||||
{menu.length ? (
|
||||
<ul className="flex w-full flex-col">
|
||||
{menu.map((item: Menu) => (
|
||||
{menu.map((item: MenuItemType) => (
|
||||
<li
|
||||
className="py-2 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white"
|
||||
key={item.title}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import Grid from 'components/grid';
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { ProductType } from 'lib/geins/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ProductGridItems({ products }: { products: Product[] }) {
|
||||
export default function ProductGridItems({ products }: { products: ProductType[] }) {
|
||||
return (
|
||||
<>
|
||||
{products.map((product) => (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { getCollections } from 'lib/shopify';
|
||||
import { getCollections } from 'lib/geins';
|
||||
import FilterList from './filter';
|
||||
|
||||
async function CollectionList() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
const locale = process.env.GEINS_LOCALE || 'en-US';
|
||||
const Price = ({
|
||||
amount,
|
||||
className,
|
||||
@ -12,7 +12,7 @@ const Price = ({
|
||||
currencyCodeClassName?: string;
|
||||
} & React.ComponentProps<'p'>) => (
|
||||
<p suppressHydrationWarning={true} className={className}>
|
||||
{`${new Intl.NumberFormat(undefined, {
|
||||
{`${new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
currencyDisplay: 'narrowSymbol'
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AddToCart } from 'components/cart/add-to-cart';
|
||||
import Price from 'components/price';
|
||||
import Prose from 'components/prose';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { ProductType } from 'lib/geins/types';
|
||||
import { VariantSelector } from './variant-selector';
|
||||
|
||||
export function ProductDescription({ product }: { product: Product }) {
|
||||
export function ProductDescription({ product }: { product: ProductType }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useProduct, useUpdateURL } from 'components/product/product-context';
|
||||
import { ProductOption, ProductVariant } from 'lib/shopify/types';
|
||||
import { ProductOptionType, ProductVariantType } from 'lib/geins/types';
|
||||
|
||||
type Combination = {
|
||||
id: string;
|
||||
@ -14,8 +14,8 @@ export function VariantSelector({
|
||||
options,
|
||||
variants
|
||||
}: {
|
||||
options: ProductOption[];
|
||||
variants: ProductVariant[];
|
||||
options: ProductOptionType[];
|
||||
variants: ProductVariantType[];
|
||||
}) {
|
||||
const { state, updateOption } = useProduct();
|
||||
const updateURL = useUpdateURL();
|
||||
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
||||
const Prose = ({ html, className }: { html: string; className?: string }) => {
|
||||
return (
|
||||
<div
|
||||
suppressHydrationWarning={true}
|
||||
className={clsx(
|
||||
'prose mx-auto max-w-6xl text-base leading-7 text-black prose-headings:mt-8 prose-headings:font-semibold prose-headings:tracking-wide prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg prose-a:text-black prose-a:underline hover:prose-a:text-neutral-300 prose-strong:text-black prose-ol:mt-8 prose-ol:list-decimal prose-ol:pl-6 prose-ul:mt-8 prose-ul:list-disc prose-ul:pl-6 dark:text-white dark:prose-headings:text-white dark:prose-a:text-white dark:prose-strong:text-white',
|
||||
className
|
||||
|
@ -16,7 +16,7 @@ export function WelcomeToast() {
|
||||
},
|
||||
description: (
|
||||
<>
|
||||
This is a high-performance, SSR storefront powered by Shopify, Next.js, and Vercel.{' '}
|
||||
This is a high-performance, SSR storefront powered by Geins, Next.js, and Vercel.{' '}
|
||||
<a
|
||||
href="https://vercel.com/templates/next.js/nextjs-commerce"
|
||||
className="text-blue-600 hover:underline"
|
||||
|
26
lib/geins/cms.ts
Normal file
26
lib/geins/cms.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { GeinsCMS } from '@geins/cms';
|
||||
import { GeinsCore } from '@geins/core';
|
||||
import { GeinsMenuType } from '@geins/types';
|
||||
import { reshapeMenu, reshapePage } from './reshape';
|
||||
import { MenuItemType } from './types';
|
||||
|
||||
export const getMenu = async (
|
||||
geinsCore: GeinsCore,
|
||||
locationId: string
|
||||
): Promise<MenuItemType[]> => {
|
||||
const geinsCMS = new GeinsCMS(geinsCore);
|
||||
const menu = await geinsCMS.menu.get({ menuLocationId: locationId }).then((result) => {
|
||||
return result as GeinsMenuType;
|
||||
});
|
||||
|
||||
return reshapeMenu(menu, locationId);
|
||||
};
|
||||
|
||||
export const getPage = async (geinsCore: GeinsCore, alias: string) => {
|
||||
const geinsCMS = new GeinsCMS(geinsCore);
|
||||
const data = await geinsCMS.page.get({ alias }).then((result) => {
|
||||
return result;
|
||||
});
|
||||
|
||||
return reshapePage(data, alias);
|
||||
};
|
9
lib/geins/constants.ts
Normal file
9
lib/geins/constants.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const USE_CATEGORY_FOR_RECOMMENDATIONS_BACKUP =
|
||||
process.env.GEINS_USE_CATEGORY_FOR_RECOMMENDATIONS_BACKUP || true;
|
||||
export const IMAGE_URL =
|
||||
process.env.GEINS_IMAGE_URL || `https://${process.env.GEINS_ACCOUNT_NAME}.commerce.services`;
|
||||
export const DEFAULT_SKU_VARIATION = process.env.GEINS_SKU_DEFAULT_VARIATION || 'Size';
|
||||
export const CURRENCY_CODE = process.env.GEINS_CURRENCY_CODE || 'USD';
|
||||
export const PAYMENT_ID = parseInt(process.env.GEINS_PAYMENT_ID ?? '0');
|
||||
export const SHORT_DESCRIPTION = process.env.GEINS_PRODUCT_DESCRIPTION_SHORT_TEXT || 'text2';
|
||||
export const LONG_DESCRIPTION = process.env.GEINS_PRODUCT_DESCRIPTION_LONG_TEXT || 'text1';
|
214
lib/geins/index.ts
Normal file
214
lib/geins/index.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { GeinsCore, GeinsSettings } from '@geins/core';
|
||||
import { TAGS } from 'lib/constants';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import * as cms from './cms';
|
||||
import * as oms from './oms';
|
||||
import * as pim from './pim';
|
||||
import { CartItemInputType, CartType, MenuItemType, PageType, ProductType } from './types';
|
||||
|
||||
const geinsSettings: GeinsSettings = {
|
||||
apiKey: process.env.GEINS_API_KEY || '',
|
||||
accountName: process.env.GEINS_ACCOUNT_NAME || '',
|
||||
channel: process.env.GEINS_CHANNEL || '',
|
||||
tld: process.env.GEINS_TLD || '',
|
||||
locale: process.env.GEINS_LOCALE || '',
|
||||
market: process.env.GEINS_MARKET || '',
|
||||
environment: 'qa'
|
||||
};
|
||||
const geinsCore = new GeinsCore(geinsSettings);
|
||||
|
||||
/*
|
||||
COLLECTIONS / CATEFORIES
|
||||
*/
|
||||
export const getCollections = async (parentNodeId: number = 0) => {
|
||||
const data = await pim.getCategories(geinsCore, parentNodeId);
|
||||
data.forEach((item) => {
|
||||
item.path = `/search/${item.slug}`;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCollection = async (slug: string): Promise<any> => {
|
||||
return pim.getCategoryMetadata(geinsCore, slug);
|
||||
};
|
||||
|
||||
/*
|
||||
PRODUCT LISTS
|
||||
*/
|
||||
export async function getProducts({
|
||||
query,
|
||||
reverse,
|
||||
sortKey
|
||||
}: {
|
||||
query?: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}): Promise<ProductType[]> {
|
||||
return await pim.getProducts(geinsCore, { query: query, reverse, sortKey });
|
||||
}
|
||||
|
||||
export async function getCollectionProducts({
|
||||
collection,
|
||||
reverse,
|
||||
sortKey
|
||||
}: {
|
||||
collection: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}): Promise<ProductType[]> {
|
||||
let category = collection;
|
||||
if (collection === 'hidden-homepage-featured-items') {
|
||||
category = 'start';
|
||||
} else if (collection === 'hidden-homepage-carousel') {
|
||||
category = 'carousel-on-start';
|
||||
}
|
||||
|
||||
return await pim.getCategoryProducts(geinsCore, { category, reverse, sortKey });
|
||||
}
|
||||
|
||||
/*
|
||||
PRODUCT
|
||||
*/
|
||||
export const getProduct = async (slug: string) => {
|
||||
return pim.getProduct(geinsCore, slug);
|
||||
};
|
||||
|
||||
export async function getProductRecommendations(product: ProductType): Promise<ProductType[]> {
|
||||
return pim.getProductRecommendations(geinsCore, product);
|
||||
}
|
||||
|
||||
/*
|
||||
CMS
|
||||
*/
|
||||
export const getMenu = async (id: string): Promise<MenuItemType[]> => {
|
||||
let menuId = '';
|
||||
if (id === 'next-js-frontend-footer-menu') {
|
||||
menuId = 'footer-first';
|
||||
} else if (id === 'next-js-frontend-header-menu') {
|
||||
menuId = 'main-desktop';
|
||||
}
|
||||
const data: MenuItemType[] = await cms.getMenu(geinsCore, menuId);
|
||||
return data.filter((item) => item.title !== '');
|
||||
};
|
||||
|
||||
export async function getPages(): Promise<PageType[]> {
|
||||
return [] as PageType[];
|
||||
}
|
||||
|
||||
export async function getPage(handle: string): Promise<PageType> {
|
||||
if (handle === 'checkout') {
|
||||
let cartId = (await cookies()).get('cartId')?.value;
|
||||
if (!cartId) {
|
||||
return {} as PageType;
|
||||
}
|
||||
return oms.getCheckoutPage(geinsCore, cartId);
|
||||
}
|
||||
return cms.getPage(geinsCore, handle);
|
||||
}
|
||||
|
||||
/*
|
||||
CART
|
||||
*/
|
||||
export async function createCart(): Promise<CartType> {
|
||||
return await oms.createCart(geinsCore);
|
||||
}
|
||||
|
||||
export async function getCart(cartId: string | undefined): Promise<CartType | undefined> {
|
||||
if (!cartId) {
|
||||
return undefined;
|
||||
}
|
||||
return await oms.getCart(geinsCore, cartId);
|
||||
}
|
||||
|
||||
export async function addToCart(
|
||||
cartId: string,
|
||||
lines: { merchandiseId: string; quantity: number }[]
|
||||
): Promise<CartType | undefined> {
|
||||
if (!cartId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items = lines.map((item) => {
|
||||
return {
|
||||
skuId: parseInt(item.merchandiseId),
|
||||
quantity: item.quantity
|
||||
};
|
||||
});
|
||||
|
||||
let cart = {} as CartType;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
cart = await oms.addToCart(geinsCore, cartId, items[i] as CartItemInputType);
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
export async function updateCart(
|
||||
cartId: string,
|
||||
lines: { id: string; merchandiseId: string; quantity: number }[]
|
||||
): Promise<CartType | undefined> {
|
||||
if (!cartId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// create geins item to add from lines
|
||||
const items = lines.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
quantity: item.quantity
|
||||
};
|
||||
});
|
||||
let cart = {} as CartType;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
cart = await oms.updateCart(geinsCore, cartId, items[i] as CartItemInputType);
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
export async function removeFromCart(
|
||||
cartId: string,
|
||||
lineIds: string[]
|
||||
): Promise<CartType | undefined> {
|
||||
if (!cartId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cart = {} as CartType;
|
||||
for (let i = 0; i < lineIds.length; i++) {
|
||||
cart = await oms.removeFromCart(geinsCore, cartId, lineIds[i] as string);
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.
|
||||
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||
// We always need to respond with a 200 status code to Geins,
|
||||
// otherwise it will continue to retry the request.
|
||||
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
|
||||
const productWebhooks = ['products/create', 'products/delete', 'products/update'];
|
||||
const topic = (await headers()).get('x-geins-topic') || 'unknown';
|
||||
const secret = req.nextUrl.searchParams.get('secret');
|
||||
const isCollectionUpdate = collectionWebhooks.includes(topic);
|
||||
const isProductUpdate = productWebhooks.includes(topic);
|
||||
|
||||
if (!secret || secret !== process.env.GEINS_REVALIDATION_SECRET) {
|
||||
console.error('Invalid revalidation secret.');
|
||||
return NextResponse.json({ status: 401 });
|
||||
}
|
||||
|
||||
if (!isCollectionUpdate && !isProductUpdate) {
|
||||
// We don't need to revalidate anything for any other topics.
|
||||
return NextResponse.json({ status: 200 });
|
||||
}
|
||||
|
||||
if (isCollectionUpdate) {
|
||||
revalidateTag(TAGS.collections);
|
||||
}
|
||||
|
||||
if (isProductUpdate) {
|
||||
revalidateTag(TAGS.products);
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
||||
}
|
102
lib/geins/oms.ts
Normal file
102
lib/geins/oms.ts
Normal file
@ -0,0 +1,102 @@
|
||||
'use server';
|
||||
|
||||
import { GeinsCore } from '@geins/core';
|
||||
import { PAYMENT_ID } from './constants';
|
||||
import { cartAddMutation } from './queries/mutations/cart-add';
|
||||
import { cartUpdateMutation } from './queries/mutations/cart-line-update';
|
||||
import { checkoutMutation } from './queries/mutations/checkout';
|
||||
import { cartCreateQuery } from './queries/queries/cart-create';
|
||||
import { cartGetQuery } from './queries/queries/cart-get';
|
||||
import { reshapeCart, reshapeCheckout } from './reshape';
|
||||
import { CartItemInputType, PageType } from './types';
|
||||
|
||||
export const createCart = async (geinsCore: GeinsCore): Promise<any> => {
|
||||
const data = await geinsCore.graphql.query({
|
||||
queryAsString: cartCreateQuery,
|
||||
variables: {},
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
if (!data || !data.getCart) {
|
||||
return {};
|
||||
}
|
||||
return reshapeCart(data.getCart);
|
||||
};
|
||||
|
||||
export const getCart = async (geinsCore: GeinsCore, id: string | undefined): Promise<any> => {
|
||||
const data = await geinsCore.graphql.query({
|
||||
queryAsString: cartGetQuery,
|
||||
variables: { id },
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
if (!data || !data.getCart) {
|
||||
return {};
|
||||
}
|
||||
return reshapeCart(data.getCart);
|
||||
};
|
||||
|
||||
export const addToCart = async (
|
||||
geinsCore: GeinsCore,
|
||||
id: string,
|
||||
item: CartItemInputType
|
||||
): Promise<any> => {
|
||||
const data = await geinsCore.graphql.mutation({
|
||||
queryAsString: cartAddMutation,
|
||||
variables: { id: id, item: item },
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
if (!data || !data.addToCart) {
|
||||
return {};
|
||||
}
|
||||
return reshapeCart(data.addToCart);
|
||||
};
|
||||
|
||||
export const removeFromCart = async (
|
||||
geinsCore: GeinsCore,
|
||||
id: string,
|
||||
itemId: string
|
||||
): Promise<any> => {
|
||||
const item = {
|
||||
id: itemId,
|
||||
quantity: 0
|
||||
};
|
||||
const data = await geinsCore.graphql.mutation({
|
||||
queryAsString: cartUpdateMutation,
|
||||
variables: { id, item },
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
if (!data || !data.updateCartItem) {
|
||||
return {};
|
||||
}
|
||||
return reshapeCart(data.updateCartItem);
|
||||
};
|
||||
|
||||
export const updateCart = async (
|
||||
geinsCore: GeinsCore,
|
||||
id: string,
|
||||
item: CartItemInputType
|
||||
): Promise<any> => {
|
||||
const data = await geinsCore.graphql.mutation({
|
||||
queryAsString: cartUpdateMutation,
|
||||
variables: { id, item },
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
if (!data || !data.updateCartItem) {
|
||||
return {};
|
||||
}
|
||||
return reshapeCart(data.updateCartItem);
|
||||
};
|
||||
|
||||
export const getCheckoutPage = async (geinsCore: GeinsCore, cartId: string): Promise<PageType> => {
|
||||
const variables = {
|
||||
cartId: cartId,
|
||||
checkout: {
|
||||
paymentId: PAYMENT_ID
|
||||
}
|
||||
};
|
||||
const data = await geinsCore.graphql.mutation({
|
||||
queryAsString: checkoutMutation,
|
||||
variables,
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
return reshapeCheckout(data);
|
||||
};
|
151
lib/geins/pim.ts
Normal file
151
lib/geins/pim.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { GeinsCore } from '@geins/core';
|
||||
import { USE_CATEGORY_FOR_RECOMMENDATIONS_BACKUP } from './constants';
|
||||
import { categoriesQuery } from './queries/queries/categories';
|
||||
import { listPageInfoQuery } from './queries/queries/listPageInfo';
|
||||
import { productQuery } from './queries/queries/product';
|
||||
import { productsQuery } from './queries/queries/products';
|
||||
import { relatedProductsQuery } from './queries/queries/products-related';
|
||||
import {
|
||||
reshapeCategories,
|
||||
reshapeListPageMetadata,
|
||||
reshapeProduct,
|
||||
reshapeProducts,
|
||||
translateSortKey
|
||||
} from './reshape';
|
||||
import {
|
||||
CategoryItemType,
|
||||
CollectionType,
|
||||
ProductRelationType,
|
||||
ProductRelationTypeEnum,
|
||||
ProductType
|
||||
} from './types';
|
||||
|
||||
export const getCategoryMetadata = async (
|
||||
geinsCore: GeinsCore,
|
||||
slug: string
|
||||
): Promise<CollectionType> => {
|
||||
const data = await geinsCore.graphql.query({
|
||||
queryAsString: listPageInfoQuery,
|
||||
variables: { url: slug }
|
||||
});
|
||||
return reshapeListPageMetadata(data);
|
||||
};
|
||||
|
||||
export const getCategories = async (
|
||||
geinsCore: GeinsCore,
|
||||
parentNodeId?: number
|
||||
): Promise<CategoryItemType[]> => {
|
||||
const variables = {
|
||||
includeHidden: false,
|
||||
parentCategoryId: parentNodeId
|
||||
};
|
||||
const data = await geinsCore.graphql.query({ queryAsString: categoriesQuery, variables });
|
||||
return reshapeCategories(data);
|
||||
};
|
||||
|
||||
export const getProduct = async (
|
||||
geinsCore: GeinsCore,
|
||||
slug: string
|
||||
): Promise<ProductType | undefined> => {
|
||||
const variables = {
|
||||
alias: slug
|
||||
};
|
||||
const data = await geinsCore.graphql.query({ queryAsString: productQuery, variables });
|
||||
if (!data.product) {
|
||||
return undefined;
|
||||
}
|
||||
return reshapeProduct(data.product);
|
||||
};
|
||||
|
||||
export const getProducts = async (
|
||||
geinsCore: GeinsCore,
|
||||
{
|
||||
query,
|
||||
reverse,
|
||||
sortKey
|
||||
}: {
|
||||
query?: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}
|
||||
): Promise<ProductType[]> => {
|
||||
const variables = {
|
||||
filter: {
|
||||
sort: translateSortKey(sortKey || '', reverse || false),
|
||||
includeCollapsed: true,
|
||||
filterMode: 'CURRENT',
|
||||
searchText: query
|
||||
}
|
||||
};
|
||||
|
||||
const data = await geinsCore.graphql.query({ queryAsString: productsQuery, variables });
|
||||
if (!data || !data.products || !data.products.products) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return reshapeProducts(data.products.products);
|
||||
};
|
||||
|
||||
export const getProductRecommendations = async (
|
||||
geinsCore: GeinsCore,
|
||||
product: ProductType
|
||||
): Promise<ProductType[]> => {
|
||||
const variables = {
|
||||
alias: product.slug
|
||||
};
|
||||
|
||||
const data = await geinsCore.graphql.query({ queryAsString: relatedProductsQuery, variables });
|
||||
|
||||
if (data?.relatedProducts && data.relatedProducts.length > 0) {
|
||||
return reshapeProducts(data.relatedProducts);
|
||||
}
|
||||
|
||||
if (USE_CATEGORY_FOR_RECOMMENDATIONS_BACKUP) {
|
||||
const categoryAlias = product.relations?.filter(
|
||||
(relation: ProductRelationType) => relation.type === ProductRelationTypeEnum.CATEGORY
|
||||
);
|
||||
if (categoryAlias && categoryAlias[0]) {
|
||||
return getCategoryProducts(geinsCore, { category: categoryAlias[0].alias, take: 4 });
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getCategoryProducts = async (
|
||||
geinsCore: GeinsCore,
|
||||
{
|
||||
category,
|
||||
reverse,
|
||||
sortKey,
|
||||
take,
|
||||
skip
|
||||
}: {
|
||||
category: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
take?: number;
|
||||
skip?: number;
|
||||
}
|
||||
): Promise<ProductType[]> => {
|
||||
const variables = {
|
||||
categoryAlias: category,
|
||||
filter: {
|
||||
sort: translateSortKey(sortKey || '', reverse || false),
|
||||
includeCollapsed: false
|
||||
},
|
||||
...(take && { take }),
|
||||
...(skip && { skip })
|
||||
};
|
||||
const data = await geinsCore.graphql.query({
|
||||
queryAsString: productsQuery,
|
||||
variables,
|
||||
requestOptions: { fetchPolicy: 'no-cache' }
|
||||
});
|
||||
|
||||
if (!data || !data.products || !data.products.products) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return reshapeProducts(data.products.products);
|
||||
};
|
7
lib/geins/queries/fragments/campaign.ts
Normal file
7
lib/geins/queries/fragments/campaign.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const campaignFragment = /* GraphQL */ `
|
||||
fragment Campaign on CampaignRuleType {
|
||||
name
|
||||
hideTitle
|
||||
}
|
||||
`;
|
||||
export default campaignFragment;
|
107
lib/geins/queries/fragments/cart.ts
Normal file
107
lib/geins/queries/fragments/cart.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import campaignFragment from './campaign';
|
||||
import priceFragment from './price';
|
||||
import stockFragment from './stock';
|
||||
|
||||
const cartFragment = /* GraphQL */ `
|
||||
fragment Cart on CartType {
|
||||
id
|
||||
promoCode
|
||||
appliedCampaigns {
|
||||
...Campaign
|
||||
}
|
||||
items {
|
||||
id
|
||||
campaign {
|
||||
appliedCampaigns {
|
||||
...Campaign
|
||||
}
|
||||
prices {
|
||||
price {
|
||||
...Price
|
||||
}
|
||||
quantity
|
||||
}
|
||||
}
|
||||
unitPrice {
|
||||
...Price
|
||||
}
|
||||
product {
|
||||
productId
|
||||
articleNumber
|
||||
brand {
|
||||
name
|
||||
}
|
||||
name
|
||||
productImages {
|
||||
fileName
|
||||
}
|
||||
alias
|
||||
canonicalUrl
|
||||
primaryCategory {
|
||||
name
|
||||
}
|
||||
skus {
|
||||
skuId
|
||||
name
|
||||
stock {
|
||||
...Stock
|
||||
}
|
||||
}
|
||||
unitPrice {
|
||||
...Price
|
||||
}
|
||||
}
|
||||
quantity
|
||||
skuId
|
||||
totalPrice {
|
||||
...Price
|
||||
}
|
||||
}
|
||||
summary {
|
||||
fixedAmountDiscountIncVat
|
||||
fixedAmountDiscountExVat
|
||||
balance {
|
||||
pending
|
||||
pendingFormatted
|
||||
totalSellingPriceExBalanceExVat
|
||||
totalSellingPriceExBalanceIncVat
|
||||
totalSellingPriceExBalanceIncVatFormatted
|
||||
}
|
||||
subTotal {
|
||||
regularPriceIncVatFormatted
|
||||
regularPriceExVatFormatted
|
||||
sellingPriceIncVatFormatted
|
||||
sellingPriceExVatFormatted
|
||||
sellingPriceExVat
|
||||
sellingPriceIncVat
|
||||
vat
|
||||
}
|
||||
shipping {
|
||||
amountLeftToFreeShipping
|
||||
amountLeftToFreeShippingFormatted
|
||||
feeExVatFormatted
|
||||
feeIncVatFormatted
|
||||
feeIncVat
|
||||
feeExVat
|
||||
isDefault
|
||||
}
|
||||
total {
|
||||
isDiscounted
|
||||
sellingPriceIncVatFormatted
|
||||
sellingPriceExVatFormatted
|
||||
sellingPriceIncVat
|
||||
sellingPriceExVat
|
||||
discountIncVatFormatted
|
||||
discountExVatFormatted
|
||||
discountExVat
|
||||
discountIncVat
|
||||
vatFormatted
|
||||
vat
|
||||
}
|
||||
}
|
||||
}
|
||||
${priceFragment}
|
||||
${stockFragment}
|
||||
${campaignFragment}
|
||||
`;
|
||||
export default cartFragment;
|
26
lib/geins/queries/fragments/list-info.ts
Normal file
26
lib/geins/queries/fragments/list-info.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import metaFragment from './meta';
|
||||
|
||||
const listInfoFragment = /* GraphQL */ `
|
||||
fragment ListInfo on PageInfoType {
|
||||
id
|
||||
alias
|
||||
canonicalUrl
|
||||
primaryImage
|
||||
name
|
||||
primaryDescription
|
||||
secondaryDescription
|
||||
hideTitle
|
||||
hideDescription
|
||||
logo
|
||||
meta {
|
||||
...Meta
|
||||
}
|
||||
subCategories {
|
||||
name
|
||||
alias
|
||||
canonicalUrl
|
||||
}
|
||||
}
|
||||
${metaFragment}
|
||||
`;
|
||||
export default listInfoFragment;
|
32
lib/geins/queries/fragments/list-product.ts
Normal file
32
lib/geins/queries/fragments/list-product.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import priceFragment from './price';
|
||||
import stockFragment from './stock';
|
||||
|
||||
const listProductFragment = /* GraphQL */ `
|
||||
fragment ListProduct on ProductType {
|
||||
brand {
|
||||
name
|
||||
alias
|
||||
}
|
||||
name
|
||||
productId
|
||||
articleNumber
|
||||
alias
|
||||
canonicalUrl
|
||||
unitPrice {
|
||||
...Price
|
||||
}
|
||||
productImages {
|
||||
fileName
|
||||
}
|
||||
primaryCategory {
|
||||
name
|
||||
alias
|
||||
}
|
||||
totalStock {
|
||||
...Stock
|
||||
}
|
||||
}
|
||||
${priceFragment}
|
||||
${stockFragment}
|
||||
`;
|
||||
export default listProductFragment;
|
7
lib/geins/queries/fragments/meta.ts
Normal file
7
lib/geins/queries/fragments/meta.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const metaFragment = /* GraphQL */ `
|
||||
fragment Meta on MetadataType {
|
||||
title
|
||||
description
|
||||
}
|
||||
`;
|
||||
export default metaFragment;
|
16
lib/geins/queries/fragments/price.ts
Normal file
16
lib/geins/queries/fragments/price.ts
Normal file
@ -0,0 +1,16 @@
|
||||
const priceFragment = /* GraphQL */ `
|
||||
fragment Price on PriceType {
|
||||
isDiscounted
|
||||
regularPriceIncVatFormatted
|
||||
sellingPriceIncVatFormatted
|
||||
regularPriceExVatFormatted
|
||||
sellingPriceExVatFormatted
|
||||
sellingPriceIncVat
|
||||
sellingPriceExVat
|
||||
regularPriceIncVat
|
||||
regularPriceExVat
|
||||
vat
|
||||
discountPercentage
|
||||
}
|
||||
`;
|
||||
export default priceFragment;
|
14
lib/geins/queries/fragments/sku.ts
Normal file
14
lib/geins/queries/fragments/sku.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import stockFragment from './stock';
|
||||
const skuFragment = /* GraphQL */ `
|
||||
fragment Sku on SkuType {
|
||||
skuId
|
||||
gtin
|
||||
name
|
||||
productId
|
||||
stock {
|
||||
...Stock
|
||||
}
|
||||
}
|
||||
${stockFragment}
|
||||
`;
|
||||
export default skuFragment;
|
10
lib/geins/queries/fragments/stock.ts
Normal file
10
lib/geins/queries/fragments/stock.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const stockFragment = /* GraphQL */ `
|
||||
fragment Stock on StockType {
|
||||
inStock
|
||||
oversellable
|
||||
totalStock
|
||||
static
|
||||
incoming
|
||||
}
|
||||
`;
|
||||
export default stockFragment;
|
22
lib/geins/queries/fragments/variant.ts
Normal file
22
lib/geins/queries/fragments/variant.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import stockFragment from './stock';
|
||||
const variantFragment = /* GraphQL */ `
|
||||
fragment Variant on VariantType {
|
||||
alias
|
||||
level
|
||||
attributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
label
|
||||
value
|
||||
dimension
|
||||
skuId
|
||||
productId
|
||||
stock {
|
||||
...Stock
|
||||
}
|
||||
primaryImage
|
||||
}
|
||||
${stockFragment}
|
||||
`;
|
||||
export default variantFragment;
|
22
lib/geins/queries/mutations/cart-add.ts
Normal file
22
lib/geins/queries/mutations/cart-add.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import cartFragment from '../fragments/cart';
|
||||
|
||||
export const cartAddMutation = /* GraphQL */ `
|
||||
mutation addToCart(
|
||||
$id: String!
|
||||
$item: CartItemInputType!
|
||||
$channelId: String
|
||||
$languageId: String
|
||||
$marketId: String
|
||||
) {
|
||||
addToCart(
|
||||
id: $id
|
||||
item: $item
|
||||
channelId: $channelId
|
||||
languageId: $languageId
|
||||
marketId: $marketId
|
||||
) {
|
||||
...Cart
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
22
lib/geins/queries/mutations/cart-line-update.ts
Normal file
22
lib/geins/queries/mutations/cart-line-update.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import cartFragment from '../fragments/cart';
|
||||
|
||||
export const cartUpdateMutation = /* GraphQL */ `
|
||||
mutation updateCartItem(
|
||||
$id: String!
|
||||
$item: CartItemInputType!
|
||||
$channelId: String
|
||||
$languageId: String
|
||||
$marketId: String
|
||||
) {
|
||||
updateCartItem(
|
||||
id: $id
|
||||
item: $item
|
||||
channelId: $channelId
|
||||
languageId: $languageId
|
||||
marketId: $marketId
|
||||
) {
|
||||
...Cart
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
21
lib/geins/queries/mutations/checkout.ts
Normal file
21
lib/geins/queries/mutations/checkout.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const checkoutMutation = /* GraphQL */ `
|
||||
mutation createOrUpdateCheckout(
|
||||
$cartId: String!
|
||||
$checkout: CheckoutInputType
|
||||
$channelId: String
|
||||
$languageId: String
|
||||
$marketId: String
|
||||
) {
|
||||
createOrUpdateCheckout(
|
||||
cartId: $cartId
|
||||
checkout: $checkout
|
||||
channelId: $channelId
|
||||
languageId: $languageId
|
||||
marketId: $marketId
|
||||
) {
|
||||
paymentOptions {
|
||||
paymentData
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
10
lib/geins/queries/queries/cart-create.ts
Normal file
10
lib/geins/queries/queries/cart-create.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import cartFragment from '../fragments/cart';
|
||||
|
||||
export const cartCreateQuery = /* GraphQL */ `
|
||||
query getCart($channelId: String, $languageId: String, $marketId: String) {
|
||||
getCart(channelId: $channelId, languageId: $languageId, marketId: $marketId) {
|
||||
...Cart
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
10
lib/geins/queries/queries/cart-get.ts
Normal file
10
lib/geins/queries/queries/cart-get.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import cartFragment from '../fragments/cart';
|
||||
|
||||
export const cartGetQuery = /* GraphQL */ `
|
||||
query getCart($id: String, $channelId: String, $languageId: String, $marketId: String) {
|
||||
getCart(id: $id, channelId: $channelId, languageId: $languageId, marketId: $marketId) {
|
||||
...Cart
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
22
lib/geins/queries/queries/categories.ts
Normal file
22
lib/geins/queries/queries/categories.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export const categoriesQuery = /* GraphQL */ `
|
||||
query categories(
|
||||
$parentCategoryId: Int
|
||||
$channelId: String
|
||||
$languageId: String
|
||||
$marketId: String
|
||||
) {
|
||||
categories(
|
||||
parentCategoryId: $parentCategoryId
|
||||
channelId: $channelId
|
||||
languageId: $languageId
|
||||
marketId: $marketId
|
||||
) {
|
||||
alias
|
||||
canonicalUrl
|
||||
name
|
||||
categoryId
|
||||
parentCategoryId
|
||||
order
|
||||
}
|
||||
}
|
||||
`;
|
10
lib/geins/queries/queries/listPageInfo.ts
Normal file
10
lib/geins/queries/queries/listPageInfo.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import listInfoFragment from '../fragments/list-info';
|
||||
|
||||
export const listPageInfoQuery = /* GraphQL */ `
|
||||
query listPageInfo($url: String!, $channelId: String, $languageId: String, $marketId: String) {
|
||||
listPageInfo(url: $url, channelId: $channelId, languageId: $languageId, marketId: $marketId) {
|
||||
...ListInfo
|
||||
}
|
||||
}
|
||||
${listInfoFragment}
|
||||
`;
|
92
lib/geins/queries/queries/product.ts
Normal file
92
lib/geins/queries/queries/product.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import campaignFragment from '../fragments/campaign';
|
||||
import metaFragment from '../fragments/meta';
|
||||
import priceFragment from '../fragments/price';
|
||||
import skuFragment from '../fragments/sku';
|
||||
import stockFragment from '../fragments/stock';
|
||||
import variantFragment from '../fragments/variant';
|
||||
|
||||
export const productQuery = /* GraphQL */ `
|
||||
query product($alias: String!, $channelId: String, $languageId: String, $marketId: String) {
|
||||
product(alias: $alias, channelId: $channelId, languageId: $languageId, marketId: $marketId) {
|
||||
productId
|
||||
alias
|
||||
articleNumber
|
||||
canonicalUrl
|
||||
name
|
||||
meta {
|
||||
...Meta
|
||||
}
|
||||
brand {
|
||||
name
|
||||
alias
|
||||
canonicalUrl
|
||||
}
|
||||
productImages {
|
||||
fileName
|
||||
}
|
||||
primaryCategory {
|
||||
name
|
||||
alias
|
||||
canonicalUrl
|
||||
}
|
||||
categories {
|
||||
name
|
||||
alias
|
||||
}
|
||||
unitPrice {
|
||||
...Price
|
||||
}
|
||||
texts {
|
||||
text1
|
||||
text2
|
||||
text3
|
||||
}
|
||||
parameterGroups {
|
||||
name
|
||||
parameterGroupId
|
||||
parameters {
|
||||
name
|
||||
value
|
||||
show
|
||||
identifier
|
||||
}
|
||||
}
|
||||
skus {
|
||||
...Sku
|
||||
}
|
||||
totalStock {
|
||||
...Stock
|
||||
}
|
||||
variantDimensions {
|
||||
dimension
|
||||
value
|
||||
level
|
||||
label
|
||||
}
|
||||
variantGroup {
|
||||
variants {
|
||||
variants {
|
||||
variants {
|
||||
...Variant
|
||||
}
|
||||
...Variant
|
||||
}
|
||||
...Variant
|
||||
}
|
||||
}
|
||||
discountCampaigns {
|
||||
...Campaign
|
||||
}
|
||||
rating {
|
||||
score
|
||||
voteCount
|
||||
}
|
||||
}
|
||||
}
|
||||
${priceFragment}
|
||||
${stockFragment}
|
||||
${skuFragment}
|
||||
${variantFragment}
|
||||
${metaFragment}
|
||||
${campaignFragment}
|
||||
`;
|
43
lib/geins/queries/queries/products-related.ts
Normal file
43
lib/geins/queries/queries/products-related.ts
Normal file
@ -0,0 +1,43 @@
|
||||
//import relatedProductFragment from './fragments/related-product';
|
||||
// ${relatedProductFragment}
|
||||
import priceFragment from '../fragments/price';
|
||||
import skuFragment from '../fragments/sku';
|
||||
|
||||
export const relatedProductsQuery = /* GraphQL */ `
|
||||
query relatedProducts(
|
||||
$alias: String!
|
||||
$channelId: String
|
||||
$languageId: String
|
||||
$marketId: String
|
||||
) {
|
||||
relatedProducts(
|
||||
alias: $alias
|
||||
channelId: $channelId
|
||||
languageId: $languageId
|
||||
marketId: $marketId
|
||||
) {
|
||||
alias
|
||||
name
|
||||
canonicalUrl
|
||||
brand {
|
||||
name
|
||||
}
|
||||
unitPrice {
|
||||
...Price
|
||||
}
|
||||
relationType
|
||||
productImages {
|
||||
fileName
|
||||
}
|
||||
primaryImage
|
||||
primaryCategory {
|
||||
name
|
||||
}
|
||||
skus {
|
||||
...Sku
|
||||
}
|
||||
}
|
||||
}
|
||||
${priceFragment}
|
||||
${skuFragment}
|
||||
`;
|
38
lib/geins/queries/queries/products.ts
Normal file
38
lib/geins/queries/queries/products.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import listProductFragment from '../fragments/list-product';
|
||||
|
||||
export const productsQuery = /* GraphQL */ `
|
||||
query products(
|
||||
$skip: Int = null
|
||||
$take: Int = null
|
||||
$categoryAlias: String = null
|
||||
$brandAlias: String = null
|
||||
$discountCampaignAlias: String = null
|
||||
$url: String = null
|
||||
$filter: FilterInputType = null
|
||||
$channelId: String
|
||||
$languageId: String
|
||||
$marketId: String
|
||||
) {
|
||||
products(
|
||||
skip: $skip
|
||||
take: $take
|
||||
categoryAlias: $categoryAlias
|
||||
brandAlias: $brandAlias
|
||||
discountCampaignAlias: $discountCampaignAlias
|
||||
url: $url
|
||||
filter: $filter
|
||||
channelId: $channelId
|
||||
languageId: $languageId
|
||||
marketId: $marketId
|
||||
) {
|
||||
products {
|
||||
brand {
|
||||
name
|
||||
}
|
||||
...ListProduct
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
${listProductFragment}
|
||||
`;
|
439
lib/geins/reshape.ts
Normal file
439
lib/geins/reshape.ts
Normal file
@ -0,0 +1,439 @@
|
||||
import { GeinsMenuType } from '@geins/types';
|
||||
import {
|
||||
CURRENCY_CODE,
|
||||
DEFAULT_SKU_VARIATION,
|
||||
IMAGE_URL,
|
||||
LONG_DESCRIPTION,
|
||||
SHORT_DESCRIPTION
|
||||
} from './constants';
|
||||
import {
|
||||
CartItemType,
|
||||
CartType,
|
||||
CategoryItemType,
|
||||
CollectionType,
|
||||
PageType,
|
||||
ProductImageType,
|
||||
ProductOptionType,
|
||||
ProductRelationType,
|
||||
ProductRelationTypeEnum,
|
||||
ProductType,
|
||||
ProductVariantType
|
||||
} from './types';
|
||||
|
||||
export const translateSortKey = (sortKey: string, reverse: boolean): string => {
|
||||
switch (sortKey) {
|
||||
case 'BEST_SELLING':
|
||||
return 'MOST_SOLD';
|
||||
case 'CREATED_AT':
|
||||
return 'LATEST';
|
||||
case 'PRICE':
|
||||
if (reverse) {
|
||||
return 'PRICE';
|
||||
}
|
||||
return 'PRICE_DESC';
|
||||
default:
|
||||
return 'RELEVANCE';
|
||||
}
|
||||
};
|
||||
|
||||
export const reshapeCategories = (geinsCategories: any): CategoryItemType[] => {
|
||||
if (!geinsCategories || !geinsCategories?.categories.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return geinsCategories.categories.map((item: any) => ({
|
||||
id: item?.categoryId ?? '',
|
||||
title: item?.name ?? '',
|
||||
name: item?.name ?? '',
|
||||
parentId: item?.parentCategoryId || '',
|
||||
path: item?.canonicalUrl?.split('/').pop() || '',
|
||||
slug: item?.alias ?? ''
|
||||
}));
|
||||
};
|
||||
|
||||
export const reshapeListPageMetadata = (geinsCategoryMetadata: any): CollectionType => {
|
||||
const rawCategory = geinsCategoryMetadata?.listPageInfo;
|
||||
if (!rawCategory) {
|
||||
return {} as CollectionType;
|
||||
}
|
||||
|
||||
let seoTitle = rawCategory?.meta?.title || rawCategory?.name || '';
|
||||
seoTitle = seoTitle.replace(/\[name\]/g, rawCategory?.name || '');
|
||||
|
||||
const seoDescription = rawCategory?.meta?.description || '';
|
||||
return {
|
||||
id: rawCategory?.id || '',
|
||||
title: rawCategory?.name || '',
|
||||
handle: rawCategory?.alias || '',
|
||||
seo: {
|
||||
title: seoTitle,
|
||||
description: seoDescription
|
||||
},
|
||||
description: rawCategory?.primaryDescription || ''
|
||||
};
|
||||
};
|
||||
|
||||
export const reshapeProducts = (geinsData: any): ProductType[] => {
|
||||
if (!geinsData) {
|
||||
throw new Error('Invalid products query response');
|
||||
}
|
||||
const rawProducts = geinsData;
|
||||
if (!rawProducts) {
|
||||
return [];
|
||||
}
|
||||
return rawProducts.map((product: any) => reshapeProduct(product));
|
||||
};
|
||||
|
||||
export const reshapeProduct = (geinsProductData: any): ProductType => {
|
||||
const rawProduct = geinsProductData;
|
||||
|
||||
const tags: string[] = [];
|
||||
const relations: ProductRelationType[] = [];
|
||||
if (rawProduct.primaryCategory) {
|
||||
tags.push(rawProduct.primaryCategory.name);
|
||||
relations.push({
|
||||
type: ProductRelationTypeEnum.CATEGORY,
|
||||
name: rawProduct.primaryCategory.name,
|
||||
alias: rawProduct.primaryCategory.alias
|
||||
});
|
||||
}
|
||||
|
||||
if (rawProduct.brand) {
|
||||
tags.push(rawProduct.brand.name);
|
||||
relations.push({
|
||||
type: ProductRelationTypeEnum.BRAND,
|
||||
name: rawProduct.brand.name,
|
||||
alias: rawProduct.brand.alias
|
||||
});
|
||||
}
|
||||
if (rawProduct.categories) {
|
||||
rawProduct.categories.forEach((category: any) => {
|
||||
tags.push(category.name);
|
||||
});
|
||||
}
|
||||
// remove duplicates
|
||||
const uniqueTags = [...new Set(tags)];
|
||||
tags.push(...uniqueTags);
|
||||
|
||||
// add descriptions from environment variables
|
||||
const shortDescription = rawProduct.texts?.[SHORT_DESCRIPTION] || '';
|
||||
const longDescription = rawProduct.texts?.[LONG_DESCRIPTION] || '';
|
||||
|
||||
// add images
|
||||
const images = rawProduct.productImages?.map(
|
||||
(image: any): ProductImageType => ({
|
||||
caption: rawProduct.name,
|
||||
altText: rawProduct.name,
|
||||
src: `${IMAGE_URL}/product/1200f1500/${image.fileName}`,
|
||||
url: `${IMAGE_URL}/product/1200f1500/${image.fileName}`,
|
||||
height: 1600,
|
||||
width: 2000
|
||||
})
|
||||
);
|
||||
|
||||
// add variations and options
|
||||
let variations: ProductVariantType[] = [];
|
||||
let options: ProductOptionType[] = [];
|
||||
|
||||
if (rawProduct.variantGroup) {
|
||||
variations = [...reshapeProductVariations(rawProduct)];
|
||||
options = [...reshapeProductOptions(variations)];
|
||||
}
|
||||
|
||||
return {
|
||||
id: rawProduct.productId,
|
||||
seo: {
|
||||
title: rawProduct.meta?.title,
|
||||
description: rawProduct.meta?.description
|
||||
},
|
||||
metaTitle: rawProduct.meta?.title || rawProduct.name,
|
||||
metaDescription: rawProduct.meta?.description || shortDescription,
|
||||
name: rawProduct.name,
|
||||
title: rawProduct.name,
|
||||
slug: rawProduct.alias,
|
||||
handle: rawProduct.alias,
|
||||
description: shortDescription,
|
||||
descriptionHtml: rawProduct.texts?.text1 || '',
|
||||
priceRange: {
|
||||
minVariantPrice: {
|
||||
amount: rawProduct.unitPrice?.sellingPriceIncVat,
|
||||
currencyCode: CURRENCY_CODE
|
||||
},
|
||||
maxVariantPrice: {
|
||||
amount: rawProduct.unitPrice?.sellingPriceIncVat,
|
||||
currencyCode: CURRENCY_CODE
|
||||
}
|
||||
},
|
||||
price: rawProduct.unitPrice.sellingPriceIncVat,
|
||||
currency: CURRENCY_CODE,
|
||||
stockTracking: !!rawProduct.totalStock,
|
||||
stockPurchasable: rawProduct.totalStock?.inStock || false,
|
||||
stockLevel: rawProduct.totalStock?.totalStock || 0,
|
||||
availableForSale: true,
|
||||
tags: tags || [],
|
||||
options: [...options] || [],
|
||||
variants: variations || [],
|
||||
featuredImage: images[0],
|
||||
images: images,
|
||||
relations: relations
|
||||
};
|
||||
};
|
||||
|
||||
const reshapeProductOptions = (variants: any[]): ProductOptionType[] => {
|
||||
const optionsMap: Record<string, { id: string; name: string; values: Set<string> }> = {};
|
||||
|
||||
variants.forEach((variant) => {
|
||||
if (variant.selectedOptions && Array.isArray(variant.selectedOptions)) {
|
||||
variant.selectedOptions.forEach((option: { name: string; value: string }) => {
|
||||
// Ensure `optionsMap[option.name]` is initialized
|
||||
if (!optionsMap[option.name]) {
|
||||
optionsMap[option.name] = {
|
||||
id: option.name.toLowerCase(),
|
||||
name: option.name,
|
||||
values: new Set()
|
||||
};
|
||||
}
|
||||
// Now it is safe to access `optionsMap[option.name].values`
|
||||
optionsMap[option.name]?.values.add(option.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Convert the map to an array and transform the Set of values to an array
|
||||
return Object.values(optionsMap).map((option) => ({
|
||||
id: option.id,
|
||||
name: option.name,
|
||||
values: Array.from(option.values)
|
||||
}));
|
||||
};
|
||||
|
||||
const reshapeSkusToVariants = (skus: any[]): ProductVariantType[] => {
|
||||
return skus.map((sku) => ({
|
||||
id: sku.skuId.toString(),
|
||||
title: sku.name,
|
||||
availableForSale: sku.stock?.totalStock > 0 || false,
|
||||
selectedOptions: [{ name: DEFAULT_SKU_VARIATION, value: sku.name }],
|
||||
price: {
|
||||
amount: '0',
|
||||
currencyCode: CURRENCY_CODE
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const reshapeProductVariations = (geinsProductData: any): ProductVariantType[] => {
|
||||
// filter out the default product from dimensions
|
||||
const dimensions = geinsProductData.variantDimensions.filter(
|
||||
(dimension: any) => dimension.dimension !== 'DefaultProduct'
|
||||
);
|
||||
if (dimensions.length === 0) {
|
||||
return reshapeSkusToVariants(geinsProductData.skus);
|
||||
}
|
||||
|
||||
const buildVariantsArray = (
|
||||
variants: any[],
|
||||
selectedOptions: any[] = [],
|
||||
parent: any = undefined
|
||||
): any[] => {
|
||||
const result: any[] = []; // Collect the final results here
|
||||
|
||||
if (!Array.isArray(variants)) {
|
||||
return result; // Safeguard against invalid input
|
||||
}
|
||||
|
||||
for (const variant of variants) {
|
||||
// Look ahead to check if the current variant is DefaultSku with a single value
|
||||
const hasNextLevel = Array.isArray(variant.variants) && variant.variants.length > 0;
|
||||
|
||||
// Create the current option
|
||||
const newSelectedOptions = [...selectedOptions];
|
||||
const currentOption = {
|
||||
name: variant.dimension,
|
||||
value: variant.value
|
||||
};
|
||||
newSelectedOptions.push(currentOption);
|
||||
|
||||
if (hasNextLevel) {
|
||||
const nestedResults = buildVariantsArray(variant.variants, newSelectedOptions, variant);
|
||||
result.push(...nestedResults);
|
||||
} else {
|
||||
const hasOnlyOneOption =
|
||||
(parent && parent.variants.length === 1 && variant.dimension === 'DefaultSku') || false;
|
||||
if (hasOnlyOneOption) {
|
||||
newSelectedOptions.pop();
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: variant.skuId.toString(),
|
||||
title: newSelectedOptions.map((opt) => opt.value).join(','),
|
||||
availableForSale: variant.stock?.totalStock > 0 || false, // Handle stock availability
|
||||
selectedOptions: newSelectedOptions,
|
||||
price: {
|
||||
amount: '',
|
||||
currencyCode: CURRENCY_CODE // Replace with actual currency code
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
const reshapedVariants = buildVariantsArray(geinsProductData.variantGroup.variants);
|
||||
|
||||
return reshapedVariants;
|
||||
};
|
||||
|
||||
export const reshapeCart = (geinsData: any): CartType => {
|
||||
if (!geinsData) {
|
||||
return {} as CartType;
|
||||
}
|
||||
|
||||
const items: CartItemType[] = [];
|
||||
let totalQuantity = 0;
|
||||
geinsData.items?.forEach((item: any) => {
|
||||
totalQuantity += item.quantity;
|
||||
const sku = item.product.skus[0];
|
||||
items.push({
|
||||
id: item.id,
|
||||
quantity: item.quantity,
|
||||
cost: {
|
||||
totalAmount: {
|
||||
amount: item.totalPrice.sellingPriceIncVat + '',
|
||||
currencyCode: CURRENCY_CODE
|
||||
}
|
||||
},
|
||||
merchandise: {
|
||||
id: item.skuId,
|
||||
title: sku.name,
|
||||
selectedOptions: [{ name: 'Size', value: sku.name }],
|
||||
product: {
|
||||
id: item.product.productId,
|
||||
handle: item.product.alias,
|
||||
title: item.product.name,
|
||||
featuredImage: {
|
||||
caption: item.product.name,
|
||||
altText: item.product.name,
|
||||
url: `${IMAGE_URL}/product/100f125/${item.product.productImages[0].fileName}`,
|
||||
src: `${IMAGE_URL}/product/100f125/${item.product.productImages[0].fileName}`,
|
||||
height: 1600,
|
||||
width: 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const vatSum =
|
||||
geinsData.summary.total.sellingPriceIncVat - geinsData.summary.total.sellingPriceExVat;
|
||||
const data = {
|
||||
id: geinsData.id || 'no-cart-id',
|
||||
lines: items || [],
|
||||
totalQuantity: totalQuantity,
|
||||
cost: {
|
||||
subtotalAmount: {
|
||||
amount: geinsData.summary.total.sellingPriceExVat + '',
|
||||
currencyCode: CURRENCY_CODE
|
||||
},
|
||||
totalTaxAmount: {
|
||||
amount: vatSum + '',
|
||||
currencyCode: CURRENCY_CODE
|
||||
},
|
||||
totalAmount: {
|
||||
amount: geinsData.summary.total.sellingPriceIncVat + '',
|
||||
currencyCode: CURRENCY_CODE
|
||||
}
|
||||
},
|
||||
|
||||
checkoutUrl: '/checkout'
|
||||
};
|
||||
return data;
|
||||
};
|
||||
|
||||
export const reshapeCheckout = (geinsData: any): PageType => {
|
||||
const checkoutPage: PageType = {
|
||||
id: 'checkout',
|
||||
title: 'Checkout example',
|
||||
handle: 'checkout',
|
||||
body: '',
|
||||
bodySummary: '',
|
||||
seo: {
|
||||
title: 'Checkout',
|
||||
description: 'Checkout page'
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (!geinsData || !geinsData.createOrUpdateCheckout) {
|
||||
checkoutPage.body = 'No payment options available';
|
||||
return checkoutPage;
|
||||
}
|
||||
if (
|
||||
!geinsData.createOrUpdateCheckout.paymentOptions ||
|
||||
geinsData.createOrUpdateCheckout.paymentOptions.length === 0
|
||||
) {
|
||||
checkoutPage.body = 'No payment options available';
|
||||
return checkoutPage;
|
||||
}
|
||||
checkoutPage.body = geinsData.createOrUpdateCheckout.paymentOptions[0].paymentData;
|
||||
return checkoutPage;
|
||||
};
|
||||
|
||||
export const reshapeMenu = (geinsMenu: GeinsMenuType, locationId: string) => {
|
||||
if (!geinsMenu.menuItems) {
|
||||
return [];
|
||||
}
|
||||
return geinsMenu.menuItems.map((item) => {
|
||||
let itemPath = item?.canonicalUrl?.split('/').pop() || '';
|
||||
if (item?.type === 'category') {
|
||||
itemPath = '/search/' + itemPath;
|
||||
} else if (item?.type === 'custom') {
|
||||
itemPath = item?.canonicalUrl || '';
|
||||
}
|
||||
return {
|
||||
id: locationId + ':' + item?.id || '',
|
||||
title: item?.title || '',
|
||||
path: itemPath || ''
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const reshapePage = (geinsPage: any, alias: string): PageType => {
|
||||
if (!geinsPage) {
|
||||
undefined;
|
||||
}
|
||||
|
||||
const title = geinsPage.meta.title || '';
|
||||
const today = new Date().toISOString();
|
||||
|
||||
const body = geinsPage.containers.map((container: any) => {
|
||||
const content = container.content.map((item: any) => {
|
||||
if (item.config.type === 'TextPageWidget') {
|
||||
return `
|
||||
<h3>${item.data.title}</h3>
|
||||
<p>
|
||||
${item.data.text}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
if (item.config.type === 'HTMLPageWidget') {
|
||||
return `${item.data.html}`;
|
||||
}
|
||||
if (item.config.type === 'ImagePageWidget') {
|
||||
return `<img src="${IMAGE_URL}/pagewidget/600w/${item.data.image.filename}" alt="${item.data.image.altText}" />`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
return content.join(' ');
|
||||
});
|
||||
|
||||
return {
|
||||
id: geinsPage.id || '',
|
||||
title: geinsPage.title || title,
|
||||
handle: alias || '',
|
||||
body: body || '',
|
||||
bodySummary: geinsPage?.bodySummary || '',
|
||||
seo: geinsPage?.seo || '',
|
||||
createdAt: geinsPage?.createdAt || today,
|
||||
updatedAt: geinsPage?.updatedAt || today
|
||||
};
|
||||
};
|
184
lib/geins/types.ts
Normal file
184
lib/geins/types.ts
Normal file
@ -0,0 +1,184 @@
|
||||
export type Maybe<T> = T | null;
|
||||
|
||||
export type Connection<T> = {
|
||||
edges: Array<Edge<T>>;
|
||||
};
|
||||
|
||||
export type Edge<T> = {
|
||||
node: T;
|
||||
};
|
||||
|
||||
export type CartType = {
|
||||
id: string | undefined;
|
||||
checkoutUrl: string;
|
||||
cost: {
|
||||
subtotalAmount: MoneyType;
|
||||
totalAmount: MoneyType;
|
||||
totalTaxAmount: MoneyType;
|
||||
};
|
||||
totalQuantity: number;
|
||||
lines: CartItemType[];
|
||||
};
|
||||
|
||||
export type CartProductType = {
|
||||
id: string;
|
||||
handle: string;
|
||||
title: string;
|
||||
featuredImage: ProductImageType;
|
||||
};
|
||||
|
||||
export type CartItemType = {
|
||||
id: string | undefined;
|
||||
quantity: number;
|
||||
cost: {
|
||||
totalAmount: MoneyType;
|
||||
};
|
||||
merchandise: {
|
||||
id: string;
|
||||
title: string;
|
||||
selectedOptions: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
product: CartProductType;
|
||||
};
|
||||
};
|
||||
|
||||
export type CartItemInputType = {
|
||||
id?: string;
|
||||
skuId?: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type SeoType = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type CollectionType = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
handle: string;
|
||||
seo?: SeoType;
|
||||
};
|
||||
|
||||
export type PageType = {
|
||||
id: string;
|
||||
title: string;
|
||||
handle: string;
|
||||
body: string;
|
||||
bodySummary: string;
|
||||
seo?: SeoType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type MenuType = {
|
||||
id: string;
|
||||
title?: string;
|
||||
name: string;
|
||||
items: MenuItemType[];
|
||||
};
|
||||
|
||||
export type MenuItemType = {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
export type CategoryItemType = {
|
||||
id: string;
|
||||
parentId: string;
|
||||
title: string;
|
||||
path: string;
|
||||
slug?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export enum ProductRelationTypeEnum {
|
||||
BRAND = 'BRAND',
|
||||
CATEGORY = 'CATEGORY',
|
||||
RELATED = 'RELATED',
|
||||
SIMILAR = 'SIMILAR',
|
||||
CROSS_SELL = 'CROSS_SELL',
|
||||
UP_SELL = 'UP_SELL'
|
||||
}
|
||||
|
||||
export type ProductRelationType = {
|
||||
type: ProductRelationTypeEnum;
|
||||
name: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||
export type MoneyType = {
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type ProductType = {
|
||||
id: string;
|
||||
seo: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
currency: string;
|
||||
slug: string;
|
||||
handle: string;
|
||||
stockTracking: boolean;
|
||||
stockPurchasable: boolean;
|
||||
stockLevel: number;
|
||||
title: string;
|
||||
name: string;
|
||||
description: string;
|
||||
price: string;
|
||||
priceRange: {
|
||||
maxVariantPrice: MoneyType;
|
||||
minVariantPrice: MoneyType;
|
||||
};
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
tags: string[];
|
||||
options: ProductOptionType[];
|
||||
variants: ProductVariantType[];
|
||||
featuredImage: ProductImageType;
|
||||
images: ProductImageType[];
|
||||
updatedAt?: string;
|
||||
availableForSale: boolean;
|
||||
descriptionHtml?: string;
|
||||
relations?: ProductRelationType[];
|
||||
};
|
||||
|
||||
export type ProductOptionType = {
|
||||
id: string;
|
||||
name: string;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type ProductVariantType = {
|
||||
id: string;
|
||||
title: string;
|
||||
availableForSale: boolean;
|
||||
selectedOptions: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
price: MoneyType;
|
||||
};
|
||||
|
||||
export type ProductImageType = {
|
||||
caption: string;
|
||||
altText: string;
|
||||
url: string;
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type ImageType = {
|
||||
url: string;
|
||||
altText: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
@ -1,53 +0,0 @@
|
||||
import productFragment from './product';
|
||||
|
||||
const cartFragment = /* GraphQL */ `
|
||||
fragment cart on Cart {
|
||||
id
|
||||
checkoutUrl
|
||||
cost {
|
||||
subtotalAmount {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
totalAmount {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
totalTaxAmount {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
lines(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
cost {
|
||||
totalAmount {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
merchandise {
|
||||
... on ProductVariant {
|
||||
id
|
||||
title
|
||||
selectedOptions {
|
||||
name
|
||||
value
|
||||
}
|
||||
product {
|
||||
...product
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
totalQuantity
|
||||
}
|
||||
${productFragment}
|
||||
`;
|
||||
|
||||
export default cartFragment;
|
@ -1,10 +0,0 @@
|
||||
const imageFragment = /* GraphQL */ `
|
||||
fragment image on Image {
|
||||
url
|
||||
altText
|
||||
width
|
||||
height
|
||||
}
|
||||
`;
|
||||
|
||||
export default imageFragment;
|
@ -1,64 +0,0 @@
|
||||
import imageFragment from './image';
|
||||
import seoFragment from './seo';
|
||||
|
||||
const productFragment = /* GraphQL */ `
|
||||
fragment product on Product {
|
||||
id
|
||||
handle
|
||||
availableForSale
|
||||
title
|
||||
description
|
||||
descriptionHtml
|
||||
options {
|
||||
id
|
||||
name
|
||||
values
|
||||
}
|
||||
priceRange {
|
||||
maxVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
minVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
variants(first: 250) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
availableForSale
|
||||
selectedOptions {
|
||||
name
|
||||
value
|
||||
}
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
featuredImage {
|
||||
...image
|
||||
}
|
||||
images(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
...image
|
||||
}
|
||||
}
|
||||
}
|
||||
seo {
|
||||
...seo
|
||||
}
|
||||
tags
|
||||
updatedAt
|
||||
}
|
||||
${imageFragment}
|
||||
${seoFragment}
|
||||
`;
|
||||
|
||||
export default productFragment;
|
@ -1,8 +0,0 @@
|
||||
const seoFragment = /* GraphQL */ `
|
||||
fragment seo on SEO {
|
||||
description
|
||||
title
|
||||
}
|
||||
`;
|
||||
|
||||
export default seoFragment;
|
@ -1,455 +0,0 @@
|
||||
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
|
||||
import { isShopifyError } from 'lib/type-guards';
|
||||
import { ensureStartsWith } from 'lib/utils';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
addToCartMutation,
|
||||
createCartMutation,
|
||||
editCartItemsMutation,
|
||||
removeFromCartMutation
|
||||
} from './mutations/cart';
|
||||
import { getCartQuery } from './queries/cart';
|
||||
import {
|
||||
getCollectionProductsQuery,
|
||||
getCollectionQuery,
|
||||
getCollectionsQuery
|
||||
} from './queries/collection';
|
||||
import { getMenuQuery } from './queries/menu';
|
||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||
import {
|
||||
getProductQuery,
|
||||
getProductRecommendationsQuery,
|
||||
getProductsQuery
|
||||
} from './queries/product';
|
||||
import {
|
||||
Cart,
|
||||
Collection,
|
||||
Connection,
|
||||
Image,
|
||||
Menu,
|
||||
Page,
|
||||
Product,
|
||||
ShopifyAddToCartOperation,
|
||||
ShopifyCart,
|
||||
ShopifyCartOperation,
|
||||
ShopifyCollection,
|
||||
ShopifyCollectionOperation,
|
||||
ShopifyCollectionProductsOperation,
|
||||
ShopifyCollectionsOperation,
|
||||
ShopifyCreateCartOperation,
|
||||
ShopifyMenuOperation,
|
||||
ShopifyPageOperation,
|
||||
ShopifyPagesOperation,
|
||||
ShopifyProduct,
|
||||
ShopifyProductOperation,
|
||||
ShopifyProductRecommendationsOperation,
|
||||
ShopifyProductsOperation,
|
||||
ShopifyRemoveFromCartOperation,
|
||||
ShopifyUpdateCartOperation
|
||||
} from './types';
|
||||
|
||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
|
||||
: '';
|
||||
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
|
||||
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
|
||||
|
||||
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
|
||||
|
||||
export async function shopifyFetch<T>({
|
||||
cache = 'force-cache',
|
||||
headers,
|
||||
query,
|
||||
tags,
|
||||
variables
|
||||
}: {
|
||||
cache?: RequestCache;
|
||||
headers?: HeadersInit;
|
||||
query: string;
|
||||
tags?: string[];
|
||||
variables?: ExtractVariables<T>;
|
||||
}): Promise<{ status: number; body: T } | never> {
|
||||
try {
|
||||
const result = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Storefront-Access-Token': key,
|
||||
...headers
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(query && { query }),
|
||||
...(variables && { variables })
|
||||
}),
|
||||
cache,
|
||||
...(tags && { next: { tags } })
|
||||
});
|
||||
|
||||
const body = await result.json();
|
||||
|
||||
if (body.errors) {
|
||||
throw body.errors[0];
|
||||
}
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
body
|
||||
};
|
||||
} catch (e) {
|
||||
if (isShopifyError(e)) {
|
||||
throw {
|
||||
cause: e.cause?.toString() || 'unknown',
|
||||
status: e.status || 500,
|
||||
message: e.message,
|
||||
query
|
||||
};
|
||||
}
|
||||
|
||||
throw {
|
||||
error: e,
|
||||
query
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const removeEdgesAndNodes = <T>(array: Connection<T>): T[] => {
|
||||
return array.edges.map((edge) => edge?.node);
|
||||
};
|
||||
|
||||
const reshapeCart = (cart: ShopifyCart): Cart => {
|
||||
if (!cart.cost?.totalTaxAmount) {
|
||||
cart.cost.totalTaxAmount = {
|
||||
amount: '0.0',
|
||||
currencyCode: cart.cost.totalAmount.currencyCode
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...cart,
|
||||
lines: removeEdgesAndNodes(cart.lines)
|
||||
};
|
||||
};
|
||||
|
||||
const reshapeCollection = (collection: ShopifyCollection): Collection | undefined => {
|
||||
if (!collection) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...collection,
|
||||
path: `/search/${collection.handle}`
|
||||
};
|
||||
};
|
||||
|
||||
const reshapeCollections = (collections: ShopifyCollection[]) => {
|
||||
const reshapedCollections = [];
|
||||
|
||||
for (const collection of collections) {
|
||||
if (collection) {
|
||||
const reshapedCollection = reshapeCollection(collection);
|
||||
|
||||
if (reshapedCollection) {
|
||||
reshapedCollections.push(reshapedCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reshapedCollections;
|
||||
};
|
||||
|
||||
const reshapeImages = (images: Connection<Image>, productTitle: string) => {
|
||||
const flattened = removeEdgesAndNodes(images);
|
||||
|
||||
return flattened.map((image) => {
|
||||
const filename = image.url.match(/.*\/(.*)\..*/)?.[1];
|
||||
return {
|
||||
...image,
|
||||
altText: image.altText || `${productTitle} - ${filename}`
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
|
||||
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { images, variants, ...rest } = product;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
images: reshapeImages(images, product.title),
|
||||
variants: removeEdgesAndNodes(variants)
|
||||
};
|
||||
};
|
||||
|
||||
const reshapeProducts = (products: ShopifyProduct[]) => {
|
||||
const reshapedProducts = [];
|
||||
|
||||
for (const product of products) {
|
||||
if (product) {
|
||||
const reshapedProduct = reshapeProduct(product);
|
||||
|
||||
if (reshapedProduct) {
|
||||
reshapedProducts.push(reshapedProduct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reshapedProducts;
|
||||
};
|
||||
|
||||
export async function createCart(): Promise<Cart> {
|
||||
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
||||
query: createCartMutation,
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
return reshapeCart(res.body.data.cartCreate.cart);
|
||||
}
|
||||
|
||||
export async function addToCart(
|
||||
cartId: string,
|
||||
lines: { merchandiseId: string; quantity: number }[]
|
||||
): Promise<Cart> {
|
||||
const res = await shopifyFetch<ShopifyAddToCartOperation>({
|
||||
query: addToCartMutation,
|
||||
variables: {
|
||||
cartId,
|
||||
lines
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
return reshapeCart(res.body.data.cartLinesAdd.cart);
|
||||
}
|
||||
|
||||
export async function removeFromCart(cartId: string, lineIds: string[]): Promise<Cart> {
|
||||
const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({
|
||||
query: removeFromCartMutation,
|
||||
variables: {
|
||||
cartId,
|
||||
lineIds
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
return reshapeCart(res.body.data.cartLinesRemove.cart);
|
||||
}
|
||||
|
||||
export async function updateCart(
|
||||
cartId: string,
|
||||
lines: { id: string; merchandiseId: string; quantity: number }[]
|
||||
): Promise<Cart> {
|
||||
const res = await shopifyFetch<ShopifyUpdateCartOperation>({
|
||||
query: editCartItemsMutation,
|
||||
variables: {
|
||||
cartId,
|
||||
lines
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
return reshapeCart(res.body.data.cartLinesUpdate.cart);
|
||||
}
|
||||
|
||||
export async function getCart(cartId: string | undefined): Promise<Cart | undefined> {
|
||||
if (!cartId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await shopifyFetch<ShopifyCartOperation>({
|
||||
query: getCartQuery,
|
||||
variables: { cartId },
|
||||
tags: [TAGS.cart]
|
||||
});
|
||||
|
||||
// Old carts becomes `null` when you checkout.
|
||||
if (!res.body.data.cart) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return reshapeCart(res.body.data.cart);
|
||||
}
|
||||
|
||||
export async function getCollection(handle: string): Promise<Collection | undefined> {
|
||||
const res = await shopifyFetch<ShopifyCollectionOperation>({
|
||||
query: getCollectionQuery,
|
||||
tags: [TAGS.collections],
|
||||
variables: {
|
||||
handle
|
||||
}
|
||||
});
|
||||
|
||||
return reshapeCollection(res.body.data.collection);
|
||||
}
|
||||
|
||||
export async function getCollectionProducts({
|
||||
collection,
|
||||
reverse,
|
||||
sortKey
|
||||
}: {
|
||||
collection: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}): Promise<Product[]> {
|
||||
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
|
||||
query: getCollectionProductsQuery,
|
||||
tags: [TAGS.collections, TAGS.products],
|
||||
variables: {
|
||||
handle: collection,
|
||||
reverse,
|
||||
sortKey: sortKey === 'CREATED_AT' ? 'CREATED' : sortKey
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.body.data.collection) {
|
||||
console.log(`No collection found for \`${collection}\``);
|
||||
return [];
|
||||
}
|
||||
|
||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products));
|
||||
}
|
||||
|
||||
export async function getCollections(): Promise<Collection[]> {
|
||||
const res = await shopifyFetch<ShopifyCollectionsOperation>({
|
||||
query: getCollectionsQuery,
|
||||
tags: [TAGS.collections]
|
||||
});
|
||||
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
|
||||
const collections = [
|
||||
{
|
||||
handle: '',
|
||||
title: 'All',
|
||||
description: 'All products',
|
||||
seo: {
|
||||
title: 'All',
|
||||
description: 'All products'
|
||||
},
|
||||
path: '/search',
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
// Filter out the `hidden` collections.
|
||||
// Collections that start with `hidden-*` need to be hidden on the search page.
|
||||
...reshapeCollections(shopifyCollections).filter(
|
||||
(collection) => !collection.handle.startsWith('hidden')
|
||||
)
|
||||
];
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
export async function getMenu(handle: string): Promise<Menu[]> {
|
||||
const res = await shopifyFetch<ShopifyMenuOperation>({
|
||||
query: getMenuQuery,
|
||||
tags: [TAGS.collections],
|
||||
variables: {
|
||||
handle
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({
|
||||
title: item.title,
|
||||
path: item.url.replace(domain, '').replace('/collections', '/search').replace('/pages', '')
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPage(handle: string): Promise<Page> {
|
||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
||||
query: getPageQuery,
|
||||
cache: 'no-store',
|
||||
variables: { handle }
|
||||
});
|
||||
|
||||
return res.body.data.pageByHandle;
|
||||
}
|
||||
|
||||
export async function getPages(): Promise<Page[]> {
|
||||
const res = await shopifyFetch<ShopifyPagesOperation>({
|
||||
query: getPagesQuery,
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
return removeEdgesAndNodes(res.body.data.pages);
|
||||
}
|
||||
|
||||
export async function getProduct(handle: string): Promise<Product | undefined> {
|
||||
const res = await shopifyFetch<ShopifyProductOperation>({
|
||||
query: getProductQuery,
|
||||
tags: [TAGS.products],
|
||||
variables: {
|
||||
handle
|
||||
}
|
||||
});
|
||||
|
||||
return reshapeProduct(res.body.data.product, false);
|
||||
}
|
||||
|
||||
export async function getProductRecommendations(productId: string): Promise<Product[]> {
|
||||
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
|
||||
query: getProductRecommendationsQuery,
|
||||
tags: [TAGS.products],
|
||||
variables: {
|
||||
productId
|
||||
}
|
||||
});
|
||||
|
||||
return reshapeProducts(res.body.data.productRecommendations);
|
||||
}
|
||||
|
||||
export async function getProducts({
|
||||
query,
|
||||
reverse,
|
||||
sortKey
|
||||
}: {
|
||||
query?: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}): Promise<Product[]> {
|
||||
const res = await shopifyFetch<ShopifyProductsOperation>({
|
||||
query: getProductsQuery,
|
||||
tags: [TAGS.products],
|
||||
variables: {
|
||||
query,
|
||||
reverse,
|
||||
sortKey
|
||||
}
|
||||
});
|
||||
|
||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
|
||||
}
|
||||
|
||||
// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.
|
||||
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||
// We always need to respond with a 200 status code to Shopify,
|
||||
// otherwise it will continue to retry the request.
|
||||
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
|
||||
const productWebhooks = ['products/create', 'products/delete', 'products/update'];
|
||||
const topic = (await headers()).get('x-shopify-topic') || 'unknown';
|
||||
const secret = req.nextUrl.searchParams.get('secret');
|
||||
const isCollectionUpdate = collectionWebhooks.includes(topic);
|
||||
const isProductUpdate = productWebhooks.includes(topic);
|
||||
|
||||
if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) {
|
||||
console.error('Invalid revalidation secret.');
|
||||
return NextResponse.json({ status: 401 });
|
||||
}
|
||||
|
||||
if (!isCollectionUpdate && !isProductUpdate) {
|
||||
// We don't need to revalidate anything for any other topics.
|
||||
return NextResponse.json({ status: 200 });
|
||||
}
|
||||
|
||||
if (isCollectionUpdate) {
|
||||
revalidateTag(TAGS.collections);
|
||||
}
|
||||
|
||||
if (isProductUpdate) {
|
||||
revalidateTag(TAGS.products);
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import cartFragment from '../fragments/cart';
|
||||
|
||||
export const addToCartMutation = /* GraphQL */ `
|
||||
mutation addToCart($cartId: ID!, $lines: [CartLineInput!]!) {
|
||||
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
||||
cart {
|
||||
...cart
|
||||
}
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
||||
|
||||
export const createCartMutation = /* GraphQL */ `
|
||||
mutation createCart($lineItems: [CartLineInput!]) {
|
||||
cartCreate(input: { lines: $lineItems }) {
|
||||
cart {
|
||||
...cart
|
||||
}
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
||||
|
||||
export const editCartItemsMutation = /* GraphQL */ `
|
||||
mutation editCartItems($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
|
||||
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
||||
cart {
|
||||
...cart
|
||||
}
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
||||
|
||||
export const removeFromCartMutation = /* GraphQL */ `
|
||||
mutation removeFromCart($cartId: ID!, $lineIds: [ID!]!) {
|
||||
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
|
||||
cart {
|
||||
...cart
|
||||
}
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
@ -1,10 +0,0 @@
|
||||
import cartFragment from '../fragments/cart';
|
||||
|
||||
export const getCartQuery = /* GraphQL */ `
|
||||
query getCart($cartId: ID!) {
|
||||
cart(id: $cartId) {
|
||||
...cart
|
||||
}
|
||||
}
|
||||
${cartFragment}
|
||||
`;
|
@ -1,56 +0,0 @@
|
||||
import productFragment from '../fragments/product';
|
||||
import seoFragment from '../fragments/seo';
|
||||
|
||||
const collectionFragment = /* GraphQL */ `
|
||||
fragment collection on Collection {
|
||||
handle
|
||||
title
|
||||
description
|
||||
seo {
|
||||
...seo
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
${seoFragment}
|
||||
`;
|
||||
|
||||
export const getCollectionQuery = /* GraphQL */ `
|
||||
query getCollection($handle: String!) {
|
||||
collection(handle: $handle) {
|
||||
...collection
|
||||
}
|
||||
}
|
||||
${collectionFragment}
|
||||
`;
|
||||
|
||||
export const getCollectionsQuery = /* GraphQL */ `
|
||||
query getCollections {
|
||||
collections(first: 100, sortKey: TITLE) {
|
||||
edges {
|
||||
node {
|
||||
...collection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${collectionFragment}
|
||||
`;
|
||||
|
||||
export const getCollectionProductsQuery = /* GraphQL */ `
|
||||
query getCollectionProducts(
|
||||
$handle: String!
|
||||
$sortKey: ProductCollectionSortKeys
|
||||
$reverse: Boolean
|
||||
) {
|
||||
collection(handle: $handle) {
|
||||
products(sortKey: $sortKey, reverse: $reverse, first: 100) {
|
||||
edges {
|
||||
node {
|
||||
...product
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${productFragment}
|
||||
`;
|
@ -1,10 +0,0 @@
|
||||
export const getMenuQuery = /* GraphQL */ `
|
||||
query getMenu($handle: String!) {
|
||||
menu(handle: $handle) {
|
||||
items {
|
||||
title
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -1,41 +0,0 @@
|
||||
import seoFragment from '../fragments/seo';
|
||||
|
||||
const pageFragment = /* GraphQL */ `
|
||||
fragment page on Page {
|
||||
... on Page {
|
||||
id
|
||||
title
|
||||
handle
|
||||
body
|
||||
bodySummary
|
||||
seo {
|
||||
...seo
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
${seoFragment}
|
||||
`;
|
||||
|
||||
export const getPageQuery = /* GraphQL */ `
|
||||
query getPage($handle: String!) {
|
||||
pageByHandle(handle: $handle) {
|
||||
...page
|
||||
}
|
||||
}
|
||||
${pageFragment}
|
||||
`;
|
||||
|
||||
export const getPagesQuery = /* GraphQL */ `
|
||||
query getPages {
|
||||
pages(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
...page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${pageFragment}
|
||||
`;
|
@ -1,32 +0,0 @@
|
||||
import productFragment from '../fragments/product';
|
||||
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct($handle: String!) {
|
||||
product(handle: $handle) {
|
||||
...product
|
||||
}
|
||||
}
|
||||
${productFragment}
|
||||
`;
|
||||
|
||||
export const getProductsQuery = /* GraphQL */ `
|
||||
query getProducts($sortKey: ProductSortKeys, $reverse: Boolean, $query: String) {
|
||||
products(sortKey: $sortKey, reverse: $reverse, query: $query, first: 100) {
|
||||
edges {
|
||||
node {
|
||||
...product
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${productFragment}
|
||||
`;
|
||||
|
||||
export const getProductRecommendationsQuery = /* GraphQL */ `
|
||||
query getProductRecommendations($productId: ID!) {
|
||||
productRecommendations(productId: $productId) {
|
||||
...product
|
||||
}
|
||||
}
|
||||
${productFragment}
|
||||
`;
|
@ -1,272 +0,0 @@
|
||||
export type Maybe<T> = T | null;
|
||||
|
||||
export type Connection<T> = {
|
||||
edges: Array<Edge<T>>;
|
||||
};
|
||||
|
||||
export type Edge<T> = {
|
||||
node: T;
|
||||
};
|
||||
|
||||
export type Cart = Omit<ShopifyCart, 'lines'> & {
|
||||
lines: CartItem[];
|
||||
};
|
||||
|
||||
export type CartProduct = {
|
||||
id: string;
|
||||
handle: string;
|
||||
title: string;
|
||||
featuredImage: Image;
|
||||
};
|
||||
|
||||
export type CartItem = {
|
||||
id: string | undefined;
|
||||
quantity: number;
|
||||
cost: {
|
||||
totalAmount: Money;
|
||||
};
|
||||
merchandise: {
|
||||
id: string;
|
||||
title: string;
|
||||
selectedOptions: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
product: CartProduct;
|
||||
};
|
||||
};
|
||||
|
||||
export type Collection = ShopifyCollection & {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
url: string;
|
||||
altText: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type Menu = {
|
||||
title: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type Money = {
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
id: string;
|
||||
title: string;
|
||||
handle: string;
|
||||
body: string;
|
||||
bodySummary: string;
|
||||
seo?: SEO;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
|
||||
variants: ProductVariant[];
|
||||
images: Image[];
|
||||
};
|
||||
|
||||
export type ProductOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type ProductVariant = {
|
||||
id: string;
|
||||
title: string;
|
||||
availableForSale: boolean;
|
||||
selectedOptions: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
price: Money;
|
||||
};
|
||||
|
||||
export type SEO = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ShopifyCart = {
|
||||
id: string | undefined;
|
||||
checkoutUrl: string;
|
||||
cost: {
|
||||
subtotalAmount: Money;
|
||||
totalAmount: Money;
|
||||
totalTaxAmount: Money;
|
||||
};
|
||||
lines: Connection<CartItem>;
|
||||
totalQuantity: number;
|
||||
};
|
||||
|
||||
export type ShopifyCollection = {
|
||||
handle: string;
|
||||
title: string;
|
||||
description: string;
|
||||
seo: SEO;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ShopifyProduct = {
|
||||
id: string;
|
||||
handle: string;
|
||||
availableForSale: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionHtml: string;
|
||||
options: ProductOption[];
|
||||
priceRange: {
|
||||
maxVariantPrice: Money;
|
||||
minVariantPrice: Money;
|
||||
};
|
||||
variants: Connection<ProductVariant>;
|
||||
featuredImage: Image;
|
||||
images: Connection<Image>;
|
||||
seo: SEO;
|
||||
tags: string[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ShopifyCartOperation = {
|
||||
data: {
|
||||
cart: ShopifyCart;
|
||||
};
|
||||
variables: {
|
||||
cartId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyCreateCartOperation = {
|
||||
data: { cartCreate: { cart: ShopifyCart } };
|
||||
};
|
||||
|
||||
export type ShopifyAddToCartOperation = {
|
||||
data: {
|
||||
cartLinesAdd: {
|
||||
cart: ShopifyCart;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
cartId: string;
|
||||
lines: {
|
||||
merchandiseId: string;
|
||||
quantity: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyRemoveFromCartOperation = {
|
||||
data: {
|
||||
cartLinesRemove: {
|
||||
cart: ShopifyCart;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
cartId: string;
|
||||
lineIds: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyUpdateCartOperation = {
|
||||
data: {
|
||||
cartLinesUpdate: {
|
||||
cart: ShopifyCart;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
cartId: string;
|
||||
lines: {
|
||||
id: string;
|
||||
merchandiseId: string;
|
||||
quantity: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyCollectionOperation = {
|
||||
data: {
|
||||
collection: ShopifyCollection;
|
||||
};
|
||||
variables: {
|
||||
handle: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyCollectionProductsOperation = {
|
||||
data: {
|
||||
collection: {
|
||||
products: Connection<ShopifyProduct>;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
handle: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyCollectionsOperation = {
|
||||
data: {
|
||||
collections: Connection<ShopifyCollection>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyMenuOperation = {
|
||||
data: {
|
||||
menu?: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
handle: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyPageOperation = {
|
||||
data: { pageByHandle: Page };
|
||||
variables: { handle: string };
|
||||
};
|
||||
|
||||
export type ShopifyPagesOperation = {
|
||||
data: {
|
||||
pages: Connection<Page>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyProductOperation = {
|
||||
data: { product: ShopifyProduct };
|
||||
variables: {
|
||||
handle: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyProductRecommendationsOperation = {
|
||||
data: {
|
||||
productRecommendations: ShopifyProduct[];
|
||||
};
|
||||
variables: {
|
||||
productId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyProductsOperation = {
|
||||
data: {
|
||||
products: Connection<ShopifyProduct>;
|
||||
};
|
||||
variables: {
|
||||
query?: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
};
|
||||
};
|
@ -4,8 +4,13 @@ export default {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.shopify.com',
|
||||
pathname: '/s/files/**'
|
||||
hostname: 'labs.commerce.services',
|
||||
pathname: '/product/**'
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'labs.commerce.services',
|
||||
pathname: '/pagewidget/**'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13,19 +13,23 @@
|
||||
"test": "pnpm prettier:check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geins/cms": "^0.3.6",
|
||||
"@geins/core": "^0.3.6",
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.3.1",
|
||||
"next": "15.0.0-rc.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"next": "15.0.3",
|
||||
"react": "19.0.0-rc-cd22717c-20241013",
|
||||
"react-dom": "19.0.0-rc-cd22717c-20241013",
|
||||
"sonner": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@geins/types": "^0.3.6",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/node": "22.9.0",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
|
991
pnpm-lock.yaml
generated
991
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
164
repopack-output.txt
Normal file
164
repopack-output.txt
Normal file
@ -0,0 +1,164 @@
|
||||
Help me finnish this reshaping of a product for a product card.
|
||||
|
||||
Here is the Types:
|
||||
|
||||
export type ProductType = {
|
||||
id: string;
|
||||
seo: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
currency: string;
|
||||
slug: string;
|
||||
handle: string;
|
||||
stockTracking: boolean;
|
||||
stockPurchasable: boolean;
|
||||
stockLevel: number;
|
||||
title: string;
|
||||
name: string;
|
||||
description: string;
|
||||
price: string;
|
||||
priceRange: {
|
||||
maxVariantPrice: MoneyType;
|
||||
minVariantPrice: MoneyType;
|
||||
};
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
tags: string[];
|
||||
options: ProductOptionType[];
|
||||
variants: ProductVariantType[];
|
||||
featuredImage: ProductImageType;
|
||||
images: ProductImageType[];
|
||||
updatedAt?: string;
|
||||
availableForSale: boolean;
|
||||
descriptionHtml?: string;
|
||||
relations?: ProductRelationType[];
|
||||
};
|
||||
|
||||
|
||||
export type ProductOptionType = {
|
||||
id: string;
|
||||
name: string;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type ProductOptionValueType = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
};
|
||||
|
||||
|
||||
export type ProductVariantType = {
|
||||
id: string;
|
||||
title: string;
|
||||
availableForSale: boolean;
|
||||
selectedOptions: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
price: MoneyType;
|
||||
};
|
||||
|
||||
Here is what i got so far:
|
||||
const reshapeProduct = (geinsProductData: any): ProductType => {
|
||||
const rawProduct = geinsProductData;
|
||||
|
||||
|
||||
const tags: string[] = [];
|
||||
const relations: ProductRelationType[] = [];
|
||||
if (rawProduct.primaryCategory) {
|
||||
tags.push(rawProduct.primaryCategory.name);
|
||||
relations.push({
|
||||
type: ProductRelationTypeEnum.CATEGORY,
|
||||
name: rawProduct.primaryCategory.name,
|
||||
alias: rawProduct.primaryCategory.alias,
|
||||
});
|
||||
}
|
||||
|
||||
if (rawProduct.brand) {
|
||||
tags.push(rawProduct.brand.name);
|
||||
relations.push({
|
||||
type: ProductRelationTypeEnum.BRAND,
|
||||
name: rawProduct.brand.name,
|
||||
alias: rawProduct.brand.alias,
|
||||
});
|
||||
}
|
||||
if(rawProduct.categories) {
|
||||
rawProduct.categories.forEach((category: any) => {
|
||||
tags.push(category.name);
|
||||
|
||||
});
|
||||
}
|
||||
// remove duplicates
|
||||
const uniqueTags = [...new Set(tags)];
|
||||
tags.push(...uniqueTags);
|
||||
|
||||
|
||||
// add descriptions from environment variables
|
||||
const shortDescription = rawProduct.texts?.[SHORT_DESCRIPTION] || '';
|
||||
const longDescription = rawProduct.texts?.[LONG_DESCRIPTION] || '';
|
||||
|
||||
// add images
|
||||
const images = rawProduct.productImages?.map((image: any): ProductImageType => ({
|
||||
caption: rawProduct.name,
|
||||
altText: rawProduct.name,
|
||||
src: `${IMAGE_URL}/product/1200f1500/${image.fileName}`,
|
||||
url: `${IMAGE_URL}/product/1200f1500/${image.fileName}`,
|
||||
height: 1600,
|
||||
width: 2000,
|
||||
}));
|
||||
|
||||
// add variations and options
|
||||
const variations: ProductVariantType[] = reshapeProductVariations(rawProduct);
|
||||
const options: ProductOptionType[] = reshapeProductOptions(rawProduct);
|
||||
|
||||
|
||||
return {
|
||||
id: rawProduct.productId,
|
||||
seo: {
|
||||
title: rawProduct.meta?.title,
|
||||
description: rawProduct.meta?.description,
|
||||
},
|
||||
metaTitle: rawProduct.meta?.title || rawProduct.name,
|
||||
metaDescription: rawProduct.meta?.description || shortDescription,
|
||||
name: rawProduct.name,
|
||||
title: rawProduct.name,
|
||||
slug: rawProduct.alias,
|
||||
handle: rawProduct.alias,
|
||||
description: shortDescription,
|
||||
descriptionHtml: rawProduct.texts?.text1 || '',
|
||||
priceRange: {
|
||||
minVariantPrice: {
|
||||
amount: rawProduct.unitPrice?.sellingPriceIncVat,
|
||||
currencyCode: CURRENCY_CODE,
|
||||
},
|
||||
maxVariantPrice: {
|
||||
amount: rawProduct.unitPrice?.sellingPriceIncVat,
|
||||
currencyCode: CURRENCY_CODE,
|
||||
},
|
||||
},
|
||||
price: rawProduct.unitPrice.sellingPriceIncVat,
|
||||
currency: CURRENCY_CODE,
|
||||
stockTracking: !!rawProduct.totalStock,
|
||||
stockPurchasable: rawProduct.totalStock?.inStock || false,
|
||||
stockLevel: rawProduct.totalStock?.totalStock || 0,
|
||||
availableForSale: true,
|
||||
tags: tags || [],
|
||||
options: [...options]|| [],
|
||||
variants: variations || [],
|
||||
featuredImage: images[0],
|
||||
images: images,
|
||||
relations: relations,
|
||||
};
|
||||
};
|
||||
|
||||
const reshapeProductVariations = (geinsProductData: any): ProductVariantType[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const reshapeProductOptions = (geinsProductData: any): ProductOptionType[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
And here is the query to API in graphql
|
Loading…
x
Reference in New Issue
Block a user