Skip to content

Commit

Permalink
Merge pull request #21 from BookHive-ufcg/feat/login
Browse files Browse the repository at this point in the history
Feat/login
  • Loading branch information
AmiltonCabral authored Sep 24, 2024
2 parents cbbea87 + 7de1e51 commit f557be2
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"next": "14.2.5",
"next-auth": "^4.24.7",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.3.0"
Expand Down
3 changes: 3 additions & 0 deletions src/app/*.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface CustomGlobalThis extends GlobalThis {
isLoggedIn: boolean;
}
42 changes: 42 additions & 0 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

const url = process.env.BACK_END_URL || 'http://localhost:8080';

const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'username' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
// TODO: Admin user, remove this
const admin = { id: "1", name: 'User', username: 'admin', password: 'admin' };
if (credentials?.username === admin.username && credentials?.password === admin.password) {
return admin;
}

const response = await fetch(`${url}/user/${credentials?.username}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const user = await response.json();

if (credentials?.username === user.username && credentials?.password === user.password) {
return user;
} else {
return null;
}
},
}),
],
pages: {
signIn: '/login',
},
});

export { handler as GET, handler as POST };
16 changes: 13 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Metamorphous } from "next/font/google";
import "./globals.css";
import Navbar from "@/components/Navbar";
import Header from "@/components/Header";
import AuthProvider from "@/components/AuthProvider";

const metamorphous = Metamorphous({
subsets: ["latin"],
Expand All @@ -14,6 +15,13 @@ export const metadata: Metadata = {
description: "Book Hive is a platform for book lovers",
};

if (
typeof window !== "undefined" &&
typeof window.localStorage["isLoggedIn"] === "undefined"
) {
window.localStorage["isLoggedIn"] = false;
}

export default function RootLayout({
children,
}: Readonly<{
Expand All @@ -22,9 +30,11 @@ export default function RootLayout({
return (
<html lang="en" className={metamorphous.className}>
<body>
<Header />
{children}
<Navbar />
<AuthProvider>
<Header />
{children}
<Navbar />
</AuthProvider>
</body>
</html>
);
Expand Down
41 changes: 39 additions & 2 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
"use client";

import styles from "./login.module.css";

import Input from "@/components/Input";
import Button from "@/components/Button";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react";

export default function Login() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);
setError(null);

const result = await signIn("credentials", {
redirect: false,
username: event.currentTarget.username.value,
password: event.currentTarget.password.value,
});

if (result?.ok) {
router.push("/");
window.localStorage["isLoggedIn"] = true;
} else {
setError("Login failed");
}

setLoading(false);
};

return (
<main>
<div className={styles.container}>
Expand All @@ -27,29 +57,36 @@ export default function Login() {
<div className={styles.rightContent}>
<h1 className={styles.title}>Sign in to BookHive</h1>
<p className={styles.subTitle}>Use your username and password</p>
<form className={styles.form}>
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.formGroup}>
<Input
type="text"
id="username"
name="username"
placeholder="Username"
label="Username"
required
/>
</div>
<div className={styles.formGroup}>
<Input
type="password"
id="password"
name="password"
placeholder="Password"
label="Password"
required
/>
</div>

<a className={styles.forgotPassword} href="#">
Forgot your password?
</a>

<Button className={styles.button}>Login</Button>
<Button type="submit" className={styles.button}>
{loading ? "Logging in..." : "Login"}
</Button>
{error && <p style={{ color: "red" }}>{error}</p>}
</form>

<div className={styles.separator}>
Expand Down
20 changes: 19 additions & 1 deletion src/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import SearchBar from "@/components/SearchBar";
import styles from "./search.module.css";
import Image from "next/image";
import Link from "next/link";
import Input from "@/components/Input";
import Button from "@/components/Button";

const genres = [
"biography",
Expand Down Expand Up @@ -41,7 +43,23 @@ export default function Search() {

<h1 className={styles.title}>Search books by emotion</h1>
<div className={styles.emotionSearchContainer}>
<SearchBar placeholder="Type your emotion" />
<form>
<Input
type="text"
id="emotion"
placeholder="Type your emotion"
label="Emotion"
/>
<Input
type="text"
id="bookTitle"
placeholder="Book title"
label="Book title"
/>
<div className={styles.buttonContainer}>
<Button>Search</Button>
</div>
</form>
</div>
</div>
</main>
Expand Down
16 changes: 14 additions & 2 deletions src/app/search/search.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
padding-bottom: 20rem;
}

.emotionSearchContainer div {
width: min(40rem, 100%);
.emotionSearchContainer form {
width: min(50rem, 100%);
display: flex;
flex-direction: column;
gap: 2rem;
}

.buttonContainer {
display: flex;
justify-content: flex-end;
}

.buttonContainer button {
width: min(20rem, 100%);
}
95 changes: 87 additions & 8 deletions src/app/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
"use client";

import Input from "@/components/Input";
import styles from "./signup.module.css";
import Button from "@/components/Button";
import { useState } from "react";
import { useRouter } from "next/navigation";

const url = process.env.BACK_END_URL || "http://localhost:8080";

export default function Signup() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);
setError(null);

const formData = new FormData(event.currentTarget);

const data: Record<string, string> = {};
formData.forEach((value, key) => {
data[key] = value.toString();
});

const response = await fetch(url + "/user", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});

if (response.ok) {
alert("Signup successful!");
router.push("/login");
} else {
setError("Signup failed");
}

setLoading(false);
};

return (
<main>
<div className={styles.signup}>
Expand All @@ -14,14 +54,53 @@ export default function Signup() {
</h1>
</div>

<div className={styles.gridContainer}>
<Input type="text" id="firstName" label="First name" />
<Input type="text" id="lastName" label="Last name" />
<Input type="date" id="date" label="Date of birth" />
<Input type="text" id="username" label="Username" />
<Input type="password" id="password" label="Password" />
<Button className={styles.button}>Join us</Button>
</div>
<form onSubmit={handleSubmit} className={styles.gridContainer}>
<Input
type="text"
id="firstName"
name="firstName"
placeholder="First name"
label="First name"
required
/>
<Input
type="text"
id="lastName"
name="lastName"
placeholder="Last name"
label="Last name"
required
/>
<Input
type="date"
id="dateOfBirth"
name="dateOfBirth"
placeholder="Date of birth"
label="Date of birth"
required
/>
<Input
type="text"
id="username"
name="username"
placeholder="Username"
label="Username"
required
/>
<Input
type="password"
id="password"
name="password"
placeholder="Password"
label="Password"
required
/>
<Button type="submit" className={styles.button}>
{loading ? "Loading" : "Join us"}
</Button>

{error && <p style={{ color: "red" }}>{error}</p>}
</form>
</div>
</main>
);
Expand Down
24 changes: 24 additions & 0 deletions src/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import React, { useEffect } from "react";
import { useRouter } from "next/navigation";

const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
let isLoggedIn = false;

if (typeof window !== "undefined") {
isLoggedIn = window.localStorage["isLoggedIn"] === "true";
}

useEffect(() => {
const path = window.location.pathname;
if (!isLoggedIn && path !== "/login" && path !== "/signup") {
router.push("/login");
}
}, [isLoggedIn, router]);

return <>{children}</>;
};

export default AuthProvider;
1 change: 1 addition & 0 deletions src/components/Button/button.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.button {
width: 100%;
height: 4rem;
padding: 12px;
font-size: 16px;
border: none;
Expand Down
Loading

1 comment on commit f557be2

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for bookhive-web ready!

✅ Preview
https://bookhive-33mqt56ay-amiltoncabrals-projects.vercel.app

Built with commit f557be2.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.