Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from maykinmedia/issue/#119
Browse files Browse the repository at this point in the history
#119 - Header menu.
alextreme authored Nov 5, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents caa8315 + d3fde73 commit 90a6a9e
Showing 36 changed files with 719 additions and 310 deletions.
39 changes: 31 additions & 8 deletions frontend/src/App.scss
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
:root {
--border-image: linear-gradient(to right, var(--color-yellow-light), var(--color-yellow-dark), var(--color-yellow));
--border-width: 2px;

--container-width: 1024px;

--color-black: #000;
--color-white: #fff;
--color-text: #333;
--color-primary: #476db7;
--color-secundary: #e5006c;
--color-lightgray: #D2D2D2;
--color-blue: #476db7;
--color-gray: #7A7A7A;
--color-gray-dark: #4b4b4b;
--color-gray-light: #D2D2D2;
--color-red: #e5006c;
--color-white: #fff;
--color-yellow: #F8D62D;
--color-yellow-dark: #CCA000;
--color-yellow-light: #FFF7D1;

--color-accent: var(--color-yellow-light);
--color-body: var(--color-gray-dark);
--color-primary: var(--color-blue);
--color-secundary: var(--color-red);

--row-height: 37px;

--spacing-tiny: 2px;
--spacing-small: 4px;
--spacing-medium: 8px;
--spacing-large: 16px;
--spacing-extra-large: 32px;
--menu-height: 125px;
--container-width: 1024px;


--font-family-heading: 'TheMix C5';

--font-family-body: 'TheSans C5';
--font-color-body: var(--color-black);
--font-size-body: 16px;
--font-line-height-body: 21px;

}

@font-face {
font-family: 'TheMix C5';
src: local('TheMix C5'), url('./fonts/TheMixC5/DesktopFonts/TheMixC5-5_Plain.otf') format('opentype');
src: local('TheMix C5'), url('./fonts/TheMixC5/DesktopFonts/TheMixC5-5_Plain.otf') format('opentype');
}

@font-face {
61 changes: 10 additions & 51 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,22 @@
import React, { useContext } from 'react';
import { NavLink } from 'react-router-dom';
import { Menu } from './Components/Menu/Menu';
import { Logo } from './Components/Menu/Logo';
import { MenuText } from './Components/Menu/MenuText';
import { Container } from './Components/Container/Container';

import { logout } from './api/calls';

import './App.scss';
import { globalContext } from './store';
import { RouterView } from './routes/RouterView';
import React from 'react';
import {Header} from './Components/Header/Header'
import {Container} from './Components/Container/Container';
import {RouterView} from './routes/RouterView';
import './fonts/TheMixC5/DesktopFonts/TheMixC5-5_Plain.otf';
import './fonts/TheSansC5/DesktopFonts/TheSansC5-5_Plain.otf';
import './App.scss';

export function App() {
const { globalState, dispatch } = useContext(globalContext);

async function handleLogout() {
await logout();
await dispatch({ type: 'PURGE_STATE' });
}

const getMenuText = () => {
if (globalState.user) {
return (
<MenuText>
Welkom
{globalState.user.firstName}
{' '}
{globalState.user.lastName}
</MenuText>
);
}
return <></>;
};

const getLoginLink = () => {
if (globalState.user) {
return <NavLink className="menu__link menu__link--highlighted" activeClassName="menu__link--active" onClick={handleLogout} to="/">Logout</NavLink>;
}
return (
<>
<NavLink className="menu__link" activeClassName="menu__link--active" to="/register">Registreer</NavLink>
<NavLink className="menu__link menu__link--highlighted" activeClassName="menu__link--active" to="/login">Login</NavLink>
</>
);
};

return (
<>
<Menu>
<Logo src="https://www.zwolle.nl/sites/all/themes/custom/zwolle_redesign/logo.png" alt="Logo van gemeente" />
<Header>

<div className="menu__info">
{ getMenuText() }
{ getLoginLink() }

</div>
</Menu>
</Header>
<Container>
<RouterView />
<RouterView/>
</Container>
</>
);
33 changes: 0 additions & 33 deletions frontend/src/Components/Breadcrumbs/Breadcrumbs.scss

This file was deleted.

24 changes: 0 additions & 24 deletions frontend/src/Components/Breadcrumbs/Breadcrumbs.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion frontend/src/Components/Container/Container.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.container {
max-width: var(--container-width);
margin: 70px auto 0;
margin: var(--row-height) auto 0;
position: relative;
}
4 changes: 0 additions & 4 deletions frontend/src/Components/Container/Grid.scss
Original file line number Diff line number Diff line change
@@ -11,10 +11,6 @@
grid-template-columns: 250px 1fr;
}

.grid__left {

}

.grid__right {
position: relative;
}
9 changes: 1 addition & 8 deletions frontend/src/Components/Container/Grid.tsx
Original file line number Diff line number Diff line change
@@ -21,20 +21,13 @@ export function Grid(props:GridProps) {
return classNames;
};

const getLeft = () => {
if (props.isLoggedIn) {
return <div className="grid__left">{ props.left }</div>;
}
return <></>;
};

const getContent = () => {
if (props.children) {
return props.children;
}
return (
<>
{ getLeft() }
{props.left}
<div className="grid__right">{ props.right }</div>
</>
);
6 changes: 3 additions & 3 deletions frontend/src/Components/Form/Input.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
.input {
--color-light-gray: darkgray;
--color-gray-light: darkgray;
box-sizing: border-box;
border: 1px solid var(--color-light-gray);
border: 1px solid var(--color-gray-light);
border-radius: 3px;
color: var(--color-light-gray);
color: var(--color-gray-light);
font-family: "TheSans C5";
font-size: 16px;
letter-spacing: 0;
File renamed without changes.
20 changes: 20 additions & 0 deletions frontend/src/Components/Header/Breadcrumbs.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.breadcrumbs {
&__list {
display: flex;
list-style: none;
margin: 0;
max-width: var(--container-width);
padding: var(--spacing-large) max(calc((100vw - var(--container-width)) / 2), var(--spacing-large));
}

&__list-item {
display: flex;
align-items: center;
white-space: nowrap;
}

&__list-item > *[class*=icon],
&__list-item > *[class*=Icon] {
margin: 0 var(--spacing-medium);
}
}
102 changes: 102 additions & 0 deletions frontend/src/Components/Header/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import {Link, useLocation} from 'react-router-dom';
import ChevronRightOutlinedIcon from '@mui/icons-material/ChevronRightOutlined';
import {ROUTES} from "../../routes/routes";
import {iRoute} from "../../types/route";
import './Breadcrumbs.scss';


interface iBreadCrumb {
part: string,
path: string,
route: iRoute,
label?: string
shouldRenderIcon?: boolean
}


/**
* Returns the breadcrumbs based the current location and matched routes.
* @return {JSX.Element}
*/
export function Breadcrumbs() {
const location = useLocation();

/**
* Returns an iBreadCrumb[] based on the current location and matched routes.
* @return {iBreadCrumb[]}
*/
const getBreadCrumbs = () => {
// Root required exception.
if (location.pathname === '/') {
const route = Object.values(ROUTES).find((route: iRoute) => route.path === `/`)

if (!route) {
return [];
}
return [{part: '', path: '/', route: route, shouldRenderIcon: true}]
}

// Recurse routes.
const paths = location.pathname.split('/')
.reduce((acc: iBreadCrumb[], part: string) => {
const currentPath = acc.map((breadCrumb) => breadCrumb.part).join('/')
const path = `${currentPath}/${part}`;
let label = null;
let route = Object.values(ROUTES).find((route: iRoute) => route.path === `/${part}`)

if (!route) {
route = Object.values(ROUTES).find((route: iRoute) => route.path.match(`${currentPath}/:[a-zA-Z-_]`))

if (typeof route?.label === 'function') {
label = route.label(part);
}
}

return [...acc, {
part: part,
label: label,
path: path,
route: route,
shouldRenderIcon: path === '/'
}] as iBreadCrumb[];
}, [])
return paths
}

/**
* Returns the label for the breadcrumb.
* @param {iBreadCrumb} breadcrumb
* @return {string}
*/
const getBreadCrumbLabel = (breadcrumb: iBreadCrumb): string => {
return breadcrumb.label || breadcrumb.route.label as string;
}

/**
* Renders the breadcrumbs.
* @return {JSX.Element}
*/
const renderBreadCrumbs = () => getBreadCrumbs().map((breadCrumb: iBreadCrumb, index: number) => {
const Icon = breadCrumb.route.icon;
const linkClassName = (index === getBreadCrumbs().length - 1) ? 'link' : 'link link--active';

return (
<li key={index} className="breadcrumbs__list-item">
{index > 0 && <ChevronRightOutlinedIcon/>}
<Link className={linkClassName} to={breadCrumb.path}>
{breadCrumb.shouldRenderIcon && Icon && <Icon/>}
{getBreadCrumbLabel(breadCrumb)}
</Link>
</li>
);
});

return (
<nav className="breadcrumbs">
<ul className="breadcrumbs__list">
{renderBreadCrumbs()}
</ul>
</nav>
);
}
62 changes: 62 additions & 0 deletions frontend/src/Components/Header/Header.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

///
/// Header.
///

.header {
display: grid;
grid-template-areas: "pad-before actions pad-after"
"pad-before logo pad-after"
"nav nav nav"
"breadcrumbs breadcrumbs breadcrumbs";
grid-template-columns: var(--spacing-large) 1fr var(--spacing-large);
grid-template-rows: auto auto auto auto;

@media (min-width: 768px) {
grid-template-areas: "pad-before logo actions pad-after"
"nav nav nav nav"
"breadcrumbs breadcrumbs breadcrumbs breadcrumbs";
grid-template-columns: max(calc((100vw - var(--container-width)) / 2), var(--spacing-large)) 1fr 1fr max(calc((100vw - var(--container-width)) / 2), var(--spacing-large));
grid-template-rows: auto auto auto;
}

/// Logo.
.logo {
grid-area: logo
}

/// Actions.
&__actions {
align-items: baseline;
display: flex;
grid-area: actions;
justify-content: space-between;

@media (min-width: 768px) {
justify-content: flex-end;
margin-top: var(--row-height);
}

}

&__actions &__list {
list-style: none;
margin: 0;
padding: 0;
}

&__actions &__list-item {
display: inline;
margin-left: var(--spacing-extra-large);
}

/// Primary navigation.
.primary-navigation {
grid-area: nav;
}

/// Breadcrumbs.
.breadcrumbs {
grid-area: breadcrumbs;
}
}
81 changes: 81 additions & 0 deletions frontend/src/Components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, {useContext} from 'react';
import {NavLink} from "react-router-dom";
import {logout} from '../../api/calls';
import {globalContext} from '../../store';
import {Breadcrumbs} from './Breadcrumbs';
import {Logo} from './Logo';
import PrimaryNavigation from './PrimaryNavigation';
import './Header.scss';
import './Link.scss';


/**
* Renders the header including all the navigation.
* @return {JSX.Element}
*/
export function Header() {
const {globalState, dispatch} = useContext(globalContext);

/**
* Logout.
*/
async function handleLogout() {
await logout();
await dispatch({type: 'PURGE_STATE'});
}

/**
* Returns the welcome message.
* @return {JSX.Element}
*/
const getWelcomeMessage = () => {
return (
<p className="link">
{globalState.user && `Welkom ${globalState.user.firstName} ${globalState.user.lastName}`}
{!globalState.user && 'Welkom'}
</p>
)
};

/**
* Returns the login/logout link(s).
* @return {JSX.Element}
*/
const getLoginLinks = () => {
if (globalState.user) {
return (
<ul className="header__list">
<li className="header__list-item">
<NavLink className="link" onClick={handleLogout} to="#">Uitloggen</NavLink>
</li>
</ul>
);
}

return (
<ul className="header__list">
<li className="header__list-item">
<NavLink className="link" to="/register">Registreren</NavLink>
</li>

<li className="header__list-item">
<NavLink className="link link--primary" activeClassName="link--active" to="/login">Inloggen</NavLink>
</li>
</ul>
);
};

return (
<header className="header">
<Logo src="https://www.zwolle.nl/sites/all/themes/custom/zwolle_redesign/logo.png" alt="Logo van gemeente"/>

<nav className="header__actions">
{getWelcomeMessage()}
{getLoginLinks()}
</nav>

<PrimaryNavigation/>
<Breadcrumbs/>
</header>
);
}
34 changes: 34 additions & 0 deletions frontend/src/Components/Header/Link.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
///
/// Navigation link.
///

.link {
color: var(--font-color-body);
display: inline-flex;
align-items: center;
font-family: var(--font-family-body);
font-size: var(--font-size-body);
line-height: var(--font-line-height-body);
text-decoration: none;

&--active,
&--primary,
&:active,
&:focus,
&:hover {
color: var(--color-primary);
}

&--active {
text-decoration: underline;
}

&:hover {
text-decoration: underline;
}

*[class*=icon],
*[class*=Icon] {
margin-right: var(--spacing-tiny)
}
}
15 changes: 15 additions & 0 deletions frontend/src/Components/Header/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import {Link} from 'react-router-dom';

interface LogoProps {
src: string,
alt: string,
}

export function Logo(props: LogoProps) {
return (
<Link className="logo" to="/">
<img src={props.src} alt={props.alt}/>
</Link>
);
}
80 changes: 80 additions & 0 deletions frontend/src/Components/Header/PrimaryNavigation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
///
/// Primary navigation.
///

.primary-navigation {
position: relative;

&:before {
content: '';

border-bottom: var(--border-width) solid;
border-image: var(--border-image);
border-image-slice: 1;
left: 0;
position: absolute;
bottom: 0;
width: 100%;
z-index: -1;
}

/// Top list.

& > &__list {
box-sizing: content-box;
display: flex;
list-style: none;
margin: auto;
max-width: var(--container-width);
padding: 0 var(--spacing-large);
width: 100%;
}

& > &__list > &__list-item {
display: flex;
align-items: center;
height: var(--row-height);
padding: var(--spacing-large);
box-sizing: content-box;

&:first-child {
padding-left: 0;
}

&:last-child {
padding-right: 0;
}
}

/// Nested list.

& > &__list > &__list-item > &__list {
background-color: var(--color-accent);
box-sizing: border-box;
padding: var(--spacing-large) max(calc((100vw - var(--container-width)) / 2), var(--spacing-large));
position: absolute;
left: 0;
list-style: none;
margin: 0;
top: 100%;
transform: scaleY(0);
width: 100%;
z-index: 100;

@media (min-width: 768px) {
column-count: 4;
}
}

& > &__list > &__list-item > &__list > &__list-item {
line-height: var(--row-height);
}

/// Interaction.

& > &__list > &__list-item:focus &__list,
& > &__list > &__list-item:focus-within &__list,
& > &__list > &__list-item:hover &__list {
transform: scaleY(1);
}
}
80 changes: 80 additions & 0 deletions frontend/src/Components/Header/PrimaryNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, {useContext, useState} from 'react';
import {generatePath, Link} from 'react-router-dom';
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
import {NAVIGATION} from '../../routes/navigation';
import {globalContext} from '../../store';
import {iMenuItem} from '../../types/menu-item';
import './Link.scss'
import './PrimaryNavigation.scss'


interface iPrimaryNavigationProps {
menuItems: iMenuItem[],
}

/**
* Renders the primary navigation.
* Prop menuItems can be set to contains an iMenuItem[] (defaults to NAVIGATION). Children of top level menu items can
* contain a "children" key containing either a nested iMenuItem[] or an async function returning a Promise for an
* iMenuItem[]
*
* @return {JSX.Element}
*/
export default function PrimaryNavigation(props: iPrimaryNavigationProps) {
const {globalState} = useContext(globalContext);
const [resolvedNavigation, setResolvedNavigation] = useState(props.menuItems);
const [tick, setTick] = useState(0);

// Run optionally async children function.
props.menuItems.forEach(async (menuItem) => {
if (!menuItem.children) {
return;
}

if (typeof menuItem.children !== "function") {
return;
}

menuItem.children = await menuItem.children()
setResolvedNavigation(props.menuItems);
setTick(tick + 1);
});


/**
* Renders menuItems as list.
* @param {iMenuItem[]} menuItems
* @param {boolean} shouldRenderIcons=true
* @return JSX.Element
*/
const renderMenuItems = (menuItems: iMenuItem[], shouldRenderIcons: boolean) => (
<ul className="primary-navigation__list">
{menuItems.filter && menuItems.filter((menuItem: iMenuItem) => globalState.user || !menuItem.route.loginRequired).map((menuItem: iMenuItem, index: number) => {
const label = (menuItem.label) ? menuItem.label : menuItem.route.label;
const to = (menuItem.routeParams) ? generatePath(menuItem.route.path, menuItem.routeParams) : menuItem.route.path;
const Icon = menuItem.route.icon;

return (
<li key={index} className="primary-navigation__list-item">
<Link className="link" to={to}>
{shouldRenderIcons && Icon && <Icon/>}
{label}
{menuItem.children && <KeyboardArrowDownOutlinedIcon/>}
</Link>
{menuItem.children && renderMenuItems(menuItem.children as iMenuItem[], false)}
</li>
);
})}
</ul>
)

return (
<nav className="primary-navigation">
{renderMenuItems(resolvedNavigation, true)}
</nav>
);
}

PrimaryNavigation.defaultProps = {
menuItems: NAVIGATION,
}
15 changes: 0 additions & 15 deletions frontend/src/Components/Menu/Logo.tsx

This file was deleted.

34 changes: 0 additions & 34 deletions frontend/src/Components/Menu/Menu.scss

This file was deleted.

12 changes: 0 additions & 12 deletions frontend/src/Components/Menu/Menu.tsx

This file was deleted.

7 changes: 0 additions & 7 deletions frontend/src/Components/Menu/MenuText.tsx

This file was deleted.

14 changes: 0 additions & 14 deletions frontend/src/Components/Menu/SideMenu.scss

This file was deleted.

23 changes: 0 additions & 23 deletions frontend/src/Components/Menu/SideMenu.tsx

This file was deleted.

5 changes: 3 additions & 2 deletions frontend/src/api/calls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import { Token } from '../store/types';
import {iCategory} from "../types/pdc";

export const logout = async () => {
const res = await axios.post(`${import.meta.env.VITE_API_URL}/api/auth/logout/`, {}).catch((err) => {
@@ -59,10 +60,10 @@ export const getCategory = async (slug: string) => {
return res.data;
};

export const getCategories = async () => {
export const getCategories = async (): Promise<iCategory[]> => {
const res = await axios.get(`${import.meta.env.VITE_API_URL}/api/categories/`).catch((err) => {
console.error(err.response.data);
throw err;
});
return res.data;
return res.data as iCategory[];
};
2 changes: 1 addition & 1 deletion frontend/src/index.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
body {
margin: 0;
font-family: TheSans;
/*font-family: TheSans;*/
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
7 changes: 2 additions & 5 deletions frontend/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import React from 'react';
import { Grid } from '../Components/Container/Grid';
import SideMenu from '../Components/Menu/SideMenu';
import HeaderMenu from '../Components/Header/SideMenu';

export default function Home() {
const getLeft = () => (
<SideMenu />
);
const getRight = () => (
<>
Home
</>
);

return (
<Grid isLoggedIn fixedLeft left={getLeft()} right={getRight()} />
<Grid isLoggedIn right={getRight()} />
);
}
15 changes: 6 additions & 9 deletions frontend/src/pages/Product/detail.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import Markdown from 'markdown-to-jsx';

import { getProduct } from '../../api/calls';
import { Grid } from '../../Components/Container/Grid';
import { File } from '../../Components/File/File';
import { TagList } from '../../Components/Tags/TagList';
import { Breadcrumbs } from '../../Components/Breadcrumbs/Breadcrumbs';
import { Divider } from '../../Components/Divider/Divider';
import AnchorMenu from '../../Components/Menu/AnchorMenu';
import AnchorMenu from '../../Components/Header/AnchorMenu';
import { File } from '../../Components/File/File';
import { Links } from '../../Components/Product/Links';
import { Related } from '../../Components/Product/Related';
import { Social } from '../../Components/Product/Social';

import { getProduct } from '../../api/calls';
import './product.scss';
import { TagList } from '../../Components/Tags/TagList';
import { iProduct } from '../../types/pdc';
import './product.scss';


export default function ProductDetail() {
const [product, setProduct] = useState<iProduct | undefined>(undefined);
@@ -67,7 +65,6 @@ export default function ProductDetail() {
);
const getRight = () => (
<>
<Breadcrumbs breadcrumbs={[{ icon: true, name: 'Home', to: '/' }, { icon: false, name: 'Themas', to: '/themas' }, { icon: false, name: product?.name || '', to: `/product/${product?.slug}` }]} />
<div className="product">
<h1 id="title">{product?.name}</h1>
<TagList tags={product?.tags} />
12 changes: 2 additions & 10 deletions frontend/src/pages/Themas/detail.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';

import { getCategory } from '../../api/calls';
import { Grid } from '../../Components/Container/Grid';
import { CardList } from '../../Components/Card/CardList';
import { Breadcrumbs } from '../../Components/Breadcrumbs/Breadcrumbs';
import SideMenu from '../../Components/Menu/SideMenu';

import { iCategory } from '../../types/pdc';
import { getCategory } from '../../api/calls';
import './theme.scss';

export default function ThemaDetail() {
@@ -22,19 +18,15 @@ export default function ThemaDetail() {
load();
}, []);

const getLeft = () => (
<SideMenu />
);
const getRight = () => (
<div className="theme-list">
<Breadcrumbs breadcrumbs={[{ icon: true, name: 'Home', to: '/' }, { icon: false, name: 'Themas', to: '/themas' }, { icon: false, name: category?.name, to: `/themas/${category?.slug}` }]} />
<h1 className="theme-list__title">{category?.name}</h1>
<p className="theme-list__description">{category?.description}</p>
<CardList title={category?.name} categories={category?.children} products={category?.product} />
</div>
);

return (
<Grid fixedLeft left={getLeft()} right={getRight()} isLoggedIn={false} />
<Grid right={getRight()} isLoggedIn={false} />
);
}
18 changes: 4 additions & 14 deletions frontend/src/pages/Themas/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import React, { useState, useEffect, useContext } from 'react';

import { getCategories } from '../../api/calls';
import { Card } from '../../Components/Card/Card';
import { CardContainer } from '../../Components/CardContainer/CardContainer';
import { Grid } from '../../Components/Container/Grid';
import { Breadcrumbs } from '../../Components/Breadcrumbs/Breadcrumbs';
import SideMenu from '../../Components/Menu/SideMenu';

import { getCategories } from '../../api/calls';

import { globalContext } from '../../store';

import './theme-list.scss';
import { iCategory } from '../../types/pdc';
import './theme-list.scss';


export default function Themas() {
const { globalState } = useContext(globalContext);
@@ -25,14 +20,9 @@ export default function Themas() {
load();
}, []);

const getLeft = () => (
<SideMenu />
);
const getRight = () => {
console.log(categories);
return (
<div className="theme-list">
<Breadcrumbs breadcrumbs={[{ icon: true, name: 'Home', to: '/' }, { icon: false, name: 'Themas', to: '/themas' }]} />
<h1 className="theme-list__title">Themas</h1>
<p className="theme-list__description">Nulla vitae elit libero, a pharetra augue.</p>
<CardContainer isLoggedIn={!!globalState.user}>
@@ -43,6 +33,6 @@ export default function Themas() {
};

return (
<Grid isLoggedIn={!!globalState.user} fixedLeft left={getLeft()} right={getRight()} />
<Grid isLoggedIn={!!globalState.user} right={getRight()} />
);
}
51 changes: 24 additions & 27 deletions frontend/src/routes/RouterView.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import React, { useContext } from 'react';
import { Switch } from 'react-router-dom';
import { GuardProvider, GuardedRoute } from 'react-router-guards';

import { getToken, getUser } from '../api/calls';

import Home from '../pages/Home';
import React, {useContext} from 'react';
import {Switch} from 'react-router-dom';
import {GuardProvider, GuardedRoute} from 'react-router-guards';
import {getToken, getUser} from '../api/calls';
import NotFoundPage from '../pages/NotFound';
import About from '../pages/About';
import Login from '../pages/Login';
import Register from '../pages/Register';
import Themas from '../pages/Themas/index';
import ThemeDetail from '../pages/Themas/detail';
import ProductDetail from '../pages/Product/detail';
import { globalContext } from '../store';
import { getIsLoggedIn } from '../utils';
import {globalContext} from '../store';
import {getIsLoggedIn} from '../utils';
import {ROUTES} from './routes';


export function RouterView() {
const { globalState, dispatch } = useContext(globalContext);
const {globalState, dispatch} = useContext(globalContext);

const requireLogin = (to, from, next) => {
if (to.meta.auth) {
@@ -29,7 +22,12 @@ export function RouterView() {
}
};

function getCookie(cookieName:string) {
/**
* Returns a cookie.
* @param {string} cookieName
* @return {string}
*/
function getCookie(cookieName: string) {
const name = `${cookieName}=`;
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
@@ -50,25 +48,24 @@ export function RouterView() {
if (sessionCookie && !getIsLoggedIn(globalState)) {
const token = await getToken();
if (token) {
await dispatch({ type: 'SET_TOKEN', payload: token });
await dispatch({type: 'SET_TOKEN', payload: token});
const user = await getUser(token);
await dispatch({ type: 'SET_USER', payload: user });
await dispatch({type: 'SET_USER', payload: user});
}
}
next();
};

const renderRoutes = () => {
return Object.entries(ROUTES).map(([key, route]) => (
<GuardedRoute key={key} component={route.component} exact={route.exact} path={route.path} meta={{auth: route.loginRequired}}/>
))
};

return (
<GuardProvider guards={[requireLogin, hasSession]} error={NotFoundPage}>
<Switch>
<GuardedRoute path="/" exact component={Home} />
<GuardedRoute path="/login" exact component={Login} />
<GuardedRoute path="/register" exact component={Register} />
<GuardedRoute path="/themas" exact component={Themas} />
<GuardedRoute path="/themas/:slug" exact component={ThemeDetail} />
<GuardedRoute path="/product/:slug" exact component={ProductDetail} />
<GuardedRoute path="/about" exact component={About} meta={{ auth: true }} />
<GuardedRoute path="*" component={NotFoundPage} />
{renderRoutes()}
</Switch>
</GuardProvider>
);
28 changes: 28 additions & 0 deletions frontend/src/routes/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {getCategories} from '../api/calls';
import {iCategory} from '../types/pdc';
import {iMenuItem} from '../types/menu-item';
import {ROUTES} from './routes';


/**
* Returns a promise for an iRoute[] based on the categories.
* @return {Promise}
*/
const getThemeNavigation = async () => {
const categories = await getCategories();
return categories.map((category: iCategory): iMenuItem => ({
label: category.name,
route: ROUTES.CATEGORY,
routeParams: {slug: category.slug}
}));
}

/**
* The main navigation.
*/
export const NAVIGATION: iMenuItem[] = [
{route: ROUTES.HOME},
{route: ROUTES.CATEGORIES, children: getThemeNavigation},
{route: ROUTES.PROFILE},
{route: ROUTES.INBOX},
]
105 changes: 105 additions & 0 deletions frontend/src/routes/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import AppsOutlinedIcon from "@mui/icons-material/AppsOutlined";
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
import InboxOutlinedIcon from "@mui/icons-material/InboxOutlined";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
import Home from "../pages/Home";
import {iRoute} from "../types/route";
import NotFoundPage from "../pages/NotFound";
import About from "../pages/About";
import ProductDetail from "../pages/Product/detail";
import ThemeDetail from "../pages/Themas/detail";
import Themas from "../pages/Themas";
import Register from "../pages/Register";
import Login from "../pages/Login";


/**
* The routes for the application.
*/
export const ROUTES: { [index: string]: iRoute } = {
HOME: {
component: Home,
label: 'Home',
path: '/',
exact: true,
icon: AppsOutlinedIcon,
loginRequired: false,
},
LOGIN: {
component: Login,
label: 'Inloggen',
path: '/login',
exact: true,
icon: AccountCircleOutlinedIcon,
loginRequired: false,
},
REGISTER: {
component: Register,
label: 'Registreren',
path: '/register',
exact: true,
icon: AccountCircleOutlinedIcon,
loginRequired: false,
},
PROFILE: {
component: Home, // TODO
label: 'Mijn profiel',
path: '/account',
exact: true,
icon: AccountCircleOutlinedIcon,
loginRequired: true
},
INBOX: {
component: Home, // TODO
label: 'Mijn berichten',
path: '/account/inbox',
exact: true,
icon: InboxOutlinedIcon,
loginRequired: true
},
CATEGORIES: {
component: Themas,
label: 'Thema\'s',
path: '/themas',
exact: true,
icon: DescriptionOutlinedIcon,
loginRequired: false,
},
CATEGORY: {
component: ThemeDetail,
label: (slug: string): string => {
if(!slug) {
return 'Thema';
}
const str = slug.replace(/[-_]/g, ' ');
return str.split(' ').reduce((label: string, word: string): string => `${label} ${word[0].toUpperCase()}${word.slice(1)}`, '');
},
path: '/themas/:slug',
exact: true,
icon: ArticleOutlinedIcon,
loginRequired: false,
},
PRODUCT: {
component: ProductDetail,
label: 'Product',
path: '/product/:slug',
exact: true,
loginRequired: false,
},
ABOUT: {
component: About,
label: 'Over',
path: '/about',
exact: true,
loginRequired: true,
},
NOTFOUND: {
component: NotFoundPage,
label: '404',
path: '*',
loginRequired: false,
},
}


10 changes: 5 additions & 5 deletions frontend/src/types/general.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface iBreadcrumb {
icon: boolean,
name: string,
to: string,
}
// export interface iBreadcrumb {
// icon: boolean,
// name: string,
// to: string,
// }
8 changes: 8 additions & 0 deletions frontend/src/types/menu-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {iRoute} from "./route";

export interface iMenuItem {
route: iRoute,
routeParams?: {[index: string]: string},
children?: iMenuItem[] | Function,
label?: string
}
11 changes: 11 additions & 0 deletions frontend/src/types/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {ComponentType} from "react";

export interface iRoute {
component: ComponentType
label: string|Function,
path: string,
exact?: boolean,
icon?: ComponentType,
loginRequired?: boolean
meta?: Object,
}

0 comments on commit 90a6a9e

Please sign in to comment.