Skip to content

Implement responsive design - convert sidebar to top navbar below 700px screen width #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"lint.validateOnly": "eslint ."
},
"dependencies": {
"classnames": "^2.2.6",
"date-fns": "^2.16.1",
"next": "10.0.0",
"predicates": "^2.0.3",
Expand Down
18 changes: 4 additions & 14 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import { GetStaticProps } from 'next';
import Head from 'next/head';

import { blogClientFromEnvOrThrow } from '../scripts/blogClient/BlogClient.factory';
import Content from '../scripts/Content';
import Menu from '../scripts/Menu';
import Page from '../scripts/page/Page';
import TopicList, { TopicAndFirstPost } from '../scripts/TopicList';
import styles from '../styles/general.module.css';

const Home: React.FC<{
topicsAndPosts: TopicAndFirstPost[];
}> = ({ topicsAndPosts }) => {
return (
<div className={styles.container}>
<Head>
<title>Sam Roberts&apos; personal website</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Menu />
<Content>
<TopicList {...{ topicsAndPosts }} />
</Content>
</div>
<Page>
<TopicList {...{ topicsAndPosts }} />
</Page>
);
};
export default Home;
Expand Down
20 changes: 5 additions & 15 deletions pages/posts/[slug].tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';

import { blogClientFromEnvOrThrow } from '../../scripts/blogClient/BlogClient.factory';
import Content from '../../scripts/Content';
import Menu from '../../scripts/Menu';
import Page from '../../scripts/page/Page';
import PostTitle from '../../scripts/PostTitle';
import { TopicAndFirstPost } from '../../scripts/TopicList';
import styles from '../../styles/general.module.css';

const Post: React.FC<{
topicAndPost: TopicAndFirstPost;
}> = ({ topicAndPost }) => {
const { topic, post } = topicAndPost;
return (
<div className={styles.container}>
<Head>
<title>Sam Roberts&apos; personal website</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Menu />
<Content>
<PostTitle title={topic.title} date={post.created_at} />
<div dangerouslySetInnerHTML={{ __html: post.cooked }} />
</Content>
</div>
<Page>
<PostTitle title={topic.title} date={post.created_at} />
<div dangerouslySetInnerHTML={{ __html: post.cooked }} />
</Page>
);
};
export default Post;
Expand Down
8 changes: 0 additions & 8 deletions scripts/Content.tsx

This file was deleted.

24 changes: 0 additions & 24 deletions scripts/Menu.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions scripts/MenuIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { forwardRef } from 'react';

/**
* Google Material icon encoded as a react `<svg />`
*
* source: https://material.io/resources/icons/?icon=menu&style=baseline
*/
const MenuIcon: React.FC<React.SVGProps<SVGSVGElement>> = forwardRef(
function MenuIcon(props, ref) {
return (
<svg
{...{ ref }}
{...props}
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
</svg>
);
}
);
export default MenuIcon;
72 changes: 72 additions & 0 deletions scripts/page/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import classNames from 'classnames';
import Image from 'next/image';
import { useRef, useState } from 'react';

import MenuIcon from '../MenuIcon';
import useOnClickOutside from '../useOnClickOutside';
import styles from './page.module.css';

/**
* Responsive container for context and navigation links.
* On small screens, will display as a "top navbar" at the top of the screen, above the content.
* In this case, will hide links within an expandable "nav menu."
* On larger screens, will display as a "left sidebar" to the left of the content.
*/
const NavBar: React.FC = () => {
const [navMenuOpen, setNavMenuOpen] = useState<boolean>(false);

const navMenuWrapperRef = useRef<HTMLElement>(null);
const menuIconWrapperRef = useRef<SVGSVGElement>(null);
useOnClickOutside(
() => setNavMenuOpen(false),
navMenuWrapperRef,
// include the menu icon itself, to avoid case where clicking menu icon
// triggers this onClickOutside, closing the menu, then the menu icon click
// registers, opening the menu again.
menuIconWrapperRef
);

return (
<div className={styles.navbar}>
<div className={styles.headshotContainer}>
<a href="/">
<Image src="/headshot.png" alt="In fact, me!" unsized />
</a>
</div>
<MenuIcon
ref={menuIconWrapperRef}
className={styles.menuIcon}
onClick={() => setNavMenuOpen(!navMenuOpen)}
/>
<nav
ref={navMenuWrapperRef}
className={classNames(styles.navMenu, {
[styles.menuOpen]: navMenuOpen
})}
>
<ul>
<li className={styles.homeLink}>
<a href="/">Home</a>
</li>
<hr />
<li>
<a href="https://twitter.com/samgqroberts">
twitter.com/samgqroberts
</a>
</li>
<li>
<a href="https://github.com/samgqroberts">
github.com/samgqroberts
</a>
</li>
<li>
<a href="https://www.linkedin.com/in/samgqroberts">
linkedin.com/in/samgqroberts
</a>
</li>
</ul>
</nav>
</div>
);
};
export default NavBar;
24 changes: 24 additions & 0 deletions scripts/page/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Head from 'next/head';

import NavBar from './NavBar';
import styles from './page.module.css';

/**
* General page layout containing NavBar and page-specific content.
*/
const Page: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<div className={styles.page}>
<Head>
<title>Sam Roberts&apos; personal website</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<NavBar />
<div className={styles.content}>{children}</div>
</div>
);
};

export default Page;
121 changes: 121 additions & 0 deletions scripts/page/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
.page {
display: flex; /* for general layout purposes, can't beat flexbox */
flex-direction: column; /* navbar on top, page content below */
}

.content {
padding: 0 14px; /* makes sure text and images don't sit directly against sides of screen */
}

.navbar {
height: 40px; /* simple - set the height of the top navbar */
display: flex; /* again, prefer flexbox for layout */
justify-content: space-between; /* pushes headshot to far left, menu icon to far right */
align-items: center; /* centers headshot and menu icon vertically */
padding: 0 10px; /* adds horizontal padding, pushing headshot and menu icon a bit toward center */
}

.navbar, .navMenu {
background: #bec1cc; /* a nice background color for the top navbar and expanded nav menu */
}

/* all nested elements related to the headshot picture */
.headshotContainer, .headshotContainer a, .headshotContainer div, .headshotContainer img {
/* directly sets the height of each of these elements in relation to container.
in effect: ensures headshot picture resizes to stay within top navbar.
delegates pixel-height setting to ultimate container (top navbar) */
height: 100%;
display: block; /* ensures <a /> and <img />s respect the given height */
}
.headshotContainer {
padding: 4px; /* ensures headshot picture doesn't directly touch top or bottom of navbar */
}

.menuIcon {
margin-right: 4px; /* gives menu icon equal amount of space from RHS of navbar as headshot has from LHS */
}

.navMenu {
position: absolute; /* this element won't flow with or push other page elements */
top: 40px; /* anchors the top of the expandable nav menu to the bottom of the top navbar */
left: 0; /* together with right, ensures nav menu takes up entire width of screen */
right: 0; /* together with left, ensures nav menu takes up entire width of screen */
text-align: center; /* centers the text horizontally */
overflow: hidden; /* ensures the unexpanded nav menu's children (the links) don't appear */
/* ensures the unexpanded nav menu does not appear (has no height).
this is preferred over using `display: none` because this gives us a basis for a transition. */
max-height: 0;
/* whenever max-height changes (the nav menu expands or collapses),
animate the change over 0.3 seconds */
transition: max-height 0.3s;
}
.navMenu.menuOpen {
/* give the nav menu a max-height approximately, but a little bigger than, the fully expanded height.
this, together with `max-height` and `transition` above will animate opening and closing
the nav menu. */
max-height: 220px;
}

.navMenu ul {
list-style-type: none; /* removes bullet points from the beginning of each list item in the nav menu */
padding: 0 10px; /* gives a little more horizontal room for the list of nav links */
}
.navMenu a {
font-weight: bold; /* simple: makes the nav menu links bold */
color: black; /* simple: makes the text of the nav menu links black */
padding: 10px 0; /* gives more vertical space (and therefore click/touch area) to each nav menu link */
display: block; /* ensures `padding` style takes effect */
}

/* only apply the following styles if the screen (or browser window) is at least 700px wide */
@media only screen and (min-width: 700px) {
.page {
flex-direction: row; /* overrides small-screen style - now our top navbar is a left sidebar! */
/* prevent left sidebar and content from expanding past 1000px.
note that this coupled with the media query means that the container will scale
with screen size between 700px to 1000px.
using percentage-based widths for child elements will mean they scale with this container. */
max-width: 1000px; /* prevent left sidebar and content from expanding past 1000px */
margin: 0 auto; /* centers left sidebar and content (takes effect if screen is over 1000px) */
/* puts remaining horizontal space in container that sidebar or content don't take up
between them */
justify-content: space-between;
padding: 30px 20px 30px; /* gives a little breathing room from edges of screen */
}

.navbar {
height: fit-content; /* overrides small-screen fixed height */
width: 25%; /* ensures left sidebar takes up 25% of container */
background: inherit; /* overrides small-screen background color */
flex-direction: column; /* overrides small-screen layout - now elements flow go top to bottom */
}

.content {
/* now content container has fixed width relative to container.
note that 5% of the container's width is not taken up by sidebar or content.
this gives (scaling) padding between sidebar and content. */
width: 70%;
}

.headshotContainer {
height: 140px; /* ensures the headshot is 140x140 (width follows height automatically) */
}

.navMenu {
position: relative; /* the nav menu will now flow with the other elements within the left sidebar */
top: inherit; /* the nav menu is no longer anchored to the top navbar (it flows) */
max-height: inherit; /* the nav menu no longer needs to control max-height - it will never transition in or out */
background: inherit; /* removes the background color that was meant to match the top navbar */
}

.navMenu a {
font-weight: normal; /* resets nav menu link font weight from small screen-specific bold style */
color: revert; /* ensures color is now set by nature as a link (blue / purple) instead of small screen black */
padding: inherit; /* removes small-screen specific padding that the links had in the nav menu */
display: inherit; /* links no longer need to be blocks (no longer need padding) */
}

.menuIcon, .homeLink, .nav hr {
display: none; /* these elements only apply to small screen form of navbar / nav menu */
}
}
34 changes: 34 additions & 0 deletions scripts/useOnClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RefObject, useEffect } from 'react';

/**
* Hook that will execute the given callback if user clicks or touches outside
* of the given ref(s).
*/
export default function useOnClickOutside(
onClickOutside: (event: MouseEvent) => void,
ref: RefObject<Node>,
...addlRefs: RefObject<Node>[]
): void {
const refs = [ref].concat(addlRefs);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const withinSomeRef = refs.some(
(ref) =>
ref.current && ref.current.contains((event?.target as Node) || null)
);
if (!withinSomeRef) {
onClickOutside(event);
}
}

// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, refs);
}
Loading