diff --git a/app/[locale]/[[...slug]]/category-page-preview.tsx b/...[[...slug]]/category-page-preview.tsx similarity index 100% rename from app/[locale]/[[...slug]]/category-page-preview.tsx rename to ...[[...slug]]/category-page-preview.tsx diff --git a/app/[locale]/[[...slug]]/category-page.tsx b/...[[...slug]]/category-page.tsx similarity index 100% rename from app/[locale]/[[...slug]]/category-page.tsx rename to ...[[...slug]]/category-page.tsx diff --git a/app/[locale]/[[...slug]]/home-page-preview.tsx b/...[[...slug]]/home-page-preview.tsx similarity index 100% rename from app/[locale]/[[...slug]]/home-page-preview.tsx rename to ...[[...slug]]/home-page-preview.tsx diff --git a/app/[locale]/[[...slug]]/home-page.tsx b/...[[...slug]]/home-page.tsx similarity index 100% rename from app/[locale]/[[...slug]]/home-page.tsx rename to ...[[...slug]]/home-page.tsx diff --git a/app/[locale]/[[...slug]]/page.tsx b/...[[...slug]]/page.tsx similarity index 98% rename from app/[locale]/[[...slug]]/page.tsx rename to ...[[...slug]]/page.tsx index 0ab67b638..af5b76004 100644 --- a/app/[locale]/[[...slug]]/page.tsx +++ b/...[[...slug]]/page.tsx @@ -41,7 +41,7 @@ export default async function Page({ params }: { params: { slug: string[]; local return (
-
+
{isEnabled ? ( @@ -65,7 +65,7 @@ export default async function Page({ params }: { params: { slug: string[]; local )}
-
); } diff --git a/app/[locale]/[[...slug]]/product-page-preview.tsx b/...[[...slug]]/product-page-preview.tsx similarity index 100% rename from app/[locale]/[[...slug]]/product-page-preview.tsx rename to ...[[...slug]]/product-page-preview.tsx diff --git a/...[[...slug]]/product-page.tsx b/...[[...slug]]/product-page.tsx new file mode 100644 index 000000000..49d34df0b --- /dev/null +++ b/...[[...slug]]/product-page.tsx @@ -0,0 +1,19 @@ +import ProductView from 'components/product/product-view'; +import { notFound } from 'next/navigation'; + +interface ProductPageProps { + data: object | any; +} + +// 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 ProductPage({ data }: ProductPageProps) { + if (!data) { + return notFound(); + } + + const { product } = data; + + return ; +} diff --git a/app/[locale]/[[...slug]]/single-page-preview.tsx b/...[[...slug]]/single-page-preview.tsx similarity index 100% rename from app/[locale]/[[...slug]]/single-page-preview.tsx rename to ...[[...slug]]/single-page-preview.tsx diff --git a/app/[locale]/[[...slug]]/single-page.tsx b/...[[...slug]]/single-page.tsx similarity index 100% rename from app/[locale]/[[...slug]]/single-page.tsx rename to ...[[...slug]]/single-page.tsx diff --git a/.env.example b/.env.example index 821ecb11f..3f31f2612 100644 --- a/.env.example +++ b/.env.example @@ -16,8 +16,19 @@ VERCEL_GIT_COMMIT_AUTHOR_LOGIN="" VERCEL_GIT_COMMIT_AUTHOR_NAME="" VERCEL_GIT_PULL_REQUEST_ID="" +# Sanity +NEXT_PUBLIC_SANITY_PROJECT_ID="" +NEXT_PUBLIC_SANITY_DATASET="" +NEXT_PUBLIC_SANITY_API_VERSION="" + +# Preview +SANITY_API_READ_TOKEN="" +SANITY_WEBHOOK_SECRET="" + +# Site TWITTER_CREATOR="@kodamera" TWITTER_SITE="https://kodamera.se" SITE_NAME="KM Storefront" +SITE_DESCRIPTION="High-performance ecommerce store built with Next.js, Vercel, Sanity and Storm." SHOPIFY_STOREFRONT_ACCESS_TOKEN= SHOPIFY_STORE_DOMAIN= diff --git a/app/[locale]/[...slug]/page.tsx b/app/[locale]/[...slug]/page.tsx new file mode 100644 index 000000000..6a621b175 --- /dev/null +++ b/app/[locale]/[...slug]/page.tsx @@ -0,0 +1,77 @@ +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 { + let queryParams = { + locale: params.locale, + slug: '' + }; + + if (params.slug.length > 1) { + queryParams = { + locale: params.locale, + slug: `${params.slug.join('/')}` + }; + } else { + queryParams = { + locale: params.locale, + slug: `${params.slug}` + }; + } + const page = await clientFetch(pageQuery, queryParams); + + 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) { + let queryParams = { + locale: params.locale, + slug: '' + }; + + if (params.slug.length > 1) { + queryParams = { + locale: params.locale, + slug: `${params.slug.join('/')}` + }; + } else { + queryParams = { + locale: params.locale, + slug: `${params.slug}` + }; + } + + const page = await clientFetch(pageQuery, queryParams); + + if (!page) return notFound(); + + return ; +} diff --git a/app/[locale]/[[...slug]]/product-page.tsx b/app/[locale]/[[...slug]]/product-page.tsx deleted file mode 100644 index 1e1e69d71..000000000 --- a/app/[locale]/[[...slug]]/product-page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import ProductView from "components/product/product-view"; -import { notFound } from "next/navigation"; - -interface ProductPageProps { - data: object | any -} - -// 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 ProductPage({data }: ProductPageProps) { - if (!data) { - return notFound(); - } - - const { product } = data; - - return ( - - ) -} diff --git a/app/[locale]/category/[slug]/page.tsx b/app/[locale]/category/[slug]/page.tsx new file mode 100644 index 000000000..a069cb3e1 --- /dev/null +++ b/app/[locale]/category/[slug]/page.tsx @@ -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 { + 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 ( +
+ {title} +
+ ); +} diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css index d66461eab..2d7943a69 100644 --- a/app/[locale]/globals.css +++ b/app/[locale]/globals.css @@ -2,9 +2,76 @@ @tailwind components; @tailwind utilities; -@supports (font: -apple-system-body) and (-webkit-appearance: none) { - img[loading='lazy'] { - clip-path: inset(0.6px); +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; } } @@ -28,7 +95,7 @@ html, body { - @apply font-sans h-full bg-white text-high-contrast; + @apply h-full bg-white font-sans text-high-contrast; box-sizing: border-box; touch-action: manipulation; @@ -74,11 +141,11 @@ body { } .glider-dots { - @apply flex !space-x-[2px] !mt-8; + @apply !mt-8 flex !space-x-[2px]; } .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; + @apply !m-0 !h-4 !w-12 !rounded-none !bg-transparent after:block after:h-[3px] after:w-12 after:bg-ui-border after:content-[''] 2xl:!w-16 2xl:after:w-16; } .glider-dot.active { @@ -86,17 +153,17 @@ body { } .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; + @apply !-top-10 !left-auto !right-12 text-high-contrast !transition-transform !duration-100 hover:scale-110 hover:!text-high-contrast lg:!-top-10 lg:!right-16 2xl:!-top-12 2xl:!right-[100px]; } .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; + @apply !-top-10 !right-4 text-high-contrast !transition-transform !duration-100 hover:scale-110 hover:!text-high-contrast lg:!-top-10 lg:!right-8 2xl:!-top-12 2xl:!right-16; } .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; + @apply absolute !left-4 !top-1/2 text-high-contrast !transition-transform !duration-100 hover:scale-100 hover:!text-high-contrast 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; -} \ No newline at end of file + @apply absolute !right-4 !top-1/2 text-high-contrast !transition-transform !duration-100 hover:scale-100 hover:!text-high-contrast lg:hidden; +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 3d2a7833c..6451f424d 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,30 +1,28 @@ +import Footer from 'components/layout/footer/footer'; +import Header from 'components/layout/header/header'; import { NextIntlClientProvider } from 'next-intl'; import { Inter } from 'next/font/google'; import { notFound } from 'next/navigation'; import { ReactNode } from 'react'; +import { supportedLanguages } from '../../i18n-config'; import './globals.css'; -const SITE_NAME = 'KM Storefront'; -const SITE_DESCRIPTION = 'Webb och digitalbyrå från Göteborg'; -const TWITTER_CREATOR = '@kodamera.se'; -const TWITTER_SITE = 'https://kodamera.se'; - export const metadata = { title: { - default: SITE_NAME, - template: `%s | ${SITE_NAME}` + default: process.env.SITE_NAME, + template: `%s | ${process.env.SITE_NAME}` }, - description: SITE_DESCRIPTION, + description: process.env.SITE_DESCRIPTION, robots: { follow: true, index: true }, - ...(TWITTER_CREATOR && - TWITTER_SITE && { + ...(process.env.TWITTER_CREATOR && + process.env.TWITTER_SITE && { twitter: { card: 'summary_large_image', - creator: TWITTER_CREATOR, - site: TWITTER_SITE + creator: process.env.TWITTER_CREATOR, + site: process.env.TWITTER_SITE } }) }; @@ -36,7 +34,7 @@ const inter = Inter({ }); export function generateStaticParams() { - return [{ locale: 'sv' }, { locale: 'en' }]; + return supportedLanguages.locales.map((locale) => ({ locale: locale.id })); } interface LocaleLayoutProps { @@ -59,7 +57,10 @@ export default async function LocaleLayout({ children, params: { locale } }: Loc - {children} +
+
{children}
+ {/* @ts-expect-error Server Component (https://github.com/vercel/next.js/issues/42292) */} +