mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 14:06:59 +00:00
Iterated with translations
This commit is contained in:
parent
a1ae2357db
commit
603bd2b880
10
app/[locale]/[[...slug]]/home-page.tsx
Normal file
10
app/[locale]/[[...slug]]/home-page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import DynamicContentManager from 'components/ui/dynamic-content-manager'
|
||||||
|
|
||||||
|
// This is a Client Component. It receives data as props and
|
||||||
|
// has access to state and effects just like Page components
|
||||||
|
// in the `pages` directory.
|
||||||
|
export default function HomePage({ data }: { data: object | any }) {
|
||||||
|
return (
|
||||||
|
<DynamicContentManager content={data?.content} />
|
||||||
|
)
|
||||||
|
}
|
@ -1,22 +1,67 @@
|
|||||||
'use client';
|
// 'use client';
|
||||||
|
|
||||||
import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher';
|
import getQueryFromSlug from 'helpers/getQueryFromSlug';
|
||||||
import { useTranslations } from 'next-intl';
|
import { docQuery } from 'lib/sanity/queries';
|
||||||
|
import { client } from 'lib/sanity/sanity.client';
|
||||||
|
import { groq } from 'next-sanity';
|
||||||
|
import HomePage from './home-page';
|
||||||
|
import SinglePage from './single-page';
|
||||||
|
|
||||||
interface PageProps {
|
export async function generateStaticParams() {
|
||||||
params: {
|
const paths = await client.fetch(groq`${docQuery}`, {
|
||||||
|
next: { revalidate: 10 },
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log(paths)
|
||||||
|
|
||||||
|
return paths.map((path: {
|
||||||
|
slug: string,
|
||||||
locale: string
|
locale: string
|
||||||
}
|
}) => ({
|
||||||
|
slug: path.slug.split('/').filter((p) => p),
|
||||||
|
locale: path.locale
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Index({params: {locale}} : PageProps) {
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
const t = useTranslations('Index');
|
if (data.length === 1) {
|
||||||
|
return data[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
return data.find((item) => item._id.startsWith(`drafts.`)) || data[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { slug: string[], locale: string };
|
||||||
|
}) {
|
||||||
|
const { slug, locale } = params;
|
||||||
|
|
||||||
|
const { query, queryParams, docType } = getQueryFromSlug(slug, locale)
|
||||||
|
|
||||||
|
const pageData = await client.fetch(query, queryParams)
|
||||||
|
|
||||||
|
const data = filterDataToSingleItem(pageData, false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{t('title')}</h1>
|
<>
|
||||||
<LocaleSwitcher currentLocale={locale} />
|
{docType === 'home' && <HomePage data={data} />}
|
||||||
|
{docType === 'page' && <SinglePage data={data} />}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
19
app/[locale]/[[...slug]]/single-page.tsx
Normal file
19
app/[locale]/[[...slug]]/single-page.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
const DynamicContentManager = dynamic(
|
||||||
|
() => import('components/ui/dynamic-content-manager')
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SinglePageProps {
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const SinglePage = ({ data }: SinglePageProps) => {
|
||||||
|
return (
|
||||||
|
<DynamicContentManager content={data?.content} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SinglePage
|
@ -7,3 +7,228 @@
|
|||||||
clip-path: inset(0.6px);
|
clip-path: inset(0.6px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* BASE */
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
/* Code for Firefox */
|
||||||
|
color: #ffffff;
|
||||||
|
background: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply font-sans h-full bg-white text-high-contrast;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
touch-action: manipulation;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COMPONENTS */
|
||||||
|
.glider {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glider::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glider-dots {
|
||||||
|
@apply flex !space-x-[2px] !mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glider-dot {
|
||||||
|
@apply !m-0 !rounded-none !w-12 !h-4 !bg-transparent after:content-[''] after:block after:w-12 after:h-[3px] after:bg-ui-border 2xl:!w-16 2xl:after:w-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glider-dot.active {
|
||||||
|
@apply after:!bg-high-contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glider slider. */
|
||||||
|
.glider-prev {
|
||||||
|
@apply text-high-contrast !right-12 !-top-10 !left-auto lg:!right-16 lg:!-top-12 2xl:!-top-16 2xl:!right-[100px] !transition-transform !duration-100 hover:!text-high-contrast hover:scale-110;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glider-next {
|
||||||
|
@apply text-high-contrast !right-4 !-top-10 lg:!right-8 lg:!-top-12 2xl:!-top-16 2xl:!right-16 !transition-transform !duration-100 hover:!text-high-contrast hover:scale-110;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdp .glider-prev {
|
||||||
|
@apply text-high-contrast absolute !left-4 !top-1/2 !transition-transform !duration-100 hover:!text-high-contrast hover:scale-100 lg:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdp .glider-next {
|
||||||
|
@apply text-high-contrast absolute !right-4 !top-1/2 !transition-transform !duration-100 hover:!text-high-contrast hover:scale-100 lg:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dynamic content */
|
||||||
|
.dynamic-content > :not(.hero) {
|
||||||
|
@apply my-16 lg:my-24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-content > :first-child {
|
||||||
|
@apply mt-0 md:mt-0 lg:mt-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-content > :last-child {
|
||||||
|
@apply mb-16 lg:mb-24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-content .dynamic-content {
|
||||||
|
@apply px-0 md:px-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-content .dynamic-content > {
|
||||||
|
@apply my-0 md:my-0 lg:my-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-content .dynamic-content > :last-child {
|
||||||
|
@apply my-0 md:my-0 lg:my-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ALGOLIA SEARCH */
|
||||||
|
.ais-SearchBox-form {
|
||||||
|
@apply w-full relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-SearchBox-input {
|
||||||
|
@apply h-[44px] pl-10 bg-ui w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-SearchBox-input::placeholder {
|
||||||
|
@apply text-high-contrast text-opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-SearchBox-submit {
|
||||||
|
@apply absolute left-0 flex items-center justify-center w-12 h-12 top-1/2 transform -translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-SearchBox-submit svg {
|
||||||
|
@apply w-4 h-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-SearchBox-reset:not([hidden]) {
|
||||||
|
@apply absolute right-[3px] bg-ui flex items-center justify-center w-12 h-8 top-1/2 transform -translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-SearchBox-reset svg {
|
||||||
|
@apply w-3 h-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-RefinementList-item {
|
||||||
|
@apply mt-1 first:mt-0;
|
||||||
|
}
|
||||||
|
.ais-RefinementList-label {
|
||||||
|
@apply flex items-center cursor-pointer text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-RefinementList-checkbox {
|
||||||
|
@apply w-4 h-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-RefinementList-labelText {
|
||||||
|
@apply ml-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-RefinementList-count {
|
||||||
|
@apply ml-2 bg-ui h-5 w-5 flex items-center justify-center text-xs rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-CurrentRefinements-label {
|
||||||
|
@apply uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-Hits-list {
|
||||||
|
@apply grid grid-cols-2 gap-4 lg:grid-cols-4 items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-Pagination-list {
|
||||||
|
@apply flex justify-center -space-x-px mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-Pagination-link {
|
||||||
|
@apply flex h-12 w-12 items-center justify-center bg-white border border-ui-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-Pagination-link--selected {
|
||||||
|
@apply bg-ui font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-ClearRefinements-button {
|
||||||
|
@apply inline-flex py-3 cursor-pointer px-6 border border-ui-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ais-ClearRefinements-button--disabled {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ALGOLIA AUTOCOMPLETE */
|
||||||
|
.aa-DetachedContainer {
|
||||||
|
@apply !shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-DetachedContainer--modal {
|
||||||
|
@apply lg:!top-4 !rounded-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-DetachedFormContainer {
|
||||||
|
@apply !border-none !px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-DetachedSearchButton {
|
||||||
|
@apply !w-10 !h-10 !p-0 cursor-pointer items-center justify-center !border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-DetachedSearchButtonIcon,
|
||||||
|
.aa-SubmitIcon {
|
||||||
|
@apply pointer-events-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-DetachedSearchButtonPlaceholder {
|
||||||
|
@apply sr-only;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-Form {
|
||||||
|
@apply !rounded-none !bg-white !border-ui-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-SubmitButton {
|
||||||
|
@apply !flex !items-center !justify-center !p-0 !h-10 !w-10 lg:!w-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-SubmitIcon {
|
||||||
|
@apply !text-high-contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-Panel {
|
||||||
|
@apply !rounded-none border border-ui-border !shadow-none !p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-PanelLayout {
|
||||||
|
@apply !p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-Item {
|
||||||
|
@apply !px-4 !py-2 lg:!px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aa-Item[aria-selected='true'] {
|
||||||
|
@apply !bg-ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
18
components/icons/flag-en.tsx
Normal file
18
components/icons/flag-en.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
export default function FlagEn ({ className = "w-6 h-auto" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 30.6 20.4"
|
||||||
|
>
|
||||||
|
<path className="fill-[#293476]" d="M.1.1h30.5v20.3H.1z"/>
|
||||||
|
<path className="fill-[#fff]" d="M30.5 7.2H18.3V.1h-6v7.1H.1v6.1h12.2v7.1h6v-7.1h12.2z"/>
|
||||||
|
<path className="fill-[#fff]" d="M30.5 18L3.6.1H.1v2.3l26.9 18h3.5z"/>
|
||||||
|
<path className="fill-[#fff]" d="M.1 18L27 .1h3.5v2.3l-26.9 18H.1z"/>
|
||||||
|
<path className="fill-[#ce202d]" d="M30.5 8.4H17.1V.1h-3.6v8.3H.1V12h13.4v8.4h3.6V12h13.4z"/>
|
||||||
|
<path className="fill-[#ce202d]" d="M30.5 20.3v-1.6l-8.2-5.4h-2.4zm0-20.2h-2.4l-9.8 6.5v.6h1.6L30.5.1zM10.7 13.3l-10.6 7v.1h2.3l9.9-6.6v-.5zM.1.1v1.6l8.2 5.5h2.4z"/>
|
||||||
|
<path className="fill-none" d="M0 20.4V0h30.6v20.5H0zm30.5 0zM.1 20.3h30.3V.1H.1v20.2z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
25
components/icons/flag-sv.tsx
Normal file
25
components/icons/flag-sv.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
export default function FlagSv ({ className = "w-6 h-auto" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 640 480"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="a">
|
||||||
|
<path fillOpacity=".67" d="M-53.421 0h682.67v512h-682.67z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clipPath="url(#a)" transform="translate(50.082) scale(.9375)">
|
||||||
|
<g fillRule="evenodd" strokeWidth="1pt">
|
||||||
|
<path fill="#006aa7" d="M-121.103.302h256V205.1h-256zm0 306.876h256v204.8h-256z"/>
|
||||||
|
<path fill="#fecc00" d="M-121.103 204.984h256v102.4h-256z"/>
|
||||||
|
<path fill="#fecc00" d="M133.843.01h102.4v511.997h-102.4z"/>
|
||||||
|
<path fill="#fecc00" d="M232.995 205.013h460.798v102.4H232.995z"/>
|
||||||
|
<path fill="#006aa7" d="M236.155 307.208h460.797v204.799H236.155zm0-306.906h460.797V205.1H236.155z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher'
|
||||||
import Logo from 'components/ui/logo/logo'
|
import Logo from 'components/ui/logo/logo'
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
@ -64,6 +65,9 @@ const Header: FC<HeaderProps> = () => {
|
|||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<LocaleSwitcher />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HeaderRoot>
|
</HeaderRoot>
|
||||||
|
115
components/ui/blurb-section/blurb-section.tsx
Normal file
115
components/ui/blurb-section/blurb-section.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
CarouselItemProps as ItemProps,
|
||||||
|
CarouselProps as Props,
|
||||||
|
} from 'components/ui/carousel/carousel'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
const Carousel = dynamic<Props>(() =>
|
||||||
|
import('components/ui/carousel/carousel').then((mod) => mod.Carousel)
|
||||||
|
)
|
||||||
|
const CarouselItem = dynamic<ItemProps>(() =>
|
||||||
|
import('components/ui/carousel/carousel').then((mod) => mod.CarouselItem)
|
||||||
|
)
|
||||||
|
const Card = dynamic(() => import('components/ui/card'))
|
||||||
|
|
||||||
|
import Text from 'components/ui/text'
|
||||||
|
|
||||||
|
interface BlurbSectionProps {
|
||||||
|
blurbs: any
|
||||||
|
title: string
|
||||||
|
mobileLayout: string
|
||||||
|
desktopLayout: string
|
||||||
|
imageFormat: 'square' | 'portrait' | 'landscape'
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlurbSection = ({
|
||||||
|
title,
|
||||||
|
mobileLayout,
|
||||||
|
desktopLayout,
|
||||||
|
blurbs,
|
||||||
|
imageFormat,
|
||||||
|
}: BlurbSectionProps) => {
|
||||||
|
const gridLayout =
|
||||||
|
desktopLayout === '2-column'
|
||||||
|
? 'lg:grid-cols-2'
|
||||||
|
: desktopLayout === '3-column'
|
||||||
|
? 'lg:grid-cols-3'
|
||||||
|
: 'lg:grid-cols-4'
|
||||||
|
|
||||||
|
const sliderLayout =
|
||||||
|
desktopLayout === '2-column' ? 2 : desktopLayout === '3-column' ? 3 : 4
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{title ? (
|
||||||
|
<Text
|
||||||
|
className="mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||||
|
variant="sectionHeading"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
className="italic mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||||
|
variant="sectionHeading"
|
||||||
|
>
|
||||||
|
No title provided yet
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`px-4 grid ${gridLayout} gap-x-4 gap-y-8 ${
|
||||||
|
mobileLayout === 'stacked' ? 'lg:hidden' : 'hidden'
|
||||||
|
} lg:px-8 2xl:!px-16`}
|
||||||
|
>
|
||||||
|
{blurbs.map((blurb: object | any, index: number) => {
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
<Card
|
||||||
|
title={blurb?.title}
|
||||||
|
link={blurb?.link}
|
||||||
|
image={blurb?.image}
|
||||||
|
text={blurb?.text}
|
||||||
|
imageFormat={blurb?.imageFormat}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
mobileLayout === 'stacked' ? 'hidden lg:block' : 'block'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{blurbs && (
|
||||||
|
<Carousel
|
||||||
|
gliderClasses={'px-4 lg:px-8 2xl:px-16'}
|
||||||
|
gliderItemWrapperClasses={'space-x-2 lg:space-x-4'}
|
||||||
|
hasDots={true}
|
||||||
|
slidesToShow={2.2}
|
||||||
|
responsive={{
|
||||||
|
breakpoint: 1024,
|
||||||
|
settings: {
|
||||||
|
slidesToShow:
|
||||||
|
sliderLayout <= 4 ? sliderLayout + 0.5 : sliderLayout,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{blurbs.map((blurb: any, index: number) => (
|
||||||
|
<CarouselItem key={`${index}`}>
|
||||||
|
<Card
|
||||||
|
title={blurb?.title}
|
||||||
|
link={blurb?.link}
|
||||||
|
image={blurb?.image}
|
||||||
|
text={blurb.text}
|
||||||
|
imageFormat={imageFormat}
|
||||||
|
/>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlurbSection
|
2
components/ui/blurb-section/index.ts
Normal file
2
components/ui/blurb-section/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './blurb-section';
|
||||||
|
|
103
components/ui/card/card.tsx
Normal file
103
components/ui/card/card.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import SanityImage from 'components/ui/sanity-image'
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
className?: string
|
||||||
|
title: string
|
||||||
|
image: object | any
|
||||||
|
link: object | any
|
||||||
|
text?: string
|
||||||
|
imageFormat?: 'square' | 'portrait' | 'landscape'
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
|
|
||||||
|
const Card: FC<CardProps> = ({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
link,
|
||||||
|
text,
|
||||||
|
imageFormat = 'square',
|
||||||
|
}) => {
|
||||||
|
const rootClassName = cn('relative', className)
|
||||||
|
|
||||||
|
const { linkType } = link
|
||||||
|
|
||||||
|
const imageWrapperClasses = cn('w-full h-full overflow-hidden relative', {
|
||||||
|
['aspect-square']: imageFormat === 'square',
|
||||||
|
['aspect-[3/4]']: imageFormat === 'portrait',
|
||||||
|
['aspect-[4/3]']: imageFormat === 'landscape',
|
||||||
|
})
|
||||||
|
const imageClasses = cn('object-cover w-full h-full')
|
||||||
|
|
||||||
|
function Card() {
|
||||||
|
if (linkType === 'internal') {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={link.internalLink.reference.slug.current}
|
||||||
|
className={rootClassName}
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{image && (
|
||||||
|
<div className={imageWrapperClasses}>
|
||||||
|
<SanityImage
|
||||||
|
className={imageClasses}
|
||||||
|
image={image}
|
||||||
|
alt={image.alt || ''}
|
||||||
|
sizes="(max-width: 1024px) 50vw, 20vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-2 text-high-contrast font-medium text-sm underline underline-offset-2 lg:text-lg lg:mt-3 lg:underline-offset-4 2xl:text-xl">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{text && (
|
||||||
|
<p className="text-sm mt-1 text-low-contrast lg:text-base lg:mt-2">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={link.externalLink.url}
|
||||||
|
className={rootClassName}
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{image && (
|
||||||
|
<div className={imageWrapperClasses}>
|
||||||
|
<SanityImage
|
||||||
|
className={imageClasses}
|
||||||
|
image={image}
|
||||||
|
alt={image.alt || ''}
|
||||||
|
sizes="(max-width: 1024px) 50vw, 20vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-2 text-high-contrast font-medium text-sm underline underline-offset-2 lg:text-lg lg:mt-3 lg:underline-offset-4 2xl:text-xl">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{text && (
|
||||||
|
<p className="text-sm mt-1 text-low-contrast lg:text-base lg:mt-2">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Card />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card
|
1
components/ui/card/index.tsx
Normal file
1
components/ui/card/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './Card'
|
@ -1,39 +0,0 @@
|
|||||||
import { getCollectionProducts } from 'lib/shopify';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export async function Carousel() {
|
|
||||||
// Collections that start with `hidden-*` are hidden from the search page.
|
|
||||||
const products = await getCollectionProducts('hidden-homepage-carousel');
|
|
||||||
|
|
||||||
if (!products?.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full overflow-hidden bg-black dark:bg-white">
|
|
||||||
<div className="flex animate-carousel">
|
|
||||||
{[...products, ...products].map((product, i) => (
|
|
||||||
<Link
|
|
||||||
key={`${product.handle}${i}`}
|
|
||||||
href={`/product/${product.handle}`}
|
|
||||||
className="relative h-[30vh] w-1/2 flex-none md:w-1/3"
|
|
||||||
>
|
|
||||||
{product.featuredImage ? (
|
|
||||||
<Image
|
|
||||||
alt={product.title}
|
|
||||||
className="h-full object-contain"
|
|
||||||
fill
|
|
||||||
sizes="33vw"
|
|
||||||
src={product.featuredImage.url}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center justify-center">
|
|
||||||
<div className="inline-flex bg-white p-4 text-xl font-semibold text-black dark:bg-black dark:text-white">
|
|
||||||
{product.title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
62
components/ui/carousel/carousel.tsx
Normal file
62
components/ui/carousel/carousel.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import 'glider-js/glider.min.css'
|
||||||
|
import { ArrowLeft, ArrowRight } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
import Glider from 'react-glider'
|
||||||
|
|
||||||
|
export interface CarouselItemProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CarouselItem: React.FC<CarouselItemProps> = ({
|
||||||
|
children,
|
||||||
|
}: CarouselItemProps) => {
|
||||||
|
return <div className="">{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarouselProps {
|
||||||
|
children: JSX.Element | JSX.Element[] | any
|
||||||
|
gliderClasses?: string
|
||||||
|
hasArrows?: boolean
|
||||||
|
hasDots?: boolean
|
||||||
|
gliderItemWrapperClasses?: string
|
||||||
|
slidesToShow?: number
|
||||||
|
slidesToScroll?: number
|
||||||
|
responsive?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Carousel: React.FC<CarouselProps> = ({
|
||||||
|
children,
|
||||||
|
gliderClasses,
|
||||||
|
hasArrows = true,
|
||||||
|
hasDots = true,
|
||||||
|
gliderItemWrapperClasses,
|
||||||
|
slidesToShow = 1,
|
||||||
|
slidesToScroll = 1,
|
||||||
|
responsive,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Glider
|
||||||
|
className={`flex w-full relative ${gliderClasses}`}
|
||||||
|
draggable
|
||||||
|
slidesToShow={slidesToShow}
|
||||||
|
scrollLock
|
||||||
|
slidesToScroll={slidesToScroll}
|
||||||
|
hasArrows={hasArrows}
|
||||||
|
hasDots={hasDots}
|
||||||
|
iconLeft={<ArrowLeft className="stroke-current" />}
|
||||||
|
iconRight={<ArrowRight className="stroke-current" />}
|
||||||
|
responsive={[responsive]}
|
||||||
|
skipTrack
|
||||||
|
>
|
||||||
|
<div className={`flex w-full ${gliderItemWrapperClasses} `}>
|
||||||
|
{React.Children.map(children, (child) => {
|
||||||
|
return React.cloneElement(child)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Glider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
46
components/ui/category-card/category-card.tsx
Normal file
46
components/ui/category-card/category-card.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import SanityImage from 'components/ui/sanity-image'
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
category: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
|
|
||||||
|
const CategoryCard: FC<Props> = ({ category, className }) => {
|
||||||
|
const rootClassName = cn(
|
||||||
|
'w-1/2 min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear cursor-pointer basis-[50%]',
|
||||||
|
className
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`${category.slug}`}
|
||||||
|
className={rootClassName}
|
||||||
|
aria-label={category.name}
|
||||||
|
>
|
||||||
|
<div className={'flex flex-col flex-1 justify-center w-full h-full'}>
|
||||||
|
<div className="w-full h-full aspect-[3/4] relative">
|
||||||
|
<SanityImage
|
||||||
|
image={category.image}
|
||||||
|
alt={category.name || 'Category Image'}
|
||||||
|
width={300}
|
||||||
|
height={400}
|
||||||
|
sizes="(max-width: 1024px) 50vw, 25vw"
|
||||||
|
/>
|
||||||
|
<div className="absolute font-medium bg-high-contrast text-white py-3 px-6 md:py-5 md:px-10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
{category.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryCard
|
2
components/ui/category-card/index.ts
Normal file
2
components/ui/category-card/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './category-card';
|
||||||
|
|
@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons'
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Info } from 'lucide-react'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
import Hero from 'components/ui/hero'
|
||||||
|
const Slider = dynamic(() => import('components/ui/slider'))
|
||||||
|
const BlurbSection = dynamic(() => import('components/ui/blurb-section'))
|
||||||
|
const FilteredProductList = dynamic(
|
||||||
|
() => import('components/ui/filtered-product-list')
|
||||||
|
)
|
||||||
|
|
||||||
|
interface getContentComponentProps {
|
||||||
|
_type: string
|
||||||
|
_key: number
|
||||||
|
disabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContentComponent = ({
|
||||||
|
_type,
|
||||||
|
_key,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: getContentComponentProps) => {
|
||||||
|
let Component: any
|
||||||
|
|
||||||
|
switch (_type) {
|
||||||
|
case 'hero':
|
||||||
|
Component = Hero
|
||||||
|
break
|
||||||
|
case 'slider':
|
||||||
|
Component = Slider
|
||||||
|
break
|
||||||
|
case 'filteredProductList':
|
||||||
|
Component = FilteredProductList
|
||||||
|
break
|
||||||
|
case 'blurbSection':
|
||||||
|
if (disabled !== true) {
|
||||||
|
Component = BlurbSection
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`px-4 lg:px-8 2xl:px-16 ${
|
||||||
|
process.env.NODE_ENV === 'production' ? 'hidden' : ''
|
||||||
|
}`}
|
||||||
|
key={`index-${_key}`}
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center bg-red font-bold p-2 text-sm">
|
||||||
|
<Info className="mr-1" />
|
||||||
|
{`No matching component (Type: ${_type})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component ? (
|
||||||
|
<Component key={`index-${_key}`} {...rest} />
|
||||||
|
) : (
|
||||||
|
<div key={`index-${_key}`}>Something else</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface dynamicContentManagerProps {
|
||||||
|
content: [] | any
|
||||||
|
}
|
||||||
|
|
||||||
|
const DynamicContentManager = ({ content }: dynamicContentManagerProps) => {
|
||||||
|
return (
|
||||||
|
<div className="dynamic-content overflow-x-hidden">
|
||||||
|
{content?.map(getContentComponent)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicContentManager
|
2
components/ui/dynamic-content-manager/index.tsx
Normal file
2
components/ui/dynamic-content-manager/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './dynamic-content-manager';
|
||||||
|
|
@ -0,0 +1,36 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Text from 'components/ui/text'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
const ProductCard = dynamic(() => import('components/ui/product-card'))
|
||||||
|
|
||||||
|
interface SliderProps {
|
||||||
|
products: any
|
||||||
|
title: string
|
||||||
|
itemsToShow: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilteredProductList = ({ title, products, itemsToShow }: SliderProps) => {
|
||||||
|
return (
|
||||||
|
<div className="px-4 lg:px-8 2xl:px-16">
|
||||||
|
{title ? (
|
||||||
|
<Text className="mb-4 lg:mb-6 2xl:mb-8" variant="sectionHeading">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text className="italic mb-4 lg:mb-6 2xl:mb-8" variant="sectionHeading">
|
||||||
|
No title provided yet
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{products.slice(0, itemsToShow).map((product: any, index: number) => (
|
||||||
|
<span>Product</span>
|
||||||
|
// <ProductCard key={`${product.id}-${index}`} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilteredProductList
|
2
components/ui/filtered-product-list/index.ts
Normal file
2
components/ui/filtered-product-list/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './filtered-product-list';
|
||||||
|
|
82
components/ui/hero/hero.tsx
Normal file
82
components/ui/hero/hero.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
const SanityImage = dynamic(() => import('components/ui/sanity-image'))
|
||||||
|
const Link = dynamic(() => import('components/ui/link'))
|
||||||
|
const Text = dynamic(() => import('components/ui/text'))
|
||||||
|
|
||||||
|
interface HeroProps {
|
||||||
|
variant: string
|
||||||
|
text?: string
|
||||||
|
label?: string
|
||||||
|
title: string
|
||||||
|
image: object | any
|
||||||
|
desktopImage: object | any
|
||||||
|
link: {
|
||||||
|
title: string
|
||||||
|
reference: {
|
||||||
|
title: string
|
||||||
|
slug: {
|
||||||
|
current: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeroSize = keyof typeof heroSize
|
||||||
|
|
||||||
|
const heroSize = {
|
||||||
|
fullScreen: 'aspect-[3/4] lg:aspect-auto lg:h-[calc(100vh-4rem)]',
|
||||||
|
halfScreen: 'aspect-square max-h-[60vh] lg:aspect-auto lg:min-h-[60vh]',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Hero = ({ variant, title, text, label, image, link }: HeroProps) => {
|
||||||
|
const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-screen ${heroClass} flex flex-col justify-end relative text-high-contrast`}
|
||||||
|
>
|
||||||
|
{image && (
|
||||||
|
<SanityImage
|
||||||
|
image={image}
|
||||||
|
alt={image.alt}
|
||||||
|
priority={true}
|
||||||
|
width={1200}
|
||||||
|
height={600}
|
||||||
|
className="absolute inset-0 h-full w-full object-cover z-10"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col items-start text-high-contrast absolute max-w-sm z-40 left-4 bottom-5 lg:max-w-xl lg:bottom-8 lg:left-8 2xl:left-16 2xl:bottom-16">
|
||||||
|
{label && (
|
||||||
|
<Text className="mb-1 lg:mb-2" variant="label">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{title ? (
|
||||||
|
<Text variant="heading">{title}</Text>
|
||||||
|
) : (
|
||||||
|
<Text variant="heading" className="italic">
|
||||||
|
No title provided yet
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{text && (
|
||||||
|
<Text className="mt-4" variant="paragraph">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{link?.reference && (
|
||||||
|
<Link
|
||||||
|
className="inline-flex transition bg-high-contrast text-white text-base py-4 px-10 mt-6 hover:bg-low-contrast lg:mt-8"
|
||||||
|
href={link.reference.slug.current}
|
||||||
|
>
|
||||||
|
{link?.title ? link.title : link.reference.title}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Hero
|
2
components/ui/hero/index.ts
Normal file
2
components/ui/hero/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './hero';
|
||||||
|
|
2
components/ui/link/index.ts
Normal file
2
components/ui/link/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './link';
|
||||||
|
|
19
components/ui/link/link.tsx
Normal file
19
components/ui/link/link.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
|
||||||
|
|
||||||
|
const Link: React.FC<
|
||||||
|
NextLinkProps & {
|
||||||
|
children?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
> = ({ href, children, className, ...props }) => {
|
||||||
|
return (
|
||||||
|
<NextLink className={cn('', className)} href={href} {...props}>
|
||||||
|
{children}
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Link
|
@ -1,142 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import { cn } from '@lib/utils'
|
|
||||||
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@components/ui/Dropdown/Dropdown'
|
|
||||||
|
|
||||||
interface LOCALE_DATA {
|
|
||||||
name: string
|
|
||||||
img: {
|
|
||||||
filename: string
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOCALES_MAP: Record<string, LOCALE_DATA> = {
|
|
||||||
sv: {
|
|
||||||
name: 'Swedish',
|
|
||||||
img: {
|
|
||||||
filename: 'flag-sv.svg',
|
|
||||||
alt: 'Swedish flag',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nn: {
|
|
||||||
name: 'Norwegian',
|
|
||||||
img: {
|
|
||||||
filename: 'flag-no.svg',
|
|
||||||
alt: 'Norwegian flag',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
name: 'English',
|
|
||||||
img: {
|
|
||||||
filename: 'flag-en.svg',
|
|
||||||
alt: 'British flag',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
interface I18nWidgetProps {
|
|
||||||
translations: [] | any
|
|
||||||
}
|
|
||||||
|
|
||||||
const I18nWidget = ({ translations }: I18nWidgetProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const { locale, locales, defaultLocale = 'sv' } = useRouter()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const options: any = locales?.filter((val) => val !== locale)
|
|
||||||
const currentLocale = locale || defaultLocale
|
|
||||||
|
|
||||||
const handleClick = (e: any, locale: string) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const parent = e.target
|
|
||||||
|
|
||||||
if (parent.nodeName !== 'LI') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let href = '/'
|
|
||||||
|
|
||||||
const hasChildLink = parent.querySelector('a').href !== null
|
|
||||||
|
|
||||||
if (hasChildLink) {
|
|
||||||
href = parent.querySelector('a').href
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({ pathname: href }, { pathname: href }, { locale: locale })
|
|
||||||
|
|
||||||
setIsOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
'w-10 h-10 duration-200 bg-app shrink-0 flex items-center justify-center transition hover:scale-105'
|
|
||||||
}
|
|
||||||
aria-label="Language selector"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
width="13"
|
|
||||||
height="18"
|
|
||||||
className="flex w-5 h-auto pointer-events-none rounded-[1px]"
|
|
||||||
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
|
||||||
alt={LOCALES_MAP[currentLocale].img.alt}
|
|
||||||
unoptimized
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="drop-shadow-xl">
|
|
||||||
<ul className="">
|
|
||||||
{options.map((locale: any) => {
|
|
||||||
const translationLink = translations?.find(
|
|
||||||
(item: object | any) => item.locale === locale
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={locale}
|
|
||||||
asChild
|
|
||||||
onClick={(e) => handleClick(e, locale)}
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href={translationLink ? translationLink.slug.current : '/'}
|
|
||||||
locale={locale}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center w-full cursor-pointer px-1 py-1 text-center transition ease-in-out duration-150 text-high-contrast capitalize'
|
|
||||||
)}
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<Image
|
|
||||||
width="13"
|
|
||||||
height="18"
|
|
||||||
className="mr-2 w-5 h-auto pointer-events-none rounded-[1px]"
|
|
||||||
src={`/${LOCALES_MAP[locale].img.filename}`}
|
|
||||||
alt={LOCALES_MAP[locale].img.alt}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>{LOCALES_MAP[locale].name}</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default I18nWidget
|
|
@ -1,22 +1,24 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import FlagEn from 'components/icons/flag-en';
|
||||||
|
import FlagSv from 'components/icons/flag-sv';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from 'components/ui/dropdown/dropdown'
|
} from 'components/ui/dropdown/dropdown';
|
||||||
import Link from 'next/link'
|
import { useLocale } from 'next-intl';
|
||||||
import { usePathname } from 'next/navigation'
|
import Link from 'next/link';
|
||||||
import { useState } from 'react'
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { i18n } from '../../../i18n-config'
|
import { useState } from 'react';
|
||||||
|
import { i18n } from '../../../i18n-config';
|
||||||
|
|
||||||
interface LocaleSwitcherProps {
|
export default function LocaleSwitcher() {
|
||||||
currentLocale: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
|
|
||||||
const pathName = usePathname()
|
const pathName = usePathname()
|
||||||
|
const locale = useLocale();
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const redirectedPathName = (locale: string) => {
|
const redirectedPathName = (locale: string) => {
|
||||||
if (!pathName) return '/'
|
if (!pathName) return '/'
|
||||||
@ -25,7 +27,27 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
|
|||||||
return segments.join('/')
|
return segments.join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const handleClick = (e: any, locale: string) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const parent = e.target
|
||||||
|
|
||||||
|
if (parent.nodeName !== 'LI') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let href = '/'
|
||||||
|
|
||||||
|
const hasChildLink = parent.querySelector('a').href !== null
|
||||||
|
|
||||||
|
if (hasChildLink) {
|
||||||
|
href = parent.querySelector('a').href
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(`${redirectedPathName(locale)}`)
|
||||||
|
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -34,23 +56,42 @@ export default function LocaleSwitcher({currentLocale}: LocaleSwitcherProps) {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
'duration-200 bg-app shrink-0 flex items-center justify-center transition hover:scale-105'
|
'duration-200 bg-app shrink-0 uppercase space-x-2 text-sm flex items-center justify-center transition hover:scale-105'
|
||||||
}
|
}
|
||||||
aria-label="Language selector"
|
aria-label="Language selector"
|
||||||
>
|
>
|
||||||
Locale: {currentLocale}
|
{locale === "sv" && (
|
||||||
|
<FlagSv />
|
||||||
|
)}
|
||||||
|
{locale === "en" && (
|
||||||
|
<FlagEn />
|
||||||
|
)}
|
||||||
|
<span>{locale}</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="drop-shadow-xl">
|
<DropdownMenuContent align="end" className="drop-shadow-xl">
|
||||||
<ul className="">
|
<ul className="">
|
||||||
{i18n.locales.map((locale) => {
|
{i18n.locales.map((locale) => {
|
||||||
|
let FlagIcon: any
|
||||||
|
|
||||||
|
FlagIcon = i18n.flags[locale]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
className='p-0'
|
||||||
key={locale}
|
key={locale}
|
||||||
asChild
|
asChild
|
||||||
|
onClick={(e) => handleClick(e, locale)}
|
||||||
>
|
>
|
||||||
<li key={locale}>
|
<li className="flex" key={locale}>
|
||||||
<Link href={redirectedPathName(locale)}>{locale}</Link>
|
<Link
|
||||||
|
className="flex w-full cursor-pointer uppercase space-x-2 text-sm p-2"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
href={redirectedPathName(locale)}
|
||||||
|
>
|
||||||
|
<FlagIcon />
|
||||||
|
<span>{locale}</span>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)
|
)
|
||||||
|
2
components/ui/product-card/index.ts
Normal file
2
components/ui/product-card/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './product-card';
|
||||||
|
|
80
components/ui/product-card/product-card.tsx
Normal file
80
components/ui/product-card/product-card.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import { FC } from 'react'
|
||||||
|
// import type { Product } from '@commerce/types/product'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
// import usePrice from '@framework/product/use-price'
|
||||||
|
|
||||||
|
// const WishlistButton = dynamic(
|
||||||
|
// () => import('@components/wishlist/WishlistButton')
|
||||||
|
// )
|
||||||
|
const ProductTag = dynamic(() => import('components/ui/product-tag'))
|
||||||
|
const SanityImage = dynamic(() => import('components/ui/sanity-image'))
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
// product: Product
|
||||||
|
variant?: 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductCard: FC<Props> = ({
|
||||||
|
// product,
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
}) => {
|
||||||
|
// const { price } = usePrice({
|
||||||
|
// amount: product.price.value,
|
||||||
|
// baseAmount: product.price.retailPrice,
|
||||||
|
// currencyCode: product.price.currencyCode!,
|
||||||
|
// })
|
||||||
|
|
||||||
|
const rootClassName = cn(
|
||||||
|
'w-full min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear basis-[50%]',
|
||||||
|
className
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>Produyct</>
|
||||||
|
// <Link
|
||||||
|
// href={`${product.slug}`}
|
||||||
|
// className={rootClassName}
|
||||||
|
// aria-label={product.name}
|
||||||
|
// locale={product.locale}
|
||||||
|
// >
|
||||||
|
// {variant === 'default' && (
|
||||||
|
// <>
|
||||||
|
// <div className={'flex flex-col flex-1 justify-center w-full h-full'}>
|
||||||
|
// {/* {process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
|
// <WishlistButton
|
||||||
|
// className={'top-4 right-4 z-10 absolute'}
|
||||||
|
// productId={product.id}
|
||||||
|
// variant={
|
||||||
|
// product?.variants ? (product.variants[0] as any) : null
|
||||||
|
// }
|
||||||
|
// />
|
||||||
|
// )} */}
|
||||||
|
// {/* <div className="w-full h-full aspect-square overflow-hidden relative">
|
||||||
|
// {product?.images && (
|
||||||
|
// <SanityImage
|
||||||
|
// image={product?.images[0]}
|
||||||
|
// alt={product.name || 'Product Image'}
|
||||||
|
// width={400}
|
||||||
|
// height={400}
|
||||||
|
// sizes="(max-width: 1024px) 50vw, 20vw"
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// </div> */}
|
||||||
|
// <ProductTag
|
||||||
|
// className="mt-2 lg:mt-3"
|
||||||
|
// name={product.title}
|
||||||
|
// price={`${price}`}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
// </Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCard
|
2
components/ui/product-tag/index.ts
Normal file
2
components/ui/product-tag/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './product-tag';
|
||||||
|
|
48
components/ui/product-tag/product-tag.tsx
Normal file
48
components/ui/product-tag/product-tag.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
const Text = dynamic(() => import('components/ui/text'))
|
||||||
|
|
||||||
|
interface ProductTagProps {
|
||||||
|
className?: string
|
||||||
|
name: string
|
||||||
|
price: string
|
||||||
|
variant?: 'productView' | 'cardView'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductTag: React.FC<ProductTagProps> = ({
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
className = '',
|
||||||
|
variant = 'cardView',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('text-high-contrast flex items-start flex-col', className)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={
|
||||||
|
variant === 'cardView'
|
||||||
|
? ''
|
||||||
|
: '!text-[32px] !leading-[32px] !font-normal'
|
||||||
|
}
|
||||||
|
variant={variant === 'cardView' ? 'listChildHeading' : 'pageHeading'}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className={
|
||||||
|
variant === 'cardView'
|
||||||
|
? '!text-sm !font-semibold !leading-tight lg:!text-base'
|
||||||
|
: '!font-bold !text-[32px] !leading-[32px]'
|
||||||
|
}
|
||||||
|
variant="paragraph"
|
||||||
|
>
|
||||||
|
{price}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductTag
|
2
components/ui/sanity-image/index.tsx
Normal file
2
components/ui/sanity-image/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './sanity-image';
|
||||||
|
|
67
components/ui/sanity-image/sanity-image.tsx
Normal file
67
components/ui/sanity-image/sanity-image.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { urlForImage } from 'lib/sanity/sanity.image'
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
interface SanityImageProps {
|
||||||
|
image: object | any
|
||||||
|
alt: string
|
||||||
|
priority?: boolean
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
quality?: number
|
||||||
|
sizes?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
|
|
||||||
|
export default function SanityImage(props: SanityImageProps) {
|
||||||
|
const {
|
||||||
|
image: source,
|
||||||
|
priority = false,
|
||||||
|
quality = 75,
|
||||||
|
alt = '',
|
||||||
|
height = 1080,
|
||||||
|
width = 1080,
|
||||||
|
sizes = '100vw',
|
||||||
|
className,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const rootClassName = cn('w-full h-auto', className)
|
||||||
|
|
||||||
|
const image = source?.asset?._rev ? (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
className={`${rootClassName}`}
|
||||||
|
placeholder="blur"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
alt={alt}
|
||||||
|
src={urlForImage(source)
|
||||||
|
.width(width)
|
||||||
|
.height(height)
|
||||||
|
.quality(quality)
|
||||||
|
.url()}
|
||||||
|
sizes={sizes}
|
||||||
|
priority={priority}
|
||||||
|
blurDataURL={source.asset.metadata.lqip}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
className={`${rootClassName}`}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
alt={alt}
|
||||||
|
src={placeholderImg}
|
||||||
|
sizes={sizes}
|
||||||
|
priority={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
1
components/ui/slider/index.ts
Normal file
1
components/ui/slider/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './Slider'
|
78
components/ui/slider/slider.tsx
Normal file
78
components/ui/slider/slider.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CarouselItemProps as ItemProps,
|
||||||
|
CarouselProps as Props,
|
||||||
|
} from 'components/ui/carousel/carousel'
|
||||||
|
import Text from 'components/ui/text'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
const Carousel = dynamic<Props>(() =>
|
||||||
|
import('components/ui/carousel/carousel').then((mod) => mod.Carousel)
|
||||||
|
)
|
||||||
|
const CarouselItem = dynamic<ItemProps>(() =>
|
||||||
|
import('components/ui/carousel/carousel').then((mod) => mod.CarouselItem)
|
||||||
|
)
|
||||||
|
const ProductCard = dynamic(() => import('components/ui/product-card'))
|
||||||
|
const CategoryCard = dynamic(() => import('components/ui/category-card'))
|
||||||
|
|
||||||
|
interface SliderProps {
|
||||||
|
products: [] | any
|
||||||
|
title: string
|
||||||
|
categories: [] | any
|
||||||
|
sliderType: String
|
||||||
|
}
|
||||||
|
|
||||||
|
const Slider = ({ products, categories, title, sliderType }: SliderProps) => {
|
||||||
|
const [items, setItems] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sliderType === 'products') setItems(products)
|
||||||
|
else if (sliderType === 'categories') setItems(categories)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{title ? (
|
||||||
|
<Text
|
||||||
|
className="mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||||
|
variant="sectionHeading"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
className="italic mb-4 px-4 lg:px-8 lg:mb-6 2xl:px-16 2xl:mb-8"
|
||||||
|
variant="sectionHeading"
|
||||||
|
>
|
||||||
|
No title provided yet
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{items && (
|
||||||
|
<Carousel
|
||||||
|
gliderClasses={'px-4 lg:px-8 2xl:px-16'}
|
||||||
|
gliderItemWrapperClasses={'space-x-2 lg:space-x-4'}
|
||||||
|
hasDots={true}
|
||||||
|
slidesToShow={2.2}
|
||||||
|
responsive={{
|
||||||
|
breakpoint: 1024,
|
||||||
|
settings: {
|
||||||
|
slidesToShow: 4.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items.map((item: any, index: number) => (
|
||||||
|
<CarouselItem key={`${sliderType}-${index}`}>
|
||||||
|
{item.title}
|
||||||
|
{/* {sliderType === 'products' && <ProductCard product={item} />}
|
||||||
|
{sliderType === 'categories' && <CategoryCard category={item} />} */}
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Slider
|
2
components/ui/text/index.ts
Normal file
2
components/ui/text/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './text';
|
||||||
|
|
88
components/ui/text/text.tsx
Normal file
88
components/ui/text/text.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { cn } from 'lib/utils'
|
||||||
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
FunctionComponent,
|
||||||
|
JSXElementConstructor,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
interface TextProps {
|
||||||
|
variant?: Variant
|
||||||
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
children?: React.ReactNode | any
|
||||||
|
html?: string
|
||||||
|
onClick?: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
type Variant =
|
||||||
|
| 'heading'
|
||||||
|
| 'body'
|
||||||
|
| 'pageHeading'
|
||||||
|
| 'sectionHeading'
|
||||||
|
| 'label'
|
||||||
|
| 'paragraph'
|
||||||
|
| 'listChildHeading'
|
||||||
|
|
||||||
|
const Text: FunctionComponent<TextProps> = ({
|
||||||
|
style,
|
||||||
|
className = '',
|
||||||
|
variant = 'body',
|
||||||
|
children,
|
||||||
|
html,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const componentsMap: {
|
||||||
|
[P in Variant]: React.ComponentType<any> | string
|
||||||
|
} = {
|
||||||
|
body: 'div',
|
||||||
|
heading: 'h1',
|
||||||
|
pageHeading: 'h1',
|
||||||
|
sectionHeading: 'h2',
|
||||||
|
listChildHeading: 'h3',
|
||||||
|
label: 'div',
|
||||||
|
paragraph: 'p',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component:
|
||||||
|
| JSXElementConstructor<any>
|
||||||
|
| React.ReactElement<any>
|
||||||
|
| React.ComponentType<any>
|
||||||
|
| string = componentsMap![variant!]
|
||||||
|
|
||||||
|
const htmlContentProps = html
|
||||||
|
? {
|
||||||
|
dangerouslySetInnerHTML: { __html: html },
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={cn(
|
||||||
|
'',
|
||||||
|
{
|
||||||
|
['text-base max-w-prose']: variant === 'body',
|
||||||
|
['max-w-prose text-4xl font-display font-bold leading-none md:text-5xl md:leading-none lg:leading-none lg:text-6xl']:
|
||||||
|
variant === 'heading',
|
||||||
|
['max-w-prose text-3xl font-display font-bold leading-none md:text-4xl md:leading-none lg:leading-none lg:text-5xl']:
|
||||||
|
variant === 'pageHeading',
|
||||||
|
['max-w-prose text-2xl font-display font-bold leading-none md:text-3xl md:leading-none lg:leading-none lg:text-4xl']:
|
||||||
|
variant === 'sectionHeading',
|
||||||
|
['text-sm font-semibold leading-tight lg:text-base']:
|
||||||
|
variant === 'listChildHeading',
|
||||||
|
['text-sm max-w-prose lg:text-base 2xl:text-lg']: variant === 'label',
|
||||||
|
['max-w-prose lg:text-lg 2xl:text-xl']: variant === 'paragraph',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
style={style}
|
||||||
|
{...htmlContentProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Text
|
@ -4,7 +4,7 @@ import {
|
|||||||
pageQuery,
|
pageQuery,
|
||||||
} from '../lib/sanity/queries'
|
} from '../lib/sanity/queries'
|
||||||
|
|
||||||
const getQueryFromSlug = (slugArray = []) => {
|
const getQueryFromSlug = (slugArray: string[], locale: string) => {
|
||||||
const docQuery = {
|
const docQuery = {
|
||||||
homePage: groq`${homePageQuery}`,
|
homePage: groq`${homePageQuery}`,
|
||||||
page: groq`${pageQuery}`,
|
page: groq`${pageQuery}`,
|
||||||
@ -12,10 +12,10 @@ const getQueryFromSlug = (slugArray = []) => {
|
|||||||
|
|
||||||
let docType = ''
|
let docType = ''
|
||||||
|
|
||||||
if (slugArray.length === 0) {
|
if (!slugArray) {
|
||||||
return {
|
return {
|
||||||
docType: 'home',
|
docType: 'home',
|
||||||
queryParams: {},
|
queryParams: {locale: locale},
|
||||||
query: docQuery.homePage,
|
query: docQuery.homePage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -25,6 +25,7 @@ const getQueryFromSlug = (slugArray = []) => {
|
|||||||
// We now have to re-combine the slug array to match our slug in Sanity.
|
// We now have to re-combine the slug array to match our slug in Sanity.
|
||||||
let queryParams = {
|
let queryParams = {
|
||||||
slug: `/${slugArray.join('/')}`,
|
slug: `/${slugArray.join('/')}`,
|
||||||
|
locale: locale
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slugStart === 'articles' && slugArray.length === 2) {
|
if (slugStart === 'articles' && slugArray.length === 2) {
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
import FlagEn from "components/icons/flag-en"
|
||||||
|
import FlagSv from "components/icons/flag-sv"
|
||||||
|
|
||||||
export const i18n = {
|
export const i18n = {
|
||||||
defaultLocale: 'sv',
|
defaultLocale: 'sv',
|
||||||
locales: ['sv', 'en', 'nn'],
|
locales: ['sv', 'en'],
|
||||||
|
flags: {
|
||||||
|
sv: FlagSv,
|
||||||
|
en: FlagEn
|
||||||
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Locale = typeof i18n['locales'][number]
|
export type Locale = typeof i18n['locales'][number]
|
@ -1,7 +1,7 @@
|
|||||||
import { sanityClient } from './sanity.client'
|
|
||||||
import createImageUrlBuilder from '@sanity/image-url'
|
import createImageUrlBuilder from '@sanity/image-url'
|
||||||
|
import { client } from './sanity.client'
|
||||||
|
|
||||||
export const imageBuilder = createImageUrlBuilder(sanityClient)
|
export const imageBuilder = createImageUrlBuilder(client)
|
||||||
|
|
||||||
export const urlForImage = (source: any) =>
|
export const urlForImage = (source: any) =>
|
||||||
imageBuilder.image(source).auto('format').fit('crop')
|
imageBuilder.image(source).auto('format').fit('crop')
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-glider": "^4.0.2",
|
||||||
"sanity": "3",
|
"sanity": "3",
|
||||||
"styled-components": "^5.2",
|
"styled-components": "^5.2",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -40,6 +40,7 @@ specifiers:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-cookie: ^4.1.1
|
react-cookie: ^4.1.1
|
||||||
react-dom: 18.2.0
|
react-dom: 18.2.0
|
||||||
|
react-glider: ^4.0.2
|
||||||
sanity: '3'
|
sanity: '3'
|
||||||
styled-components: ^5.2
|
styled-components: ^5.2
|
||||||
tailwind-merge: ^1.12.0
|
tailwind-merge: ^1.12.0
|
||||||
@ -71,6 +72,7 @@ dependencies:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-cookie: 4.1.1_react@18.2.0
|
react-cookie: 4.1.1_react@18.2.0
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
react-glider: 4.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
sanity: 3.9.1_inskn5v7aqlrr54h6fubgcms5y
|
sanity: 3.9.1_inskn5v7aqlrr54h6fubgcms5y
|
||||||
styled-components: 5.3.10_7i5myeigehqah43i5u7wbekgba
|
styled-components: 5.3.10_7i5myeigehqah43i5u7wbekgba
|
||||||
tailwind-merge: 1.12.0
|
tailwind-merge: 1.12.0
|
||||||
@ -4095,6 +4097,10 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/glider-js/1.7.8:
|
||||||
|
resolution: {integrity: sha512-env4+yfoNYaeHJmEBGygjcZDpZ5LRUjGhCB6Ag+XNQfvuLVD5PhCYQShBVCAd5On7XfknM57O+V2Fwfcsii3Dw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/glob-parent/5.1.2:
|
/glob-parent/5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -5966,6 +5972,17 @@ packages:
|
|||||||
use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i
|
use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-glider/4.0.2_biqbaboplfbrettd7655fr4n2y:
|
||||||
|
resolution: {integrity: sha512-nZnkV4x8mSzfhGgrTkMrlL/g07T6hR0bLHg0q1iRumuORwfX+kqxCYoRy4/WXTRhlmurhcFKTaH007Vz2HjeHA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0
|
||||||
|
react-dom: ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
glider-js: 1.7.8
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-is/16.13.1:
|
/react-is/16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user