| name | frontend_development |
| description | Build and optimize Next.js 14 frontend components with TypeScript, Tailwind, and theme support |
Next.js Frontend Development Skill
Purpose
Build high-quality, type-safe, and accessible components for the AARLP Next.js frontend. This skill covers App Router patterns, Server/Client components, theming, and performance optimization.
Frontend Architecture
frontend/
├── app/ # Next.js App Router
│ ├── (auth)/ # Auth routes (login, register)
│ ├── (dashboard)/ # Protected app routes
│ ├── layout.tsx # Root layout
│ └── providers.tsx # Context providers
├── components/ # Reusable UI components
│ ├── ui/ # Base UI primitives
│ ├── layout/ # Layout components (Sidebar, Header)
│ └── theme/ # Theme toggle, providers
├── features/ # Feature-specific components
│ ├── jobs/ # Job management UI
│ ├── candidates/ # Candidate views
│ └── dashboard/ # Dashboard widgets
├── lib/ # Utilities
│ ├── api.ts # API client
│ ├── types.ts # TypeScript types
│ └── utils.ts # Helper functions
└── styles/
└── globals.css # Global styles + Tailwind
Component Patterns
1. Server Components (Default)
Use for data fetching, SEO, and static content:
import { getJobs } from '@/lib/api';
import { JobCard } from '@/features/jobs/JobCard';
export default async function JobsPage() {
const jobs = await getJobs();
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">My Jobs</h1>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
</div>
);
}
export const revalidate = 60;
2. Client Components
Use for interactivity, hooks, and browser APIs:
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
export function JobWizard() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({});
const handleSubmit = async () => {
const response = await fetch('/api/jobs/create', {
method: 'POST',
body: JSON.stringify(formData)
});
const data = await response.json();
};
return (
<div className="space-y-6">
<h2>Create Job - Step {step}/4</h2>
{/* Form steps */}
<Button onClick={handleSubmit}>
Next
</Button>
</div>
);
}
3. Hybrid Pattern (Server + Client)
import { getJob } from '@/lib/api';
import { JobActions } from './JobActions';
export default async function JobDetailPage({ params }: { params: { id: string } }) {
const job = await getJob(params.id);
return (
<div>
{/* Server-rendered content */}
<h1>{job.title}</h1>
<p>{job.company}</p>
{/* Client-side interactivity */}
<JobActions jobId={job.id} />
</div>
);
}
'use client';
export function JobActions({ jobId }: { jobId: string }) {
const handleApprove = async () => {
await fetch(`/api/jobs/${jobId}/approve`, { method: 'POST' });
};
return <button onClick={handleApprove}>Approve JD</button>;
}
Theming (Light/Dark Mode)
Setup (Already in AARLP)
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
Theme-Aware Components
'use client';
import { useTheme } from 'next-themes';
import { Moon, Sun } from 'lucide-react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800"
>
<Sun className="h-5 w-5 dark:hidden" />
<Moon className="h-5 w-5 hidden dark:block" />
</button>
);
}
Tailwind Theme Configuration
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0fdfc',
100: '#ccfbf6',
500: '#06b6d4',
600: '#0891b2',
900: '#164e63',
},
dark: {
bg: '#0a0a0a',
card: '#1a1a1a',
border: '#2a2a2a',
}
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
}
}
}
}
Styling Patterns
Glassmorphism (AARLP Style)
export function GlassCard({ children, className = '' }: {
children: React.ReactNode;
className?: string;
}) {
return (
<div className={`
backdrop-blur-xl
bg-white/80 dark:bg-gray-900/80
border border-gray-200/50 dark:border-gray-700/50
rounded-2xl
shadow-xl
p-6
${className}
`}>
{children}
</div>
);
}
<GlassCard>
<h2>Job Details</h2>
<p>Content goes here</p>
</GlassCard>
Gradient Accents
export function GradientText({ children }: { children: React.ReactNode }) {
return (
<span className="bg-gradient-to-r from-cyan-500 to-blue-600 bg-clip-text text-transparent">
{children}
</span>
);
}
<h1 className="text-4xl font-bold">
<GradientText>AI-Powered Recruitment</GradientText>
</h1>
TypeScript Best Practices
Type Definitions
export interface Job {
id: string;
title: string;
company_name: string;
location: string;
employment_type: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT';
experience_level: 'JUNIOR' | 'MID' | 'SENIOR';
workflow_status: WorkflowStatus;
jd_content?: string;
created_at: string;
updated_at: string;
}
export interface Candidate {
id: string;
job_id: string;
name: string;
email: string;
resume_url: string;
semantic_score: number;
status: 'PENDING' | 'SHORTLISTED' | 'REJECTED';
}
export interface JobCardProps {
job: Job;
onApprove?: (jobId: string) => void;
showActions?: boolean;
}
export interface JobFormData {
title: string;
company_name: string;
location: string;
employment_type: string;
experience_level: string;
description?: string;
}
Type-Safe API Client
import { Job, Candidate } from './types';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
async function fetchAPI<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
return response.json();
}
export const api = {
jobs: {
list: () => fetchAPI<Job[]>('/jobs/'),
get: (id: string) => fetchAPI<Job>(`/jobs/${id}`),
create: (data: JobFormData) => fetchAPI<Job>('/jobs/create', {
method: 'POST',
body: JSON.stringify(data),
}),
},
candidates: {
list: (jobId: string) => fetchAPI<Candidate[]>(`/jobs/${jobId}/candidates`),
},
};
const jobs = await api.jobs.list();
Performance Optimization
1. Image Optimization
import Image from 'next/image';
<Image
src="/company-logo.png"
alt="Company Logo"
width={200}
height={100}
priority={false} // Only true for above-fold images
placeholder="blur"
blurDataURL="data:image/..." // Optional blur placeholder
/>
2. Code Splitting
import dynamic from 'next/dynamic';
const JobEditor = dynamic(() => import('@/features/jobs/JobEditor'), {
loading: () => <div>Loading editor...</div>,
ssr: false
});
export function JobDetail() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<button onClick={() => setShowEditor(true)}>Edit JD</button>
{showEditor && <JobEditor />}
</div>
);
}
3. Memoization
'use client';
import { useMemo, useCallback } from 'react';
export function CandidateList({ candidates }: { candidates: Candidate[] }) {
const sortedCandidates = useMemo(() => {
return [...candidates].sort((a, b) => b.semantic_score - a.semantic_score);
}, [candidates]);
const handleSelect = useCallback((id: string) => {
console.log('Selected:', id);
}, []);
return (
<ul>
{sortedCandidates.map((c) => (
<li key={c.id} onClick={() => handleSelect(c.id)}>
{c.name} - Score: {c.semantic_score}
</li>
))}
</ul>
);
}
State Management
1. React Context (Global State)
'use client';
import { createContext, useContext, useState } from 'react';
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const data = await response.json();
setUser(data.user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
2. URL State (Search Params)
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
export function JobFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const currentType = searchParams.get('type') || 'all';
const handleFilterChange = (type: string) => {
const params = new URLSearchParams(searchParams);
params.set('type', type);
router.push(`/jobs?${params.toString()}`);
};
return (
<select value={currentType} onChange={(e) => handleFilterChange(e.target.value)}>
<option value="all">All Jobs</option>
<option value="active">Active</option>
<option value="draft">Draft</option>
</select>
);
}
Form Handling
With React Hook Form + Zod
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const jobSchema = z.object({
title: z.string().min(5, 'Title must be at least 5 characters'),
company_name: z.string().min(2),
location: z.string(),
employment_type: z.enum(['FULL_TIME', 'PART_TIME', 'CONTRACT']),
});
type JobFormData = z.infer<typeof jobSchema>;
export function JobCreateForm() {
const { register, handleSubmit, formState: { errors } } = useForm<JobFormData>({
resolver: zodResolver(jobSchema),
});
const onSubmit = async (data: JobFormData) => {
await api.jobs.create(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label>Job Title</label>
<input {...register('title')} className="input" />
{errors.title && <p className="text-red-500">{errors.title.message}</p>}
</div>
<button type="submit">Create Job</button>
</form>
);
}
Testing
import { render, screen } from '@testing-library/react';
import { JobCard } from '@/features/jobs/JobCard';
describe('JobCard', () => {
const mockJob = {
id: '123',
title: 'Senior Engineer',
company_name: 'TechCorp',
location: 'Remote',
};
it('renders job title', () => {
render(<JobCard job={mockJob} />);
expect(screen.getByText('Senior Engineer')).toBeInTheDocument();
});
it('calls onApprove when approved', async () => {
const mockApprove = jest.fn();
render(<JobCard job={mockJob} onApprove={mockApprove} />);
const approveButton = screen.getByRole('button', { name: /approve/i });
approveButton.click();
expect(mockApprove).toHaveBeenCalledWith('123');
});
});
Best Practices Checklist
Related Skills
Resources