Iterated with translations

This commit is contained in:
Henrik Larsson 2023-05-03 15:16:42 +02:00
parent a1ae2357db
commit 603bd2b880
41 changed files with 1373 additions and 214 deletions

View File

@ -0,0 +1,10 @@
import DynamicContentManager from 'components/ui/dynamic-content-manager'
// This is a Client Component. It receives data as props and
// has access to state and effects just like Page components
// in the `pages` directory.
export default function HomePage({ data }: { data: object | any }) {
return (
<DynamicContentManager content={data?.content} />
)
}

View File

@ -1,22 +1,67 @@
'use client'; // 'use client';
import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher'; import getQueryFromSlug from 'helpers/getQueryFromSlug';
import { useTranslations } from 'next-intl'; import { docQuery } from 'lib/sanity/queries';
import { client } from 'lib/sanity/sanity.client';
import { groq } from 'next-sanity';
import HomePage from './home-page';
import SinglePage from './single-page';
interface PageProps { export async function generateStaticParams() {
params: { const paths = await client.fetch(groq`${docQuery}`, {
next: { revalidate: 10 },
})
// console.log(paths)
return paths.map((path: {
slug: string,
locale: string locale: string
} }) => ({
slug: path.slug.split('/').filter((p) => p),
locale: path.locale
}))
} }
export default function Index({params: {locale}} : PageProps) {
const t = useTranslations('Index'); /**
* Helper function to return the correct version of the document
* If we're in "preview mode" and have multiple documents, return the draft
*/
function filterDataToSingleItem(data: any, preview = false) {
if (!Array.isArray(data)) {
return data
}
if (data.length === 1) {
return data[0]
}
if (preview) {
return data.find((item) => item._id.startsWith(`drafts.`)) || data[0]
}
return data[0]
}
export default async function Page({
params,
}: {
params: { slug: string[], locale: string };
}) {
const { slug, locale } = params;
const { query, queryParams, docType } = getQueryFromSlug(slug, locale)
const pageData = await client.fetch(query, queryParams)
const data = filterDataToSingleItem(pageData, false)
return ( return (
<div> <div>
<h1>{t('title')}</h1> <>
<LocaleSwitcher currentLocale={locale} /> {docType === 'home' && <HomePage data={data} />}
{docType === 'page' && <SinglePage data={data} />}
</>
</div> </div>
) )
} }

View File

@ -0,0 +1,19 @@
'use client'
import dynamic from 'next/dynamic'
const DynamicContentManager = dynamic(
() => import('components/ui/dynamic-content-manager')
)
interface SinglePageProps {
data: any
}
const SinglePage = ({ data }: SinglePageProps) => {
return (
<DynamicContentManager content={data?.content} />
)
}
export default SinglePage

View File

@ -7,3 +7,228 @@
clip-path: inset(0.6px); clip-path: inset(0.6px);
} }
} }
/* BASE */
*,
*:before,
*:after {
box-sizing: inherit;
}
::-moz-selection {
/* Code for Firefox */
color: #ffffff;
background: #333333;
}
::selection {
color: #ffffff;
background: #333333;
}
html,
body {
@apply font-sans h-full bg-white text-high-contrast;
box-sizing: border-box;
touch-action: manipulation;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overscroll-behavior-x: none;
}
/* COMPONENTS */
.glider {
scrollbar-width: none;
-ms-overflow-style: none;
}
.glider::-webkit-scrollbar {
display: none;
}
.glider-dots {
@apply flex !space-x-[2px] !mt-8;
}
.glider-dot {
@apply !m-0 !rounded-none !w-12 !h-4 !bg-transparent after:content-[''] after:block after:w-12 after:h-[3px] after:bg-ui-border 2xl:!w-16 2xl:after:w-16;
}
.glider-dot.active {
@apply after:!bg-high-contrast;
}
/* Glider slider. */
.glider-prev {
@apply text-high-contrast !right-12 !-top-10 !left-auto lg:!right-16 lg:!-top-12 2xl:!-top-16 2xl:!right-[100px] !transition-transform !duration-100 hover:!text-high-contrast hover:scale-110;
}
.glider-next {
@apply text-high-contrast !right-4 !-top-10 lg:!right-8 lg:!-top-12 2xl:!-top-16 2xl:!right-16 !transition-transform !duration-100 hover:!text-high-contrast hover:scale-110;
}
.pdp .glider-prev {
@apply text-high-contrast absolute !left-4 !top-1/2 !transition-transform !duration-100 hover:!text-high-contrast hover:scale-100 lg:hidden;
}
.pdp .glider-next {
@apply text-high-contrast absolute !right-4 !top-1/2 !transition-transform !duration-100 hover:!text-high-contrast hover:scale-100 lg:hidden;
}
/* Dynamic content */
.dynamic-content > :not(.hero) {
@apply my-16 lg:my-24;
}
.dynamic-content > :first-child {
@apply mt-0 md:mt-0 lg:mt-0;
}
.dynamic-content > :last-child {
@apply mb-16 lg:mb-24;
}
.dynamic-content .dynamic-content {
@apply px-0 md:px-0;
}
.dynamic-content .dynamic-content > {
@apply my-0 md:my-0 lg:my-0;
}
.dynamic-content .dynamic-content > :last-child {
@apply my-0 md:my-0 lg:my-0;
}
/* ALGOLIA SEARCH */
.ais-SearchBox-form {
@apply w-full relative;
}
.ais-SearchBox-input {
@apply h-[44px] pl-10 bg-ui w-full;
}
.ais-SearchBox-input::placeholder {
@apply text-high-contrast text-opacity-50;
}
.ais-SearchBox-submit {
@apply absolute left-0 flex items-center justify-center w-12 h-12 top-1/2 transform -translate-y-1/2;
}
.ais-SearchBox-submit svg {
@apply w-4 h-4;
}
.ais-SearchBox-reset:not([hidden]) {
@apply absolute right-[3px] bg-ui flex items-center justify-center w-12 h-8 top-1/2 transform -translate-y-1/2;
}
.ais-SearchBox-reset svg {
@apply w-3 h-3;
}
.ais-RefinementList-item {
@apply mt-1 first:mt-0;
}
.ais-RefinementList-label {
@apply flex items-center cursor-pointer text-sm;
}
.ais-RefinementList-checkbox {
@apply w-4 h-4;
}
.ais-RefinementList-labelText {
@apply ml-2;
}
.ais-RefinementList-count {
@apply ml-2 bg-ui h-5 w-5 flex items-center justify-center text-xs rounded-full;
}
.ais-CurrentRefinements-label {
@apply uppercase;
}
.ais-Hits-list {
@apply grid grid-cols-2 gap-4 lg:grid-cols-4 items-start;
}
.ais-Pagination-list {
@apply flex justify-center -space-x-px mt-8;
}
.ais-Pagination-link {
@apply flex h-12 w-12 items-center justify-center bg-white border border-ui-border;
}
.ais-Pagination-link--selected {
@apply bg-ui font-bold;
}
.ais-ClearRefinements-button {
@apply inline-flex py-3 cursor-pointer px-6 border border-ui-border;
}
.ais-ClearRefinements-button--disabled {
@apply opacity-50 cursor-not-allowed;
}
/* ALGOLIA AUTOCOMPLETE */
.aa-DetachedContainer {
@apply !shadow-none;
}
.aa-DetachedContainer--modal {
@apply lg:!top-4 !rounded-none;
}
.aa-DetachedFormContainer {
@apply !border-none !px-4;
}
.aa-DetachedSearchButton {
@apply !w-10 !h-10 !p-0 cursor-pointer items-center justify-center !border-none;
}
.aa-DetachedSearchButtonIcon,
.aa-SubmitIcon {
@apply pointer-events-none;
}
.aa-DetachedSearchButtonPlaceholder {
@apply sr-only;
}
.aa-Form {
@apply !rounded-none !bg-white !border-ui-border;
}
.aa-SubmitButton {
@apply !flex !items-center !justify-center !p-0 !h-10 !w-10 lg:!w-10;
}
.aa-SubmitIcon {
@apply !text-high-contrast;
}
.aa-Panel {
@apply !rounded-none border border-ui-border !shadow-none !p-0;
}
.aa-PanelLayout {
@apply !p-0;
}
.aa-Item {
@apply !px-4 !py-2 lg:!px-4;
}
.aa-Item[aria-selected='true'] {
@apply !bg-ui;
}

View File

@ -0,0 +1,18 @@
export default function FlagEn ({ className = "w-6 h-auto" }: { className?: string }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30.6 20.4"
>
<path className="fill-[#293476]" d="M.1.1h30.5v20.3H.1z"/>
<path className="fill-[#fff]" d="M30.5 7.2H18.3V.1h-6v7.1H.1v6.1h12.2v7.1h6v-7.1h12.2z"/>
<path className="fill-[#fff]" d="M30.5 18L3.6.1H.1v2.3l26.9 18h3.5z"/>
<path className="fill-[#fff]" d="M.1 18L27 .1h3.5v2.3l-26.9 18H.1z"/>
<path className="fill-[#ce202d]" d="M30.5 8.4H17.1V.1h-3.6v8.3H.1V12h13.4v8.4h3.6V12h13.4z"/>
<path className="fill-[#ce202d]" d="M30.5 20.3v-1.6l-8.2-5.4h-2.4zm0-20.2h-2.4l-9.8 6.5v.6h1.6L30.5.1zM10.7 13.3l-10.6 7v.1h2.3l9.9-6.6v-.5zM.1.1v1.6l8.2 5.5h2.4z"/>
<path className="fill-none" d="M0 20.4V0h30.6v20.5H0zm30.5 0zM.1 20.3h30.3V.1H.1v20.2z"/>
</svg>
)
}

View File

@ -0,0 +1,25 @@
export default function FlagSv ({ className = "w-6 h-auto" }: { className?: string }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 480"
>
<defs>
<clipPath id="a">
<path fillOpacity=".67" d="M-53.421 0h682.67v512h-682.67z"/>
</clipPath>
</defs>
<g clipPath="url(#a)" transform="translate(50.082) scale(.9375)">
<g fillRule="evenodd" strokeWidth="1pt">
<path fill="#006aa7" d="M-121.103.302h256V205.1h-256zm0 306.876h256v204.8h-256z"/>
<path fill="#fecc00" d="M-121.103 204.984h256v102.4h-256z"/>
<path fill="#fecc00" d="M133.843.01h102.4v511.997h-102.4z"/>
<path fill="#fecc00" d="M232.995 205.013h460.798v102.4H232.995z"/>
<path fill="#006aa7" d="M236.155 307.208h460.797v204.799H236.155zm0-306.906h460.797V205.1H236.155z"/>
</g>
</g>
</svg>
)
}

View File

@ -1,5 +1,6 @@
'use client' 'use client'
import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher'
import Logo from 'components/ui/logo/logo' import Logo from 'components/ui/logo/logo'
import { import {
NavigationMenu, NavigationMenu,
@ -64,6 +65,9 @@ const Header: FC<HeaderProps> = () => {
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>
<div>
<LocaleSwitcher />
</div>
</div> </div>
</div> </div>
</HeaderRoot> </HeaderRoot>

View 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

View File

@ -0,0 +1,2 @@
export { default } from './blurb-section';

103
components/ui/card/card.tsx Normal file
View 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

View File

@ -0,0 +1 @@
export { default } from './Card'

View File

@ -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>
);
}

View 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>
)
}

View 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

View File

@ -0,0 +1,2 @@
export { default } from './category-card';

View File

@ -1,3 +1,5 @@
'use client'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons' import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons'
import * as React from 'react' import * as React from 'react'

View File

@ -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

View File

@ -0,0 +1,2 @@
export { default } from './dynamic-content-manager';

View File

@ -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

View File

@ -0,0 +1,2 @@
export { default } from './filtered-product-list';

View 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

View File

@ -0,0 +1,2 @@
export { default } from './hero';

View File

@ -0,0 +1,2 @@
export { default } from './link';

View 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

View File

@ -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

View File

@ -1,22 +1,24 @@
'use client' 'use client'
import FlagEn from 'components/icons/flag-en';
import FlagSv from 'components/icons/flag-sv';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from 'components/ui/dropdown/dropdown' } from 'components/ui/dropdown/dropdown';
import Link from 'next/link' import { useLocale } from 'next-intl';
import { usePathname } from 'next/navigation' import Link from 'next/link';
import { useState } from 'react' import { usePathname, useRouter } from 'next/navigation';
import { i18n } from '../../../i18n-config' import { useState } from 'react';
import { i18n } from '../../../i18n-config';
interface LocaleSwitcherProps { export default function LocaleSwitcher() {
currentLocale: string
}
export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
const pathName = usePathname() const pathName = usePathname()
const locale = useLocale();
const [isOpen, setIsOpen] = useState(false)
const router = useRouter();
const redirectedPathName = (locale: string) => { const redirectedPathName = (locale: string) => {
if (!pathName) return '/' if (!pathName) return '/'
@ -24,8 +26,28 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
segments[1] = locale segments[1] = locale
return segments.join('/') 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 ( return (
<div> <div>
@ -34,23 +56,42 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
className={ 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" aria-label="Language selector"
> >
Locale: {currentLocale} {locale === "sv" && (
<FlagSv />
)}
{locale === "en" && (
<FlagEn />
)}
<span>{locale}</span>
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="drop-shadow-xl"> <DropdownMenuContent align="end" className="drop-shadow-xl">
<ul className=""> <ul className="">
{i18n.locales.map((locale) => { {i18n.locales.map((locale) => {
let FlagIcon: any
FlagIcon = i18n.flags[locale]
return ( return (
<DropdownMenuItem <DropdownMenuItem
className='p-0'
key={locale} key={locale}
asChild asChild
onClick={(e) => handleClick(e, locale)}
> >
<li key={locale}> <li className="flex" key={locale}>
<Link href={redirectedPathName(locale)}>{locale}</Link> <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> </li>
</DropdownMenuItem> </DropdownMenuItem>
) )

View File

@ -0,0 +1,2 @@
export { default } from './product-card';

View 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

View File

@ -0,0 +1,2 @@
export { default } from './product-tag';

View 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

View File

@ -0,0 +1,2 @@
export { default } from './sanity-image';

View 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
}

View File

@ -0,0 +1 @@
export { default } from './Slider'

View 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

View File

@ -0,0 +1,2 @@
export { default } from './text';

View 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

View File

@ -4,7 +4,7 @@ import {
pageQuery, pageQuery,
} from '../lib/sanity/queries' } from '../lib/sanity/queries'
const getQueryFromSlug = (slugArray = []) => { const getQueryFromSlug = (slugArray: string[], locale: string) => {
const docQuery = { const docQuery = {
homePage: groq`${homePageQuery}`, homePage: groq`${homePageQuery}`,
page: groq`${pageQuery}`, page: groq`${pageQuery}`,
@ -12,10 +12,10 @@ const getQueryFromSlug = (slugArray = []) => {
let docType = '' let docType = ''
if (slugArray.length === 0) { if (!slugArray) {
return { return {
docType: 'home', docType: 'home',
queryParams: {}, queryParams: {locale: locale},
query: docQuery.homePage, query: docQuery.homePage,
} }
} }
@ -25,6 +25,7 @@ const getQueryFromSlug = (slugArray = []) => {
// We now have to re-combine the slug array to match our slug in Sanity. // We now have to re-combine the slug array to match our slug in Sanity.
let queryParams = { let queryParams = {
slug: `/${slugArray.join('/')}`, slug: `/${slugArray.join('/')}`,
locale: locale
} }
if (slugStart === 'articles' && slugArray.length === 2) { if (slugStart === 'articles' && slugArray.length === 2) {

View File

@ -1,6 +1,13 @@
import FlagEn from "components/icons/flag-en"
import FlagSv from "components/icons/flag-sv"
export const i18n = { export const i18n = {
defaultLocale: 'sv', defaultLocale: 'sv',
locales: ['sv', 'en', 'nn'], locales: ['sv', 'en'],
flags: {
sv: FlagSv,
en: FlagEn
}
} as const } as const
export type Locale = typeof i18n['locales'][number] export type Locale = typeof i18n['locales'][number]

View File

@ -1,7 +1,7 @@
import { sanityClient } from './sanity.client'
import createImageUrlBuilder from '@sanity/image-url' import createImageUrlBuilder from '@sanity/image-url'
import { client } from './sanity.client'
export const imageBuilder = createImageUrlBuilder(sanityClient) export const imageBuilder = createImageUrlBuilder(client)
export const urlForImage = (source: any) => export const urlForImage = (source: any) =>
imageBuilder.image(source).auto('format').fit('crop') imageBuilder.image(source).auto('format').fit('crop')

View File

@ -46,6 +46,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-glider": "^4.0.2",
"sanity": "3", "sanity": "3",
"styled-components": "^5.2", "styled-components": "^5.2",
"tailwind-merge": "^1.12.0", "tailwind-merge": "^1.12.0",

17
pnpm-lock.yaml generated
View File

@ -40,6 +40,7 @@ specifiers:
react: 18.2.0 react: 18.2.0
react-cookie: ^4.1.1 react-cookie: ^4.1.1
react-dom: 18.2.0 react-dom: 18.2.0
react-glider: ^4.0.2
sanity: '3' sanity: '3'
styled-components: ^5.2 styled-components: ^5.2
tailwind-merge: ^1.12.0 tailwind-merge: ^1.12.0
@ -71,6 +72,7 @@ dependencies:
react: 18.2.0 react: 18.2.0
react-cookie: 4.1.1_react@18.2.0 react-cookie: 4.1.1_react@18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-glider: 4.0.2_biqbaboplfbrettd7655fr4n2y
sanity: 3.9.1_inskn5v7aqlrr54h6fubgcms5y sanity: 3.9.1_inskn5v7aqlrr54h6fubgcms5y
styled-components: 5.3.10_7i5myeigehqah43i5u7wbekgba styled-components: 5.3.10_7i5myeigehqah43i5u7wbekgba
tailwind-merge: 1.12.0 tailwind-merge: 1.12.0
@ -4095,6 +4097,10 @@ packages:
- supports-color - supports-color
dev: false dev: false
/glider-js/1.7.8:
resolution: {integrity: sha512-env4+yfoNYaeHJmEBGygjcZDpZ5LRUjGhCB6Ag+XNQfvuLVD5PhCYQShBVCAd5On7XfknM57O+V2Fwfcsii3Dw==}
dev: false
/glob-parent/5.1.2: /glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -5966,6 +5972,17 @@ packages:
use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i
dev: false dev: false
/react-glider/4.0.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-nZnkV4x8mSzfhGgrTkMrlL/g07T6hR0bLHg0q1iRumuORwfX+kqxCYoRy4/WXTRhlmurhcFKTaH007Vz2HjeHA==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
glider-js: 1.7.8
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react-is/16.13.1: /react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}