- Getting Started
- Project Structure
- Development Setup
- Next.js App Router
- Component Architecture
- State Management
- API Integration
- Authentication
- Custom Hooks
- Styling
- Testing
- Deployment
- Node.js: 18+ (recommended 20+)
- npm or yarn: Package manager
# 1. Install dependencies
npm install
# 2. Setup environment variables
# Create .env.local file (see Environment Variables)
# 3. Start development server
npm run devThe application will start on http://localhost:3000
frontend/
├── app/ # Next.js App Router
│ ├── admin/ # Admin portal pages
│ │ ├── dashboard/
│ │ ├── exams/
│ │ ├── attempts/
│ │ └── layout.tsx # Admin layout
│ │
│ ├── student/ # Student portal pages
│ │ ├── dashboard/
│ │ ├── exams/
│ │ ├── attempts/
│ │ └── layout.tsx # Student layout
│ │
│ ├── login/ # Authentication
│ ├── layout.tsx # Root layout
│ └── page.tsx # Landing page
│
├── components/ # React Components
│ ├── admin/ # Admin components
│ │ ├── AdminHeader.tsx
│ │ ├── exam/
│ │ ├── question/
│ │ └── ...
│ │
│ ├── student/ # Student components
│ │ ├── exam/
│ │ ├── questions/
│ │ └── StudentHeader.tsx
│ │
│ ├── ui/ # Reusable UI components
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ ├── Modal.tsx
│ │ └── ...
│ │
│ ├── layout/ # Layout components
│ │ ├── AdminSidebar.tsx
│ │ └── StudentSidebar.tsx
│ │
│ ├── Landing/ # Landing page components
│ └── ErrorBoundary.tsx # Error boundary component
│
├── hooks/ # Custom React Hooks
│ ├── useExamSubmission.ts
│ ├── useAutoSave.ts
│ ├── useCheatingPrevention.ts
│ ├── useExamMonitoring.ts
│ └── ...
│
├── context/ # React Context
│ ├── AuthContext.tsx # Authentication state
│ ├── ToastContext.tsx # Toast notifications
│ └── ConfirmationContext.tsx # Confirmation dialogs
│
├── lib/ # Utilities & Services
│ ├── api.ts # Axios instance
│ ├── socket.ts # Socket.IO client
│ └── google-auth.ts # Google OAuth
│
├── utils/ # Helper Functions
│ ├── answerFormatting.ts
│ ├── examCalculations.ts
│ ├── examDataUtils.ts
│ └── ...
│
├── types/ # TypeScript Types
│ ├── exam.ts
│ └── index.ts
│
├── constants/ # Constants
│ └── index.ts
│
├── public/ # Static Assets
│ ├── images/
│ ├── fonts/
│ └── videos/
│
├── package.json
├── tsconfig.json
├── next.config.ts
├── proxy.ts # Next.js proxy (replaces middleware)
└── README.md
Create a .env.local file in the frontend directory:
# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api
# Google OAuth (optional)
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id# Standard development
npm run dev
# Local development (localhost only)
npm run dev:local
# Production build
npm run build
npm startapp/
├── page.tsx → / (Landing page)
├── login/
│ └── page.tsx → /login
├── admin/
│ ├── layout.tsx → Admin layout wrapper
│ ├── dashboard/
│ │ └── page.tsx → /admin/dashboard
│ └── exams/
│ └── [examId]/
│ └── page.tsx → /admin/exams/:examId
└── student/
└── ...
Layouts wrap multiple pages and persist across navigation:
// app/admin/layout.tsx
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<AdminSidebar />
<main>{children}</main>
</div>
);
}By default, all components are Server Components. Use 'use client' for Client Components:
// Server Component (default)
export default function Page() {
return <div>Static content</div>;
}
// Client Component
'use client';
import { useState } from 'react';
export default function InteractiveComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Page Component (app/student/exams/page.tsx)
↓
Layout Component (components/layout/StudentSidebar.tsx)
↓
Feature Components (components/student/exam/*)
↓
UI Components (components/ui/*)
↓
Custom Hooks (hooks/*)
// Container Component (logic)
'use client';
export default function ExamsPage() {
const [exams, setExams] = useState([]);
const { data, isLoading } = useExams();
return <ExamsList exams={data} loading={isLoading} />;
}
// Presentational Component (UI)
function ExamsList({ exams, loading }: Props) {
if (loading) return <LoadingSpinner />;
return (
<div>
{exams.map(exam => <ExamCard key={exam.id} exam={exam} />)}
</div>
);
}// Modal with Header, Body, Footer
<Modal>
<Modal.Header>Title</Modal.Header>
<Modal.Body>Content</Modal.Body>
<Modal.Footer>
<Button>Cancel</Button>
<Button>Confirm</Button>
</Modal.Footer>
</Modal>// Example: Data fetching component
<DataFetcher
url="/api/exams"
render={(data, loading, error) => (
loading ? <Spinner /> : <ExamsList exams={data} />
)}
/>For component-specific state:
const [count, setCount] = useState(0);
const [name, setName] = useState('');For global state across components:
// Context
const AuthContext = createContext<AuthContextType | null>(null);
// Provider
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
// Usage
const { user } = useAuth();For data from API:
// Custom hook for fetching
function useExams() {
const [exams, setExams] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get('/student/exams').then(res => {
setExams(res.data);
setLoading(false);
});
}, []);
return { exams, loading };
}import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const form = useForm({
resolver: zodResolver(loginSchema),
defaultValues: { email: '', password: '' }
});
// In JSX
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('email')} />
{form.formState.errors.email && <span>Error</span>}
</form>// lib/api.ts
import axios from 'axios';
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
withCredentials: true,
});
// Usage
import { api } from '@/lib/api';
const response = await api.get('/student/exams');
const data = await api.post('/student/exams/:id/start', {});Automatic token refresh is handled in lib/api.ts:
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Refresh token automatically
await api.post('/auth/refresh');
return api(error.config);
}
return Promise.reject(error);
}
);try {
const response = await api.get('/student/exams');
return response.data;
} catch (error) {
if (error.response?.status === 401) {
// Handle unauthorized
} else if (error.response?.status === 404) {
// Handle not found
} else {
// Handle other errors
}
throw error;
}// Use authentication
import { useAuth } from '@/context/AuthContext';
function MyComponent() {
const { user, login, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <div>Please login</div>;
}
return <div>Welcome, {user?.name}</div>;
}Using Next.js proxy (proxy.ts):
// Automatically redirects unauthenticated users
export function proxy(request: NextRequest) {
const refreshToken = request.cookies.get('refreshToken');
if (!refreshToken && pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}'use client';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
export default function ProtectedPage() {
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, isLoading]);
if (isLoading) return <LoadingSpinner />;
if (!isAuthenticated) return null;
return <div>Protected content</div>;
}import { useExamSubmission } from '@/hooks/useExamSubmission';
const {
isSubmitting,
error,
handleSubmitExam,
setError
} = useExamSubmission(
attempt,
questions,
answers,
attemptId,
clearLocalStorage,
exitFullscreenRef,
isFullscreenRef,
confirmSubmit,
emitActivityUpdate
);
// Usage
<button onClick={() => handleSubmitExam(false)}>
Submit Exam
</button>import { useAutoSave } from '@/hooks/useAutoSave';
useAutoSave({
attemptId,
questions,
answers,
enabled: true,
onSaveSuccess: (questionId) => {
// Handle success
},
onSaveError: (questionId, error) => {
// Handle error
}
});import { useCheatingPrevention } from '@/hooks/useCheatingPrevention';
const {
warningCount,
fullscreenWarning,
setFullscreenWarning
} = useCheatingPrevention(attempt, handleAutoSubmit);// hooks/useCustomHook.ts
export function useCustomHook(dependency: string) {
const [state, setState] = useState(null);
useEffect(() => {
// Effect logic
}, [dependency]);
const action = useCallback(() => {
// Action logic
}, []);
return { state, action };
}
// Usage
const { state, action } = useCustomHook('value');// Utility classes
<div className="bg-blue-500 text-white p-4 rounded-lg">
Content
</div>
// Responsive
<div className="w-full md:w-1/2 lg:w-1/3">
Content
</div>
// Hover states
<button className="bg-blue-500 hover:bg-blue-600">
Button
</button>// Component.module.css
.container {
background: blue;
padding: 1rem;
}
// Component.tsx
import styles from './Component.module.css';
<div className={styles.container}>Content</div>Fonts are configured in app/layout.tsx:
const poppins = Poppins({
subsets: ['latin'],
variable: '--font-poppins',
weight: ['400', '600', '700'],
});
// Usage in Tailwind
<div className="font-poppins">Content</div>// Component.test.tsx
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders component', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});// e2e/test.spec.ts
import { test, expect } from '@playwright/test';
test('login flow', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', '[email protected]');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/student/dashboard');
});# Build Next.js application
npm run build
# Start production server
npm startSet production environment variables:
NEXT_PUBLIC_API_URL(production API URL)NEXT_PUBLIC_API_BASE_URL(production API base URL)NEXT_PUBLIC_GOOGLE_CLIENT_ID(if using Google OAuth)
- Connect GitHub repository to Vercel
- Set environment variables in Vercel dashboard
- Deploy automatically on push to main branch
- Environment variables configured
- API URLs point to production
- Error boundaries implemented
- Loading states handled
- SEO metadata configured
- Analytics setup (future)
- Error logging configured (future)
- Use TypeScript strict mode
- Follow React best practices
- Use functional components
- Extract reusable logic into hooks
- Keep components small and focused
- Use
useMemofor expensive calculations - Use
useCallbackfor function props - Implement code splitting
- Optimize images (Next.js Image component)
- Lazy load components when possible (e.g., Monaco Editor, Recharts, xlsx)
- Use
next/dynamicfor heavy libraries - Optimize images using Next.js
<Image/>component
- Use semantic HTML
- Add ARIA labels
- Ensure keyboard navigation
- Test with screen readers
- Maintain color contrast
-
API Connection Error
- Check
NEXT_PUBLIC_API_URLin.env.local - Verify backend is running
- Check CORS configuration
- Check
-
Authentication Issues
- Clear cookies and localStorage
- Verify token is being set
- Check refresh token logic
-
Build Errors
- Clear
.nextdirectory - Delete
node_modulesand reinstall - Check TypeScript errors
- Clear
-
Hydration Errors
- Ensure server and client render same content
- Use
suppressHydrationWarningif necessary - Check for browser-only APIs in render
Last Updated: November 2024 Version: 1.0.0