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

+
+
+

Email

+
+ + +
+ {errors.email && ( + + This field is required + + )} +
+
+

Password

+
+ + {isPassVisible ? ( + + ) : ( + + )} +
+ {errors.password && ( + + This field is required + + )} +
+ +
+

+ 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"