mirror of
https://github.com/rjNemo/devbook_ts
synced 2026-06-11 21:16:45 +00:00
Navigation (#4)
* specification test * set public and private links sets based on authentication state * enable frontend navigation * set frontend link in sign in/up and landing pages * refactor navbar tests * style 404 page
This commit is contained in:
parent
cdba48cc72
commit
1bde399408
13 changed files with 253 additions and 35 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,6 +8,8 @@
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
/cypress/integration/examples
|
/cypress/integration/examples
|
||||||
|
/cypress/fixtures/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React, {FC} from 'react';
|
||||||
import {BrowserRouter} from 'react-router-dom';
|
import {BrowserRouter} from 'react-router-dom';
|
||||||
import NavBar from './components/NavBar';
|
import NavBar from './components/NavBar';
|
||||||
import Router from './router/Router';
|
import Router from './router/Router';
|
||||||
|
|
||||||
/** Main App container */
|
/** Main App container */
|
||||||
const App = () => {
|
const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import {
|
||||||
faUser,
|
faUser,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
faGraduationCap,
|
faGraduationCap,
|
||||||
|
faExclamation,
|
||||||
|
faExclamationCircle,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faCode,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {faConnectdevelop} from '@fortawesome/free-brands-svg-icons';
|
import {faConnectdevelop} from '@fortawesome/free-brands-svg-icons';
|
||||||
|
|
||||||
|
|
@ -30,9 +34,15 @@ const Header: FC<IProps> = ({title, lead, icon = 'faUser'}) => {
|
||||||
if (icon === 'code-branch') {
|
if (icon === 'code-branch') {
|
||||||
return <FontAwesomeIcon icon={faCodeBranch} />;
|
return <FontAwesomeIcon icon={faCodeBranch} />;
|
||||||
}
|
}
|
||||||
|
if (icon === 'code') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (icon === 'graduation-cap') {
|
if (icon === 'graduation-cap') {
|
||||||
return <FontAwesomeIcon icon={faGraduationCap} />;
|
return <FontAwesomeIcon icon={faGraduationCap} />;
|
||||||
}
|
}
|
||||||
|
if (icon === 'not-found') {
|
||||||
|
return <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,77 @@
|
||||||
import React, {FC} from 'react';
|
import React, {FC} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
import {faCode} from '@fortawesome/free-solid-svg-icons';
|
import {faCode, faSignOutAlt, faUser} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import * as ROUTES from '../constants/routes';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isAuthenticated?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Main Navbar serves navigation routes.
|
* Main Navbar serves navigation routes.
|
||||||
*/
|
*/
|
||||||
const NavBar: FC = () => (
|
const NavBar: FC<IProps> = ({isAuthenticated = false, loading = false}) => {
|
||||||
<nav className="navbar bg-dark">
|
const publicLinks = (
|
||||||
<h1>
|
<ul data-testid="publicLinks">
|
||||||
<a href="dashboard.html">
|
|
||||||
<FontAwesomeIcon icon={faCode} /> DevBook
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul>
|
|
||||||
<li>
|
<li>
|
||||||
<a href="profiles.html">Developers</a>
|
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink">
|
||||||
|
Developers
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="register.html">Register</a>
|
<Link to={ROUTES.SIGN_UP} data-testid="signupLink">
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="login.html">Login</a>
|
<Link to={ROUTES.SIGN_IN} data-testid="loginLink">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
);
|
||||||
);
|
|
||||||
|
const privateLinks = (
|
||||||
|
<ul data-testid="privateLinks">
|
||||||
|
<li>
|
||||||
|
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink">
|
||||||
|
Developers
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to={ROUTES.POSTS} data-testid="postsLink">
|
||||||
|
Posts
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to={ROUTES.DASHBOARD} data-testid="dashboardLink">
|
||||||
|
<FontAwesomeIcon icon={faUser} />
|
||||||
|
<span className="hide-sm"> Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to={ROUTES.SIGN_IN} data-testid="logoutLink">
|
||||||
|
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||||
|
<span className="hide-sm"> Log out</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Display appropriated links after loading given authenticated prop */
|
||||||
|
const RenderLinks = !loading && isAuthenticated ? privateLinks : publicLinks;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar bg-dark">
|
||||||
|
<h1>
|
||||||
|
<Link to={ROUTES.LANDING} data-testid="homeLink">
|
||||||
|
<FontAwesomeIcon icon={faCode} /> DevBook
|
||||||
|
</Link>
|
||||||
|
</h1>
|
||||||
|
{RenderLinks}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default NavBar;
|
export default NavBar;
|
||||||
|
|
|
||||||
110
src/components/__tests__/NavBar.test.tsx
Normal file
110
src/components/__tests__/NavBar.test.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {BrowserRouter} from 'react-router-dom';
|
||||||
|
import NavBar from '../NavBar';
|
||||||
|
import {render, RenderResult} from '@testing-library/react';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
describe('Navbar displays', () => {
|
||||||
|
let context: RenderResult;
|
||||||
|
let navProps: IProps;
|
||||||
|
|
||||||
|
describe('before loading', () => {
|
||||||
|
navProps = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
context = render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<NavBar {...navProps} />
|
||||||
|
</BrowserRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('landing page link', () => {
|
||||||
|
const {getAllByTestId} = context;
|
||||||
|
const link = getAllByTestId('homeLink');
|
||||||
|
expect(link[0]).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no links while loading', () => {
|
||||||
|
const {queryByTestId} = context;
|
||||||
|
const links = queryByTestId('privateLinks');
|
||||||
|
expect(links).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when loaded', () => {
|
||||||
|
describe('when user is not authenticated', () => {
|
||||||
|
navProps = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
context = render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<NavBar {...navProps} />
|
||||||
|
</BrowserRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('developers link', () => {
|
||||||
|
const {getAllByTestId} = context;
|
||||||
|
const link = getAllByTestId('devsLink');
|
||||||
|
expect(link[0]).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('register link', () => {
|
||||||
|
const {getAllByTestId} = context;
|
||||||
|
const link = getAllByTestId('signupLink');
|
||||||
|
expect(link[0]).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login page link', () => {
|
||||||
|
const {getAllByTestId} = context;
|
||||||
|
const link = getAllByTestId('loginLink');
|
||||||
|
expect(link[0]).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// describe('when user is authenticated', () => {
|
||||||
|
// navProps = {
|
||||||
|
// isAuthenticated: true,
|
||||||
|
// loading: false,
|
||||||
|
// };
|
||||||
|
// beforeEach(() => {
|
||||||
|
// context = render(
|
||||||
|
// <BrowserRouter>
|
||||||
|
// <NavBar {...navProps} />
|
||||||
|
// </BrowserRouter>,
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// test('developers link', () => {
|
||||||
|
// const {getAllByTestId} = context;
|
||||||
|
// const link = getAllByTestId('devsLink');
|
||||||
|
// expect(link[0]).toBeTruthy();
|
||||||
|
// });
|
||||||
|
// // test('posts page link', () => {
|
||||||
|
// // const {getAllByTestId} = context;
|
||||||
|
// // const link = getAllByTestId('postsLink');
|
||||||
|
// // expect(link[0]).toBeTruthy();
|
||||||
|
// // });
|
||||||
|
// // test('dashboard page link', () => {
|
||||||
|
// // const {getAllByTestId} = context;
|
||||||
|
// // const link = getAllByTestId('dashboardLink');
|
||||||
|
// // expect(link[0]).toBeTruthy();
|
||||||
|
// // });
|
||||||
|
// // test('logout page link', () => {
|
||||||
|
// // const {getAllByTestId} = context;
|
||||||
|
// // const link = getAllByTestId('logoutLink');
|
||||||
|
// // expect(link[0]).toBeTruthy();
|
||||||
|
// // });
|
||||||
|
// // });
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import React, {FC} from 'react';
|
import React, {FC} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
import * as ROUTES from '../constants/routes';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing page
|
* Landing page
|
||||||
|
|
@ -7,18 +10,18 @@ const Landing: FC = () => (
|
||||||
<section className="landing">
|
<section className="landing">
|
||||||
<div className="dark-overlay">
|
<div className="dark-overlay">
|
||||||
<div className="landing-inner">
|
<div className="landing-inner">
|
||||||
<h1 className="x-large">DevBook</h1>
|
<Header
|
||||||
<p className="lead">
|
title="DevBook"
|
||||||
Create developer profiles, portfolio, share and get help from other
|
lead="Create developer profiles, portfolio, share and get help from other devs"
|
||||||
devs
|
icon="code"
|
||||||
</p>
|
/>
|
||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
<a href="register.html" className="btn btn-primary">
|
<Link to={ROUTES.SIGN_UP} className="btn btn-primary">
|
||||||
Sign up
|
Sign up
|
||||||
</a>
|
</Link>
|
||||||
<a href="login.html" className="btn btn-light">
|
<Link to={ROUTES.SIGN_IN} className="btn btn-light">
|
||||||
Login
|
Login
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
28
src/pages/NotFound.tsx
Normal file
28
src/pages/NotFound.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import * as ROUTES from '../constants/routes';
|
||||||
|
|
||||||
|
const NotFound: FC = () => (
|
||||||
|
<section className="not-found">
|
||||||
|
<div className="dark-overlay">
|
||||||
|
<div className="landing-inner">
|
||||||
|
<Header
|
||||||
|
title="Nothing Here"
|
||||||
|
lead="Sorry the page requested does not exist."
|
||||||
|
icon="not-found"
|
||||||
|
/>
|
||||||
|
<div className="buttons">
|
||||||
|
<Link to={ROUTES.SIGN_UP} className="btn btn-primary">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
<Link to={ROUTES.SIGN_IN} className="btn btn-light">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import React, {FC} from 'react';
|
import React, {FC} from 'react';
|
||||||
|
import * as ROUTES from '../constants/routes';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign in form
|
* Sign in form
|
||||||
|
|
@ -19,7 +21,7 @@ const SignIn: FC = () => (
|
||||||
<input type="submit" value="Login" className="btn btn-primary" />
|
<input type="submit" value="Login" className="btn btn-primary" />
|
||||||
</form>
|
</form>
|
||||||
<p className="my-1">
|
<p className="my-1">
|
||||||
Don't have an account? <a href="register.html">Sign in</a>
|
Don't have an account? <Link to={ROUTES.SIGN_UP}>Sign up</Link>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {FC} from 'react';
|
import React, {FC} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
import * as ROUTES from '../constants/routes';
|
||||||
/**
|
/**
|
||||||
* Sign up form
|
* Sign up form
|
||||||
*/
|
*/
|
||||||
|
|
@ -26,7 +27,7 @@ const SignUp: FC = () => (
|
||||||
<input type="submit" value="Register" className="btn btn-primary" />
|
<input type="submit" value="Register" className="btn btn-primary" />
|
||||||
</form>
|
</form>
|
||||||
<p className="my-1">
|
<p className="my-1">
|
||||||
Already have an account? <a href="login.html">Sign in</a>
|
Already have an account? <Link to={ROUTES.SIGN_IN}>Sign in</Link>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import AddExperience from '../pages/AddExperience';
|
||||||
import AddEducation from '../pages/AddEducation';
|
import AddEducation from '../pages/AddEducation';
|
||||||
import PostPage from '../pages/Post';
|
import PostPage from '../pages/Post';
|
||||||
import Posts from '../pages/Posts';
|
import Posts from '../pages/Posts';
|
||||||
|
import NotFound from '../pages/NotFound';
|
||||||
import * as ROUTES from '../constants/routes';
|
import * as ROUTES from '../constants/routes';
|
||||||
|
|
||||||
/** Register navigation paths accessible */
|
/** Register navigation paths accessible */
|
||||||
|
|
@ -27,6 +28,8 @@ const Router: FC = () => (
|
||||||
<Route exact path={ROUTES.ADD_EDUCATION} component={AddEducation} />
|
<Route exact path={ROUTES.ADD_EDUCATION} component={AddEducation} />
|
||||||
<Route exact path={ROUTES.POST} component={PostPage} />
|
<Route exact path={ROUTES.POST} component={PostPage} />
|
||||||
<Route exact path={ROUTES.POSTS} component={Posts} />
|
<Route exact path={ROUTES.POSTS} component={Posts} />
|
||||||
|
<Route exact path={ROUTES.POSTS} component={Posts} />
|
||||||
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
5
src/static/css/style.min.css
vendored
5
src/static/css/style.min.css
vendored
|
|
@ -540,3 +540,8 @@ img {
|
||||||
.post img {
|
.post img {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
.not-found {
|
||||||
|
position: relative;
|
||||||
|
background: url('../img/404.jpg') no-repeat center center/cover;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
src/static/img/404.jpg
Normal file
BIN
src/static/img/404.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
|
|
@ -1,7 +1,7 @@
|
||||||
@import "_config";
|
@import '_config';
|
||||||
@import "_utils";
|
@import '_utils';
|
||||||
@import "_form";
|
@import '_form';
|
||||||
@import "_mobile";
|
@import '_mobile';
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "Raleway", sans-serif;
|
font-family: 'Raleway', sans-serif;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
@ -62,7 +62,7 @@ img {
|
||||||
// landing
|
// landing
|
||||||
.landing {
|
.landing {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: url("../img/showcase.jpg") no-repeat center center/cover;
|
background: url('../img/showcase.jpg') no-repeat center center/cover;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
&-inner {
|
&-inner {
|
||||||
|
|
@ -93,7 +93,7 @@ img {
|
||||||
|
|
||||||
.profile-grid {
|
.profile-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "top top" "about about" "exp edu" "github github";
|
grid-template-areas: 'top top' 'about about' 'exp edu' 'github github';
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
|
|
||||||
.profile-top {
|
.profile-top {
|
||||||
|
|
@ -190,3 +190,9 @@ img {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.not-found {
|
||||||
|
position: relative;
|
||||||
|
background: url('../img/404.jpg') no-repeat center center/cover;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue