Skip to content

Commit

Permalink
Merge pull request #18 from thomasKn/thomas/fv-186-header
Browse files Browse the repository at this point in the history
Add shadcn navigation menu
  • Loading branch information
thomasKn authored Jan 31, 2024
2 parents 8dd9ea6 + 8658971 commit 33dbb61
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 238 deletions.
4 changes: 3 additions & 1 deletion app/components/cart/CartLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export function CartLines({
lines: CartType['lines'] | undefined;
onClose?: () => void;
}) {
const currentLines = cartLines ? flattenConnection(cartLines) : [];
const currentLines = cartLines?.nodes.length
? flattenConnection(cartLines)
: [];

const className = cx([
layout === 'page'
Expand Down
39 changes: 0 additions & 39 deletions app/components/icons/IconArrow.tsx

This file was deleted.

27 changes: 27 additions & 0 deletions app/components/icons/IconChevron.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {cn} from '~/lib/utils';

import type {IconProps} from './Icon';

import {Icon} from './Icon';

export function IconChevron(
props: {direction: 'down' | 'left' | 'right' | 'up'} & IconProps,
) {
return (
<Icon
className={cn('size-5', props.className)}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
>
<title>Chevron</title>
{props.direction === 'down' && <path d="m6 9 6 6 6-6" />}
{props.direction === 'up' && <path d="m18 15-6-6-6 6" />}
{props.direction === 'left' && <path d="m15 18-6-6 6-6" />}
{props.direction === 'right' && <path d="m9 18 6-6-6-6" />}
</Icon>
);
}
4 changes: 2 additions & 2 deletions app/components/layout/CountrySelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type {I18nLocale} from '~/lib/type';
import {useLocale} from '~/hooks/useLocale';
import {useLocalePath} from '~/hooks/useLocalePath';

import {IconArrow} from '../icons/IconArrow';
import {IconCheck} from '../icons/IconCheck';
import {IconChevron} from '../icons/IconChevron';
import {Button} from '../ui/Button';
import {
DropdownMenu,
Expand All @@ -28,7 +28,7 @@ export function CountrySelector() {
<DropdownMenuTrigger asChild>
<Button className="flex max-w-fit gap-2" variant="outline">
{currentLocale?.label}
<IconArrow className="size-3" direction="down" />
<IconChevron className="size-3" direction="down" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
Expand Down
27 changes: 12 additions & 15 deletions app/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {useSettingsCssVars} from '~/hooks/useSettingsCssVars';

import {headerVariants} from '../cva/header';
import {Navigation} from '../navigation/Navigation';
import {Button} from '../ui/Button';
import {CartCount} from './CartCount';
import {Logo} from './Logo';

Expand All @@ -33,7 +32,7 @@ function DesktopHeader() {
return (
<header
className={cx([
'section-padding relative bg-background text-foreground',
'section-padding relative hidden bg-background text-foreground md:block',
headerVariants({
optional: showSeparatorLine ? 'separator-line' : null,
}),
Expand All @@ -42,19 +41,17 @@ function DesktopHeader() {
<style dangerouslySetInnerHTML={{__html: cssVars}} />
<div className="container">
<div className="flex items-center justify-between">
<Button asChild variant="primitive">
<Link prefetch="intent" to={homePath}>
<Logo
className="h-auto w-[var(--logoWidth)]"
sizes={logoWidth}
style={
{
'--logoWidth': logoWidth || 'auto',
} as CSSProperties
}
/>
</Link>
</Button>
<Link prefetch="intent" to={homePath}>
<Logo
className="h-auto w-[var(--logoWidth)]"
sizes={logoWidth}
style={
{
'--logoWidth': logoWidth || 'auto',
} as CSSProperties
}
/>
</Link>
<div className="flex items-center gap-3">
<Navigation data={header?.menu} />
<CartCount />
Expand Down
108 changes: 88 additions & 20 deletions app/components/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,106 @@
import type {InferType} from 'groqd';

import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import {useEffect, useRef, useState} from 'react';

import type {HEADER_QUERY} from '~/qroq/queries';

import {SanityExternalLink} from '../sanity/link/SanityExternalLink';
import {SanityInternalLink} from '../sanity/link/SanityInternalLink';
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuList,
navigationMenuTriggerStyle,
} from '../ui/NavigationMenu';
import {NestedNavigation} from './NestedNavigation';

type HeaderQuery = InferType<typeof HEADER_QUERY>;
type NavigationProps = NonNullable<HeaderQuery>['menu'];

export function Navigation(props: {data?: NavigationProps}) {
const menuRef = useRef<HTMLUListElement>(null);
const [activeItem, setActiveItem] = useState<null | string | undefined>(null);
const dropdownWidth = 200;
const viewportPosition = useViewportPosition(
menuRef,
activeItem,
dropdownWidth,
);

return (
<NavigationMenu.Root>
<NavigationMenu.List className="flex gap-5">
<NavigationMenu id="header-nav">
<CssVars
dropdownWidth={dropdownWidth}
viewportPosition={viewportPosition}
/>
<NavigationMenuList ref={menuRef}>
{props.data &&
props.data?.length > 0 &&
props.data?.map((item) => {
return (
<NavigationMenu.Item className="relative" key={item._key}>
{item._type === 'internalLink' && (
<SanityInternalLink data={item} />
)}
{item._type === 'externalLink' && (
<SanityExternalLink data={item} />
)}
{item._type === 'nestedNavigation' && (
<NestedNavigation data={item} />
)}
</NavigationMenu.Item>
);
})}
</NavigationMenu.List>
</NavigationMenu.Root>
props.data?.map((item) => (
<NavigationMenuItem id={item._key!} key={item._key}>
{item._type === 'internalLink' && (
<SanityInternalLink
className={navigationMenuTriggerStyle()}
data={item}
/>
)}
{item._type === 'externalLink' && (
<SanityExternalLink
className={navigationMenuTriggerStyle()}
data={item}
/>
)}
{item._type === 'nestedNavigation' && (
<NestedNavigation data={item} setActiveItem={setActiveItem} />
)}
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
);
}

function CssVars(props: {dropdownWidth: number; viewportPosition: number}) {
const cssVar = `
#header-nav {
--viewport-position: ${props.viewportPosition}%;
--dropdown-width: ${props.dropdownWidth}px;
}
`;

return <style dangerouslySetInnerHTML={{__html: cssVar}} />;
}

// Dynamically calculate the position of the <NavigationMenuPrimitive.Viewport /> based on the active item
function useViewportPosition(
menuRef: React.RefObject<HTMLUListElement>,
activeItem: null | string | undefined,
dropdownWidth: number,
) {
const [viewportPosition, setViewportPosition] = useState(0);

useEffect(() => {
const menuElement = menuRef.current;

if (!menuElement) return;

const menuWidth = menuElement.offsetWidth;
const menuLeft = menuElement.getBoundingClientRect().left;
const activeChild = Array.from(menuElement.children).find(
(child) => child.id === activeItem,
);
const dropdownWidthPercentage = (dropdownWidth / menuWidth) * 100;
const rect = activeChild?.getBoundingClientRect();
const positionPercentage = rect
? ((rect.left - menuLeft) / menuWidth) * 100
: 0;

if (positionPercentage + dropdownWidthPercentage > 100) {
setViewportPosition(100 - dropdownWidthPercentage);
} else {
setViewportPosition(positionPercentage);
}
}, [menuRef, activeItem, dropdownWidth]);

return viewportPosition;
}
Loading

0 comments on commit 33dbb61

Please sign in to comment.