Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:56:35 +08:00
commit 0b8dfef442
16 changed files with 5044 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
/**
* React Component Template
*
* This template provides a starting point for creating well-structured,
* type-safe React components following best practices.
*
* Usage:
* 1. Copy this template to your components directory
* 2. Rename file and component
* 3. Define props interface
* 4. Implement component logic
* 5. Add tests and Storybook stories
*/
import React from 'react';
import { cn } from '@/lib/utils'; // Utility for className merging (clsx + tailwind-merge)
// ============================================================================
// TYPES
// ============================================================================
/**
* Props for the ComponentName component
*
* @property prop1 - Description of prop1
* @property prop2 - Description of prop2 (optional)
* @property className - Additional CSS classes (optional)
* @property children - React children (optional)
*/
export interface ComponentNameProps {
/** Required prop with specific type */
prop1: string;
/** Optional prop with default value */
prop2?: number;
/** Callback function */
onAction?: (value: string) => void;
/** Additional CSS classes for customization */
className?: string;
/** React children */
children?: React.ReactNode;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const DEFAULT_PROP2 = 42;
// ============================================================================
// COMPONENT
// ============================================================================
/**
* ComponentName - Brief description of what this component does
*
* More detailed description of the component's purpose, use cases,
* and any important implementation notes.
*
* @example
* ```tsx
* <ComponentName
* prop1="value"
* prop2={100}
* onAction={(value) => console.log(value)}
* >
* Content
* </ComponentName>
* ```
*/
export const ComponentName = React.forwardRef<
HTMLDivElement,
ComponentNameProps
>(
(
{
prop1,
prop2 = DEFAULT_PROP2,
onAction,
className,
children,
...restProps
},
ref
) => {
// ========================================================================
// STATE
// ========================================================================
const [internalState, setInternalState] = React.useState<string>('');
// ========================================================================
// EFFECTS
// ========================================================================
React.useEffect(() => {
// Side effects here
}, [prop1]);
// ========================================================================
// HANDLERS
// ========================================================================
const handleClick = React.useCallback(() => {
if (onAction) {
onAction(internalState);
}
}, [internalState, onAction]);
// ========================================================================
// RENDER
// ========================================================================
return (
<div
ref={ref}
className={cn(
// Base styles
'rounded-lg border bg-white p-4 shadow-sm',
// Conditional styles
prop2 > 50 && 'border-blue-500',
// Custom className
className
)}
{...restProps}
>
<h2 className="text-lg font-semibold">{prop1}</h2>
<p className="text-sm text-gray-600">Value: {prop2}</p>
{children && <div className="mt-4">{children}</div>}
<button
onClick={handleClick}
className="mt-4 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
Click Me
</button>
</div>
);
}
);
ComponentName.displayName = 'ComponentName';
// ============================================================================
// EXPORTS
// ============================================================================
export default ComponentName;

View File

@@ -0,0 +1,311 @@
/**
* Custom React Hook Template
*
* This template provides a starting point for creating reusable
* custom hooks following React best practices.
*
* Usage:
* 1. Copy this template to your hooks directory
* 2. Rename file and hook
* 3. Define input parameters and return type
* 4. Implement hook logic
* 5. Add tests and documentation
*/
import { useState, useEffect, useCallback, useRef } from 'react';
// ============================================================================
// TYPES
// ============================================================================
/**
* Options for the useCustomHook
*/
export interface UseCustomHookOptions {
/** Enable/disable the hook */
enabled?: boolean;
/** Callback when data is loaded */
onSuccess?: (data: string) => void;
/** Callback when error occurs */
onError?: (error: Error) => void;
/** Retry count */
retryCount?: number;
}
/**
* Return type of useCustomHook
*/
export interface UseCustomHookResult {
/** Current data */
data: string | null;
/** Loading state */
isLoading: boolean;
/** Error state */
error: Error | null;
/** Refetch function */
refetch: () => void;
/** Reset function */
reset: () => void;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const DEFAULT_OPTIONS: Required<UseCustomHookOptions> = {
enabled: true,
onSuccess: () => {},
onError: () => {},
retryCount: 3,
};
// ============================================================================
// HOOK
// ============================================================================
/**
* useCustomHook - Brief description of what this hook does
*
* More detailed description of the hook's purpose, use cases,
* and any important implementation notes.
*
* @param param1 - Description of param1
* @param options - Configuration options
* @returns Hook result with data, loading, error states and utility functions
*
* @example
* ```tsx
* function MyComponent() {
* const { data, isLoading, error, refetch } = useCustomHook('value', {
* enabled: true,
* onSuccess: (data) => console.log('Success:', data),
* });
*
* if (isLoading) return <Spinner />;
* if (error) return <Error message={error.message} />;
*
* return <div>{data}</div>;
* }
* ```
*/
export function useCustomHook(
param1: string,
options: UseCustomHookOptions = {}
): UseCustomHookResult {
// ========================================================================
// OPTIONS
// ========================================================================
const {
enabled = DEFAULT_OPTIONS.enabled,
onSuccess = DEFAULT_OPTIONS.onSuccess,
onError = DEFAULT_OPTIONS.onError,
retryCount = DEFAULT_OPTIONS.retryCount,
} = options;
// ========================================================================
// STATE
// ========================================================================
const [data, setData] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
// ========================================================================
// REFS
// ========================================================================
// Use refs for values that shouldn't trigger re-renders
const retryCountRef = useRef(0);
const mountedRef = useRef(true);
// ========================================================================
// CLEANUP
// ========================================================================
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
// ========================================================================
// FETCH LOGIC
// ========================================================================
const fetchData = useCallback(async () => {
if (!enabled) return;
setIsLoading(true);
setError(null);
try {
// Simulate async operation
const result = await new Promise<string>((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.8) {
reject(new Error('Random error'));
} else {
resolve(`Data for ${param1}`);
}
}, 1000);
});
// Only update state if component is still mounted
if (mountedRef.current) {
setData(result);
setIsLoading(false);
onSuccess(result);
retryCountRef.current = 0; // Reset retry count on success
}
} catch (err) {
if (mountedRef.current) {
const error = err instanceof Error ? err : new Error('Unknown error');
// Retry logic
if (retryCountRef.current < retryCount) {
retryCountRef.current++;
setTimeout(() => fetchData(), 1000 * retryCountRef.current); // Exponential backoff
} else {
setError(error);
setIsLoading(false);
onError(error);
}
}
}
}, [enabled, param1, onSuccess, onError, retryCount]);
// ========================================================================
// EFFECTS
// ========================================================================
useEffect(() => {
if (enabled) {
fetchData();
}
// Cleanup function
return () => {
// Cancel any pending operations
};
}, [enabled, fetchData]);
// ========================================================================
// HANDLERS
// ========================================================================
const refetch = useCallback(() => {
retryCountRef.current = 0; // Reset retry count
fetchData();
}, [fetchData]);
const reset = useCallback(() => {
setData(null);
setError(null);
setIsLoading(false);
retryCountRef.current = 0;
}, []);
// ========================================================================
// RETURN
// ========================================================================
return {
data,
isLoading,
error,
refetch,
reset,
};
}
// ============================================================================
// HELPER HOOKS (Optional)
// ============================================================================
/**
* useDebounce - Debounce a value
*
* Useful for search inputs, window resize handlers, etc.
*
* @example
* ```tsx
* const [searchTerm, setSearchTerm] = useState('');
* const debouncedSearchTerm = useDebounce(searchTerm, 500);
*
* useEffect(() => {
* // API call with debounced value
* searchAPI(debouncedSearchTerm);
* }, [debouncedSearchTerm]);
* ```
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
/**
* useLocalStorage - Sync state with localStorage
*
* @example
* ```tsx
* const [theme, setTheme] = useLocalStorage('theme', 'light');
* ```
*/
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
return [storedValue, setValue];
}
// ============================================================================
// EXPORTS
// ============================================================================
export default useCustomHook;

View File

@@ -0,0 +1,228 @@
/**
* Next.js App Router Page Template
*
* This template demonstrates best practices for creating pages
* in Next.js 14+ App Router with Server Components.
*
* Key Features:
* - Server Component by default (no 'use client' needed)
* - SEO metadata generation
* - Suspense boundaries for streaming
* - Error handling
* - TypeScript types
*/
import { Metadata } from 'next';
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
// Components
import { PageHeader } from '@/components/organisms/PageHeader';
import { DataTable } from '@/components/organisms/DataTable';
import { Skeleton } from '@/components/atoms/Skeleton';
// ============================================================================
// TYPES
// ============================================================================
interface PageProps {
params: {
id: string;
};
searchParams: {
page?: string;
sort?: string;
};
}
// ============================================================================
// METADATA (SEO)
// ============================================================================
/**
* Generate static metadata for SEO
*
* This function runs on the server and generates metadata
* for search engines and social media.
*/
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
// Fetch data needed for metadata
const data = await fetchData(params.id);
if (!data) {
return {
title: 'Not Found',
};
}
return {
title: `${data.title} | Your App Name`,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
images: [data.imageUrl],
},
twitter: {
card: 'summary_large_image',
title: data.title,
description: data.description,
images: [data.imageUrl],
},
};
}
/**
* Optional: Generate static paths for Static Site Generation (SSG)
*
* Uncomment this function if you want to pre-render pages at build time.
*/
// export async function generateStaticParams() {
// const items = await fetchAllItems();
//
// return items.map((item) => ({
// id: item.id,
// }));
// }
// ============================================================================
// DATA FETCHING
// ============================================================================
/**
* Fetch data on the server
*
* This runs on the server, so you can safely access databases,
* private APIs, etc.
*/
async function fetchData(id: string) {
const response = await fetch(`https://api.example.com/items/${id}`, {
next: {
revalidate: 3600, // ISR: Revalidate every hour
// OR use 'force-cache' for static generation
// OR use 'no-store' for dynamic rendering
},
});
if (!response.ok) {
return null;
}
return response.json();
}
async function fetchRelatedData(filters: { page: number; sort: string }) {
const response = await fetch(
`https://api.example.com/related?page=${filters.page}&sort=${filters.sort}`,
{
next: { revalidate: 60 }, // Revalidate every minute
}
);
return response.json();
}
// ============================================================================
// PAGE COMPONENT
// ============================================================================
/**
* Page Component (Server Component by default)
*
* Server Components:
* - Run on the server only
* - Can access backend resources directly
* - Don't add JavaScript to the client bundle
* - Can't use hooks like useState, useEffect
* - Can't add event handlers
*/
export default async function Page({ params, searchParams }: PageProps) {
// ========================================================================
// DATA FETCHING (Server-side)
// ========================================================================
// Parse search params
const page = searchParams.page ? parseInt(searchParams.page) : 1;
const sort = searchParams.sort || 'desc';
// Fetch data in parallel for performance
const [mainData, relatedData] = await Promise.all([
fetchData(params.id),
fetchRelatedData({ page, sort }),
]);
// Handle not found
if (!mainData) {
notFound(); // Renders app/not-found.tsx
}
// ========================================================================
// RENDER
// ========================================================================
return (
<div className="container mx-auto px-4 py-8">
{/* Page Header */}
<PageHeader
title={mainData.title}
description={mainData.description}
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Items', href: '/items' },
{ label: mainData.title, href: `/items/${params.id}` },
]}
/>
{/* Main Content */}
<section className="mt-8">
<h2 className="text-2xl font-bold">Details</h2>
<div className="mt-4 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<p className="text-gray-600">ID: {mainData.id}</p>
<p className="text-gray-600">Created: {mainData.createdAt}</p>
</div>
</div>
</section>
{/* Related Data with Suspense */}
<section className="mt-12">
<h2 className="text-2xl font-bold">Related Items</h2>
<Suspense fallback={<Skeleton className="mt-4 h-96" />}>
<DataTable
data={relatedData.items}
columns={['id', 'name', 'status']}
pagination={{
currentPage: page,
totalPages: relatedData.totalPages,
}}
/>
</Suspense>
</section>
</div>
);
}
// ============================================================================
// CLIENT COMPONENT EXAMPLE (if needed)
// ============================================================================
/**
* If you need interactivity, create a separate Client Component:
*
* 'use client';
*
* export function InteractiveSection({ initialData }) {
* const [state, setState] = useState(initialData);
*
* return (
* <div>
* <button onClick={() => setState(...)}>Click me</button>
* </div>
* );
* }
*
* Then use it in the Server Component:
* <InteractiveSection initialData={mainData} />
*/