aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2021-12-13 22:04:03 +0100
committerArmand Philippot <git@armandphilippot.com>2021-12-13 22:04:26 +0100
commitaec575c3b5797069e4964cfafa26e3de3b92f99e (patch)
treeca58b4e85b4f0d3eb78b57cfa58aa9bad9fd4c2f /src/components
parent5bc55ac0a801cbe82c41a10f7e680612be4deaf8 (diff)
chore: add main-nav component
I choose to implement main-nav paths manually instead of fetching them from GraphQL to ensure functional navigation without JS.
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Header/Header.module.scss18
-rw-r--r--src/components/Header/Header.tsx9
-rw-r--r--src/components/Icons/Hamburger/Hamburger.module.scss54
-rw-r--r--src/components/Icons/Hamburger/Hamburger.tsx13
-rw-r--r--src/components/Icons/index.tsx3
-rw-r--r--src/components/MainNav/MainNav.module.scss148
-rw-r--r--src/components/MainNav/MainNav.tsx71
7 files changed, 314 insertions, 2 deletions
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
new file mode 100644
index 0000000..b6bd15d
--- /dev/null
+++ b/src/components/Header/Header.module.scss
@@ -0,0 +1,18 @@
+.wrapper {
+ display: grid;
+ grid-template-columns:
+ minmax(0, 1fr) min(calc(100vw - var(--spacing-md) * 2), 100ch)
+ minmax(0, 1fr);
+ align-items: center;
+ padding: var(--spacing-sm) 0 var(--spacing-md);
+ position: relative;
+}
+
+.body {
+ grid-column: 2;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-md);
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 599fdc0..52da2e8 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -1,9 +1,14 @@
import Branding from '@components/Branding/Branding';
+import MainNav from '@components/MainNav/MainNav';
+import styles from './Header.module.scss';
const Header = () => {
return (
- <header>
- <Branding />
+ <header className={styles.wrapper}>
+ <div className={styles.body}>
+ <Branding />
+ <MainNav />
+ </div>
</header>
);
};
diff --git a/src/components/Icons/Hamburger/Hamburger.module.scss b/src/components/Icons/Hamburger/Hamburger.module.scss
new file mode 100644
index 0000000..1ae01bb
--- /dev/null
+++ b/src/components/Icons/Hamburger/Hamburger.module.scss
@@ -0,0 +1,54 @@
+@use "@styles/abstracts/functions" as fun;
+
+.icon {
+ position: relative;
+
+ &,
+ &::before,
+ &::after {
+ background: var(--color-primary);
+ background-image: linear-gradient(
+ to right,
+ var(--color-primary-light) 0%,
+ var(--color-highlight) 100%
+ );
+ border: fun.convert-px(1) solid var(--color-border);
+ border-radius: fun.convert-px(3);
+ display: block;
+ width: var(--btn-size, fun.convert-px(50));
+ height: fun.convert-px(6);
+ margin: auto;
+ transition: all 0.25s ease-in-out 0s, transform 0.4s ease-in 0s;
+ }
+
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ }
+
+ &::before {
+ bottom: fun.convert-px(15);
+ }
+
+ &::after {
+ top: fun.convert-px(15);
+ }
+
+ &--active {
+ background: transparent;
+ border: transparent;
+
+ &::before {
+ transform-origin: 50% 50%;
+ transform: rotate(45deg);
+ bottom: 0;
+ }
+
+ &::after {
+ transform-origin: 50% 50%;
+ transform: rotate(-45deg);
+ top: 0;
+ }
+ }
+}
diff --git a/src/components/Icons/Hamburger/Hamburger.tsx b/src/components/Icons/Hamburger/Hamburger.tsx
new file mode 100644
index 0000000..3b9e609
--- /dev/null
+++ b/src/components/Icons/Hamburger/Hamburger.tsx
@@ -0,0 +1,13 @@
+import styles from './Hamburger.module.scss';
+
+const HamburgerIcon = ({ isActive }: { isActive: boolean }) => {
+ return (
+ <span
+ className={`${styles.icon}${
+ isActive ? ` ${styles['icon--active']}` : ''
+ }`}
+ ></span>
+ );
+};
+
+export default HamburgerIcon;
diff --git a/src/components/Icons/index.tsx b/src/components/Icons/index.tsx
new file mode 100644
index 0000000..da4e029
--- /dev/null
+++ b/src/components/Icons/index.tsx
@@ -0,0 +1,3 @@
+import HamburgerIcon from './Hamburger/Hamburger';
+
+export { HamburgerIcon };
diff --git a/src/components/MainNav/MainNav.module.scss b/src/components/MainNav/MainNav.module.scss
new file mode 100644
index 0000000..cc4b45b
--- /dev/null
+++ b/src/components/MainNav/MainNav.module.scss
@@ -0,0 +1,148 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.wrapper {
+ --btn-size: #{fun.convert-px(50)};
+
+ display: flex;
+ flex-flow: column nowrap;
+ place-items: center;
+ height: var(--btn-size);
+ width: var(--btn-size);
+ background: var(--color-bg);
+ position: relative;
+ z-index: 5;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ background: inherit;
+ }
+
+ @include mix.dimensions("md") {
+ width: unset;
+ height: unset;
+ }
+ }
+}
+
+.label {
+ display: flex;
+ flex-flow: column nowrap;
+ width: 100%;
+ height: 100%;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ display: none;
+ }
+ }
+}
+
+.checkbox {
+ position: absolute;
+ top: calc((var(--btn-size) - #{fun.convert-px(14)}) / 2);
+ left: calc((var(--btn-size) - #{fun.convert-px(14)}) / 2);
+ opacity: 0;
+ cursor: pointer;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ display: none;
+ }
+ }
+}
+
+.nav {
+ display: flex;
+ flex-flow: column nowrap;
+ place-content: center;
+ padding-bottom: var(--toolbar-size);
+ position: absolute;
+ bottom: auto;
+ left: auto;
+ right: auto;
+ top: var(--btn-size);
+ background: var(--color-bg-opacity);
+ box-shadow: 0 0 3px 0 var(--color-shadow);
+ text-align: center;
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ transition: all 0.8s ease-in-out 0s;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ border-bottom-width: fun.convert-px(5);
+ transform-origin: 50% -100%;
+ }
+
+ @include mix.dimensions("md") {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ position: relative;
+ top: 0;
+ }
+ }
+}
+
+.list {
+ @extend %reset-list;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ }
+ }
+}
+
+.link {
+ display: block;
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--color-bg);
+ font-size: var(--font-size-sm);
+ font-variant: small-caps;
+ font-weight: 600;
+ text-decoration: none;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ background: inherit;
+ }
+ }
+}
+
+.icon {
+ display: block;
+ width: fun.convert-px(25);
+ margin: auto;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ width: fun.convert-px(30);
+ }
+ }
+}
+
+.checkbox:not(:checked) {
+ ~ .nav {
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(100vw);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ transform: perspective(20rem) translate3d(0, 100%, -20rem);
+ }
+
+ @include mix.dimensions("md") {
+ opacity: 1;
+ visibility: visible;
+ transform: none;
+ }
+ }
+ }
+}
diff --git a/src/components/MainNav/MainNav.tsx b/src/components/MainNav/MainNav.tsx
new file mode 100644
index 0000000..3140e64
--- /dev/null
+++ b/src/components/MainNav/MainNav.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react';
+import Link from 'next/link';
+import { t } from '@lingui/macro';
+import { HamburgerIcon } from '@components/Icons';
+import { mainNav } from '@config/nav';
+import ArticlesIcon from '@assets/images/icon-articles.svg';
+import ContactIcon from '@assets/images/icon-contact.svg';
+import CVIcon from '@assets/images/icon-cv.svg';
+import HomeIcon from '@assets/images/icon-home.svg';
+import styles from './MainNav.module.scss';
+
+const MainNav = () => {
+ const [isChecked, setIsChecked] = useState<boolean>(false);
+
+ const getIcon = (id: string) => {
+ switch (id) {
+ case 'home':
+ return <HomeIcon />;
+ case 'blog':
+ return <ArticlesIcon />;
+ case 'contact':
+ return <ContactIcon />;
+ case 'cv':
+ return <CVIcon />;
+ default:
+ break;
+ }
+ };
+
+ const navItems = mainNav.map((item) => {
+ return (
+ <li key={item.id} className={styles.item}>
+ <Link href={item.slug}>
+ <a className={styles.link}>
+ <span className={styles.icon}>{getIcon(item.id)}</span>
+ <span>{item.name}</span>
+ </a>
+ </Link>
+ </li>
+ );
+ });
+
+ return (
+ <div className={styles.wrapper}>
+ <input
+ type="checkbox"
+ name="main-nav__checkbox"
+ id="main-nav__checkbox"
+ aria-labelledby="main-nav-toggle"
+ className={styles.checkbox}
+ checked={isChecked}
+ onChange={() => setIsChecked(!isChecked)}
+ />
+ <label
+ htmlFor="main-nav__checkbox"
+ id="main-nav-toggle"
+ className={styles.label}
+ >
+ <HamburgerIcon isActive={isChecked} />
+ <span className="screen-reader-text">
+ {isChecked ? t`Close menu` : t`Open menu`}
+ </span>
+ </label>
+ <nav className={styles.nav}>
+ <ul className={styles.list}>{navItems}</ul>
+ </nav>
+ </div>
+ );
+};
+
+export default MainNav;