import React, { useState, useEffect, useCallback } from 'react';
// ============================================
// Custom Hooks
// ============================================
/**
* Custom hook for fetching data with loading and error states
*/
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
/**
* Custom hook for local storage with SSR support
*/
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
/**
* Custom hook for debounced values
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// ============================================
// Utility Components
// ============================================
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
loading = false,
disabled,
className = '',
...props
}) => {
const baseStyles = 'font-medium rounded-lg transition-colors focus:outline-none focus:ring-2';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
disabled={disabled || loading}
{...props}
>
{loading ? 'Loading...' : children}
</button>
);
};
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
export const Input: React.FC<InputProps> = ({
label,
error,
className = '',
id,
...props
}) => {
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
return (
<div className="flex flex-col gap-1">
{label && (
<label htmlFor={inputId} className="text-sm font-medium text-gray-700">
{label}
</label>
)}
<input
id={inputId}
className={`px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${error ? 'border-red-500' : 'border-gray-300'
} ${className}`}
{...props}
/>
{error && <span className="text-sm text-red-500">{error}</span>}
</div>
);
};
// ============================================
// Form Example with Validation
// ============================================
interface FormData {
email: string;
password: string;
}
export const LoginForm: React.FC = () => {
const [formData, setFormData] = useState<FormData>({ email: '', password: '' });
const [errors, setErrors] = useState<Partial<FormData>>({});
const [submitting, setSubmitting] = useState(false);
const validate = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
setSubmitting(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', formData);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4 max-w-md">
<Input
label="Email"
type="email"
value={formData.email}
onChange={e => setFormData(prev => ({ ...prev, email: e.target.value }))}
error={errors.email}
/>
<Input
label="Password"
type="password"
value={formData.password}
onChange={e => setFormData(prev => ({ ...prev, password: e.target.value }))}
error={errors.password}
/>
<Button type="submit" loading={submitting}>
Sign In
</Button>
</form>
);
};
export default { useFetch, useLocalStorage, useDebounce, Button, Input, LoginForm };