diff --git a/app/layout.tsx b/app/layout.tsx index a5cd58c84..df9c59c6f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,7 @@ import Banner from 'components/banner'; import Navbar from 'components/layout/navbar'; import { GeistSans } from 'geist/font/sans'; -import { ensureStartsWith } from 'lib/shopify/utils'; +import { ensureStartsWith } from 'lib/utils'; import { ReactNode, Suspense } from 'react'; import './globals.css'; diff --git a/app/sitemap.ts b/app/sitemap.ts index 88cd70745..9a1095b63 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,5 +1,5 @@ import { getCollections, getPages, getProducts } from 'lib/shopify'; -import { validateEnvironmentVariables } from 'lib/shopify/utils'; +import { validateEnvironmentVariables } from 'lib/utils'; import { MetadataRoute } from 'next'; type Route = { diff --git a/components/breadcrumb/breadcrumb-list.tsx b/components/breadcrumb/breadcrumb-list.tsx index 89b1bf058..ad8e2cc3d 100644 --- a/components/breadcrumb/breadcrumb-list.tsx +++ b/components/breadcrumb/breadcrumb-list.tsx @@ -1,5 +1,5 @@ import { ChevronRightIcon, EllipsisHorizontalIcon } from '@heroicons/react/16/solid'; -import { cn } from 'lib/shopify/utils'; +import { cn } from 'lib/utils'; import Link, { LinkProps } from 'next/link'; import { ComponentPropsWithoutRef, ReactNode, forwardRef } from 'react'; diff --git a/components/breadcrumb/index.tsx b/components/breadcrumb/index.tsx index d6089f606..046df568e 100644 --- a/components/breadcrumb/index.tsx +++ b/components/breadcrumb/index.tsx @@ -8,7 +8,7 @@ import { BreadcrumbPage, BreadcrumbSeparator } from './breadcrumb-list'; -import { findParentCollection } from 'lib/shopify/utils'; +import { findParentCollection } from 'lib/utils'; type BreadcrumbProps = { type: 'product' | 'collection'; diff --git a/components/cart/line-item.tsx b/components/cart/line-item.tsx index bfc3e1dc4..201615823 100644 --- a/components/cart/line-item.tsx +++ b/components/cart/line-item.tsx @@ -2,7 +2,7 @@ import { PlusIcon } from '@heroicons/react/16/solid'; import Price from 'components/price'; import { DEFAULT_OPTION } from 'lib/constants'; import { CartItem } from 'lib/shopify/types'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import Image from 'next/image'; import Link from 'next/link'; import { DeleteItemButton } from './delete-item-button'; diff --git a/components/checkbox.tsx b/components/checkbox.tsx index f68455c7d..197cb3b1d 100644 --- a/components/checkbox.tsx +++ b/components/checkbox.tsx @@ -2,7 +2,7 @@ import { CheckIcon } from '@heroicons/react/24/outline'; import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import { cn } from 'lib/shopify/utils'; +import { cn } from 'lib/utils'; import { forwardRef } from 'react'; const Checkbox = forwardRef< diff --git a/components/filters/filters-list.tsx b/components/filters/filters-list.tsx index a24e6b645..6c0661699 100644 --- a/components/filters/filters-list.tsx +++ b/components/filters/filters-list.tsx @@ -3,7 +3,7 @@ import { Button } from '@headlessui/react'; import { MAKE_FILTER_ID, MODEL_FILTER_ID, PART_TYPES, YEAR_FILTER_ID } from 'lib/constants'; import { Menu, Metaobject } from 'lib/shopify/types'; -import { createUrl, findParentCollection } from 'lib/shopify/utils'; +import { createUrl, findParentCollection } from 'lib/utils'; import get from 'lodash.get'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx index c2982b25e..551d781c2 100644 --- a/components/layout/navbar/search.tsx +++ b/components/layout/navbar/search.tsx @@ -1,7 +1,7 @@ 'use client'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import { useRouter, useSearchParams } from 'next/navigation'; export default function Search() { diff --git a/components/layout/search/filters/filters-list.tsx b/components/layout/search/filters/filters-list.tsx index a1c137b71..96e89aa07 100644 --- a/components/layout/search/filters/filters-list.tsx +++ b/components/layout/search/filters/filters-list.tsx @@ -3,7 +3,7 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react import { ChevronDownIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; import { Filter, FilterType } from 'lib/shopify/types'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import PriceRange from './price-range'; import SelectedList from './selected-list'; diff --git a/components/layout/search/filters/price-range.tsx b/components/layout/search/filters/price-range.tsx index a4a2ea9cb..22048d195 100644 --- a/components/layout/search/filters/price-range.tsx +++ b/components/layout/search/filters/price-range.tsx @@ -3,7 +3,7 @@ import Price from 'components/price'; import { useDebounce } from 'hooks'; import { Filter } from 'lib/shopify/types'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import get from 'lodash.get'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useRef, useState } from 'react'; diff --git a/components/layout/search/filters/selected-list.tsx b/components/layout/search/filters/selected-list.tsx index d6429b300..16f8a8cfa 100644 --- a/components/layout/search/filters/selected-list.tsx +++ b/components/layout/search/filters/selected-list.tsx @@ -2,7 +2,7 @@ import { XMarkIcon } from '@heroicons/react/16/solid'; import { Filter } from 'lib/shopify/types'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; const SelectedList = ({ filters }: { filters: Filter[] }) => { diff --git a/components/layout/search/sorting-menu/item.tsx b/components/layout/search/sorting-menu/item.tsx index 046572229..c828cc03b 100644 --- a/components/layout/search/sorting-menu/item.tsx +++ b/components/layout/search/sorting-menu/item.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { SortFilterItem } from 'lib/constants'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; diff --git a/components/manufacturers-grid/button-group.tsx b/components/manufacturers-grid/button-group.tsx index 6b40832d1..a9385b685 100644 --- a/components/manufacturers-grid/button-group.tsx +++ b/components/manufacturers-grid/button-group.tsx @@ -3,7 +3,7 @@ import { ArrowRightIcon } from '@heroicons/react/16/solid'; import { MAKE_FILTER_ID } from 'lib/constants'; import { Metaobject } from 'lib/shopify/types'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import { useRouter, useSearchParams } from 'next/navigation'; const ButtonGroup = ({ manufacturer }: { manufacturer: Metaobject }) => { diff --git a/components/product/core-charge.tsx b/components/product/core-charge.tsx index fd994f1c7..8a8e166df 100644 --- a/components/product/core-charge.tsx +++ b/components/product/core-charge.tsx @@ -6,7 +6,7 @@ import Price from 'components/price'; import SideDialog from 'components/side-dialog'; import { CORE_VARIANT_ID_KEY, CORE_WAIVER } from 'lib/constants'; import { CoreChargeOption, ProductVariant } from 'lib/shopify/types'; -import { cn, createUrl } from 'lib/shopify/utils'; +import { cn, createUrl } from 'lib/utils'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useState } from 'react'; diff --git a/components/product/gallery.tsx b/components/product/gallery.tsx index dad362414..efa3771b5 100644 --- a/components/product/gallery.tsx +++ b/components/product/gallery.tsx @@ -2,7 +2,7 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'; import { TileImage } from 'components/grid/tile'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import Image from 'next/image'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; diff --git a/components/product/variant-selector.tsx b/components/product/variant-selector.tsx index 41bd211bb..1cbc0ffbe 100644 --- a/components/product/variant-selector.tsx +++ b/components/product/variant-selector.tsx @@ -6,7 +6,7 @@ import clsx from 'clsx'; import Price from 'components/price'; import { CORE_VARIANT_ID_KEY, CORE_WAIVER } from 'lib/constants'; import { CoreChargeOption, Money, ProductOption, ProductVariant } from 'lib/shopify/types'; -import { createUrl } from 'lib/shopify/utils'; +import { createUrl } from 'lib/utils'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { Fragment, useEffect, useState } from 'react'; diff --git a/components/product/warranty-selector.tsx b/components/product/warranty-selector.tsx index 96ec264f3..7526f3f29 100644 --- a/components/product/warranty-selector.tsx +++ b/components/product/warranty-selector.tsx @@ -1,7 +1,7 @@ 'use client'; import Price from 'components/price'; -import { cn } from 'lib/shopify/utils'; +import { cn } from 'lib/utils'; import { ReactNode, useState } from 'react'; const options = ['Included', 'Premium Labor', '+1 Year'] as const; diff --git a/components/tooltip.tsx b/components/tooltip.tsx index 31790b30f..adea2636a 100644 --- a/components/tooltip.tsx +++ b/components/tooltip.tsx @@ -1,6 +1,6 @@ 'use client'; -import { cn } from 'lib/shopify/utils'; +import { cn } from 'lib/utils'; import { ITooltip, Tooltip as ReactTooltip } from 'react-tooltip'; const Tooltip = ({ id, children, className }: ITooltip) => { diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 652d6b4c0..3190575f1 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -12,7 +12,7 @@ import { YEAR_FILTER_ID } from 'lib/constants'; import { isShopifyError } from 'lib/type-guards'; -import { ensureStartsWith, normalizeUrl, parseJSON, parseMetaFieldValue } from 'lib/shopify/utils'; +import { ensureStartsWith, normalizeUrl, parseJSON, parseMetaFieldValue } from 'lib/utils'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 000000000..10d018ac8 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,89 @@ +import clsx, { ClassValue } from 'clsx'; +import { ReadonlyURLSearchParams } from 'next/navigation'; +import { twMerge } from 'tailwind-merge'; +import { Menu } from './types'; + +export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => { + const paramsString = params.toString(); + const queryString = `${paramsString.length ? '?' : ''}${paramsString}`; + + return `${pathname}${queryString}`; +}; + +export const ensureStartsWith = (stringToCheck: string, startsWith: string) => + stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`; + +export const validateEnvironmentVariables = () => { + const requiredEnvironmentVariables = [ + 'SHOPIFY_STORE_DOMAIN', + 'SHOPIFY_STOREFRONT_ACCESS_TOKEN', + 'SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID', + 'SHOPIFY_CUSTOMER_ACCOUNT_API_URL', + 'SHOPIFY_CUSTOMER_API_VERSION', + 'SHOPIFY_ORIGIN_URL' + ]; + const missingEnvironmentVariables = [] as string[]; + + requiredEnvironmentVariables.forEach((envVar) => { + if (!process.env[envVar]) { + missingEnvironmentVariables.push(envVar); + } + }); + + if (missingEnvironmentVariables.length) { + throw new Error( + `The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/shopify#configure-environment-variables\n\n${missingEnvironmentVariables.join( + '\n' + )}\n` + ); + } + + if ( + process.env.SHOPIFY_STORE_DOMAIN?.includes('[') || + process.env.SHOPIFY_STORE_DOMAIN?.includes(']') + ) { + throw new Error( + 'Your `SHOPIFY_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.' + ); + } +}; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function normalizeUrl(domain: string, url: string) { + return url.replace(domain, '').replace('/collections', '/search').replace('/pages', ''); +} + +export const parseMetaFieldValue = (field: { value: string } | null): T | null => { + try { + return field?.value ? JSON.parse(field.value) : null; + } catch (error) { + return null; + } +}; + +export const findParentCollection = (menu: Menu[], collection: string): Menu | null => { + let parentCollection: Menu | null = null; + for (const item of menu) { + if (item.items.length) { + const hasParent = item.items.some((subItem) => subItem.path.includes(collection)); + if (hasParent) { + return item; + } else { + parentCollection = findParentCollection(item.items, collection); + } + } + } + return parentCollection; +}; + +export function parseJSON(json: any) { + if (String(json).includes('__proto__')) return JSON.parse(json, noproto); + return JSON.parse(json); +} + +function noproto(k: string, v: string) { + if (k !== '__proto__') return v; +}