A modern, themeable admin dashboard built with React 19.2, TypeScript 6.0, Vite 8.0, Tailwind CSS 4.2, TanStack Form 1.28.6, and shadcn/ui components generated via shadcn 4.1.2 (base-nova style), with Lucide React and lucide-animated icons.
- Admin Panel
Node.js: Vite 8 requires Node.js
20.19+or22.12+. Package manager: The repository ships withbun.lock, and bothbun installand plainnpm installwork with the current dependency graph.
# Install dependencies (recommended)
bun install
# Alternative with npm
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run previewIf you prefer to stay fully on Bun, the equivalent commands are:
# Start development server
bun run dev
# Build for production
bun run build
# Preview production build
bun run previewCreate a .env file in the project root:
VITE_API_URL=http://localhost:3000/apiThe admin panel ships with a fully dynamic theme system that allows users to switch color themes, appearance mode, and border radius at runtime — all changes are persisted to localStorage and survive page reloads.
src/
├── store/theme-store.ts # Zustand store — state, color definitions, CSS applicator
├── components/theme/
│ ├── theme-customizer.tsx # Side sheet UI for color & radius selection
│ └── mode-toggle.tsx # Dropdown for light / dark / system toggle
├── providers/providers.tsx # ThemeInitializer wrapper (applies theme on mount)
└── index.css # CSS custom properties (default values)
| Layer | File | Responsibility |
|---|---|---|
| State | theme-store.ts |
Stores mode, colorTheme, radius via Zustand with persist middleware |
| UI | theme-customizer.tsx |
Visual picker rendered as a slide-out sheet |
| UI | mode-toggle.tsx |
Light / Dark / System dropdown |
| Init | providers.tsx |
Calls initializeTheme() on app mount, listens for state changes |
| CSS | index.css |
Declares CSS custom properties consumed by Tailwind |
10 built-in color themes are available out of the box:
| Theme | Primary (Light) | Preview |
|---|---|---|
| Sky (default) | oklch(0.59 0.14 242) |
🔵 |
| Blue | oklch(0.55 0.20 255) |
🔷 |
| Green | oklch(0.55 0.16 155) |
🟢 |
| Rose | oklch(0.58 0.20 12) |
🌹 |
| Orange | oklch(0.65 0.18 55) |
🟠 |
| Purple | oklch(0.55 0.20 290) |
🟣 |
| Emerald | oklch(0.60 0.15 165) |
💚 |
| Violet | oklch(0.54 0.22 275) |
💜 |
| Amber | oklch(0.70 0.16 75) |
🟡 |
| Red | oklch(0.55 0.22 25) |
🔴 |
Each theme defines both light and dark variants with the following CSS custom properties:
--primary/--primary-foreground--chart-1through--chart-5--sidebar-primary/--sidebar-primary-foreground
All colors use the OKLCH color space for perceptually uniform brightness across themes.
The mode toggle supports three states:
| Mode | Behavior |
|---|---|
light |
Forces light appearance |
dark |
Forces dark appearance (adds .dark class to <html>) |
system |
Follows prefers-color-scheme media query |
The resolved mode is used to pick either the light or dark variant from the active color theme.
A continuous slider controls the global --radius CSS variable:
- Range:
0rem(square) →1rem(fully rounded) - Default:
0.625rem - Step:
0.05rem
All shadcn/ui components derive their border radius from this single variable via Tailwind's @theme inline block:
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* ... */-
On app mount,
ThemeInitializerinproviders.tsxcallsinitializeTheme()which reads persisted state fromlocalStoragekey"theme-storage"and applies CSS variables todocument.documentElement. -
When user selects a theme, Zustand's
setColorTheme()updates state and immediately callsapplyTheme():// theme-store.ts setColorTheme: (colorTheme) => { set({ colorTheme }); applyTheme(colorTheme, get().mode, get().radius); };
-
applyTheme()does three things:- Toggles the
.darkclass on<html> - Sets all color CSS variables via
style.setProperty() - Sets
--radius
- Toggles the
-
Tailwind picks up the changes because all design tokens are mapped through
@theme inlineinindex.css:@theme inline { --color-primary: var(--primary); --color-chart-1: var(--chart-1); /* ... */ }
-
Persistence is handled by Zustand's
persistmiddleware — the entire theme state is serialized tolocalStorageunder"theme-storage".
To add a new color theme (e.g., Teal):
Step 1 — Add the type:
// src/store/theme-store.ts
export type ColorTheme =
| "sky"
| "blue"
// ...existing themes...
| "teal"; // ← add hereStep 2 — Define colors:
// src/store/theme-store.ts
export const colorThemes: Record<
ColorTheme,
{ light: ThemeColors; dark: ThemeColors; label: string }
> = {
// ...existing themes...
teal: {
label: "Teal",
light: {
primary: "oklch(0.60 0.13 185)",
primaryForeground: "oklch(0.98 0.01 185)",
chart1: "oklch(0.83 0.08 178)",
chart2: "oklch(0.75 0.11 180)",
chart3: "oklch(0.67 0.13 183)",
chart4: "oklch(0.60 0.13 185)",
chart5: "oklch(0.50 0.11 188)",
sidebarPrimary: "oklch(0.60 0.13 185)",
sidebarPrimaryForeground: "oklch(0.98 0.01 185)",
},
dark: {
primary: "oklch(0.70 0.13 185)",
primaryForeground: "oklch(0.25 0.05 188)",
chart1: "oklch(0.83 0.08 178)",
chart2: "oklch(0.75 0.11 180)",
chart3: "oklch(0.67 0.13 183)",
chart4: "oklch(0.60 0.13 185)",
chart5: "oklch(0.50 0.11 188)",
sidebarPrimary: "oklch(0.75 0.11 180)",
sidebarPrimaryForeground: "oklch(0.25 0.05 188)",
},
},
};Step 3 — Add a Tailwind preview color:
// src/components/theme/theme-customizer.tsx
const colorPreviews: Record<ColorTheme, string> = {
// ...existing previews...
teal: "bg-teal-500",
};Step 4 — Add translation keys:
// src/i18n/locales/en/common.json
{
"themeCustomizer": {
"colors": {
"teal": "Teal"
}
}
}// src/i18n/locales/tr/common.json
{
"themeCustomizer": {
"colors": {
"teal": "Turkuaz"
}
}
}That's it — the new theme will automatically appear in the customizer grid.
All theme colors are available as standard Tailwind utility classes:
// Primary color
<div className="bg-primary text-primary-foreground" />
// Chart colors (useful for recharts)
<div className="fill-chart-1 stroke-chart-2" />
// Sidebar colors
<div className="bg-sidebar-primary text-sidebar-primary-foreground" />
// Border radius
<div className="rounded-lg" /> {/* Uses --radius */}
<div className="rounded-sm" /> {/* Uses --radius - 4px */}
<div className="rounded-xl" /> {/* Uses --radius + 4px */}For dynamic access in JS/TS:
// Read current theme from store (outside React)
import { useThemeStore } from "@/store/theme-store";
const { colorTheme, mode, radius } = useThemeStore.getState();
// Read from CSS variable
const primary = getComputedStyle(document.documentElement).getPropertyValue(
"--primary",
);src/
├── components/
│ ├── data-table/ # Reusable data table with sorting, filtering, pagination
│ ├── layout/ # Admin layout, sidebar, header, navigation
│ ├── theme/ # Theme customizer & mode toggle
│ └── ui/ # shadcn/ui components (base-nova style)
├── hooks/ # Custom React hooks
├── lib/ # Utility functions
├── pages/ # Route pages (dashboard, users, products, orders, etc.)
├── providers/ # React context providers (QueryClient, Theme, Tooltip)
├── service/
│ ├── config/config.ts # Axios instance with interceptors
│ ├── url/url.ts # API endpoint URL constants
│ └── request/ # Domain-specific request functions
├── store/ # Zustand stores (auth, theme)
└── i18n/ # Internationalization (EN/TR)
The API layer follows a 3-tier architecture:
| Layer | Path | Purpose |
|---|---|---|
| Config | service/config/config.ts |
Axios instance, auth interceptor, error handling |
| URL | service/url/url.ts |
Centralized endpoint constants |
| Request | service/request/*.ts |
Domain-grouped API calls |
// Usage example
import { usersRequest } from "@/service/request/users-request";
// GET all users
const { data } = await usersRequest.getAll({ page: 1, limit: 10 });
// GET single user
const { data } = await usersRequest.getById("user-123");
// POST create user
await usersRequest.create({
name: "John",
email: "john@example.com",
password: "...",
});The interceptor automatically:
- Attaches
Authorization: Bearer <token>from the auth store - Sets
Accept-Languageheader from the current i18n locale - Redirects to
/loginand clears auth state on401responses (except auth endpoints)
The project now uses TanStack Form for client-side form state while keeping Zod schemas as the single validation source.
TanStack Form is now used across the app in the login page, settings page, and CRUD/action dialogs under users, products, and orders.
src/
├── pages/login.tsx # Auth form
├── pages/settings.tsx # Profile, company, and API settings forms
├── pages/users/dialogs/ # Create / edit user forms
├── pages/products/dialogs/ # Create / edit product forms
├── pages/orders/dialogs/ # Track shipment + refund forms
├── service/request/schemas.ts # Zod schemas reused by form validators
├── components/ui/field.tsx # Label / inline error presentation
└── i18n/locales/*/common.json # Localized validation messages
This setup gives you:
- Typed form state with
@tanstack/react-form - Inline field errors rendered through the existing shadcn/ui-style field helpers
- Localized validation feedback using
common.validation.*translation keys - Reactive submit state via
form.Subscribefor loading/disable behavior
const form = useForm({
defaultValues: {
email: "admin@example.com",
password: "password",
},
onSubmit: async ({ value }) => {
// submit logic
},
onSubmitInvalid: ({ formApi }) => {
const firstError = Object.values(formApi.state.fieldMeta)
.flatMap((meta) => meta?.errors ?? [])
.find(Boolean);
toast.error(t(firstError ?? "validation.required", { ns: "common" }));
},
});Each field reuses the existing Zod schema fragments (loginPayloadSchema.shape.email, loginPayloadSchema.shape.password) so validation rules stay centralized instead of drifting across components.
The project uses react-i18next with namespace-based JSON files:
src/i18n/locales/
├── en/
│ ├── common.json # Shared strings (theme, months, days)
│ ├── dashboard.json # Dashboard-specific
│ ├── products.json # Products page
│ ├── users.json # Users page
│ └── orders.json # Orders page
└── tr/
└── ... # Same structure
Supported languages: English (en) and Turkish (tr).
Versions below mirror the semver entries currently declared in package.json.
| Technology | Version | Purpose |
|---|---|---|
| React | ^19.2.4 |
UI library |
| TypeScript | ~6.0.2 |
Type safety |
| Vite | ^8.0.3 |
Build tool & dev server |
| Tailwind CSS | ^4.2.2 |
Utility-first CSS |
| TanStack Form | ^1.28.6 |
Form state & validation |
| TanStack Query | ^5.96.2 |
Server state & caching |
| TanStack Table | ^8.21.3 |
Data tables |
| React Router | ^7.14.0 |
Client-side routing |
| Axios | ^1.15.0 |
HTTP client |
| i18next | ^26.0.3 |
Core internationalization engine |
| react-i18next | ^17.0.2 |
React i18n bindings |
| Recharts | ^3.8.1 |
Charts & data visualization |
| Motion | ^12.38.0 |
Animations |
| Lucide React | ^1.7.0 |
Static icon library |
| lucide-animated | ^1.0.0 |
Animated icon library |
| Zod | ^4.3.6 |
Runtime validation |
| Zustand | ^5.0.12 |
State management |
| shadcn CLI / ui | ^4.1.2 |
Component scaffolding |
MIT