Skip to content
This repository was archived by the owner on Feb 27, 2024. It is now read-only.

Commit 5d4af7e

Browse files
author
Greg Rickaby
authored
Merge pull request #98 from WebDevStudios/feature/menus
Feature/menus
2 parents 7fff59c + 6fad607 commit 5d4af7e

27 files changed

+529
-179
lines changed

api/wordpress/_global/getPostTypeStaticProps.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {algoliaIndexName} from '@/api/algolia/connector'
22
import getPostTypeById from './getPostTypeById'
33
import getPostTypeArchive from './getPostTypeArchive'
44
import {addApolloState} from '@/api/apolloConfig'
5+
import getMenus from '@/api/wordpress/menus/getMenus'
6+
import config from '@/functions/config'
57

68
/**
79
* Retrieve static props by post type.
@@ -17,10 +19,21 @@ export default async function getPostTypeStaticProps(
1719
// preview = false, // TODO - add preview handling.
1820
// previewData = null
1921
) {
22+
// Get WP Nav Menus.
23+
const menus = await getMenus(config.menuLocations)
24+
2025
// Check for dynamic archive display.
2126
if (!Object.keys(params).length) {
2227
const {apolloClient, ...archiveData} = await getPostTypeArchive(postType)
2328

29+
// Add WP Nav Menus to archive.
30+
archiveData.menus = menus
31+
32+
// Add Algolia env vars to archive.
33+
archiveData.algolia = {
34+
indexName: algoliaIndexName
35+
}
36+
2437
// Merge in query results as Apollo state.
2538
return addApolloState(apolloClient, {
2639
props: {
@@ -49,6 +62,9 @@ export default async function getPostTypeStaticProps(
4962
props.error = false
5063
}
5164

65+
// Set WP Nav Menus.
66+
props.menus = menus
67+
5268
// Add Algolia env vars.
5369
props.algolia = {
5470
indexName: algoliaIndexName
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import formatHeirarchialMenu from '@/api/wordpress/menus/formatHeirarchialMenu'
2+
3+
/**
4+
* Filter menus array by menu location.
5+
*
6+
* @author WebDevStudios
7+
* @param {Array} menus The array of WP menus to filter.
8+
* @param {Array} locations The array of locations for filtering.
9+
* @return {object} An object containing the requested locations as individual objects.
10+
*/
11+
export default function filterByLocation(menus, locations) {
12+
const data = {}
13+
14+
// Loop each menu location.
15+
locations.forEach((location) => {
16+
// Convert dashes to underscores.
17+
const locationName = location.replace(/-/g, '_')
18+
19+
// Filter menus array by location and assign to new object.
20+
const wpmenu = menus.filter(function (menu) {
21+
return menu['locations'].includes(locationName.toUpperCase())
22+
})
23+
24+
// Format the returned menu.
25+
data[locationName] = formatHeirarchialMenu(wpmenu[0]?.menuItems?.nodes)
26+
})
27+
28+
return data
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Format a flat list WP nav menu into a heirarchial list.
3+
*
4+
* @author WebDevStudios
5+
* @see https://www.wpgraphql.com/docs/menus/#hierarchical-data
6+
* @param {array} data The array containing menu data.
7+
* @param {object} Default Object keys
8+
* @return {array} Array containing a updated menu list.
9+
*/
10+
export default function formatHeirarchialMenu(
11+
data = [],
12+
{idKey = 'id', parentKey = 'parentId', childrenKey = 'children'} = {}
13+
) {
14+
const tree = []
15+
const childrenOf = {}
16+
data.forEach((item) => {
17+
const newItem = {...item}
18+
const {[idKey]: id, [parentKey]: parentId = 0} = newItem
19+
childrenOf[id] = childrenOf[id] || []
20+
newItem[childrenKey] = childrenOf[id]
21+
parentId
22+
? (childrenOf[parentId] = childrenOf[parentId] || []).push(newItem)
23+
: tree.push(newItem)
24+
})
25+
return tree
26+
}

api/wordpress/menus/getMenus.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import filterByLocation from '@/api/wordpress/menus/filterByLocation'
2+
import queryMenus from './queryMenus'
3+
4+
/**
5+
* Get menu data from WPGraphQL.
6+
*
7+
* @author WebDevStudios
8+
* @param {array} locations The menu locations as an array.
9+
* @return {array} Returns array of menu objects.
10+
*/
11+
export default async function getMenus(locations = []) {
12+
if (!locations.length > 0) {
13+
return [] // Exit if empty.
14+
}
15+
16+
// Query WP Menus.
17+
const menus = await queryMenus()
18+
19+
// Filter returned menus by specific menu location.
20+
const filteredMenus = filterByLocation(menus?.data?.menus?.nodes, locations)
21+
22+
return filteredMenus || []
23+
}

api/wordpress/menus/queryMenus.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {gql} from '@apollo/client'
2+
import {initializeWpApollo} from '../connector'
3+
4+
/**
5+
* Query all WP Menus.
6+
*
7+
* @author WebDevStudios
8+
* @return {object} Menu data or error object.
9+
*/
10+
export default async function queryMenus() {
11+
const query = gql`
12+
query menuQuery {
13+
menus {
14+
nodes {
15+
locations
16+
menuItems {
17+
nodes {
18+
id
19+
parentId
20+
label
21+
path
22+
target
23+
title
24+
}
25+
}
26+
}
27+
}
28+
}
29+
`
30+
31+
// Get/create Apollo instance.
32+
const apolloClient = initializeWpApollo()
33+
34+
const menus = await apolloClient
35+
.query({query})
36+
.then((menus) => menus ?? null)
37+
.catch((error) => {
38+
return {
39+
isError: true,
40+
message: error.message
41+
}
42+
})
43+
44+
if (!menus) {
45+
return {
46+
isError: true,
47+
message: `An error occurred while trying to get menu data"`
48+
}
49+
}
50+
51+
return menus
52+
}

components/atoms/Logo/Logo.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Link from 'next/link'
2+
3+
// TODO: Create Storybook for this component.
4+
5+
export default function Logo() {
6+
return (
7+
<Link href="/">
8+
<a>
9+
<img
10+
src="/logo.svg"
11+
alt="site logo"
12+
loading="lazy"
13+
height="128"
14+
width="128"
15+
/>
16+
</a>
17+
</Link>
18+
)
19+
}

components/atoms/Logo/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {default} from './Logo'

components/common/Layout.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ Layout.propTypes = {
3737
noFollow: PropTypes.bool,
3838
noIndex: PropTypes.bool,
3939
openGraph: PropTypes.object,
40-
title: PropTypes.string.isRequired
40+
title: PropTypes.string.isRequired,
41+
menus: PropTypes.object
4142
}

components/common/MenuProvider.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import PropTypes from 'prop-types'
2+
import {createContext} from 'react'
3+
4+
// Initialize Menu context object.
5+
export const MenuContext = createContext({
6+
menus: null
7+
})
8+
9+
/**
10+
* Provide menus for components.
11+
*
12+
* @param {Object} props The component attributes as props.
13+
* @return {Element} The child elements wrapped in a context provider.
14+
*/
15+
export default function MenuProvider(props) {
16+
const {value, children} = props
17+
18+
return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>
19+
}
20+
21+
MenuProvider.propTypes = {
22+
menus: PropTypes.string,
23+
children: PropTypes.object,
24+
value: PropTypes.object
25+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import isLinkActive from '@/functions/isLinkActive'
2+
import cn from 'classnames'
3+
import Link from 'next/link'
4+
import {useRouter} from 'next/router'
5+
import styles from './Navigation.module.css'
6+
7+
/**
8+
* Render the Navigation component.
9+
*
10+
* @author WebDevStudios
11+
* @param {object} props Navigation props.
12+
* @param {array} props.menu Array of menu items.
13+
* @param {string} props.className Optional classname for the element.
14+
* @return {Element} The Navigation component.
15+
*/
16+
export default function Navigation({menu, className}) {
17+
const {asPath} = useRouter()
18+
return (
19+
<>
20+
{!!menu?.length && (
21+
<nav className={cn(styles.navigation, className)}>
22+
<ul>
23+
{menu.map((item, index) => {
24+
const children =
25+
item.children && item.children.length > 0 ? item.children : ''
26+
return (
27+
<li key={index}>
28+
<Link href={item.path}>
29+
<a
30+
target={item.target ? item.target : '_self'}
31+
className={cn(
32+
'nav-item',
33+
isLinkActive(asPath, item.path) && styles.active
34+
)}
35+
>
36+
{item.label}
37+
</a>
38+
</Link>
39+
{children && (
40+
<ul>
41+
{children.map((item, index) => {
42+
return (
43+
<li key={index}>
44+
<Link href={item.path}>
45+
<a
46+
target={item.target ? item.target : '_self'}
47+
className={cn(
48+
'nav-item',
49+
isLinkActive(asPath, item.path) &&
50+
styles.active
51+
)}
52+
>
53+
{item.label}
54+
</a>
55+
</Link>
56+
</li>
57+
)
58+
})}
59+
</ul>
60+
)}
61+
</li>
62+
)
63+
})}
64+
</ul>
65+
</nav>
66+
)}
67+
</>
68+
)
69+
}

0 commit comments

Comments
 (0)