From aec575c3b5797069e4964cfafa26e3de3b92f99e Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 13 Dec 2021 22:04:03 +0100 Subject: 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. --- src/assets/images/icon-articles.svg | 154 +++++++++++++++++++++ src/assets/images/icon-contact.svg | 66 +++++++++ src/assets/images/icon-cv.svg | 71 ++++++++++ src/assets/images/icon-home.svg | 76 ++++++++++ src/components/Header/Header.module.scss | 18 +++ src/components/Header/Header.tsx | 9 +- .../Icons/Hamburger/Hamburger.module.scss | 54 ++++++++ src/components/Icons/Hamburger/Hamburger.tsx | 13 ++ src/components/Icons/index.tsx | 3 + src/components/MainNav/MainNav.module.scss | 148 ++++++++++++++++++++ src/components/MainNav/MainNav.tsx | 71 ++++++++++ src/config/nav.ts | 9 ++ src/services/graphql/client.ts | 1 - src/styles/globals.scss | 1 + src/ts/types/nav.ts | 5 + 15 files changed, 696 insertions(+), 3 deletions(-) create mode 100644 src/assets/images/icon-articles.svg create mode 100644 src/assets/images/icon-contact.svg create mode 100644 src/assets/images/icon-cv.svg create mode 100644 src/assets/images/icon-home.svg create mode 100644 src/components/Header/Header.module.scss create mode 100644 src/components/Icons/Hamburger/Hamburger.module.scss create mode 100644 src/components/Icons/Hamburger/Hamburger.tsx create mode 100644 src/components/Icons/index.tsx create mode 100644 src/components/MainNav/MainNav.module.scss create mode 100644 src/components/MainNav/MainNav.tsx create mode 100644 src/config/nav.ts create mode 100644 src/ts/types/nav.ts diff --git a/src/assets/images/icon-articles.svg b/src/assets/images/icon-articles.svg new file mode 100644 index 0000000..e8141d6 --- /dev/null +++ b/src/assets/images/icon-articles.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icon-contact.svg b/src/assets/images/icon-contact.svg new file mode 100644 index 0000000..462a299 --- /dev/null +++ b/src/assets/images/icon-contact.svg @@ -0,0 +1,66 @@ + + diff --git a/src/assets/images/icon-cv.svg b/src/assets/images/icon-cv.svg new file mode 100644 index 0000000..691443a --- /dev/null +++ b/src/assets/images/icon-cv.svg @@ -0,0 +1,71 @@ + + + + diff --git a/src/assets/images/icon-home.svg b/src/assets/images/icon-home.svg new file mode 100644 index 0000000..11b4f0a --- /dev/null +++ b/src/assets/images/icon-home.svg @@ -0,0 +1,76 @@ + + + + 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 ( -
- +
+
+ + +
); }; 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 ( + + ); +}; + +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(false); + + const getIcon = (id: string) => { + switch (id) { + case 'home': + return ; + case 'blog': + return ; + case 'contact': + return ; + case 'cv': + return ; + default: + break; + } + }; + + const navItems = mainNav.map((item) => { + return ( +
  • + + + {getIcon(item.id)} + {item.name} + + +
  • + ); + }); + + return ( +
    + setIsChecked(!isChecked)} + /> + + +
    + ); +}; + +export default MainNav; diff --git a/src/config/nav.ts b/src/config/nav.ts new file mode 100644 index 0000000..de60369 --- /dev/null +++ b/src/config/nav.ts @@ -0,0 +1,9 @@ +import { t } from '@lingui/macro'; +import { NavItem } from '@ts/types/nav'; + +export const mainNav: NavItem[] = [ + { id: 'home', name: t`Home`, slug: '/' }, + { id: 'blog', name: t`Blog`, slug: '/blog' }, + { id: 'cv', name: t`Resume`, slug: '/cv' }, + { id: 'contact', name: t`Contact`, slug: '/contact' }, +]; diff --git a/src/services/graphql/client.ts b/src/services/graphql/client.ts index 160021b..a58441f 100644 --- a/src/services/graphql/client.ts +++ b/src/services/graphql/client.ts @@ -2,7 +2,6 @@ import { GraphQLClient } from 'graphql-request'; export const getGraphQLClient = () => { const apiUrl: string = process.env.NEXT_PUBLIC_GRAPHQL_API || ''; - console.log(apiUrl); if (!apiUrl) throw new Error('API URL not defined.'); diff --git a/src/styles/globals.scss b/src/styles/globals.scss index c1b6d95..8a3daca 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -15,5 +15,6 @@ @use "base/base"; @use "base/colors"; @use "base/fonts"; +@use "base/helpers"; @use "base/spacings"; @use "base/typography"; diff --git a/src/ts/types/nav.ts b/src/ts/types/nav.ts new file mode 100644 index 0000000..7cfc46b --- /dev/null +++ b/src/ts/types/nav.ts @@ -0,0 +1,5 @@ +export type NavItem = { + id: string; + name: string; + slug: string; +}; -- cgit v1.2.3