mirror of
https://github.com/rjNemo/devbook_ts
synced 2026-06-06 02:36:39 +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
|
||||
/coverage
|
||||
/cypress/integration/examples
|
||||
/cypress/fixtures/
|
||||
/cypress/screenshots/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, {FC} from 'react';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import NavBar from './components/NavBar';
|
||||
import Router from './router/Router';
|
||||
|
||||
/** Main App container */
|
||||
const App = () => {
|
||||
const App: FC = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<NavBar />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import {
|
|||
faUser,
|
||||
faCodeBranch,
|
||||
faGraduationCap,
|
||||
faExclamation,
|
||||
faExclamationCircle,
|
||||
faExclamationTriangle,
|
||||
faCode,
|
||||
} from '@fortawesome/free-solid-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') {
|
||||
return <FontAwesomeIcon icon={faCodeBranch} />;
|
||||
}
|
||||
if (icon === 'code') {
|
||||
return null;
|
||||
}
|
||||
if (icon === 'graduation-cap') {
|
||||
return <FontAwesomeIcon icon={faGraduationCap} />;
|
||||
}
|
||||
if (icon === 'not-found') {
|
||||
return <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,29 +1,77 @@
|
|||
import React, {FC} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
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.
|
||||
*/
|
||||
const NavBar: FC = () => (
|
||||
<nav className="navbar bg-dark">
|
||||
<h1>
|
||||
<a href="dashboard.html">
|
||||
<FontAwesomeIcon icon={faCode} /> DevBook
|
||||
</a>
|
||||
</h1>
|
||||
<ul>
|
||||
const NavBar: FC<IProps> = ({isAuthenticated = false, loading = false}) => {
|
||||
const publicLinks = (
|
||||
<ul data-testid="publicLinks">
|
||||
<li>
|
||||
<a href="profiles.html">Developers</a>
|
||||
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink">
|
||||
Developers
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="register.html">Register</a>
|
||||
<Link to={ROUTES.SIGN_UP} data-testid="signupLink">
|
||||
Register
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="login.html">Login</a>
|
||||
<Link to={ROUTES.SIGN_IN} data-testid="loginLink">
|
||||
Login
|
||||
</Link>
|
||||
</li>
|
||||
</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;
|
||||
|
|
|
|||
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 {Link} from 'react-router-dom';
|
||||
import * as ROUTES from '../constants/routes';
|
||||
import Header from '../components/Header';
|
||||
|
||||
/**
|
||||
* Landing page
|
||||
|
|
@ -7,18 +10,18 @@ const Landing: FC = () => (
|
|||
<section className="landing">
|
||||
<div className="dark-overlay">
|
||||
<div className="landing-inner">
|
||||
<h1 className="x-large">DevBook</h1>
|
||||
<p className="lead">
|
||||
Create developer profiles, portfolio, share and get help from other
|
||||
devs
|
||||
</p>
|
||||
<Header
|
||||
title="DevBook"
|
||||
lead="Create developer profiles, portfolio, share and get help from other devs"
|
||||
icon="code"
|
||||
/>
|
||||
<div className="buttons">
|
||||
<a href="register.html" className="btn btn-primary">
|
||||
<Link to={ROUTES.SIGN_UP} className="btn btn-primary">
|
||||
Sign up
|
||||
</a>
|
||||
<a href="login.html" className="btn btn-light">
|
||||
</Link>
|
||||
<Link to={ROUTES.SIGN_IN} className="btn btn-light">
|
||||
Login
|
||||
</a>
|
||||
</Link>
|
||||
</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 * as ROUTES from '../constants/routes';
|
||||
import Header from '../components/Header';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Sign in form
|
||||
|
|
@ -19,7 +21,7 @@ const SignIn: FC = () => (
|
|||
<input type="submit" value="Login" className="btn btn-primary" />
|
||||
</form>
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, {FC} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import Header from '../components/Header';
|
||||
|
||||
import * as ROUTES from '../constants/routes';
|
||||
/**
|
||||
* Sign up form
|
||||
*/
|
||||
|
|
@ -26,7 +27,7 @@ const SignUp: FC = () => (
|
|||
<input type="submit" value="Register" className="btn btn-primary" />
|
||||
</form>
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import AddExperience from '../pages/AddExperience';
|
|||
import AddEducation from '../pages/AddEducation';
|
||||
import PostPage from '../pages/Post';
|
||||
import Posts from '../pages/Posts';
|
||||
import NotFound from '../pages/NotFound';
|
||||
import * as ROUTES from '../constants/routes';
|
||||
|
||||
/** Register navigation paths accessible */
|
||||
|
|
@ -27,6 +28,8 @@ const Router: FC = () => (
|
|||
<Route exact path={ROUTES.ADD_EDUCATION} component={AddEducation} />
|
||||
<Route exact path={ROUTES.POST} component={PostPage} />
|
||||
<Route exact path={ROUTES.POSTS} component={Posts} />
|
||||
<Route exact path={ROUTES.POSTS} component={Posts} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
|
|
|
|||
5
src/static/css/style.min.css
vendored
5
src/static/css/style.min.css
vendored
|
|
@ -540,3 +540,8 @@ img {
|
|||
.post img {
|
||||
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 "_utils";
|
||||
@import "_form";
|
||||
@import "_mobile";
|
||||
@import '_config';
|
||||
@import '_utils';
|
||||
@import '_form';
|
||||
@import '_mobile';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: "Raleway", sans-serif;
|
||||
font-family: 'Raleway', sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
background-color: white;
|
||||
|
|
@ -62,7 +62,7 @@ img {
|
|||
// landing
|
||||
.landing {
|
||||
position: relative;
|
||||
background: url("../img/showcase.jpg") no-repeat center center/cover;
|
||||
background: url('../img/showcase.jpg') no-repeat center center/cover;
|
||||
height: 100vh;
|
||||
|
||||
&-inner {
|
||||
|
|
@ -93,7 +93,7 @@ img {
|
|||
|
||||
.profile-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;
|
||||
|
||||
.profile-top {
|
||||
|
|
@ -190,3 +190,9 @@ img {
|
|||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.not-found {
|
||||
position: relative;
|
||||
background: url('../img/404.jpg') no-repeat center center/cover;
|
||||
height: 100vh;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue