feat: init page, router, layout

This commit is contained in:
2026-08-12 22:15:06 +07:00
parent 593de37987
commit c75e412051
19 changed files with 2163 additions and 378 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>aplp.frontend.lms</title> <title>APLP LMS</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1438 -5
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -10,11 +10,16 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8" "react-dom": "^19.2.8",
"react-router-dom": "^7.18.2",
"tailwindcss": "^4.3.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.3",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.4", "@vitejs/plugin-react": "^6.0.4",
+293 -160
View File
@@ -1,184 +1,317 @@
.counter { @import 'tailwindcss';
font-size: 16px; @import '@heroui/styles';
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover { .app-shell {
border-color: var(--accent-border); display: grid;
} grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
&:focus-visible { min-height: 100vh;
outline: 2px solid var(--accent); transition: grid-template-columns 180ms ease;
outline-offset: 2px;
}
} }
.hero { .app-shell--collapsed {
position: relative; grid-template-columns: var(--sidebar-collapsed-width) minmax(0, 1fr);
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
} }
#center { .sidebar {
display: flex; position: sticky;
flex-direction: column; top: 0;
gap: 25px; height: 100vh;
place-content: center; padding: 24px 16px;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
background: var(--surface);
overflow: hidden;
}
@media (max-width: 1024px) { .sidebar__brand {
border-right: none; display: flex;
align-items: center;
gap: 12px;
padding: 0 8px 28px;
}
.sidebar__logo {
display: inline-grid;
width: 40px;
height: 40px;
place-items: center;
border-radius: 8px;
color: #ffffff;
background: var(--primary);
font-weight: 800;
}
.sidebar__brand-text,
.sidebar__link-text {
min-width: 0;
white-space: nowrap;
opacity: 1;
transition:
opacity 140ms ease,
width 180ms ease;
}
.sidebar__brand strong,
.sidebar__brand span {
display: block;
}
.sidebar__brand strong {
line-height: 1.2;
}
.sidebar__brand span {
margin-top: 3px;
color: var(--text-muted);
font-size: 13px;
}
.sidebar__toggle {
display: grid;
width: 100%;
height: 36px;
margin-bottom: 14px;
place-items: center;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-muted);
background: var(--surface);
cursor: pointer;
font-weight: 800;
transition:
border-color 160ms ease,
color 160ms ease,
background-color 160ms ease;
}
.sidebar__toggle:hover {
color: var(--primary);
border-color: var(--primary);
background: var(--primary-soft);
}
.sidebar__toggle:focus-visible,
.sidebar__link:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.sidebar__nav {
display: grid;
gap: 6px;
}
.sidebar__link {
display: flex;
align-items: center;
min-height: 42px;
padding: 10px 12px;
border-radius: 8px;
color: var(--text-muted);
font-size: 15px;
font-weight: 600;
gap: 10px;
text-decoration: none;
transition:
background-color 160ms ease,
color 160ms ease;
}
.sidebar__link-icon {
display: inline-grid;
flex: 0 0 28px;
width: 28px;
height: 28px;
place-items: center;
border-radius: 7px;
color: var(--primary-dark);
background: var(--primary-soft);
font-size: 11px;
font-weight: 800;
}
.sidebar__link:hover {
color: var(--text);
background: var(--interactive-hover);
}
.sidebar__link--active {
color: var(--primary);
background: var(--interactive-active);
}
.app-shell--collapsed .sidebar {
padding-inline: 14px;
}
.app-shell--collapsed .sidebar__brand {
justify-content: center;
padding-inline: 0;
}
.app-shell--collapsed .sidebar__brand-text,
.app-shell--collapsed .sidebar__link-text {
width: 0;
opacity: 0;
pointer-events: none;
}
.app-shell--collapsed .sidebar__link {
justify-content: center;
padding-inline: 8px;
}
.app-shell--collapsed .sidebar__link-icon {
flex-basis: 30px;
width: 30px;
height: 30px;
}
.main-content {
min-width: 0;
padding: 32px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
}
.page-header h1 {
margin: 0;
font-size: 30px;
line-height: 1.2;
}
.page-header p {
max-width: 720px;
margin: 8px 0 0;
color: var(--text-muted);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.stat-card,
.content-panel {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
box-shadow: 0 16px 42px var(--shadow-color);
}
.stat-card {
padding: 18px;
}
.stat-card span {
display: block;
color: var(--text-muted);
font-size: 14px;
}
.stat-card strong {
display: block;
margin-top: 10px;
font-size: 28px;
line-height: 1.1;
}
.content-panel {
padding: 24px;
}
.content-panel h2 {
margin: 0;
font-size: 20px;
}
.content-panel p {
margin: 8px 0 0;
color: var(--text-muted);
}
@media (max-width: 900px) {
.app-shell {
grid-template-columns: 1fr;
}
.app-shell--collapsed {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
height: auto;
padding: 16px;
border-right: 0;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
}
#next-steps ul { .sidebar__brand {
list-style: none; padding: 0 0 16px;
padding: 0; }
.app-shell--collapsed .sidebar {
padding: 16px;
}
.app-shell--collapsed .sidebar__brand {
justify-content: flex-start;
padding: 0 0 16px;
}
.app-shell--collapsed .sidebar__brand-text,
.app-shell--collapsed .sidebar__link-text {
width: auto;
opacity: 1;
pointer-events: auto;
}
.sidebar__toggle {
display: none;
}
.sidebar__nav {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin: 32px 0 0; overflow-x: auto;
padding-bottom: 2px;
.logo {
height: 18px;
} }
a { .sidebar__link {
color: var(--text-h); flex: 0 0 auto;
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
} }
@media (max-width: 1024px) { .app-shell--collapsed .sidebar__link {
margin-top: 20px; justify-content: flex-start;
flex-wrap: wrap; padding-inline: 12px;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
} }
a { .main-content {
width: 100%; padding: 24px 16px;
justify-content: center;
box-sizing: border-box;
} }
.page-header {
margin-bottom: 18px;
}
.page-header h1 {
font-size: 26px;
}
.stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
#spacer { @media (max-width: 560px) {
height: 88px; .stat-grid {
border-top: 1px solid var(--border); grid-template-columns: 1fr;
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
} }
} }
+22 -117
View File
@@ -1,122 +1,27 @@
import { useState } from 'react' import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import reactLogo from './assets/react.svg' import MainLayout from './layouts/MainLayout.jsx';
import viteLogo from './assets/vite.svg' import DashboardPage from './pages/Dashboard/DashboardPage.jsx';
import heroImg from './assets/hero.png' import CoursesPage from './pages/CoursesPage.jsx';
import './App.css' import StudentsPage from './pages/StudentsPage.jsx';
import ClassesPage from './pages/ClassesPage.jsx';
import ReportsPage from './pages/ReportsPage.jsx';
import './App.css';
function App() { function App() {
const [count, setCount] = useState(0)
return ( return (
<> <BrowserRouter>
<section id="center"> <Routes>
<div className="hero"> <Route element={<MainLayout />}>
<img src={heroImg} className="base" width="170" height="179" alt="" /> <Route index element={<Navigate to="/dashboard" replace />} />
<img src={reactLogo} className="framework" alt="React logo" /> <Route path="/dashboard" element={<DashboardPage />} />
<img src={viteLogo} className="vite" alt="Vite logo" /> <Route path="/courses" element={<CoursesPage />} />
</div> <Route path="/students" element={<StudentsPage />} />
<div> <Route path="/classes" element={<ClassesPage />} />
<h1>Get started</h1> <Route path="/reports" element={<ReportsPage />} />
<p> </Route>
Edit <code>src/App.jsx</code> and save to test <code>HMR</code> </Routes>
</p> </BrowserRouter>
</div> );
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
} }
export default App export default App;
+12
View File
@@ -0,0 +1,12 @@
function PageHeader({ title, description }) {
return (
<header className="page-header">
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
</header>
)
}
export default PageHeader
+14
View File
@@ -0,0 +1,14 @@
function StatGrid({ items }) {
return (
<section className="stat-grid" aria-label="Metrics">
{items.map((item) => (
<article className="stat-card" key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
</article>
))}
</section>
)
}
export default StatGrid
+121
View File
@@ -0,0 +1,121 @@
export const colors = {
// =========================
// Brand
// =========================
primary: {
main: '#1A73E8',
light: '#E8F1FD',
dark: '#1558B0',
contrast: '#FFFFFF',
},
// =========================
// Text & Icon
// =========================
slate: {
50: '#F8FAFC',
100: '#F1F5F9',
200: '#E2E8F0',
300: '#CBD5E1',
400: '#94A3B8',
500: '#64748B',
600: '#475569',
700: '#334155',
800: '#1E293B',
900: '#0F172A',
},
// =========================
// Background
// =========================
background: {
default: '#F8FAFC',
surface: '#FFFFFF',
muted: '#F1F5F9',
},
// =========================
// Border
// =========================
border: {
default: '#E2E8F0',
input: '#CBD5E1',
focus: '#1A73E8',
divider: '#E2E8F0',
},
// =========================
// Status
// =========================
status: {
success: {
main: '#16A34A',
light: '#DCFCE7',
dark: '#166534',
},
warning: {
main: '#D97706',
light: '#FEF3C7',
dark: '#92400E',
},
error: {
main: '#DC2626',
light: '#FEE2E2',
dark: '#991B1B',
},
info: {
main: '#2563EB',
light: '#DBEAFE',
dark: '#1E40AF',
},
neutral: {
main: '#64748B',
light: '#F1F5F9',
dark: '#475569',
},
},
// =========================
// Interactive
// =========================
interactive: {
hover: '#F1F5F9',
active: '#E8F1FD',
selected: '#E8F1FD',
focus: '#1A73E8',
disabled: '#CBD5E1',
},
// =========================
// Drag & Drop
// =========================
drag: {
outline: '#1A73E8',
background: '#FFFFFF',
dropZone: '#1A73E8',
dropZoneBackground: '#E8F1FD',
},
// =========================
// Overlay / Shadow
// =========================
overlay: {
shadow: 'rgba(0, 0, 0, 0.05)',
backdrop: 'rgba(15, 23, 42, 0.32)',
},
// =========================
// Common
// =========================
common: {
white: '#FFFFFF',
black: '#000000',
transparent: 'transparent',
},
} as const;
export type Colors = typeof colors;
+27
View File
@@ -0,0 +1,27 @@
export const navigationItems = [
{
label: 'Tong quan',
shortLabel: 'TQ',
path: '/dashboard',
},
{
label: 'Khoa hoc',
shortLabel: 'KH',
path: '/courses',
},
{
label: 'Hoc vien',
shortLabel: 'HV',
path: '/students',
},
{
label: 'Lop hoc',
shortLabel: 'LH',
path: '/classes',
},
{
label: 'Bao cao',
shortLabel: 'BC',
path: '/reports',
},
]
+26
View File
@@ -0,0 +1,26 @@
import { colors } from './color'
export const themeCssVariables = {
'--bg': colors.background.default,
'--surface': colors.background.surface,
'--surface-muted': colors.background.muted,
'--border': colors.border.default,
'--border-input': colors.border.input,
'--text': colors.slate[800],
'--text-strong': colors.slate[900],
'--text-muted': colors.slate[500],
'--primary': colors.primary.main,
'--primary-dark': colors.primary.dark,
'--primary-soft': colors.primary.light,
'--primary-contrast': colors.primary.contrast,
'--interactive-hover': colors.interactive.hover,
'--interactive-active': colors.interactive.active,
'--focus': colors.interactive.focus,
'--shadow-color': colors.overlay.shadow,
}
export function applyThemeCssVariables() {
Object.entries(themeCssVariables).forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value)
})
}
+35 -92
View File
@@ -1,111 +1,54 @@
:root { :root {
--text: #6b6375; --bg: #f6f7f9;
--text-h: #08060d; --surface: #ffffff;
--bg: #fff; --surface-muted: #f0f3f7;
--border: #e5e4e7; --border: #dfe3ea;
--code-bg: #f4f3ec; --text: #1d2433;
--accent: #aa3bff; --text-muted: #667085;
--accent-bg: rgba(170, 59, 255, 0.1); --primary: #2563eb;
--accent-border: rgba(170, 59, 255, 0.5); --primary-dark: #1d4ed8;
--social-bg: rgba(244, 243, 236, 0.5); --primary-soft: #e9f0ff;
--shadow: --primary-contrast: #ffffff;
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; --interactive-hover: #f0f3f7;
--interactive-active: #e9f0ff;
--focus: #2563eb;
--shadow: 0 16px 42px rgba(29, 36, 51, 0.08);
--shadow-color: rgba(0, 0, 0, 0.05);
--sidebar-width: 264px;
--sidebar-collapsed-width: 88px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif; font-family:
--heading: system-ui, 'Segoe UI', Roboto, sans-serif; Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
--mono: ui-monospace, Consolas, monospace; sans-serif;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text); color: var(--text);
background: var(--bg); background: var(--bg);
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
} }
@media (prefers-color-scheme: dark) { * {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
h1, body {
h2 { min-width: 320px;
font-family: var(--heading); min-height: 100vh;
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0; margin: 0;
} }
code, a {
.counter { color: inherit;
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
} }
code { button,
font-size: 15px; input,
line-height: 135%; textarea,
padding: 4px 8px; select {
background: var(--code-bg); font: inherit;
}
#root {
min-height: 100vh;
} }
+53
View File
@@ -0,0 +1,53 @@
import { useState } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
import { navigationItems } from '../config/navigation.js'
function MainLayout() {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
return (
<div className={isSidebarCollapsed ? 'app-shell app-shell--collapsed' : 'app-shell'}>
<aside className="sidebar">
<div className="sidebar__brand">
<span className="sidebar__logo">L</span>
<div className="sidebar__brand-text">
<strong>APLP LMS</strong>
<span>Learning platform</span>
</div>
</div>
<button
type="button"
className="sidebar__toggle"
aria-label={isSidebarCollapsed ? 'Mo rong sidebar' : 'Thu gon sidebar'}
title={isSidebarCollapsed ? 'Mo rong sidebar' : 'Thu gon sidebar'}
onClick={() => setIsSidebarCollapsed((current) => !current)}
>
{isSidebarCollapsed ? '>' : '<'}
</button>
<nav className="sidebar__nav" aria-label="Main navigation">
{navigationItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
isActive ? 'sidebar__link sidebar__link--active' : 'sidebar__link'
}
title={item.label}
>
<span className="sidebar__link-icon">{item.shortLabel}</span>
<span className="sidebar__link-text">{item.label}</span>
</NavLink>
))}
</nav>
</aside>
<main className="main-content">
<Outlet />
</main>
</div>
)
}
export default MainLayout
+3
View File
@@ -2,6 +2,9 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from './App.jsx' import App from './App.jsx'
import { applyThemeCssVariables } from './config/theme.js'
applyThemeCssVariables()
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
+18
View File
@@ -0,0 +1,18 @@
import PageHeader from '../components/PageHeader.jsx'
function ClassesPage() {
return (
<>
<PageHeader
title="Lop hoc"
description="Sap xep lop, lich hoc, giang vien va danh sach hoc vien."
/>
<section className="content-panel">
<h2>Lich lop hoc</h2>
<p>Khoi tao lich lop va trang thai diem danh tai day.</p>
</section>
</>
)
}
export default ClassesPage
+18
View File
@@ -0,0 +1,18 @@
import PageHeader from '../components/PageHeader.jsx'
function CoursesPage() {
return (
<>
<PageHeader
title="Khoa hoc"
description="Quan ly danh sach khoa hoc, noi dung bai hoc va trang thai xuat ban."
/>
<section className="content-panel">
<h2>Danh sach khoa hoc</h2>
<p>Khoi tao noi dung quan ly khoa hoc tai day.</p>
</section>
</>
)
}
export default CoursesPage
+37
View File
@@ -0,0 +1,37 @@
import PageHeader from '../../components/PageHeader.jsx';
import StatGrid from '../../components/StatGrid.jsx';
import { Label, SearchField } from '@heroui/react';
const stats = [
{ label: 'Khoa hoc dang mo', value: '24' },
{ label: 'Hoc vien', value: '1,248' },
{ label: 'Lop trong tuan', value: '18' },
{ label: 'Ty le hoan thanh', value: '82%' }
];
function DashboardPage() {
return (
<>
<PageHeader
title="Tong quan"
description="Theo doi nhanh tinh hinh dao tao va cac chi so LMS."
/>
<div className="mb-3 flex flex-col">
<SearchField name="search">
<Label>Search</Label>
<SearchField.Group className="w-[280px]">
<SearchField.SearchIcon />
<SearchField.Input className="flex-1" placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
</div>
<StatGrid items={stats} />
</>
);
}
export default DashboardPage;
+18
View File
@@ -0,0 +1,18 @@
import PageHeader from '../components/PageHeader.jsx'
function ReportsPage() {
return (
<>
<PageHeader
title="Bao cao"
description="Tong hop so lieu hoc tap, hieu qua khoa hoc va ket qua danh gia."
/>
<section className="content-panel">
<h2>Bao cao dao tao</h2>
<p>Khoi tao bieu do va bao cao xuat file tai day.</p>
</section>
</>
)
}
export default ReportsPage
+18
View File
@@ -0,0 +1,18 @@
import PageHeader from '../components/PageHeader.jsx'
function StudentsPage() {
return (
<>
<PageHeader
title="Hoc vien"
description="Quan ly ho so hoc vien, tien do hoc tap va lich su ghi danh."
/>
<section className="content-panel">
<h2>Danh sach hoc vien</h2>
<p>Khoi tao bang hoc vien va bo loc tai day.</p>
</section>
</>
)
}
export default StudentsPage
+2 -1
View File
@@ -1,7 +1,8 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react(), tailwindcss()],
}) })