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,
};