--- name: frontend description: > Use this skill whenever the user wants to scaffold, build, or configure a frontend application. Triggers include: 'create a React/Next.js/Vue app', 'build a UI', 'set up a landing page', 'design a dashboard', 'build a chat interface', 'add a component', 'set up routing', 'connect frontend to backend API', 'add state management', 'style with Tailwind/CSS', or any request to create something the user will see in a browser. Also use for setting up environment variables, API client layers, auth flows on the frontend, and frontend deployment (Vercel, Netlify). Do NOT use for backend-only server tasks, database work, or DevOps pipelines not related to static site deployment. license: MIT --- # 🎨 Frontend Setup Skill > Build polished, production-ready frontend apps that connect seamlessly to your backend and AI layer. --- ## πŸ—ΊοΈ Quick Reference | Goal | Stack | Bootstrap Command | |---------------------------|--------------------------------|-------------------------------------------| | Full-stack web app | Next.js 14 (App Router) | `npx create-next-app@latest` | | SPA / dashboard | React + Vite | `npm create vite@latest -- --template react-ts` | | Static site | Astro | `npm create astro@latest` | | Styling | Tailwind CSS | `npm install -D tailwindcss postcss autoprefixer` | | Components | shadcn/ui | `npx shadcn@latest init` | | State management | Zustand (lightweight) | `npm install zustand` | | Data fetching | TanStack Query | `npm install @tanstack/react-query` | | Forms | React Hook Form + Zod | `npm install react-hook-form zod` | | HTTP client | Axios or native fetch | `npm install axios` | --- ## πŸ—οΈ Project Structure (Next.js 14 App Router) ``` frontend/ β”œβ”€β”€ app/ β”‚ β”œβ”€β”€ layout.tsx # Root layout β”‚ β”œβ”€β”€ page.tsx # Home page β”‚ β”œβ”€β”€ (auth)/ β”‚ β”‚ β”œβ”€β”€ login/page.tsx β”‚ β”‚ └── register/page.tsx β”‚ β”œβ”€β”€ dashboard/ β”‚ β”‚ └── page.tsx β”‚ └── api/ # API route handlers (optional) β”œβ”€β”€ components/ β”‚ β”œβ”€β”€ ui/ # shadcn/ui primitives β”‚ β”œβ”€β”€ layout/ β”‚ β”‚ β”œβ”€β”€ Navbar.tsx β”‚ β”‚ └── Sidebar.tsx β”‚ └── features/ β”‚ └── chat/ β”‚ β”œβ”€β”€ ChatWindow.tsx β”‚ └── MessageBubble.tsx β”œβ”€β”€ lib/ β”‚ β”œβ”€β”€ api.ts # Axios/fetch client β”‚ β”œβ”€β”€ auth.ts # Auth helpers β”‚ └── utils.ts # cn() and shared utilities β”œβ”€β”€ store/ β”‚ └── useAppStore.ts # Zustand global state β”œβ”€β”€ hooks/ β”‚ └── useChat.ts # Custom hooks β”œβ”€β”€ types/ β”‚ └── index.ts # Shared TypeScript types β”œβ”€β”€ .env.local β”œβ”€β”€ tailwind.config.ts └── next.config.ts ``` --- ## πŸš€ Bootstrap: Next.js 14 ### 1. Create & Install ```bash npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir no cd my-app npx shadcn@latest init npm install axios zustand @tanstack/react-query react-hook-form zod ``` ### 2. `lib/api.ts` β€” Centralized API Client ```typescript import axios from 'axios'; export const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000', headers: { 'Content-Type': 'application/json' }, withCredentials: true, }); // Attach JWT token from localStorage api.interceptors.request.use((config) => { const token = localStorage.getItem('access_token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // Handle 401 globally api.interceptors.response.use( (res) => res, (err) => { if (err.response?.status === 401) { localStorage.removeItem('access_token'); window.location.href = '/login'; } return Promise.reject(err); } ); ``` ### 3. `store/useAppStore.ts` β€” Zustand Global State ```typescript import { create } from 'zustand'; interface User { id: string; email: string; name: string; } interface AppState { user: User | null; token: string | null; setUser: (user: User, token: string) => void; logout: () => void; } export const useAppStore = create((set) => ({ user: null, token: typeof window !== 'undefined' ? localStorage.getItem('access_token') : null, setUser: (user, token) => { localStorage.setItem('access_token', token); set({ user, token }); }, logout: () => { localStorage.removeItem('access_token'); set({ user: null, token: null }); }, })); ``` ### 4. `.env.local` ```env NEXT_PUBLIC_API_URL=http://localhost:8000 NEXT_PUBLIC_APP_NAME=MyApp ``` --- ## πŸ’¬ AI Chat Interface Component ```tsx // components/features/chat/ChatWindow.tsx 'use client'; import { useState, useRef, useEffect } from 'react'; import { api } from '@/lib/api'; interface Message { role: 'user' | 'assistant'; content: string; } export function ChatWindow() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const send = async () => { if (!input.trim() || loading) return; const userMsg: Message = { role: 'user', content: input }; setMessages((prev) => [...prev, userMsg]); setInput(''); setLoading(true); try { const { data } = await api.post('/api/ask', { query: input }); setMessages((prev) => [...prev, { role: 'assistant', content: data.answer }]); } catch { setMessages((prev) => [...prev, { role: 'assistant', content: '⚠️ Something went wrong.' }]); } finally { setLoading(false); } }; return (
{messages.map((m, i) => (
{m.content}
))} {loading && (
Thinking…
)}
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && send()} disabled={loading} />
); } ``` --- ## πŸ” Auth Flow (Login Page) ```tsx // app/(auth)/login/page.tsx 'use client'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { api } from '@/lib/api'; import { useAppStore } from '@/store/useAppStore'; import { useRouter } from 'next/navigation'; const schema = z.object({ email: z.string().email(), password: z.string().min(8), }); type FormData = z.infer; export default function LoginPage() { const { setUser } = useAppStore(); const router = useRouter(); const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), }); const onSubmit = async (data: FormData) => { const { data: res } = await api.post('/api/auth/login', data); setUser(res.user, res.access_token); router.push('/dashboard'); }; return (

Sign in

{errors.email &&

{errors.email.message}

} {errors.password &&

{errors.password.message}

}
); } ``` --- ## 🌐 Streaming AI Responses (Server-Sent Events) ```typescript // For streaming responses from the RAG backend async function* streamResponse(query: string) { const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/ask/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }), }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; yield decoder.decode(value); } } ``` --- ## βœ… Checklist Before Shipping - [ ] `NEXT_PUBLIC_API_URL` set correctly for each environment - [ ] Auth token refresh logic in place - [ ] Loading & error states on every async action - [ ] Mobile responsive (Tailwind `sm:` / `md:` breakpoints checked) - [ ] `` metadata: title, description, OG tags - [ ] No API keys in frontend code or `.env.local` (only `NEXT_PUBLIC_*` vars) - [ ] `next build` passes with zero errors - [ ] Lighthouse score > 90 --- ## 🚨 Common Mistakes to Avoid | ❌ Mistake | βœ… Fix | |-----------------------------------------|------------------------------------------------------| | API URL hardcoded as localhost | Always use `NEXT_PUBLIC_API_URL` env var | | Fetching in render with no cache | Wrap with TanStack Query for caching + deduplication | | Storing sensitive data in localStorage | Use httpOnly cookies via server for tokens | | No error boundaries | Add `error.tsx` in each route segment | | Huge bundle size | Dynamic import heavy components with `next/dynamic` |