diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
index 5e4afc7..867aaf8 100644
--- a/src/pages/Home/index.tsx
+++ b/src/pages/Home/index.tsx
@@ -7,6 +7,7 @@ import Slider from './Slider'
import TopProducts from './TopProducts'
import FreeShipping from './FreeShipping'
import News from './News'
+import { ToastContainer } from 'react-toastify'
const Home = () => {
const { t } = useTranslation('home')
@@ -20,6 +21,7 @@ const Home = () => {
+
>
)
}
diff --git a/src/pages/Login/LoginForm.tsx b/src/pages/Login/LoginForm.tsx
new file mode 100644
index 0000000..fee55a0
--- /dev/null
+++ b/src/pages/Login/LoginForm.tsx
@@ -0,0 +1,114 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { ToastContainer, toast, Bounce } from 'react-toastify'
+import { AiFillEye, AiFillEyeInvisible } from 'react-icons/ai'
+
+import 'react-toastify/dist/ReactToastify.css'
+import { validateLogin } from './ValidateLogin'
+import { useNavigate } from 'react-router-dom'
+import { SignJWT } from 'jose'
+
+const LoginForm = () => {
+ const key = import.meta.env.VITE_APP_KEY
+ const navigate = useNavigate()
+ const { t } = useTranslation('login')
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError('')
+ setLoading(true)
+ try {
+ const users = await validateLogin(username, password)
+ if (users.length > 0) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { password, ...payload } = users[0]
+ const encoder = new TextEncoder()
+ const keyPrivate = encoder.encode(key)
+ const token = await new SignJWT(payload)
+ .setProtectedHeader({ alg: 'HS256' })
+ .setIssuedAt()
+ .setExpirationTime('2h')
+ .sign(keyPrivate)
+ sessionStorage.setItem('token', token)
+ toast.success('Login successful!', {
+ position: 'top-right',
+ autoClose: 1000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'light',
+ transition: Bounce
+ })
+ setTimeout(() => {
+ navigate('/')
+ }, 1000)
+ } else {
+ toast.error('Invalid username or password!', {
+ position: 'top-right',
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'light',
+ transition: Bounce
+ })
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ setError(error.message)
+ } else {
+ setError('An unknown error occurred')
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default LoginForm
diff --git a/src/pages/Login/ValidateLogin.tsx b/src/pages/Login/ValidateLogin.tsx
new file mode 100644
index 0000000..c2035b7
--- /dev/null
+++ b/src/pages/Login/ValidateLogin.tsx
@@ -0,0 +1,13 @@
+import { hashPassword } from '../../components/Hash'
+
+export const validateLogin = async (username: string, password: string) => {
+ try {
+ const hashedPassword = await hashPassword(password)
+ const response = await fetch(`http://localhost:5000/users?username=${username}&password=${hashedPassword}`)
+ const users = await response.json()
+ return users
+ } catch (error) {
+ console.error('Login error:', error)
+ return false
+ }
+}
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx
new file mode 100644
index 0000000..ca72366
--- /dev/null
+++ b/src/pages/Login/index.tsx
@@ -0,0 +1,42 @@
+import { useTranslation } from 'react-i18next'
+import LoginImg from '../../assets/images/top-bg.jpg'
+import Breadcrumb from '../../components/Breadcrumb'
+import LoginForm from './LoginForm'
+import { Link } from 'react-router-dom'
+
+const Login = () => {
+ const { t } = useTranslation('login')
+ return (
+ <>
+
+
+
+ {t('title')}
+
+
+
+
+
+
+
+
+
{t('welcome')}
+
+
+
+
{t('new_here')}
+
+ {t('register_now')}
+
+
+
+
+
+ >
+ )
+}
+
+export default Login
diff --git a/src/pages/Register/RegisterForm.tsx b/src/pages/Register/RegisterForm.tsx
new file mode 100644
index 0000000..b8c2c02
--- /dev/null
+++ b/src/pages/Register/RegisterForm.tsx
@@ -0,0 +1,157 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { ToastContainer, toast, Bounce } from 'react-toastify'
+import { AiFillEye, AiFillEyeInvisible } from 'react-icons/ai'
+
+import 'react-toastify/dist/ReactToastify.css'
+import { validateRegistration } from './ValidateRegister'
+import { useNavigate } from 'react-router-dom'
+
+const RegisterForm = () => {
+ const { t } = useTranslation('register')
+ const navigate = useNavigate()
+ const [name, setName] = useState('')
+ const [email, setEmail] = useState('')
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+ const [error, setError] = useState('')
+ const [avatar, setAvatar] = useState
(null)
+ const [preview, setPreview] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ const handleAvatarChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0] || null
+ if (file) {
+ setAvatar(file)
+ setPreview(URL.createObjectURL(file))
+ }
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError('')
+ setLoading(true)
+
+ if (password !== confirmPassword) {
+ toast.error(t('alert.password_mismatch'), {
+ position: 'top-right',
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'light',
+ transition: Bounce
+ })
+ setLoading(false)
+ return
+ }
+
+ try {
+ const isRegistered = await validateRegistration(name, username, email, password, avatar)
+ if (isRegistered) {
+ toast.success(t('alert.registration_successful'), {
+ position: 'top-right',
+ autoClose: 1000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'light',
+ transition: Bounce
+ })
+ setTimeout(() => {
+ navigate('/login')
+ }, 1000)
+ } else {
+ toast.error(t('alert.registration_failed'), {
+ position: 'top-right',
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'light',
+ transition: Bounce
+ })
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ setError(error.message)
+ } else {
+ setError('An unknown error occurred')
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default RegisterForm
diff --git a/src/pages/Register/ValidateRegister.tsx b/src/pages/Register/ValidateRegister.tsx
new file mode 100644
index 0000000..387844d
--- /dev/null
+++ b/src/pages/Register/ValidateRegister.tsx
@@ -0,0 +1,42 @@
+import { hashPassword } from '../../components/Hash'
+
+export const validateRegistration = async (
+ name: string,
+ username: string,
+ email: string,
+ password: string,
+ avatar: File | null
+) => {
+ try {
+ const existingUserResponse = await fetch(`http://localhost:5000/users?username=${username}`)
+ const existingUsers = await existingUserResponse.json()
+
+ if (existingUsers.length > 0) {
+ throw new Error('Username already exists')
+ }
+
+ const hashedPassword = await hashPassword(password)
+ let avatarBase64 = null
+ if (avatar) {
+ avatarBase64 = `/src/assets/images/${avatar.name}`
+ }
+
+ const response = await fetch('http://localhost:5000/users', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ name, username, email, password: hashedPassword, avatar: avatarBase64 })
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to register user')
+ }
+
+ const newUser = await response.json()
+ return newUser
+ } catch (error) {
+ console.error('Registration error:', error)
+ return false
+ }
+}
diff --git a/src/pages/Register/index.tsx b/src/pages/Register/index.tsx
new file mode 100644
index 0000000..cbee7d4
--- /dev/null
+++ b/src/pages/Register/index.tsx
@@ -0,0 +1,42 @@
+import { useTranslation } from 'react-i18next'
+import LoginImg from '../../assets/images/top-bg.jpg'
+import Breadcrumb from '../../components/Breadcrumb'
+import { Link } from 'react-router-dom'
+import RegisterForm from './RegisterForm'
+
+const Register = () => {
+ const { t } = useTranslation('register')
+ return (
+ <>
+
+
+
+ {t('createAccount')}
+
+
+
+
+
+
+
+
+
{t('welcome')}
+
+
+
+
{t('have_account')}
+
+ {t('login_now')}
+
+
+
+
+
+ >
+ )
+}
+
+export default Register
diff --git a/src/routers/index.tsx b/src/routers/index.tsx
index b9cbc34..33eb15a 100644
--- a/src/routers/index.tsx
+++ b/src/routers/index.tsx
@@ -3,6 +3,9 @@ import { useRoutes } from 'react-router-dom'
import MainLayout from '../layouts/MainLayout'
import Home from '../pages/Home'
import ProductList from '../page/ProductList'
+import Login from '../pages/Login'
+import Register from '../pages/Register'
+import Cart from '../pages/Cart'
import ProductDetailPage from '../page/ProductDetailPage'
import Wishlist from '../page/Wishlist'
@@ -14,6 +17,9 @@ export default function Router() {
children: [
{ index: true, element: },
{ path: '/products', element: },
+ { path: '/login', element: },
+ { path: '/register', element: },
+ { path: '/cart', element: },
{ path: '/product/:id', element: },
{ path: '/wishlist', element: }
]
diff --git a/src/services/dataService.ts b/src/services/dataService.ts
index 71e7ada..8305660 100644
--- a/src/services/dataService.ts
+++ b/src/services/dataService.ts
@@ -13,7 +13,6 @@ export const getData = async (endpoint: string, params: ProductQuery = {}) => {
)
).toString()
const url = query ? `${URL}/${endpoint}?${query}` : `${URL}/${endpoint}`
- console.log(url)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch data from ${endpoint}`)
@@ -25,3 +24,49 @@ export const getData = async (endpoint: string, params: ProductQuery = {}) => {
return []
}
}
+
+export const postData = async (endpoint: string, data: unknown) => {
+ try {
+ const url = `${URL}/${endpoint}`
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to post data to ${endpoint}`)
+ }
+
+ const result = await response.json()
+ return result
+ } catch (error) {
+ console.error(`Error posting data to ${endpoint}:`, error)
+ return null
+ }
+}
+
+export const putData = async (endpoint: string, data: unknown) => {
+ try {
+ const url = `${URL}/${endpoint}`
+ const response = await fetch(url, {
+ method: 'PUT', // Đặt method là 'PUT' để cập nhật dữ liệu
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data) // Dữ liệu cần cập nhật
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to update data at ${endpoint}`)
+ }
+
+ const result = await response.json() // Đọc kết quả trả về từ server
+ return result
+ } catch (error) {
+ console.error(`Error updating data at ${endpoint}:`, error)
+ return null
+ }
+}
diff --git a/src/types/productQuery.type.ts b/src/types/productQuery.type.ts
index 3483706..79eeea3 100644
--- a/src/types/productQuery.type.ts
+++ b/src/types/productQuery.type.ts
@@ -3,4 +3,5 @@ export interface ProductQuery {
_skip?: number
_sort?: string
_order?: string
+ userId?: number
}