mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 06:56:59 +00:00
Merge branch 'main' into update-next
This commit is contained in:
commit
04eb717e2c
16
README.md
16
README.md
@ -20,11 +20,11 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
|||||||
|
|
||||||
## Run minimal version locally
|
## Run minimal version locally
|
||||||
|
|
||||||
> To run a minimal version of Next.js Commerce you can start with the default local provider `@vercel/commerce-local` that has disabled all features (cart, auth) and use static files for the backend
|
> To run a minimal version of Next.js Commerce you can start with the default local provider `@vercel/commerce-local` that has all features disabled (cart, auth) and uses static files for the backend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install & pnpm build # run this commands in root folder of the mono repo
|
pnpm install & pnpm build # run these commands in the root folder of the mono repo
|
||||||
pnpm dev # run this commands in the site folder
|
pnpm dev # run this command in the site folder
|
||||||
```
|
```
|
||||||
|
|
||||||
> If you encounter any problems while installing and running for the first time, please see the Troubleshoot section
|
> If you encounter any problems while installing and running for the first time, please see the Troubleshoot section
|
||||||
@ -47,10 +47,10 @@ Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Sal
|
|||||||
|
|
||||||
## Considerations
|
## Considerations
|
||||||
|
|
||||||
- `packages/commerce` contains all types, helpers and functions to be used as base to build a new **provider**.
|
- `packages/commerce` contains all types, helpers and functions to be used as a base to build a new **provider**.
|
||||||
- **Providers** live under `packages`'s root folder and they will extend Next.js Commerce types and functionality (`packages/commerce`).
|
- **Providers** live under `packages`'s root folder and they will extend Next.js Commerce types and functionality (`packages/commerce`).
|
||||||
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically.
|
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programmatically.
|
||||||
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in case of BigCommerce, the images CDN and additional API routes.
|
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in the case of BigCommerce, the images CDN and additional API routes.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ Every provider defines the features that it supports under `packages/{provider}/
|
|||||||
#### Features Available
|
#### Features Available
|
||||||
|
|
||||||
The following features can be enabled or disabled. This means that the UI will remove all code related to the feature.
|
The following features can be enabled or disabled. This means that the UI will remove all code related to the feature.
|
||||||
For example: Turning `cart` off will disable Cart capabilities.
|
For example: turning `cart` off will disable Cart capabilities.
|
||||||
|
|
||||||
- cart
|
- cart
|
||||||
- search
|
- search
|
||||||
@ -83,7 +83,7 @@ For example: Turning `cart` off will disable Cart capabilities.
|
|||||||
|
|
||||||
#### How to turn Features on and off
|
#### How to turn Features on and off
|
||||||
|
|
||||||
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
|
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out of the box)
|
||||||
|
|
||||||
- Open `site/commerce.config.json`
|
- Open `site/commerce.config.json`
|
||||||
- You'll see a config file like this:
|
- You'll see a config file like this:
|
||||||
|
@ -42,7 +42,9 @@ export class CommerceNetworkError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const normalizeZodIssues = (issues: ZodError['issues']) =>
|
export const normalizeZodIssues = (issues: ZodError['issues']) =>
|
||||||
issues.map(({ path, message }) => `${message} at "${path.join('.')}" field`)
|
issues.map(({ path, message }) =>
|
||||||
|
path.length ? `${message} at "${path.join('.')}" field` : message
|
||||||
|
)
|
||||||
|
|
||||||
export const getOperationError = (operation: string, error: unknown) => {
|
export const getOperationError = (operation: string, error: unknown) => {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
|
@ -21,7 +21,7 @@ export const withOperationCallback =
|
|||||||
const parse = ({ name, data }: Operation) => {
|
const parse = ({ name, data }: Operation) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'getProduct':
|
case 'getProduct':
|
||||||
productSchema.nullable().parse(data.product)
|
productSchema.optional().parse(data.product)
|
||||||
break
|
break
|
||||||
case 'getAllProducts':
|
case 'getAllProducts':
|
||||||
z.array(productSchema).parse(data.products)
|
z.array(productSchema).parse(data.products)
|
||||||
|
@ -27,7 +27,7 @@ export default function getSiteInfoOperation({
|
|||||||
const { sdkFetch } = commerce.getConfig(config)
|
const { sdkFetch } = commerce.getConfig(config)
|
||||||
const { data: categories } = await sdkFetch('categories', 'list')
|
const { data: categories } = await sdkFetch('categories', 'list')
|
||||||
|
|
||||||
const formattedCategories = categories.map(normalizeCategory)
|
const formattedCategories = categories?.map(normalizeCategory) ?? []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: formattedCategories,
|
categories: formattedCategories,
|
||||||
|
@ -22,17 +22,17 @@ export const handler: MutationHook<AddItemHook> = {
|
|||||||
variables.push(item.variantId)
|
variables.push(item.variantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cart } = await fetch<{ cart: CommercejsCart }>({
|
const cart = await fetch<CommercejsCart>({
|
||||||
query: options.query,
|
query: options.query,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
variables,
|
variables,
|
||||||
})
|
})
|
||||||
|
|
||||||
return normalizeCart(cart)
|
return normalizeCart(cart)
|
||||||
},
|
},
|
||||||
useHook: ({ fetch }) =>
|
useHook: ({ fetch }) =>
|
||||||
function useHook() {
|
function useHook() {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input) {
|
async function addItem(input) {
|
||||||
const cart = await fetch({ input })
|
const cart = await fetch({ input })
|
||||||
|
@ -16,7 +16,7 @@ export const handler: MutationHook<RemoveItemHook> = {
|
|||||||
method: 'remove',
|
method: 'remove',
|
||||||
},
|
},
|
||||||
async fetcher({ input, options, fetch }) {
|
async fetcher({ input, options, fetch }) {
|
||||||
const { cart } = await fetch<{ cart: CommercejsCart }>({
|
const cart = await fetch<CommercejsCart>({
|
||||||
query: options.query,
|
query: options.query,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
variables: input.itemId,
|
variables: input.itemId,
|
||||||
|
@ -30,7 +30,7 @@ export const handler = {
|
|||||||
},
|
},
|
||||||
async fetcher({ input, options, fetch }: HookFetcherContext<UpdateItemHook>) {
|
async fetcher({ input, options, fetch }: HookFetcherContext<UpdateItemHook>) {
|
||||||
const variables = [input.itemId, { quantity: input.item.quantity }]
|
const variables = [input.itemId, { quantity: input.item.quantity }]
|
||||||
const { cart } = await fetch<{ cart: CommercejsCart }>({
|
const cart = await fetch<CommercejsCart>({
|
||||||
query: options.query,
|
query: options.query,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
variables,
|
variables,
|
||||||
@ -57,7 +57,7 @@ export const handler = {
|
|||||||
const variantId = input.productId ?? item?.variantId
|
const variantId = input.productId ?? item?.variantId
|
||||||
const quantity = input?.quantity ?? item?.quantity
|
const quantity = input?.quantity ?? item?.quantity
|
||||||
|
|
||||||
if (!itemId || !productId || !variantId) {
|
if (!itemId || !productId) {
|
||||||
throw new ValidationError({
|
throw new ValidationError({
|
||||||
message: 'Invalid input for updating cart item',
|
message: 'Invalid input for updating cart item',
|
||||||
})
|
})
|
||||||
@ -69,7 +69,7 @@ export const handler = {
|
|||||||
item: {
|
item: {
|
||||||
quantity,
|
quantity,
|
||||||
productId,
|
productId,
|
||||||
variantId,
|
variantId: variantId ?? '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -44,14 +44,16 @@ const normalizeLineItem = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalizeCart = (commercejsCart: CommercejsCart): Cart => {
|
export const normalizeCart = (
|
||||||
|
commercejsCart: CommercejsCart | { cart: CommercejsCart }
|
||||||
|
): Cart => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
created,
|
created,
|
||||||
subtotal: { raw: rawPrice },
|
subtotal: { raw: rawPrice },
|
||||||
currency,
|
currency,
|
||||||
line_items,
|
line_items,
|
||||||
} = commercejsCart
|
} = 'cart' in commercejsCart ? commercejsCart.cart : commercejsCart
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
@ -54,6 +54,7 @@ export function normalizeProduct(
|
|||||||
): Product {
|
): Product {
|
||||||
const { id, name, description, permalink, assets, price, variant_groups } =
|
const { id, name, description, permalink, assets, price, variant_groups } =
|
||||||
commercejsProduct
|
commercejsProduct
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -61,15 +62,19 @@ export function normalizeProduct(
|
|||||||
descriptionHtml: description,
|
descriptionHtml: description,
|
||||||
slug: permalink,
|
slug: permalink,
|
||||||
path: `/${permalink}`,
|
path: `/${permalink}`,
|
||||||
images: assets.map(({ url, description, filename }) => ({
|
images:
|
||||||
url,
|
assets?.map(({ url, description, filename }) => ({
|
||||||
alt: description || filename,
|
url,
|
||||||
})),
|
alt: description || filename,
|
||||||
|
})) || [],
|
||||||
price: {
|
price: {
|
||||||
value: price.raw,
|
value: price.raw,
|
||||||
currencyCode: 'USD',
|
currencyCode: 'USD',
|
||||||
},
|
},
|
||||||
variants: normalizeVariants(commercejsProductVariants, variant_groups),
|
variants: normalizeVariants(
|
||||||
options: getOptionsFromVariantGroups(variant_groups),
|
commercejsProductVariants,
|
||||||
|
variant_groups || []
|
||||||
|
),
|
||||||
|
options: variant_groups ? getOptionsFromVariantGroups(variant_groups) : [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import type { GraphQLFetcher } from '@vercel/commerce/api'
|
|||||||
|
|
||||||
import { API_URL } from '../../const'
|
import { API_URL } from '../../const'
|
||||||
import { getError } from '../../utils/handle-fetch-response'
|
import { getError } from '../../utils/handle-fetch-response'
|
||||||
import { getCommerceApi } from '..'
|
|
||||||
import { getToken } from '../../utils/index'
|
import { getToken } from '../../utils/index'
|
||||||
|
|
||||||
const fetchGraphqlApi: GraphQLFetcher = async (
|
const fetchGraphqlApi: GraphQLFetcher = async (
|
||||||
@ -10,7 +9,6 @@ const fetchGraphqlApi: GraphQLFetcher = async (
|
|||||||
{ variables } = {},
|
{ variables } = {},
|
||||||
headers?: HeadersInit
|
headers?: HeadersInit
|
||||||
) => {
|
) => {
|
||||||
const config = getCommerceApi().getConfig()
|
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
|
|
||||||
const res = await fetch(API_URL!, {
|
const res = await fetch(API_URL!, {
|
||||||
@ -28,10 +26,17 @@ const fetchGraphqlApi: GraphQLFetcher = async (
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data, errors, status } = await res.json()
|
const { data, errors, message, type, status } = await res.json()
|
||||||
|
|
||||||
if (errors) {
|
if (errors || res.status >= 400) {
|
||||||
throw getError(errors, status)
|
throw getError(
|
||||||
|
errors || [
|
||||||
|
{
|
||||||
|
message: `${type ? `${type}, ` : ''}${message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status || res.status
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data, res }
|
return { data, res }
|
||||||
|
@ -7,6 +7,7 @@ import getSlug from '@lib/get-slug'
|
|||||||
import { Github, Vercel } from '@components/icons'
|
import { Github, Vercel } from '@components/icons'
|
||||||
import { Logo, Container } from '@components/ui'
|
import { Logo, Container } from '@components/ui'
|
||||||
import { I18nWidget } from '@components/common'
|
import { I18nWidget } from '@components/common'
|
||||||
|
import ThemeSwitcher from '@components/ui/ThemeSwitcher'
|
||||||
import s from './Footer.module.css'
|
import s from './Footer.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -41,7 +42,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
<span>ACME</span>
|
<span>ACME</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 lg:col-span-8">
|
<div className="col-span-1 lg:col-span-7">
|
||||||
<div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
|
<div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
|
||||||
{[...links, ...sitePages].map((page) => (
|
{[...links, ...sitePages].map((page) => (
|
||||||
<span key={page.url} className="py-3 md:py-0 md:pb-4">
|
<span key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||||
@ -55,8 +56,10 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 lg:col-span-2 flex items-start lg:justify-end text-primary">
|
<div className="col-span-1 lg:col-span-3 flex items-start lg:justify-end text-primary">
|
||||||
<div className="flex space-x-6 items-center h-10">
|
<div className="flex space-x-4 items-center h-10">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
<I18nWidget />
|
||||||
<a
|
<a
|
||||||
className={s.link}
|
className={s.link}
|
||||||
aria-label="Github Repository"
|
aria-label="Github Repository"
|
||||||
@ -64,7 +67,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
>
|
>
|
||||||
<Github />
|
<Github />
|
||||||
</a>
|
</a>
|
||||||
<I18nWidget />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply h-10 px-2 rounded-md border border-accent-2 flex items-center justify-center transition-colors ease-linear;
|
@apply h-10 pl-2 pr-1 rounded-md border border-accent-2 flex items-center justify-center transition-colors ease-linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon.active {
|
.icon.active {
|
||||||
transform: rotate(180deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
|
@ -3,7 +3,7 @@ import Link from 'next/link'
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import s from './I18nWidget.module.css'
|
import s from './I18nWidget.module.css'
|
||||||
import { Cross, ChevronUp } from '@components/icons'
|
import { Cross, ChevronRight } from '@components/icons'
|
||||||
import ClickOutside from '@lib/click-outside'
|
import ClickOutside from '@lib/click-outside'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
interface LOCALE_DATA {
|
interface LOCALE_DATA {
|
||||||
@ -54,14 +54,14 @@ const I18nWidget: FC = () => {
|
|||||||
<Image
|
<Image
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
className="block mr-2 w-5"
|
className="block w-5"
|
||||||
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
||||||
alt={LOCALES_MAP[currentLocale].img.alt}
|
alt={LOCALES_MAP[currentLocale].img.alt}
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
{options && (
|
{options && (
|
||||||
<span className="cursor-pointer">
|
<span className="cursor-pointer ml-1">
|
||||||
<ChevronUp className={cn(s.icon, { [s.active]: display })} />
|
<ChevronRight className={cn(s.icon, { [s.active]: display })} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
19
site/components/icons/System.tsx
Normal file
19
site/components/icons/System.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const System = ({ ...props }) => (
|
||||||
|
<svg
|
||||||
|
data-testid="geist-icon"
|
||||||
|
fill="none"
|
||||||
|
height="16"
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="text-current"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M2 13.381h20M8.66 19.05V22m6.84-2.95V22m-8.955 0h10.932M4 19.05h16a2 2 0 002-2V4a2 2 0 00-2-2H4a2 2 0 00-2 2v13.05a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default System
|
@ -11,6 +11,7 @@ export { default as Cross } from './Cross'
|
|||||||
export { default as Minus } from './Minus'
|
export { default as Minus } from './Minus'
|
||||||
export { default as Check } from './Check'
|
export { default as Check } from './Check'
|
||||||
export { default as Github } from './Github'
|
export { default as Github } from './Github'
|
||||||
|
export { default as System } from './System'
|
||||||
export { default as Vercel } from './Vercel'
|
export { default as Vercel } from './Vercel'
|
||||||
export { default as MapPin } from './MapPin'
|
export { default as MapPin } from './MapPin'
|
||||||
export { default as ArrowLeft } from './ArrowLeft'
|
export { default as ArrowLeft } from './ArrowLeft'
|
||||||
|
22
site/components/ui/ThemeSwitcher/ThemeIcon.tsx
Normal file
22
site/components/ui/ThemeSwitcher/ThemeIcon.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Moon, Sun, System } from '@components/icons'
|
||||||
|
|
||||||
|
interface ThemeIconProps {
|
||||||
|
theme?: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeIcon = ({ theme, ...props }: ThemeIconProps) => {
|
||||||
|
switch (theme) {
|
||||||
|
case 'light':
|
||||||
|
return <Sun {...props} />
|
||||||
|
|
||||||
|
case 'dark':
|
||||||
|
return <Moon {...props} />
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <System {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeIcon
|
80
site/components/ui/ThemeSwitcher/ThemeSwitcher.tsx
Normal file
80
site/components/ui/ThemeSwitcher/ThemeSwitcher.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { ChevronRight, Cross } from '@components/icons'
|
||||||
|
import { useToggleTheme } from '@lib/hooks/useToggleTheme'
|
||||||
|
import cn from 'clsx'
|
||||||
|
import ClickOutside from '@lib/click-outside'
|
||||||
|
import ThemeIcon from './ThemeIcon'
|
||||||
|
|
||||||
|
const ThemeSwitcher = () => {
|
||||||
|
const [display, setDisplay] = useState(false)
|
||||||
|
const { theme, themes, setTheme } = useToggleTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClickOutside active={display} onClick={() => setDisplay(false)}>
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className="flex items-center relative"
|
||||||
|
onClick={() => setDisplay(!display)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
'w-[125px] h-10 pl-2 pr-1 rounded-md border border-accent-2 flex items-center justify-between transition-colors ease-linear hover:border-accent-3 hover:shadow-sm'
|
||||||
|
}
|
||||||
|
aria-label="Theme Switcher"
|
||||||
|
>
|
||||||
|
<span className="flex flex-shrink items-center">
|
||||||
|
<ThemeIcon width={20} height={20} theme={theme} />
|
||||||
|
<span className={cn('capitalize leading-none ml-2')}>
|
||||||
|
{theme}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="cursor-pointer">
|
||||||
|
<ChevronRight
|
||||||
|
className={cn('transition duration-300', {
|
||||||
|
['rotate-90']: display,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-0 right-0">
|
||||||
|
{themes.length && display ? (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'fixed shadow-lg right-0 top-12 mt-2 origin-top-right w-full h-full outline-none bg-accent-0 z-40 lg:absolute lg:border lg:border-accent-1 lg:shadow-lg lg:w-56 lg:h-auto'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row justify-end px-6">
|
||||||
|
<button
|
||||||
|
className="md:hidden"
|
||||||
|
onClick={() => setDisplay(false)}
|
||||||
|
aria-label="Close panel"
|
||||||
|
>
|
||||||
|
<Cross className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{themes.map((t: string) => (
|
||||||
|
<li key={t}>
|
||||||
|
<button
|
||||||
|
className="flex w-full capitalize cursor-pointer px-6 py-3 transition ease-in-out duration-150 text-primary leading-6 font-medium items-center hover:bg-accent-1"
|
||||||
|
role={'link'}
|
||||||
|
onClick={() => {
|
||||||
|
setTheme(t)
|
||||||
|
setDisplay(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ClickOutside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeSwitcher
|
1
site/components/ui/ThemeSwitcher/index.ts
Normal file
1
site/components/ui/ThemeSwitcher/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ThemeSwitcher'
|
11
site/lib/hooks/useToggleTheme.ts
Normal file
11
site/lib/hooks/useToggleTheme.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export const useToggleTheme = () => {
|
||||||
|
const { theme, themes, setTheme } = useTheme()
|
||||||
|
const [themeValue, setThemeValue] = useState<string>('system')
|
||||||
|
|
||||||
|
useEffect(() => setThemeValue(theme), [theme])
|
||||||
|
|
||||||
|
return { theme: themeValue, setTheme, themes }
|
||||||
|
}
|
@ -34,7 +34,9 @@ export async function getStaticProps({
|
|||||||
const { products: relatedProducts } = await allProductsPromise
|
const { products: relatedProducts } = await allProductsPromise
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new Error(`Product with slug '${params!.slug}' not found`)
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user