| name | component-development |
| description | Create and modify React components following project patterns. Use when building UI components, forms, layouts, navigation, or implementing React hooks. Includes DaisyUI, Tailwind CSS, and lucide-react icons. Use when this capability is needed. |
| metadata | {"author":"doitsu2014"} |
Component Development
Overview
This skill helps you create and modify React components following the project's established patterns and conventions. The project uses React 19, TypeScript, DaisyUI, Tailwind CSS, and lucide-react icons.
Project Structure
Component Organization
src/
├── app/
│ └── admin/
│ ├── components/ # Reusable admin components
│ │ ├── inputs/ # Form input components
│ │ ├── skeleton/ # Loading skeletons
│ │ ├── left-menu.tsx # Navigation menu
│ │ ├── top-bar.tsx # Top navigation bar
│ │ ├── menu-item.tsx # Menu item component
│ │ └── my-breadcrumbs.tsx # Breadcrumb navigation
│ ├── blogs/ # Blog-related pages
│ ├── categories/ # Category-related pages
│ ├── layout.tsx # Admin layout wrapper
│ ├── layoutContext.tsx # Layout context
│ └── layoutMain.tsx # Main layout component
├── auth/ # Auth components
│ ├── AuthContext.tsx # Auth context provider
│ └── ProtectedRoute.tsx # Route protection
└── domains/ # Domain models/types
Technology Stack
Core
- React: 19.1.x (latest)
- TypeScript: 5.9.x
- React Router: 7.9.x for routing
UI Framework
- DaisyUI: 5.0.x - Component library
- Tailwind CSS: 4.0.x - Utility-first CSS
- lucide-react: 0.476.x - Icon library
Form Components
- Quill: 2.0.x - Rich text editor
- Custom form components in
src/app/admin/components/inputs/
Component Patterns
Functional Components with TypeScript
import { useState } from 'react';
import type { ComponentProps } from 'react';
interface MyComponentProps {
title: string;
onSubmit: (data: FormData) => void;
optional?: string;
}
export default function MyComponent({
title,
onSubmit,
optional
}: MyComponentProps) {
const [state, setState] = useState('');
return (
<div className="container">
<h1>{title}</h1>
{/* Component JSX */}
</div>
);
}
Named Exports for Utilities
export function helperFunction() { }
export const CONSTANT_VALUE = 'value';
export default function MainComponent() { }
DaisyUI Components
Common DaisyUI Classes
Buttons
<button className="btn btn-primary">Primary</button>
<button className="btn btn-secondary">Secondary</button>
<button className="btn btn-accent">Accent</button>
<button className="btn btn-ghost">Ghost</button>
<button className="btn btn-link">Link</button>
Forms
<label className="form-control w-full">
<div className="label">
<span className="label-text">Label</span>
</div>
<input
type="text"
className="input input-bordered w-full"
placeholder="Type here"
/>
</label>
<select className="select select-bordered w-full">
<option disabled selected>Pick one</option>
<option>Option 1</option>
</select>
<textarea
className="textarea textarea-bordered w-full"
placeholder="Enter text"
></textarea>
Cards
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">Card Title</h2>
<p>Card content goes here</p>
<div className="card-actions justify-end">
<button className="btn btn-primary">Action</button>
</div>
</div>
</div>
Navigation
<div className="navbar bg-base-100">
<div className="navbar-start">
<a className="btn btn-ghost text-xl">Brand</a>
</div>
<div className="navbar-center">
{/* Menu items */}
</div>
<div className="navbar-end">
<button className="btn">Button</button>
</div>
</div>
Tables
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Job</th>
<th>Company</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cy Ganderton</td>
<td>Quality Control Specialist</td>
<td>Littel, Schaden and Vandervort</td>
</tr>
</tbody>
</table>
</div>
Loading States
<span className="loading loading-spinner loading-xs"></span>
<span className="loading loading-spinner loading-sm"></span>
<span className="loading loading-spinner loading-md"></span>
<span className="loading loading-spinner loading-lg"></span>
Tailwind CSS Utilities
Layout
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
// Grid
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
// Spacing
<div className="p-4 m-4"> // padding, margin
<div className="px-4 py-2"> // horizontal, vertical padding
<div className="space-y-4"> // gap between children
Responsive Design
<div className="w-full md:w-1/2 lg:w-1/3">
<div className="text-sm md:text-base lg:text-lg">
<div className="hidden md:block"> // Hide on mobile
Icons with lucide-react
Available Icons
import {
Info, ImagePlus, Tag, BookOpen, Save, FileText,
Home, Settings, Users, LogOut, Menu, X,
Edit, Trash, Plus, Check, AlertCircle
} from 'lucide-react';
<Save className="w-4 h-4" />
<Edit className="w-5 h-5 text-blue-500" />
<Trash className="w-6 h-6 hover:text-red-500" />
Icon Sizes
w-4 h-4 - Small (16px)
w-5 h-5 - Medium (20px)
w-6 h-6 - Large (24px)
w-8 h-8 - Extra Large (32px)
React Hooks
useState
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
useEffect
useEffect(() => {
return () => {
};
}, [dependencies]);
Custom Hooks
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
useNavigate (React Router)
import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const handleClick = () => {
navigate('/admin/blogs');
};
}
Form Handling
Controlled Inputs
const [value, setValue] = useState('');
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
className="input input-bordered"
/>
Form Submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
} catch (error) {
console.error('Submit error:', error);
}
};
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
Loading States
Conditional Rendering
if (loading) {
return <LoadingSkeleton />;
}
if (error) {
return <ErrorMessage error={error} />;
}
return <Content data={data} />;
Skeleton Components
Use skeletons from src/app/admin/components/skeleton/:
import TableSkeleton from '../components/skeleton/table-skeleton';
{loading ? <TableSkeleton /> : <DataTable data={data} />}
Context Providers
Creating Context
import { createContext, useContext, useState } from 'react';
interface MyContextType {
value: string;
setValue: (value: string) => void;
}
const MyContext = createContext<MyContextType | undefined>(undefined);
export function MyProvider({ children }: { children: React.ReactNode }) {
const [value, setValue] = useState('');
return (
<MyContext.Provider value={{ value, setValue }}>
{children}
</MyContext.Provider>
);
}
export function useMyContext() {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within MyProvider');
}
return context;
}
Routing
Route Configuration
import { BrowserRouter, Routes, Route } from 'react-router-dom';
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<AdminLayout />}>
<Route path="blogs" element={<BlogList />} />
<Route path="blogs/create" element={<BlogCreate />} />
<Route path="blogs/edit/:id" element={<BlogEdit />} />
</Route>
</Routes>
</BrowserRouter>
Protected Routes
<ProtectedRoute>
<AdminDashboard />
</ProtectedRoute>
TypeScript Best Practices
Props Types
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
type ButtonProps = {
label: string;
onClick: () => void;
}
Event Types
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { };
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { };
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { };
Children Props
interface LayoutProps {
children: React.ReactNode;
}
Common Patterns
List Rendering
{items.map((item) => (
<div key={item.id}>
{item.name}
</div>
))}
Conditional Classes
<button
className={`btn ${isActive ? 'btn-primary' : 'btn-ghost'}`}
>
Button
</button>
className={`btn ${loading ? 'loading' : ''}`}
Error Boundaries
try {
} catch (error) {
console.error('Error:', error);
}
Best Practices
- Use TypeScript types for all props and state
- Destructure props in function parameters
- Use meaningful variable names
- Keep components small and focused
- Extract reusable logic into custom hooks
- Use DaisyUI classes instead of custom CSS
- Follow Tailwind conventions for styling
- Add loading states for async operations
- Handle errors gracefully
- Use path aliases (
@/) for imports
Component Checklist
When creating a new component:
Example Component
import { useState, useEffect } from 'react';
import { Save, AlertCircle } from 'lucide-react';
import { useAuth } from '@/auth/AuthContext';
import { getApiUrl, authenticatedFetch } from '@/config/api.config';
interface BlogFormProps {
id?: string;
onSuccess?: () => void;
}
export default function BlogForm({ id, onSuccess }: BlogFormProps) {
const { token } = useAuth();
const [title, setTitle] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (id) {
fetchBlog();
}
}, [id]);
const fetchBlog = async () => {
setLoading(true);
try {
const response = (
(),
token
);
(response.) {
{ data } = response.();
(data.);
}
} (err) {
();
} {
();
}
};
= () => {
e.();
();
();
{
response = (
(),
token,
{
: ,
: .({ title }),
}
);
(response.) {
onSuccess?.();
} {
();
}
} (err) {
();
} {
();
}
};
(loading && id) {
(
);
}
(
);
}
This example demonstrates:
- TypeScript props interface
- State management with useState
- Data fetching with useEffect
- Form handling
- Loading states
- Error handling
- DaisyUI components
- Tailwind CSS classes
- lucide-react icons
- Authentication integration
Form Handling with react-hook-form + zod
The project uses react-hook-form for form state management and zod for schema validation.
Setup
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
title: z.string().min(1, 'Title is required').max(200),
email: z.string().email('Invalid email'),
published: z.boolean().default(false),
tags: z.array(z.string()).default([]),
});
type FormData = z.infer<typeof formSchema>;
Basic Form Component
export default function MyForm() {
const {
register,
handleSubmit,
control,
reset,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
email: '',
published: false,
tags: [],
},
});
const onSubmit = async (data: FormData) => {
try {
toast.success('Saved successfully');
} catch (error) {
toast.error('Failed to save');
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Simple input with register */}
<input
{...register('title')}
className={`input input-bordered ${errors.title ? 'input-error' : ''}`}
/>
{errors.title && (
<span className="text-error">{errors.title.message}
)}
{/* Complex input with Controller */}
(
)}
/>
{isSubmitting ? 'Saving...' : 'Save'}
);
}
useFieldArray for Dynamic Lists
import { useFieldArray } from 'react-hook-form';
const { fields, append, remove } = useFieldArray({
control,
name: 'translations',
});
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`translations.${index}.value`)} />
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => append({ value: '' })}>Add</button>
Zod Schema Patterns
title: z.string().min(1, 'Required').max(200, 'Too long')
categoryType: z.nativeEnum(CategoryTypeEnum)
published: z.boolean().default(false)
tags: z.array(z.string()).default([])
translation: z.object({
id: z.string().optional(),
languageCode: z.string().min(2),
displayName: z.string().min(1),
})
Toast Notifications with Sonner
The project uses sonner for toast notifications.
Basic Usage
import { toast } from 'sonner';
toast.success('Operation completed successfully');
toast.error('Something went wrong');
toast.info('Here is some information');
toast.warning('Please check your input');
With Options
toast.success('Saved!', {
duration: 5000,
description: 'Your changes have been saved',
});
toast.error('Failed to save', {
duration: Infinity,
action: {
label: 'Retry',
onClick: () => handleRetry(),
},
});
Promise Toast
toast.promise(saveData(), {
loading: 'Saving...',
success: 'Data saved successfully',
error: 'Failed to save data',
});
Custom Toast
toast.custom((t) => (
<div className="bg-base-100 p-4 rounded-lg shadow-lg">
<h3>Custom Toast</h3>
<button onClick={() => toast.dismiss(t)}>Close</button>
</div>
));
DaisyUI v5 CSS Variables
DaisyUI v5 uses new CSS variable naming. Use these when writing custom CSS:
Color Variables
oklch(var(--p))
oklch(var(--b1))
oklch(var(--bc))
var(--color-primary)
var(--color-base-100)
var(--color-base-content)
Available Color Variables
--color-base-100
--color-base-200
--color-base-300
--color-base-content
--color-primary
--color-primary-content
--color-secondary
--color-secondary-content
--color-accent
--color-accent-content
--color-neutral
--color-neutral-content
--color-info
--color-success
--color-warning
--color-error
Using with Opacity (color-mix)
background-color: color-mix(in oklch, var(--color-primary) 20%, transparent);
color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
Theme Configuration
@plugin 'daisyui' {
themes: emerald --default, dark;
}
html: {
htmlAttrs: {
'data-theme': 'emerald',
},
},
Converted and distributed by TomeVault — claim your Tome and manage your conversions.