diff --git a/package.json b/package.json
index 090ea81..6065fa3 100644
--- a/package.json
+++ b/package.json
@@ -13,13 +13,19 @@
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
+ "@reduxjs/toolkit": "^2.2.6",
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cookies-next": "^4.2.1",
+ "jwt-decode": "^4.0.0",
"lucide-react": "^0.407.0",
"next": "14.2.2",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.52.1",
+ "react-redux": "^9.1.2",
+ "react-toastify": "^10.0.5",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7"
},
diff --git a/src/api/auth.api.ts b/src/api/auth.api.ts
new file mode 100644
index 0000000..bcf5250
--- /dev/null
+++ b/src/api/auth.api.ts
@@ -0,0 +1,23 @@
+import axios from 'axios';
+import { SignInRequest, SignUpRequest } from '@/constant/auth.constant';
+
+const BASE_URL: string = 'http://localhost:3000';
+
+export const signup = async (data: SignUpRequest) => {
+ let res = await axios.post(`${BASE_URL}/auth/signup`, {
+ name: data.name,
+ email: data.email,
+ password: data.password,
+ });
+ res = res.data;
+ return res;
+};
+
+export const signin = async (data: SignInRequest) => {
+ const res = await axios.post(`${BASE_URL}/auth/signin`, {
+ email: data.email,
+ password: data.password,
+ });
+ const result = res.data;
+ return result;
+};
diff --git a/src/api/api.tsx b/src/api/movies.api.ts
similarity index 100%
rename from src/api/api.tsx
rename to src/api/movies.api.ts
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 8597b90..3dcf6b0 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -3,6 +3,8 @@ import { Inter } from 'next/font/google';
import './globals.css';
import Header from '@/components/header/header';
import GoTop from '@/components/goTop/goTop';
+import UserProvider from '@/provider/userProvider';
+import { ToastContainer } from 'react-toastify';
const inter = Inter({ subsets: ['latin'] });
@@ -19,9 +21,12 @@ export default function RootLayout({
return (
-
- {children}
-
+
+
+ {children}
+
+
+
);
diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx
new file mode 100644
index 0000000..f3ebe67
--- /dev/null
+++ b/src/app/signin/page.tsx
@@ -0,0 +1,5 @@
+import SignInModule from '@/modules/signin/signin';
+
+export default function Page() {
+ return ;
+}
diff --git a/src/constant/auth.constant.ts b/src/constant/auth.constant.ts
new file mode 100644
index 0000000..d277cdd
--- /dev/null
+++ b/src/constant/auth.constant.ts
@@ -0,0 +1,17 @@
+export interface SignUpRequest {
+ name: string;
+ email: string;
+ password: string;
+}
+
+export interface SignUpResponse {
+ id: number;
+ name: string;
+ email: string;
+ password: string;
+}
+
+export interface SignInRequest {
+ email: string;
+ password: string;
+}
diff --git a/src/modules/detailMovie/detailMovie.tsx b/src/modules/detailMovie/detailMovie.tsx
index c091e5a..bd2f858 100644
--- a/src/modules/detailMovie/detailMovie.tsx
+++ b/src/modules/detailMovie/detailMovie.tsx
@@ -1,6 +1,6 @@
'use client';
-import { getMovieById } from '@/api/api';
+import { getMovieById } from '@/api/movies.api';
import { DetailMovie } from '@/constant/detailMovie';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
diff --git a/src/modules/home/home.tsx b/src/modules/home/home.tsx
index 09412cc..bb0688f 100644
--- a/src/modules/home/home.tsx
+++ b/src/modules/home/home.tsx
@@ -1,7 +1,7 @@
'use client';
import React, { ChangeEvent, useEffect, useState } from 'react';
-import { getAllMovies, getMoviesWithQuery } from '@/api/api';
+import { getAllMovies, getMoviesWithQuery } from '@/api/movies.api';
import { Movie } from '@/constant/movie';
import Backdrop from '@/components/backdrop/backdrop';
import ListMovies from '@/components/listMovies/listMovies';
diff --git a/src/modules/signin/signin.tsx b/src/modules/signin/signin.tsx
new file mode 100644
index 0000000..365df8b
--- /dev/null
+++ b/src/modules/signin/signin.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import Link from 'next/link';
+import React, { useState } from 'react';
+import { useForm, SubmitHandler } from 'react-hook-form';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useDispatch } from 'react-redux';
+import { setUser } from '@/store/userSlice';
+import { decodeToken } from '@/utils/jwt-decode';
+import { useRouter } from 'next/navigation';
+import { setCookie } from 'cookies-next';
+import { Eye, EyeOff, Mail } from 'lucide-react';
+import { signin } from '@/api/auth.api';
+
+type SignInInputs = {
+ email: string;
+ password: string;
+};
+
+const SignInModule: React.FC = () => {
+ const dispatch = useDispatch();
+ const router = useRouter();
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors },
+ } = useForm();
+
+ const [isPassVisible, setIsPassVisible] = useState(false);
+ const togglePassVisibility = () => {
+ setIsPassVisible(!isPassVisible);
+ };
+
+ const onSubmit: SubmitHandler = async (data) => {
+ try {
+ const res = await signin(data);
+ if (!res) {
+ toast.error('Login failed');
+ return;
+ }
+ setCookie('accessToken', res.access_token); // store access token in cookie. use getCookie('accessToken')
+
+ console.log('decoding token...');
+ const user = decodeToken(res.access_token).user;
+ console.log(user);
+ dispatch(setUser(user));
+ toast.success('Login success');
+ setTimeout(() => {
+ router.push('/');
+ }, 1500);
+ } catch (error) {
+ toast.error('An error occurred during logging in');
+ }
+ };
+
+ return (
+
+
+
Sign In
+
+
+ doesn't have an account?{' '}
+
+ register
+
+
+
+
+ );
+};
+
+export default SignInModule;
diff --git a/src/provider/userProvider.tsx b/src/provider/userProvider.tsx
new file mode 100644
index 0000000..471e3e9
--- /dev/null
+++ b/src/provider/userProvider.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { ReactNode } from 'react';
+import { Provider } from 'react-redux';
+import { store } from '@/store/userStore';
+
+const UserProvider = ({ children }: { children: ReactNode }) => {
+ return {children};
+};
+
+export default UserProvider;
diff --git a/src/store/userSlice.ts b/src/store/userSlice.ts
new file mode 100644
index 0000000..0639720
--- /dev/null
+++ b/src/store/userSlice.ts
@@ -0,0 +1,33 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface User {
+ id: number;
+ name: string;
+ email: string;
+ password: string;
+}
+
+interface UserState {
+ user: User | null;
+}
+
+const initialState: UserState = {
+ user: null,
+};
+
+const userSlice = createSlice({
+ name: 'user',
+ initialState,
+ reducers: {
+ setUser: (state, action: PayloadAction) => {
+ state.user = action.payload;
+ },
+ logout: (state) => {
+ state.user = null;
+ },
+ },
+});
+
+export const { setUser, logout } = userSlice.actions;
+
+export default userSlice.reducer;
diff --git a/src/store/userStore.ts b/src/store/userStore.ts
new file mode 100644
index 0000000..7f6b30f
--- /dev/null
+++ b/src/store/userStore.ts
@@ -0,0 +1,11 @@
+import { configureStore } from '@reduxjs/toolkit';
+import userReducer from './userSlice';
+
+export const store = configureStore({
+ reducer: {
+ user: userReducer,
+ },
+});
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/utils/jwt-decode.ts b/src/utils/jwt-decode.ts
new file mode 100644
index 0000000..38d4cb0
--- /dev/null
+++ b/src/utils/jwt-decode.ts
@@ -0,0 +1,19 @@
+import { jwtDecode } from 'jwt-decode';
+
+interface User {
+ id: number;
+ name: string;
+ email: string;
+ password: string;
+}
+
+interface DecodedToken {
+ user: User;
+ iat: number;
+ exp: number;
+}
+
+export const decodeToken = (accessToken: string) => {
+ const decoded = jwtDecode(accessToken);
+ return decoded;
+};
diff --git a/yarn.lock b/yarn.lock
index 5c7fabc..61fe8d9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -202,6 +202,16 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
+"@reduxjs/toolkit@^2.2.6":
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.6.tgz#4a8356dad9d0c1ab255607a555d492168e0e3bc1"
+ integrity sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==
+ dependencies:
+ immer "^10.0.3"
+ redux "^5.0.1"
+ redux-thunk "^3.1.0"
+ reselect "^5.1.0"
+
"@rushstack/eslint-patch@^1.3.3":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"
@@ -220,6 +230,11 @@
"@swc/counter" "^0.1.3"
tslib "^2.4.0"
+"@types/cookie@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
+ integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
+
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -252,6 +267,11 @@
"@types/prop-types" "*"
csstype "^3.0.2"
+"@types/use-sync-external-store@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
+ integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
+
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a"
@@ -622,7 +642,7 @@ clsx@2.0.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
-clsx@^2.1.1:
+clsx@^2.1.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -656,6 +676,19 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+cookie@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
+
+cookies-next@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/cookies-next/-/cookies-next-4.2.1.tgz#a0c2942afee16f1ffc2bc05a003c7c0cf32deda5"
+ integrity sha512-qsjtZ8TLlxCSX2JphMQNhkm3V3zIMQ05WrLkBKBwu50npBbBfiZWIdmSMzBGcdGKfMK19E0PIitTfRFAdMGHXg==
+ dependencies:
+ "@types/cookie" "^0.6.0"
+ cookie "^0.6.0"
+
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1483,6 +1516,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
+immer@^10.0.3:
+ version "10.1.1"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
+ integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
+
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -1796,6 +1834,11 @@ json5@^1.0.2:
object.assign "^4.1.4"
object.values "^1.1.6"
+jwt-decode@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
+ integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
+
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -2283,11 +2326,31 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.2"
+react-hook-form@^7.52.1:
+ version "7.52.1"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
+ integrity sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==
+
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-redux@^9.1.2:
+ version "9.1.2"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b"
+ integrity sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==
+ dependencies:
+ "@types/use-sync-external-store" "^0.0.3"
+ use-sync-external-store "^1.0.0"
+
+react-toastify@^10.0.5:
+ version "10.0.5"
+ resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e"
+ integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==
+ dependencies:
+ clsx "^2.1.0"
+
react@^18:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
@@ -2309,6 +2372,16 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+redux-thunk@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
+ integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
+
+redux@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
+ integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
+
reflect.getprototypeof@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859"
@@ -2332,6 +2405,11 @@ regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2:
es-errors "^1.3.0"
set-function-name "^2.0.1"
+reselect@^5.1.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
+ integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
+
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -2815,6 +2893,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+use-sync-external-store@^1.0.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
+ integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
+
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"