@@ -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'
6262import { 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
6467type 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+
9961150function MenuRail ( {
9971151 rail,
9981152 onNavigate,
0 commit comments