diff --git a/index.html b/index.html index ba10976cd32775fa92b681815f6caf3eb04d6975..dc7726bf4c2894c2d804f13bf3f497180924f658 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ <!doctype html> -<html lang="en"> +<html lang="en" class="overflow-x-hidden h-full font-Uhh"> <head> <meta charset="UTF-8" /> diff --git a/src/assets/css/tailwind.presets.min.css b/src/assets/css/tailwind.presets.min.css index 130dc89a9acb85d53355c1d14d5b2aa3620a3101..c630ac15b1800210a718c35c907ce6d03e64f0db 100644 --- a/src/assets/css/tailwind.presets.min.css +++ b/src/assets/css/tailwind.presets.min.css @@ -1 +1 @@ -@tailwind base;@tailwind components;@tailwind utilities;@layer base{img,svg,video,canvas,audio,iframe,embed,object{display:inline;vertical-align:middle}}/*# sourceMappingURL=tailwind.presets.min.css.map */ \ No newline at end of file +@tailwind base;@tailwind components;@tailwind utilities;@layer base{img,svg,video,canvas,audio,iframe,embed,object{display:inline;vertical-align:middle}*{@apply border-border}body{@apply bg-background text-foreground}}/*# sourceMappingURL=tailwind.presets.min.css.map */ \ No newline at end of file diff --git a/src/assets/css/tailwind.presets.min.css.map b/src/assets/css/tailwind.presets.min.css.map index 21d66c4d5130ef65b09d4e888d296ade96c3146a..83dac443bc2aa8cbed5b40d0a278694aa092dc9d 100644 --- a/src/assets/css/tailwind.presets.min.css.map +++ b/src/assets/css/tailwind.presets.min.css.map @@ -1 +1 @@ -{"version":3,"sources":["../sass/tailwind.presets.scss"],"names":[],"mappings":"AAAA,cAAA,CACA,oBAAA,CACA,mBAAA,CAEA,YACE,+CAQE,cAAA,CACA,qBAAA,CAAA","file":"tailwind.presets.min.css"} \ No newline at end of file +{"version":3,"sources":["../sass/tailwind.presets.scss"],"names":[],"mappings":"AAAA,cAAA,CACA,oBAAA,CACA,mBAAA,CAEA,YACE,+CAQE,cAAA,CACA,qBAAA,CAEF,EACE,oBAAA,CAEF,KACE,oCAAA,CAAA","file":"tailwind.presets.min.css"} \ No newline at end of file diff --git a/src/assets/sass/tailwind.presets.scss b/src/assets/sass/tailwind.presets.scss index d184c0b6bbc333d23a5d0c98bbbbe1c20c4ea98b..e360c8fd4070d4114ecf22b1915a63e6a371de1a 100644 --- a/src/assets/sass/tailwind.presets.scss +++ b/src/assets/sass/tailwind.presets.scss @@ -14,4 +14,10 @@ display: inline; vertical-align: middle; } + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } diff --git a/src/layouts/MainLayout.jsx b/src/layouts/MainLayout.jsx index 7a3abbaed2db25e27b2367bb7297f01af742b6ba..c11d12a7f4950315c072db62cb63d272b5e5766c 100644 --- a/src/layouts/MainLayout.jsx +++ b/src/layouts/MainLayout.jsx @@ -1,29 +1,46 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Outlet } from 'react-router-dom'; import Header from './partials/Header'; import Subheader from './partials/subheader/Subheader'; +import Navbar from './partials/navbar/Navbar'; function MainLayout() { // ################################# // HOOKS // ################################# + // ### SET BODY CLASSES + useEffect(() => { + // ### on run exec this code + document.body.className = ''; + document.body.classList.add('overflow-x-hidden', 'h-full'); + + document.getElementById('root').classList.remove('grid-rows-[auto_1fr]', 'h-full'); + document.getElementById('root').classList.add('grid-rows-[auto_1fr]', 'min-h-full', 'w-screen', 'max-h-full', 'sm:grid-rows-[auto_auto_auto_1fr]'); + }, []); + // ### CHECK MOBILE MENU STATE + const [showMobileNav, setShowMobileNav] = useState(false); // ################################# // FUNCTIONS // ################################# + const toggleMobileNav = (toggle) => { + setShowMobileNav(!showMobileNav); + }; // ################################# // OUTPUT // ################################# return ( <> - <Header /> + <Header showMobileNav={showMobileNav} toggleMobileNav={toggleMobileNav} /> <Subheader /> <main role="main" className="relative row-start-2 col-span-full flex sm:justify-center overflow-y-auto sm:row-start-4"> - <Outlet /> + <div className="box-border px-4 mt-2 container"> + <Outlet /> + </div> </main> - <div>Navbar</div> + <Navbar showMobileNav={showMobileNav} toggleMobileNav={toggleMobileNav} /> </> ); } diff --git a/src/layouts/partials/Header.jsx b/src/layouts/partials/Header.jsx index e35daaf6b848aa5acb415c3cd75e2dc07c0919b3..dcc89fbf132ada751e72c51c157d3ae788331a32 100644 --- a/src/layouts/partials/Header.jsx +++ b/src/layouts/partials/Header.jsx @@ -7,7 +7,7 @@ import Hamburger from 'hamburger-react'; -function Header() { +function Header({ showMobileNav, toggleMobileNav }) { // ################################# // HOOKS // ################################# @@ -37,7 +37,7 @@ function Header() { <RiFilter2Line className='text-3xl' /> </div> <div className="h-full w-8"> - <Hamburger label='show navigation' duration={0.3} /> + <Hamburger label='show navigation' toggled={showMobileNav} onToggle={toggleMobileNav} duration={0.3} /> </div> </span> </div> diff --git a/src/layouts/partials/navbar/DesktopLink.jsx b/src/layouts/partials/navbar/DesktopLink.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6c8e14ce5644643e4a0c5c985ee7bd8deb25e5df --- /dev/null +++ b/src/layouts/partials/navbar/DesktopLink.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +function DesktopLink({ to, children }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <> + <Link to={to} className="peer block p-2 hover:bg-UhhWhite hover:text-UhhBlue">{children}</Link> + </> + ); +} + +export default React.memo(DesktopLink); \ No newline at end of file diff --git a/src/layouts/partials/navbar/DesktopNav.jsx b/src/layouts/partials/navbar/DesktopNav.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5fd7c77e894cda3feedda23c5b3a5c6cef812af1 --- /dev/null +++ b/src/layouts/partials/navbar/DesktopNav.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import DesktopLink from './DesktopLink'; + +function DesktopNav({ filteredSitemap }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + // recursively render given menu + const renderMenu = (menu, parent = null) => { + if (!menu) return; + return menu.map((item, idx) => ( + <li key={`link-${idx}`} className="relative"> + {item.children?.length ? ( + <> + <DesktopLink to={parent ? `${parent.path}/${item.path}` : item.path}>{item.title}</DesktopLink> + <ul className="absolute z-50 h-0 overflow-y-hidden bg-UhhBlue border-UhhBlue hover:h-auto peer-hover:h-auto"> + {renderMenu(item.children, item)} + </ul> + </> + ) : ( + <DesktopLink to={parent ? `${parent.path}/${item.path}` : item.path}>{item.title}</DesktopLink> + )} + </li> + + )); + }; + // ################################# + // OUTPUT + // ################################# + return ( + <ul className="hidden sm:flex px-4 ont-UhhBC text-2xl container flex justify-between text-UhhWhite font-UhhSLC"> + {filteredSitemap[0]?.children && renderMenu(filteredSitemap[0].children)} + </ul> + ); +} + +export default React.memo(DesktopNav); \ No newline at end of file diff --git a/src/layouts/partials/navbar/MobileLink.jsx b/src/layouts/partials/navbar/MobileLink.jsx new file mode 100644 index 0000000000000000000000000000000000000000..76a848e462f09381b849f8f39a80896855d47541 --- /dev/null +++ b/src/layouts/partials/navbar/MobileLink.jsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri'; +import { Link } from 'react-router-dom'; + +function MobileLink({ to, children, mobileNavState, dispatchMobileNavState, childNode, level, toggleMobileNav }) { + // ################################# + // HOOKS + // ################################# + + // ### HAS VALID CHILDS + const [hasValidChilds, setHasValidChilds] = useState(false); + useEffect(() => { + // only nodes with children + if (childNode?.children) { + let validChilds = childNode.children.map(child => { + // hidden or index childs aren't valid + if (child.hidden || child.index) return false; + // others are + return true; + }).filter(Boolean); + // if > 0 valid childs found set to true + if (validChilds.length) setHasValidChilds(true); + } + }, []); + + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <> + <span className={`flex border-b border-b-UhhWhite text-2xl ${level === 'pov' ? 'font-UhhBC' : null}`}> + <span className={`w-16 py-2 px-6 text-center + ${level === 'pov' && mobileNavState.povParentNode[0] ? + 'cursor-pointer md:hover:bg-UhhWhite md:hover:text-UhhBlue opacity-100' + : null}`}> + {level === 'pov' && mobileNavState.povParentNode[0] ? + <RiArrowLeftSLine + onClick={() => { dispatchMobileNavState({ type: 'setPath', payload: mobileNavState.povParentNode[0]?.path }); }} /> + : null} + </span> + + <Link + to={to} + onClick={() => toggleMobileNav()} + className={`md:hover:bg-UhhWhite md:hover:text-UhhBlue py-2 pr-2 w-full + ${level === 'pov' ? + 'pl-20' + : 'pl-28'}`}> + {children} + </Link> + + <span + className={`w-16 py-2 px-6 text-center + ${level !== 'pov' && hasValidChilds ? + 'cursor-pointer md:hover:bg-UhhWhite md:hover:text-UhhBlue opacity-100' + : null}`}> + {level !== 'pov' && hasValidChilds ? + <RiArrowRightSLine + onClick={() => { dispatchMobileNavState({ type: 'setPath', payload: childNode?.path }); }} /> + : null} + </span> + </span> + + </> + ); +} + +export default React.memo(MobileLink); \ No newline at end of file diff --git a/src/layouts/partials/navbar/MobileNav.jsx b/src/layouts/partials/navbar/MobileNav.jsx new file mode 100644 index 0000000000000000000000000000000000000000..30ab333b6b1f8acecba3765af3776d73375548d6 --- /dev/null +++ b/src/layouts/partials/navbar/MobileNav.jsx @@ -0,0 +1,97 @@ +import React, { useEffect, useReducer } from 'react'; +import MobileLink from './MobileLink'; + +function reducer(mobileNavState, action) { + switch (action.type) { + case 'setNode': + return { ...mobileNavState, povNode: action.payload }; + case 'setParentNode': + return { ...mobileNavState, povParentNode: action.payload }; + case 'setPath': + return { ...mobileNavState, povPath: action.payload }; + default: + return action.payload; + } +} + +function MobileNav({ filteredSitemap, showMobileNav, toggleMobileNav }) { + // ################################# + // HOOKS + // ################################# + const [mobileNavState, dispatchMobileNavState] = useReducer(reducer, { povNode: [], povPath: location.pathname, povParentNode: [] }); + + // fetch new nodes on each change of PoV + useEffect(() => { + // loop & search for new PoV's path + function sitemapSearch(array, needle, parent) { + for (let i = 0; i < array.length; i++) { + if (array[i].path === needle) { + // set node + dispatchMobileNavState({ type: 'setNode', payload: [array[i]] }); + // set parent + dispatchMobileNavState({ type: 'setParentNode', payload: [parent] }); + return 'found'; + } + else if (array[i].children?.length) { + const result = sitemapSearch(array[i].children, needle, array[i]); + // return the result only if it's actually found otherwise keep looping + if (result) return result; + } + } + } + // search nodes for new PoV Path + sitemapSearch(filteredSitemap, mobileNavState.povPath); + + // triggered on PoV path change + }, [mobileNavState.povPath, filteredSitemap]); + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + // TODO: smoothen mobile nav output + return ( + <div className={`sm:hidden h-full relative flex z-[900] max-h-full transform transition-all duration-150 ease-out ${showMobileNav ? 'right-0 opacity-100' : '-right-full opacity-0'}`}> + <div className="grow bg-UhhLightBlue" onClick={() => { toggleMobileNav(); }}></div> + <div className="absolute bg-UhhBlue right-0 z-[900] inset-y-0 min-w-[300px] text-UhhWhite"> + <ul className="max-h-full"> + {filteredSitemap && mobileNavState.povNode.map((link, idx) => + <li key={`link-${idx}`}> + <MobileLink + level='pov' + toggleMobileNav={toggleMobileNav} + mobileNavState={mobileNavState} + dispatchMobileNavState={dispatchMobileNavState} + to={link.path}> + {link.title} + </MobileLink> + { + link.children?.length ? ( + <ul key={`${idx}-childs`}> + {link.children.map((child, cidx) => { + if (child.hidden || child.index) return false; + return <MobileLink + key={`${idx}-child-${cidx}`} + toggleMobileNav={toggleMobileNav} + mobileNavState={mobileNavState} + dispatchMobileNavState={dispatchMobileNavState} + childNode={child} + to={link.path !== '/' ? `${link.path}/${child.path}` : `${child.path}`}> + {child.title} + </MobileLink>; + }).filter(Boolean)} + </ul> + ) : (null) + } + </li> + )} + </ul> + </div> + </div> + ); +} + +export default React.memo(MobileNav); \ No newline at end of file diff --git a/src/layouts/partials/navbar/Navbar.jsx b/src/layouts/partials/navbar/Navbar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..059023a4e4264b37081ee7c0a4731cc14b88737a --- /dev/null +++ b/src/layouts/partials/navbar/Navbar.jsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { sitemap } from "/src/routes/Sitemap"; +import DesktopNav from './DesktopNav'; +import MobileNav from './MobileNav'; + + +function Navbar(props) { + // ################################# + // HOOKS + // ################################# + // ### FILTERED SITEMAP + const [filteredSitemap, setFilteredSitemap] = useState([]); + + useEffect(() => { + // fetch all links for navbars + const [overall] = sitemap.filter((item) => item.title === 'MenuBar'); + // fetch all children from / + const [home] = overall.children; + + // recursively filter sitemap + function flatFilter(nestedProp, searchKey, searchValue, arr) { + return arr.filter(o => { + // slightly customized for searchKey = object + const keep = o[searchKey] && o[searchKey].hasOwnProperty(searchValue); + if (keep && o[nestedProp]) { + o[nestedProp] = flatFilter(nestedProp, searchKey, searchValue, o[nestedProp]); + } + return keep; + }); + } + + setFilteredSitemap(flatFilter('children', 'handle', 'crumb', [home])); + }, []); + // ################################# + // FUNCTIONS + // ################################# + + + // ################################# + // OUTPUT + // ################################# + return ( + <nav className="row-start-2 col-span-full sm:flex sm:justify-center sm:bg-UhhBlue"> + {/* mobile */} + <MobileNav filteredSitemap={filteredSitemap} showMobileNav={props.showMobileNav} toggleMobileNav={props.toggleMobileNav} /> + + {/* desktop - render starts with children of home */} + <DesktopNav filteredSitemap={filteredSitemap} /> + </nav > + + ); +} + +export default React.memo(Navbar); \ No newline at end of file