@notionhive/menus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/menus.js +16 -0
- package/category.config.json +7 -0
- package/package.json +24 -0
- package/registry/index.json +74 -0
- package/registry/menu-01.json +14 -0
- package/registry/menu-02.json +12 -0
- package/registry/menu-03.json +12 -0
- package/registry/menu-04.json +10 -0
- package/registry/menu-05.json +10 -0
- package/registry/menu-06.json +10 -0
- package/templates/components/atoms/SafeImage/SafeImage.jsx +101 -0
- package/templates/components/atoms/SafeImage/index.js +1 -0
- package/templates/components/hooks/useCarousel.js +73 -0
- package/templates/components/molecules/CorporateHeader/CorporateHeader.jsx +174 -0
- package/templates/components/molecules/CorporateHeader/CorporateHeader.propTypes.js +22 -0
- package/templates/components/molecules/CorporateHeader/index.js +1 -0
- package/templates/components/molecules/EquipmentMegaMenu/EquipmentMegaMenu.jsx +202 -0
- package/templates/components/molecules/EquipmentMegaMenu/EquipmentMegaMenu.propTypes.js +104 -0
- package/templates/components/molecules/EquipmentMegaMenu/index.js +5 -0
- package/templates/components/molecules/FullScreenNavOverlay/FullScreenNavOverlay.jsx +117 -0
- package/templates/components/molecules/FullScreenNavOverlay/FullScreenNavOverlay.propTypes.js +34 -0
- package/templates/components/molecules/FullScreenNavOverlay/index.js +2 -0
- package/templates/components/molecules/MobileNavDrawer/MobileNavDrawer.jsx +139 -0
- package/templates/components/molecules/MobileNavDrawer/index.js +2 -0
- package/templates/components/molecules/SiteHeader/SiteHeader.jsx +177 -0
- package/templates/components/molecules/SiteHeader/SiteHeader.propTypes.js +20 -0
- package/templates/components/molecules/SiteHeader/index.js +1 -0
- package/templates/components/molecules/SlideIndicators/SlideIndicators.jsx +75 -0
- package/templates/components/molecules/SlideIndicators/SlideIndicators.propTypes.js +10 -0
- package/templates/components/molecules/SlideIndicators/index.js +1 -0
- package/templates/components/organisms/Menu01/Menu01.jsx +211 -0
- package/templates/components/organisms/Menu01/Menu01.propTypes.js +63 -0
- package/templates/components/organisms/Menu01/index.js +1 -0
- package/templates/components/organisms/Menu02/Menu02.jsx +133 -0
- package/templates/components/organisms/Menu02/Menu02.propTypes.js +71 -0
- package/templates/components/organisms/Menu02/index.js +1 -0
- package/templates/components/organisms/Menu03/Menu03.jsx +411 -0
- package/templates/components/organisms/Menu03/Menu03.propTypes.js +113 -0
- package/templates/components/organisms/Menu03/index.js +1 -0
- package/templates/components/organisms/Menu04/Menu04.jsx +232 -0
- package/templates/components/organisms/Menu04/Menu04.propTypes.js +53 -0
- package/templates/components/organisms/Menu04/index.js +2 -0
- package/templates/components/organisms/Menu05/Menu05.jsx +318 -0
- package/templates/components/organisms/Menu05/Menu05.propTypes.js +79 -0
- package/templates/components/organisms/Menu05/index.js +2 -0
- package/templates/components/organisms/Menu06/Menu06.jsx +241 -0
- package/templates/components/organisms/Menu06/Menu06.propTypes.js +55 -0
- package/templates/components/organisms/Menu06/index.js +2 -0
- package/templates/public/menus/menu01/agriculture.png +0 -0
- package/templates/public/menus/menu01/banner.jpg +0 -0
- package/templates/public/menus/menu01/build-your-own.png +0 -0
- package/templates/public/menus/menu01/construction.png +0 -0
- package/templates/public/menus/menu01/lawn-care.png +0 -0
- package/templates/public/menus/menu01/logo.png +0 -0
- package/templates/public/menus/menu01/promotions.png +0 -0
- package/templates/public/menus/menu03/logo.png +46 -0
- package/templates/public/menus/menu04/featured.jpg +0 -0
- package/templates/public/menus/menu04/logo.png +0 -0
- package/templates/public/menus/menu05/hero-bg.jpg +0 -0
- package/templates/public/menus/menu05/logo.png +0 -0
- package/templates/public/menus/menu06/pool.jpg +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
5
|
+
import { CorporateHeader } from "../../molecules/CorporateHeader";
|
|
6
|
+
import { SlideIndicators } from "../../molecules/SlideIndicators";
|
|
7
|
+
import { MobileNavDrawer } from "../../molecules/MobileNavDrawer";
|
|
8
|
+
import { menu02DefaultProps, menu02PropTypes } from "./Menu02.propTypes";
|
|
9
|
+
|
|
10
|
+
function ServiceCard({ card }) {
|
|
11
|
+
const className =
|
|
12
|
+
"flex min-w-[280px] shrink-0 flex-col gap-2 rounded-sm border border-white/10 bg-white/5 px-5 py-4 backdrop-blur-sm transition-colors duration-200 ease-out hover:bg-white/10 sm:min-w-[320px] sm:px-6 sm:py-5 xl:min-w-0";
|
|
13
|
+
|
|
14
|
+
const content = (
|
|
15
|
+
<>
|
|
16
|
+
<p className="text-base font-bold leading-snug text-white sm:text-lg">{card.title}</p>
|
|
17
|
+
<p className="text-sm leading-relaxed text-[#f7f8fa]/90 sm:text-base">{card.description}</p>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (card.href) {
|
|
22
|
+
return (
|
|
23
|
+
<a href={card.href} className={className}>
|
|
24
|
+
{content}
|
|
25
|
+
</a>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<button type="button" onClick={card.onClick} className={`text-left ${className}`}>
|
|
31
|
+
{content}
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Menu02 — Brac EPL investment hero with horizontal scrolling service cards.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} props - See Menu02.propTypes.js for full prop list.
|
|
40
|
+
*/
|
|
41
|
+
export function Menu02({
|
|
42
|
+
logoSrc = menu02DefaultProps.logoSrc,
|
|
43
|
+
logoAlt = menu02DefaultProps.logoAlt,
|
|
44
|
+
navItems = menu02DefaultProps.navItems,
|
|
45
|
+
secondaryCtaText = menu02DefaultProps.secondaryCtaText,
|
|
46
|
+
primaryCtaText = menu02DefaultProps.primaryCtaText,
|
|
47
|
+
onSecondaryClick,
|
|
48
|
+
onPrimaryClick,
|
|
49
|
+
onMenuClick,
|
|
50
|
+
headline = menu02DefaultProps.headline,
|
|
51
|
+
serviceCards = menu02DefaultProps.serviceCards,
|
|
52
|
+
slideCount = menu02DefaultProps.slideCount,
|
|
53
|
+
activeSlide: activeSlideProp = menu02DefaultProps.activeSlide,
|
|
54
|
+
onSlideChange,
|
|
55
|
+
className = "",
|
|
56
|
+
}) {
|
|
57
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
58
|
+
|
|
59
|
+
const { activeSlide, goTo, pause, resume } = useCarousel({
|
|
60
|
+
count: slideCount,
|
|
61
|
+
initialIndex: activeSlideProp,
|
|
62
|
+
autoPlayMs: 5000,
|
|
63
|
+
loop: true,
|
|
64
|
+
onChange: onSlideChange,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
goTo(activeSlideProp);
|
|
69
|
+
}, [activeSlideProp, goTo]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<section
|
|
73
|
+
className={["relative flex w-full flex-col", className].filter(Boolean).join(" ")}
|
|
74
|
+
data-menu="menu02"
|
|
75
|
+
>
|
|
76
|
+
<CorporateHeader
|
|
77
|
+
logoSrc={logoSrc}
|
|
78
|
+
logoAlt={logoAlt}
|
|
79
|
+
navItems={navItems}
|
|
80
|
+
secondaryCtaText={secondaryCtaText}
|
|
81
|
+
primaryCtaText={primaryCtaText}
|
|
82
|
+
onSecondaryClick={onSecondaryClick}
|
|
83
|
+
onPrimaryClick={onPrimaryClick}
|
|
84
|
+
onMenuClick={() => {
|
|
85
|
+
setMenuOpen(true);
|
|
86
|
+
onMenuClick?.();
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<MobileNavDrawer
|
|
91
|
+
isOpen={menuOpen}
|
|
92
|
+
onClose={() => setMenuOpen(false)}
|
|
93
|
+
navItems={navItems}
|
|
94
|
+
accentColor="#0058b5"
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<div
|
|
98
|
+
className="relative min-h-[60vh] w-full overflow-hidden bg-[#0a1628] md:min-h-[700px] lg:min-h-[906px]"
|
|
99
|
+
onMouseEnter={pause}
|
|
100
|
+
onMouseLeave={resume}
|
|
101
|
+
>
|
|
102
|
+
<div className="absolute inset-0 bg-gradient-to-b from-[#010623]/20 via-[#010623]/60 to-[#010623]" />
|
|
103
|
+
|
|
104
|
+
<div className="absolute inset-x-0 bottom-0 top-[28%] flex flex-col justify-end gap-6 px-4 pb-8 sm:gap-8 sm:px-6 sm:pb-10 md:px-10 lg:gap-10 lg:px-14 lg:pb-12">
|
|
105
|
+
<h1 className="max-w-[1123px] font-serif text-3xl font-extralight leading-tight tracking-tight text-[#fbfbfb] sm:text-4xl md:text-5xl lg:text-[72px] lg:leading-[1.05] xl:text-[104px] xl:leading-[108px] xl:tracking-[-2.08px]">
|
|
106
|
+
{headline}
|
|
107
|
+
</h1>
|
|
108
|
+
|
|
109
|
+
<div className="flex w-full flex-col gap-5 sm:gap-6">
|
|
110
|
+
<div className="-mx-4 overflow-x-auto px-4 pb-2 sm:-mx-6 sm:px-6 md:-mx-10 md:px-10 lg:mx-0 lg:overflow-visible lg:px-0">
|
|
111
|
+
<div className="flex gap-4 xl:grid xl:grid-cols-4 xl:gap-4">
|
|
112
|
+
{serviceCards.map((card) => (
|
|
113
|
+
<ServiceCard key={card.title} card={card} />
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<SlideIndicators
|
|
119
|
+
count={slideCount}
|
|
120
|
+
activeIndex={activeSlide}
|
|
121
|
+
onSelect={goTo}
|
|
122
|
+
variant="pill"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Menu02.propTypes = menu02PropTypes;
|
|
132
|
+
|
|
133
|
+
export default Menu02;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
|
|
3
|
+
const navItemShape = PropTypes.shape({
|
|
4
|
+
label: PropTypes.string.isRequired,
|
|
5
|
+
href: PropTypes.string,
|
|
6
|
+
onClick: PropTypes.func,
|
|
7
|
+
hasDropdown: PropTypes.bool,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const serviceCardShape = PropTypes.shape({
|
|
11
|
+
title: PropTypes.string.isRequired,
|
|
12
|
+
description: PropTypes.string.isRequired,
|
|
13
|
+
href: PropTypes.string,
|
|
14
|
+
onClick: PropTypes.func,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const menu02PropTypes = {
|
|
18
|
+
logoSrc: PropTypes.string,
|
|
19
|
+
logoAlt: PropTypes.string,
|
|
20
|
+
navItems: PropTypes.arrayOf(navItemShape),
|
|
21
|
+
secondaryCtaText: PropTypes.string,
|
|
22
|
+
primaryCtaText: PropTypes.string,
|
|
23
|
+
onSecondaryClick: PropTypes.func,
|
|
24
|
+
onPrimaryClick: PropTypes.func,
|
|
25
|
+
onMenuClick: PropTypes.func,
|
|
26
|
+
headline: PropTypes.string,
|
|
27
|
+
serviceCards: PropTypes.arrayOf(serviceCardShape),
|
|
28
|
+
slideCount: PropTypes.number,
|
|
29
|
+
activeSlide: PropTypes.number,
|
|
30
|
+
onSlideChange: PropTypes.func,
|
|
31
|
+
className: PropTypes.string,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const menu02DefaultProps = {
|
|
35
|
+
logoSrc: "/heroes/hero04/logo.png",
|
|
36
|
+
logoAlt: "BRAC EPL Stock Brokerage",
|
|
37
|
+
navItems: [
|
|
38
|
+
{ label: "About us", href: "#", hasDropdown: true },
|
|
39
|
+
{ label: "Services", href: "#", hasDropdown: true },
|
|
40
|
+
{ label: "Research", href: "#", hasDropdown: true },
|
|
41
|
+
{ label: "Foreign Invest", href: "#", hasDropdown: true },
|
|
42
|
+
{ label: "Knowledge Center", href: "#", hasDropdown: true },
|
|
43
|
+
],
|
|
44
|
+
secondaryCtaText: "BRAC EPL Trade",
|
|
45
|
+
primaryCtaText: "Let's Get Started",
|
|
46
|
+
headline: "Your gateway to smart investments in Bangladesh.",
|
|
47
|
+
serviceCards: [
|
|
48
|
+
{
|
|
49
|
+
title: "Brac EPL Trade",
|
|
50
|
+
description: "Trade easily and grow your investments.",
|
|
51
|
+
href: "#",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: "Open An Account",
|
|
55
|
+
description: "Start your investment journey today.",
|
|
56
|
+
href: "#",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: "Equity Trading",
|
|
60
|
+
description: "Maximizing returns with strategic investments.",
|
|
61
|
+
href: "#",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: "Full DP Service",
|
|
65
|
+
description: "Reliable depository services for protected investments.",
|
|
66
|
+
href: "#",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
slideCount: 3,
|
|
70
|
+
activeSlide: 0,
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Menu02, default } from "./Menu02";
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
5
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
6
|
+
import { SlideIndicators } from "../../molecules/SlideIndicators";
|
|
7
|
+
import { MobileNavDrawer } from "../../molecules/MobileNavDrawer";
|
|
8
|
+
import { menu03DefaultProps, menu03PropTypes } from "./Menu03.propTypes";
|
|
9
|
+
|
|
10
|
+
function ChevronDownIcon({ className = "text-[#0e274e]" }) {
|
|
11
|
+
return (
|
|
12
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true" className={className}>
|
|
13
|
+
<path
|
|
14
|
+
d="M3.5 5.25L7 8.75L10.5 5.25"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
strokeWidth="1.2"
|
|
17
|
+
strokeLinecap="round"
|
|
18
|
+
strokeLinejoin="round"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function SearchIcon() {
|
|
25
|
+
return (
|
|
26
|
+
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" aria-hidden="true">
|
|
27
|
+
<circle cx="18" cy="18" r="10" stroke="#0E274E" strokeWidth="1.5" />
|
|
28
|
+
<path d="M25 25L33 33" stroke="#0E274E" strokeWidth="1.5" strokeLinecap="round" />
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function MenuIcon() {
|
|
34
|
+
return (
|
|
35
|
+
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden="true">
|
|
36
|
+
<path d="M4 7H24M4 14H24M4 21H16" stroke="#0E274E" strokeWidth="1.5" strokeLinecap="round" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function PauseIcon() {
|
|
42
|
+
return (
|
|
43
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
44
|
+
<rect x="6" y="5" width="4" height="14" rx="1" fill="white" />
|
|
45
|
+
<rect x="14" y="5" width="4" height="14" rx="1" fill="white" />
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function PlayIcon() {
|
|
51
|
+
return (
|
|
52
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
53
|
+
<path d="M8 5L19 12L8 19V5Z" fill="white" />
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function UtilityLink({ link }) {
|
|
59
|
+
const className = [
|
|
60
|
+
"text-sm font-semibold leading-[22px] transition-opacity duration-200 ease-out hover:opacity-80",
|
|
61
|
+
link.highlighted ? "text-[#2a5caa]" : "text-[#1a2942]/80",
|
|
62
|
+
].join(" ");
|
|
63
|
+
|
|
64
|
+
const content = (
|
|
65
|
+
<>
|
|
66
|
+
{link.label}
|
|
67
|
+
{link.hasDropdown ? (
|
|
68
|
+
<ChevronDownIcon className={link.highlighted ? "text-[#2a5caa]" : "text-[#1a2942]/80"} />
|
|
69
|
+
) : null}
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (link.href) {
|
|
74
|
+
return (
|
|
75
|
+
<a href={link.href} className={`inline-flex items-center gap-1 ${className}`}>
|
|
76
|
+
{content}
|
|
77
|
+
</a>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<button type="button" onClick={link.onClick} className={`inline-flex items-center gap-1 ${className}`}>
|
|
83
|
+
{content}
|
|
84
|
+
</button>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function AboutMenuLink({ link }) {
|
|
89
|
+
const isPrimary = link.variant !== "secondary";
|
|
90
|
+
const className = isPrimary
|
|
91
|
+
? "block w-full text-left text-base font-semibold leading-[26px] text-[#0e274e] transition-opacity duration-200 ease-out hover:opacity-80"
|
|
92
|
+
: "block w-full text-left text-sm leading-[22px] text-[#0e274e] transition-opacity duration-200 ease-out hover:opacity-80";
|
|
93
|
+
|
|
94
|
+
if (link.href) {
|
|
95
|
+
return (
|
|
96
|
+
<a href={link.href} className={className}>
|
|
97
|
+
{link.label}
|
|
98
|
+
</a>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<button type="button" onClick={link.onClick} className={className}>
|
|
104
|
+
{link.label}
|
|
105
|
+
</button>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function AboutMegaMenu({
|
|
110
|
+
isOpen,
|
|
111
|
+
onClose,
|
|
112
|
+
primaryLinks,
|
|
113
|
+
secondaryHeading,
|
|
114
|
+
secondaryLinks,
|
|
115
|
+
}) {
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!isOpen) return undefined;
|
|
118
|
+
const handler = (event) => {
|
|
119
|
+
if (event.key === "Escape") onClose?.();
|
|
120
|
+
};
|
|
121
|
+
window.addEventListener("keydown", handler);
|
|
122
|
+
return () => window.removeEventListener("keydown", handler);
|
|
123
|
+
}, [isOpen, onClose]);
|
|
124
|
+
|
|
125
|
+
if (!isOpen) return null;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
aria-label="Close About menu"
|
|
132
|
+
onClick={onClose}
|
|
133
|
+
className="fixed inset-0 z-30 hidden bg-black/10 lg:block"
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
<div
|
|
137
|
+
className="absolute left-1/2 top-full z-40 hidden w-max max-w-[calc(100%-2rem)] -translate-x-1/2 rounded-b-lg border-t-2 border-[#2a5caa] bg-white px-8 py-8 shadow-[0_8px_30px_rgba(14,39,78,0.06)] sm:px-12 sm:py-10 lg:flex lg:gap-5"
|
|
138
|
+
role="dialog"
|
|
139
|
+
aria-modal="true"
|
|
140
|
+
aria-label="About menu"
|
|
141
|
+
>
|
|
142
|
+
<div className="flex w-[240px] flex-col gap-4 sm:w-[296px] sm:gap-4">
|
|
143
|
+
{primaryLinks.map((link) => (
|
|
144
|
+
<AboutMenuLink key={link.label} link={link} />
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="flex w-[240px] flex-col gap-3 sm:w-[296px] sm:gap-5">
|
|
149
|
+
<p className="text-base font-semibold leading-[26px] text-[#0e274e]">{secondaryHeading}</p>
|
|
150
|
+
<div className="flex flex-col gap-3">
|
|
151
|
+
{secondaryLinks.map((link) => (
|
|
152
|
+
<AboutMenuLink key={link.label} link={link} />
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Menu03 — BRAC University header, hero slider, and About mega menu dropdown.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} props - See Menu03.propTypes.js for full prop list.
|
|
165
|
+
*/
|
|
166
|
+
export function Menu03({
|
|
167
|
+
logoSrc = menu03DefaultProps.logoSrc,
|
|
168
|
+
logoAlt = menu03DefaultProps.logoAlt,
|
|
169
|
+
utilityLinks = menu03DefaultProps.utilityLinks,
|
|
170
|
+
navItems = menu03DefaultProps.navItems,
|
|
171
|
+
onSearchClick,
|
|
172
|
+
onMenuClick,
|
|
173
|
+
aboutMenuOpen: aboutMenuOpenProp,
|
|
174
|
+
defaultAboutMenuOpen = menu03DefaultProps.defaultAboutMenuOpen,
|
|
175
|
+
onAboutMenuChange,
|
|
176
|
+
aboutPrimaryLinks = menu03DefaultProps.aboutPrimaryLinks,
|
|
177
|
+
aboutSecondaryHeading = menu03DefaultProps.aboutSecondaryHeading,
|
|
178
|
+
aboutSecondaryLinks = menu03DefaultProps.aboutSecondaryLinks,
|
|
179
|
+
slides = menu03DefaultProps.slides,
|
|
180
|
+
slideCount = menu03DefaultProps.slideCount,
|
|
181
|
+
activeSlide: activeSlideProp = menu03DefaultProps.activeSlide,
|
|
182
|
+
defaultPlaying = menu03DefaultProps.defaultPlaying,
|
|
183
|
+
onPlayPauseClick,
|
|
184
|
+
onSlideChange,
|
|
185
|
+
className = "",
|
|
186
|
+
}) {
|
|
187
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
188
|
+
const [internalAboutOpen, setInternalAboutOpen] = useState(defaultAboutMenuOpen);
|
|
189
|
+
const [isPlaying, setIsPlaying] = useState(defaultPlaying);
|
|
190
|
+
|
|
191
|
+
const isAboutControlled = aboutMenuOpenProp !== undefined;
|
|
192
|
+
const aboutMenuOpen = isAboutControlled ? aboutMenuOpenProp : internalAboutOpen;
|
|
193
|
+
|
|
194
|
+
const setAboutMenuOpen = useCallback(
|
|
195
|
+
(next) => {
|
|
196
|
+
const value = typeof next === "function" ? next(aboutMenuOpen) : next;
|
|
197
|
+
if (!isAboutControlled) setInternalAboutOpen(value);
|
|
198
|
+
onAboutMenuChange?.(value);
|
|
199
|
+
},
|
|
200
|
+
[aboutMenuOpen, isAboutControlled, onAboutMenuChange]
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const toggleAboutMenu = useCallback(() => {
|
|
204
|
+
setAboutMenuOpen((open) => !open);
|
|
205
|
+
}, [setAboutMenuOpen]);
|
|
206
|
+
|
|
207
|
+
const headerNavItems = useMemo(
|
|
208
|
+
() =>
|
|
209
|
+
navItems.map((item) => {
|
|
210
|
+
if (item.label === "About") {
|
|
211
|
+
return {
|
|
212
|
+
...item,
|
|
213
|
+
active: aboutMenuOpen,
|
|
214
|
+
onClick: () => {
|
|
215
|
+
item.onClick?.();
|
|
216
|
+
toggleAboutMenu();
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return item;
|
|
221
|
+
}),
|
|
222
|
+
[navItems, aboutMenuOpen, toggleAboutMenu]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const drawerNavItems = useMemo(
|
|
226
|
+
() => [
|
|
227
|
+
...aboutPrimaryLinks.map((link) => ({ label: link.label, href: link.href, onClick: link.onClick })),
|
|
228
|
+
{ label: aboutSecondaryHeading, href: "#" },
|
|
229
|
+
...aboutSecondaryLinks.map((link) => ({ label: link.label, href: link.href, onClick: link.onClick })),
|
|
230
|
+
...navItems.filter((item) => item.label !== "About"),
|
|
231
|
+
],
|
|
232
|
+
[aboutPrimaryLinks, aboutSecondaryHeading, aboutSecondaryLinks, navItems]
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const { activeSlide, goTo, pause, resume } = useCarousel({
|
|
236
|
+
count: slideCount,
|
|
237
|
+
initialIndex: activeSlideProp,
|
|
238
|
+
autoPlayMs: isPlaying ? 6000 : 0,
|
|
239
|
+
loop: true,
|
|
240
|
+
onChange: onSlideChange,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
goTo(activeSlideProp);
|
|
245
|
+
}, [activeSlideProp, goTo]);
|
|
246
|
+
|
|
247
|
+
const currentSlide = slides[activeSlide] || slides[0];
|
|
248
|
+
|
|
249
|
+
const handlePlayPause = () => {
|
|
250
|
+
setIsPlaying((playing) => !playing);
|
|
251
|
+
onPlayPauseClick?.();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<section
|
|
256
|
+
className={["relative flex w-full flex-col", className].filter(Boolean).join(" ")}
|
|
257
|
+
data-menu="menu03"
|
|
258
|
+
onMouseEnter={pause}
|
|
259
|
+
onMouseLeave={resume}
|
|
260
|
+
>
|
|
261
|
+
<header className="relative z-20 w-full shrink-0 bg-white px-4 py-4 sm:px-6 md:px-8 lg:h-[106px] lg:px-8 lg:py-0">
|
|
262
|
+
<div className="flex flex-wrap items-center justify-end gap-2 sm:gap-4 lg:absolute lg:right-8 lg:top-[18px]">
|
|
263
|
+
{utilityLinks.map((link, index) => (
|
|
264
|
+
<span key={link.label} className="flex items-center gap-2 sm:gap-4">
|
|
265
|
+
{index > 0 ? (
|
|
266
|
+
<span className="hidden h-2 w-px bg-[#f2f2f2] sm:block" aria-hidden="true" />
|
|
267
|
+
) : null}
|
|
268
|
+
<UtilityLink link={link} />
|
|
269
|
+
</span>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div className="mt-4 flex items-center justify-between gap-4 lg:absolute lg:bottom-[7px] lg:right-8 lg:mt-0 lg:justify-end lg:gap-7">
|
|
274
|
+
<nav aria-label="Primary" className="hidden overflow-x-auto lg:block">
|
|
275
|
+
<ul className="flex items-center gap-4 xl:gap-7">
|
|
276
|
+
{headerNavItems.map((item) => {
|
|
277
|
+
const itemClass = [
|
|
278
|
+
"inline-flex items-center gap-1 text-base font-semibold leading-[26px] transition-opacity duration-200 ease-out hover:opacity-80",
|
|
279
|
+
item.active ? "text-[#2a5caa]" : "text-[#0e274e]",
|
|
280
|
+
].join(" ");
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<li key={item.label} className="shrink-0">
|
|
284
|
+
{item.href && !item.onClick ? (
|
|
285
|
+
<a href={item.href} className={itemClass}>
|
|
286
|
+
{item.label}
|
|
287
|
+
{item.hasDropdown ? (
|
|
288
|
+
<ChevronDownIcon className={item.active ? "text-[#2a5caa]" : "text-[#0e274e]"} />
|
|
289
|
+
) : null}
|
|
290
|
+
</a>
|
|
291
|
+
) : (
|
|
292
|
+
<button type="button" onClick={item.onClick} className={itemClass}>
|
|
293
|
+
{item.label}
|
|
294
|
+
{item.hasDropdown ? (
|
|
295
|
+
<ChevronDownIcon className={item.active ? "text-[#2a5caa]" : "text-[#0e274e]"} />
|
|
296
|
+
) : null}
|
|
297
|
+
</button>
|
|
298
|
+
)}
|
|
299
|
+
</li>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
</ul>
|
|
303
|
+
</nav>
|
|
304
|
+
|
|
305
|
+
<div className="flex items-center gap-3">
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
onClick={onSearchClick}
|
|
309
|
+
aria-label="Search"
|
|
310
|
+
className="transition-opacity duration-200 ease-out hover:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
311
|
+
>
|
|
312
|
+
<SearchIcon />
|
|
313
|
+
</button>
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
onClick={() => {
|
|
317
|
+
setMobileMenuOpen(true);
|
|
318
|
+
onMenuClick?.();
|
|
319
|
+
}}
|
|
320
|
+
aria-label="Open menu"
|
|
321
|
+
className="flex size-10 items-center justify-center transition-opacity duration-200 ease-out hover:opacity-70 lg:hidden"
|
|
322
|
+
>
|
|
323
|
+
<MenuIcon />
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<AboutMegaMenu
|
|
329
|
+
isOpen={aboutMenuOpen}
|
|
330
|
+
onClose={() => setAboutMenuOpen(false)}
|
|
331
|
+
primaryLinks={aboutPrimaryLinks}
|
|
332
|
+
secondaryHeading={aboutSecondaryHeading}
|
|
333
|
+
secondaryLinks={aboutSecondaryLinks}
|
|
334
|
+
/>
|
|
335
|
+
|
|
336
|
+
<div className="absolute left-8 right-8 top-1/2 hidden h-px -translate-y-px bg-[#f2f2f2] lg:block lg:left-[176px]" />
|
|
337
|
+
|
|
338
|
+
<div className="relative h-16 w-20 sm:h-[90px] sm:w-[98px] lg:absolute lg:left-8 lg:top-2">
|
|
339
|
+
<SafeImage
|
|
340
|
+
src={logoSrc}
|
|
341
|
+
alt={logoAlt}
|
|
342
|
+
fill
|
|
343
|
+
className="object-contain object-left"
|
|
344
|
+
sizes="98px"
|
|
345
|
+
priority
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
</header>
|
|
349
|
+
|
|
350
|
+
<MobileNavDrawer
|
|
351
|
+
isOpen={mobileMenuOpen}
|
|
352
|
+
onClose={() => setMobileMenuOpen(false)}
|
|
353
|
+
navItems={drawerNavItems}
|
|
354
|
+
accentColor="#2a5caa"
|
|
355
|
+
/>
|
|
356
|
+
|
|
357
|
+
<div className="relative min-h-[60vh] w-full overflow-hidden md:min-h-[700px] lg:min-h-[594px]">
|
|
358
|
+
<div className="overflow-hidden">
|
|
359
|
+
<div
|
|
360
|
+
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
361
|
+
style={{ transform: `translateX(-${activeSlide * 100}%)` }}
|
|
362
|
+
>
|
|
363
|
+
{slides.map((slide, index) => (
|
|
364
|
+
<div key={slide.headline || index} className="relative min-h-[60vh] w-full shrink-0 md:min-h-[700px] lg:min-h-[594px]">
|
|
365
|
+
<SafeImage
|
|
366
|
+
src={slide.backgroundImage}
|
|
367
|
+
alt={slide.backgroundAlt || logoAlt}
|
|
368
|
+
fill
|
|
369
|
+
className="object-cover object-center"
|
|
370
|
+
sizes="100vw"
|
|
371
|
+
priority={index === 0}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-[rgba(11,22,41,0.3)] from-[75%] to-[#0e274e]" />
|
|
379
|
+
<div className="pointer-events-none absolute left-4 top-1/4 h-32 w-48 bg-[#0e274e] blur-[300px] sm:left-[67px] sm:h-[200px] sm:w-[633px]" />
|
|
380
|
+
|
|
381
|
+
<div className="absolute inset-x-0 top-1/2 flex -translate-y-1/2 flex-col gap-6 px-4 sm:gap-8 sm:px-6 md:px-10 lg:left-[75px] lg:w-[649px] lg:px-0">
|
|
382
|
+
<div className="h-px w-24 bg-white sm:w-[132px]" aria-hidden="true" />
|
|
383
|
+
<h1 className="text-2xl font-semibold leading-tight text-white sm:text-3xl md:text-4xl lg:text-[44px] lg:leading-[56px]">
|
|
384
|
+
{currentSlide?.headline}
|
|
385
|
+
</h1>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<SlideIndicators
|
|
389
|
+
count={slideCount}
|
|
390
|
+
activeIndex={activeSlide}
|
|
391
|
+
onSelect={goTo}
|
|
392
|
+
variant="bar"
|
|
393
|
+
className="absolute bottom-5 left-4 sm:bottom-5 sm:left-6 md:left-[75px]"
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={handlePlayPause}
|
|
399
|
+
aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"}
|
|
400
|
+
className="absolute bottom-5 right-4 flex size-12 items-center justify-center rounded-full bg-white/20 p-4 backdrop-blur-[6px] transition-colors duration-200 ease-out hover:bg-white/30 focus-visible:outline-2 focus-visible:outline-offset-2 sm:right-6 sm:size-[64px] sm:p-5 lg:right-10"
|
|
401
|
+
>
|
|
402
|
+
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
</section>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
Menu03.propTypes = menu03PropTypes;
|
|
410
|
+
|
|
411
|
+
export default Menu03;
|