Skip to content

Commit

Permalink
feat(auth): create signin page and its integration
Browse files Browse the repository at this point in the history
  • Loading branch information
iqbalpa committed Jul 12, 2024
1 parent 800905f commit ca94f08
Show file tree
Hide file tree
Showing 14 changed files with 348 additions and 6 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
23 changes: 23 additions & 0 deletions src/api/auth.api.ts
Original file line number Diff line number Diff line change
@@ -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;
};
File renamed without changes.
11 changes: 8 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'] });

Expand All @@ -19,9 +21,12 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`inter.className relative`}>
<Header />
{children}
<GoTop />
<UserProvider>
<Header />
{children}
<GoTop />
<ToastContainer />
</UserProvider>
</body>
</html>
);
Expand Down
5 changes: 5 additions & 0 deletions src/app/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import SignInModule from '@/modules/signin/signin';

export default function Page() {
return <SignInModule />;
}
17 changes: 17 additions & 0 deletions src/constant/auth.constant.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion src/modules/detailMovie/detailMovie.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/modules/home/home.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
129 changes: 129 additions & 0 deletions src/modules/signin/signin.tsx
Original file line number Diff line number Diff line change
@@ -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<SignInInputs>();

const [isPassVisible, setIsPassVisible] = useState<boolean>(false);
const togglePassVisibility = () => {
setIsPassVisible(!isPassVisible);
};

const onSubmit: SubmitHandler<SignInInputs> = 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 (
<div className="flex h-screen items-center justify-center bg-black bg-cover bg-no-repeat">
<div className="flex w-1/3 flex-col items-center justify-center border border-gray-200 bg-white bg-opacity-10 p-14 backdrop-blur-md backdrop-filter">
<h1 className="mb-5 text-xl font-bold uppercase text-white">Sign In</h1>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex w-full flex-col"
>
<div className="mb-2 flex flex-col">
<p className="mb-1 font-semibold text-white">Email</p>
<div className="flex flex-row items-center">
<input
placeholder="email"
{...register('email', { required: true })}
className="mr-2 grow rounded-md border-[1px] border-slate-400 px-4 py-2"
/>
<Mail color="#ffffff" size={35} />
</div>
{errors.email && (
<span className="text-sm text-red-500">
This field is required
</span>
)}
</div>
<div className="mb-4 flex flex-col">
<p className="mb-1 font-semibold text-white">Password</p>
<div className="flex flex-row items-center">
<input
placeholder="password"
type={isPassVisible ? 'text' : 'password'}
{...register('password', { required: true })}
className="mr-2 grow rounded-md border-[1px] border-slate-400 px-4 py-2"
/>
{isPassVisible ? (
<Eye
color="#ffffff"
size={35}
onClick={togglePassVisibility}
className="cursor-pointer"
/>
) : (
<EyeOff
color="#ffffff"
size={35}
onClick={togglePassVisibility}
className="cursor-pointer"
/>
)}
</div>
{errors.password && (
<span className="text-sm text-red-500">
This field is required
</span>
)}
</div>
<input
type="submit"
className="rounded-lg bg-emerald-500 px-4 py-2 text-white duration-100 hover:cursor-pointer hover:bg-emerald-700"
/>
</form>
<p className="mt-5 text-sm text-white">
doesn't have an account?{' '}
<Link href="/signup" className="text-green-500 hover:text-green-700">
register
</Link>
</p>
</div>
</div>
);
};

export default SignInModule;
11 changes: 11 additions & 0 deletions src/provider/userProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 <Provider store={store}>{children}</Provider>;
};

export default UserProvider;
33 changes: 33 additions & 0 deletions src/store/userSlice.ts
Original file line number Diff line number Diff line change
@@ -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<User>) => {
state.user = action.payload;
},
logout: (state) => {
state.user = null;
},
},
});

export const { setUser, logout } = userSlice.actions;

export default userSlice.reducer;
11 changes: 11 additions & 0 deletions src/store/userStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';

export const store = configureStore({
reducer: {
user: userReducer,
},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
19 changes: 19 additions & 0 deletions src/utils/jwt-decode.ts
Original file line number Diff line number Diff line change
@@ -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<DecodedToken>(accessToken);
return decoded;
};
Loading

0 comments on commit ca94f08

Please sign in to comment.