├── src
├── state
│ ├── index.js
│ ├── Theme.js
│ └── Menu.js
├── About.js
├── theme.js
├── hooks
│ ├── index.js
│ ├── useToggle.js
│ ├── useLocalStorage.js
│ ├── useScrollFreeze.js
│ ├── useTheme.js
│ ├── useMedia.js
│ └── useScroll.js
├── index.js
├── index.css
├── components
│ ├── nav
│ │ ├── index.js
│ │ ├── MobileNav.js
│ │ ├── DesktopNav.js
│ │ └── NavLinks.js
│ └── Icon.js
├── App.js
└── logo.svg
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── manifest.json
└── index.html
├── .gitignore
├── README.md
└── package.json
/src/state/index.js:
--------------------------------------------------------------------------------
1 | export * from "./Menu";
2 | export * from "./Theme";
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trujic1000/react-navbar/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trujic1000/react-navbar/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trujic1000/react-navbar/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/About.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const About = () => {
4 | return
About page
;
5 | };
6 |
7 | export default About;
8 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | export const lightTheme = {
2 | bg: "#fff",
3 | text: "#000",
4 | };
5 |
6 | export const darkTheme = {
7 | bg: "#232323",
8 | text: "#fff",
9 | };
10 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export * from "./useLocalStorage";
2 | export * from "./useScroll";
3 | export * from "./useScrollFreeze";
4 | export * from "./useTheme";
5 | export * from "./useToggle";
6 | export * from "./useMedia";
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --text: #fff;
3 | --bg: #232323;
4 | --headerBg: #1c1c1c;
5 | --headerBoxShadow: rgba(0, 0, 0, 0.8) 0 1px 4px;
6 | }
7 |
8 | body.light {
9 | --text: #000;
10 | --bg: #f6f7f8;
11 | --headerBg: #fff;
12 | --headerBoxShadow: rgba(0, 0, 0, 0.15) 0 1px 4px;
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/useToggle.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export const useToggle = (initialState) => {
4 | const [isToggled, setToggle] = useState(initialState);
5 | const toggle = () => setToggle((prevState) => !prevState);
6 | // return [isToggled, toggle];
7 | return { isToggled, setToggle, toggle };
8 | };
9 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | export const useLocalStorage = (key, initialValue) => {
4 | const item = window.localStorage.getItem(key);
5 | const [value, setValue] = useState(item || initialValue);
6 |
7 | useEffect(() => {
8 | window.localStorage.setItem(key, value);
9 | }, [value, key]);
10 | return [value, setValue];
11 | };
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## React Responsive Navbar
2 |
3 | This project uses the following technologies:
4 |
5 | - [React](https://reactjs.org) for frontend
6 | - [Styled Components](https://styled-components.com/) for styling the app
7 |
8 | ## Quick Start
9 |
10 | ```javascript
11 | // Install dependencies
12 | npm install or yarn
13 |
14 | // Run client
15 | npm run dev
16 |
17 | // Server runs on http://localhost:3000
18 | ```
19 |
--------------------------------------------------------------------------------
/src/hooks/useScrollFreeze.js:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from "react";
2 |
3 | export const useScrollFreeze = (isMenuOpen) => {
4 | useLayoutEffect(() => {
5 | const original = window.getComputedStyle(document.body).overflow;
6 |
7 | if (isMenuOpen) {
8 | document.body.style.overflow = "hidden";
9 | }
10 |
11 | return () => {
12 | document.body.style.overflow = original;
13 | };
14 | }, [isMenuOpen]);
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/nav/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import DesktopNav from "./DesktopNav";
5 | import MobileNav from "./MobileNav";
6 |
7 | const Navbar = () => {
8 | return (
9 |
13 | );
14 | };
15 |
16 | export default Navbar;
17 |
18 | const Nav = styled.div`
19 | display: flex;
20 | flex-flow: column nowrap;
21 | `;
22 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useLocalStorage } from "./useLocalStorage";
3 |
4 | export const useTheme = () => {
5 | const [theme, setTheme] = useLocalStorage("theme", "dark");
6 |
7 | const toggleTheme = () =>
8 | setTheme((prevTheme) => (prevTheme === "dark" ? "light" : "dark"));
9 |
10 | useEffect(() => {
11 | document.body.className = "";
12 | document.body.classList.add(theme);
13 | }, [theme]);
14 |
15 | return [theme, toggleTheme];
16 | };
17 |
--------------------------------------------------------------------------------
/src/hooks/useMedia.js:
--------------------------------------------------------------------------------
1 | import { useState, useLayoutEffect } from "react";
2 |
3 | export const useMedia = () => {
4 | const [isMobile, setMobile] = useState(false);
5 |
6 | const onResize = () => {
7 | const isMobile = window.innerWidth < 768;
8 | setMobile(isMobile);
9 | };
10 |
11 | useLayoutEffect(() => {
12 | window.addEventListener("resize", onResize);
13 | return () => {
14 | window.removeEventListener("resize", onResize);
15 | };
16 | }, []);
17 |
18 | return { isMobile };
19 | };
20 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/state/Theme.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from "react";
2 | import { useTheme } from "../hooks";
3 |
4 | const initialState = {
5 | theme: "dark",
6 | toggleTheme: () => {},
7 | };
8 |
9 | export const ThemeContext = createContext(initialState);
10 |
11 | export const ThemeStateProvider = ({ children }) => {
12 | const [theme, toggleTheme] = useTheme();
13 | return (
14 |
20 | {children}
21 |
22 | );
23 | };
24 |
25 | export const useThemeContext = () => {
26 | return useContext(ThemeContext);
27 | };
28 |
--------------------------------------------------------------------------------
/src/state/Menu.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from "react";
2 | import { useToggle } from "../hooks";
3 |
4 | const initialState = {
5 | isMenuOpen: false,
6 | toggle: () => {},
7 | closeMenu: () => {},
8 | };
9 |
10 | export const MenuContext = createContext(initialState);
11 |
12 | export const MenuProvider = ({ children }) => {
13 | const { isToggled, setToggle, toggle } = useToggle(false);
14 | const closeMenu = () => setToggle(false);
15 | return (
16 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | export const useMenuContext = () => {
29 | return useContext(MenuContext);
30 | };
31 |
--------------------------------------------------------------------------------
/src/hooks/useScroll.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | export const useScroll = () => {
4 | const [isScrolled, setIsScrolled] = useState(false);
5 |
6 | const onScroll = () => {
7 | const scrollTop = window !== undefined ? window.pageYOffset : 0;
8 |
9 | setIsScrolled(scrollTop > 0);
10 | };
11 |
12 | useEffect(() => {
13 | // Learn more about how { passive: true } improves scrolling performance
14 | // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners
15 | window.addEventListener("scroll", onScroll, { passive: true });
16 | return () => {
17 | window.removeEventListener("scroll", onScroll, { passive: true });
18 | };
19 | }, []);
20 |
21 | return { isScrolled };
22 | };
23 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
19 | React Responsive Navbar
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/nav/MobileNav.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import styled from "styled-components";
3 | import { useMenuContext } from "../../state";
4 | import { useScrollFreeze, useMedia } from "../../hooks";
5 | import NavLinks from "./NavLinks";
6 |
7 | const MobileNavbar = () => {
8 | const { isMenuOpen, closeMenu } = useMenuContext();
9 | const { isMobile } = useMedia();
10 | useScrollFreeze(isMenuOpen);
11 |
12 | useEffect(() => {
13 | if (!isMobile) {
14 | closeMenu();
15 | }
16 | }, [isMobile]);
17 |
18 | return (
19 | <>
20 | {isMenuOpen && (
21 |
22 |
23 |
24 | )}
25 | >
26 | );
27 | };
28 |
29 | export default MobileNavbar;
30 |
31 | const MobileNav = styled.nav`
32 | position: fixed;
33 | top: 0;
34 | left: 0;
35 | height: 100%;
36 | width: 100%;
37 | background: var(--bg);
38 | display: flex;
39 | justify-content: center;
40 | align-items: center;
41 | `;
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-navbar",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "hamburger-react": "^2.0.0",
10 | "prop-types": "^15.7.2",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-router-dom": "^5.1.2",
14 | "react-scripts": "3.4.1",
15 | "styled-components": "^5.1.0",
16 | "styled-reset": "^4.1.4"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": "react-app"
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router, Route } from "react-router-dom";
3 | import { createGlobalStyle } from "styled-components";
4 | import reset from "styled-reset";
5 | import { MenuProvider } from "./state";
6 | import Navbar from "./components/nav";
7 | import About from "./About";
8 |
9 | const GlobalStyle = createGlobalStyle`
10 | ${reset};
11 |
12 | html {
13 | box-sizing: border-box;
14 | }
15 |
16 | body {
17 | font-family: "Montserrat", sans-serif;
18 | -webkit-font-smoothing: antialiased;
19 | -moz-osx-font-smoothing: grayscale;
20 | background-color: var(--bg);
21 | color: var(--text);
22 | }
23 |
24 | * {
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
29 | *,
30 | *::before,
31 | *::after {
32 | box-sizing: inherit;
33 | }
34 |
35 | a {
36 | text-decoration: none;
37 | }
38 | `;
39 |
40 | const App = () => {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
Blabla
49 |
Blabla
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/src/components/nav/DesktopNav.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled, { css } from "styled-components";
4 | import { useMenuContext } from "../../state";
5 | import { Squash as Hamburger } from "hamburger-react";
6 | import NavLinks from "./NavLinks";
7 | import { useScroll } from "../../hooks";
8 |
9 | const DesktopNavbar = () => {
10 | const { isMenuOpen, toggleMenu } = useMenuContext();
11 | const { isScrolled } = useScroll();
12 | return (
13 |
14 |
15 | Logo
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default DesktopNavbar;
24 |
25 | const DesktopNav = styled.nav`
26 | display: flex;
27 | flex-flow: row nowrap;
28 | justify-content: space-around;
29 | align-items: center;
30 |
31 | background: var(--bg);
32 | color: var(--text);
33 | transition: all 150ms linear;
34 |
35 | ${(props) =>
36 | props.isScrolled &&
37 | css`
38 | background: var(--headerBg);
39 | box-shadow: var(--headerBoxShadow);
40 | `}
41 |
42 | position: fixed;
43 | top: 0;
44 | left: 0;
45 | width: 100%;
46 | height: 64px;
47 | padding: 0 60px;
48 | z-index: 2;
49 |
50 | @media screen and (max-width: 768px) {
51 | justify-content: space-between;
52 | padding: 0 30px;
53 | }
54 |
55 | .logo {
56 | flex: 2;
57 | color: var(--text);
58 | font-size: 32px;
59 | }
60 |
61 | .nav-links {
62 | @media screen and (max-width: 768px) {
63 | display: none;
64 | }
65 | }
66 |
67 | .hamburger-react {
68 | display: none;
69 | z-index: 99;
70 | & > div > div {
71 | background: var(--text) !important;
72 | }
73 | @media screen and (max-width: 768px) {
74 | display: block;
75 | }
76 | }
77 | `;
78 |
--------------------------------------------------------------------------------
/src/components/nav/NavLinks.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import { useMenuContext } from "../../state";
5 | import { useTheme } from "../../hooks";
6 | import Icon from "../Icon";
7 |
8 | export const links = ["home", "about", "contact"];
9 |
10 | const DesktopNavLinks = () => {
11 | const { closeMenu } = useMenuContext();
12 | const [theme, toggleTheme] = useTheme();
13 |
14 | return (
15 |
16 | {links.map((link) => (
17 |
18 |
19 | {link}
20 |
21 |
22 | ))}
23 |
24 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default DesktopNavLinks;
33 |
34 | const NavLinksWrapper = styled.ul`
35 | flex: 1;
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | list-style: none;
40 |
41 | li:not(:last-child) {
42 | margin-right: 26px;
43 | }
44 |
45 | li:last-child {
46 | margin-left: auto;
47 | }
48 |
49 | button {
50 | background: transparent;
51 | outline: none;
52 | border: none;
53 | cursor: pointer;
54 | }
55 |
56 | @media screen and (max-width: 768px) {
57 | flex-direction: column;
58 | li {
59 | padding: 12px;
60 | margin: 0 !important;
61 | }
62 | }
63 | `;
64 |
65 | export const NavLink = styled(Link)`
66 | position: relative;
67 | color: white;
68 | text-decoration: none;
69 | text-transform: capitalize;
70 | color: var(--text);
71 | &::before {
72 | content: "";
73 | display: block;
74 | position: absolute;
75 | left: 0;
76 | bottom: -2px;
77 | height: 2px;
78 | width: 0;
79 | background: var(--text);
80 | transition: width 150ms linear;
81 | }
82 | &:hover::before {
83 | width: 100%;
84 | }
85 | `;
86 |
--------------------------------------------------------------------------------
/src/components/Icon.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Icon = ({ name, size = "24" }) => {
4 | switch (name) {
5 | case "day":
6 | return (
7 |
18 | );
19 | case "night":
20 | return (
21 |
29 | );
30 | default:
31 | return "Icon does not exist";
32 | }
33 | };
34 |
35 | export default Icon;
36 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------