diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css
index 7a899dedd..0fd7dd278 100644
--- a/app/[locale]/globals.css
+++ b/app/[locale]/globals.css
@@ -56,6 +56,10 @@ body {
@apply !m-0 !rounded-none !w-12 !h-4 !bg-transparent after:content-[''] after:block after:w-12 after:h-[3px] after:bg-ui-border 2xl:!w-16 2xl:after:w-16;
}
+.glider-slide {
+ @apply max-w-full;
+}
+
.glider-dot.active {
@apply after:!bg-high-contrast;
}
diff --git a/components/grid/tile.tsx b/components/grid/tile.tsx
index b2f219276..7d4ccab41 100644
--- a/components/grid/tile.tsx
+++ b/components/grid/tile.tsx
@@ -1,7 +1,7 @@
import clsx from 'clsx';
import Image from 'next/image';
-import Price from 'components/price';
+import Price from 'components/product/price';
export function GridTileImage({
isInteractive = true,
diff --git a/components/product/add-to-cart.tsx b/components/product/add-to-cart.tsx
index 9da11bafa..2eba6e800 100644
--- a/components/product/add-to-cart.tsx
+++ b/components/product/add-to-cart.tsx
@@ -4,7 +4,7 @@ import clsx from 'clsx';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState, useTransition } from 'react';
-import LoadingDots from 'components/loading-dots';
+import LoadingDots from 'components/ui/loading-dots';
import { ProductVariant } from 'lib/shopify/types';
export function AddToCart({
diff --git a/components/product/price.tsx b/components/product/price.tsx
index 90f6558a0..42098a172 100644
--- a/components/product/price.tsx
+++ b/components/product/price.tsx
@@ -1,17 +1,19 @@
const Price = ({
amount,
- currencyCode = 'USD',
+ currencyCode,
...props
}: {
amount: string;
- currencyCode: string;
+ currencyCode: string | 'SEK' | 'GPB';
} & React.ComponentProps<'p'>) => (
+
{`${new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currencyCode,
currencyDisplay: 'narrowSymbol'
}).format(parseFloat(amount))} ${currencyCode}`}
+
);
diff --git a/components/ui/blurb-section/blurb-section.tsx b/components/ui/blurb-section/blurb-section.tsx
index 3b120754b..90dc20052 100644
--- a/components/ui/blurb-section/blurb-section.tsx
+++ b/components/ui/blurb-section/blurb-section.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import {
CarouselItemProps as ItemProps,
CarouselProps as Props,
diff --git a/components/ui/card/index.tsx b/components/ui/card/index.tsx
index 95cfda28e..cf9543f67 100644
--- a/components/ui/card/index.tsx
+++ b/components/ui/card/index.tsx
@@ -1 +1,2 @@
-export { default } from './Card'
+export { default } from './card';
+
diff --git a/components/ui/carousel/carousel.tsx b/components/ui/carousel/carousel.tsx
index 8aa60edf8..bd8a5694b 100644
--- a/components/ui/carousel/carousel.tsx
+++ b/components/ui/carousel/carousel.tsx
@@ -12,7 +12,7 @@ export interface CarouselItemProps {
export const CarouselItem: React.FC = ({
children,
}: CarouselItemProps) => {
- return {children}
+ return <>{children}>
}
export interface CarouselProps {
@@ -39,7 +39,7 @@ export const Carousel: React.FC = ({
return (
= ({
responsive={[responsive]}
skipTrack
>
-
+
{React.Children.map(children, (child) => {
return React.cloneElement(child)
})}
diff --git a/components/ui/filtered-product-list/filtered-product-list.tsx b/components/ui/filtered-product-list/filtered-product-list.tsx
index fb403ea04..9b6340b98 100644
--- a/components/ui/filtered-product-list/filtered-product-list.tsx
+++ b/components/ui/filtered-product-list/filtered-product-list.tsx
@@ -25,8 +25,7 @@ const FilteredProductList = ({ title, products, itemsToShow }: SliderProps) => {
)}
{products.slice(0, itemsToShow).map((product: any, index: number) => (
-
Product
- //
+
))}
diff --git a/components/ui/product-card/product-card.tsx b/components/ui/product-card/product-card.tsx
index d0744bf98..ccaf863dc 100644
--- a/components/ui/product-card/product-card.tsx
+++ b/components/ui/product-card/product-card.tsx
@@ -1,79 +1,81 @@
'use client'
+import Price from 'components/product/price'
+import Text from 'components/ui/text'
+import type { Product } from 'lib/storm/types/product'
import { cn } from 'lib/utils'
-import { FC } from 'react'
-// import type { Product } from '@commerce/types/product'
import dynamic from 'next/dynamic'
-// import usePrice from '@framework/product/use-price'
+import Link from 'next/link'
+import { FC } from 'react'
+
+const WishlistButton = dynamic(
+ () => import('components/ui/wishlist-button')
+)
-// const WishlistButton = dynamic(
-// () => import('@components/wishlist/WishlistButton')
-// )
-const ProductTag = dynamic(() => import('components/ui/product-tag'))
const SanityImage = dynamic(() => import('components/ui/sanity-image'))
interface Props {
className?: string
- // product: Product
+ product: Product
variant?: 'default'
}
const ProductCard: FC
= ({
- // product,
+ product,
className,
variant = 'default',
}) => {
- // const { price } = usePrice({
- // amount: product.price.value,
- // baseAmount: product.price.retailPrice,
- // currencyCode: product.price.currencyCode!,
- // })
const rootClassName = cn(
- 'w-full min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear basis-[50%]',
+ 'w-full group relative overflow-hidden transition-transform ease-linear',
className
)
return (
- <>Produyct>
- //
- // {variant === 'default' && (
- // <>
- //
- // {/* {process.env.COMMERCE_WISHLIST_ENABLED && (
- //
- // )} */}
- // {/*
- // {product?.images && (
- //
- // )}
- //
*/}
- //
- //
- // >
- // )}
- //
+
+ {variant === 'default' && (
+
+
+
+
+ {product?.images && (
+
+ )}
+
+
+
+
+ {product.title}
+
+
+
+
+ )}
+
)
}
diff --git a/components/ui/slider/slider.tsx b/components/ui/slider/slider.tsx
index 87ab65000..b05107d46 100644
--- a/components/ui/slider/slider.tsx
+++ b/components/ui/slider/slider.tsx
@@ -1,5 +1,3 @@
-'use client'
-
import {
CarouselItemProps as ItemProps,
CarouselProps as Props,
@@ -32,7 +30,7 @@ const Slider = ({ products, categories, title, sliderType }: SliderProps) => {
}, [])
return (
-
+
{title ? (
{
{items.map((item: any, index: number) => (
- {item.title}
- {/* {sliderType === 'products' && }
- {sliderType === 'categories' && } */}
+ {sliderType === 'products' && }
+ {sliderType === 'categories' && }
))}
diff --git a/components/ui/wishlist-button/index.ts b/components/ui/wishlist-button/index.ts
new file mode 100644
index 000000000..8ed750d03
--- /dev/null
+++ b/components/ui/wishlist-button/index.ts
@@ -0,0 +1 @@
+export { default } from './wishlist-button';
diff --git a/components/ui/wishlist-button/wishlist-button.tsx b/components/ui/wishlist-button/wishlist-button.tsx
new file mode 100644
index 000000000..c721a24b4
--- /dev/null
+++ b/components/ui/wishlist-button/wishlist-button.tsx
@@ -0,0 +1,79 @@
+import type { Product, ProductVariant } from 'lib/storm/types/product'
+import { cn } from 'lib/utils'
+import { Heart } from 'lucide-react'
+import { useTranslations } from 'next-intl'
+import React, { FC, useState } from 'react'
+
+type Props = {
+ productId: Product['id']
+ variant: ProductVariant
+} & React.ButtonHTMLAttributes
+
+const WishlistButton: FC = ({
+ productId,
+ variant,
+ className,
+ ...props
+}) => {
+ const [loading, setLoading] = useState(false)
+ const t = useTranslations('ui.button')
+
+ // @ts-ignore Wishlist is not always enabled
+ // const itemInWishlist = data?.items?.find(
+ // // @ts-ignore Wishlist is not always enabled
+ // (item) => item.product_id === productId && item.variant_id === variant.id
+ // )
+
+ const handleWishlistChange = async (e: any) => {
+ e.preventDefault()
+
+ if (loading) return
+
+ // A login is required before adding an item to the wishlist
+ // if (!customer) {
+ // setModalView('LOGIN_VIEW')
+ // return openModal()
+ // }
+
+ // setLoading(true)
+
+ // try {
+ // if (itemInWishlist) {
+ // await removeItem({ id: itemInWishlist.id! })
+ // } else {
+ // await addItem({
+ // productId,
+ // variantId: variant?.id!,
+ // })
+ // }
+
+ // setLoading(false)
+ // } catch (err) {
+ // setLoading(false)
+ // }
+ }
+
+ return (
+
+ )
+}
+
+export default WishlistButton
diff --git a/lib/storm/types/common.ts b/lib/storm/types/common.ts
new file mode 100644
index 000000000..d63dfc0b9
--- /dev/null
+++ b/lib/storm/types/common.ts
@@ -0,0 +1,36 @@
+export interface Discount {
+ /**
+ * The value of the discount, can be an amount or percentage.
+ */
+ value: number
+}
+
+export interface Measurement {
+ /**
+ * The measurement's value.
+ */
+ value: number
+ /**
+ * The measurement's unit, such as "KILOGRAMS", "GRAMS", "POUNDS" & "OOUNCES".
+ */
+ unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
+}
+
+export interface Image {
+ /**
+ * The URL of the image.
+ */
+ url: string
+ /**
+ * A word or phrase that describes the content of an image.
+ */
+ alt?: string
+ /**
+ * The image's width.
+ */
+ width?: number
+ /**
+ * The image's height.
+ */
+ height?: number
+}
diff --git a/lib/storm/types/product.ts b/lib/storm/types/product.ts
new file mode 100644
index 000000000..786f07e66
--- /dev/null
+++ b/lib/storm/types/product.ts
@@ -0,0 +1,227 @@
+import { Image } from './common'
+
+export interface ProductPrice {
+ /**
+ * The price after all discounts are applied.
+ */
+ value: number
+ /**
+ * The currency code for the price. This is a 3-letter ISO 4217 code.
+ * @example USD
+ */
+ currencyCode?: 'USD' | 'EUR' | 'ARS' | 'GBP' | string
+ /**
+ * The retail price of the product. This can be used to mark a product as on sale, when `retailPrice` is higher than the price a.k.a `value`.
+ */
+ retailPrice?: number
+}
+
+export interface ProductOption {
+ __typename?: 'MultipleChoiceOption'
+ /**
+ * The unique identifier for the option.
+ */
+ id: string
+ /**
+ * The product option’s name.
+ * @example `Color` or `Size`
+ */
+ displayName: string
+ /**
+ * List of option values.
+ * @example `["Red", "Green", "Blue"]`
+ */
+ values: ProductOptionValues[]
+}
+
+export interface ProductOptionValues {
+ /**
+ * A string that uniquely identifies the option value.
+ */
+ label: string
+ /**
+ * List of hex colors used to display the actual colors in the swatches instead of the name.
+ */
+ hexColors?: string[]
+}
+
+export interface ProductVariant {
+ /**
+ * The unique identifier for the variant.
+ */
+ id: string
+ /**
+ * The SKU (stock keeping unit) associated with the product variant.
+ */
+ sku?: string
+ /**
+ * The product variant’s name, or the product's name.
+ */
+ name?: string
+ /**
+ * List of product options.
+ */
+ options: ProductOption[]
+ /**
+ * The product variant’s price after all discounts are applied.
+ */
+ price?: ProductPrice
+ /**
+ * The retail price of the product. This can be used to mark a product as on sale, when `retailPrice` is higher than the `price`.
+ */
+ retailPrice?: ProductPrice
+ /**
+ * Indicates if the variant is available for sale.
+ */
+ availableForSale?: boolean
+ /**
+ * Whether a customer needs to provide a shipping address when placing an order for the product variant.
+ */
+ requiresShipping?: boolean
+ /**
+ * The image associated with the variant.
+ */
+ image?: Image
+}
+
+export interface Product {
+ /**
+ * The currency for the product.
+ */
+ currencyCode: string
+ /**
+ * The title for the product.
+ */
+ title: string
+ /**
+ * The unique identifier for the product.
+ */
+ id: string
+ /**
+ * The name of the product.
+ */
+ name: string
+ /**
+ * Stripped description of the product, single line.
+ */
+ description: string
+ /**
+ * The description of the product, complete with HTML formatting.
+ */
+ descriptionHtml?: string
+ /**
+ * The SKU (stock keeping unit) associated with the product.
+ */
+ sku?: string
+ /**
+ * A human-friendly unique string for the product, automatically generated from its title.
+ */
+ slug?: string
+ /**
+ * Relative URL on the storefront for the product.
+ */
+ path?: string
+ /**
+ * List of images associated with the product.
+ */
+ images: Image[]
+ /**
+ * List of the product’s variants.
+ */
+ variants: ProductVariant[]
+ /**
+ * The product's base price. Could be the minimum value, or default variant price.
+ */
+ price: ProductPrice
+ /**
+ * List of product's options.
+ */
+ options: ProductOption[]
+ /**
+ * The product’s vendor name.
+ */
+ vendor?: string
+ /**
+ * The locale version of the product.
+ */
+ locale?: string
+}
+
+export interface SearchProductsBody {
+ /**
+ * The search query string to filter the products by.
+ */
+ search?: string
+ /**
+ * The category ID to filter the products by.
+ */
+ categoryId?: string
+ /**
+ * The brand ID to filter the products by.
+ */
+ brandId?: string
+ /**
+ * The sort key to sort the products by.
+ * @example 'trending-desc' | 'latest-desc' | 'price-asc' | 'price-desc'
+ */
+ sort?: string
+ /**
+ * The locale code, used to localize the product data (if the provider supports it).
+ */
+ locale?: string
+}
+
+/**
+ * Fetches a list of products based on the given search criteria.
+ */
+export type SearchProductsHook = {
+ data: {
+ /**
+ * List of products matching the query.
+ */
+ products: Product[]
+ /**
+ * Indicates if there are any products matching the query.
+ */
+ found: boolean
+ }
+ body: SearchProductsBody
+ input: SearchProductsBody
+ fetcherInput: SearchProductsBody
+}
+
+/**
+ * Product API schema
+ */
+
+export type ProductsSchema = {
+ endpoint: {
+ options: {}
+ handlers: {
+ getProducts: SearchProductsHook
+ }
+ }
+}
+
+/**
+ * Product operations
+ */
+
+export type GetAllProductPathsOperation = {
+ data: { products: Pick[] }
+ variables: { first?: number }
+}
+
+export type GetAllProductsOperation = {
+ data: { products: Product[] }
+ variables: {
+ relevance?: 'featured' | 'best_selling' | 'newest'
+ ids?: string[]
+ first?: number
+ }
+}
+
+export type GetProductOperation = {
+ data: { product?: Product }
+ variables: { path: string; slug?: never } | { path?: never; slug: string }
+}