From 603bd2b880d05092c2ad472c5c06f6c164345209 Mon Sep 17 00:00:00 2001 From: Henrik Larsson Date: Wed, 3 May 2023 15:16:42 +0200 Subject: [PATCH] Iterated with translations --- app/[locale]/[[...slug]]/home-page.tsx | 10 + app/[locale]/[[...slug]]/page.tsx | 67 +++++- app/[locale]/[[...slug]]/single-page.tsx | 19 ++ app/[locale]/globals.css | 225 ++++++++++++++++++ components/icons/flag-en.tsx | 18 ++ components/icons/flag-sv.tsx | 25 ++ components/layout/header/header.tsx | 4 + components/ui/blurb-section/blurb-section.tsx | 115 +++++++++ components/ui/blurb-section/index.ts | 2 + components/ui/card/card.tsx | 103 ++++++++ components/ui/card/index.tsx | 1 + components/ui/carousel.tsx | 39 --- components/ui/carousel/carousel.tsx | 62 +++++ components/ui/category-card/category-card.tsx | 46 ++++ components/ui/category-card/index.ts | 2 + components/ui/dropdown/dropdown.tsx | 2 + .../dynamic-content-manager.tsx | 79 ++++++ .../ui/dynamic-content-manager/index.tsx | 2 + .../filtered-product-list.tsx | 36 +++ components/ui/filtered-product-list/index.ts | 2 + components/ui/hero/hero.tsx | 82 +++++++ components/ui/hero/index.ts | 2 + components/ui/link/index.ts | 2 + components/ui/link/link.tsx | 19 ++ components/ui/locale-switcher/I18nWidget.tsx | 142 ----------- .../ui/locale-switcher/locale-switcher.tsx | 73 ++++-- components/ui/product-card/index.ts | 2 + components/ui/product-card/product-card.tsx | 80 +++++++ components/ui/product-tag/index.ts | 2 + components/ui/product-tag/product-tag.tsx | 48 ++++ components/ui/sanity-image/index.tsx | 2 + components/ui/sanity-image/sanity-image.tsx | 67 ++++++ components/ui/slider/index.ts | 1 + components/ui/slider/slider.tsx | 78 ++++++ components/ui/text/index.ts | 2 + components/ui/text/text.tsx | 88 +++++++ helpers/getQueryFromSlug.ts | 7 +- i18n-config.ts | 9 +- lib/sanity/sanity.image.ts | 4 +- package.json | 1 + pnpm-lock.yaml | 17 ++ 41 files changed, 1373 insertions(+), 214 deletions(-) create mode 100644 app/[locale]/[[...slug]]/home-page.tsx create mode 100644 app/[locale]/[[...slug]]/single-page.tsx create mode 100644 components/icons/flag-en.tsx create mode 100644 components/icons/flag-sv.tsx create mode 100644 components/ui/blurb-section/blurb-section.tsx create mode 100644 components/ui/blurb-section/index.ts create mode 100644 components/ui/card/card.tsx create mode 100644 components/ui/card/index.tsx delete mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/carousel/carousel.tsx create mode 100644 components/ui/category-card/category-card.tsx create mode 100644 components/ui/category-card/index.ts create mode 100644 components/ui/dynamic-content-manager/dynamic-content-manager.tsx create mode 100644 components/ui/dynamic-content-manager/index.tsx create mode 100644 components/ui/filtered-product-list/filtered-product-list.tsx create mode 100644 components/ui/filtered-product-list/index.ts create mode 100644 components/ui/hero/hero.tsx create mode 100644 components/ui/hero/index.ts create mode 100644 components/ui/link/index.ts create mode 100644 components/ui/link/link.tsx delete mode 100644 components/ui/locale-switcher/I18nWidget.tsx create mode 100644 components/ui/product-card/index.ts create mode 100644 components/ui/product-card/product-card.tsx create mode 100644 components/ui/product-tag/index.ts create mode 100644 components/ui/product-tag/product-tag.tsx create mode 100644 components/ui/sanity-image/index.tsx create mode 100644 components/ui/sanity-image/sanity-image.tsx create mode 100644 components/ui/slider/index.ts create mode 100644 components/ui/slider/slider.tsx create mode 100644 components/ui/text/index.ts create mode 100644 components/ui/text/text.tsx diff --git a/app/[locale]/[[...slug]]/home-page.tsx b/app/[locale]/[[...slug]]/home-page.tsx new file mode 100644 index 000000000..a1c871b59 --- /dev/null +++ b/app/[locale]/[[...slug]]/home-page.tsx @@ -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 ( + + ) +} diff --git a/app/[locale]/[[...slug]]/page.tsx b/app/[locale]/[[...slug]]/page.tsx index 0eb490984..064210dd0 100644 --- a/app/[locale]/[[...slug]]/page.tsx +++ b/app/[locale]/[[...slug]]/page.tsx @@ -1,22 +1,67 @@ -'use client'; +// 'use client'; -import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher'; -import { useTranslations } from 'next-intl'; +import getQueryFromSlug from 'helpers/getQueryFromSlug'; +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 { - params: { +export async function generateStaticParams() { + const paths = await client.fetch(groq`${docQuery}`, { + next: { revalidate: 10 }, + }) + + // console.log(paths) + + return paths.map((path: { + slug: 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 (
-

{t('title')}

- + <> + {docType === 'home' && } + {docType === 'page' && } +
) } \ No newline at end of file diff --git a/app/[locale]/[[...slug]]/single-page.tsx b/app/[locale]/[[...slug]]/single-page.tsx new file mode 100644 index 000000000..af08f5fe3 --- /dev/null +++ b/app/[locale]/[[...slug]]/single-page.tsx @@ -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 ( + + ) +} + +export default SinglePage diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css index c0eca5423..7a899dedd 100644 --- a/app/[locale]/globals.css +++ b/app/[locale]/globals.css @@ -7,3 +7,228 @@ 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; +} + diff --git a/components/icons/flag-en.tsx b/components/icons/flag-en.tsx new file mode 100644 index 000000000..2d4005b69 --- /dev/null +++ b/components/icons/flag-en.tsx @@ -0,0 +1,18 @@ + +export default function FlagEn ({ className = "w-6 h-auto" }: { className?: string }) { + return ( + + + + + + + + + + ) +} \ No newline at end of file diff --git a/components/icons/flag-sv.tsx b/components/icons/flag-sv.tsx new file mode 100644 index 000000000..d219dea08 --- /dev/null +++ b/components/icons/flag-sv.tsx @@ -0,0 +1,25 @@ + +export default function FlagSv ({ className = "w-6 h-auto" }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/components/layout/header/header.tsx b/components/layout/header/header.tsx index ec4d10287..f20752dbd 100644 --- a/components/layout/header/header.tsx +++ b/components/layout/header/header.tsx @@ -1,5 +1,6 @@ 'use client' +import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher' import Logo from 'components/ui/logo/logo' import { NavigationMenu, @@ -64,6 +65,9 @@ const Header: FC = () => { +
+ +
diff --git a/components/ui/blurb-section/blurb-section.tsx b/components/ui/blurb-section/blurb-section.tsx new file mode 100644 index 000000000..3b120754b --- /dev/null +++ b/components/ui/blurb-section/blurb-section.tsx @@ -0,0 +1,115 @@ +import { + CarouselItemProps as ItemProps, + CarouselProps as Props, +} from 'components/ui/carousel/carousel' +import dynamic from 'next/dynamic' +const Carousel = dynamic(() => + import('components/ui/carousel/carousel').then((mod) => mod.Carousel) +) +const CarouselItem = dynamic(() => + 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 ( +
+ {title ? ( + + {title} + + ) : ( + + No title provided yet + + )} +
+ {blurbs.map((blurb: object | any, index: number) => { + return ( +
+ +
+ ) + })} +
+ +
+ {blurbs && ( + + {blurbs.map((blurb: any, index: number) => ( + + + + ))} + + )} +
+
+ ) +} + +export default BlurbSection diff --git a/components/ui/blurb-section/index.ts b/components/ui/blurb-section/index.ts new file mode 100644 index 000000000..ed14d040f --- /dev/null +++ b/components/ui/blurb-section/index.ts @@ -0,0 +1,2 @@ +export { default } from './blurb-section'; + diff --git a/components/ui/card/card.tsx b/components/ui/card/card.tsx new file mode 100644 index 000000000..723c63021 --- /dev/null +++ b/components/ui/card/card.tsx @@ -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 = ({ + 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 ( + +
+ {image && ( +
+ +
+ )} +

+ {title} +

+ {text && ( +

+ {text} +

+ )} +
+ + ) + } + + return ( + +
+ {image && ( +
+ +
+ )} +

+ {title} +

+ {text && ( +

+ {text} +

+ )} +
+
+ ) + } + + return +} + +export default Card diff --git a/components/ui/card/index.tsx b/components/ui/card/index.tsx new file mode 100644 index 000000000..95cfda28e --- /dev/null +++ b/components/ui/card/index.tsx @@ -0,0 +1 @@ +export { default } from './Card' diff --git a/components/ui/carousel.tsx b/components/ui/carousel.tsx deleted file mode 100644 index 30b6a4733..000000000 --- a/components/ui/carousel.tsx +++ /dev/null @@ -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 ( -
-
- {[...products, ...products].map((product, i) => ( - - {product.featuredImage ? ( - {product.title} - ) : null} -
-
- {product.title} -
-
- - ))} -
-
- ); -} diff --git a/components/ui/carousel/carousel.tsx b/components/ui/carousel/carousel.tsx new file mode 100644 index 000000000..8aa60edf8 --- /dev/null +++ b/components/ui/carousel/carousel.tsx @@ -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 = ({ + children, +}: CarouselItemProps) => { + return
{children}
+} + +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 = ({ + children, + gliderClasses, + hasArrows = true, + hasDots = true, + gliderItemWrapperClasses, + slidesToShow = 1, + slidesToScroll = 1, + responsive, +}) => { + return ( +
+ } + iconRight={} + responsive={[responsive]} + skipTrack + > +
+ {React.Children.map(children, (child) => { + return React.cloneElement(child) + })} +
+
+
+ ) +} diff --git a/components/ui/category-card/category-card.tsx b/components/ui/category-card/category-card.tsx new file mode 100644 index 000000000..923c30e55 --- /dev/null +++ b/components/ui/category-card/category-card.tsx @@ -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 = ({ 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 ( + +
+
+ +
+ {category.title} +
+
+
+ + ) +} + +export default CategoryCard diff --git a/components/ui/category-card/index.ts b/components/ui/category-card/index.ts new file mode 100644 index 000000000..40185966a --- /dev/null +++ b/components/ui/category-card/index.ts @@ -0,0 +1,2 @@ +export { default } from './category-card'; + diff --git a/components/ui/dropdown/dropdown.tsx b/components/ui/dropdown/dropdown.tsx index 76032d320..1d4d37f12 100644 --- a/components/ui/dropdown/dropdown.tsx +++ b/components/ui/dropdown/dropdown.tsx @@ -1,3 +1,5 @@ +'use client' + import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons' import * as React from 'react' diff --git a/components/ui/dynamic-content-manager/dynamic-content-manager.tsx b/components/ui/dynamic-content-manager/dynamic-content-manager.tsx new file mode 100644 index 000000000..87c586f4e --- /dev/null +++ b/components/ui/dynamic-content-manager/dynamic-content-manager.tsx @@ -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 ( +
+ + + {`No matching component (Type: ${_type})`} + +
+ ) + } + + return Component ? ( + + ) : ( +
Something else
+ ) +} + +interface dynamicContentManagerProps { + content: [] | any +} + +const DynamicContentManager = ({ content }: dynamicContentManagerProps) => { + return ( +
+ {content?.map(getContentComponent)} +
+ ) +} + +export default DynamicContentManager diff --git a/components/ui/dynamic-content-manager/index.tsx b/components/ui/dynamic-content-manager/index.tsx new file mode 100644 index 000000000..ef80695e1 --- /dev/null +++ b/components/ui/dynamic-content-manager/index.tsx @@ -0,0 +1,2 @@ +export { default } from './dynamic-content-manager'; + diff --git a/components/ui/filtered-product-list/filtered-product-list.tsx b/components/ui/filtered-product-list/filtered-product-list.tsx new file mode 100644 index 000000000..fb403ea04 --- /dev/null +++ b/components/ui/filtered-product-list/filtered-product-list.tsx @@ -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 ( +
+ {title ? ( + + {title} + + ) : ( + + No title provided yet + + )} +
+ {products.slice(0, itemsToShow).map((product: any, index: number) => ( + Product + // + ))} +
+
+ ) +} + +export default FilteredProductList diff --git a/components/ui/filtered-product-list/index.ts b/components/ui/filtered-product-list/index.ts new file mode 100644 index 000000000..bf21a0ae3 --- /dev/null +++ b/components/ui/filtered-product-list/index.ts @@ -0,0 +1,2 @@ +export { default } from './filtered-product-list'; + diff --git a/components/ui/hero/hero.tsx b/components/ui/hero/hero.tsx new file mode 100644 index 000000000..b7b5ac1dc --- /dev/null +++ b/components/ui/hero/hero.tsx @@ -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 ( +
+ {image && ( + + )} +
+ {label && ( + + {label} + + )} + {title ? ( + {title} + ) : ( + + No title provided yet + + )} + {text && ( + + {label} + + )} + {link?.reference && ( + + {link?.title ? link.title : link.reference.title} + + )} +
+
+ ) +} + +export default Hero diff --git a/components/ui/hero/index.ts b/components/ui/hero/index.ts new file mode 100644 index 000000000..4bcc64419 --- /dev/null +++ b/components/ui/hero/index.ts @@ -0,0 +1,2 @@ +export { default } from './hero'; + diff --git a/components/ui/link/index.ts b/components/ui/link/index.ts new file mode 100644 index 000000000..f5561bd64 --- /dev/null +++ b/components/ui/link/index.ts @@ -0,0 +1,2 @@ +export { default } from './link'; + diff --git a/components/ui/link/link.tsx b/components/ui/link/link.tsx new file mode 100644 index 000000000..75b1cff60 --- /dev/null +++ b/components/ui/link/link.tsx @@ -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 ( + + {children} + + ) +} + +export default Link diff --git a/components/ui/locale-switcher/I18nWidget.tsx b/components/ui/locale-switcher/I18nWidget.tsx deleted file mode 100644 index beae0ecac..000000000 --- a/components/ui/locale-switcher/I18nWidget.tsx +++ /dev/null @@ -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 = { - 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 ( - setIsOpen(!isOpen)}> - - - - -
    - {options.map((locale: any) => { - const translationLink = translations?.find( - (item: object | any) => item.locale === locale - ) - - return ( - handleClick(e, locale)} - > -
  • - setIsOpen(false)} - > - - {LOCALES_MAP[locale].img.alt} - - {LOCALES_MAP[locale].name} - -
  • -
    - ) - })} -
-
-
- ) -} - -export default I18nWidget diff --git a/components/ui/locale-switcher/locale-switcher.tsx b/components/ui/locale-switcher/locale-switcher.tsx index 3966f1a08..037142e62 100644 --- a/components/ui/locale-switcher/locale-switcher.tsx +++ b/components/ui/locale-switcher/locale-switcher.tsx @@ -1,22 +1,24 @@ 'use client' +import FlagEn from 'components/icons/flag-en'; +import FlagSv from 'components/icons/flag-sv'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from 'components/ui/dropdown/dropdown' -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { useState } from 'react' -import { i18n } from '../../../i18n-config' +} from 'components/ui/dropdown/dropdown'; +import { useLocale } from 'next-intl'; +import Link from 'next/link'; +import { usePathname, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { i18n } from '../../../i18n-config'; -interface LocaleSwitcherProps { - currentLocale: string -} - -export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) { +export default function LocaleSwitcher() { const pathName = usePathname() + const locale = useLocale(); + const [isOpen, setIsOpen] = useState(false) + const router = useRouter(); const redirectedPathName = (locale: string) => { if (!pathName) return '/' @@ -24,8 +26,28 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) { segments[1] = locale return segments.join('/') } - - const [isOpen, setIsOpen] = useState(false) + + const handleClick = (e: any, locale: string) => { + e.preventDefault() + + const parent = e.target + + if (parent.nodeName !== 'LI') { + return + } + + let href = '/' + + const hasChildLink = parent.querySelector('a').href !== null + + if (hasChildLink) { + href = parent.querySelector('a').href + } + + router.push(`${redirectedPathName(locale)}`) + + setIsOpen(false) + } return (
@@ -34,23 +56,42 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
    {i18n.locales.map((locale) => { + let FlagIcon: any + + FlagIcon = i18n.flags[locale] + return ( handleClick(e, locale)} > -
  • - {locale} +
  • + setIsOpen(false)} + href={redirectedPathName(locale)} + > + + {locale} +
  • ) diff --git a/components/ui/product-card/index.ts b/components/ui/product-card/index.ts new file mode 100644 index 000000000..6eec70851 --- /dev/null +++ b/components/ui/product-card/index.ts @@ -0,0 +1,2 @@ +export { default } from './product-card'; + diff --git a/components/ui/product-card/product-card.tsx b/components/ui/product-card/product-card.tsx new file mode 100644 index 000000000..d0744bf98 --- /dev/null +++ b/components/ui/product-card/product-card.tsx @@ -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 = ({ + // 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 + // + // {variant === 'default' && ( + // <> + //
    + // {/* {process.env.COMMERCE_WISHLIST_ENABLED && ( + // + // )} */} + // {/*
    + // {product?.images && ( + // + // )} + //
    */} + // + //
    + // + // )} + // + ) +} + +export default ProductCard diff --git a/components/ui/product-tag/index.ts b/components/ui/product-tag/index.ts new file mode 100644 index 000000000..0c00d74ce --- /dev/null +++ b/components/ui/product-tag/index.ts @@ -0,0 +1,2 @@ +export { default } from './product-tag'; + diff --git a/components/ui/product-tag/product-tag.tsx b/components/ui/product-tag/product-tag.tsx new file mode 100644 index 000000000..c9ddc319f --- /dev/null +++ b/components/ui/product-tag/product-tag.tsx @@ -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 = ({ + name, + price, + className = '', + variant = 'cardView', +}) => { + return ( +
    + + {name} + + + {price} + +
    + ) +} + +export default ProductTag diff --git a/components/ui/sanity-image/index.tsx b/components/ui/sanity-image/index.tsx new file mode 100644 index 000000000..515353417 --- /dev/null +++ b/components/ui/sanity-image/index.tsx @@ -0,0 +1,2 @@ +export { default } from './sanity-image'; + diff --git a/components/ui/sanity-image/sanity-image.tsx b/components/ui/sanity-image/sanity-image.tsx new file mode 100644 index 000000000..805e4b457 --- /dev/null +++ b/components/ui/sanity-image/sanity-image.tsx @@ -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 ? ( + <> + {alt} + + ) : ( + <> + {alt} + + ) + + return image +} diff --git a/components/ui/slider/index.ts b/components/ui/slider/index.ts new file mode 100644 index 000000000..19a6da94b --- /dev/null +++ b/components/ui/slider/index.ts @@ -0,0 +1 @@ +export { default } from './Slider' diff --git a/components/ui/slider/slider.tsx b/components/ui/slider/slider.tsx new file mode 100644 index 000000000..87ab65000 --- /dev/null +++ b/components/ui/slider/slider.tsx @@ -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(() => + import('components/ui/carousel/carousel').then((mod) => mod.Carousel) +) +const CarouselItem = dynamic(() => + 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 ( +
    + {title ? ( + + {title} + + ) : ( + + No title provided yet + + )} + + {items && ( + + {items.map((item: any, index: number) => ( + + {item.title} + {/* {sliderType === 'products' && } + {sliderType === 'categories' && } */} + + ))} + + )} +
    + ) +} + +export default Slider diff --git a/components/ui/text/index.ts b/components/ui/text/index.ts new file mode 100644 index 000000000..31bf6903a --- /dev/null +++ b/components/ui/text/index.ts @@ -0,0 +1,2 @@ +export { default } from './text'; + diff --git a/components/ui/text/text.tsx b/components/ui/text/text.tsx new file mode 100644 index 000000000..54220011a --- /dev/null +++ b/components/ui/text/text.tsx @@ -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 = ({ + style, + className = '', + variant = 'body', + children, + html, + onClick, +}) => { + const componentsMap: { + [P in Variant]: React.ComponentType | string + } = { + body: 'div', + heading: 'h1', + pageHeading: 'h1', + sectionHeading: 'h2', + listChildHeading: 'h3', + label: 'div', + paragraph: 'p', + } + + const Component: + | JSXElementConstructor + | React.ReactElement + | React.ComponentType + | string = componentsMap![variant!] + + const htmlContentProps = html + ? { + dangerouslySetInnerHTML: { __html: html }, + } + : {} + + return ( + + {children} + + ) +} + +export default Text diff --git a/helpers/getQueryFromSlug.ts b/helpers/getQueryFromSlug.ts index b90ee0292..c0fc0388e 100644 --- a/helpers/getQueryFromSlug.ts +++ b/helpers/getQueryFromSlug.ts @@ -4,7 +4,7 @@ import { pageQuery, } from '../lib/sanity/queries' -const getQueryFromSlug = (slugArray = []) => { +const getQueryFromSlug = (slugArray: string[], locale: string) => { const docQuery = { homePage: groq`${homePageQuery}`, page: groq`${pageQuery}`, @@ -12,10 +12,10 @@ const getQueryFromSlug = (slugArray = []) => { let docType = '' - if (slugArray.length === 0) { + if (!slugArray) { return { docType: 'home', - queryParams: {}, + queryParams: {locale: locale}, 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. let queryParams = { slug: `/${slugArray.join('/')}`, + locale: locale } if (slugStart === 'articles' && slugArray.length === 2) { diff --git a/i18n-config.ts b/i18n-config.ts index 0915c37a9..258ff807e 100644 --- a/i18n-config.ts +++ b/i18n-config.ts @@ -1,6 +1,13 @@ +import FlagEn from "components/icons/flag-en" +import FlagSv from "components/icons/flag-sv" + export const i18n = { defaultLocale: 'sv', - locales: ['sv', 'en', 'nn'], + locales: ['sv', 'en'], + flags: { + sv: FlagSv, + en: FlagEn + } } as const export type Locale = typeof i18n['locales'][number] \ No newline at end of file diff --git a/lib/sanity/sanity.image.ts b/lib/sanity/sanity.image.ts index b41ba8c13..221251499 100644 --- a/lib/sanity/sanity.image.ts +++ b/lib/sanity/sanity.image.ts @@ -1,7 +1,7 @@ -import { sanityClient } from './sanity.client' 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) => imageBuilder.image(source).auto('format').fit('crop') diff --git a/package.json b/package.json index af8ce4341..8a739d543 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react": "18.2.0", "react-cookie": "^4.1.1", "react-dom": "18.2.0", + "react-glider": "^4.0.2", "sanity": "3", "styled-components": "^5.2", "tailwind-merge": "^1.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 350e5a525..499e3d053 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,7 @@ specifiers: react: 18.2.0 react-cookie: ^4.1.1 react-dom: 18.2.0 + react-glider: ^4.0.2 sanity: '3' styled-components: ^5.2 tailwind-merge: ^1.12.0 @@ -71,6 +72,7 @@ dependencies: react: 18.2.0 react-cookie: 4.1.1_react@18.2.0 react-dom: 18.2.0_react@18.2.0 + react-glider: 4.0.2_biqbaboplfbrettd7655fr4n2y sanity: 3.9.1_inskn5v7aqlrr54h6fubgcms5y styled-components: 5.3.10_7i5myeigehqah43i5u7wbekgba tailwind-merge: 1.12.0 @@ -4095,6 +4097,10 @@ packages: - supports-color dev: false + /glider-js/1.7.8: + resolution: {integrity: sha512-env4+yfoNYaeHJmEBGygjcZDpZ5LRUjGhCB6Ag+XNQfvuLVD5PhCYQShBVCAd5On7XfknM57O+V2Fwfcsii3Dw==} + dev: false + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5966,6 +5972,17 @@ packages: use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i 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: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}