Skip to content
Open
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
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
142 changes: 23 additions & 119 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,130 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Bower dependency directory (https://bower.io/)
bower_components
# dependencies
/node_modules
/.pnp
.pnp.js

# node-waf configuration
.lock-wscript
# testing
/coverage

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# next.js
/.next/
/out/

# Dependency directories
node_modules/
jspm_packages/
# production
/build

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# misc
.DS_Store
*.pem

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# dotenv environment variable files
.env
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# vercel
.vercel
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License
The MIT License (MIT)

Copyright (c) 2023 CTH-Controls-Inc
Copyright (c) 2021 Jason Watmore

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
22 changes: 3 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
# Dev Test
# next-js-11-registration-login-example

## Exercise
Next.js 11 - User Registration and Login Example

Create a basic web application using next.js + React to display a login page. The login page should include fields for email and password. The page doesn't need to have actual functionality, but it should have the following feature:

- When the user enters a value that is not a valid email address in the email field, the box should have a red border and display a notification informing the user that the entered value is not a valid email.

You have the freedom to choose any component library you prefer.

Additionally, please include a Dockerfile that enables us to run the next application easily by running the Docker container.

# Other notes

While we are not monitoring the time, the exercise should take you less than an hour to complete. Consider things like code quality, proper git usage (such a granular commits, meaningful commit messages etc.), ease of use etc.

## How to submit

1. Fork this repository to a public one on your account.
2. Complete the exercise.
3. When you're done, submit your fork as a pull request back to this repository.
Documentation and live demo available at https://jasonwatmore.com/post/2021/08/19/next-js-11-user-registration-and-login-tutorial-with-example-app
116 changes: 116 additions & 0 deletions components/Alert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';

import { alertService, AlertType } from 'services';

export { Alert };

Alert.propTypes = {
id: PropTypes.string,
fade: PropTypes.bool
};

Alert.defaultProps = {
id: 'default-alert',
fade: true
};

function Alert({ id, fade }) {
const router = useRouter();
const [alerts, setAlerts] = useState([]);

useEffect(() => {
// subscribe to new alert notifications
const subscription = alertService.onAlert(id)
.subscribe(alert => {
// clear alerts when an empty alert is received
if (!alert.message) {
setAlerts(alerts => {
// filter out alerts without 'keepAfterRouteChange' flag
const filteredAlerts = alerts.filter(x => x.keepAfterRouteChange);

// set 'keepAfterRouteChange' flag to false on the rest
filteredAlerts.forEach(x => delete x.keepAfterRouteChange);
return filteredAlerts;
});
} else {
// add alert to array
setAlerts(alerts => ([...alerts, alert]));

// auto close alert if required
if (alert.autoClose) {
setTimeout(() => removeAlert(alert), 3000);
}
}
});


// clear alerts on location change
const clearAlerts = () => {
setTimeout(() => alertService.clear(id));
};
router.events.on('routeChangeStart', clearAlerts);

// clean up function that runs when the component unmounts
return () => {
// unsubscribe to avoid memory leaks
subscription.unsubscribe();
router.events.off('routeChangeStart', clearAlerts);
};

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

function removeAlert(alert) {
if (fade) {
// fade out alert
const alertWithFade = { ...alert, fade: true };
setAlerts(alerts => alerts.map(x => x === alert ? alertWithFade : x));

// remove alert after faded out
setTimeout(() => {
setAlerts(alerts => alerts.filter(x => x !== alertWithFade));
}, 250);
} else {
// remove alert
setAlerts(alerts => alerts.filter(x => x !== alert));
}
};

function cssClasses(alert) {
if (!alert) return;

const classes = ['alert', 'alert-dismissable'];

const alertTypeClass = {
[AlertType.Success]: 'alert-success',
[AlertType.Error]: 'alert-danger',
[AlertType.Info]: 'alert-info',
[AlertType.Warning]: 'alert-warning'
}

classes.push(alertTypeClass[alert.type]);

if (alert.fade) {
classes.push('fade');
}

return classes.join(' ');
}

if (!alerts.length) return null;

return (
<div className="container">
<div className="m-3">
{alerts.map((alert, index) =>
<div key={index} className={cssClasses(alert)}>
<a className="close" onClick={() => removeAlert(alert)}>&times;</a>
<span dangerouslySetInnerHTML={{ __html: alert.message }}></span>
</div>
)}
</div>
</div>
);
}
13 changes: 13 additions & 0 deletions components/Link.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import NextLink from 'next/link';

export { Link };

function Link({ href, children, ...props }) {
return (
<NextLink href={href}>
<a {...props}>
{children}
</a>
</NextLink>
);
}
32 changes: 32 additions & 0 deletions components/Nav.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState, useEffect } from 'react';

import { NavLink } from '.';
import { userService } from 'services';

export { Nav };

function Nav() {
const [user, setUser] = useState(null);

useEffect(() => {
const subscription = userService.user.subscribe(x => setUser(x));
return () => subscription.unsubscribe();
}, []);

function logout() {
userService.logout();
}

// only show nav when logged in
if (!user) return null;

return (
<nav className="navbar navbar-expand navbar-dark bg-dark">
<div className="navbar-nav">
<NavLink href="/" exact className="nav-item nav-link">Home</NavLink>
<NavLink href="/users" className="nav-item nav-link">Users</NavLink>
<a onClick={logout} className="nav-item nav-link">Logout</a>
</div>
</nav>
);
}
26 changes: 26 additions & 0 deletions components/NavLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';

import { Link } from '.';

export { NavLink };

NavLink.propTypes = {
href: PropTypes.string.isRequired,
exact: PropTypes.bool
};

NavLink.defaultProps = {
exact: false
};

function NavLink({ children, href, exact, ...props }) {
const { pathname } = useRouter();
const isActive = exact ? pathname === href : pathname.startsWith(href);

if (isActive) {
props.className += ' active';
}

return <Link href={href} {...props}>{children}</Link>;
}
9 changes: 9 additions & 0 deletions components/Spinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { Spinner };

function Spinner() {
return (
<div className="text-center p-3">
<span className="spinner-border spinner-border-lg align-center"></span>
</div>
);
}
Loading