+
+
+
+
-
-
-
-
-
-
- Junior
-
-
-
-
-
-
- Tröjor
-
-
-
-
-
-
- Byxor
-
-
-
-
-
+
+ {/* @ts-expect-error Server Component (https://github.com/vercel/next.js/issues/42292) */}
+
-
-
+
+ }>
+ {/* @ts-expect-error Server Component (https://github.com/vercel/next.js/issues/42292) */}
+
+
diff --git a/components/layout/header/mobile-modal.tsx b/components/layout/header/mobile-modal.tsx
new file mode 100644
index 000000000..3f2f1fb29
--- /dev/null
+++ b/components/layout/header/mobile-modal.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import MenuIcon from '@/components/icons/menu';
+import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
+import { useState } from 'react';
+
+export default function MobileModal() {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <>
+
setIsOpen(!isOpen)}>
+
+
+
+
+
+
+
+ Menu
+
+
+
+ >
+ );
+}
diff --git a/components/layout/product-grid-items.tsx b/components/layout/product-grid-items.tsx
deleted file mode 100644
index 0c0e907ed..000000000
--- a/components/layout/product-grid-items.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import Grid from 'components/grid';
-import { GridTileImage } from 'components/grid/tile';
-import { Product } from 'lib/shopify/types';
-import Link from 'next/link';
-
-export default function ProductGridItems({ products }: { products: Product[] }) {
- return (
- <>
- {products.map((product) => (
-
-
-
-
-
- ))}
- >
- );
-}
diff --git a/components/ui/loading-dots.tsx b/components/loading-dots.tsx
similarity index 100%
rename from components/ui/loading-dots.tsx
rename to components/loading-dots.tsx
diff --git a/components/modules/blurb-section/blurb-section.tsx b/components/modules/blurb-section/blurb-section.tsx
index ee5241d4e..1ed8ddc0d 100644
--- a/components/modules/blurb-section/blurb-section.tsx
+++ b/components/modules/blurb-section/blurb-section.tsx
@@ -1,16 +1,16 @@
-import dynamic from 'next/dynamic'
+import dynamic from 'next/dynamic';
-import { Carousel, CarouselItem } from 'components/modules/carousel/carousel'
-const Card = dynamic(() => import('components/ui/card'))
+import { Carousel, CarouselItem } from 'components/modules/carousel/carousel';
+const Card = dynamic(() => import('components/ui/card'));
-import Text from 'components/ui/text'
+import Text from 'components/ui/text';
interface BlurbSectionProps {
- blurbs: any
- title: string
- mobileLayout: string
- desktopLayout: string
- imageFormat: 'square' | 'portrait' | 'landscape'
+ blurbs: any;
+ title: string;
+ mobileLayout: string;
+ desktopLayout: string;
+ imageFormat: 'square' | 'portrait' | 'landscape';
}
const BlurbSection = ({
@@ -18,37 +18,33 @@ const BlurbSection = ({
mobileLayout,
desktopLayout,
blurbs,
- imageFormat,
+ imageFormat
}: BlurbSectionProps) => {
const gridLayout =
desktopLayout === '2-column'
? 'lg:grid-cols-2'
: desktopLayout === '3-column'
? 'lg:grid-cols-3'
- : 'lg:grid-cols-4'
+ : 'lg:grid-cols-4';
- const sliderLayout =
- desktopLayout === '2-column' ? 2 : desktopLayout === '3-column' ? 3 : 4
+ const sliderLayout = desktopLayout === '2-column' ? 2 : desktopLayout === '3-column' ? 3 : 4;
return (
{title ? (
-
+
{title}
) : (
No title provided yet
)}
@@ -63,15 +59,11 @@ const BlurbSection = ({
imageFormat={blurb?.imageFormat}
/>
- )
+ );
})}
-
+
{blurbs && (
{blurbs.map((blurb: any, index: number) => (
@@ -100,7 +91,7 @@ const BlurbSection = ({
)}
- )
-}
+ );
+};
-export default BlurbSection
+export default BlurbSection;
diff --git a/components/modules/carousel/carousel.tsx b/components/modules/carousel/carousel.tsx
index 105bfccf1..76d35a296 100644
--- a/components/modules/carousel/carousel.tsx
+++ b/components/modules/carousel/carousel.tsx
@@ -1,28 +1,28 @@
+import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
import 'glider-js/glider.min.css';
-import { ArrowLeft, ArrowRight } from 'lucide-react';
import React from 'react';
import Glider from 'react-glider';
export interface CarouselItemProps {
- children: React.ReactNode
- className?: string
+ children: React.ReactNode;
+ className?: string;
}
export const CarouselItem: React.FC
= ({
children,
className = 'ml-2 first:ml-0 lg:ml-4'
}: CarouselItemProps) => {
- return {children}
-}
+ return {children}
;
+};
export interface CarouselProps {
- children: JSX.Element | JSX.Element[] | any
- gliderClasses?: string
- hasArrows?: boolean
- hasDots?: boolean
- slidesToShow?: number
- slidesToScroll?: number
- responsive?: any
+ children: JSX.Element | JSX.Element[] | any;
+ gliderClasses?: string;
+ hasArrows?: boolean;
+ hasDots?: boolean;
+ slidesToShow?: number;
+ slidesToScroll?: number;
+ responsive?: any;
}
export const Carousel: React.FC = ({
@@ -32,29 +32,26 @@ export const Carousel: React.FC = ({
hasDots = true,
slidesToShow = 1,
slidesToScroll = 1,
- responsive,
+ responsive
}) => {
-
-
return (
<>
}
- iconRight={}
+ iconLeft={}
+ iconRight={}
responsive={[responsive]}
>
- {React.Children.map(children, (child) => {
- return React.cloneElement(child)
- })}
-
+ {React.Children.map(children, (child) => {
+ return React.cloneElement(child);
+ })}
>
- )
-}
+ );
+};
diff --git a/components/modules/hero/hero.tsx b/components/modules/hero/hero.tsx
index 928a078ff..9b5754230 100644
--- a/components/modules/hero/hero.tsx
+++ b/components/modules/hero/hero.tsx
@@ -1,41 +1,40 @@
-import dynamic from 'next/dynamic'
+import dynamic from 'next/dynamic';
-
-import Link from 'components/ui/link/link'
-import Text from 'components/ui/text/text'
-const SanityImage = dynamic(() => import('components/ui/sanity-image'))
+import Link from 'components/ui/link/link';
+import Text from 'components/ui/text/text';
+const SanityImage = dynamic(() => import('components/ui/sanity-image'));
interface HeroProps {
- variant: string
- text?: string
- label?: string
- title: string
- image: object | any
- desktopImage: object | any
+ variant: string;
+ text?: string;
+ label?: string;
+ title: string;
+ image: object | any;
+ desktopImage: object | any;
link: {
- title: string
+ title: string;
reference: {
- title: string
+ title: string;
slug: {
- current: string
- }
- }
- }
+ current: string;
+ };
+ };
+ };
}
-type HeroSize = keyof typeof heroSize
+type HeroSize = keyof typeof heroSize;
const heroSize = {
- fullScreen: 'aspect-[3/4] lg:aspect-auto lg:h-[calc(100vh-4rem)]',
- halfScreen: 'aspect-square max-h-[60vh] lg:aspect-auto lg:min-h-[60vh]',
-}
+ fullScreen: 'aspect-[3/4] lg:aspect-auto lg:h-[calc(75vh-4rem)]',
+ halfScreen: 'aspect-square max-h-[50vh] lg:aspect-auto lg:min-h-[50vh]'
+};
const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
- const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen
+ const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen;
return (
{image && (
{
priority={true}
width={1200}
height={600}
- className="absolute inset-0 h-full w-full object-cover z-10"
+ className="absolute inset-0 z-10 h-full w-full object-cover"
/>
)}
-
+
{label && (
{label}
@@ -67,7 +66,7 @@ const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
)}
{link?.reference && (
{link?.title ? link.title : link.reference.title}
@@ -75,7 +74,7 @@ const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
)}
- )
-}
+ );
+};
-export default Hero
+export default Hero;
diff --git a/components/preview-suspense.tsx b/components/preview-suspense.tsx
index d45e24877..a0acc97e0 100644
--- a/components/preview-suspense.tsx
+++ b/components/preview-suspense.tsx
@@ -1,4 +1,4 @@
-'use client'
+'use client';
// Once rollup supports 'use client' module directives then 'next-sanity' will include them and this re-export will no longer be necessary
-export { PreviewSuspense as default } from 'next-sanity/preview'
+export { PreviewSuspense as default } from 'next-sanity/preview';
diff --git a/components/price.tsx b/components/price.tsx
new file mode 100644
index 000000000..e7090148d
--- /dev/null
+++ b/components/price.tsx
@@ -0,0 +1,24 @@
+import clsx from 'clsx';
+
+const Price = ({
+ amount,
+ className,
+ currencyCode = 'USD',
+ currencyCodeClassName
+}: {
+ amount: string;
+ className?: string;
+ currencyCode: string;
+ currencyCodeClassName?: string;
+} & React.ComponentProps<'p'>) => (
+
+ {`${new Intl.NumberFormat(undefined, {
+ style: 'currency',
+ currency: currencyCode,
+ currencyDisplay: 'narrowSymbol'
+ }).format(parseFloat(amount))}`}
+ {`${currencyCode}`}
+
+);
+
+export default Price;
diff --git a/components/product/add-to-cart.tsx b/components/product/add-to-cart.tsx
index 550f1ccb9..4314e982b 100644
--- a/components/product/add-to-cart.tsx
+++ b/components/product/add-to-cart.tsx
@@ -6,7 +6,7 @@ import clsx from 'clsx';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState, useTransition } from 'react';
-import LoadingDots from 'components/ui/loading-dots';
+import LoadingDots from 'components/loading-dots';
import { ProductVariant } from 'lib/shopify/types';
export function AddToCart({
diff --git a/components/product/product-view.tsx b/components/product/product-view.tsx
index 106499290..07176bead 100644
--- a/components/product/product-view.tsx
+++ b/components/product/product-view.tsx
@@ -1,14 +1,14 @@
'use client';
import { Carousel, CarouselItem } from 'components/modules/carousel/carousel';
-import Price from 'components/product/price';
-import SanityImage from 'components/ui/sanity-image';
+import SanityImage from 'components/ui/sanity-image/sanity-image';
+import Text from 'components/ui/text/text';
import { Product } from 'lib/storm/types/product';
import { cn } from 'lib/utils';
import { useTranslations } from 'next-intl';
import dynamic from 'next/dynamic';
+import Price from './price';
const ProductCard = dynamic(() => import('components/ui/product-card'));
-const Text = dynamic(() => import('components/ui/text'));
interface ProductViewProps {
product: Product;
relatedProducts: Product[];
@@ -27,7 +27,7 @@ export default function ProductView({ product, relatedProducts }: ProductViewPro
{images && (
1 ? true : false}
hasDots={false}
gliderClasses={'lg:px-8 2xl:px-16'}
slidesToScroll={1}
@@ -69,11 +69,11 @@ export default function ProductView({ product, relatedProducts }: ProductViewPro
-
+
{product.name}
diff --git a/components/product/variant-selector.tsx b/components/product/variant-selector.tsx
index 6aaf06a7c..89f2a322c 100644
--- a/components/product/variant-selector.tsx
+++ b/components/product/variant-selector.tsx
@@ -1,22 +1,15 @@
-// @ts-nocheck
-
'use client';
import clsx from 'clsx';
import { ProductOption, ProductVariant } from 'lib/shopify/types';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { usePathname, useSearchParams } from 'next/navigation';
-type ParamsMap = {
- [key: string]: string; // ie. { color: 'Red', size: 'Large', ... }
-};
-
-type OptimizedVariant = {
+type Combination = {
id: string;
availableForSale: boolean;
- params: URLSearchParams;
- [key: string]: string | boolean | URLSearchParams; // ie. { color: 'Red', size: 'Large', ... }
+ [key: string]: string | boolean; // ie. { color: 'Red', size: 'Large', ... }
};
export function VariantSelector({
@@ -27,8 +20,7 @@ export function VariantSelector({
variants: ProductVariant[];
}) {
const pathname = usePathname();
- const currentParams = useSearchParams();
- const router = useRouter();
+ const searchParams = useSearchParams();
const hasNoOptionsOrJustOneOption =
!options.length || (options.length === 1 && options[0]?.values.length === 1);
@@ -36,96 +28,77 @@ export function VariantSelector({
return null;
}
- // Discard any unexpected options or values from url and create params map.
- const paramsMap: ParamsMap = Object.fromEntries(
- Array.from(currentParams.entries()).filter(([key, value]) =>
- options.find((option) => option.name.toLowerCase() === key && option.values.includes(value))
+ const combinations: Combination[] = variants.map((variant) => ({
+ id: variant.id,
+ availableForSale: variant.availableForSale,
+ // Adds key / value pairs for each variant (ie. "color": "Black" and "size": 'M").
+ ...variant.selectedOptions.reduce(
+ (accumulator, option) => ({ ...accumulator, [option.name.toLowerCase()]: option.value }),
+ {}
)
- );
-
- // Optimize variants for easier lookups.
- const optimizedVariants: OptimizedVariant[] = variants.map((variant) => {
- const optimized: OptimizedVariant = {
- id: variant.id,
- availableForSale: variant.availableForSale,
- params: new URLSearchParams()
- };
-
- variant.selectedOptions.forEach((selectedOption) => {
- const name = selectedOption.name.toLowerCase();
- const value = selectedOption.value;
-
- optimized[name] = value;
- optimized.params.set(name, value);
- });
-
- return optimized;
- });
-
- // Find the first variant that is:
- //
- // 1. Available for sale
- // 2. Matches all options specified in the url (note that this
- // could be a partial match if some options are missing from the url).
- //
- // If no match (full or partial) is found, use the first variant that is
- // available for sale.
- const selectedVariant: OptimizedVariant | undefined =
- optimizedVariants.find(
- (variant) =>
- variant.availableForSale &&
- Object.entries(paramsMap).every(([key, value]) => variant[key] === value)
- ) || optimizedVariants.find((variant) => variant.availableForSale);
-
- const selectedVariantParams = new URLSearchParams(selectedVariant?.params);
- const currentUrl = createUrl(pathname, currentParams);
- const selectedVariantUrl = createUrl(pathname, selectedVariantParams);
-
- if (currentUrl !== selectedVariantUrl) {
- router.replace(selectedVariantUrl);
- }
+ }));
return options.map((option) => (
- {option.name}
-
{option.values.map((value) => {
- // Base option params on selected variant params.
- const optionParams = new URLSearchParams(selectedVariantParams);
- // Update the params using the current option to reflect how the url would change.
- optionParams.set(option.name.toLowerCase(), value);
+ const optionNameLowerCase = option.name.toLowerCase();
- const optionUrl = createUrl(pathname, optionParams);
+ // Base option params on current params so we can preserve any other param state in the url.
+ const optionSearchParams = new URLSearchParams(searchParams.toString());
- // The option is active if it in the url params.
- const isActive = selectedVariantParams.get(option.name.toLowerCase()) === value;
+ // Update the option params using the current option to reflect how the url *would* change,
+ // if the option was clicked.
+ optionSearchParams.set(optionNameLowerCase, value);
+ const optionUrl = createUrl(pathname, optionSearchParams);
- // The option is available for sale if it fully matches the variant in the option's url params.
- // It's super important to note that this is the options params, *not* the selected variant's params.
- // This is the "magic" that will cross check possible future variant combinations and preemptively
- // disable combinations that are not possible.
- const isAvailableForSale = optimizedVariants.find((a) =>
- Array.from(optionParams.entries()).every(([key, value]) => a[key] === value)
- )?.availableForSale;
+ // In order to determine if an option is available for sale, we need to:
+ //
+ // 1. Filter out all other param state
+ // 2. Filter out invalid options
+ // 3. Check if the option combination is available for sale
+ //
+ // This is the "magic" that will cross check possible variant combinations and preemptively
+ // disable combinations that are not available. For example, if the color gray is only available in size medium,
+ // then all other sizes should be disabled.
+ const filtered = Array.from(optionSearchParams.entries()).filter(([key, value]) =>
+ options.find(
+ (option) => option.name.toLowerCase() === key && option.values.includes(value)
+ )
+ );
+ const isAvailableForSale = combinations.find((combination) =>
+ filtered.every(
+ ([key, value]) => combination[key] === value && combination.availableForSale
+ )
+ );
+ // The option is active if it's in the url params.
+ const isActive = searchParams.get(optionNameLowerCase) === value;
+
+ // You can't disable a link, so we need to render something that isn't clickable.
const DynamicTag = isAvailableForSale ? Link : 'p';
+ const dynamicProps = {
+ ...(isAvailableForSale && { scroll: false })
+ };
return (
{value}
diff --git a/components/prose.tsx b/components/prose.tsx
index 2445ee588..f910d2296 100644
--- a/components/prose.tsx
+++ b/components/prose.tsx
@@ -10,7 +10,7 @@ const Prose: FunctionComponent = ({ html, className }) => {
return (
= ({ className, title, image, link, text, imageFormat = 'square' }) => {
+ const rootClassName = cn('relative', className);
-const Card: FC
= ({
- className,
- title,
- image,
- link,
- text,
- imageFormat = 'square',
-}) => {
- const rootClassName = cn('relative', className)
-
- const { linkType } = link
+ const { linkType } = link;
const imageWrapperClasses = cn('w-full h-full overflow-hidden relative', {
['aspect-square']: imageFormat === 'square',
['aspect-[3/4]']: imageFormat === 'portrait',
- ['aspect-[4/3]']: imageFormat === 'landscape',
- })
- const imageClasses = cn('object-cover w-full h-full')
+ ['aspect-[4/3]']: imageFormat === 'landscape'
+ });
+ const imageClasses = cn('object-cover w-full h-full');
function Card() {
if (linkType === 'internal') {
+ const type = link.internalLink.reference._type;
+
+ let href = '';
+
+ if (type === 'product') {
+ href = `/product/${link.internalLink.reference.slug.current}`;
+ } else if (type === 'category') {
+ href = `/category/${link.internalLink.reference.slug.current}`;
+ } else {
+ href = `${link.internalLink.reference.slug.current}`;
+ }
+
return (
-
+
{image && (
@@ -54,25 +53,17 @@ const Card: FC = ({
/>
)}
-
+
{title}
- {text && (
-
- {text}
-
- )}
+ {text &&
{text}
}
- )
+ );
}
return (
-
+
{image && (
@@ -84,20 +75,16 @@ const Card: FC = ({
/>
)}
-
+
{title}
- {text && (
-
- {text}
-
- )}
+ {text &&
{text}
}
- )
+ );
}
- return
-}
+ return ;
+};
-export default Card
+export default Card;
diff --git a/components/ui/dropdown/dropdown.tsx b/components/ui/dropdown-menu.tsx
similarity index 58%
rename from components/ui/dropdown/dropdown.tsx
rename to components/ui/dropdown-menu.tsx
index 1d4d37f12..ef793c703 100644
--- a/components/ui/dropdown/dropdown.tsx
+++ b/components/ui/dropdown-menu.tsx
@@ -1,33 +1,34 @@
-'use client'
+'use client';
-import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
-import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons'
-import * as React from 'react'
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons';
-import { cn } from 'lib/utils'
+import * as React from 'react';
-const DropdownMenu = DropdownMenuPrimitive.Root
+import { cn } from '@/lib/utils';
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+const DropdownMenu = DropdownMenuPrimitive.Root;
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
-))
-DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
+));
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
@@ -47,14 +47,13 @@ const DropdownMenuSubContent = React.forwardRef<
-))
-DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
+));
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
@@ -65,32 +64,32 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- 'z-50 min-w-[8rem] overflow-hidden bg-app text-high-contrast animate-in data-[side=right]:slide-in-from-left-2 data-[side=left]:slide-in-from-right-2 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
@@ -99,7 +98,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children}
-))
-DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
+));
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
@@ -123,7 +121,7 @@ const DropdownMenuRadioItem = React.forwardRef<
{children}
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
@@ -162,32 +156,33 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
-const DropdownMenuShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes) => {
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
return (
-
- )
-}
-DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
+
+ );
+};
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
- DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
- DropdownMenuShortcut, DropdownMenuSub,
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
DropdownMenuSubContent,
- DropdownMenuSubTrigger, DropdownMenuTrigger
-}
-
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger
+};
diff --git a/components/ui/locale-switcher/locale-switcher.tsx b/components/ui/locale-switcher/locale-switcher.tsx
index da7381659..b08651dab 100644
--- a/components/ui/locale-switcher/locale-switcher.tsx
+++ b/components/ui/locale-switcher/locale-switcher.tsx
@@ -1,82 +1,82 @@
-import LanguageIcon from 'components/icons/language';
+'use client';
+
import {
DropdownMenu,
DropdownMenuContent,
+ DropdownMenuItem,
DropdownMenuTrigger
-} from 'components/ui/dropdown/dropdown';
+} from '@/components/ui/dropdown-menu';
+import LanguageIcon from 'components/icons/language';
+import { useLocale, useTranslations } from 'next-intl';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
-import { i18n } from '../../../i18n-config';
+import { supportedLanguages } from '../../../i18n-config';
-interface LocaleSwitcherProps {
- currentLocale: string;
- localeData: {
- type: string;
- locale: string;
- translations: [];
- };
-}
+// interface LocaleSwitcherProps {
+// localeData: {
+// type: string;
+// locale: string;
+// translations: [];
+// };
+// }
-export default function LocaleSwitcher({ currentLocale, localeData }: LocaleSwitcherProps) {
+export default function LocaleSwitcher() {
const pathName = usePathname();
- const translations = localeData.translations;
+ const currentLocale = useLocale();
+ const t = useTranslations('ui');
+
+ // const translations = localeData.translations;
const redirectedPathName = (locale: string) => {
- if (!pathName || translations.length === 0) return '/';
+ if (!pathName) return '/';
- if (translations.length > 0) {
- const translation = translations.find((obj) => {
- return obj['locale'] === locale;
- });
+ // if (translations.length > 0) {
+ // const translation = translations.find((obj) => {
+ // return obj['locale'] === locale;
+ // });
- if (translation) {
- const url = `/${translation['locale']}${translation['slug']}`;
+ // if (translation) {
+ // const url = `/${translation['locale']}${translation['slug']}`;
- return url;
- }
- }
+ // return url;
+ // }
+ // }
- return '/';
+ return `/${locale}`;
};
const [isOpen, setIsOpen] = useState(false);
return (
-
+ <>
setIsOpen(!isOpen)}>
-
-
+
+
+ {currentLocale}
-
+
- {i18n.locales.map((locale) => {
- if (currentLocale === locale.id) {
- return;
- } else {
- return (
- -
-
- {locale.title}
-
-
- );
- }
+ {supportedLanguages.locales.map((locale) => {
+ return (
+
+
+ {locale.title}
+
+
+ );
})}
-
+ >
);
}
diff --git a/components/ui/navigation-menu/index.ts b/components/ui/navigation-menu/index.ts
deleted file mode 100644
index 1f801506b..000000000
--- a/components/ui/navigation-menu/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './navigation-menu';
-
diff --git a/components/ui/navigation-menu/navigation-menu.tsx b/components/ui/navigation-menu/navigation-menu.tsx
deleted file mode 100644
index 064c38aef..000000000
--- a/components/ui/navigation-menu/navigation-menu.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-'use client'
-
-import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
-import { cva } from 'class-variance-authority'
-import { ChevronDown } from 'lucide-react'
-import * as React from 'react'
-
-import { cn } from 'lib/utils'
-
-const NavigationMenu = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- {children}
-
-
-))
-NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
-
-const NavigationMenuList = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
-
-const NavigationMenuItem = NavigationMenuPrimitive.Item
-
-const navigationMenuTriggerStyle = cva(
- 'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:bg-subtle disabled:opacity-50 disabled:pointer-events-none hover:bg-ui-hover data-[state=open]:bg-ui-hover data-[active]:bg-ui-active h-10 py-1 px-3 group w-max'
-)
-
-const NavigationMenuTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- {children}{' '}
-
-
-))
-NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
-
-const NavigationMenuContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
-
-const NavigationMenuLink = NavigationMenuPrimitive.Link
-
-const NavigationMenuViewport = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-))
-NavigationMenuViewport.displayName =
- NavigationMenuPrimitive.Viewport.displayName
-
-const NavigationMenuIndicator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-))
-NavigationMenuIndicator.displayName =
- NavigationMenuPrimitive.Indicator.displayName
-
-export {
- NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, navigationMenuTriggerStyle
-}
-
diff --git a/components/ui/product-card/product-card.tsx b/components/ui/product-card/product-card.tsx
index fb73aa980..f3aeeb57e 100644
--- a/components/ui/product-card/product-card.tsx
+++ b/components/ui/product-card/product-card.tsx
@@ -8,8 +8,6 @@ import dynamic from 'next/dynamic';
import Link from 'next/link';
import { FC } from 'react';
-const WishlistButton = dynamic(() => import('components/ui/wishlist-button'));
-
const SanityImage = dynamic(() => import('components/ui/sanity-image'));
interface Props {
@@ -23,18 +21,13 @@ const ProductCard: FC = ({ product, className, variant = 'default' }) =>
return (
{variant === 'default' && (
-
{product?.images && (
= ({ product, className, variant = 'default' }) =>
-
+
{product.title}
diff --git a/components/ui/sanity-image/sanity-image.tsx b/components/ui/sanity-image/sanity-image.tsx
index 805e4b457..10399978b 100644
--- a/components/ui/sanity-image/sanity-image.tsx
+++ b/components/ui/sanity-image/sanity-image.tsx
@@ -1,21 +1,21 @@
-'use client'
+'use client';
-import { urlForImage } from 'lib/sanity/sanity.image'
-import { cn } from 'lib/utils'
-import Image from 'next/image'
+import { urlForImage } from 'lib/sanity/sanity.image';
+import { cn } from 'lib/utils';
+import Image from 'next/image';
interface SanityImageProps {
- image: object | any
- alt: string
- priority?: boolean
- width?: number
- height?: number
- quality?: number
- sizes?: string
- className?: string
+ image: object | any;
+ alt: string;
+ priority?: boolean;
+ width?: number;
+ height?: number;
+ quality?: number;
+ sizes?: string;
+ className?: string;
}
-const placeholderImg = '/product-img-placeholder.svg'
+const placeholderImg = '/product-img-placeholder.svg';
export default function SanityImage(props: SanityImageProps) {
const {
@@ -26,10 +26,10 @@ export default function SanityImage(props: SanityImageProps) {
height = 1080,
width = 1080,
sizes = '100vw',
- className,
- } = props
+ className
+ } = props;
- const rootClassName = cn('w-full h-auto', className)
+ const rootClassName = cn('w-full h-auto', className);
const image = source?.asset?._rev ? (
<>
@@ -39,11 +39,7 @@ export default function SanityImage(props: SanityImageProps) {
width={width}
height={height}
alt={alt}
- src={urlForImage(source)
- .width(width)
- .height(height)
- .quality(quality)
- .url()}
+ src={urlForImage(source).width(width).height(height).quality(quality).url()}
sizes={sizes}
priority={priority}
blurDataURL={source.asset.metadata.lqip}
@@ -61,7 +57,7 @@ export default function SanityImage(props: SanityImageProps) {
priority={false}
/>
>
- )
+ );
- return image
+ return image;
}
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
new file mode 100644
index 000000000..bbcfe2f6f
--- /dev/null
+++ b/components/ui/sheet.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import * as SheetPrimitive from '@radix-ui/react-dialog';
+import { Cross1Icon } from '@radix-ui/react-icons';
+import { cva, type VariantProps } from 'class-variance-authority';
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Sheet = SheetPrimitive.Root;
+
+const SheetTrigger = SheetPrimitive.Trigger;
+
+const SheetClose = SheetPrimitive.Close;
+
+const SheetPortal = ({ className, ...props }: SheetPrimitive.DialogPortalProps) => (
+
+);
+SheetPortal.displayName = SheetPrimitive.Portal.displayName;
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
+
+const sheetVariants = cva(
+ 'fixed z-50 gap-4 bg-background p-4 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 lg:p-6',
+ {
+ variants: {
+ side: {
+ top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
+ bottom:
+ 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
+ left: 'inset-y-0 left-0 h-full w-full border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left md:max-w-md',
+ right:
+ 'inset-y-0 right-0 h-full w-full border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right md:max-w-md'
+ }
+ },
+ defaultVariants: {
+ side: 'right'
+ }
+ }
+);
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = 'right', className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+SheetContent.displayName = SheetPrimitive.Content.displayName;
+
+const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = 'SheetHeader';
+
+const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+SheetFooter.displayName = 'SheetFooter';
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetClose,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger
+};
diff --git a/components/ui/text/text.tsx b/components/ui/text/text.tsx
index 66ad02881..d533a7d72 100644
--- a/components/ui/text/text.tsx
+++ b/components/ui/text/text.tsx
@@ -69,8 +69,8 @@ const Text: FunctionComponent = ({
variant === 'productHeading',
['max-w-prose font-display text-2xl font-extrabold leading-none md:text-3xl md:leading-none lg:text-4xl lg:leading-none']:
variant === 'sectionHeading',
- ['text-sm font-medium leading-tight lg:text-base']: variant === 'listChildHeading',
- ['max-w-prose text-sm lg:text-base 2xl:text-lg']: variant === 'label',
+ ['text-sm leading-tight lg:text-base']: variant === 'listChildHeading',
+ ['max-w-prose text-lg text-high-contrast lg:text-xl']: variant === 'label',
['max-w-prose lg:text-lg 2xl:text-xl']: variant === 'paragraph'
},
className
diff --git a/components/ui/wishlist-button/index.ts b/components/ui/wishlist-button/index.ts
deleted file mode 100644
index 8ed750d03..000000000
--- a/components/ui/wishlist-button/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './wishlist-button';
diff --git a/components/ui/wishlist-button/wishlist-button.tsx b/components/ui/wishlist-button/wishlist-button.tsx
deleted file mode 100644
index c721a24b4..000000000
--- a/components/ui/wishlist-button/wishlist-button.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-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/i18n-config.ts b/i18n-config.ts
index c320efda0..8a34813cd 100644
--- a/i18n-config.ts
+++ b/i18n-config.ts
@@ -1,4 +1,4 @@
-export const i18n = {
+export const supportedLanguages = {
defaultLocale: 'sv',
locales: [
{
@@ -12,4 +12,4 @@ export const i18n = {
],
} as const
-export type Locale = typeof i18n['locales'][number]
\ No newline at end of file
+export type Locale = typeof supportedLanguages['locales'][number]
\ No newline at end of file
diff --git a/lib/constants.ts b/lib/constants.ts
new file mode 100644
index 000000000..99711221a
--- /dev/null
+++ b/lib/constants.ts
@@ -0,0 +1,30 @@
+export type SortFilterItem = {
+ title: string;
+ slug: string | null;
+ sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
+ reverse: boolean;
+};
+
+export const defaultSort: SortFilterItem = {
+ title: 'Relevance',
+ slug: null,
+ sortKey: 'RELEVANCE',
+ reverse: false
+};
+
+export const sorting: SortFilterItem[] = [
+ defaultSort,
+ { title: 'Trending', slug: 'trending-desc', sortKey: 'BEST_SELLING', reverse: false }, // asc
+ { title: 'Latest arrivals', slug: 'latest-desc', sortKey: 'CREATED_AT', reverse: true },
+ { title: 'Price: Low to high', slug: 'price-asc', sortKey: 'PRICE', reverse: false }, // asc
+ { title: 'Price: High to low', slug: 'price-desc', sortKey: 'PRICE', reverse: true }
+];
+
+export const TAGS = {
+ collections: 'collections',
+ products: 'products'
+};
+
+export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
+export const DEFAULT_OPTION = 'Default Title';
+export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
diff --git a/lib/constants.tsx b/lib/constants.tsx
index 9038127e3..e69de29bb 100644
--- a/lib/constants.tsx
+++ b/lib/constants.tsx
@@ -1,25 +0,0 @@
-export type SortFilterItem = {
- title: string;
- slug: string | null;
- sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
- reverse: boolean;
-};
-
-export const defaultSort: SortFilterItem = {
- title: 'Relevance',
- slug: null,
- sortKey: 'RELEVANCE',
- reverse: false
-};
-
-export const sorting: SortFilterItem[] = [
- defaultSort,
- { title: 'Trending', slug: 'trending-desc', sortKey: 'BEST_SELLING', reverse: false }, // asc
- { title: 'Latest arrivals', slug: 'latest-desc', sortKey: 'CREATED_AT', reverse: true },
- { title: 'Price: Low to high', slug: 'price-asc', sortKey: 'PRICE', reverse: false }, // asc
- { title: 'Price: High to low', slug: 'price-desc', sortKey: 'PRICE', reverse: true }
-];
-
-export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
-export const DEFAULT_OPTION = 'Default Title';
-export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
diff --git a/lib/sanity/queries.tsx b/lib/sanity/queries.tsx
index 1e35dcbff..8418280d0 100644
--- a/lib/sanity/queries.tsx
+++ b/lib/sanity/queries.tsx
@@ -107,6 +107,7 @@ export const modules = `
desktopLayout,
imageFormat,
blurbs[]->{
+ _type,
title,
text,
link {
@@ -209,29 +210,27 @@ export const productQuery = `*[_type == "product" && slug.current == $slug && la
"slug": slug.current,
"locale": language
},
- "product": {
- id,
- "name": title,
- description,
- "descriptionHtml": "",
- images[] {
- ${imageFields}
- },
- price {
- value,
- currencyCode,
- retailPrice
- },
- options[] {
- id,
- displayName,
- values[] {
- label,
- "hexColors": hexColors.hex
- }
- },
- "variants": []
+ id,
+ "name": title,
+ description,
+ "descriptionHtml": "",
+ images[] {
+ ${imageFields}
},
+ price {
+ value,
+ currencyCode,
+ retailPrice
+ },
+ options[] {
+ id,
+ displayName,
+ values[] {
+ label,
+ "hexColors": hexColors.hex
+ }
+ },
+ "variants": [],
seo {
${seoFields}
}
@@ -243,15 +242,8 @@ export const categoryQuery = `*[_type == "category" && slug.current == $slug &&
title,
"slug": slug.current,
"locale": language,
- showBanner,
- banner {
- _type,
- _key,
- title,
- text,
- image {
- ${imageFields}
- }
+ image {
+ ${imageFields}
},
"translations": *[_type == "translation.metadata" && references(^._id)].translations[].value->{
title,
@@ -263,6 +255,34 @@ export const categoryQuery = `*[_type == "category" && slug.current == $slug &&
}
}`;
+// Categories query
+export const categoriesQuery = `*[_type == "category" && language == $locale] | order(title asc) {
+ _type,
+ title,
+ "slug": slug.current,
+ "locale": language,
+}`;
+
+// Footer menu query
+export const footerMenusQuery = `*[_type == "footerMenu" && language == $locale] | order(title asc) {
+ _type,
+ title,
+ "locale": language,
+ menu {
+ title,
+ links[] {
+ _type,
+ title,
+ reference-> {
+ slug,
+ "locale": language
+ },
+ url,
+ newWindow
+ }
+ }
+}`;
+
// Site settings query
export const siteSettingsQuery = `*[_type == "settings" && language == $locale][0] {
menuMain {
diff --git a/lib/sanity/sanity.client.ts b/lib/sanity/sanity.client.ts
index 3c10a146e..b7bd07fb9 100644
--- a/lib/sanity/sanity.client.ts
+++ b/lib/sanity/sanity.client.ts
@@ -9,7 +9,7 @@ export const client = createClient({
projectId,
dataset,
apiVersion,
- useCdn: false,
+ useCdn: true,
});
export const clientFetch = cache(client.fetch.bind(client));
\ No newline at end of file
diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts
index a2f45a36f..a8804d045 100644
--- a/lib/shopify/index.ts
+++ b/lib/shopify/index.ts
@@ -1,5 +1,8 @@
-import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT } from 'lib/constants';
+import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
+import { revalidateTag } from 'next/cache';
+import { headers } from 'next/headers';
+import { NextRequest, NextResponse } from 'next/server';
import {
addToCartMutation,
createCartMutation,
@@ -23,6 +26,7 @@ import {
Cart,
Collection,
Connection,
+ Image,
Menu,
Page,
Product,
@@ -52,15 +56,17 @@ const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
type ExtractVariables = T extends { variables: object } ? T['variables'] : never;
export async function shopifyFetch({
- query,
- variables,
+ cache = 'force-cache',
headers,
- cache = 'force-cache'
+ query,
+ tags,
+ variables
}: {
- query: string;
- variables?: ExtractVariables;
- headers?: HeadersInit;
cache?: RequestCache;
+ headers?: HeadersInit;
+ query: string;
+ tags?: string[];
+ variables?: ExtractVariables;
}): Promise<{ status: number; body: T } | never> {
try {
const result = await fetch(endpoint, {
@@ -75,7 +81,7 @@ export async function shopifyFetch({
...(variables && { variables })
}),
cache,
- next: { revalidate: 900 } // 15 minutes
+ ...(tags && { next: { tags } })
});
const body = await result.json();
@@ -149,6 +155,18 @@ const reshapeCollections = (collections: ShopifyCollection[]) => {
return reshapedCollections;
};
+const reshapeImages = (images: Connection, 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;
@@ -158,7 +176,7 @@ const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean =
return {
...rest,
- images: removeEdgesAndNodes(images),
+ images: reshapeImages(images, product.title),
variants: removeEdgesAndNodes(variants)
};
};
@@ -232,15 +250,16 @@ export async function updateCart(
return reshapeCart(res.body.data.cartLinesUpdate.cart);
}
-export async function getCart(cartId: string): Promise {
+export async function getCart(cartId: string): Promise {
const res = await shopifyFetch({
query: getCartQuery,
variables: { cartId },
cache: 'no-store'
});
+ // Old carts becomes `null` when you checkout.
if (!res.body.data.cart) {
- return null;
+ return undefined;
}
return reshapeCart(res.body.data.cart);
@@ -249,6 +268,7 @@ export async function getCart(cartId: string): Promise {
export async function getCollection(handle: string): Promise {
const res = await shopifyFetch({
query: getCollectionQuery,
+ tags: [TAGS.collections],
variables: {
handle
}
@@ -257,16 +277,27 @@ export async function getCollection(handle: string): Promise {
+export async function getCollectionProducts({
+ collection,
+ reverse,
+ sortKey
+}: {
+ collection: string;
+ reverse?: boolean;
+ sortKey?: string;
+}): Promise {
const res = await shopifyFetch({
query: getCollectionProductsQuery,
+ tags: [TAGS.collections, TAGS.products],
variables: {
- handle
+ handle: collection,
+ reverse,
+ sortKey: sortKey === 'CREATED_AT' ? 'CREATED' : sortKey
}
});
if (!res.body.data.collection) {
- console.log('No collection found for handle', handle);
+ console.log(`No collection found for \`${collection}\``);
return [];
}
@@ -274,7 +305,10 @@ export async function getCollectionProducts(handle: string): Promise
}
export async function getCollections(): Promise {
- const res = await shopifyFetch({ query: getCollectionsQuery });
+ const res = await shopifyFetch({
+ query: getCollectionsQuery,
+ tags: [TAGS.collections]
+ });
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
const collections = [
{
@@ -301,6 +335,7 @@ export async function getCollections(): Promise {
export async function getMenu(handle: string): Promise