mirror of
https://github.com/vercel/commerce.git
synced 2025-07-23 04:36:49 +00:00
Iterated with translations
This commit is contained in:
115
components/ui/blurb-section/blurb-section.tsx
Normal file
115
components/ui/blurb-section/blurb-section.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
CarouselItemProps as ItemProps,
|
||||
CarouselProps as Props,
|
||||
} from 'components/ui/carousel/carousel'
|
||||
import dynamic from 'next/dynamic'
|
||||
const Carousel = dynamic<Props>(() =>
|
||||
import('components/ui/carousel/carousel').then((mod) => mod.Carousel)
|
||||
)
|
||||
const CarouselItem = dynamic<ItemProps>(() =>
|
||||
import('components/ui/carousel/carousel').then((mod) => mod.CarouselItem)
|
||||
)
|
||||
const Card = dynamic(() => import('components/ui/card'))
|
||||
|
||||
import Text from 'components/ui/text'
|
||||
|
||||
interface BlurbSectionProps {
|
||||
blurbs: any
|
||||
title: string
|
||||
mobileLayout: string
|
||||
desktopLayout: string
|
||||
imageFormat: 'square' | 'portrait' | 'landscape'
|
||||
}
|
||||
|
||||
const BlurbSection = ({
|
||||
title,
|
||||
mobileLayout,
|
||||
desktopLayout,
|
||||
blurbs,
|
||||
imageFormat,
|
||||
}: BlurbSectionProps) => {
|
||||
const gridLayout =
|
||||
desktopLayout === '2-column'
|
||||
? 'lg:grid-cols-2'
|
||||
: desktopLayout === '3-column'
|
||||
? 'lg:grid-cols-3'
|
||||
: 'lg:grid-cols-4'
|
||||
|
||||
const sliderLayout =
|
||||
desktopLayout === '2-column' ? 2 : desktopLayout === '3-column' ? 3 : 4
|
||||
|
||||
return (
|
||||
<div>
|
||||
{title ? (
|
||||
<Text
|
||||
className="mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||
variant="sectionHeading"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
className="italic mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||
variant="sectionHeading"
|
||||
>
|
||||
No title provided yet
|
||||
</Text>
|
||||
)}
|
||||
<div
|
||||
className={`px-4 grid ${gridLayout} gap-x-4 gap-y-8 ${
|
||||
mobileLayout === 'stacked' ? 'lg:hidden' : 'hidden'
|
||||
} lg:px-8 2xl:!px-16`}
|
||||
>
|
||||
{blurbs.map((blurb: object | any, index: number) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<Card
|
||||
title={blurb?.title}
|
||||
link={blurb?.link}
|
||||
image={blurb?.image}
|
||||
text={blurb?.text}
|
||||
imageFormat={blurb?.imageFormat}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${
|
||||
mobileLayout === 'stacked' ? 'hidden lg:block' : 'block'
|
||||
}`}
|
||||
>
|
||||
{blurbs && (
|
||||
<Carousel
|
||||
gliderClasses={'px-4 lg:px-8 2xl:px-16'}
|
||||
gliderItemWrapperClasses={'space-x-2 lg:space-x-4'}
|
||||
hasDots={true}
|
||||
slidesToShow={2.2}
|
||||
responsive={{
|
||||
breakpoint: 1024,
|
||||
settings: {
|
||||
slidesToShow:
|
||||
sliderLayout <= 4 ? sliderLayout + 0.5 : sliderLayout,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{blurbs.map((blurb: any, index: number) => (
|
||||
<CarouselItem key={`${index}`}>
|
||||
<Card
|
||||
title={blurb?.title}
|
||||
link={blurb?.link}
|
||||
image={blurb?.image}
|
||||
text={blurb.text}
|
||||
imageFormat={imageFormat}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlurbSection
|
2
components/ui/blurb-section/index.ts
Normal file
2
components/ui/blurb-section/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './blurb-section';
|
||||
|
103
components/ui/card/card.tsx
Normal file
103
components/ui/card/card.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import SanityImage from 'components/ui/sanity-image'
|
||||
import { cn } from 'lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface CardProps {
|
||||
className?: string
|
||||
title: string
|
||||
image: object | any
|
||||
link: object | any
|
||||
text?: string
|
||||
imageFormat?: 'square' | 'portrait' | 'landscape'
|
||||
}
|
||||
|
||||
const placeholderImg = '/product-img-placeholder.svg'
|
||||
|
||||
const Card: FC<CardProps> = ({
|
||||
className,
|
||||
title,
|
||||
image,
|
||||
link,
|
||||
text,
|
||||
imageFormat = 'square',
|
||||
}) => {
|
||||
const rootClassName = cn('relative', className)
|
||||
|
||||
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')
|
||||
|
||||
function Card() {
|
||||
if (linkType === 'internal') {
|
||||
return (
|
||||
<Link
|
||||
href={link.internalLink.reference.slug.current}
|
||||
className={rootClassName}
|
||||
aria-label={title}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{image && (
|
||||
<div className={imageWrapperClasses}>
|
||||
<SanityImage
|
||||
className={imageClasses}
|
||||
image={image}
|
||||
alt={image.alt || ''}
|
||||
sizes="(max-width: 1024px) 50vw, 20vw"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h3 className="mt-2 text-high-contrast font-medium text-sm underline underline-offset-2 lg:text-lg lg:mt-3 lg:underline-offset-4 2xl:text-xl">
|
||||
{title}
|
||||
</h3>
|
||||
{text && (
|
||||
<p className="text-sm mt-1 text-low-contrast lg:text-base lg:mt-2">
|
||||
{text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={link.externalLink.url}
|
||||
className={rootClassName}
|
||||
aria-label={title}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{image && (
|
||||
<div className={imageWrapperClasses}>
|
||||
<SanityImage
|
||||
className={imageClasses}
|
||||
image={image}
|
||||
alt={image.alt || ''}
|
||||
sizes="(max-width: 1024px) 50vw, 20vw"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h3 className="mt-2 text-high-contrast font-medium text-sm underline underline-offset-2 lg:text-lg lg:mt-3 lg:underline-offset-4 2xl:text-xl">
|
||||
{title}
|
||||
</h3>
|
||||
{text && (
|
||||
<p className="text-sm mt-1 text-low-contrast lg:text-base lg:mt-2">
|
||||
{text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return <Card />
|
||||
}
|
||||
|
||||
export default Card
|
1
components/ui/card/index.tsx
Normal file
1
components/ui/card/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Card'
|
@@ -1,39 +0,0 @@
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export async function Carousel() {
|
||||
// Collections that start with `hidden-*` are hidden from the search page.
|
||||
const products = await getCollectionProducts('hidden-homepage-carousel');
|
||||
|
||||
if (!products?.length) return null;
|
||||
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden bg-black dark:bg-white">
|
||||
<div className="flex animate-carousel">
|
||||
{[...products, ...products].map((product, i) => (
|
||||
<Link
|
||||
key={`${product.handle}${i}`}
|
||||
href={`/product/${product.handle}`}
|
||||
className="relative h-[30vh] w-1/2 flex-none md:w-1/3"
|
||||
>
|
||||
{product.featuredImage ? (
|
||||
<Image
|
||||
alt={product.title}
|
||||
className="h-full object-contain"
|
||||
fill
|
||||
sizes="33vw"
|
||||
src={product.featuredImage.url}
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute inset-y-0 right-0 flex items-center justify-center">
|
||||
<div className="inline-flex bg-white p-4 text-xl font-semibold text-black dark:bg-black dark:text-white">
|
||||
{product.title}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
62
components/ui/carousel/carousel.tsx
Normal file
62
components/ui/carousel/carousel.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export const CarouselItem: React.FC<CarouselItemProps> = ({
|
||||
children,
|
||||
}: CarouselItemProps) => {
|
||||
return <div className="">{children}</div>
|
||||
}
|
||||
|
||||
export interface CarouselProps {
|
||||
children: JSX.Element | JSX.Element[] | any
|
||||
gliderClasses?: string
|
||||
hasArrows?: boolean
|
||||
hasDots?: boolean
|
||||
gliderItemWrapperClasses?: string
|
||||
slidesToShow?: number
|
||||
slidesToScroll?: number
|
||||
responsive?: any
|
||||
}
|
||||
|
||||
export const Carousel: React.FC<CarouselProps> = ({
|
||||
children,
|
||||
gliderClasses,
|
||||
hasArrows = true,
|
||||
hasDots = true,
|
||||
gliderItemWrapperClasses,
|
||||
slidesToShow = 1,
|
||||
slidesToScroll = 1,
|
||||
responsive,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Glider
|
||||
className={`flex w-full relative ${gliderClasses}`}
|
||||
draggable
|
||||
slidesToShow={slidesToShow}
|
||||
scrollLock
|
||||
slidesToScroll={slidesToScroll}
|
||||
hasArrows={hasArrows}
|
||||
hasDots={hasDots}
|
||||
iconLeft={<ArrowLeft className="stroke-current" />}
|
||||
iconRight={<ArrowRight className="stroke-current" />}
|
||||
responsive={[responsive]}
|
||||
skipTrack
|
||||
>
|
||||
<div className={`flex w-full ${gliderItemWrapperClasses} `}>
|
||||
{React.Children.map(children, (child) => {
|
||||
return React.cloneElement(child)
|
||||
})}
|
||||
</div>
|
||||
</Glider>
|
||||
</div>
|
||||
)
|
||||
}
|
46
components/ui/category-card/category-card.tsx
Normal file
46
components/ui/category-card/category-card.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import SanityImage from 'components/ui/sanity-image'
|
||||
import { cn } from 'lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
category: any
|
||||
}
|
||||
|
||||
const placeholderImg = '/product-img-placeholder.svg'
|
||||
|
||||
const CategoryCard: FC<Props> = ({ category, className }) => {
|
||||
const rootClassName = cn(
|
||||
'w-1/2 min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear cursor-pointer basis-[50%]',
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`${category.slug}`}
|
||||
className={rootClassName}
|
||||
aria-label={category.name}
|
||||
>
|
||||
<div className={'flex flex-col flex-1 justify-center w-full h-full'}>
|
||||
<div className="w-full h-full aspect-[3/4] relative">
|
||||
<SanityImage
|
||||
image={category.image}
|
||||
alt={category.name || 'Category Image'}
|
||||
width={300}
|
||||
height={400}
|
||||
sizes="(max-width: 1024px) 50vw, 25vw"
|
||||
/>
|
||||
<div className="absolute font-medium bg-high-contrast text-white py-3 px-6 md:py-5 md:px-10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
{category.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default CategoryCard
|
2
components/ui/category-card/index.ts
Normal file
2
components/ui/category-card/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './category-card';
|
||||
|
@@ -1,3 +1,5 @@
|
||||
'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'
|
||||
|
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
|
||||
import { Info } from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
import Hero from 'components/ui/hero'
|
||||
const Slider = dynamic(() => import('components/ui/slider'))
|
||||
const BlurbSection = dynamic(() => import('components/ui/blurb-section'))
|
||||
const FilteredProductList = dynamic(
|
||||
() => import('components/ui/filtered-product-list')
|
||||
)
|
||||
|
||||
interface getContentComponentProps {
|
||||
_type: string
|
||||
_key: number
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
const getContentComponent = ({
|
||||
_type,
|
||||
_key,
|
||||
disabled,
|
||||
...rest
|
||||
}: getContentComponentProps) => {
|
||||
let Component: any
|
||||
|
||||
switch (_type) {
|
||||
case 'hero':
|
||||
Component = Hero
|
||||
break
|
||||
case 'slider':
|
||||
Component = Slider
|
||||
break
|
||||
case 'filteredProductList':
|
||||
Component = FilteredProductList
|
||||
break
|
||||
case 'blurbSection':
|
||||
if (disabled !== true) {
|
||||
Component = BlurbSection
|
||||
} else {
|
||||
return
|
||||
}
|
||||
break
|
||||
default:
|
||||
return (
|
||||
<div
|
||||
className={`px-4 lg:px-8 2xl:px-16 ${
|
||||
process.env.NODE_ENV === 'production' ? 'hidden' : ''
|
||||
}`}
|
||||
key={`index-${_key}`}
|
||||
>
|
||||
<span className="inline-flex items-center bg-red font-bold p-2 text-sm">
|
||||
<Info className="mr-1" />
|
||||
{`No matching component (Type: ${_type})`}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return Component ? (
|
||||
<Component key={`index-${_key}`} {...rest} />
|
||||
) : (
|
||||
<div key={`index-${_key}`}>Something else</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface dynamicContentManagerProps {
|
||||
content: [] | any
|
||||
}
|
||||
|
||||
const DynamicContentManager = ({ content }: dynamicContentManagerProps) => {
|
||||
return (
|
||||
<div className="dynamic-content overflow-x-hidden">
|
||||
{content?.map(getContentComponent)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DynamicContentManager
|
2
components/ui/dynamic-content-manager/index.tsx
Normal file
2
components/ui/dynamic-content-manager/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './dynamic-content-manager';
|
||||
|
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import Text from 'components/ui/text'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const ProductCard = dynamic(() => import('components/ui/product-card'))
|
||||
|
||||
interface SliderProps {
|
||||
products: any
|
||||
title: string
|
||||
itemsToShow: number
|
||||
}
|
||||
|
||||
const FilteredProductList = ({ title, products, itemsToShow }: SliderProps) => {
|
||||
return (
|
||||
<div className="px-4 lg:px-8 2xl:px-16">
|
||||
{title ? (
|
||||
<Text className="mb-4 lg:mb-6 2xl:mb-8" variant="sectionHeading">
|
||||
{title}
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="italic mb-4 lg:mb-6 2xl:mb-8" variant="sectionHeading">
|
||||
No title provided yet
|
||||
</Text>
|
||||
)}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{products.slice(0, itemsToShow).map((product: any, index: number) => (
|
||||
<span>Product</span>
|
||||
// <ProductCard key={`${product.id}-${index}`} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilteredProductList
|
2
components/ui/filtered-product-list/index.ts
Normal file
2
components/ui/filtered-product-list/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './filtered-product-list';
|
||||
|
82
components/ui/hero/hero.tsx
Normal file
82
components/ui/hero/hero.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const SanityImage = dynamic(() => import('components/ui/sanity-image'))
|
||||
const Link = dynamic(() => import('components/ui/link'))
|
||||
const Text = dynamic(() => import('components/ui/text'))
|
||||
|
||||
interface HeroProps {
|
||||
variant: string
|
||||
text?: string
|
||||
label?: string
|
||||
title: string
|
||||
image: object | any
|
||||
desktopImage: object | any
|
||||
link: {
|
||||
title: string
|
||||
reference: {
|
||||
title: string
|
||||
slug: {
|
||||
current: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]',
|
||||
}
|
||||
|
||||
const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
|
||||
const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative w-screen ${heroClass} flex flex-col justify-end relative text-high-contrast`}
|
||||
>
|
||||
{image && (
|
||||
<SanityImage
|
||||
image={image}
|
||||
alt={image.alt}
|
||||
priority={true}
|
||||
width={1200}
|
||||
height={600}
|
||||
className="absolute inset-0 h-full w-full object-cover z-10"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col items-start text-high-contrast absolute max-w-sm z-40 left-4 bottom-5 lg:max-w-xl lg:bottom-8 lg:left-8 2xl:left-16 2xl:bottom-16">
|
||||
{label && (
|
||||
<Text className="mb-1 lg:mb-2" variant="label">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{title ? (
|
||||
<Text variant="heading">{title}</Text>
|
||||
) : (
|
||||
<Text variant="heading" className="italic">
|
||||
No title provided yet
|
||||
</Text>
|
||||
)}
|
||||
{text && (
|
||||
<Text className="mt-4" variant="paragraph">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{link?.reference && (
|
||||
<Link
|
||||
className="inline-flex transition bg-high-contrast text-white text-base py-4 px-10 mt-6 hover:bg-low-contrast lg:mt-8"
|
||||
href={link.reference.slug.current}
|
||||
>
|
||||
{link?.title ? link.title : link.reference.title}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hero
|
2
components/ui/hero/index.ts
Normal file
2
components/ui/hero/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './hero';
|
||||
|
2
components/ui/link/index.ts
Normal file
2
components/ui/link/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './link';
|
||||
|
19
components/ui/link/link.tsx
Normal file
19
components/ui/link/link.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from 'lib/utils'
|
||||
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
|
||||
|
||||
const Link: React.FC<
|
||||
NextLinkProps & {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
> = ({ href, children, className, ...props }) => {
|
||||
return (
|
||||
<NextLink className={cn('', className)} href={href} {...props}>
|
||||
{children}
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
|
||||
export default Link
|
@@ -1,142 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { cn } from '@lib/utils'
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@components/ui/Dropdown/Dropdown'
|
||||
|
||||
interface LOCALE_DATA {
|
||||
name: string
|
||||
img: {
|
||||
filename: string
|
||||
alt: string
|
||||
}
|
||||
}
|
||||
|
||||
const LOCALES_MAP: Record<string, LOCALE_DATA> = {
|
||||
sv: {
|
||||
name: 'Swedish',
|
||||
img: {
|
||||
filename: 'flag-sv.svg',
|
||||
alt: 'Swedish flag',
|
||||
},
|
||||
},
|
||||
nn: {
|
||||
name: 'Norwegian',
|
||||
img: {
|
||||
filename: 'flag-no.svg',
|
||||
alt: 'Norwegian flag',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
name: 'English',
|
||||
img: {
|
||||
filename: 'flag-en.svg',
|
||||
alt: 'British flag',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
interface I18nWidgetProps {
|
||||
translations: [] | any
|
||||
}
|
||||
|
||||
const I18nWidget = ({ translations }: I18nWidgetProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { locale, locales, defaultLocale = 'sv' } = useRouter()
|
||||
const router = useRouter()
|
||||
|
||||
const options: any = locales?.filter((val) => val !== locale)
|
||||
const currentLocale = locale || defaultLocale
|
||||
|
||||
const handleClick = (e: any, locale: string) => {
|
||||
e.preventDefault()
|
||||
|
||||
const parent = e.target
|
||||
|
||||
if (parent.nodeName !== 'LI') {
|
||||
return
|
||||
}
|
||||
|
||||
let href = '/'
|
||||
|
||||
const hasChildLink = parent.querySelector('a').href !== null
|
||||
|
||||
if (hasChildLink) {
|
||||
href = parent.querySelector('a').href
|
||||
}
|
||||
|
||||
router.push({ pathname: href }, { pathname: href }, { locale: locale })
|
||||
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={
|
||||
'w-10 h-10 duration-200 bg-app shrink-0 flex items-center justify-center transition hover:scale-105'
|
||||
}
|
||||
aria-label="Language selector"
|
||||
>
|
||||
<Image
|
||||
width="13"
|
||||
height="18"
|
||||
className="flex w-5 h-auto pointer-events-none rounded-[1px]"
|
||||
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
||||
alt={LOCALES_MAP[currentLocale].img.alt}
|
||||
unoptimized
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="drop-shadow-xl">
|
||||
<ul className="">
|
||||
{options.map((locale: any) => {
|
||||
const translationLink = translations?.find(
|
||||
(item: object | any) => item.locale === locale
|
||||
)
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={locale}
|
||||
asChild
|
||||
onClick={(e) => handleClick(e, locale)}
|
||||
>
|
||||
<li>
|
||||
<Link
|
||||
href={translationLink ? translationLink.slug.current : '/'}
|
||||
locale={locale}
|
||||
className={cn(
|
||||
'flex items-center w-full cursor-pointer px-1 py-1 text-center transition ease-in-out duration-150 text-high-contrast capitalize'
|
||||
)}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span>
|
||||
<Image
|
||||
width="13"
|
||||
height="18"
|
||||
className="mr-2 w-5 h-auto pointer-events-none rounded-[1px]"
|
||||
src={`/${LOCALES_MAP[locale].img.filename}`}
|
||||
alt={LOCALES_MAP[locale].img.alt}
|
||||
/>
|
||||
</span>
|
||||
<span>{LOCALES_MAP[locale].name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default I18nWidget
|
@@ -1,22 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import FlagEn from 'components/icons/flag-en';
|
||||
import FlagSv from 'components/icons/flag-sv';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'components/ui/dropdown/dropdown'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { i18n } from '../../../i18n-config'
|
||||
} from 'components/ui/dropdown/dropdown';
|
||||
import { useLocale } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { i18n } from '../../../i18n-config';
|
||||
|
||||
interface LocaleSwitcherProps {
|
||||
currentLocale: string
|
||||
}
|
||||
|
||||
export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
|
||||
export default function LocaleSwitcher() {
|
||||
const pathName = usePathname()
|
||||
const locale = useLocale();
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const router = useRouter();
|
||||
|
||||
const redirectedPathName = (locale: string) => {
|
||||
if (!pathName) return '/'
|
||||
@@ -24,8 +26,28 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
|
||||
segments[1] = locale
|
||||
return segments.join('/')
|
||||
}
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const handleClick = (e: any, locale: string) => {
|
||||
e.preventDefault()
|
||||
|
||||
const parent = e.target
|
||||
|
||||
if (parent.nodeName !== 'LI') {
|
||||
return
|
||||
}
|
||||
|
||||
let href = '/'
|
||||
|
||||
const hasChildLink = parent.querySelector('a').href !== null
|
||||
|
||||
if (hasChildLink) {
|
||||
href = parent.querySelector('a').href
|
||||
}
|
||||
|
||||
router.push(`${redirectedPathName(locale)}`)
|
||||
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -34,23 +56,42 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={
|
||||
'duration-200 bg-app shrink-0 flex items-center justify-center transition hover:scale-105'
|
||||
'duration-200 bg-app shrink-0 uppercase space-x-2 text-sm flex items-center justify-center transition hover:scale-105'
|
||||
}
|
||||
aria-label="Language selector"
|
||||
>
|
||||
Locale: {currentLocale}
|
||||
{locale === "sv" && (
|
||||
<FlagSv />
|
||||
)}
|
||||
{locale === "en" && (
|
||||
<FlagEn />
|
||||
)}
|
||||
<span>{locale}</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="drop-shadow-xl">
|
||||
<ul className="">
|
||||
{i18n.locales.map((locale) => {
|
||||
let FlagIcon: any
|
||||
|
||||
FlagIcon = i18n.flags[locale]
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className='p-0'
|
||||
key={locale}
|
||||
asChild
|
||||
onClick={(e) => handleClick(e, locale)}
|
||||
>
|
||||
<li key={locale}>
|
||||
<Link href={redirectedPathName(locale)}>{locale}</Link>
|
||||
<li className="flex" key={locale}>
|
||||
<Link
|
||||
className="flex w-full cursor-pointer uppercase space-x-2 text-sm p-2"
|
||||
onClick={() => setIsOpen(false)}
|
||||
href={redirectedPathName(locale)}
|
||||
>
|
||||
<FlagIcon />
|
||||
<span>{locale}</span>
|
||||
</Link>
|
||||
</li>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
|
2
components/ui/product-card/index.ts
Normal file
2
components/ui/product-card/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './product-card';
|
||||
|
80
components/ui/product-card/product-card.tsx
Normal file
80
components/ui/product-card/product-card.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from 'lib/utils'
|
||||
import { FC } from 'react'
|
||||
// import type { Product } from '@commerce/types/product'
|
||||
import dynamic from 'next/dynamic'
|
||||
// import usePrice from '@framework/product/use-price'
|
||||
|
||||
// const WishlistButton = dynamic(
|
||||
// () => import('@components/wishlist/WishlistButton')
|
||||
// )
|
||||
const ProductTag = dynamic(() => import('components/ui/product-tag'))
|
||||
const SanityImage = dynamic(() => import('components/ui/sanity-image'))
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
// product: Product
|
||||
variant?: 'default'
|
||||
}
|
||||
|
||||
const ProductCard: FC<Props> = ({
|
||||
// product,
|
||||
className,
|
||||
variant = 'default',
|
||||
}) => {
|
||||
// const { price } = usePrice({
|
||||
// amount: product.price.value,
|
||||
// baseAmount: product.price.retailPrice,
|
||||
// currencyCode: product.price.currencyCode!,
|
||||
// })
|
||||
|
||||
const rootClassName = cn(
|
||||
'w-full min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear basis-[50%]',
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<>Produyct</>
|
||||
// <Link
|
||||
// href={`${product.slug}`}
|
||||
// className={rootClassName}
|
||||
// aria-label={product.name}
|
||||
// locale={product.locale}
|
||||
// >
|
||||
// {variant === 'default' && (
|
||||
// <>
|
||||
// <div className={'flex flex-col flex-1 justify-center w-full h-full'}>
|
||||
// {/* {process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
// <WishlistButton
|
||||
// className={'top-4 right-4 z-10 absolute'}
|
||||
// productId={product.id}
|
||||
// variant={
|
||||
// product?.variants ? (product.variants[0] as any) : null
|
||||
// }
|
||||
// />
|
||||
// )} */}
|
||||
// {/* <div className="w-full h-full aspect-square overflow-hidden relative">
|
||||
// {product?.images && (
|
||||
// <SanityImage
|
||||
// image={product?.images[0]}
|
||||
// alt={product.name || 'Product Image'}
|
||||
// width={400}
|
||||
// height={400}
|
||||
// sizes="(max-width: 1024px) 50vw, 20vw"
|
||||
// />
|
||||
// )}
|
||||
// </div> */}
|
||||
// <ProductTag
|
||||
// className="mt-2 lg:mt-3"
|
||||
// name={product.title}
|
||||
// price={`${price}`}
|
||||
// />
|
||||
// </div>
|
||||
// </>
|
||||
// )}
|
||||
// </Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductCard
|
2
components/ui/product-tag/index.ts
Normal file
2
components/ui/product-tag/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './product-tag';
|
||||
|
48
components/ui/product-tag/product-tag.tsx
Normal file
48
components/ui/product-tag/product-tag.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from 'lib/utils'
|
||||
import dynamic from 'next/dynamic'
|
||||
const Text = dynamic(() => import('components/ui/text'))
|
||||
|
||||
interface ProductTagProps {
|
||||
className?: string
|
||||
name: string
|
||||
price: string
|
||||
variant?: 'productView' | 'cardView'
|
||||
}
|
||||
|
||||
const ProductTag: React.FC<ProductTagProps> = ({
|
||||
name,
|
||||
price,
|
||||
className = '',
|
||||
variant = 'cardView',
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn('text-high-contrast flex items-start flex-col', className)}
|
||||
>
|
||||
<Text
|
||||
className={
|
||||
variant === 'cardView'
|
||||
? ''
|
||||
: '!text-[32px] !leading-[32px] !font-normal'
|
||||
}
|
||||
variant={variant === 'cardView' ? 'listChildHeading' : 'pageHeading'}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
className={
|
||||
variant === 'cardView'
|
||||
? '!text-sm !font-semibold !leading-tight lg:!text-base'
|
||||
: '!font-bold !text-[32px] !leading-[32px]'
|
||||
}
|
||||
variant="paragraph"
|
||||
>
|
||||
{price}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductTag
|
2
components/ui/sanity-image/index.tsx
Normal file
2
components/ui/sanity-image/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './sanity-image';
|
||||
|
67
components/ui/sanity-image/sanity-image.tsx
Normal file
67
components/ui/sanity-image/sanity-image.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const placeholderImg = '/product-img-placeholder.svg'
|
||||
|
||||
export default function SanityImage(props: SanityImageProps) {
|
||||
const {
|
||||
image: source,
|
||||
priority = false,
|
||||
quality = 75,
|
||||
alt = '',
|
||||
height = 1080,
|
||||
width = 1080,
|
||||
sizes = '100vw',
|
||||
className,
|
||||
} = props
|
||||
|
||||
const rootClassName = cn('w-full h-auto', className)
|
||||
|
||||
const image = source?.asset?._rev ? (
|
||||
<>
|
||||
<Image
|
||||
className={`${rootClassName}`}
|
||||
placeholder="blur"
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt}
|
||||
src={urlForImage(source)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.quality(quality)
|
||||
.url()}
|
||||
sizes={sizes}
|
||||
priority={priority}
|
||||
blurDataURL={source.asset.metadata.lqip}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Image
|
||||
className={`${rootClassName}`}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt}
|
||||
src={placeholderImg}
|
||||
sizes={sizes}
|
||||
priority={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return image
|
||||
}
|
1
components/ui/slider/index.ts
Normal file
1
components/ui/slider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Slider'
|
78
components/ui/slider/slider.tsx
Normal file
78
components/ui/slider/slider.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
CarouselItemProps as ItemProps,
|
||||
CarouselProps as Props,
|
||||
} from 'components/ui/carousel/carousel'
|
||||
import Text from 'components/ui/text'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useEffect, useState } from 'react'
|
||||
const Carousel = dynamic<Props>(() =>
|
||||
import('components/ui/carousel/carousel').then((mod) => mod.Carousel)
|
||||
)
|
||||
const CarouselItem = dynamic<ItemProps>(() =>
|
||||
import('components/ui/carousel/carousel').then((mod) => mod.CarouselItem)
|
||||
)
|
||||
const ProductCard = dynamic(() => import('components/ui/product-card'))
|
||||
const CategoryCard = dynamic(() => import('components/ui/category-card'))
|
||||
|
||||
interface SliderProps {
|
||||
products: [] | any
|
||||
title: string
|
||||
categories: [] | any
|
||||
sliderType: String
|
||||
}
|
||||
|
||||
const Slider = ({ products, categories, title, sliderType }: SliderProps) => {
|
||||
const [items, setItems] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (sliderType === 'products') setItems(products)
|
||||
else if (sliderType === 'categories') setItems(categories)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{title ? (
|
||||
<Text
|
||||
className="mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||
variant="sectionHeading"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
className="italic mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||
variant="sectionHeading"
|
||||
>
|
||||
No title provided yet
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{items && (
|
||||
<Carousel
|
||||
gliderClasses={'px-4 lg:px-8 2xl:px-16'}
|
||||
gliderItemWrapperClasses={'space-x-2 lg:space-x-4'}
|
||||
hasDots={true}
|
||||
slidesToShow={2.2}
|
||||
responsive={{
|
||||
breakpoint: 1024,
|
||||
settings: {
|
||||
slidesToShow: 4.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{items.map((item: any, index: number) => (
|
||||
<CarouselItem key={`${sliderType}-${index}`}>
|
||||
{item.title}
|
||||
{/* {sliderType === 'products' && <ProductCard product={item} />}
|
||||
{sliderType === 'categories' && <CategoryCard category={item} />} */}
|
||||
</CarouselItem>
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Slider
|
2
components/ui/text/index.ts
Normal file
2
components/ui/text/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './text';
|
||||
|
88
components/ui/text/text.tsx
Normal file
88
components/ui/text/text.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from 'lib/utils'
|
||||
import React, {
|
||||
CSSProperties,
|
||||
FunctionComponent,
|
||||
JSXElementConstructor,
|
||||
} from 'react'
|
||||
|
||||
interface TextProps {
|
||||
variant?: Variant
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
children?: React.ReactNode | any
|
||||
html?: string
|
||||
onClick?: () => any
|
||||
}
|
||||
|
||||
type Variant =
|
||||
| 'heading'
|
||||
| 'body'
|
||||
| 'pageHeading'
|
||||
| 'sectionHeading'
|
||||
| 'label'
|
||||
| 'paragraph'
|
||||
| 'listChildHeading'
|
||||
|
||||
const Text: FunctionComponent<TextProps> = ({
|
||||
style,
|
||||
className = '',
|
||||
variant = 'body',
|
||||
children,
|
||||
html,
|
||||
onClick,
|
||||
}) => {
|
||||
const componentsMap: {
|
||||
[P in Variant]: React.ComponentType<any> | string
|
||||
} = {
|
||||
body: 'div',
|
||||
heading: 'h1',
|
||||
pageHeading: 'h1',
|
||||
sectionHeading: 'h2',
|
||||
listChildHeading: 'h3',
|
||||
label: 'div',
|
||||
paragraph: 'p',
|
||||
}
|
||||
|
||||
const Component:
|
||||
| JSXElementConstructor<any>
|
||||
| React.ReactElement<any>
|
||||
| React.ComponentType<any>
|
||||
| string = componentsMap![variant!]
|
||||
|
||||
const htmlContentProps = html
|
||||
? {
|
||||
dangerouslySetInnerHTML: { __html: html },
|
||||
}
|
||||
: {}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
'',
|
||||
{
|
||||
['text-base max-w-prose']: variant === 'body',
|
||||
['max-w-prose text-4xl font-display font-bold leading-none md:text-5xl md:leading-none lg:leading-none lg:text-6xl']:
|
||||
variant === 'heading',
|
||||
['max-w-prose text-3xl font-display font-bold leading-none md:text-4xl md:leading-none lg:leading-none lg:text-5xl']:
|
||||
variant === 'pageHeading',
|
||||
['max-w-prose text-2xl font-display font-bold leading-none md:text-3xl md:leading-none lg:leading-none lg:text-4xl']:
|
||||
variant === 'sectionHeading',
|
||||
['text-sm font-semibold leading-tight lg:text-base']:
|
||||
variant === 'listChildHeading',
|
||||
['text-sm max-w-prose lg:text-base 2xl:text-lg']: variant === 'label',
|
||||
['max-w-prose lg:text-lg 2xl:text-xl']: variant === 'paragraph',
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
{...htmlContentProps}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
export default Text
|
Reference in New Issue
Block a user