← Back to list

frontend-dev
by shaitamam-80
⭐ 0🍴 0📅 Jan 20, 2026
SKILL.md
name: frontend-dev description: React + TypeScript + Vite frontend development specialist. Use when building UI components, pages, forms, API integration, state management, or styling with Tailwind CSS. Triggers on requests for component creation, form handling, data fetching, responsive design, or debugging React applications. Includes Supabase integration patterns.
Frontend Dev Skill
React + TypeScript + Vite specialist for modern web applications.
Stack
| Layer | Technology |
|---|---|
| Framework | React 18+ |
| Language | TypeScript |
| Build | Vite |
| Styling | Tailwind CSS |
| Backend | Supabase (Auth + DB) |
| HTTP | fetch / TanStack Query |
Project Structure
frontend/
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── ui/ # Base components (Button, Input, Card)
│ │ └── features/ # Feature-specific components
│ ├── pages/ # Route pages
│ ├── hooks/ # Custom hooks
│ ├── lib/ # Utilities & config
│ │ └── supabase.ts # Supabase client
│ ├── types/ # TypeScript types
│ ├── services/ # API calls
│ └── App.tsx
├── .env.local # Environment variables
└── tailwind.config.js
Component Patterns
Basic Component Template
// components/features/SearchForm.tsx
import { useState } from 'react'
interface SearchFormProps {
onSearch: (query: string) => void
isLoading?: boolean
}
export function SearchForm({ onSearch, isLoading = false }: SearchFormProps) {
const [query, setQuery] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (query.trim()) onSearch(query)
}
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="flex-1 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !query.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? 'Searching...' : 'Search'}
</button>
</form>
)
}
Custom Hook Pattern
// hooks/useSearch.ts
import { useState, useCallback } from 'react'
interface UseSearchResult<T> {
data: T | null
isLoading: boolean
error: string | null
search: (query: string) => Promise<void>
}
export function useSearch<T>(endpoint: string): UseSearchResult<T> {
const [data, setData] = useState<T | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const search = useCallback(async (query: string) => {
setIsLoading(true)
setError(null)
try {
const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`)
if (!res.ok) throw new Error('Search failed')
setData(await res.json())
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setIsLoading(false)
}
}, [endpoint])
return { data, isLoading, error, search }
}
Supabase Integration
Client Setup
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import type { Database } from '@/types/database'
export const supabase = createClient<Database>(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
Auth Hook
// hooks/useAuth.ts
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import type { User } from '@supabase/supabase-js'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setLoading(false)
})
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => setUser(session?.user ?? null)
)
return () => subscription.unsubscribe()
}, [])
return { user, loading, isAuthenticated: !!user }
}
Data Fetching
// services/journals.ts
import { supabase } from '@/lib/supabase'
export async function getSearchHistory(userId: string) {
const { data, error } = await supabase
.from('search_history')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(10)
if (error) throw error
return data
}
Common Patterns
Loading States
function SearchResults({ isLoading, error, data }) {
if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage message={error} />
if (!data?.length) return <EmptyState message="No results found" />
return (
<ul className="space-y-4">
{data.map(item => <ResultCard key={item.id} {...item} />)}
</ul>
)
}
Form with Validation
const [errors, setErrors] = useState<Record<string, string>>({})
const validate = (values: FormValues): boolean => {
const newErrors: Record<string, string> = {}
if (!values.title || values.title.length < 10) {
newErrors.title = 'Title must be at least 10 characters'
}
if (!values.abstract || values.abstract.length < 50) {
newErrors.abstract = 'Abstract must be at least 50 characters'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
Protected Route
// components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth()
if (loading) return <LoadingSpinner />
if (!user) return <Navigate to="/login" replace />
return <>{children}</>
}
Tailwind Patterns
Responsive Design
// Mobile-first approach
className="
w-full // Mobile: full width
md:w-1/2 // Tablet: half width
lg:w-1/3 // Desktop: third width
p-4 md:p-6 lg:p-8 // Responsive padding
"
Common Utilities
// Card
"bg-white rounded-lg shadow-md p-6 border border-gray-200"
// Button Primary
"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
// Button Secondary
"px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
// Input
"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
// Error Text
"text-sm text-red-600 mt-1"
Environment Variables
# .env.local (NEVER commit!)
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
VITE_API_URL=https://api.example.com
Access in code:
const apiUrl = import.meta.env.VITE_API_URL
Error Handling
Error Boundary (react-error-boundary)
// Install: npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="p-6 bg-red-50 rounded-lg text-center">
<h2 className="text-red-800 font-semibold">Something went wrong</h2>
<p className="text-red-600 text-sm mt-2">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg"
>
Try again
</button>
</div>
)
}
// Usage in App.tsx
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Routes />
</ErrorBoundary>
Toast Notifications (sonner)
// Install: npm install sonner
// App.tsx - add Toaster
import { Toaster } from 'sonner'
function App() {
return (
<>
<Toaster position="top-right" richColors />
<Routes />
</>
)
}
// Usage anywhere
import { toast } from 'sonner'
toast.success('Saved successfully')
toast.error('Failed to save')
toast.loading('Searching...')
Loading States
Skeleton Component
// components/ui/Skeleton.tsx
export function Skeleton({ className }: { className?: string }) {
return (
<div className={`animate-pulse bg-slate-200 rounded ${className}`} />
)
}
// JournalCardSkeleton.tsx
export function JournalCardSkeleton() {
return (
<div className="bg-white rounded-2xl border p-6">
<Skeleton className="h-6 w-3/4 mb-4" />
<Skeleton className="h-4 w-1/2 mb-2" />
<Skeleton className="h-4 w-1/3" />
<div className="flex gap-2 mt-4">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-20 rounded-full" />
</div>
</div>
)
}
// Usage
{isLoading ? (
<div className="space-y-4">
{[...Array(3)].map((_, i) => <JournalCardSkeleton key={i} />)}
</div>
) : (
<JournalList data={data} />
)}
Performance Optimization
When to Use memo
import { memo } from 'react'
// ✅ Use memo when:
// - Component re-renders often with same props
// - Component is expensive to render
// - Parent re-renders frequently
const JournalCard = memo(function JournalCard({ journal, onSave }: Props) {
return (/* ... */)
})
// ✅ Use useMemo for expensive calculations
const sortedJournals = useMemo(() =>
journals.sort((a, b) => b.impactFactor - a.impactFactor),
[journals]
)
// ✅ Use useCallback for stable function references
const handleSave = useCallback((id: string) => {
saveJournal(id)
}, [saveJournal])
Component Splitting Guidelines
⚠️ Split component when:
- File exceeds 200-300 lines
- Multiple unrelated responsibilities
- Reusable sub-sections exist
- Testing becomes difficult
📁 Pattern:
Search.tsx (465 lines) → Split into:
├── SearchForm.tsx (form logic)
├── SearchResults.tsx (results display)
├── useJournalSearch.ts (data fetching hook)
└── SearchFilters.tsx (filter controls)
RTL Support (Hebrew/Arabic)
Logical Properties
// ❌ Physical (breaks in RTL)
"ml-4 mr-2 pl-6 text-left border-l"
// ✅ Logical (works in RTL)
"ms-4 me-2 ps-6 text-start border-s"
// Mapping:
// ml/mr → ms/me (margin-start/end)
// pl/pr → ps/pe (padding-start/end)
// left/right → start/end
// border-l/r → border-s/e
// rounded-l/r → rounded-s/e
Common Issues & Fixes
| Issue | Solution |
|---|---|
| "VITE_ env not found" | Restart dev server after adding env vars |
| Hook called conditionally | Hooks must be at component top level |
| Infinite re-renders | Add dependencies to useEffect/useCallback |
| Type errors on Supabase | Generate types with supabase gen types |
| CORS errors | Check API URL, ensure backend allows origin |
| Component too large | Split when >200 lines, extract hooks |
| RTL layout broken | Use logical properties (ms/me/ps/pe) |
Recommended Packages
# UI & UX
npm install sonner # Toast notifications
npm install react-error-boundary # Error handling
# Forms
npm install react-hook-form # Form management
npm install zod # Schema validation
# Data Fetching
npm install @tanstack/react-query # Server state management
File References
- Component Examples: See
references/components.md - Tailwind Cheatsheet: See
references/tailwind.md - TypeScript Patterns: See
references/typescript.md
Quick Commands
# Dev server
npm run dev
# Type check
npm run type-check
# Build
npm run build
# Generate Supabase types
npx supabase gen types typescript --project-id YOUR_ID > src/types/database.ts
Score
Total Score
40/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon