Package Exports
- @libeyondea/base-cms
Readme
📦 @libeyondea/base-cms
Thư viện React CMS chuyên nghiệp với bộ component, hooks và utilities đầy đủ, được xây dựng trên Material-UI v7 và các công nghệ hiện đại nhất.
📋 Mục lục
- Giới thiệu
- Cài đặt
- Bắt đầu nhanh
- Components
- Hooks
- Services
- Utilities
- Theme System
- Ví dụ chi tiết
- API Reference
- TypeScript Support
- License
🎯 Giới thiệu
@libeyondea/base-cms là một thư viện React toàn diện, cung cấp tất cả những gì bạn cần để xây dựng một hệ thống CMS (Content Management System) hiện đại và mạnh mẽ. Được thiết kế với triết lý "batteries included" - tất cả đều có sẵn và sẵn sàng sử dụng ngay lập tức.
✨ Tính năng chính
- 🎨 40+ UI Components - Dựa trên Material-UI v7, tùy chỉnh sâu cho use-case CMS
- 📝 Form System hoàn chỉnh - React Hook Form 7.63 + Yup validation
- 📊 Advanced Table - TanStack Table v8 với filtering, sorting, pagination
- 🎭 Theme System linh hoạt - Dark/Light mode, customizable colors
- 🔐 Auth & Routing - Guards, layouts cho auth/private/public routes
- 📡 API Integration - BaseService class với full CRUD operations
- 🎯 TypeScript First - 100% type-safe với full type definitions
- 🚀 Production Ready - Optimized build, tree-shakeable, < 200KB gzipped
- ♿ Accessibility - WCAG 2.1 Level AA compliant
- 📱 Responsive - Mobile-first design
🏗️ Tech Stack
Category | Technologies |
---|---|
Core | React 19.2, TypeScript 5.9, Vite 7.1 |
UI Framework | Material-UI v7, Emotion |
Form | React Hook Form 7.63, Yup 1.7 |
State | Redux Toolkit 2.9, React Redux 9.2 |
Data Fetching | TanStack React Query 5.90, Axios 1.12 |
Table | TanStack React Table 8.21 |
Routing | React Router DOM 7.9 |
Utils | Lodash-es, Moment, Qs |
📦 Cài đặt
Bước 1: Cài đặt package chính
npm install @libeyondea/base-cms
Bước 2: Cài đặt dev dependencies (Khuyến nghị)
npm install --save-dev @libeyondea/base-cms-dev
Package @libeyondea/base-cms-dev
bao gồm:
- TypeScript 5.9.3
- Vite 7.1.9 + plugins
- ESLint 9.36.0 + Prettier 3.6.2
- Type definitions (@types/*)
Bước 3: Cài đặt peer dependencies (Bắt buộc)
# Cài đặt tất cả peer dependencies cần thiết
npm install react@19.2.0 react-dom@19.2.0 react-router-dom@7.9.3 @reduxjs/toolkit@2.9.0 react-redux@9.2.0 @emotion/react@11.14.0 @emotion/styled@11.14.1 @mui/icons-material@7.3.4 @mui/material@7.3.4 @mui/system@7.3.3 @mui/x-date-pickers@8.12.0 @tanstack/react-query@5.90.2 @tanstack/react-table@8.21.3 @hookform/resolvers@5.2.2 axios@1.12.2 dayjs@1.11.18 js-cookie@3.0.5 lodash-es@4.17.21 qs@6.14.0 react-big-calendar@1.19.4 react-hook-form@7.63.0 react-icons@5.5.0 react-number-format@5.4.4 react-toastify@11.0.5 sweetalert2@11.23.0 yup@1.7.1
⚠️ Peer Dependencies (BẮT BUỘC phải cài đặt)
Library này yêu cầu các dependencies sau phải được cài đặt trong project của bạn:
Xem danh sách peer dependencies cần cài đặt
UI & Styling
@emotion/react
(11.14.0)@emotion/styled
(11.14.1)@mui/material
(7.3.4)@mui/system
(7.3.3)@mui/icons-material
(7.3.4)@mui/x-date-pickers
(8.12.0)
Form & Validation
react-hook-form
(7.63.0)@hookform/resolvers
(5.2.2)yup
(1.7.1)
State Management
@reduxjs/toolkit
(2.9.0)react-redux
(9.2.0)
Data Fetching & Tables
@tanstack/react-query
(5.90.2)@tanstack/react-table
(8.21.3)axios
(1.12.2)
Utilities
dayjs
(1.11.18)lodash-es
(4.17.21)js-cookie
(3.0.5)qs
(6.14.0)
UI Components
react-big-calendar
(1.19.4)react-icons
(5.5.0)react-number-format
(5.4.4)react-toastify
(11.0.5)sweetalert2
(11.23.0)
⚠️ Lưu ý quan trọng: Tất cả các dependencies trên đều là peer dependencies và BẮT BUỘC phải cài đặt trong project của bạn.
Tại sao sử dụng Peer Dependencies?
Library sử dụng peer dependencies thay vì bundle dependencies vì:
- ✅ Tránh xung đột phiên bản: Người dùng có thể kiểm soát phiên bản dependencies
- ✅ Giảm kích thước bundle: Library nhẹ hơn, chỉ chứa code thực tế
- ✅ Tương thích tốt hơn: Hoạt động với project hiện có mà không gây xung đột
- ✅ Flexibility: Người dùng có thể chọn phiên bản dependencies phù hợp
- ✅ Tree-shaking tốt hơn: Chỉ import những gì thực sự cần thiết
🚀 Bắt đầu nhanh
Setup cơ bản
import React from 'react';
import { AppProvider } from '@libeyondea/base-cms';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<AppProvider>
<h1>Hello Base CMS!</h1>
</AppProvider>
</BrowserRouter>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
Sử dụng với custom theme
import { AppProvider, createCustomTheme } from '@libeyondea/base-cms';
import { PaletteMode } from '@mui/material';
// Tạo custom theme
const myCustomTheme = (mode: PaletteMode) => ({
palette: {
mode,
primary: {
main: '#1976d2'
},
secondary: {
main: '#dc004e'
}
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
}
});
function App() {
return <AppProvider customTheme={myCustomTheme}>{/* Your app content */}</AppProvider>;
}
Ví dụ Form đơn giản
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, MainCard, RHFTextField } from '@libeyondea/base-cms';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
const schema = yup.object({
email: yup.string().email('Email không hợp lệ').required('Email là bắt buộc'),
password: yup.string().min(6, 'Mật khẩu tối thiểu 6 ký tự').required('Mật khẩu là bắt buộc')
});
function LoginForm() {
const methods = useForm({
resolver: yupResolver(schema),
defaultValues: { email: '', password: '' }
});
const onSubmit = (data: any) => {
console.log('Login data:', data);
};
return (
<MainCard title="Đăng nhập">
<FormProvider methods={methods} onSubmit={onSubmit}>
<RHFTextField name="email" label="Email" type="email" />
<RHFTextField name="password" label="Mật khẩu" type="password" />
<button type="submit">Đăng nhập</button>
</FormProvider>
</MainCard>
);
}
🎨 Components
Form Components
Lưu ý quan trọng: Tất cả component có prefix
RHF
(React Hook Form) phải được wrap trongFormProvider
để hoạt động.
Component | Mô tả | Props chính |
---|---|---|
FormProvider | Provider cho React Hook Form context | methods , onSubmit , children |
FormLabel | Label component với styling nhất quán | label , required , tooltip |
RHFTextField | Text input với validation | name , label , type , placeholder |
RHFPhone | Input số điện thoại (format VN) | name , label , placeholder |
RHFDatePicker | Date picker với validation | name , label , minDate , maxDate |
RHFTimePicker | Time picker | name , label |
RHFAutocomplete | Autocomplete single selection | name , label , options |
RHFAutocompleteMulti | Autocomplete multiple selection | name , label , options |
RHFNationalID | Input CMND/CCCD với validation | name , label |
RHFTextFieldSelect | Text field kết hợp dropdown | name , label , options |
RHFSelect | Select dropdown | name , label , options |
RHFSwitch | Toggle switch | name , label |
RHFTextFieldAdvanced | Text field nâng cao với nhiều tùy chọn | name , label , multiline , rows |
Ví dụ sử dụng Form Components
import { FormProvider, RHFAutocomplete, RHFDatePicker, RHFSelect, RHFTextField } from '@libeyondea/base-cms';
import { useForm } from 'react-hook-form';
const roleOptions = [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'User' }
];
function UserForm() {
const methods = useForm({
defaultValues: {
name: '',
role: 1,
birthDate: null,
department: null
}
});
return (
<FormProvider methods={methods} onSubmit={methods.handleSubmit(console.log)}>
<RHFTextField name="name" label="Họ và tên" />
<RHFSelect name="role" label="Vai trò" options={roleOptions} />
<RHFDatePicker name="birthDate" label="Ngày sinh" />
<RHFAutocomplete
name="department"
label="Phòng ban"
options={[
{ id: 1, name: 'IT' },
{ id: 2, name: 'HR' }
]}
/>
<button type="submit">Lưu</button>
</FormProvider>
);
}
Input Components (Không cần Form Provider)
Components có prefix N
là các input component độc lập, không cần React Hook Form:
Component | Mô tả | Use Case |
---|---|---|
NTextField | Text input cơ bản | Khi không cần validation phức tạp |
NSelect | Select dropdown cơ bản | Simple dropdown |
NAutocompleteMulti | Autocomplete multiple | Tag selection |
NTextFieldSelect | Text field + dropdown | Combined input |
Table Components
Hệ thống table mạnh mẽ dựa trên TanStack Table v8:
Component | Mô tả |
---|---|
StanstackTable | Table component chính với full features |
SubTable | Nested table cho hierarchical data |
TableToolbar | Toolbar với search, filters, actions |
TableSkeletonRow | Loading skeleton cho table |
DefaultColumnFilter | Default filter component |
EmptyView | Empty state view |
Row | Custom row component |
Table Props chính
interface StanstackTableProps {
data: any[]; // Dữ liệu table
columns: ColumnDef<any>[]; // Column definitions
enablePagination?: boolean; // Bật pagination
enableSorting?: boolean; // Bật sorting
enableFiltering?: boolean; // Bật filtering
enableRowSelection?: boolean; // Bật row selection
pageSize?: number; // Số rows mỗi page
onRowClick?: (row: any) => void; // Click handler
}
Ví dụ Table
import { StanstackTable, StatusChip } from '@libeyondea/base-cms';
const columns = [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'name',
header: 'Tên'
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'status',
header: 'Trạng thái',
cell: ({ getValue }) => <StatusChip status={getValue() === 1 ? 'active' : 'inactive'} label={getValue() === 1 ? 'Hoạt động' : 'Không hoạt động'} />
}
];
const data = [
{ id: 1, name: 'Nguyễn Văn A', email: 'a@example.com', status: 1 },
{ id: 2, name: 'Trần Thị B', email: 'b@example.com', status: 0 }
];
function UserTable() {
return (
<StanstackTable
data={data}
columns={columns}
enablePagination
enableSorting
enableFiltering
pageSize={10}
onRowClick={(row) => console.log('Clicked:', row)}
/>
);
}
UI Components
Component | Mô tả | Use Case |
---|---|---|
MainCard | Card container chính | Wrap content sections |
PageContainer | Page layout container | Main page wrapper |
CommonModal | Modal component | Dialogs, confirmations |
CustomBreadcrumbs | Breadcrumb navigation | Page hierarchy |
CustomTabs | Tab component | Tabbed interfaces |
DateRangePicker | Date range selector | Filter by date range |
LoadingScreen | Full-screen loader | Loading states |
MenuPopup | Popup menu | Actions menu |
SoundButton | Button với sound effect | Interactive buttons |
StatusChip | Status badge | Display status |
TruncatedText | Text với tooltip | Long text handling |
Layout Components
Component | Mô tả |
---|---|
Header | Top navigation header |
Sidebar | Side navigation menu |
Layout/Auth | Layout cho authentication pages |
Layout/Private | Layout cho private/protected pages |
Layout/Public | Layout cho public pages |
🎣 Hooks
useTheme
Hook để truy cập và điều khiển theme:
import { useTheme } from '@libeyondea/base-cms';
function ThemeToggler() {
const { mode, toggleTheme, setThemeMode } = useTheme();
return (
<div>
<p>Current mode: {mode}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
<button onClick={() => setThemeMode('dark')}>Set Dark</button>
<button onClick={() => setThemeMode('light')}>Set Light</button>
</div>
);
}
API:
theme
- Current theme object (Material-UI Theme)mode
- Current mode:'light' | 'dark'
toggleTheme()
- Toggle between light/darksetThemeMode(mode)
- Set specific mode
useSidebar
Hook để điều khiển sidebar:
import { useSidebar } from '@libeyondea/base-cms';
function SidebarToggle() {
const { drawerOpen, toggleDrawer, setDrawerOpen } = useSidebar();
return (
<div>
<button onClick={toggleDrawer}>Toggle Sidebar</button>
<button onClick={() => setDrawerOpen(true)}>Open Sidebar</button>
<button onClick={() => setDrawerOpen(false)}>Close Sidebar</button>
</div>
);
}
API:
drawerOpen
- Boolean state của drawertoggleDrawer()
- Toggle drawer open/closesetDrawerOpen(open)
- Set drawer state
useTableContext
Hook để quản lý table filters:
import { useTableContext } from '@libeyondea/base-cms';
function TableFilter() {
const { filters, setFilterTable } = useTableContext();
const handleFilter = () => {
setFilterTable({
type: 'users',
value: { status: 1, role: 'admin' }
});
};
return <button onClick={handleFilter}>Apply Filter</button>;
}
API:
filters
- Object chứa tất cả filters theo typesetFilterTable({ type, value })
- Set filter cho một table type
useStateValue
Hook để truy cập Redux state (deprecated, dùng React Redux hooks thay thế):
import { useStateValue } from '@libeyondea/base-cms';
function UserProfile() {
const { state, dispatch } = useStateValue();
return <div>User: {state.user?.name}</div>;
}
useAudioPlayer
Hook để play audio:
import { useAudioPlayer } from '@libeyondea/base-cms';
function SoundPlayer() {
const { play, pause, stop, isPlaying } = useAudioPlayer('/sounds/click.mp3');
return (
<div>
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
<button onClick={stop}>Stop</button>
<p>Status: {isPlaying ? 'Playing' : 'Stopped'}</p>
</div>
);
}
API:
play()
- Play audiopause()
- Pause audiostop()
- Stop và reset audioisPlaying
- Boolean playing state
useSweetAlert
Hook để hiển thị SweetAlert2 dialogs:
import { useSweetAlert } from '@libeyondea/base-cms';
function DeleteButton() {
const { showConfirm, showSuccess, showError } = useSweetAlert();
const handleDelete = async () => {
const result = await showConfirm({
title: 'Xác nhận xóa',
text: 'Bạn có chắc muốn xóa?'
});
if (result.isConfirmed) {
// Delete logic
showSuccess('Đã xóa thành công!');
}
};
return <button onClick={handleDelete}>Delete</button>;
}
API:
showConfirm(options)
- Show confirmation dialogshowSuccess(message)
- Show success messageshowError(message)
- Show error messageshowWarning(message)
- Show warning messageshowInfo(message)
- Show info message
🛠️ Services
BaseService
Abstract class cung cấp các phương thức CRUD chuẩn:
import { BaseService } from '@libeyondea/base-cms';
// Tạo service cho User
class UserService extends BaseService {
constructor() {
super('/users'); // API endpoint prefix
}
// Custom methods
async getUserProfile(userId: string) {
return this.getById(userId, {}, '/profile');
}
async updateUserRole(userId: string, role: string) {
return this.update(userId, { role }, '/role');
}
}
const userService = new UserService();
// Sử dụng
async function loadUsers() {
// GET /users?page=1&limit=10
const response = await userService.getAll({ page: 1, limit: 10 });
console.log(response.data);
// GET /users/123
const user = await userService.getById('123');
// POST /users
const newUser = await userService.create({ name: 'John', email: 'john@example.com' });
// PUT /users/123
const updated = await userService.update('123', { name: 'Jane' });
// DELETE /users/123
await userService.delete('123');
}
BaseService API
Method | Signature | Mô tả |
---|---|---|
getAll |
getAll<T>(params?, path?, config?, options?) |
GET danh sách với filtering & pagination |
getById |
getById<T>(id, params?, path?, config?, options?) |
GET theo ID |
create |
create<T>(data, path?, config?, options?) |
POST tạo mới |
update |
update<T>(id, data, path?, config?, options?) |
PUT cập nhật |
delete |
delete(id, path?, config?, options?) |
DELETE xóa |
deleteWithPayload |
deleteWithPayload(payload, path?, config?, options?) |
DELETE với body |
uploadFile |
uploadFile<T>(endpoint, file, config?, options?) |
POST upload file |
ServiceOptions
interface ServiceOptions {
isOtherUrl?: boolean; // Sử dụng URL khác (không prefix apiName)
isFormData?: boolean; // Request là FormData
timeout?: number; // Timeout (ms), default: 30000
}
Ví dụ Upload File
class FileService extends BaseService {
constructor() {
super('/files');
}
async uploadAvatar(file: File) {
return this.uploadFile('/upload/avatar', file);
}
async uploadMultiple(files: File[]) {
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`file${index}`, file);
});
return this.uploadFile('/upload/multiple', formData);
}
}
🧰 Utilities
Axios Instance
Pre-configured axios instance:
import { axiosServices } from '@libeyondea/base-cms';
// Đã có sẵn interceptors cho auth, error handling
const response = await axiosServices.get('/api/users');
Color Utilities
import { generateColorPalette, getContrastColor, hexToRgb } from '@libeyondea/base-cms';
// Generate color palette
const palette = generateColorPalette('#1976d2');
// Returns: { 50: '#...', 100: '#...', ..., 900: '#...' }
// Get contrast color (black or white)
const contrast = getContrastColor('#1976d2');
// Returns: '#ffffff' hoặc '#000000'
// Convert hex to RGB
const rgb = hexToRgb('#1976d2');
// Returns: { r: 25, g: 118, b: 210 }
Time Utilities
import { formatDate, formatDateTime, formatRelativeTime, formatTime, isToday, isYesterday } from '@libeyondea/base-cms';
const now = new Date();
formatDate(now); // "04/10/2025"
formatDateTime(now); // "04/10/2025 14:30"
formatTime(now); // "14:30"
formatRelativeTime(now); // "vừa xong", "5 phút trước", etc.
isToday(now); // true/false
isYesterday(now); // true/false
Format Utilities
import { formatCurrency, formatFileSize, formatNumber, formatPhone } from '@libeyondea/base-cms';
formatCurrency(1000000); // "1,000,000 VND"
formatNumber(1234.56); // "1,234.56"
formatPhone('0123456789'); // "0123 456 789"
formatFileSize(1024); // "1 KB"
formatFileSize(1048576); // "1 MB"
Cookie Utilities
import { getCookie, removeCookie, setCookie } from '@libeyondea/base-cms';
// Set cookie (expires in 7 days)
setCookie('token', 'abc123', 7);
// Get cookie
const token = getCookie('token');
// Remove cookie
removeCookie('token');
// Set cookie with options
setCookie('user', JSON.stringify({ id: 1 }), 7, {
secure: true,
sameSite: 'strict'
});
Constants
import { MONTHS, REQUIRED_MESSAGE, STATUS_CONSTANT, USER_CONSTANT, WEEK_DAYS } from '@libeyondea/base-cms';
// Validation messages
console.log(REQUIRED_MESSAGE); // "Trường này là bắt buộc"
// Status options
console.log(STATUS_CONSTANT);
// [{ id: 0, name: 'Không hoạt động' }, { id: 1, name: 'Hoạt động' }]
// User role options
console.log(USER_CONSTANT);
// [{ id: 0, name: 'Quản trị viên' }, { id: 1, name: 'Người dùng' }]
// Week days
console.log(WEEK_DAYS);
// [{ id: '0', name: 'CN' }, { id: '1', name: 'T2' }, ...]
// Months
console.log(MONTHS);
// [{ id: '0', name: 'Tháng 1' }, { id: '1', name: 'Tháng 2' }, ...]
Array & Character Formatters
import { capitalizeFirstLetter, groupBy, slugify, sortBy, truncate, uniqueArray } from '@libeyondea/base-cms';
// Unique array
uniqueArray([1, 2, 2, 3]); // [1, 2, 3]
// Group by property
const users = [
{ id: 1, role: 'admin' },
{ id: 2, role: 'user' },
{ id: 3, role: 'admin' }
];
groupBy(users, 'role');
// { admin: [{...}, {...}], user: [{...}] }
// Sort by property
sortBy(users, 'id', 'desc');
// Capitalize
capitalizeFirstLetter('hello'); // "Hello"
// Slugify
slugify('Xin chào Việt Nam'); // "xin-chao-viet-nam"
// Truncate
truncate('Long text...', 10); // "Long text..."
🎨 Theme System
Sử dụng Theme Provider
import { AppProvider } from '@libeyondea/base-cms';
function App() {
return (
<AppProvider>
{/* Theme tự động: light/dark dựa trên system preference */}
{/* Theme được persist vào localStorage */}
</AppProvider>
);
}
Custom Theme
import { AppProvider } from '@libeyondea/base-cms';
import { PaletteMode } from '@mui/material';
const customTheme = (mode: PaletteMode) => ({
palette: {
mode,
primary: {
main: '#00acc1', // Cyan
light: '#5ddef4',
dark: '#007c91',
contrastText: '#fff'
},
secondary: {
main: '#f50057', // Pink
light: '#ff5983',
dark: '#bb002f',
contrastText: '#fff'
},
background: {
default: mode === 'light' ? '#f5f5f5' : '#121212',
paper: mode === 'light' ? '#ffffff' : '#1e1e1e'
}
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h1: { fontSize: '2.5rem', fontWeight: 700 },
h2: { fontSize: '2rem', fontWeight: 600 },
button: { textTransform: 'none' }
},
shape: {
borderRadius: 12
},
shadows: [
'none',
'0px 2px 4px rgba(0,0,0,0.1)'
// ... more shadows
],
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
padding: '8px 16px'
}
}
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}
}
}
}
});
function App() {
return <AppProvider customTheme={customTheme}>{/* Your app */}</AppProvider>;
}
Theme Hook
import { useTheme } from '@libeyondea/base-cms';
import { Box } from '@mui/material';
function ThemedComponent() {
const { theme, mode, toggleTheme } = useTheme();
return (
<Box
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadius
}}
>
<p>Current mode: {mode}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</Box>
);
}
📚 Ví dụ chi tiết
Ví dụ 1: Form đăng ký phức tạp
import React from 'react';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, MainCard, RHFDatePicker, RHFNationalID, RHFPhone, RHFSelect, RHFSwitch, RHFTextField } from '@libeyondea/base-cms';
import { Box, Button, Grid } from '@mui/material';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
// Schema validation
const schema = yup.object({
fullName: yup.string().required('Họ tên là bắt buộc'),
email: yup.string().email('Email không hợp lệ').required('Email là bắt buộc'),
phone: yup.string().required('Số điện thoại là bắt buộc'),
nationalId: yup.string().required('CMND/CCCD là bắt buộc'),
birthDate: yup.date().required('Ngày sinh là bắt buộc').nullable(),
gender: yup.number().required('Giới tính là bắt buộc'),
address: yup.string().required('Địa chỉ là bắt buộc'),
agreeTerms: yup.boolean().oneOf([true], 'Bạn phải đồng ý với điều khoản')
});
const genderOptions = [
{ id: 1, name: 'Nam' },
{ id: 2, name: 'Nữ' },
{ id: 3, name: 'Khác' }
];
function RegisterForm() {
const methods = useForm({
resolver: yupResolver(schema),
defaultValues: {
fullName: '',
email: '',
phone: '',
nationalId: '',
birthDate: null,
gender: 1,
address: '',
agreeTerms: false
}
});
const onSubmit = async (data: any) => {
try {
console.log('Form data:', data);
// Call API to register
// await userService.create(data);
alert('Đăng ký thành công!');
} catch (error) {
console.error('Registration failed:', error);
}
};
return (
<MainCard title="Đăng ký tài khoản">
<FormProvider methods={methods} onSubmit={onSubmit}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<RHFTextField name="fullName" label="Họ và tên" fullWidth />
</Grid>
<Grid item xs={12} md={6}>
<RHFTextField name="email" label="Email" type="email" fullWidth />
</Grid>
<Grid item xs={12} md={6}>
<RHFPhone name="phone" label="Số điện thoại" fullWidth />
</Grid>
<Grid item xs={12} md={6}>
<RHFNationalID name="nationalId" label="CMND/CCCD" fullWidth />
</Grid>
<Grid item xs={12} md={6}>
<RHFDatePicker name="birthDate" label="Ngày sinh" fullWidth />
</Grid>
<Grid item xs={12} md={6}>
<RHFSelect name="gender" label="Giới tính" options={genderOptions} fullWidth />
</Grid>
<Grid item xs={12}>
<RHFTextField name="address" label="Địa chỉ" multiline rows={3} fullWidth />
</Grid>
<Grid item xs={12}>
<RHFSwitch name="agreeTerms" label="Tôi đồng ý với điều khoản sử dụng" />
</Grid>
<Grid item xs={12}>
<Box display="flex" gap={2} justifyContent="flex-end">
<Button variant="outlined" onClick={() => methods.reset()}>
Hủy
</Button>
<Button variant="contained" type="submit">
Đăng ký
</Button>
</Box>
</Grid>
</Grid>
</FormProvider>
</MainCard>
);
}
export default RegisterForm;
Ví dụ 2: Table với API Integration
import React, { useState } from 'react';
import { MainCard, MenuPopup, StanstackTable, StatusChip } from '@libeyondea/base-cms';
import { IconButton } from '@mui/material';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FiEdit, FiEye, FiTrash2 } from 'react-icons/fi';
// Service
class UserService extends BaseService {
constructor() {
super('/users');
}
}
const userService = new UserService();
function UserManagement() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
// Fetch users
const { data, isLoading, error } = useQuery({
queryKey: ['users', page, limit],
queryFn: () => userService.getAll({ page, limit })
});
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (id: string) => userService.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
alert('Xóa thành công!');
},
onError: (error) => {
console.error('Delete failed:', error);
alert('Xóa thất bại!');
}
});
// Table columns
const columns = [
{
accessorKey: 'id',
header: 'ID',
size: 80
},
{
accessorKey: 'avatar',
header: 'Avatar',
cell: ({ getValue }) => <img src={getValue()} alt="avatar" style={{ width: 40, height: 40, borderRadius: '50%' }} />
},
{
accessorKey: 'name',
header: 'Họ và tên'
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'phone',
header: 'Số điện thoại'
},
{
accessorKey: 'role',
header: 'Vai trò',
cell: ({ getValue }) => <span>{getValue() === 1 ? 'Admin' : 'User'}</span>
},
{
accessorKey: 'status',
header: 'Trạng thái',
cell: ({ getValue }) => <StatusChip status={getValue() === 1 ? 'active' : 'inactive'} label={getValue() === 1 ? 'Hoạt động' : 'Không hoạt động'} />
},
{
accessorKey: 'createdAt',
header: 'Ngày tạo',
cell: ({ getValue }) => formatDateTime(getValue())
},
{
id: 'actions',
header: 'Thao tác',
cell: ({ row }) => (
<MenuPopup
options={[
{
label: 'Xem',
icon: <FiEye />,
onClick: () => console.log('View', row.original.id)
},
{
label: 'Sửa',
icon: <FiEdit />,
onClick: () => console.log('Edit', row.original.id)
},
{
label: 'Xóa',
icon: <FiTrash2 />,
onClick: () => {
if (confirm('Bạn có chắc muốn xóa?')) {
deleteMutation.mutate(row.original.id);
}
},
color: 'error'
}
]}
/>
)
}
];
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<MainCard title="Quản lý người dùng">
<StanstackTable
data={data?.data || []}
columns={columns}
enablePagination
enableSorting
enableFiltering
enableRowSelection
pageSize={limit}
isLoading={isLoading}
onPageChange={(newPage) => setPage(newPage)}
onPageSizeChange={(newLimit) => setLimit(newLimit)}
onRowClick={(row) => console.log('Row clicked:', row)}
/>
</MainCard>
);
}
export default UserManagement;
Ví dụ 3: Complete CMS Layout
import React from 'react';
import { AppProvider, AuthLayout, LoadingScreen, PrivateLayout, PublicLayout } from '@libeyondea/base-cms';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Provider } from 'react-redux';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import NotFound from './pages/NotFound';
// Pages
import Login from './pages/auth/Login';
import Dashboard from './pages/private/Dashboard';
import Settings from './pages/private/Settings';
import Users from './pages/private/Users';
import Home from './pages/public/Home';
import { store } from './store';
// Query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false
}
}
});
// Auth guard
function PrivateRoute({ children }: { children: React.ReactNode }) {
const token = localStorage.getItem('token');
return token ? <>{children}</> : <Navigate to="/auth/login" replace />;
}
function App() {
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppProvider>
<Routes>
{/* Public routes */}
<Route path="/" element={<PublicLayout />}>
<Route index element={<Home />} />
</Route>
{/* Auth routes */}
<Route path="/auth" element={<AuthLayout />}>
<Route path="login" element={<Login />} />
</Route>
{/* Private routes */}
<Route
path="/dashboard"
element={
<PrivateRoute>
<PrivateLayout />
</PrivateRoute>
}
>
<Route index element={<Dashboard />} />
<Route path="users" element={<Users />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</AppProvider>
</BrowserRouter>
</QueryClientProvider>
</Provider>
);
}
export default App;
📘 API Reference
AppProvider Props
interface AppProviderProps {
children: ReactNode;
customTheme?: (mode: PaletteMode) => any;
}
FormProvider Props
interface FormProviderProps {
children: ReactNode;
methods: UseFormReturn<any>;
onSubmit: (data: any) => void | Promise<void>;
}
StanstackTable Props
interface StanstackTableProps {
data: any[];
columns: ColumnDef<any>[];
enablePagination?: boolean;
enableSorting?: boolean;
enableFiltering?: boolean;
enableRowSelection?: boolean;
enableColumnVisibility?: boolean;
pageSize?: number;
isLoading?: boolean;
onRowClick?: (row: any) => void;
onPageChange?: (page: number) => void;
onPageSizeChange?: (size: number) => void;
onSelectionChange?: (selectedRows: any[]) => void;
}
BaseService Constructor
constructor(apiName: string, options?: ServiceOptions)
interface ServiceOptions {
isOtherUrl?: boolean;
isFormData?: boolean;
timeout?: number;
}
💡 TypeScript Support
Library được viết 100% bằng TypeScript và cung cấp đầy đủ type definitions:
import type { ApiResponse, FilterObject, IUniqueId, ServiceOptions } from '@libeyondea/base-cms';
// Type-safe service
class ProductService extends BaseService {
constructor() {
super('/products');
}
async getProducts(filters: FilterObject): Promise<ApiResponse<Product[]>> {
const response = await this.getAll<Product[]>(filters);
return response.data;
}
}
// Type-safe form
interface UserFormData {
name: string;
email: string;
role: number;
}
const methods = useForm<UserFormData>({
defaultValues: {
name: '',
email: '',
role: 1
}
});
🎓 Best Practices
1. Form Validation
Luôn sử dụng Yup schema cho validation phức tạp:
const schema = yup.object({
email: yup.string().email('Email không hợp lệ').required('Email là bắt buộc'),
password: yup
.string()
.min(8, 'Mật khẩu tối thiểu 8 ký tự')
.matches(/[A-Z]/, 'Phải có ít nhất 1 chữ hoa')
.matches(/[0-9]/, 'Phải có ít nhất 1 số')
.required('Mật khẩu là bắt buộc')
});
2. API Service Organization
Tổ chức services theo domain:
services/
├── core/
│ └── baseService.ts
├── auth/
│ ├── authService.ts
│ └── tokenService.ts
├── user/
│ └── userService.ts
└── product/
└── productService.ts
3. Component Composition
Tái sử dụng components thông qua composition:
// Bad
function UserForm() {
return (
<div>
<input name="name" />
<input name="email" />
<button>Submit</button>
</div>
);
}
// Good
function UserForm() {
return (
<FormProvider methods={methods} onSubmit={handleSubmit}>
<RHFTextField name="name" label="Name" />
<RHFTextField name="email" label="Email" />
<Button type="submit">Submit</Button>
</FormProvider>
);
}
4. Error Handling
Luôn handle errors properly:
async function loadData() {
try {
const response = await userService.getAll();
setData(response.data);
} catch (error) {
console.error('Failed to load data:', error);
showError('Không thể tải dữ liệu');
}
}
5. Performance
Sử dụng React Query cho data fetching:
const { data, isLoading, error } = useQuery({
queryKey: ['users', filters],
queryFn: () => userService.getAll(filters),
staleTime: 5 * 60 * 1000 // 5 minutes
});
🐛 Troubleshooting
Lỗi: "useTheme must be used within an AppProvider"
Nguyên nhân: Component sử dụng useTheme
không được wrap trong AppProvider
.
Giải pháp:
// ❌ Bad
function App() {
return <MyComponent />;
}
// ✅ Good
function App() {
return (
<AppProvider>
<MyComponent />
</AppProvider>
);
}
Lỗi: Form validation không hoạt động
Nguyên nhân: Chưa wrap RHF components trong FormProvider
.
Giải pháp:
// ❌ Bad
<RHFTextField name="email" label="Email" />
// ✅ Good
<FormProvider methods={methods} onSubmit={handleSubmit}>
<RHFTextField name="email" label="Email" />
</FormProvider>
Lỗi: Missing peer dependencies
Nguyên nhân: Chưa cài đặt đầy đủ peer dependencies.
Giải pháp: Cài đặt tất cả peer dependencies theo hướng dẫn ở phần Cài đặt.
Lỗi: Xung đột phiên bản dependencies
Nguyên nhân: Phiên bản dependencies không tương thích với yêu cầu của library.
Giải pháp: Sử dụng đúng phiên bản dependencies như đã liệt kê trong peer dependencies.
🔄 Migration Guide
Từ v1.0.x lên v1.0.21
⚠️ BREAKING CHANGES: Từ v1.0.21, tất cả dependencies đã được chuyển thành peer dependencies.
Migration steps:
- Update library:
npm update @libeyondea/base-cms
- Cài đặt peer dependencies:
npm install react@19.2.0 react-dom@19.2.0 react-router-dom@7.9.3 @reduxjs/toolkit@2.9.0 react-redux@9.2.0 @emotion/react@11.14.0 @emotion/styled@11.14.1 @mui/icons-material@7.3.4 @mui/material@7.3.4 @mui/system@7.3.3 @mui/x-date-pickers@8.12.0 @tanstack/react-query@5.90.2 @tanstack/react-table@8.21.3 @hookform/resolvers@5.2.2 axios@1.12.2 dayjs@1.11.18 js-cookie@3.0.5 lodash-es@4.17.21 qs@6.14.0 react-big-calendar@1.19.4 react-hook-form@7.63.0 react-icons@5.5.0 react-number-format@5.4.4 react-toastify@11.0.5 sweetalert2@11.23.0 yup@1.7.1
- Xóa dependencies cũ (nếu có):
# Xóa các dependencies đã được chuyển thành peer dependencies
npm uninstall @emotion/react @emotion/styled @mui/material @mui/icons-material @mui/system @mui/x-date-pickers @tanstack/react-query @tanstack/react-table react-redux @hookform/resolvers axios dayjs js-cookie lodash-es qs react-big-calendar react-hook-form react-icons react-number-format react-toastify sweetalert2 yup
📄 License
MIT License - xem file LICENSE để biết thêm chi tiết.
Copyright (c) 2025 Nguyen Thuc
👨💻 Tác giả
Nguyen Thuc
- GitHub: @libeyondea
- Twitter: @libeyondea
🔗 Liên kết
🙏 Acknowledgements
Cảm ơn các thư viện open-source tuyệt vời:
- Material-UI - UI Framework
- React Hook Form - Form Management
- TanStack Table - Table Component
- TanStack Query - Data Fetching
- Redux Toolkit - State Management