mirror of
https://github.com/vercel/commerce.git
synced 2025-05-19 16:07:01 +00:00
Structural changes
This commit is contained in:
parent
1857278dda
commit
a75a2e7feb
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
44
app/[locale]/[slug]/page.tsx
Normal file
44
app/[locale]/[slug]/page.tsx
Normal 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} />;
|
||||||
|
}
|
41
app/[locale]/category/[slug]/page.tsx
Normal file
41
app/[locale]/category/[slug]/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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
27
app/[locale]/page.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
89
app/[locale]/product/[slug]/page.tsx
Normal file
89
app/[locale]/product/[slug]/page.tsx
Normal 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={[]} />;
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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">
|
||||||
© {copyrightDate} Kodamera - {t('copyright')}
|
© {copyrightDate} Kodamera - {t('copyright')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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}
|
||||||
|
@ -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]
|
@ -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,
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user