Skip to main content
Glama
components.tsx6.2 kB
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, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/millsydotdev/Code-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server