Skip to content

Commit

Permalink
MainMenuNav implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ArianZargaran committed Jan 3, 2025
1 parent 45504bb commit b857ca8
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 142 deletions.
30 changes: 30 additions & 0 deletions app/components/main-menu-nav/main-menu-nav-item.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.nav-item_wrapper {
display: flex;
justify-content: center;
flex-direction: column;
width: 100%;
cursor: pointer;
}

@media (min-width: 992px) {
.nav-item_wrapper {
margin-top: calc(var(--spacing-2xl) + var(--spacing-sm));
min-height: calc(100% - (var(--spacing-2xl) + var(--spacing-sm)));
box-sizing: border-box;
}
}

.nav-item_heading {
color: var(--theme_text);
margin-left: var(--spacing-xl);
}

.nav-item_hr {
background-color: var(--theme_text);
width: calc(100% - var(--spacing-xl));
}

.nav-item_caption {
margin-left: var(--spacing-xl);
color: var(--theme_text);
}
42 changes: 42 additions & 0 deletions app/components/main-menu-nav/main-menu-nav-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { motion } from "framer-motion";
import React, { useState } from "react";

import { useMediaQuery } from "~/hooks/useMediaQuery";

import styles from "./main-menu-nav-item.module.css";

export interface Option {
id: string;
option: string;
caption: string;
href?: string;
}

export const MainMenuNavItem: React.FC<Option> = ({
option,
caption,
href,
}) => {
const [isHovered, setIsHovered] = useState<boolean>(false);
const isMediumBreakpoint = useMediaQuery("(max-width: 992px)");

return (
<motion.a
href={href}
className={styles["nav-item_wrapper"]}
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => setIsHovered(false)}
>
<h2 className={styles["nav-item_heading"]}>{option}</h2>
<motion.hr
className={styles["nav-item_hr"]}
animate={{
x: isHovered || isMediumBreakpoint ? "-24rem" : "-120%",
opacity: isHovered || isMediumBreakpoint ? 1 : 0,
transition: { type: "linear", duration: 0.2 },
}}
/>
<h3 className={styles["nav-item_caption"]}>{caption}</h3>
</motion.a>
);
};
46 changes: 46 additions & 0 deletions app/components/main-menu-nav/main-menu-nav.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.main-menu_nav {
position: fixed;
z-index: var(--z-index_xl);
}

.main-menu_ul {
display: flex;
min-width: 100vw;
min-height: 100vh;
flex-direction: column;
}

.main-menu_li {
display: flex;
}

.main-menu_nav-item {
background: var(--theme_bg);
min-height: calc(100vh / var(--options-length));
display: flex;
}

@media (min-width: 992px) {
.main-menu_ul {
flex-direction: row-reverse;
}

.main-menu_nav-item {
min-width: calc(100vw / var(--options-length));
}
}

.main-menu_toggle {
position: fixed;
right: var(--spacing-sm);
top: var(--spacing-sm);
padding-top: var(--spacing-sm);
padding-left: var(--spacing-sm);
padding-right: var(--spacing-sm);
padding-bottom: var(--spacing-xs);
z-index: var(--z-index_2xl);
}

.main-menu_toggle:hover {
cursor: pointer;
}
85 changes: 85 additions & 0 deletions app/components/main-menu-nav/main-menu-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { HamburgerIcon as MainMenuToggle } from "animatea";
import classnames from "classnames";
import { AnimatePresence, motion } from "framer-motion";
import React, { useState } from "react";

import { useMediaQuery } from "~/hooks/useMediaQuery";

import { MainMenuNavItem, Option } from "./main-menu-nav-item";
import styles from "./main-menu-nav.module.css";

export interface MainMenuProps {
options: Option[];
}

const MainMenuNav: React.FC<MainMenuProps> = ({ options }) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const isMediumBreakpoint = useMediaQuery("(max-width: 992px)");

return (
<>
<MainMenuToggle
className={styles["main-menu_toggle"]}
isOpen={isOpen}
onClick={() => setIsOpen(!isOpen)}
transition={{ ease: "easeOut", duration: 0.2 }}
height={32}
width={32}
/>
<AnimatePresence>
{isOpen ? (
<motion.nav className={styles["main-menu_nav"]}>
<ul className={styles["main-menu_ul"]}>
{options.map(({ id, option, caption, href }, idx) => (
<motion.li
initial={
isMediumBreakpoint
? { y: 0, x: idx % 2 === 0 ? "100%" : "-100%" }
: {
y: "-100%",
x: 0,
}
}
animate={{ y: 0, x: 0 }}
exit={
isMediumBreakpoint
? {
x: idx % 2 === 0 ? "-100%" : "100%",
y: 0,
}
: {
x: 0,
y: "-100%",
}
}
transition={{
duration: 0.2,
delay: isMediumBreakpoint ? 0 : 0.2 * idx,
}}
style={{ "--options-length": options.length }}
className={classnames(styles["main-menu_nav-item"], {
french: idx === 0,
oxford: idx === 1,
mirage: idx === 2,
stratos: idx === 3,
royal: idx === 4,
})}
key={id}
>
<MainMenuNavItem
id={id}
option={option}
caption={caption}
href={href}
/>
</motion.li>
))}
</ul>
</motion.nav>
) : null}
</AnimatePresence>
</>
);
};

export default MainMenuNav;
39 changes: 39 additions & 0 deletions app/hooks/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useState } from "react";

type MediaQuery =
| "(max-width: 575px)"
| "(max-width: 576px)"
| "(max-width: 768px)"
| "(max-width: 992px)"
| "(max-width: 1200px)";

export const useMediaQuery = (query: MediaQuery) => {
const [matches, setMatches] = useState<boolean>(false);

useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}

const listener = () => {
setMatches(media.matches);
};

if (typeof media.addEventListener === "function") {
media.addEventListener("change", listener);
} else {
media.addListener(listener);
}

return () => {
if (typeof media.removeEventListener === "function") {
media.removeEventListener("change", listener);
} else {
media.removeListener(listener);
}
};
}, [matches, query]);

return matches;
};
16 changes: 15 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@ import {
ScrollRestoration,
} from "@remix-run/react";

import MainMenuNav from "~/components/main-menu-nav/main-menu-nav";
import colorsStylesheet from "~/stylesheets/colors.css";
import elevationStylesheet from "~/stylesheets/elevation.css";
import fontsStylesheet from "~/stylesheets/fonts.css";
import resetStylesheet from "~/stylesheets/reset.css";
import rootStylesheet from "~/stylesheets/root.css";
import themesStylesheet from "~/stylesheets/themes.css";
import themesStylesheet from "~/stylesheets/spacing.css";
import spacingStylesheet from "~/stylesheets/themes.css";

export const links: LinksFunction = () => [
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
{ rel: "stylesheet", href: resetStylesheet },
{ rel: "stylesheet", href: fontsStylesheet },
{ rel: "stylesheet", href: colorsStylesheet },
{ rel: "stylesheet", href: themesStylesheet },
{ rel: "stylesheet", href: spacingStylesheet },
{ rel: "stylesheet", href: elevationStylesheet },
{ rel: "stylesheet", href: rootStylesheet },
];

Expand All @@ -34,6 +39,15 @@ export default function App() {
<Links />
</head>
<body className="body pure">
<MainMenuNav
options={[
{ id: "ID", option: "Option", caption: "Caption", href: "/" },
{ id: "ID", option: "Option", caption: "Caption", href: "/" },
{ id: "ID", option: "Option", caption: "Caption", href: "/" },
{ id: "ID", option: "Option", caption: "Caption", href: "/" },
{ id: "ID", option: "Option", caption: "Caption", href: "/" },
]}
/>
<Outlet />
<ScrollRestoration />
<Scripts />
Expand Down
9 changes: 2 additions & 7 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { json, LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import React from "react";

import FloatingParticlesBackground from "~/components/backgrounds/floating-particles-background";
import indexStyleSheet from "~/stylesheets/index.css";

export const links: LinksFunction = () => [
{ rel: "stylesheet", href: indexStyleSheet },
];

interface HelloProps {
greetings?: string;
Expand All @@ -23,7 +18,7 @@ const Hello: React.FC<HelloProps> = ({ greetings = "Hello!" }) => {

return (
<div>
<h1 className="heading">{greetings}</h1>
<h1>{greetings}</h1>
BOO
<Link to="/foo" viewTransition style={{ viewTransitionName: "header" }}>
{foo}
Expand Down
2 changes: 1 addition & 1 deletion app/stylesheets/colors.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
:root {
--color_electric-lime: #ccff00;
--color_bunting: #0d1434;
--color_pure-wite: #fff;
--color_pure-white: #fff;
--color_royal-blue: #1d6feb;
--color_stratos: #000053;
--color_mirage: #1b2530;
Expand Down
9 changes: 9 additions & 0 deletions app/stylesheets/elevation.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:root {
--z-index_2xs: 200;
--z-index_xs: 300;
--z-index_sm: 400;
--z-index_md: 500;
--z-index_lg: 600;
--z-index_xl: 700;
--z-index_2xl: 800;
}
4 changes: 0 additions & 4 deletions app/stylesheets/index.css

This file was deleted.

Loading

0 comments on commit b857ca8

Please sign in to comment.