Skip to content

Commit 3d52b84

Browse files
committed
Update merch nav dropdown
1 parent dccfc63 commit 3d52b84

1 file changed

Lines changed: 168 additions & 14 deletions

File tree

src/components/Navbar.tsx

Lines changed: 168 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
Newspaper,
2828
Paintbrush,
2929
ShieldCheck,
30-
Shirt,
30+
ShoppingBag,
3131
Sparkles,
3232
TrendingUp,
3333
User,
@@ -60,6 +60,9 @@ import {
6060
CollapsibleTrigger,
6161
} from '~/components/Collapsible'
6262
import { groupToSlug } from '~/components/stack/stack-categories'
63+
import { getProducts } from '~/utils/shop.functions'
64+
import { formatMoney, shopifyImageUrl } from '~/utils/shopify-format'
65+
import type { ProductListItem } from '~/utils/shopify-queries'
6366

6467
type LogoProps = {
6568
title?: React.ComponentType | null
@@ -264,19 +267,8 @@ const NAV_GROUPS = [
264267
{
265268
key: 'merch',
266269
label: 'Merch',
267-
sections: [
268-
{
269-
label: 'Shop',
270-
items: [
271-
{
272-
label: 'New Apparel',
273-
to: '/merch',
274-
description: 'TanStack shirts, hoodies, and new drops.',
275-
icon: Shirt,
276-
},
277-
],
278-
},
279-
],
270+
to: '/shop',
271+
sections: [],
280272
},
281273
{
282274
key: 'support',
@@ -694,6 +686,9 @@ function DesktopNavTrigger({ group }: { group: NavMenuGroup }) {
694686
type="button"
695687
data-menu-key={group.key}
696688
className={triggerClassName}
689+
onMouseDown={(event) => {
690+
event.preventDefault()
691+
}}
697692
>
698693
<span>{group.label}</span>
699694
</button>
@@ -786,6 +781,10 @@ function MegaMenuContent({
786781
return <LibrariesMenuContent onNavigate={onNavigate} variant={variant} />
787782
}
788783

784+
if (group.key === 'merch') {
785+
return <MerchMenuContent onNavigate={onNavigate} variant={variant} />
786+
}
787+
789788
return (
790789
<div
791790
className={twMerge(
@@ -993,6 +992,161 @@ function LibraryMenuItem({
993992
)
994993
}
995994

995+
function MerchMenuContent({
996+
onNavigate,
997+
variant,
998+
}: {
999+
onNavigate: () => void
1000+
variant: 'desktop' | 'mobile'
1001+
}) {
1002+
const [products, setProducts] = React.useState<Array<ProductListItem>>([])
1003+
const [loading, setLoading] = React.useState(true)
1004+
1005+
React.useEffect(() => {
1006+
let cancelled = false
1007+
1008+
async function loadProducts() {
1009+
setLoading(true)
1010+
1011+
try {
1012+
const page = await getProducts({
1013+
data: {
1014+
first: 3,
1015+
sortKey: 'CREATED_AT',
1016+
reverse: true,
1017+
},
1018+
})
1019+
1020+
if (!cancelled) {
1021+
setProducts(page.nodes)
1022+
}
1023+
} catch {
1024+
if (!cancelled) {
1025+
setProducts([])
1026+
}
1027+
} finally {
1028+
if (!cancelled) {
1029+
setLoading(false)
1030+
}
1031+
}
1032+
}
1033+
1034+
loadProducts()
1035+
1036+
return () => {
1037+
cancelled = true
1038+
}
1039+
}, [])
1040+
1041+
const allMerchItem: NavMenuItem = {
1042+
label: 'All Merch',
1043+
to: '/shop',
1044+
description: 'Browse all TanStack apparel, accessories, and stickers.',
1045+
icon: ShoppingBag,
1046+
}
1047+
1048+
return (
1049+
<div
1050+
className={twMerge(variant === 'desktop' ? 'grid gap-4' : 'grid gap-3')}
1051+
>
1052+
<section>
1053+
<div className="mb-2 px-2 text-xs font-black uppercase text-gray-500 dark:text-gray-400">
1054+
Recent Products
1055+
</div>
1056+
<div
1057+
className={twMerge(
1058+
'grid gap-1',
1059+
variant === 'desktop' && 'md:grid-cols-3',
1060+
)}
1061+
>
1062+
{loading
1063+
? Array.from({ length: 3 }, (_, index) => (
1064+
<div
1065+
key={index}
1066+
className="rounded-lg px-2 py-2.5"
1067+
aria-hidden="true"
1068+
>
1069+
<div className="aspect-[4/3] animate-pulse rounded-md bg-gray-200 dark:bg-gray-800" />
1070+
<div className="mt-2 h-4 w-3/4 animate-pulse rounded bg-gray-200 dark:bg-gray-800" />
1071+
<div className="mt-1 h-3 w-1/3 animate-pulse rounded bg-gray-200 dark:bg-gray-800" />
1072+
</div>
1073+
))
1074+
: products.map((product) => (
1075+
<MerchProductLink
1076+
key={product.id}
1077+
product={product}
1078+
onNavigate={onNavigate}
1079+
variant={variant}
1080+
/>
1081+
))}
1082+
</div>
1083+
</section>
1084+
<MenuItemLink
1085+
item={allMerchItem}
1086+
onNavigate={onNavigate}
1087+
variant={variant}
1088+
compact
1089+
/>
1090+
</div>
1091+
)
1092+
}
1093+
1094+
function MerchProductLink({
1095+
product,
1096+
onNavigate,
1097+
variant,
1098+
}: {
1099+
product: ProductListItem
1100+
onNavigate: () => void
1101+
variant: 'desktop' | 'mobile'
1102+
}) {
1103+
const image = product.featuredImage
1104+
const price = product.priceRange.minVariantPrice
1105+
1106+
return (
1107+
<Link
1108+
to="/shop/products/$handle"
1109+
params={{ handle: product.handle }}
1110+
onClick={onNavigate}
1111+
className={twMerge(
1112+
'group rounded-lg px-2 py-2.5 text-left hover:bg-gray-500/10 focus:bg-gray-500/10 focus:outline-none',
1113+
variant === 'mobile' && 'flex items-center gap-3 py-3',
1114+
)}
1115+
preload="intent"
1116+
>
1117+
<span
1118+
className={twMerge(
1119+
'block overflow-hidden rounded-md bg-gray-100 dark:bg-gray-900',
1120+
variant === 'desktop' ? 'aspect-[4/3]' : 'h-14 w-14 shrink-0',
1121+
)}
1122+
>
1123+
{image ? (
1124+
<img
1125+
src={shopifyImageUrl(image.url, { width: 360, format: 'webp' })}
1126+
alt={image.altText ?? product.title}
1127+
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
1128+
loading="lazy"
1129+
/>
1130+
) : (
1131+
<span className="flex h-full w-full items-center justify-center text-gray-400">
1132+
<ShoppingBag className="h-5 w-5" />
1133+
</span>
1134+
)}
1135+
</span>
1136+
<span
1137+
className={twMerge('block min-w-0', variant === 'desktop' && 'mt-2')}
1138+
>
1139+
<span className="block truncate font-bold text-gray-950 dark:text-white">
1140+
{product.title}
1141+
</span>
1142+
<span className="mt-0.5 block text-sm text-gray-600 dark:text-gray-400">
1143+
{formatMoney(price.amount, price.currencyCode)}
1144+
</span>
1145+
</span>
1146+
</Link>
1147+
)
1148+
}
1149+
9961150
function MenuRail({
9971151
rail,
9981152
onNavigate,

0 commit comments

Comments
 (0)