Structural changes

This commit is contained in:
Henrik Larsson 2023-08-07 15:06:13 +02:00
parent 1857278dda
commit a75a2e7feb
25 changed files with 359 additions and 180 deletions

View File

@ -41,7 +41,7 @@ export default async function Page({ params }: { params: { slug: string[]; local
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<Header localeData={localeData} /> <Header />
<main className="flex-1"> <main className="flex-1">
<article> <article>
{isEnabled ? ( {isEnabled ? (
@ -65,7 +65,7 @@ export default async function Page({ params }: { params: { slug: string[]; local
)} )}
</article> </article>
</main> </main>
<Footer /> <Footer localeData={localeData} />
</div> </div>
); );
} }

View File

@ -0,0 +1,44 @@
import DynamicContentManager from 'components/layout/dynamic-content-manager';
import { pageQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
export const runtime = 'edge';
export const revalidate = 43200; // 12 hours in seconds
export async function generateMetadata({
params
}: {
params: { locale: string; slug: string };
}): Promise<Metadata> {
const page = await clientFetch(pageQuery, params);
if (!page) return notFound();
return {
title: page.seo?.title || page.title,
description: page.seo?.description || page.bodySummary,
openGraph: {
publishedTime: page.createdAt,
modifiedTime: page.updatedAt,
type: 'article'
}
};
}
interface PageParams {
params: {
locale: string;
slug: string;
};
}
export default async function Page({ params }: PageParams) {
const page = await clientFetch(pageQuery, params);
if (!page) return notFound();
return <DynamicContentManager content={page?.content} />;
}

View File

@ -0,0 +1,41 @@
import Text from 'components/ui/text/text';
import { categoryQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
export async function generateMetadata({
params
}: {
params: { slug: string; locale: string };
}): Promise<Metadata> {
const category = await clientFetch(categoryQuery, params);
if (!category) return notFound();
return {
title: category.seo.title || category.title,
description: category.seo.description || category.description
};
}
interface CategoryPageParams {
params: {
locale: string;
slug: string;
};
}
export default async function ProductPage({ params }: CategoryPageParams) {
const category = await clientFetch(categoryQuery, params);
if (!category) return notFound();
const { title } = category;
return (
<div className="mb-8 flex w-full flex-col px-4 lg:my-16 lg:px-8 2xl:px-16">
<Text variant={'pageHeading'}>{title}</Text>
</div>
);
}

View File

@ -1,7 +1,10 @@
import Footer from 'components/layout/footer/footer';
import Header from 'components/layout/header/header';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { supportedLanguages } from '../../i18n-config';
import './globals.css'; import './globals.css';
const SITE_NAME = 'KM Storefront'; const SITE_NAME = 'KM Storefront';
@ -36,7 +39,7 @@ const inter = Inter({
}); });
export function generateStaticParams() { export function generateStaticParams() {
return [{ locale: 'sv' }, { locale: 'en' }]; return supportedLanguages.locales.map((locale) => ({ locale: locale.id }));
} }
interface LocaleLayoutProps { interface LocaleLayoutProps {
@ -59,7 +62,9 @@ export default async function LocaleLayout({ children, params: { locale } }: Loc
<html lang={locale} className={inter.variable}> <html lang={locale} className={inter.variable}>
<body className="flex min-h-screen flex-col"> <body className="flex min-h-screen flex-col">
<NextIntlClientProvider locale={locale} messages={messages}> <NextIntlClientProvider locale={locale} messages={messages}>
{children} <Header />
<main className="flex-1">{children}</main>
<Footer />
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
</html> </html>

27
app/[locale]/page.tsx Normal file
View File

@ -0,0 +1,27 @@
import DynamicContentManager from 'components/layout/dynamic-content-manager/dynamic-content-manager';
import { homePageQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client';
export const runtime = 'edge';
export const metadata = {
description: 'High-performance ecommerce store built with Next.js, Vercel, Sanity and Storm.',
openGraph: {
type: 'website'
}
};
interface HomePageParams {
params: {
locale: string;
};
}
export default async function HomePage({ params }: HomePageParams) {
const data = await clientFetch(homePageQuery, params);
return (
<>
<DynamicContentManager content={data?.content} />
</>
);
}

View File

@ -0,0 +1,89 @@
import ProductView from 'components/product/product-view';
import { productQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface ProductPageParams {
params: {
locale: string;
slug: string;
};
}
export async function generateMetadata({
params
}: {
params: { slug: string; locale: string };
}): Promise<Metadata> {
const product = await clientFetch(productQuery, params);
if (!product) return notFound();
const { alt } = product.images[0] || '';
const { url } = product.images[0].asset || {};
const { width, height } = product.images[0].asset.metadata.dimensions;
// const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
return {
title: product.seo.title || product.title,
description: product.seo.description || product.description,
// @TODO ROBOTS SETTINGS???
// robots: {
// index: indexable,
// follow: indexable,
// googleBot: {
// index: indexable,
// follow: indexable
// }
// },
openGraph: url
? {
images: [
{
url,
width,
height,
alt
}
]
}
: null
};
}
export default async function ProductPage({ params }: ProductPageParams) {
const product = await clientFetch(productQuery, params);
if (!product) return notFound();
const productJsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
// @TODO UPDATE TO STORM URL???
image: product.images[0].asset.url
// offers: {
// '@type': 'AggregateOffer',
// availability: product.availableForSale
// ? 'https://schema.org/InStock'
// : 'https://schema.org/OutOfStock',
// priceCurrency: product.priceRange.minVariantPrice.currencyCode,
// highPrice: product.priceRange.maxVariantPrice.amount,
// lowPrice: product.priceRange.minVariantPrice.amount
// }
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productJsonLd)
}}
/>
<ProductView product={product} relatedProducts={[]} />;
</>
);
}

View File

@ -1,28 +1,40 @@
'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 { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
// interface FooterProps {
// localeData: {
// type: string;
// locale: string;
// translations: [];
// };
// }
const Footer = () => { const Footer = () => {
// const locale = useLocale();
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : ''); const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
const t = useTranslations('ui'); const t = useTranslations('ui');
return ( return (
<footer className="border-t border-ui-border bg-app"> <footer className="border-t border-ui-border bg-app">
<div className="mx-auto w-full py-4 px-4 lg:py-6 lg:px-8 2xl:px-16 2xl:py-8"> <div className="mx-auto flex w-full flex-col">
<div className="flex flex-col w-full space-y-2 items-center transition-colors duration-150 md:flex-row md:items-baseline md:justify-between md:space-y-0"> <div className="flex w-full flex-col items-center space-y-2 p-4 transition-colors duration-150 md:flex-row md:items-baseline md:justify-between md:space-y-0 lg:px-8 lg:py-6 2xl:px-16 2xl:py-8">
<Link className="flex flex-initial items-center font-bold md:mr-24" href="/"> <Link className="flex flex-initial items-center font-bold md:mr-24" href="/">
<Logo /> <Logo />
</Link> </Link>
<p className="text-sm text-low-contrast"> {/* <LocaleSwitcher localeData={localeData} currentLocale={locale} /> */}
</div>
<div className="flex items-center justify-center border-t border-ui-border bg-black px-4 py-3 lg:px-8 2xl:px-16">
<p className="text-xs text-white">
&copy; {copyrightDate} Kodamera - {t('copyright')} &copy; {copyrightDate} Kodamera - {t('copyright')}
</p> </p>
</div> </div>
</div> </div>
</footer> </footer>
); );
} };
export default Footer; export default Footer;

View File

@ -1,6 +1,5 @@
'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,
@ -14,15 +13,9 @@ import Link from 'next/link';
import { FC } from 'react'; import { FC } from 'react';
import HeaderRoot from './header-root'; import HeaderRoot from './header-root';
interface HeaderProps { interface HeaderProps {}
localeData: {
type: string;
locale: string;
translations: [];
};
}
const Header: FC<HeaderProps> = ({ localeData }: HeaderProps) => { const Header: FC<HeaderProps> = () => {
const locale = useLocale(); const locale = useLocale();
return ( return (
@ -43,21 +36,21 @@ const Header: FC<HeaderProps> = ({ localeData }: HeaderProps) => {
<NavigationMenu delayDuration={0} className="hidden lg:block"> <NavigationMenu delayDuration={0} className="hidden lg:block">
<NavigationMenuList> <NavigationMenuList>
<NavigationMenuItem> <NavigationMenuItem>
<Link href={'/kategori/junior'} legacyBehavior passHref> <Link href={'/category/barn'} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}> <NavigationMenuLink className={navigationMenuTriggerStyle()}>
Junior Junior
</NavigationMenuLink> </NavigationMenuLink>
</Link> </Link>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<Link href={'/kategori/trojor'} legacyBehavior passHref> <Link href={'/category/trojor'} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}> <NavigationMenuLink className={navigationMenuTriggerStyle()}>
Tröjor Tröjor
</NavigationMenuLink> </NavigationMenuLink>
</Link> </Link>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<Link href={'/kategori/byxor'} legacyBehavior passHref> <Link href={'/category/byxor'} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}> <NavigationMenuLink className={navigationMenuTriggerStyle()}>
Byxor Byxor
</NavigationMenuLink> </NavigationMenuLink>
@ -66,9 +59,7 @@ const Header: FC<HeaderProps> = ({ localeData }: HeaderProps) => {
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>
<div> <div></div>
<LocaleSwitcher localeData={localeData} currentLocale={locale} />
</div>
</div> </div>
</div> </div>
</HeaderRoot> </HeaderRoot>

View File

@ -1,16 +1,16 @@
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic';
import { Carousel, CarouselItem } from 'components/modules/carousel/carousel' import { Carousel, CarouselItem } from 'components/modules/carousel/carousel';
const Card = dynamic(() => import('components/ui/card')) const Card = dynamic(() => import('components/ui/card'));
import Text from 'components/ui/text' import Text from 'components/ui/text';
interface BlurbSectionProps { interface BlurbSectionProps {
blurbs: any blurbs: any;
title: string title: string;
mobileLayout: string mobileLayout: string;
desktopLayout: string desktopLayout: string;
imageFormat: 'square' | 'portrait' | 'landscape' imageFormat: 'square' | 'portrait' | 'landscape';
} }
const BlurbSection = ({ const BlurbSection = ({
@ -18,37 +18,33 @@ const BlurbSection = ({
mobileLayout, mobileLayout,
desktopLayout, desktopLayout,
blurbs, blurbs,
imageFormat, imageFormat
}: BlurbSectionProps) => { }: BlurbSectionProps) => {
const gridLayout = const gridLayout =
desktopLayout === '2-column' desktopLayout === '2-column'
? 'lg:grid-cols-2' ? 'lg:grid-cols-2'
: desktopLayout === '3-column' : desktopLayout === '3-column'
? 'lg:grid-cols-3' ? 'lg:grid-cols-3'
: 'lg:grid-cols-4' : 'lg:grid-cols-4';
const sliderLayout = const sliderLayout = desktopLayout === '2-column' ? 2 : desktopLayout === '3-column' ? 3 : 4;
desktopLayout === '2-column' ? 2 : desktopLayout === '3-column' ? 3 : 4
return ( return (
<div> <div>
{title ? ( {title ? (
<Text <Text className="mb-4 px-4 lg:mb-6 lg:px-8 2xl:mb-8 2xl:px-16" variant="sectionHeading">
className="mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
variant="sectionHeading"
>
{title} {title}
</Text> </Text>
) : ( ) : (
<Text <Text
className="italic mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8" className="mb-4 px-4 italic lg:mb-6 lg:px-8 2xl:mb-8 2xl:px-16"
variant="sectionHeading" variant="sectionHeading"
> >
No title provided yet No title provided yet
</Text> </Text>
)} )}
<div <div
className={`px-4 grid ${gridLayout} gap-x-4 gap-y-8 ${ className={`grid px-4 ${gridLayout} gap-x-4 gap-y-8 ${
mobileLayout === 'stacked' ? 'lg:hidden' : 'hidden' mobileLayout === 'stacked' ? 'lg:hidden' : 'hidden'
} lg:px-8 2xl:!px-16`} } lg:px-8 2xl:!px-16`}
> >
@ -63,15 +59,11 @@ const BlurbSection = ({
imageFormat={blurb?.imageFormat} imageFormat={blurb?.imageFormat}
/> />
</div> </div>
) );
})} })}
</div> </div>
<div <div className={`${mobileLayout === 'stacked' ? 'hidden lg:block' : 'block'}`}>
className={`${
mobileLayout === 'stacked' ? 'hidden lg:block' : 'block'
}`}
>
{blurbs && ( {blurbs && (
<Carousel <Carousel
gliderClasses={'px-4 lg:px-8 2xl:px-16'} gliderClasses={'px-4 lg:px-8 2xl:px-16'}
@ -80,9 +72,8 @@ const BlurbSection = ({
responsive={{ responsive={{
breakpoint: 1024, breakpoint: 1024,
settings: { settings: {
slidesToShow: slidesToShow: sliderLayout <= 4 ? sliderLayout + 0.5 : sliderLayout
sliderLayout <= 4 ? sliderLayout + 0.5 : sliderLayout, }
},
}} }}
> >
{blurbs.map((blurb: any, index: number) => ( {blurbs.map((blurb: any, index: number) => (
@ -100,7 +91,7 @@ const BlurbSection = ({
)} )}
</div> </div>
</div> </div>
) );
} };
export default BlurbSection export default BlurbSection;

View File

@ -1,41 +1,40 @@
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic';
import Link from 'components/ui/link/link';
import Link from 'components/ui/link/link' import Text from 'components/ui/text/text';
import Text from 'components/ui/text/text' const SanityImage = dynamic(() => import('components/ui/sanity-image'));
const SanityImage = dynamic(() => import('components/ui/sanity-image'))
interface HeroProps { interface HeroProps {
variant: string variant: string;
text?: string text?: string;
label?: string label?: string;
title: string title: string;
image: object | any image: object | any;
desktopImage: object | any desktopImage: object | any;
link: { link: {
title: string title: string;
reference: { reference: {
title: string title: string;
slug: { slug: {
current: string current: string;
} };
} };
} };
} }
type HeroSize = keyof typeof heroSize type HeroSize = keyof typeof heroSize;
const heroSize = { const heroSize = {
fullScreen: 'aspect-[3/4] lg:aspect-auto lg:h-[calc(100vh-4rem)]', fullScreen: 'aspect-[3/4] lg:aspect-auto lg:h-[calc(75vh-4rem)]',
halfScreen: 'aspect-square max-h-[60vh] lg:aspect-auto lg:min-h-[60vh]', halfScreen: 'aspect-square max-h-[60vh] lg:aspect-auto lg:min-h-[60vh]'
} };
const Hero = ({ variant, title, text, label, image, link }: HeroProps) => { const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen;
return ( return (
<div <div
className={`relative w-screen ${heroClass} flex flex-col justify-end relative text-high-contrast`} className={`relative w-screen ${heroClass} relative flex flex-col justify-end text-high-contrast`}
> >
{image && ( {image && (
<SanityImage <SanityImage
@ -44,10 +43,10 @@ const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
priority={true} priority={true}
width={1200} width={1200}
height={600} height={600}
className="absolute inset-0 h-full w-full object-cover z-10" className="absolute inset-0 z-10 h-full w-full object-cover"
/> />
)} )}
<div className="flex flex-col items-start text-high-contrast absolute max-w-md z-50 left-4 bottom-5 lg:max-w-2xl lg:bottom-8 lg:left-8 2xl:left-16 2xl:bottom-16"> <div className="absolute bottom-5 left-4 z-50 flex max-w-md flex-col items-start text-high-contrast lg:bottom-8 lg:left-8 lg:max-w-2xl 2xl:bottom-16 2xl:left-16">
{label && ( {label && (
<Text className="mb-1 lg:mb-2" variant="label"> <Text className="mb-1 lg:mb-2" variant="label">
{label} {label}
@ -67,7 +66,7 @@ const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
)} )}
{link?.reference && ( {link?.reference && (
<Link <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" className="mt-6 inline-flex bg-high-contrast px-10 py-4 text-base text-white transition hover:bg-low-contrast lg:mt-8"
href={link.reference.slug.current} href={link.reference.slug.current}
> >
{link?.title ? link.title : link.reference.title} {link?.title ? link.title : link.reference.title}
@ -75,7 +74,7 @@ const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
)} )}
</div> </div>
</div> </div>
) );
} };
export default Hero export default Hero;

View File

@ -1,14 +1,14 @@
'use client'; 'use client';
import { Carousel, CarouselItem } from 'components/modules/carousel/carousel'; import { Carousel, CarouselItem } from 'components/modules/carousel/carousel';
import Price from 'components/product/price'; import SanityImage from 'components/ui/sanity-image/sanity-image';
import SanityImage from 'components/ui/sanity-image'; import Text from 'components/ui/text/text';
import { Product } from 'lib/storm/types/product'; import { Product } from 'lib/storm/types/product';
import { cn } from 'lib/utils'; import { cn } from 'lib/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Price from './price';
const ProductCard = dynamic(() => import('components/ui/product-card')); const ProductCard = dynamic(() => import('components/ui/product-card'));
const Text = dynamic(() => import('components/ui/text'));
interface ProductViewProps { interface ProductViewProps {
product: Product; product: Product;
relatedProducts: Product[]; relatedProducts: Product[];
@ -27,7 +27,7 @@ export default function ProductView({ product, relatedProducts }: ProductViewPro
<div className={`pdp aspect-square lg:hidden`}> <div className={`pdp aspect-square lg:hidden`}>
{images && ( {images && (
<Carousel <Carousel
hasArrows={true} hasArrows={images.length > 1 ? true : false}
hasDots={false} hasDots={false}
gliderClasses={'lg:px-8 2xl:px-16'} gliderClasses={'lg:px-8 2xl:px-16'}
slidesToScroll={1} slidesToScroll={1}
@ -69,11 +69,11 @@ export default function ProductView({ product, relatedProducts }: ProductViewPro
</div> </div>
</div> </div>
<div className="col-span-1 mx-auto flex h-auto w-full flex-col px-4 py-6 lg:sticky lg:top-8 lg:col-span-5 lg:px-8 lg:py-0 lg:pr-0 2xl:top-16 2xl:px-16 2xl:pr-0"> <div className="col-span-1 mx-auto flex h-auto w-full flex-col p-4 lg:sticky lg:top-8 lg:col-span-5 lg:px-8 lg:py-0 lg:pr-0 2xl:top-16 2xl:px-16 2xl:pr-0">
<Text variant={'productHeading'}>{product.name}</Text> <Text variant={'productHeading'}>{product.name}</Text>
<Price <Price
className="text-sm font-medium leading-tight lg:text-base" className="mt-2 text-sm font-medium leading-tight lg:text-base"
amount={`${product.price.value}`} amount={`${product.price.value}`}
currencyCode={product.price.currencyCode ? product.price.currencyCode : 'SEK'} currencyCode={product.price.currencyCode ? product.price.currencyCode : 'SEK'}
/> />

View File

@ -1,48 +1,49 @@
'use client' 'use client';
import SanityImage from 'components/ui/sanity-image' import SanityImage from 'components/ui/sanity-image';
import { cn } from 'lib/utils' import { cn } from 'lib/utils';
import Link from 'next/link' import Link from 'next/link';
import { FC } from 'react' import { FC } from 'react';
interface CardProps { interface CardProps {
className?: string className?: string;
title: string title: string;
image: object | any image: object | any;
link: object | any link: object | any;
text?: string text?: string;
imageFormat?: 'square' | 'portrait' | 'landscape' imageFormat?: 'square' | 'portrait' | 'landscape';
} }
const placeholderImg = '/product-img-placeholder.svg' const placeholderImg = '/product-img-placeholder.svg';
const Card: FC<CardProps> = ({ const Card: FC<CardProps> = ({ className, title, image, link, text, imageFormat = 'square' }) => {
className, const rootClassName = cn('relative', className);
title,
image,
link,
text,
imageFormat = 'square',
}) => {
const rootClassName = cn('relative', className)
const { linkType } = link const { linkType } = link;
const imageWrapperClasses = cn('w-full h-full overflow-hidden relative', { const imageWrapperClasses = cn('w-full h-full overflow-hidden relative', {
['aspect-square']: imageFormat === 'square', ['aspect-square']: imageFormat === 'square',
['aspect-[3/4]']: imageFormat === 'portrait', ['aspect-[3/4]']: imageFormat === 'portrait',
['aspect-[4/3]']: imageFormat === 'landscape', ['aspect-[4/3]']: imageFormat === 'landscape'
}) });
const imageClasses = cn('object-cover w-full h-full') const imageClasses = cn('object-cover w-full h-full');
function Card() { function Card() {
if (linkType === 'internal') { if (linkType === 'internal') {
const type = link.internalLink.reference._type;
let href = '';
if (type === 'product') {
href = `/product/${link.internalLink.reference.slug.current}`;
} else if (type === 'category') {
href = `/category/${link.internalLink.reference.slug.current}`;
} else {
return `${link.internalLink.reference.slug.current}`;
}
return ( return (
<Link <Link href={href} className={rootClassName} aria-label={title}>
href={link.internalLink.reference.slug.current}
className={rootClassName}
aria-label={title}
>
<div className="flex flex-col"> <div className="flex flex-col">
{image && ( {image && (
<div className={imageWrapperClasses}> <div className={imageWrapperClasses}>
@ -54,25 +55,17 @@ const Card: FC<CardProps> = ({
/> />
</div> </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"> <h3 className="mt-2 text-sm font-medium text-high-contrast underline underline-offset-2 lg:mt-3 lg:text-lg lg:underline-offset-4 2xl:text-xl">
{title} {title}
</h3> </h3>
{text && ( {text && <p className="mt-1 text-sm text-low-contrast lg:mt-2 lg:text-base">{text}</p>}
<p className="text-sm mt-1 text-low-contrast lg:text-base lg:mt-2">
{text}
</p>
)}
</div> </div>
</Link> </Link>
) );
} }
return ( return (
<a <a href={link.externalLink.url} className={rootClassName} aria-label={title}>
href={link.externalLink.url}
className={rootClassName}
aria-label={title}
>
<div className="flex flex-col"> <div className="flex flex-col">
{image && ( {image && (
<div className={imageWrapperClasses}> <div className={imageWrapperClasses}>
@ -84,20 +77,16 @@ const Card: FC<CardProps> = ({
/> />
</div> </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"> <h3 className="mt-2 text-sm font-medium text-high-contrast underline underline-offset-2 lg:mt-3 lg:text-lg lg:underline-offset-4 2xl:text-xl">
{title} {title}
</h3> </h3>
{text && ( {text && <p className="mt-1 text-sm text-low-contrast lg:mt-2 lg:text-base">{text}</p>}
<p className="text-sm mt-1 text-low-contrast lg:text-base lg:mt-2">
{text}
</p>
)}
</div> </div>
</a> </a>
) );
} }
return <Card /> return <Card />;
} };
export default Card export default Card;

View File

@ -7,7 +7,7 @@ import {
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { i18n } from '../../../i18n-config'; import { supportedLanguages } from '../../../i18n-config';
interface LocaleSwitcherProps { interface LocaleSwitcherProps {
currentLocale: string; currentLocale: string;
@ -58,7 +58,7 @@ export default function LocaleSwitcher({ currentLocale, localeData }: LocaleSwit
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="drop-shadow-xl"> <DropdownMenuContent align="end" className="drop-shadow-xl">
<ul className=""> <ul className="">
{i18n.locales.map((locale) => { {supportedLanguages.locales.map((locale) => {
if (currentLocale === locale.id) { if (currentLocale === locale.id) {
return; return;
} else { } else {

View File

@ -23,7 +23,7 @@ const ProductCard: FC<Props> = ({ product, className, variant = 'default' }) =>
return ( return (
<Link <Link
href={`${product.slug}`} href={`/product/${product.slug}`}
className={rootClassName} className={rootClassName}
aria-label={product.name} aria-label={product.name}
locale={product.locale} locale={product.locale}

View File

@ -1,4 +1,4 @@
export const i18n = { export const supportedLanguages = {
defaultLocale: 'sv', defaultLocale: 'sv',
locales: [ locales: [
{ {
@ -12,4 +12,4 @@ export const i18n = {
], ],
} as const } as const
export type Locale = typeof i18n['locales'][number] export type Locale = typeof supportedLanguages['locales'][number]

View File

@ -107,6 +107,7 @@ export const modules = `
desktopLayout, desktopLayout,
imageFormat, imageFormat,
blurbs[]->{ blurbs[]->{
_type,
title, title,
text, text,
link { link {
@ -209,29 +210,27 @@ export const productQuery = `*[_type == "product" && slug.current == $slug && la
"slug": slug.current, "slug": slug.current,
"locale": language "locale": language
}, },
"product": { id,
id, "name": title,
"name": title, description,
description, "descriptionHtml": "",
"descriptionHtml": "", images[] {
images[] { ${imageFields}
${imageFields}
},
price {
value,
currencyCode,
retailPrice
},
options[] {
id,
displayName,
values[] {
label,
"hexColors": hexColors.hex
}
},
"variants": []
}, },
price {
value,
currencyCode,
retailPrice
},
options[] {
id,
displayName,
values[] {
label,
"hexColors": hexColors.hex
}
},
"variants": [],
seo { seo {
${seoFields} ${seoFields}
} }
@ -243,15 +242,8 @@ export const categoryQuery = `*[_type == "category" && slug.current == $slug &&
title, title,
"slug": slug.current, "slug": slug.current,
"locale": language, "locale": language,
showBanner, image {
banner { ${imageFields}
_type,
_key,
title,
text,
image {
${imageFields}
}
}, },
"translations": *[_type == "translation.metadata" && references(^._id)].translations[].value->{ "translations": *[_type == "translation.metadata" && references(^._id)].translations[].value->{
title, title,

View File

@ -6,7 +6,6 @@ export default createMiddleware({
// If this locale is matched, pathnames work without a prefix (e.g. `/about`) // If this locale is matched, pathnames work without a prefix (e.g. `/about`)
defaultLocale: 'sv', defaultLocale: 'sv',
localeDetection: false
}); });
export const config = { export const config = {