Parcourir la source

feat modules 1, 2, 4 : modified navbar, page view for every menu, exclude sold out products from view all products - web, mobile

master
nadia il y a 17 heures
Parent
révision
d6a7bd13c2

+ 83
- 8
src/components/Navbar/Navbar.jsx Voir le fichier

1
-import { useState, useEffect, useRef } from "react";
1
+import { useState, useEffect, useRef, useCallback } from "react";
2
 import AppBar from "@mui/material/AppBar";
2
 import AppBar from "@mui/material/AppBar";
3
 import Toolbar from "@mui/material/Toolbar";
3
 import Toolbar from "@mui/material/Toolbar";
4
 import Button from "@mui/material/Button";
4
 import Button from "@mui/material/Button";
103
       {
103
       {
104
         label: "Traditional",
104
         label: "Traditional",
105
         children: [
105
         children: [
106
+          // label is the display name, titles are the collection names that will be matched to the product collections in shopify
106
           { label: "Eid's Time For Love", titles: ["EID'S TIME FOR LOVE"] },
107
           { label: "Eid's Time For Love", titles: ["EID'S TIME FOR LOVE"] },
107
           { label: "Raya Romantics", titles: ["RAYA ROMANTICS COLLECTION 2025"] },
108
           { label: "Raya Romantics", titles: ["RAYA ROMANTICS COLLECTION 2025"] },
108
           { label: "Atma Sari", titles: ["ATMA SARI"] },
109
           { label: "Atma Sari", titles: ["ATMA SARI"] },
200
 
201
 
201
   const swiperRef = useRef(null); // Create a ref for the Swiper instance
202
   const swiperRef = useRef(null); // Create a ref for the Swiper instance
202
   const childTransitionTimerRef = useRef(null);
203
   const childTransitionTimerRef = useRef(null);
204
+  const appBarRef = useRef(null);
205
+  const navMenuRefs = useRef({});
203
   const [showHeader, setShowHeader] = useState(true);
206
   const [showHeader, setShowHeader] = useState(true);
204
   const [showSearch, setShowSearch] = useState(false);
207
   const [showSearch, setShowSearch] = useState(false);
205
   const [isAtTop, setIsAtTop] = useState(false);
208
   const [isAtTop, setIsAtTop] = useState(false);
218
   const [navItemCompanyInfo, setNavItemCompanyInfo] = useState([]);
221
   const [navItemCompanyInfo, setNavItemCompanyInfo] = useState([]);
219
   const [activeMenu, setActiveMenu] = useState(null);
222
   const [activeMenu, setActiveMenu] = useState(null);
220
   const [activeGroup, setActiveGroup] = useState(null);
223
   const [activeGroup, setActiveGroup] = useState(null);
224
+  const [dropdownTopOffset, setDropdownTopOffset] = useState(52);
221
 
225
 
222
   const [displayCollection, setDisplayCollection] = useState({
226
   const [displayCollection, setDisplayCollection] = useState({
223
     productType: null,
227
     productType: null,
379
     setDisplayCollection([])
383
     setDisplayCollection([])
380
   }
384
   }
381
 
385
 
386
+  const updateDropdownTopOffset = useCallback((menuLabel = activeMenu) => {
387
+    const appBarNode = appBarRef.current;
388
+    const menuNode = navMenuRefs.current[menuLabel];
389
+
390
+    if (!appBarNode || !menuNode) return;
391
+
392
+    const appBarRect = appBarNode.getBoundingClientRect();
393
+    const menuRect = menuNode.getBoundingClientRect();
394
+
395
+    setDropdownTopOffset(Math.max(0, appBarRect.bottom - menuRect.top));
396
+  }, [activeMenu]);
397
+
398
+  useEffect(() => {
399
+    if (!activeMenu) return;
400
+
401
+    const updateOffset = () => {
402
+      requestAnimationFrame(() => updateDropdownTopOffset(activeMenu));
403
+    };
404
+
405
+    updateOffset();
406
+    window.addEventListener("scroll", updateOffset, { passive: true });
407
+    window.addEventListener("resize", updateOffset);
408
+
409
+    return () => {
410
+      window.removeEventListener("scroll", updateOffset);
411
+      window.removeEventListener("resize", updateOffset);
412
+    };
413
+  }, [activeMenu, showHeader, updateDropdownTopOffset]);
414
+
382
   const handleGroupMouseEnter = (groupLabel) => {
415
   const handleGroupMouseEnter = (groupLabel) => {
383
     if (activeGroup === groupLabel) return;
416
     if (activeGroup === groupLabel) return;
384
 
417
 
392
     }, 40);
425
     }, 40);
393
   }
426
   }
394
 
427
 
428
+  const findCollectionForMenuItem = (productType, item) => {
429
+    const targetTitles = (item?.titles || []).map(normalizeTitle);
430
+
431
+    return products
432
+      .filter((product) => product.productType === productType)
433
+      .flatMap((product) => product.collections || [])
434
+      .find((collection) => targetTitles.includes(normalizeTitle(collection?.title || "")));
435
+  }
436
+
437
+  const getMenuCollectionTitles = (productType) => {
438
+    const menu = NAV_MENU_STRUCTURE.find((menuItem) => menuItem.productType === productType);
439
+
440
+    if (!menu) return [];
441
+
442
+    if (menu.groups) {
443
+      return menu.groups.flatMap((group) =>
444
+        group.children.flatMap((item) => item.titles || [])
445
+      );
446
+    }
447
+
448
+    return (menu.children || []).flatMap((item) => item.titles || []);
449
+  }
450
+
395
   const navigateToMenuItem = (productType, item = null) => {
451
   const navigateToMenuItem = (productType, item = null) => {
396
     sessionStorage.setItem('amber-select-product-type', productType)
452
     sessionStorage.setItem('amber-select-product-type', productType)
397
     sessionStorage.removeItem('amber-select-collection')
453
     sessionStorage.removeItem('amber-select-collection')
398
     sessionStorage.removeItem('amber-select-collections')
454
     sessionStorage.removeItem('amber-select-collections')
399
 
455
 
400
     if (item) {
456
     if (item) {
457
+      const matchedCollection = findCollectionForMenuItem(productType, item);
458
+
401
       sessionStorage.setItem('amber-select-collection', JSON.stringify({
459
       sessionStorage.setItem('amber-select-collection', JSON.stringify({
402
         title: item.label,
460
         title: item.label,
403
-        image: null
461
+        image: matchedCollection?.image || null
404
       }))
462
       }))
405
       sessionStorage.setItem('amber-select-collections', JSON.stringify(item.titles || []))
463
       sessionStorage.setItem('amber-select-collections', JSON.stringify(item.titles || []))
464
+    } else {
465
+      // Main menu click only shows collections that exist in the current navbar menu.
466
+      const menuCollectionTitles = getMenuCollectionTitles(productType);
467
+
468
+      if (menuCollectionTitles.length > 0) {
469
+        sessionStorage.setItem('amber-select-collections', JSON.stringify(menuCollectionTitles))
470
+      }
406
     }
471
     }
407
 
472
 
408
     window.location.href = `/products`;
473
     window.location.href = `/products`;
411
   return (
476
   return (
412
     <>
477
     <>
413
       <AppBar position="fixed"
478
       <AppBar position="fixed"
479
+        ref={appBarRef}
414
         sx={{
480
         sx={{
415
           zIndex: 998,
481
           zIndex: 998,
416
           backgroundColor: (isAtTop) ? "rgba(0, 0, 0, 0)" : "rgba(255, 255, 255, 0.3)",
482
           backgroundColor: (isAtTop) ? "rgba(0, 0, 0, 0)" : "rgba(255, 255, 255, 0.3)",
515
               {NAV_MENU_STRUCTURE.map((menu) => (
581
               {NAV_MENU_STRUCTURE.map((menu) => (
516
                 <Box
582
                 <Box
517
                   key={menu.label}
583
                   key={menu.label}
584
+                  ref={(node) => {
585
+                    if (node) navMenuRefs.current[menu.label] = node;
586
+                  }}
518
                   sx={{ position: "relative" }}
587
                   sx={{ position: "relative" }}
519
                   onMouseEnter={() => {
588
                   onMouseEnter={() => {
520
                     if (childTransitionTimerRef.current) {
589
                     if (childTransitionTimerRef.current) {
521
                       clearTimeout(childTransitionTimerRef.current);
590
                       clearTimeout(childTransitionTimerRef.current);
522
                     }
591
                     }
592
+                    updateDropdownTopOffset(menu.label);
523
                     setActiveMenu(menu.label);
593
                     setActiveMenu(menu.label);
524
                     setActiveGroup(null);
594
                     setActiveGroup(null);
525
                   }}
595
                   }}
533
                     }}
603
                     }}
534
                     color="inherit"
604
                     color="inherit"
535
                     onClick={() => {
605
                     onClick={() => {
536
-                      if (!menu.groups && !menu.children) {
537
-                        navigateToMenuItem(menu.productType)
538
-                      }
606
+                      navigateToMenuItem(menu.productType)
539
                     }}
607
                     }}
540
                   >
608
                   >
609
+                    {/* Navbar menu label: APPAREL, ESSENTIALS */}
541
                     <Typography variant="body2"
610
                     <Typography variant="body2"
542
                       className="navItem"
611
                       className="navItem"
543
                       sx={{
612
                       sx={{
570
 
639
 
571
                   {(
640
                   {(
572
                     <>
641
                     <>
642
+                      {/* Invisible hover bridge: keeps dropdown open while moving cursor from menu to submenu */}
573
                       <Box
643
                       <Box
574
                         sx={{
644
                         sx={{
575
                           position: "absolute",
645
                           position: "absolute",
576
                           top: "100%",
646
                           top: "100%",
577
                           left: 0,
647
                           left: 0,
578
                           width: menu.groups ? (activeGroup ? 560 : 260) : 260,
648
                           width: menu.groups ? (activeGroup ? 560 : 260) : 260,
579
-                          height: 28,
649
+                          height: dropdownTopOffset,
580
                           zIndex: 1000,
650
                           zIndex: 1000,
581
                           backgroundColor: "transparent",
651
                           backgroundColor: "transparent",
582
                           visibility: activeMenu === menu.label ? "visible" : "hidden",
652
                           visibility: activeMenu === menu.label ? "visible" : "hidden",
583
                           pointerEvents: activeMenu === menu.label ? "auto" : "none",
653
                           pointerEvents: activeMenu === menu.label ? "auto" : "none",
584
                         }}
654
                         }}
585
                       />
655
                       />
656
+                      {/* Submenu dropdown card: controls dropdown position, width, background, and animation */}
586
                       <Box
657
                       <Box
587
                         sx={{
658
                         sx={{
588
                           position: "absolute",
659
                           position: "absolute",
589
-                          top: 60,
660
+                          // Measured from this menu label to the real bottom of the navbar.
661
+                          top: dropdownTopOffset,
590
                           left: 0,
662
                           left: 0,
591
                           width: menu.groups ? (activeGroup ? 560 : 260) : 260,
663
                           width: menu.groups ? (activeGroup ? 560 : 260) : 260,
592
                           backgroundColor: "#FFF",
664
                           backgroundColor: "#FFF",
607
                       >
679
                       >
608
                       {menu.groups ? (
680
                       {menu.groups ? (
609
                         <>
681
                         <>
682
+                          {/* Submenu group list: CASUAL, TRADITIONAL */}
610
                           <Box sx={{ width: 260, pb: 1.5, borderRight: "1px solid rgba(0,0,0,0.12)" }}>
683
                           <Box sx={{ width: 260, pb: 1.5, borderRight: "1px solid rgba(0,0,0,0.12)" }}>
611
                             {menu.groups.map((group) => (
684
                             {menu.groups.map((group) => (
612
                               <Button
685
                               <Button
632
                             ))}
705
                             ))}
633
                           </Box>
706
                           </Box>
634
 
707
 
708
+                          {/* Child submenu links: collection names shown after hovering a submenu group */}
635
                           {activeGroup && (
709
                           {activeGroup && (
636
                             <Box key={activeGroup} sx={{
710
                             <Box key={activeGroup} sx={{
637
                               pb: 1.5,
711
                               pb: 1.5,
677
                           )}
751
                           )}
678
                         </>
752
                         </>
679
                       ) : (
753
                       ) : (
754
+                        /* Direct submenu links: used by ESSENTIALS */
680
                         <Box sx={{ pb: 1.5, px: 3, width: 260 }}>
755
                         <Box sx={{ pb: 1.5, px: 3, width: 260 }}>
681
                           {menu.children.map((item) => (
756
                           {menu.children.map((item) => (
682
                             <Button
757
                             <Button
903
       </AppBar>
978
       </AppBar>
904
       {showSearch && <SearchProduct onClose={() => { setShowSearch(false) }} />}
979
       {showSearch && <SearchProduct onClose={() => { setShowSearch(false) }} />}
905
 
980
 
906
-      <MobileNav open={openSideMenu} menu={NAV_MENU_STRUCTURE} infomenu={navItemCompanyInfo} onClose={() => { setOpenSideMenu(false) }} />
981
+      <MobileNav open={openSideMenu} menu={NAV_MENU_STRUCTURE} products={products} infomenu={navItemCompanyInfo} onClose={() => { setOpenSideMenu(false) }} />
907
       <SideCart open={openSideCart} onClose={() => { setOpenSideCart(false) }} />
982
       <SideCart open={openSideCart} onClose={() => { setOpenSideCart(false) }} />
908
 
983
 
909
     </>
984
     </>

+ 39
- 3
src/components/Navbar/components/MobileNav/MobileNav.jsx Voir le fichier

9
   return productType.trim().toUpperCase() === "BEAUTY" ? "Essentials" : productType;
9
   return productType.trim().toUpperCase() === "BEAUTY" ? "Essentials" : productType;
10
 };
10
 };
11
 
11
 
12
-const MobileNav = ({ open, onClose, menu = [], infomenu = [] }) => {
12
+const normalizeTitle = (value = "") => value.trim().toUpperCase();
13
+
14
+const MobileNav = ({ open, onClose, menu = [], infomenu = [], products = [] }) => {
13
   const [expandedMenu, setExpandedMenu] = React.useState(null);
15
   const [expandedMenu, setExpandedMenu] = React.useState(null);
14
   const [expandedGroup, setExpandedGroup] = React.useState(null);
16
   const [expandedGroup, setExpandedGroup] = React.useState(null);
15
 
17
 
26
     onClose();
28
     onClose();
27
   }
29
   }
28
 
30
 
31
+  const findCollectionForMenuItem = (productType, item) => {
32
+    const targetTitles = (item?.titles || []).map(normalizeTitle);
33
+
34
+    return products
35
+      .filter((product) => product.productType === productType)
36
+      .flatMap((product) => product.collections || [])
37
+      .find((collection) => targetTitles.includes(normalizeTitle(collection?.title || "")));
38
+  }
39
+
40
+  const getMenuCollectionTitles = (productType) => {
41
+    const menuItem = menu.find((item) => item.productType === productType);
42
+
43
+    if (!menuItem) return [];
44
+
45
+    if (menuItem.groups) {
46
+      return menuItem.groups.flatMap((group) =>
47
+        group.children.flatMap((item) => item.titles || [])
48
+      );
49
+    }
50
+
51
+    return (menuItem.children || []).flatMap((item) => item.titles || []);
52
+  }
53
+
29
   const navigateToMenuItem = (productType, item = null) => {
54
   const navigateToMenuItem = (productType, item = null) => {
30
     sessionStorage.setItem('amber-select-product-type', productType)
55
     sessionStorage.setItem('amber-select-product-type', productType)
31
     sessionStorage.removeItem('amber-select-collection')
56
     sessionStorage.removeItem('amber-select-collection')
32
     sessionStorage.removeItem('amber-select-collections')
57
     sessionStorage.removeItem('amber-select-collections')
33
 
58
 
34
     if (item) {
59
     if (item) {
60
+      const matchedCollection = findCollectionForMenuItem(productType, item);
61
+
35
       sessionStorage.setItem('amber-select-collection', JSON.stringify({
62
       sessionStorage.setItem('amber-select-collection', JSON.stringify({
36
         title: item.label,
63
         title: item.label,
37
-        image: null
64
+        image: matchedCollection?.image || null
38
       }))
65
       }))
39
       sessionStorage.setItem('amber-select-collections', JSON.stringify(item.titles || []))
66
       sessionStorage.setItem('amber-select-collections', JSON.stringify(item.titles || []))
67
+    } else {
68
+      // Main menu click only shows collections that exist in the current mobile menu.
69
+      const menuCollectionTitles = getMenuCollectionTitles(productType);
70
+
71
+      if (menuCollectionTitles.length > 0) {
72
+        sessionStorage.setItem('amber-select-collections', JSON.stringify(menuCollectionTitles))
73
+      }
40
     }
74
     }
41
 
75
 
42
     window.location.href = `/products`;
76
     window.location.href = `/products`;
157
           return (
191
           return (
158
             <Box key={menuLabel}>
192
             <Box key={menuLabel}>
159
               <Box
193
               <Box
160
-                onClick={() => handleMenuToggle(menuLabel)}
161
                 sx={{
194
                 sx={{
162
                   display: "flex",
195
                   display: "flex",
163
                   alignItems: "center",
196
                   alignItems: "center",
170
               >
203
               >
171
                 <Typography
204
                 <Typography
172
                   variant="body1"
205
                   variant="body1"
206
+                  onClick={() => navigateToMenuItem(productType)}
173
                   sx={{
207
                   sx={{
174
                     color: "#4B2F34",
208
                     color: "#4B2F34",
175
                     fontSize: "1.05rem",
209
                     fontSize: "1.05rem",
176
                     letterSpacing: 0,
210
                     letterSpacing: 0,
177
                     fontWeight: "400",
211
                     fontWeight: "400",
212
+                    cursor: "pointer",
178
                   }}
213
                   }}
179
                 >
214
                 >
180
                   {menuLabel}
215
                   {menuLabel}
181
                 </Typography>
216
                 </Typography>
182
                 <Typography
217
                 <Typography
183
                   component="span"
218
                   component="span"
219
+                  onClick={() => handleMenuToggle(menuLabel)}
184
                   sx={{
220
                   sx={{
185
                     color: "#4B2F34",
221
                     color: "#4B2F34",
186
                     fontSize: "1.8rem",
222
                     fontSize: "1.8rem",

Chargement…
Annuler
Enregistrer