From a9ad63d0563c77260f4d138254b5b5a6f5872fe3 Mon Sep 17 00:00:00 2001 From: Henrik Larsson Date: Wed, 3 May 2023 23:16:19 +0200 Subject: [PATCH] Work with displaying content --- app/[locale]/globals.css | 4 + components/grid/tile.tsx | 2 +- components/product/add-to-cart.tsx | 2 +- components/product/price.tsx | 6 +- components/ui/blurb-section/blurb-section.tsx | 2 + components/ui/card/index.tsx | 3 +- components/ui/carousel/carousel.tsx | 6 +- .../filtered-product-list.tsx | 3 +- components/ui/product-card/product-card.tsx | 110 ++++----- components/ui/slider/slider.tsx | 12 +- components/ui/wishlist-button/index.ts | 1 + .../ui/wishlist-button/wishlist-button.tsx | 79 ++++++ lib/storm/types/common.ts | 36 +++ lib/storm/types/product.ts | 227 ++++++++++++++++++ 14 files changed, 421 insertions(+), 72 deletions(-) create mode 100644 components/ui/wishlist-button/index.ts create mode 100644 components/ui/wishlist-button/wishlist-button.tsx create mode 100644 lib/storm/types/common.ts create mode 100644 lib/storm/types/product.ts 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 } +}