Просмотр исходного кода

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

master
nadia 19 часов назад
Родитель
Сommit
d6a7bd13c2
2 измененных файлов: 122 добавлений и 11 удалений
  1. 83
    8
      src/components/Navbar/Navbar.jsx
  2. 39
    3
      src/components/Navbar/components/MobileNav/MobileNav.jsx

+ 83
- 8
src/components/Navbar/Navbar.jsx Просмотреть файл

@@ -1,4 +1,4 @@
1
-import { useState, useEffect, useRef } from "react";
1
+import { useState, useEffect, useRef, useCallback } from "react";
2 2
 import AppBar from "@mui/material/AppBar";
3 3
 import Toolbar from "@mui/material/Toolbar";
4 4
 import Button from "@mui/material/Button";
@@ -103,6 +103,7 @@ const NAV_MENU_STRUCTURE = [
103 103
       {
104 104
         label: "Traditional",
105 105
         children: [
106
+          // label is the display name, titles are the collection names that will be matched to the product collections in shopify
106 107
           { label: "Eid's Time For Love", titles: ["EID'S TIME FOR LOVE"] },
107 108
           { label: "Raya Romantics", titles: ["RAYA ROMANTICS COLLECTION 2025"] },
108 109
           { label: "Atma Sari", titles: ["ATMA SARI"] },
@@ -200,6 +201,8 @@ const Navbar = () => {
200 201
 
201 202
   const swiperRef = useRef(null); // Create a ref for the Swiper instance
202 203
   const childTransitionTimerRef = useRef(null);
204
+  const appBarRef = useRef(null);
205
+  const navMenuRefs = useRef({});
203 206
   const [showHeader, setShowHeader] = useState(true);
204 207
   const [showSearch, setShowSearch] = useState(false);
205 208
   const [isAtTop, setIsAtTop] = useState(false);
@@ -218,6 +221,7 @@ const Navbar = () => {
218 221
   const [navItemCompanyInfo, setNavItemCompanyInfo] = useState([]);
219 222
   const [activeMenu, setActiveMenu] = useState(null);
220 223
   const [activeGroup, setActiveGroup] = useState(null);
224
+  const [dropdownTopOffset, setDropdownTopOffset] = useState(52);
221 225
 
222 226
   const [displayCollection, setDisplayCollection] = useState({
223 227
     productType: null,
@@ -379,6 +383,35 @@ const Navbar = () => {
379 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 415
   const handleGroupMouseEnter = (groupLabel) => {
383 416
     if (activeGroup === groupLabel) return;
384 417
 
@@ -392,17 +425,49 @@ const Navbar = () => {
392 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 451
   const navigateToMenuItem = (productType, item = null) => {
396 452
     sessionStorage.setItem('amber-select-product-type', productType)
397 453
     sessionStorage.removeItem('amber-select-collection')
398 454
     sessionStorage.removeItem('amber-select-collections')
399 455
 
400 456
     if (item) {
457
+      const matchedCollection = findCollectionForMenuItem(productType, item);
458
+
401 459
       sessionStorage.setItem('amber-select-collection', JSON.stringify({
402 460
         title: item.label,
403
-        image: null
461
+        image: matchedCollection?.image || null
404 462
       }))
405 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 473
     window.location.href = `/products`;
@@ -411,6 +476,7 @@ const Navbar = () => {
411 476
   return (
412 477
     <>
413 478
       <AppBar position="fixed"
479
+        ref={appBarRef}
414 480
         sx={{
415 481
           zIndex: 998,
416 482
           backgroundColor: (isAtTop) ? "rgba(0, 0, 0, 0)" : "rgba(255, 255, 255, 0.3)",
@@ -515,11 +581,15 @@ const Navbar = () => {
515 581
               {NAV_MENU_STRUCTURE.map((menu) => (
516 582
                 <Box
517 583
                   key={menu.label}
584
+                  ref={(node) => {
585
+                    if (node) navMenuRefs.current[menu.label] = node;
586
+                  }}
518 587
                   sx={{ position: "relative" }}
519 588
                   onMouseEnter={() => {
520 589
                     if (childTransitionTimerRef.current) {
521 590
                       clearTimeout(childTransitionTimerRef.current);
522 591
                     }
592
+                    updateDropdownTopOffset(menu.label);
523 593
                     setActiveMenu(menu.label);
524 594
                     setActiveGroup(null);
525 595
                   }}
@@ -533,11 +603,10 @@ const Navbar = () => {
533 603
                     }}
534 604
                     color="inherit"
535 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 610
                     <Typography variant="body2"
542 611
                       className="navItem"
543 612
                       sx={{
@@ -570,23 +639,26 @@ const Navbar = () => {
570 639
 
571 640
                   {(
572 641
                     <>
642
+                      {/* Invisible hover bridge: keeps dropdown open while moving cursor from menu to submenu */}
573 643
                       <Box
574 644
                         sx={{
575 645
                           position: "absolute",
576 646
                           top: "100%",
577 647
                           left: 0,
578 648
                           width: menu.groups ? (activeGroup ? 560 : 260) : 260,
579
-                          height: 28,
649
+                          height: dropdownTopOffset,
580 650
                           zIndex: 1000,
581 651
                           backgroundColor: "transparent",
582 652
                           visibility: activeMenu === menu.label ? "visible" : "hidden",
583 653
                           pointerEvents: activeMenu === menu.label ? "auto" : "none",
584 654
                         }}
585 655
                       />
656
+                      {/* Submenu dropdown card: controls dropdown position, width, background, and animation */}
586 657
                       <Box
587 658
                         sx={{
588 659
                           position: "absolute",
589
-                          top: 60,
660
+                          // Measured from this menu label to the real bottom of the navbar.
661
+                          top: dropdownTopOffset,
590 662
                           left: 0,
591 663
                           width: menu.groups ? (activeGroup ? 560 : 260) : 260,
592 664
                           backgroundColor: "#FFF",
@@ -607,6 +679,7 @@ const Navbar = () => {
607 679
                       >
608 680
                       {menu.groups ? (
609 681
                         <>
682
+                          {/* Submenu group list: CASUAL, TRADITIONAL */}
610 683
                           <Box sx={{ width: 260, pb: 1.5, borderRight: "1px solid rgba(0,0,0,0.12)" }}>
611 684
                             {menu.groups.map((group) => (
612 685
                               <Button
@@ -632,6 +705,7 @@ const Navbar = () => {
632 705
                             ))}
633 706
                           </Box>
634 707
 
708
+                          {/* Child submenu links: collection names shown after hovering a submenu group */}
635 709
                           {activeGroup && (
636 710
                             <Box key={activeGroup} sx={{
637 711
                               pb: 1.5,
@@ -677,6 +751,7 @@ const Navbar = () => {
677 751
                           )}
678 752
                         </>
679 753
                       ) : (
754
+                        /* Direct submenu links: used by ESSENTIALS */
680 755
                         <Box sx={{ pb: 1.5, px: 3, width: 260 }}>
681 756
                           {menu.children.map((item) => (
682 757
                             <Button
@@ -903,7 +978,7 @@ const Navbar = () => {
903 978
       </AppBar>
904 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 982
       <SideCart open={openSideCart} onClose={() => { setOpenSideCart(false) }} />
908 983
 
909 984
     </>

+ 39
- 3
src/components/Navbar/components/MobileNav/MobileNav.jsx Просмотреть файл

@@ -9,7 +9,9 @@ const getProductTypeMenuLabel = (productType = "") => {
9 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 15
   const [expandedMenu, setExpandedMenu] = React.useState(null);
14 16
   const [expandedGroup, setExpandedGroup] = React.useState(null);
15 17
 
@@ -26,17 +28,49 @@ const MobileNav = ({ open, onClose, menu = [], infomenu = [] }) => {
26 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 54
   const navigateToMenuItem = (productType, item = null) => {
30 55
     sessionStorage.setItem('amber-select-product-type', productType)
31 56
     sessionStorage.removeItem('amber-select-collection')
32 57
     sessionStorage.removeItem('amber-select-collections')
33 58
 
34 59
     if (item) {
60
+      const matchedCollection = findCollectionForMenuItem(productType, item);
61
+
35 62
       sessionStorage.setItem('amber-select-collection', JSON.stringify({
36 63
         title: item.label,
37
-        image: null
64
+        image: matchedCollection?.image || null
38 65
       }))
39 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 76
     window.location.href = `/products`;
@@ -157,7 +191,6 @@ const MobileNav = ({ open, onClose, menu = [], infomenu = [] }) => {
157 191
           return (
158 192
             <Box key={menuLabel}>
159 193
               <Box
160
-                onClick={() => handleMenuToggle(menuLabel)}
161 194
                 sx={{
162 195
                   display: "flex",
163 196
                   alignItems: "center",
@@ -170,17 +203,20 @@ const MobileNav = ({ open, onClose, menu = [], infomenu = [] }) => {
170 203
               >
171 204
                 <Typography
172 205
                   variant="body1"
206
+                  onClick={() => navigateToMenuItem(productType)}
173 207
                   sx={{
174 208
                     color: "#4B2F34",
175 209
                     fontSize: "1.05rem",
176 210
                     letterSpacing: 0,
177 211
                     fontWeight: "400",
212
+                    cursor: "pointer",
178 213
                   }}
179 214
                 >
180 215
                   {menuLabel}
181 216
                 </Typography>
182 217
                 <Typography
183 218
                   component="span"
219
+                  onClick={() => handleMenuToggle(menuLabel)}
184 220
                   sx={{
185 221
                     color: "#4B2F34",
186 222
                     fontSize: "1.8rem",

Загрузка…
Отмена
Сохранить