Переглянути джерело

feat: add Login, Register and ForgotPassword page

hover-contact
ntasicc 3 роки тому
джерело
коміт
55c3c029f6

+ 1
- 0
.gitignore Переглянути файл

@@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel

+ 76
- 0
components/forms/forgot-password/ForgotPasswordForm.jsx Переглянути файл

@@ -0,0 +1,76 @@
import {
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import Link from 'next/link';
import React from 'react';
import { LOGIN_PAGE } from '../../../constants/pages';
import { forgotPasswordSchema } from '../../../schemas/forgotPasswordSchema';

const ForgotPasswordForm = () => {
const handleSubmit = (values) => {
console.log('Values', values);
};

const formik = useFormik({
initialValues: {
email: '',
},
validationSchema: forgotPasswordSchema,
onSubmit: handleSubmit,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
Forgot password
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="email"
label="Email"
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
autoFocus
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
Send email
</Button>
<Grid container justifyContent="center">
<Link href={LOGIN_PAGE}>Back</Link>
</Grid>
</Box>
</Box>
</Container>
);
};

export default ForgotPasswordForm;

+ 5
- 0
components/forms/forgot-password/ForgotPasswordForm.mock.js Переглянути файл

@@ -0,0 +1,5 @@
const base = {};

export const mockForgotPasswordFormProps = {
base,
};

+ 20
- 0
components/forms/forgot-password/ForgotPasswordForm.stories.jsx Переглянути файл

@@ -0,0 +1,20 @@
import ForgotPasswordForm from './ForgotPasswordForm';
import { mockForgotPasswordFormProps } from './ForgotPasswordForm.mock';

const obj = {
title: 'forms/ForgotPasswordForm',
component: ForgotPasswordForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <ForgotPasswordForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockForgotPasswordFormProps.base,
};

+ 139
- 0
components/forms/login/LoginForm.jsx Переглянути файл

@@ -0,0 +1,139 @@
import {
Box,
Button,
Container,
Grid,
IconButton,
InputAdornment,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { signIn } from 'next-auth/react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import {
BASE_PAGE,
FORGOT_PASSWORD_PAGE,
REGISTER_PAGE,
} from '../../../constants/pages';
import { loginSchema } from '../../../schemas/loginSchema';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

const LoginForm = () => {
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const router = useRouter();
const [error, setError] = useState({ hasError: false, errorMessage: '' });

const submitHandler = async (values) => {
const result = await signIn('credentials', {
redirect: false,
username: values.username,
password: values.password,
});
if (!result.error) {
router.replace(BASE_PAGE);
} else {
setError({ hasError: true, errorMessage: result.error });
}
};

const formik = useFormik({
initialValues: {
username: '',
password: '',
},
validationSchema: loginSchema,
onSubmit: submitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
Login
</Typography>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="username"
label="Username"
margin="normal"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
autoFocus
fullWidth
/>
<TextField
name="password"
label="Password"
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
Login
</Button>
<Grid container>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'left' } }}
>
<Link href={FORGOT_PASSWORD_PAGE}>Forgot your password?</Link>
</Grid>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'right' } }}
>
<Link href={REGISTER_PAGE}>Dont have an account?</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

export default LoginForm;

+ 5
- 0
components/forms/login/LoginForm.mock.js Переглянути файл

@@ -0,0 +1,5 @@
const base = {};

export const mockLoginFormProps = {
base,
};

+ 20
- 0
components/forms/login/LoginForm.stories.jsx Переглянути файл

@@ -0,0 +1,20 @@
import LoginForm from './LoginForm';
import { mockLoginFormProps } from './LoginForm.mock';

const obj = {
title: 'forms/LoginForm',
component: LoginForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <LoginForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockLoginFormProps.base,
};

+ 190
- 0
components/forms/register/RegisterForm.jsx Переглянути файл

@@ -0,0 +1,190 @@
import {
Box,
Button,
Container,
Grid,
IconButton,
InputAdornment,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import Link from 'next/link';
import { useState } from 'react';

import { FORGOT_PASSWORD_PAGE, LOGIN_PAGE } from '../../../constants/pages';
import { createUser } from '../../../requests/accountRequests';
import { registerSchema } from '../../../schemas/registerSchema';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

const RegisterForm = () => {
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleClickShowConfirmPassword = () =>
setShowConfirmPassword(!showConfirmPassword);
const handleMouseDownConfirmPassword = () =>
setShowConfirmPassword(!showConfirmPassword);

const [error, setError] = useState({ hasError: false, errorMessage: '' });

const submitHandler = async (values) => {
try {
const result = await createUser(
values.fullName,
values.username,
values.email,
values.password
);
console.log(result);
} catch (error) {
setError({ hasError: true, errorMessage: error.message });
}
};

const formik = useFormik({
initialValues: {
fullName: '',
username: '',
email: '',
password: '',
confirmPassword: '',
},
validationSchema: registerSchema,
onSubmit: submitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 10,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
Register
</Typography>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="fullName"
label="Full name"
margin="normal"
value={formik.values.fullName}
onChange={formik.handleChange}
error={formik.touched.fullName && Boolean(formik.errors.fullName)}
helperText={formik.touched.fullName && formik.errors.fullName}
autoFocus
fullWidth
/>
<TextField
name="username"
label="Username"
margin="normal"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
fullWidth
/>
<TextField
name="email"
label="Email"
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
fullWidth
/>
<TextField
name="password"
label="Password"
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<TextField
name="confirmPassword"
label="Confirm password"
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.confirmPassword}
onChange={formik.handleChange}
error={
formik.touched.confirmPassword &&
Boolean(formik.errors.confirmPassword)
}
helperText={
formik.touched.confirmPassword && formik.errors.confirmPassword
}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowConfirmPassword}
onMouseDown={handleMouseDownConfirmPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
Register
</Button>
<Grid container>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'left' } }}
>
<Link href={FORGOT_PASSWORD_PAGE}>Forgot your password?</Link>
</Grid>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'right' } }}
>
<Link href={LOGIN_PAGE}>Already have an account?</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

export default RegisterForm;

+ 5
- 0
components/forms/register/RegisterForm.mock.js Переглянути файл

@@ -0,0 +1,5 @@
const base = {};

export const mockRegisterFormProps = {
base,
};

+ 20
- 0
components/forms/register/RegisterForm.stories.jsx Переглянути файл

@@ -0,0 +1,20 @@
import RegisterForm from './RegisterForm';
import { mockRegisterFormProps } from './RegisterForm.mock';

const obj = {
title: 'forms/RegisterForm',
component: RegisterForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <RegisterForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockRegisterFormProps.base,
};

+ 15
- 0
components/mui/ErrorMessageComponent.jsx Переглянути файл

@@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Typography } from '@mui/material';

const ErrorMessageComponent = ({ error }) => (
<Typography variant="body1" color="error" my={2}>
{error}
</Typography>
);

ErrorMessageComponent.propTypes = {
error: PropTypes.string.isRequired,
};

export default ErrorMessageComponent;

+ 3
- 2
components/templates/base/BaseTemplate.stories.jsx Переглянути файл

@@ -1,13 +1,14 @@
import BaseTemplate from './BaseTemplate';
import { mockBaseTemplateProps } from './BaseTemplate.mocks';

export default {
const obj = {
title: 'templates/BaseTemplate',
component: BaseTemplate,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
};
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <BaseTemplate {...args} />;


+ 5
- 0
constants/pages.js Переглянути файл

@@ -0,0 +1,5 @@
export const BASE_PAGE = "/";
export const LOGIN_PAGE = "/auth";
export const PROFILE_PAGE = "/profile";
export const REGISTER_PAGE = "/auth/register";
export const FORGOT_PASSWORD_PAGE = "/auth/forgot-password";

+ 17
- 1
package.json Переглянути файл

@@ -13,12 +13,27 @@
"build-storybook": "build-storybook"
},
"dependencies": {
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@mui/codemod": "^5.8.7",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.9.2",
"bcryptjs": "^2.4.3",
"date-fns": "^2.29.1",
"formik": "^2.2.9",
"mongodb": "^4.8.1",
"next": "12.2.3",
"next-auth": "^4.10.2",
"prop-types": "^15.8.1",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"sass": "^1.54.0",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@storybook/addon-actions": "^6.5.9",
@@ -30,6 +45,7 @@
"@storybook/react": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"babel-loader": "^8.2.5",
"babel-plugin-import": "^1.13.5",
"cross-env": "^7.0.3",
"eslint": "8.21.0",
"eslint-config-next": "12.2.3",

+ 42
- 0
pages/api/auth/[...nextauth].js Переглянути файл

@@ -0,0 +1,42 @@
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';

import { connectToDatabase } from '../../../utils/helpers/dbHelpers';
import { verifyPassword } from '../../../utils/helpers/hashPasswordHelpers';

export default NextAuth({
session: {
jwt: true,
},
providers: [
Credentials({
async authorize(credentials) {
const client = await connectToDatabase();

const usersCollection = client.db().collection('users');

const user = await usersCollection.findOne({
username: credentials.username,
});

if (!user) {
client.close();
throw new Error('No user found!');
}

const isValid = await verifyPassword(
credentials.password,
user.password
);

if (!isValid) {
client.close();
throw new Error('Could not log you in!');
}

client.close();
return { name: user.fullName };
},
}),
],
});

+ 51
- 0
pages/api/auth/signup.js Переглянути файл

@@ -0,0 +1,51 @@
import { connectToDatabase } from '../../../utils/helpers/dbHelpers';
import { hashPassword } from '../../../utils/helpers/hashPasswordHelpers';

async function handler(req, res) {
if (req.method !== 'POST') {
return;
}

const { fullName, username, email, password } = req.body;

if (
!fullName ||
!username ||
!email ||
!email.includes('@') ||
!password ||
password.trim().length < 7
) {
res.status(422).json({
message: 'Invalid input ',
});
return;
}

const client = await connectToDatabase();
const db = client.db();

const existingUser = await db
.collection('users')
.findOne({ $or: [{ email: email }, { username: username }] });

if (existingUser) {
res.status(422).json({ message: 'User exists already!' });
client.close();
return;
}

const hashedPassword = await hashPassword(password);

const result = await db.collection('users').insertOne({
fullName: fullName,
username: username,
email: email,
password: hashedPassword,
});

res.status(201).json({ message: 'Created user!', result: result });
client.close();
}

export default handler;

+ 21
- 0
pages/auth/forgot-password/index.js Переглянути файл

@@ -0,0 +1,21 @@
import { getSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import ForgotPasswordForm from '../../../components/forms/forgot-password/ForgotPasswordForm';
import { BASE_PAGE } from '../../../constants/pages';

const ForgotPasswordPage = () => {
const router = useRouter();

useEffect(() => {
getSession().then((session) => {
if (session) {
router.replace(BASE_PAGE);
}
});
}, [router]);

return <ForgotPasswordForm />;
};

export default ForgotPasswordPage;

+ 21
- 0
pages/auth/index.js Переглянути файл

@@ -0,0 +1,21 @@
import { getSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import LoginForm from '../../components/forms/login/LoginForm';
import { BASE_PAGE } from '../../constants/pages';

const AuthPage = () => {
const router = useRouter();

useEffect(() => {
getSession().then((session) => {
if (session) {
router.replace(BASE_PAGE);
}
});
}, [router]);

return <LoginForm />;
};

export default AuthPage;

+ 21
- 0
pages/auth/register/index.js Переглянути файл

@@ -0,0 +1,21 @@
import { getSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import RegisterForm from '../../../components/forms/register/RegisterForm';
import { BASE_PAGE } from '../../../constants/pages';

const RegisterPage = () => {
const router = useRouter();

useEffect(() => {
getSession().then((session) => {
if (session) {
router.replace(BASE_PAGE);
}
});
}, [router]);

return <RegisterForm />;
};

export default RegisterPage;

+ 12
- 66
pages/index.js Переглянути файл

@@ -1,69 +1,15 @@
import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';
import { signOut } from 'next-auth/react';

export default function Home() {
const Home = () => {
function logoutHandler() {
signOut();
}
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>

<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>

<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.js</code>
</p>

<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>

<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>

<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>

<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>

<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
<>
<h1>Home</h1>
<button onClick={logoutHandler}>Logout</button>
</>
);
}
};

export default Home;

+ 19
- 0
requests/accountRequests.js Переглянути файл

@@ -0,0 +1,19 @@
import apiEndpoints from "./apiEndpoints";

export const createUser = async (fullName, username, email, password) => {
const response = await fetch(apiEndpoints.account.createUser, {
method: "POST",
body: JSON.stringify({ fullName, username, email, password }),
headers: {
"Content-Type": "application/json",
},
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.message || "Something went wrong!");
}

return data;
};

+ 5
- 0
requests/apiEndpoints.js Переглянути файл

@@ -0,0 +1,5 @@
export default {
account: {
createUser: "/api/auth/signup",
},
};

+ 5
- 0
schemas/forgotPasswordSchema.js Переглянути файл

@@ -0,0 +1,5 @@
import * as Yup from "yup";

export const forgotPasswordSchema = Yup.object().shape({
email: Yup.string().required("Email is required").email(),
});

+ 6
- 0
schemas/loginSchema.js Переглянути файл

@@ -0,0 +1,6 @@
import * as Yup from "yup";

export const loginSchema = Yup.object().shape({
username: Yup.string().required("Username is required"),
password: Yup.string().required("Password is required"),
});

+ 12
- 0
schemas/registerSchema.js Переглянути файл

@@ -0,0 +1,12 @@
import * as Yup from "yup";

export const registerSchema = Yup.object().shape({
fullName: Yup.string().required("Full name is required"),
username: Yup.string().required("Username is required"),
email: Yup.string().email().required("Email is required"),
password: Yup.string().required("Password is required"),
confirmPassword: Yup.string().oneOf(
[Yup.ref("password"), null],
"Passwords must match"
),
});

+ 0
- 129
styles/Home.module.css Переглянути файл

@@ -1,129 +0,0 @@
.container {
padding: 0 2rem;
}

.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}

.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}

.title a {
color: #0070f3;
text-decoration: none;
}

.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}

.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}

.title,
.description {
text-align: center;
}

.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}

.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}

.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}

.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}

.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}

.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}

.logo {
height: 1em;
margin-left: 0.5rem;
}

@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}

+ 12
- 20
styles/globals.css Переглянути файл

@@ -1,26 +1,18 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
color: inherit;
text-decoration: none;
}
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@700&family=Open+Sans:wght@400;700&display=swap');

* {
box-sizing: border-box;
}

@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
body {
font-family: 'Open Sans', 'Lato', sans-serif;
}

h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Lato', sans-serif;
}

+ 7
- 0
utils/helpers/dbHelpers.js Переглянути файл

@@ -0,0 +1,7 @@
import { MongoClient } from "mongodb";

export async function connectToDatabase() {
const client = await MongoClient.connect(process.env.MONGODB_AUTH);

return client;
}

+ 11
- 0
utils/helpers/hashPasswordHelpers.js Переглянути файл

@@ -0,0 +1,11 @@
import { hash, compare } from "bcryptjs";

export async function hashPassword(password) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}

export async function verifyPassword(password, hashedPassword) {
const isValid = await compare(password, hashedPassword);
return isValid;
}

+ 659
- 35
yarn.lock
Різницю між файлами не показано, бо вона завелика
Переглянути файл


Завантаження…
Відмінити
Зберегти