This commit is contained in:
2025-10-08 07:03:34 +07:00
commit aeb06a0992
43 changed files with 6174 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
README.md Normal file
View File

@@ -0,0 +1,2 @@
## Dự Án React (chưa có api, redux)
## Có đa ngôn ngữ

16
abc.js Normal file
View File

@@ -0,0 +1,16 @@
let targetEL = document.querySelector(".icon-listing")
let arrayI = targetEL.querySelectorAll("i")
let result = []
for (let i = 0; i < arrayI.length; i++) {
let item = ""
for (let j = 0; j < arrayI[i].classList.length; j++) {
item += `${arrayI[i].classList[j]} `
}
result.push(item)
}
console.log(result)

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TMDT</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4586
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "react_router_dom",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"antd": "^5.27.4",
"i18next": "^25.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-i18next": "^15.7.3",
"react-router": "^7.9.1"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react-swc": "^4.1.0",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"sass": "^1.93.1",
"typescript": "~5.8.3",
"typescript-eslint": "^8.44.0",
"vite": "^7.1.7"
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

53
src/App.tsx Normal file
View File

@@ -0,0 +1,53 @@
import React from 'react'
import RouteConfig from './RouteConfig'
import { UserRole, type User } from './interfaces/user.interface'
import type { Category } from './interfaces/cateogry.interface'
export default function App() {
const mockUserData: User[] = [
{
id: 1,
fullName: "Lưu Hoàng Xuân Nguyên",
age: 31,
phone: "+84329577177",
userName: "user1",
password: "123",
email: "user@abc.com",
role: UserRole.USER,
isActive: true
},
{
id: 2,
fullName: "nguyễn Thanh bình Phước",
age: 30,
phone: "+84329577177",
userName: "admin",
password: "123",
email: "admin@abc.com",
role: UserRole.ADMIN,
isActive: true
}
]
if(!localStorage.getItem("userList")) {
localStorage.setItem("userList", JSON.stringify(mockUserData))
}
const mockCategoryData: Category[] = [
{
id: 1,
title: "Điện thoại",
isActive: true,
icon: "fa-solid fa-circle-user"
}
]
if(!localStorage.getItem("categoryList")) {
localStorage.setItem("categoryList", JSON.stringify(mockCategoryData))
}
return <RouteConfig/>
}

28
src/RouteConfig.tsx Normal file
View File

@@ -0,0 +1,28 @@
import React from 'react'
import { Route, Routes } from 'react-router'
import { Admin } from './pages/admin/Admin'
import UserManagement from './pages/admin/user-management/UserManagement'
import ProductManagement from './pages/admin/product-management/ProductManagement'
import CategoryManagement from './pages/admin/category-management/CategoryManagement'
import Home from './pages/user/Home/Home'
import Auth from './pages/admin/auth/Auth'
import About from './pages/user/Home/About/About'
import CollectionDetail from './pages/user/Collection/CollectionDetail'
export default function RouteConfig() {
return (
<Routes>
<Route path='*' element={<Home />}>
<Route path='about' element={<About />}></Route>
<Route path='collections/:slugCollection' element={<CollectionDetail />}></Route>
</Route>
<Route path='admin' element={<Auth>
<Admin />
</Auth>}>
<Route path='user' element={<UserManagement />}></Route>
<Route path='product' element={<ProductManagement />}></Route>
<Route path='category' element={<CategoryManagement />}></Route>
</Route>
</Routes>
)
}

BIN
src/assets/img/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

31
src/i18n/en.json Normal file
View File

@@ -0,0 +1,31 @@
{
"Welcome to React": "Welcome to React and react-i18next",
"user-management": "User Management",
"product-management": "Product Management",
"category-management": "Category Management",
"user-id": "User Id",
"fullname": "Fullname",
"age": "Age",
"phone-number": "Phone Number",
"tools": "tools",
"block": "Block",
"hello-friend": "Hello, Friend!",
"enter-your-personal-details-and-start-journey-with-us": "Enter your personal details and start journey with us",
"welcome-back": "Welcome Back!",
"to-keep-connected-with-us-please-login-with-your-personal-info": "To keep connected with us please login with your personal info",
"sign-in": "Sign In",
"forgot-your-password": "Forgot your password?",
"or-use-your-account": "or use your account",
"sign-in-0": "Sign in",
"logout": "Logout",
"password-incorrect": "Password incorrect",
"ban-khong-co-quyen-truy-cap": " You do not have access",
"user-not-found": "User not found",
"username": "Username",
"password": "Password",
"username-0": "Username",
"email": "Email",
"role": "Role",
"active": "Active",
"unlock": "Unlock"
}

26
src/i18n/i18n.setup.ts Normal file
View File

@@ -0,0 +1,26 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from './en.json'
import vi from './vi.json'
import ja from './ja.json'
const resources = {
en: {
translation: en
},
vi: {
translation: vi
},
ja: {
translation: ja
}
};
i18n
.use(initReactI18next)
.init({
resources,
lng: localStorage.getItem("lng") || "vi"
});
export default i18n;

27
src/i18n/ja.json Normal file
View File

@@ -0,0 +1,27 @@
{
"category-management": "カテゴリ管理",
"product-management": "製品管理",
"user-id": "ユーザーID",
"fullname": "フルネーム",
"age": "年",
"phone-number": "電話番号",
"tools": "ツール",
"block": "ブロック",
"hello-friend": "こんにちは、友達!",
"enter-your-personal-details-and-start-journey-with-us": "あなたの個人的な詳細を入力して、私たちと一緒に旅を始めてください",
"welcome-back": "おかえり!",
"to-keep-connected-with-us-please-login-with-your-personal-info": "私たちとつながり続けるには、あなたの個人情報にログインしてください",
"sign-in": "サインイン",
"forgot-your-password": "パスワードをお忘れですか?",
"or-use-your-account": "またはアカウントを使用します",
"logout": "ログアウト",
"password-incorrect": "パスワードが正しくありません",
"ban-khong-co-quyen-truy-cap": "アクセスできません",
"user-not-found": "ユーザーが見つかりません",
"username": "ユーザー名",
"password": "パスワード",
"email": "メール",
"role": "役割",
"active": "アクティブ",
"unlock": "ロックを解除します"
}

29
src/i18n/vi.json Normal file
View File

@@ -0,0 +1,29 @@
{
"Welcome to React": "Xin Chào Đến Với React",
"user-management": "Quản lý người dùng",
"product-management": "Quản lý sản phẩm",
"category-management": "Quản lý danh mục",
"user-id": "ID người dùng",
"fullname": "Tên đầy đủ",
"age": "Tuổi",
"phone-number": "Số điện thoại",
"tools": "công cụ",
"block": "Khóa",
"hello-friend": "Chào bạn!",
"enter-your-personal-details-and-start-journey-with-us": "Nhập chi tiết cá nhân của bạn và bắt đầu hành trình với chúng tôi",
"welcome-back": "Chào mừng trở lại!",
"to-keep-connected-with-us-please-login-with-your-personal-info": "Để giữ kết nối với chúng tôi, vui lòng đăng nhập với thông tin cá nhân của bạn",
"sign-in": "Đăng nhập",
"forgot-your-password": "Quên mật khẩu của bạn?",
"or-use-your-account": "hoặc sử dụng tài khoản của bạn",
"logout": "Đăng xuất",
"password-incorrect": "Mật khẩu không chính xác",
"ban-khong-co-quyen-truy-cap": "Bạn không có quyền truy cập",
"user-not-found": "Người dùng không tìm thấy",
"username": "Tên người dùng",
"password": "Mật khẩu",
"email": "E-mail",
"role": "Vai trò",
"active": "Tích cực",
"unlock": "Mở khóa"
}

View File

@@ -0,0 +1,8 @@
import type React from "react"
export interface Category {
id: number
title: string
icon: string
isActive: boolean
}

View File

@@ -0,0 +1,15 @@
export enum UserRole {
ADMIN="ADMIN",
USER="USER"
}
export interface User {
id: number
fullName: string
age: number
phone: string
userName: string
password: string
email: string
role: UserRole
isActive: boolean
}

5
src/main.scss Normal file
View File

@@ -0,0 +1,5 @@
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}

13
src/main.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import { BrowserRouter } from 'react-router'
import './main.scss'
import './i18n/i18n.setup.ts'
import '@ant-design/v5-patch-for-react-19';
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>
)

34
src/pages/admin/Admin.tsx Normal file
View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import { Layout, theme } from 'antd';
import Slider from './components/Slider';
import HeaderCom from './components/Header';
import { Outlet } from 'react-router';
const { Content } = Layout;
export const Admin: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
return (
<Layout>
<Slider collapsed={collapsed} />
<Layout>
<HeaderCom collapsed={collapsed} setCollapsed={setCollapsed} />
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
borderRadius: borderRadiusLG,
}}
>
<Outlet/>
</Content>
</Layout>
</Layout>
);
};

View File

@@ -0,0 +1,112 @@
import { type ReactNode } from 'react'
import './auth.scss'
import { useTranslation } from 'react-i18next';
import { UserRole, type User } from '../../../interfaces/user.interface';
import { message } from 'antd';
export default function Auth({ children }: {
children: ReactNode;
}) {
const {t} = useTranslation()
let isLogin = localStorage.getItem("userLogin")
function loginHandle(data: {
userName: string,
password: string
}) {
let userList: User[] = JSON.parse(localStorage.getItem("userList"))
let user = userList.find(item => item.userName == data.userName)
if(user) {
if(user.password != data.password) {
message.error(t('password-incorrect'))
return
}
if(user.role != UserRole.ADMIN) {
message.error(t('ban-khong-co-quyen-truy-cap'))
return
}
localStorage.setItem("userLogin", JSON.stringify(user))
window.location.reload()
}else {
message.error(t('user-not-found'))
return
}
}
if (isLogin) {
return <>{children}</>
} else {
return <div className='admin_login_page'>
<div className="container" id="container">
{/* <div className="form-container sign-up-container">
<form action="#">
<h1>Create Account</h1>
<div className="social-container">
<a href="#" className="social">
<i className="fab fa-facebook-f" />
</a>
<a href="#" className="social">
<i className="fab fa-google-plus-g" />
</a>
<a href="#" className="social">
<i className="fab fa-linkedin-in" />
</a>
</div>
<span>or use your email for registration</span>
<input type="text" placeholder="Name" />
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button>Sign Up</button>
</form>
</div> */}
<div className="form-container sign-in-container">
<form onSubmit={(e) => {
e.preventDefault()
loginHandle({
userName: (e.target as any).userName.value,
password: (e.target as any).password.value,
})
}}>
<h1>{t('sign-in')}</h1>
<div className="social-container">
<a href="#" className="social">
<i className="fab fa-facebook-f" />
</a>
<a href="#" className="social">
<i className="fab fa-google-plus-g" />
</a>
<a href="#" className="social">
<i className="fab fa-linkedin-in" />
</a>
</div>
<span>{t('or-use-your-account')}</span>
<input type="text" name='userName' placeholder={t('username')} />
<input type="password" name='password' placeholder={t('password')} />
<a href="#">{t('forgot-your-password')}</a>
<button>{t('sign-in')}</button>
</form>
</div>
<div className="overlay-container">
<div className="overlay">
<div className="overlay-panel overlay-left">
<h1>{t('welcome-back')}</h1>
<p>{t('to-keep-connected-with-us-please-login-with-your-personal-info')}</p>
<button className="ghost" id="signIn">
{t('sign-in')}
</button>
</div>
<div className="overlay-panel overlay-right">
<h1>{t('hello-friend')}</h1>
<p>{t('enter-your-personal-details-and-start-journey-with-us')}</p>
{/* <button className="ghost" id="signUp">
Sign Up
</button> */}
</div>
</div>
</div>
</div>
</div>
}
}

View File

@@ -0,0 +1,258 @@
@import url('https://fonts.googleapis.com/css?family=Montserrat:400,800');
.admin_login_page {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
background: #f6f5f7;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: 'Montserrat', sans-serif;
height: 100vh;
h1 {
font-weight: bold;
margin: 0;
}
h2 {
text-align: center;
}
p {
font-size: 14px;
font-weight: 100;
line-height: 20px;
letter-spacing: 0.5px;
margin: 20px 0 30px;
}
span {
font-size: 12px;
}
a {
color: #333;
font-size: 14px;
text-decoration: none;
margin: 15px 0;
}
button {
border-radius: 20px;
border: 1px solid #FF4B2B;
background-color: #FF4B2B;
color: #FFFFFF;
font-size: 12px;
font-weight: bold;
padding: 12px 45px;
letter-spacing: 1px;
text-transform: uppercase;
transition: transform 80ms ease-in;
}
button:active {
transform: scale(0.95);
}
button:focus {
outline: none;
}
button.ghost {
background-color: transparent;
border-color: #FFFFFF;
}
form {
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 50px;
height: 100%;
text-align: center;
}
input {
background-color: #eee;
border: none;
padding: 12px 15px;
margin: 8px 0;
width: 100%;
}
.container {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
0 10px 10px rgba(0, 0, 0, 0.22);
position: relative;
overflow: hidden;
width: 768px;
max-width: 100%;
min-height: 480px;
}
.form-container {
position: absolute;
top: 0;
height: 100%;
transition: all 0.6s ease-in-out;
}
.sign-in-container {
left: 0;
width: 50%;
z-index: 2;
}
.container.right-panel-active .sign-in-container {
transform: translateX(100%);
}
.sign-up-container {
left: 0;
width: 50%;
opacity: 0;
z-index: 1;
}
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
opacity: 1;
z-index: 5;
animation: show 0.6s;
}
@keyframes show {
0%,
49.99% {
opacity: 0;
z-index: 1;
}
50%,
100% {
opacity: 1;
z-index: 5;
}
}
.overlay-container {
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
overflow: hidden;
transition: transform 0.6s ease-in-out;
z-index: 100;
}
.container.right-panel-active .overlay-container {
transform: translateX(-100%);
}
.overlay {
background: #FF416C;
background: -webkit-linear-gradient(to right, #FF4B2B, #FF416C);
background: linear-gradient(to right, #FF4B2B, #FF416C);
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #FFFFFF;
position: relative;
left: -100%;
height: 100%;
width: 200%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.container.right-panel-active .overlay {
transform: translateX(50%);
}
.overlay-panel {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 40px;
text-align: center;
top: 0;
height: 100%;
width: 50%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.overlay-left {
transform: translateX(-20%);
}
.container.right-panel-active .overlay-left {
transform: translateX(0);
}
.overlay-right {
right: 0;
transform: translateX(0);
}
.container.right-panel-active .overlay-right {
transform: translateX(20%);
}
.social-container {
margin: 20px 0;
}
.social-container a {
border: 1px solid #DDDDDD;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 0 5px;
height: 40px;
width: 40px;
}
footer {
background-color: #222;
color: #fff;
font-size: 14px;
bottom: 0;
position: fixed;
left: 0;
right: 0;
text-align: center;
z-index: 999;
}
footer p {
margin: 10px 0;
}
footer i {
color: red;
}
footer a {
color: #3c97bf;
text-decoration: none;
}
}

View File

@@ -0,0 +1,55 @@
import React, { useState } from 'react'
import { IconData } from './iconData'
import type { Category } from '../../../interfaces/cateogry.interface'
import { Button, Space, Table } from 'antd'
import { useTranslation } from 'react-i18next'
export default function CategoryManagement() {
const [categoryList, setCategoryList] = useState<Category[]>(JSON.parse(localStorage.getItem("categoryList") || "[]"))
console.log('categoryList', categoryList[0].icon)
const {t} = useTranslation()
const columns = [
{
title: 'Id',
dataIndex: 'id',
key: 'id',
},
{
title: 'Title',
dataIndex: 'title',
key: 'title',
},
{
title: 'Icon',
key: 'address',
render: (_: any, record: Category) => (
<Space size="middle">
<i className={record.icon}></i>
</Space>
),
},
{
title: 'Status',
key: 'isActive',
render: (_: any, record: Category) => (
<Space size="middle">
{
record.isActive ?
<Button color="danger" variant="solid">{t('block')}</Button> :
<Button color="cyan" variant="solid" >{t('unlock')}</Button>
}
</Space>
),
},
]
return (
<div>
<h1>CategoryManagement</h1>
<Table dataSource={categoryList} columns={columns} />;
</div>
)
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function FormAdd() {
return (
<div>
FormAdd
</div>
)
}

View File

@@ -0,0 +1,182 @@
export const IconData = [
"fa-classic fa-solid fa-house ",
"fa-classic fa-solid fa-circle-user ",
"fa-classic fa-solid fa-image ",
"fa-classic fa-solid fa-file ",
"fa-classic fa-solid fa-camera ",
"fa-classic fa-solid fa-calendar ",
"fa-classic fa-solid fa-cloud ",
"fa-classic fa-solid fa-alarm-clock ",
"fa-classic fa-solid fa-truck ",
"fa-classic fa-solid fa-thumbs-up ",
"fa-classic fa-solid fa-face-smile ",
"fa-classic fa-solid fa-headphones ",
"fa-classic fa-solid fa-bell ",
"fa-classic fa-solid fa-user ",
"fa-classic fa-solid fa-comment ",
"fa-classic fa-solid fa-envelope ",
"fa-classic fa-solid fa-globe ",
"fa-classic fa-solid fa-trophy ",
"fa-classic fa-solid fa-eye ",
"fa-classic fa-solid fa-inbox ",
"fa-classic fa-solid fa-print ",
"fa-classic fa-solid fa-suitcase ",
"fa-classic fa-solid fa-volume ",
"fa-classic fa-solid fa-magnifying-glass ",
"fa-classic fa-solid fa-check ",
"fa-classic fa-solid fa-download ",
"fa-classic fa-solid fa-font-awesome ",
"fa-classic fa-solid fa-web-awesome ",
"fa-classic fa-solid fa-phone ",
"fa-classic fa-solid fa-bars ",
"fa-classic fa-solid fa-star ",
"fa-classic fa-solid fa-location-dot ",
"fa-classic fa-solid fa-music ",
"fa-classic fa-solid fa-wand-magic-sparkles ",
"fa-classic fa-solid fa-face-awesome ",
"fa-classic fa-solid fa-heart ",
"fa-classic fa-solid fa-arrow-right ",
"fa-classic fa-solid fa-circle-xmark ",
"fa-classic fa-solid fa-bomb ",
"fa-classic fa-solid fa-poo ",
"fa-classic fa-solid fa-camera-retro ",
"fa-classic fa-solid fa-xmark ",
"fa-classic fa-solid fa-caret-up ",
"fa-classic fa-solid fa-truck-fast ",
"fa-classic fa-solid fa-pen-nib ",
"fa-classic fa-solid fa-arrow-up ",
"fa-classic fa-solid fa-hippo ",
"fa-classic fa-solid fa-calendar-days ",
"fa-classic fa-solid fa-paperclip ",
"fa-classic fa-solid fa-shield-halved ",
"fa-classic fa-solid fa-cart-shopping ",
"fa-classic fa-solid fa-clipboard ",
"fa-classic fa-solid fa-filter ",
"fa-classic fa-solid fa-circle-info ",
"fa-classic fa-solid fa-arrow-up-from-bracket ",
"fa-classic fa-solid fa-bolt ",
"fa-classic fa-solid fa-raygun ",
"fa-classic fa-solid fa-car ",
"fa-classic fa-solid fa-ghost ",
"fa-classic fa-solid fa-mug-hot ",
"fa-classic fa-solid fa-rocket-launch ",
"fa-classic fa-solid fa-pen ",
"fa-classic fa-solid fa-umbrella ",
"fa-classic fa-solid fa-gift ",
"fa-classic fa-solid fa-film ",
"fa-classic fa-solid fa-grid-2 ",
"fa-classic fa-solid fa-list ",
"fa-classic fa-solid fa-gear ",
"fa-classic fa-solid fa-trash ",
"fa-classic fa-solid fa-circle-up ",
"fa-classic fa-solid fa-circle-down ",
"fa-classic fa-solid fa-rotate-right ",
"fa-classic fa-solid fa-sparkles ",
"fa-classic fa-solid fa-lock ",
"fa-classic fa-solid fa-barcode ",
"fa-classic fa-solid fa-tag ",
"fa-classic fa-solid fa-book ",
"fa-classic fa-solid fa-bookmark ",
"fa-classic fa-solid fa-font ",
"fa-classic fa-solid fa-video ",
"fa-classic fa-solid fa-circle-half-stroke ",
"fa-classic fa-solid fa-droplet ",
"fa-classic fa-solid fa-pen-to-square ",
"fa-classic fa-solid fa-share-from-square ",
"fa-classic fa-solid fa-plus ",
"fa-classic fa-solid fa-minus ",
"fa-classic fa-solid fa-share ",
"fa-classic fa-solid fa-circle-exclamation ",
"fa-classic fa-solid fa-fire ",
"fa-classic fa-solid fa-eye-slash ",
"fa-classic fa-solid fa-plane ",
"fa-classic fa-solid fa-magnet ",
"fa-classic fa-solid fa-hand ",
"fa-classic fa-solid fa-folder ",
"fa-classic fa-solid fa-folder-open ",
"fa-classic fa-solid fa-money-bill ",
"fa-classic fa-solid fa-thumbs-down ",
"fa-classic fa-solid fa-comments ",
"fa-classic fa-solid fa-grill-hot ",
"fa-classic fa-solid fa-lemon ",
"fa-classic fa-solid fa-key ",
"fa-classic fa-solid fa-thumbtack ",
"fa-classic fa-solid fa-gears ",
"fa-classic fa-solid fa-paper-plane ",
"fa-classic fa-solid fa-code ",
"fa-classic fa-solid fa-text ",
"fa-classic fa-solid fa-arrow-down-to-line ",
"fa-classic fa-solid fa-city ",
"fa-classic fa-solid fa-ticket ",
"fa-classic fa-solid fa-tree ",
"fa-classic fa-solid fa-wifi ",
"fa-classic fa-solid fa-window ",
"fa-classic fa-solid fa-burger-soda ",
"fa-classic fa-solid fa-camera-movie ",
"fa-classic fa-solid fa-paint-roller ",
"fa-classic fa-solid fa-bicycle ",
"fa-classic fa-solid fa-sliders ",
"fa-classic fa-solid fa-brush ",
"fa-classic fa-solid fa-hashtag ",
"fa-classic fa-solid fa-megaphone ",
"fa-classic fa-solid fa-flask ",
"fa-classic fa-solid fa-briefcase ",
"fa-classic fa-solid fa-compass ",
"fa-classic fa-solid fa-dumpster-fire ",
"fa-classic fa-solid fa-person ",
"fa-classic fa-solid fa-person-dress ",
"fa-classic fa-solid fa-balloons ",
"fa-classic fa-solid fa-address-book ",
"fa-classic fa-solid fa-bath ",
"fa-classic fa-solid fa-handshake ",
"fa-classic fa-solid fa-snowflake ",
"fa-classic fa-solid fa-right-to-bracket ",
"fa-classic fa-solid fa-earth-americas ",
"fa-classic fa-solid fa-cloud-arrow-up ",
"fa-classic fa-solid fa-binoculars ",
"fa-classic fa-solid fa-palette ",
"fa-classic fa-solid fa-layer-group ",
"fa-classic fa-solid fa-brackets-curly ",
"fa-classic fa-solid fa-users ",
"fa-classic fa-solid fa-gamepad ",
"fa-classic fa-solid fa-business-time ",
"fa-classic fa-solid fa-feather ",
"fa-classic fa-solid fa-sun ",
"fa-classic fa-solid fa-computer-classic ",
"fa-classic fa-solid fa-link ",
"fa-classic fa-solid fa-pen-fancy ",
"fa-classic fa-solid fa-badge-check ",
"fa-classic fa-solid fa-fish ",
"fa-classic fa-solid fa-joystick ",
"fa-classic fa-solid fa-play-pause ",
"fa-classic fa-solid fa-bug ",
"fa-classic fa-solid fa-shop ",
"fa-classic fa-solid fa-mug-saucer ",
"fa-classic fa-solid fa-planet-ringed ",
"fa-classic fa-solid fa-landmark ",
"fa-classic fa-solid fa-campfire ",
"fa-classic fa-solid fa-poo-storm ",
"fa-classic fa-solid fa-chart-simple ",
"fa-classic fa-solid fa-shirt ",
"fa-classic fa-solid fa-files ",
"fa-classic fa-solid fa-dolphin ",
"fa-classic fa-solid fa-anchor ",
"fa-classic fa-solid fa-quote-left ",
"fa-classic fa-solid fa-atom-simple ",
"fa-classic fa-solid fa-bag-shopping ",
"fa-classic fa-solid fa-bed-front ",
"fa-classic fa-solid fa-people-simple ",
"fa-classic fa-solid fa-gauge ",
"fa-classic fa-solid fa-signal-bars ",
"fa-classic fa-solid fa-code-compare ",
"fa-classic fa-solid fa-user-secret ",
"fa-classic fa-solid fa-stethoscope ",
"fa-classic fa-solid fa-car-side ",
"fa-classic fa-solid fa-hand-holding-heart ",
"fa-classic fa-solid fa-ufo ",
"fa-classic fa-solid fa-alien-8bit ",
"fa-classic fa-solid fa-block-question ",
"fa-classic fa-solid fa-aperture ",
"fa-classic fa-solid fa-bird ",
"fa-classic fa-solid fa-strawberry "
]

View File

@@ -0,0 +1,39 @@
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { Button, Layout, theme } from 'antd';
import { useTranslation } from 'react-i18next';
const { Header } = Layout;
export default function HeaderCom({ collapsed, setCollapsed }: { collapsed: boolean, setCollapsed: any }) {
const {
token: { colorBgContainer },
} = theme.useToken();
const { t,i18n } = useTranslation()
return (
<Header style={{ padding: 0, background: colorBgContainer }}>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
width: 64,
height: 64,
}}
/>
<select onChange={(e) => {
localStorage.setItem("lng", e.target.value)
i18n.changeLanguage(e.target.value)
}}>
<option value="vi">Tiếng Việt</option>
<option value="en">Tiếng Anh</option>
<option value="ja">Tiếng Nhật</option>
</select>
<Button onClick={() => {
localStorage.removeItem("userLogin")
window.location.reload()
}}>{t('logout')}</Button>
</Header>
)
}

View File

@@ -0,0 +1,45 @@
import { UploadOutlined, UserOutlined, VideoCameraOutlined } from '@ant-design/icons'
import { Menu } from 'antd'
import Sider from 'antd/es/layout/Sider'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
export default function Slider({ collapsed }: { collapsed: boolean }) {
const { t } = useTranslation()
const navigate = useNavigate()
return (
<Sider style={{
height: "100vh"
}} trigger={null} collapsible collapsed={collapsed}>
<div className="demo-logo-vertical" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: 'user',
icon: <UserOutlined />,
label: t("Quản lý người dùng"),
},
{
key: 'category',
icon: <VideoCameraOutlined />,
label: t('category-management'),
},
{
key: 'product',
icon: <VideoCameraOutlined />,
label: t('product-management'),
},
]}
onClick={(e) => {
navigate(e.key)
}}
/>
</Sider>
)
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function ProductManagement() {
return (
<div>
ProductManagement
</div>
)
}

View File

@@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react'
import { UserRole, type User } from '../../../interfaces/user.interface'
import { useTranslation } from 'react-i18next';
import { Button, Space, Table } from 'antd';
export default function UserManagement() {
const { t } = useTranslation()
const [userList, setUserList] = useState<User[]>(JSON.parse(localStorage.getItem("userList") || "[]"))
const columns = [
{
title: t('user-id'),
dataIndex: 'id',
key: 'id',
},
{
title: t('fullname'),
dataIndex: 'fullName',
key: 'fullName',
},
{
title: t('age'),
dataIndex: 'age',
key: 'age',
},
{
title: t('phone-number'),
dataIndex: 'phone',
key: 'phone',
},
{
title: t('username'),
dataIndex: 'userName',
key: 'userName',
},
{
title: t('email'),
dataIndex: 'email',
key: 'email',
},
{
title: t('role'),
dataIndex: 'role',
key: 'role',
},
{
title: t('active'),
key: 'isActive',
render: (_: any, record: User) => (
<Space size="middle">
{
record.role != UserRole.ADMIN && (record.isActive ?
<Button color="danger" variant="solid" onClick={() => [
setUserList(userList.map((user) => {
if(user.id == record.id) {
return {
...record,
isActive: false
}
}
return user
}))
]}>{t('block')}</Button> :
<Button color="cyan" variant="solid" onClick={() => [
setUserList(userList.map((user) => {
if(user.id == record.id) {
return {
...record,
isActive: true
}
}
return user
}))
]}>{t('unlock')}</Button>)
}
</Space>
),
},
{
title: t('tools'),
key: 'tools',
render: (_: any, record: User) => (
<Space size="middle">
<Button onClick={() => [
console.log("record", record)
]}>Test</Button>
</Space>
),
},
];
useEffect(() => {
localStorage.setItem("userList", JSON.stringify(userList))
}, [userList])
return (
<div>
<p>{t("user-management")}</p>
<Table dataSource={userList.slice().sort((userA, userB) => {
return (userB.role == UserRole.ADMIN ? 1 : 0) - (userA.role == UserRole.ADMIN ? 1 : 0)
})} columns={columns} />;
</div>
)
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { useParams } from 'react-router'
export default function CollectionDetail() {
const {slugCollection} = useParams()
return (
<div>
Collection = {slugCollection}
</div>
)
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function About() {
return (
<div>
Về Chúng Tôi
</div>
)
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import HomeHeader from './components/HomeHeader'
import HomeFooter from './components/HomeFooter'
import './home.scss'
import HomeBanner from './components/HomeBanner'
import { Outlet } from 'react-router'
export default function Home() {
return (
<div className="home_page">
<HomeBanner/>
<HomeHeader/>
<div className='container'>
<div className='content'>
<Outlet/>
</div>
</div>
<HomeFooter/>
</div>
)
}

View File

@@ -0,0 +1,12 @@
import React from 'react'
import banner from '../../../../assets/img/banner.png'
export default function HomeBanner() {
return (
<div className='home_banner'>
<div style={{
backgroundImage: `url(${banner})`
}} className='content'>
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
export default function HomeFooter() {
return (
<footer>
<div className='content'>
footer
</div>
</footer>
)
}

View File

@@ -0,0 +1,124 @@
header {
background-color: white;
position: sticky;
top: 0;
.content {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
img {
height: 60px;
}
.menu-btn {
color: red;
cursor: pointer;
font-size: 20px;
}
.media_box {
height: 100%;
display: flex;
align-items: center;
.item {
display: flex;
align-items: center;
font-size: 10px;
cursor: pointer;
position: relative;
i {
font-size: 2em;
color: red;
margin-right: 5px;
}
&:last-child {
i {
margin-right: 12px;
}
}
.text_box {
font-size: 1.5em;
color: red;
p {
&:first-child {}
&:last-child {}
}
}
.cart_count {
position: absolute;
top: 0;
left: 30%;
width: 1.5em;
height: 1.5em;
font-size: 1em;
background-color: yellow;
color: black;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
border: 1px solid red;
}
}
}
.user_box {
width: 100px;
height: 100%;
display: flex;
align-items: center;
.login {}
.unlogin {
height: 100%;
display: flex;
align-items: center;
.unlogin-content {
background-color: rgba(220, 218, 218, 0.612);
padding: 2px;
display: flex;
align-items: center;
font-size: 10px;
cursor: pointer;
border-radius: 5px;
i {
font-size: 2em;
color: red;
margin-right: 5px;
}
&:last-child {
i {
margin-right: 12px;
}
}
.text_box {
font-size: 1.5em;
color: red;
p {
&:first-child {}
&:last-child {}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,83 @@
import React, { useState } from 'react'
import './HomeHeader.scss'
import logo from '../../../../assets/img/logo.png'
import Search from 'antd/es/input/Search'
import { useNavigate } from 'react-router'
export default function HomeHeader() {
const [isLogin, setIsLogin] = useState(false)
const navigate = useNavigate()
const menu = [
{
iconClass: "fa-solid fa-headphones",
text: ["Hotline", "1900.5301"],
path: "/hotline"
},
{
iconClass: "fa-solid fa-location-dot",
text: ["Hệ thống", "Showroom"],
path: "/about"
},
{
iconClass: "fa-solid fa-clipboard",
text: ["Tra cứu", "Đơn hàng"],
path: "/order-history"
}
]
return (
<header>
<div className='content'>
<img onClick={() => {
navigate("/")
}} src={logo} />
<i className="fa-solid fa-bars menu-btn"></i>
<Search placeholder="input search text" style={{ width: 200 }} />
<div className='media_box'>
{
menu.map((item) => {
return (
<div onClick={() => {
navigate(item.path)
}} className='item'>
<i className={item.iconClass}></i>
<div className='text_box'>
<p>{item.text[0]}</p>
<p>{item.text[1]}</p>
</div>
</div>
)
})
}
<div className='item'>
<i className="fa-solid fa-cart-shopping"></i>
<p className='cart_count'>0</p>
<div className='text_box'>
<p>Giỏ</p>
<p>Hàng</p>
</div>
</div>
</div>
<div className='user_box'>
{
isLogin ? (
<div className='login'>
</div>
) : (
<div className='unlogin'>
<div className='unlogin-content'>
<i className="fa-solid fa-user"></i>
<div className='text_box'>
<p>Đăng</p>
<p>Nhập</p>
</div>
</div>
</div>
)
}
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,54 @@
$widthContent: 1080px;
.home_page {
.home_banner {
width: 100%;
display: flex;
justify-content: center;
background-color: #C30100;
.content {
width: $widthContent;
}
height: 60px;
}
header {
width: 100%;
display: flex;
justify-content: center;
.content {
width: $widthContent;
}
height: 60px;
border-bottom: 1px solid black;
}
.container {
width: 100%;
display: flex;
justify-content: center;
.content {
width: $widthContent;
}
height: calc(100vh - 60px);
}
footer {
width: 100%;
height: 120px;
display: flex;
justify-content: center;
.content {
width: $widthContent;
}
background-color: yellow;
}
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

27
tsconfig.app.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": false,
"noUncheckedSideEffectImports": false
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": false,
"noUncheckedSideEffectImports": false
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})