JSPM

  • Created
  • Published
  • Downloads 1024
  • Score
    100M100P100Q103952F
  • License MIT

Base CMS library for React

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.

npm version License: MIT TypeScript

📋 Mục lục

🎯 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 dependenciesBẮ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 trong FormProvider để 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/dark
  • setThemeMode(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 drawer
  • toggleDrawer() - Toggle drawer open/close
  • setDrawerOpen(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 type
  • setFilterTable({ 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 audio
  • pause() - Pause audio
  • stop() - Stop và reset audio
  • isPlaying - 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 dialog
  • showSuccess(message) - Show success message
  • showError(message) - Show error message
  • showWarning(message) - Show warning message
  • showInfo(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"
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:

  1. Update library:
npm update @libeyondea/base-cms
  1. 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
  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

🔗 Liên kết

🙏 Acknowledgements

Cảm ơn các thư viện open-source tuyệt vời:


Built with ❤️ by Nguyen Thuc