+
+
+
+
+
+
+
+
+
+
+ {item.merchandise.product.title}
+
+ {item.merchandise.title !== DEFAULT_OPTION ? (
+
+ {item.merchandise.title}
+
+ ) : null}
+
+
+
-
-
- {item.merchandise.product.title}
-
- {item.merchandise.title !== DEFAULT_OPTION ? (
-
- {item.merchandise.title}
+
+
+
+ {item.quantity}
- ) : null}
+
+
-
-
-
-
-
- {item.quantity}
-
-
-
);
})}
-
-
-
+
+
-
+
Shipping
Calculated at checkout
-
- ) : null}
+ )}
-
+
- )}
-
+
+ >
);
}
diff --git a/components/cart/open-cart.tsx b/components/cart/open-cart.tsx
new file mode 100644
index 000000000..fa8226ab5
--- /dev/null
+++ b/components/cart/open-cart.tsx
@@ -0,0 +1,24 @@
+import { ShoppingCartIcon } from '@heroicons/react/24/outline';
+import clsx from 'clsx';
+
+export default function OpenCart({
+ className,
+ quantity
+}: {
+ className?: string;
+ quantity?: number;
+}) {
+ return (
+
+
+
+ {quantity ? (
+
+ {quantity}
+
+ ) : null}
+
+ );
+}
diff --git a/components/grid/index.tsx b/components/grid/index.tsx
deleted file mode 100644
index 07dc0d2a9..000000000
--- a/components/grid/index.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import clsx from 'clsx';
-
-function Grid(props: React.ComponentProps<'ul'>) {
- return (
-
- );
-}
-
-function GridItem(props: React.ComponentProps<'li'>) {
- return (
-
- {props.children}
-
- );
-}
-
-Grid.Item = GridItem;
-export default Grid;
diff --git a/components/grid/three-items.tsx b/components/grid/three-items.tsx
deleted file mode 100644
index 6814a171a..000000000
--- a/components/grid/three-items.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { GridTileImage } from 'components/grid/tile';
-import { getCollectionProducts } from 'lib/shopify';
-import type { Product } from 'lib/shopify/types';
-import Link from 'next/link';
-
-function ThreeItemGridItem({
- item,
- size,
- background
-}: {
- item: Product;
- size: 'full' | 'half';
- background: 'white' | 'pink' | 'purple' | 'black';
-}) {
- return (
-
-
-
-
-
- );
-}
-
-export async function ThreeItemGrid() {
- // Collections that start with `hidden-*` are hidden from the search page.
- const homepageItems = await getCollectionProducts('hidden-homepage-featured-items');
-
- if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
-
- const [firstProduct, secondProduct, thirdProduct] = homepageItems;
-
- return (
-
- );
-}
diff --git a/components/grid/tile.tsx b/components/grid/tile.tsx
deleted file mode 100644
index 7d4ccab41..000000000
--- a/components/grid/tile.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import clsx from 'clsx';
-import Image from 'next/image';
-
-import Price from 'components/product/price';
-
-export function GridTileImage({
- isInteractive = true,
- background,
- active,
- labels,
- ...props
-}: {
- isInteractive?: boolean;
- background?: 'white' | 'pink' | 'purple' | 'black' | 'purple-dark' | 'blue' | 'cyan' | 'gray';
- active?: boolean;
- labels?: {
- title: string;
- amount: string;
- currencyCode: string;
- isSmall?: boolean;
- };
-} & React.ComponentProps
) {
- return (
-
- {active !== undefined && active ? (
-
- ) : null}
- {props.src ? (
-
- ) : null}
- {labels ? (
-
- ) : null}
-
- );
-}
diff --git a/components/layout/footer/copyright.tsx b/components/layout/footer/copyright.tsx
index d93063b4e..966d08d87 100644
--- a/components/layout/footer/copyright.tsx
+++ b/components/layout/footer/copyright.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import { useTranslations } from 'next-intl';
export default function CopyRight() {
diff --git a/components/layout/header/header.tsx b/components/layout/header/header.tsx
index 8244d222e..cec0819ae 100644
--- a/components/layout/header/header.tsx
+++ b/components/layout/header/header.tsx
@@ -1,21 +1,9 @@
-'use client';
-
import Logo from 'components/ui/logo/logo';
-import {
- NavigationMenu,
- NavigationMenuItem,
- NavigationMenuLink,
- NavigationMenuList,
- navigationMenuTriggerStyle
-} from 'components/ui/navigation-menu';
import { useLocale } from 'next-intl';
import Link from 'next/link';
-import { FC } from 'react';
import HeaderRoot from './header-root';
-interface HeaderProps {}
-
-const Header: FC = () => {
+const Header = () => {
const locale = useLocale();
return (
@@ -33,33 +21,8 @@ const Header: FC = () => {
-
-
-
-
-
- Junior
-
-
-
-
-
-
- Tröjor
-
-
-
-
-
-
- Byxor
-
-
-
-
-
+ 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/modules/hero/hero.tsx b/components/modules/hero/hero.tsx
index 9b8d46346..9b5754230 100644
--- a/components/modules/hero/hero.tsx
+++ b/components/modules/hero/hero.tsx
@@ -26,7 +26,7 @@ type HeroSize = keyof typeof heroSize;
const heroSize = {
fullScreen: 'aspect-[3/4] lg:aspect-auto lg:h-[calc(75vh-4rem)]',
- halfScreen: 'aspect-square max-h-[60vh] lg:aspect-auto lg:min-h-[60vh]'
+ halfScreen: 'aspect-square max-h-[50vh] lg:aspect-auto lg:min-h-[50vh]'
};
const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
diff --git a/components/preview-suspense.tsx b/components/preview-suspense.tsx
deleted file mode 100644
index d45e24877..000000000
--- a/components/preview-suspense.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-'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'
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/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 (
import('components/ui/wishlist-button'));
-
const SanityImage = dynamic(() => import('components/ui/sanity-image'));
interface Props {
@@ -30,11 +28,6 @@ const ProductCard: FC
= ({ product, className, variant = 'default' }) =>
>
{variant === 'default' && (
-
{product?.images && (
-
-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/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/storm/types/product.ts b/lib/storm/types/product.ts
index 786f07e66..9758c801f 100644
--- a/lib/storm/types/product.ts
+++ b/lib/storm/types/product.ts
@@ -147,61 +147,6 @@ export interface 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
diff --git a/package.json b/package.json
index 0d9c946fa..9df2095e6 100644
--- a/package.json
+++ b/package.json
@@ -37,9 +37,9 @@
"framer-motion": "^8.5.5",
"is-empty-iterable": "^3.0.0",
"lucide-react": "^0.194.0",
- "next": "13.4.7",
- "next-intl": "^2.14.6",
- "next-sanity": "^4.3.2",
+ "next": "13.4.13",
+ "next-intl": "3.0.0-beta.9",
+ "next-sanity": "^5.2.3",
"react": "18.2.0",
"react-cookie": "^4.1.1",
"react-dom": "18.2.0",