component-development
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.
$ 安裝
git clone https://github.com/doitsu2014/my-blogs-rsbuild /tmp/my-blogs-rsbuild && cp -r /tmp/my-blogs-rsbuild/admin_side/.claude/skills/component-development ~/.claude/skills/my-blogs-rsbuild// tip: Run this command in your terminal to install the skill
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.
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
// For utility functions and constants
export function helperFunction() { }
export const CONSTANT_VALUE = 'value';
// Default export for main component
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
// Flexbox
<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
// Mobile-first approach
<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';
// Usage
<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(() => {
// Effect logic
return () => {
// Cleanup
};
}, [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 {
// Submit logic
} 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 for props
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
// Or use type
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>
// Or use template literals
className={`btn ${loading ? 'loading' : ''}`}
Error Boundaries
try {
// Risky operation
} catch (error) {
console.error('Error:', error);
// Show error to user
}
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:
- Define TypeScript interface for props
- Use functional component syntax
- Add proper imports (React, icons, utilities)
- Implement loading states if fetching data
- Add error handling
- Use DaisyUI components where possible
- Apply responsive Tailwind classes
- Export component as default
- Add JSDoc comments for complex components
- Test component in browser
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 = await authenticatedFetch(
getApiUrl(`/posts/${id}`),
token
);
if (response.ok) {
const { data } = await response.json();
setTitle(data.title);
}
} catch (err) {
setError('Failed to load blog');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await authenticatedFetch(
getApiUrl('/posts'),
token,
{
method: 'POST',
body: JSON.stringify({ title }),
}
);
if (response.ok) {
onSuccess?.();
} else {
setError('Failed to save blog');
}
} catch (err) {
setError('Network error');
} finally {
setLoading(false);
}
};
if (loading && id) {
return (
<div className="flex justify-center p-8">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="alert alert-error">
<AlertCircle className="w-5 h-5" />
<span>{error}</span>
</div>
)}
<label className="form-control w-full">
<div className="label">
<span className="label-text">Blog Title</span>
</div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="input input-bordered w-full"
placeholder="Enter title"
required
/>
</label>
<button
type="submit"
className={`btn btn-primary ${loading ? 'loading' : ''}`}
disabled={loading}
>
{!loading && <Save className="w-4 h-4" />}
{loading ? 'Saving...' : 'Save Blog'}
</button>
</form>
);
}
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';
// Define schema
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 {
// Submit logic
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}</span>
)}
{/* Complex input with Controller */}
<Controller
name="published"
control={control}
render={({ field }) => (
<input
type="checkbox"
checked={field.value}
onChange={field.onChange}
/>
)}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</form>
);
}
useFieldArray for Dynamic Lists
import { useFieldArray } from 'react-hook-form';
const { fields, append, remove } = useFieldArray({
control,
name: 'translations',
});
// Render dynamic fields
{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
// Location: src/schemas/
// Basic string validation
title: z.string().min(1, 'Required').max(200, 'Too long')
// Enum validation
categoryType: z.nativeEnum(CategoryTypeEnum)
// Optional with default
published: z.boolean().default(false)
// Array validation
tags: z.array(z.string()).default([])
// Nested object
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';
// Success
toast.success('Operation completed successfully');
// Error
toast.error('Something went wrong');
// Info
toast.info('Here is some information');
// Warning
toast.warning('Please check your input');
With Options
toast.success('Saved!', {
duration: 5000, // 5 seconds
description: 'Your changes have been saved',
});
toast.error('Failed to save', {
duration: Infinity, // Won't auto-dismiss
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
/* Old DaisyUI v4 */
oklch(var(--p)) /* Primary */
oklch(var(--b1)) /* Base 100 */
oklch(var(--bc)) /* Base content */
/* New DaisyUI v5 */
var(--color-primary) /* Primary */
var(--color-base-100) /* Base 100 */
var(--color-base-content) /* Base content */
Available Color Variables
/* Base colors */
--color-base-100
--color-base-200
--color-base-300
--color-base-content
/* Semantic colors */
--color-primary
--color-primary-content
--color-secondary
--color-secondary-content
--color-accent
--color-accent-content
--color-neutral
--color-neutral-content
/* State colors */
--color-info
--color-success
--color-warning
--color-error
Using with Opacity (color-mix)
/* 20% opacity primary color */
background-color: color-mix(in oklch, var(--color-primary) 20%, transparent);
/* 50% opacity base content */
color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
Theme Configuration
/* In App.css */
@plugin 'daisyui' {
themes: emerald --default, dark;
}
/* In rsbuild.config.ts */
html: {
htmlAttrs: {
'data-theme': 'emerald',
},
},
Repository
