# Copilot Instructions for VU Frontend
## Project Overview
**VU** is an AI-powered virtual interview platform with two modes: **Practice Mode** (job seekers, mock interviews) and **Recruitment Mode** (companies, AI-led candidate screening). This React 19 SPA uses Vite 7, Tailwind CSS 4, and follows a design-first approach from [Figma](https://www.figma.com/design/LgLS6zCwbhl4yISLlsN2qC/VU-WebApp).
## Tech Stack & Tooling
| Technology | Version | Purpose |
| ---------------- | ------- | -------------------------------------------------------------------- |
| **React** | 19 | UI Framework |
| **Vite** | 7 | Build tool & HMR dev server |
| **Tailwind CSS** | 4 | Utility-first styling (via `@import 'tailwindcss'` + `@theme` block) |
| **Lucide React** | 0.562+ | Icon components (component-based, not font icons) |
| **PropTypes** | 15.8+ | Runtime prop validation (required for all components) |
| **ESLint** | 9 | Linting (flat config in `eslint.config.js`) |
| **Prettier** | 3.7 | Code formatting + Tailwind class sorting |
**No routing library yet** - manual state-based navigation in `App.jsx` (React Router planned).
## Development Commands
```bash
npm run dev # Dev server on http://localhost:5173 (Vite HMR)
npm run build # Production build to dist/
npm run preview # Preview production build locally
npm run lint # ESLint validation
```
**Note**: Prettier runs on save in VS Code. Tailwind classes are auto-sorted by `prettier-plugin-tailwindcss`.
---
## Architecture & File Organization
```
src/
├── components/
│ ├── ui/ # Reusable UI primitives
│ │ ├── Badge/ # Badge + RoleBadge + variants.js
│ │ ├── Breadcrumb/
│ │ ├── Button/
│ │ ├── Cards/ # ActionCard, EntityCard, InfoCard, QuestionCard, QuickInfoCard
│ │ ├── Charts/ # DonutChart, StatsChart
│ │ ├── Input/ # Input, InputField, Label, Hint + variants.jsx
│ │ ├── Pagination/
│ │ ├── SidebarButton/
│ │ ├── Tables/ # TableHeader, TableRow, TableCell
│ │ ├── Tabs/
│ │ ├── Tags/
│ │ ├── Toggle/
│ │ ├── User/
│ │ └── index.js # Barrel export for all UI components
│ └── layout/ # Layout components
│ ├── Navbar/ # Navbar + NotificationDropdown
│ ├── PageLayout/ # Main app shell (sidebar + navbar + content)
│ ├── Shortcuts/ # Action bar (filters + search + buttons)
│ ├── Sidebar/
│ └── index.js # Barrel export for layout components
├── pages/ # Route-level page components
│ ├── Candidates/ # CandidatesPage + Pipeline/
│ ├── CompanyTeam/ # (placeholder)
│ ├── Jobs/ # (placeholder)
│ ├── Mocks/ # (placeholder)
│ ├── Profile/ # (placeholder)
│ └── _showcase/ # ComponentShowcase (demo page)
├── styles/
│ ├── index.css # Tailwind CSS 4 import + @theme config + utilities
│ └── tokens.css # 460+ Figma-exported CSS variables
└── assets/ # Static images, icons
```
### Key Files
| File | Purpose |
| --------------------------------------- | -------------------------------------------------------------- |
| `src/components/ui/index.js` | Central barrel export - **add new UI components here** |
| `src/components/layout/index.js` | Barrel export for layout components |
| `src/styles/tokens.css` | 460+ design tokens from Figma - **single source of truth** |
| `src/styles/index.css` | Tailwind CSS 4 config via `@theme` block + custom utilities |
| `src/App.jsx` | Main entry point with state-based navigation in `renderPage()` |
| `src/pages/_showcase/ComponentShowcase` | Component demo page for testing |
---
## Component Patterns
### 1. Standard Component Structure
Every UI component follows this folder structure:
```
src/components/ui/{ComponentName}/
├── {ComponentName}.jsx # Component logic + PropTypes
├── {ComponentName}.css # BEM-like CSS with design tokens
└── index.js # Re-export: export { ComponentName } from './{ComponentName}'
```
**Complete Example** (`src/components/ui/Button/Button.jsx`):
```jsx
import './Button.css';
import PropTypes from 'prop-types';
const VARIANTS = ['primary', 'secondary', 'ghost'];
export function Button({
children,
variant = 'primary',
disabled = false,
iconLeft,
iconRight,
type = 'button',
className = '',
...props
}) {
// Fallback to primary if invalid variant passed
const safeVariant = VARIANTS.includes(variant) ? variant : 'primary';
return (
<button
type={type}
className={`btn btn--${safeVariant} ${className}`.trim()}
disabled={disabled}
{...props}
>
{iconLeft && (
<span className="btn__icon" aria-hidden="true">
{iconLeft}
</span>
)}
{children && <span>{children}</span>}
{iconRight && (
<span className="btn__icon" aria-hidden="true">
{iconRight}
</span>
)}
</button>
);
}
Button.propTypes = {
children: PropTypes.node,
variant: PropTypes.oneOf(['primary', 'secondary', 'ghost']),
disabled: PropTypes.bool,
iconLeft: PropTypes.node,
iconRight: PropTypes.node,
type: PropTypes.oneOf(['button', 'submit', 'reset']),
className: PropTypes.string,
};
```
### 2. PropTypes (REQUIRED)
All components must include PropTypes for runtime validation:
```jsx
import PropTypes from 'prop-types';
Component.propTypes = {
// Required props
label: PropTypes.string.isRequired,
// Optional with defaults
variant: PropTypes.oneOf(['primary', 'secondary', 'ghost']),
disabled: PropTypes.bool,
className: PropTypes.string,
// Icons (Lucide components passed as JSX)
icon: PropTypes.elementType, // For component reference: icon={Mail}
iconLeft: PropTypes.node, // For rendered JSX: iconLeft={<Mail size={16} />}
// Children
children: PropTypes.node,
// Event handlers
onClick: PropTypes.func,
onChange: PropTypes.func,
// Complex shapes
user: PropTypes.shape({
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
icon: PropTypes.elementType,
}),
// Arrays
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
isActive: PropTypes.bool,
onClick: PropTypes.func,
})
),
};
```
### 3. Styling Approach (Hybrid CSS + Tailwind)
**Component CSS** (`{Component}.css`):
- Use BEM-like naming: `.component`, `.component--modifier`, `.component__element`
- Reference design tokens: `var(--token-name)`
- **NO `@apply`** (not supported in Tailwind CSS 4)
```css
/* Button.css */
.btn {
display: inline-flex;
align-items: center;
gap: var(--gap-sm);
padding: var(--btn-padding-y) var(--btn-padding-x);
border-radius: var(--radius-md);
font-weight: var(--font-medium);
transition: all 0.2s ease;
}
.btn--primary {
background-color: var(--btn-primary-bg);
color: var(--btn-primary-fg);
}
.btn--primary:hover:not(:disabled) {
background-color: var(--btn-primary-hover);
}
.btn:disabled {
opacity: 0.5;
copilot: not-allowed;
}
.btn__icon {
display: flex;
align-items: center;
}
```
**Tailwind in JSX**:
- Use `className` prop for layout/spacing utilities
- Passed classes are merged with component classes
```jsx
<Button className="mt-4 w-full" variant="primary">
Submit
</Button>
```
### 4. Design Tokens (`src/styles/tokens.css`)
460+ CSS variables exported from Figma. **Never hardcode colors/sizes.**
```css
:root {
/* Typography */
--font-sans: 'Inter', sans-serif;
--font-mono: 'Roboto Mono', ui-monospace, monospace;
--font-regular: 300;
--font-medium: 400;
--font-semibold: 500;
--font-bold: 600;
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.8125rem; /* 13px */
--text-base: 0.875rem; /* 14px */
--text-md: 1rem; /* 16px */
/* Spacing */
--size-1: 0.25rem; /* 4px */
--size-2: 0.5rem; /* 8px */
--size-4: 1rem; /* 16px */
--gap-xs: 0.25rem;
--gap-sm: 0.5rem;
--gap-md: 0.75rem;
--gap-lg: 1rem;
/* Colors - see tokens.css for full palette */
--brand-default: #...;
--bg-base: #...;
--text-primary: #...;
/* Component-specific */
--btn-primary-bg: var(--brand-default);
--btn-primary-fg: #fff;
--btn-primary-hover: var(--brand-600);
}
```
### 5. Centralized Variant Configs
For components with multiple variant types, use a `variants.js` file:
**`src/components/ui/Badge/variants.js`**:
```js
import {
CircleCheck,
Clock,
Star,
Ban,
ShieldCheck,
ShieldAlert,
ShieldX,
Crown,
Pencil,
Eye,
} from 'lucide-react';
export const BADGE_VARIANTS = {
candidateState: {
accepted: { label: 'Accepted', color: 'green', Icon: CircleCheck },
pending: { label: 'Pending', color: 'yellow', Icon: Clock },
shortlist: { label: 'Shortlist', color: 'blue', Icon: Star },
rejected: { label: 'Rejected', color: 'red', Icon: Ban },
},
cheatingFlag: {
clean: { label: 'Clean', color: 'green', Icon: ShieldCheck },
flagged: { label: 'Flagged', color: 'yellow', Icon: ShieldAlert },
critical: { label: 'Critical', color: 'red', Icon: ShieldX },
},
jobStatus: {
active: { label: 'Active', color: 'green', Icon: Sparkles },
scheduled: { label: 'Scheduled', color: 'yellow', Icon: CalendarClock },
closed: { label: 'Closed', color: 'gray', Icon: CircleMinus },
},
role: {
owner: { label: 'Owner', color: 'purple', Icon: Crown },
editor: { label: 'Editor', color: 'teal', Icon: Pencil },
viewer: { label: 'Viewer', color: 'gray', Icon: Eye },
},
};
export const BADGE_TYPES = Object.keys(BADGE_VARIANTS);
```
**Usage**:
```jsx
<Badge type="candidateState" variant="accepted" />
<Badge type="cheatingFlag" variant="flagged" iconLeft />
<Badge type="role" variant="owner" />
```
### 6. Input Variants (`src/components/ui/Input/variants.jsx`)
Specialized input components that compose from base `Input`:
| Component | Features |
| --------------- | -------------------------------- |
| `TextInput` | Basic text input |
| `EmailInput` | Email validation on blur |
| `PasswordInput` | Show/hide toggle |
| `SearchInput` | Search icon + clear button |
| `DropdownInput` | Chevron icon + dropdown behavior |
| `Textarea` | Multi-line text |
| `FileInput` | File upload with drag & drop |
All use `forwardRef` for ref forwarding:
```jsx
export const TextInput = forwardRef((props, ref) => <Input ref={ref} type="text" {...props} />);
TextInput.displayName = 'TextInput';
```
### 7. Accessibility (Non-Negotiable)
```jsx
// Icons - always hide from screen readers when decorative
{iconLeft && (
<span className="btn__icon" aria-hidden="true">
{iconLeft}
</span>
)}
// Interactive elements - always have accessible names
<button aria-label="Notifications" aria-expanded={isOpen}>
<Bell size={20} />
</button>
// Inputs - associate labels and error messages
<Input
id={inputId}
aria-invalid={error ? 'true' : undefined}
aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
/>
// Tables - use proper roles
<div role="row">
<div role="columnheader">Name</div>
</div>
// Tabs - use ARIA roles
<div role="tablist">
<button role="tab" aria-selected={isActive}>Tab 1</button>
</div>
```
### 8. Animation Patterns
This project uses **three distinct CSS animation strategies** — pick based on context.
---
**A. Entrance via IntersectionObserver** (all Cards, Charts)
Every animated component wraps in `memo`, uses `animated` prop (default `true`), and fires once:
```jsx
export const QuickInfoCard = memo(function QuickInfoCard({ animated = true, ... }) {
const [isVisible, setIsVisible] = useState(!animated); // skip state if animated=false
const cardRef = useRef(null);
useEffect(() => {
if (!animated) return;
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } },
{ threshold: 0.2 }
);
if (cardRef.current) observer.observe(cardRef.current);
return () => observer.disconnect();
}, [animated]);
return (
<div ref={cardRef} className={`quick-info-card${isVisible ? ' quick-info-card--visible' : ''}`}>
```
CSS pattern — hidden by default, visible via modifier class:
```css
.quick-info-card {
opacity: 0;
transform: translateY(var(--size-4)); /* translateY(var(--size-8)) for larger cards */
transition:
opacity var(--transition-slow),
transform var(--transition-slow);
}
.quick-info-card--visible {
opacity: 1;
transform: translateY(0);
}
```
> **Hover states** are added separately with `border-color`, `box-shadow`, and `background-color` transitions. Left-border accent (`border-left-color: var(--brand-600)`) is used on InfoCard and QuickInfoCard for the hover highlight.
---
**B. Staggered entrance via CSS custom property** (chart lists, table rows)
Each item gets `style={{ '--item-delay': `${index \* 100}ms` }}` in JSX. CSS reads it:
```jsx
// StatsChart.jsx / DonutChart.jsx
{stats.map((stat, index) => (
<div key={index} className="stats-chart__item" style={{ '--item-delay': `${index * 100}ms` }}>
```
```css
/* StatsChart.css */
.stats-chart__item {
opacity: 0;
transition-delay: var(--item-delay, 0ms);
}
.stats-chart--visible .stats-chart__item {
opacity: 1;
}
```
---
**C. Staggered CSS `@keyframes`** (Breadcrumb items)
When transitions aren't enough, use `animation-delay` with a per-item CSS variable:
```jsx
// Breadcrumb.jsx
<li style={{ '--item-index': index }} className="breadcrumb__item">
```
```css
/* Breadcrumb.css */
.breadcrumb__item {
animation: breadcrumb-fade-in var(--transition-slow) both;
animation-delay: calc(var(--item-index, 0) * 50ms);
}
@keyframes breadcrumb-fade-in {
from {
opacity: 0;
transform: translateX(calc(-1 * var(--size-2)));
}
to {
opacity: 1;
transform: translateX(0);
}
}
```
---
**D. Collapsible/expand** (CSS Grid height transition)
```css
.card__content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.card--expanded .card__content {
grid-template-rows: 1fr;
}
.card__content > div {
overflow: hidden;
}
```
**E. Immediate entrance via `requestAnimationFrame`** (QuestionCard — dynamically added to a list)
When an item is programmatically added (e.g. "Add Question" button), use `requestAnimationFrame` instead of IntersectionObserver so the animation fires on the very next paint:
```jsx
// QuestionCard.jsx
const [isVisible, setIsVisible] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
useEffect(() => {
requestAnimationFrame(() => setIsVisible(true));
}, []);
const handleRemove = () => {
setIsRemoving(true);
setTimeout(() => onRemove?.(), 300); // match CSS transition duration
};
// className combines all three states:
`question-card ${isExpanded ? 'question-card--expanded' : ''} ${isVisible ? 'question-card--visible' : ''} ${isRemoving ? 'question-card--removing' : ''}`;
```
The `question-card--removing` class plays an exit animation (opacity → 0, scale down) before the item is actually removed from the DOM.
### 9. Dropdown/Modal Patterns
**Close on outside click + Escape** (`src/components/layout/Navbar/Navbar.jsx`):
```jsx
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const close = useCallback(() => setIsOpen(false), []);
useEffect(() => {
if (!isOpen) return;
const handleEvent = (e) => {
if (e.type === 'keydown' && e.key === 'Escape') close();
if (e.type === 'mousedown' && dropdownRef.current && !dropdownRef.current.contains(e.target)) {
close();
}
};
document.addEventListener('mousedown', handleEvent);
document.addEventListener('keydown', handleEvent);
return () => {
document.removeEventListener('mousedown', handleEvent);
document.removeEventListener('keydown', handleEvent);
};
}, [isOpen, close]);
```
---
## Page Development
### PageLayout Pattern
All pages are wrapped in `PageLayout` which provides sidebar + navbar + content area:
```jsx
// App.jsx
import { PageLayout } from './components/layout/PageLayout';
export default function App() {
const [activePage, setActivePage] = useState('candidates');
const navItems = [
{
icon: Users,
label: 'Candidates',
isActive: activePage === 'candidates',
onClick: () => setActivePage('candidates'),
},
{
icon: Briefcase,
label: 'Jobs',
isActive: activePage === 'jobs',
onClick: () => setActivePage('jobs'),
},
// ... more nav items
];
const user = { name: 'User Name', email: '[email protected]', icon: Hash };
return (
<PageLayout navItems={navItems} user={user} breadcrumbItems={getBreadcrumbItems()}>
{renderPage()}
</PageLayout>
);
}
```
### Page Structure
```
src/pages/{PageName}/
├── {PageName}Page.jsx # Main page component (or just PageName.jsx)
├── {PageName}Page.css # Page-specific styles
├── index.js # Re-export
└── {SubPage}/ # Optional sub-pages
├── {SubPage}.jsx
├── {SubPage}.css
└── index.js
```
### Standard Page Pattern (`src/pages/Candidates/Pipeline/Pipeline.jsx`):
```jsx
import { useState } from 'react';
import { Shortcuts } from '../../../components/layout/Shortcuts';
import { Tabs } from '../../../components/ui/Tabs';
import { TableHeader, TableRow, TableCell } from '../../../components/ui/Tables';
import { Badge } from '../../../components/ui/Badge';
import { Pagination } from '../../../components/ui/Pagination';
import './Pipeline.css';
// Mock data - will be replaced with API calls
const MOCK_CANDIDATES = [
/* ... */
];
export function Pipeline() {
const [activeTab, setActiveTab] = useState('pipeline');
const [currentPage, setCurrentPage] = useState(1);
const [sortColumn, setSortColumn] = useState(null);
const [sortDirection, setSortDirection] = useState(null);
return (
<div className="pipeline">
{/* Shortcuts - filters, search, actions */}
<Shortcuts
onFilterClick={() => {}}
searchPlaceholder="Search candidates"
primaryAction={{ label: 'Add Candidate', icon: Plus, onClick: () => {} }}
/>
{/* Tabs - page sections */}
<Tabs
items={[
{
label: 'Pipeline',
isActive: activeTab === 'pipeline',
onClick: () => setActiveTab('pipeline'),
},
{
label: 'Overview',
isActive: activeTab === 'overview',
onClick: () => setActiveTab('overview'),
},
]}
/>
{/* Content */}
<div className="pipeline__table">
<TableHeader columns={columns} onSort={handleSort} />
{candidates.map((candidate) => (
<TableRow key={candidate.id}>
<TableCell>{candidate.name}</TableCell>
<TableCell>
<Badge type="candidateState" variant={candidate.status} />
</TableCell>
</TableRow>
))}
</div>
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
</div>
);
}
```
### Adding a New Page
1. Create folder: `src/pages/{PageName}/`
2. Create `{PageName}.jsx`, `{PageName}.css`, `index.js`
3. Add to `App.jsx`:
- Add nav item in `navItems` array with `onClick` that calls `setActivePage()` and `setBreadcrumbItems()`
- Add case in the `renderPage()` switch
### Candidates: Drill-Down Navigation Pattern
Sub-page navigation within a page is **flat state inside the parent page** — no routing. `Pipeline` owns `selectedCandidate` state:
```jsx
// Pipeline.jsx
const [selectedCandidate, setSelectedCandidate] = useState(null);
// Row click → drill in
const handleRowClick = (candidate) => setSelectedCandidate(candidate);
// Back button → back to list
const handleBack = () => setSelectedCandidate(null);
// Conditional render — replaces the whole pipeline view
return selectedCandidate ? (
<CandidateDetails candidate={selectedCandidate} onBack={handleBack} />
) : (
<div className="pipeline">...</div>
);
```
`CandidateDetails` receives the full candidate object as a prop and derives all display data locally via `useMemo` (skill distribution, AI insights, question review, completed mocks). It **does not fetch** — all data is currently generated/mocked from the candidate object.
```jsx
// CandidateDetails.jsx
export function CandidateDetails({ candidate }) {
const skillDistribution = useMemo(
() => buildSkillDistribution(candidate.score),
[candidate.score]
);
const aiInsights = useMemo(() => buildAIInsights(candidate), [candidate]);
// ...
}
```
When backend integration arrives, replace the `build*()` helper functions with API calls, keeping the component signature unchanged.
---
## Barrel Exports
### UI Components (`src/components/ui/index.js`):
```js
// UI Components
export { default as Button } from './Button';
export { Toggle } from './Toggle';
export { Badge, BADGE_VARIANTS, BADGE_TYPES } from './Badge';
export { Breadcrumb } from './Breadcrumb';
export { Pagination } from './Pagination';
export { SidebarButton } from './SidebarButton';
export { User } from './User';
export { Tabs } from './Tabs';
export { Tags } from './Tags';
export { TableHeader, TableRow, TableCell } from './Tables';
export { StatsChart } from './StatsChart';
```
### Layout Components (`src/components/layout/index.js`):
```js
export { Navbar } from './Navbar';
export { Sidebar } from './Sidebar';
export { Shortcuts } from './Shortcuts';
export { PageLayout } from './PageLayout';
```
---
## Key Conventions Summary
| Pattern | Convention |
| --------------------- | ---------------------------------------------------------------------- |
| **Component exports** | Named exports: `export function Button() {}` |
| **PropTypes** | Required for all components |
| **Icons** | Lucide React: `import { Icon } from 'lucide-react'` |
| **Icon sizing** | In JSX: `<Icon size={16} />` |
| **CSS naming** | BEM-like: `.component`, `.component--modifier`, `.component__element` |
| **CSS values** | Design tokens: `var(--token-name)` - never hardcode |
| **Disabled styling** | CSS `:disabled` pseudo-class |
| **Ref forwarding** | `forwardRef` for input components |
| **Variant fallback** | `const safeVariant = VARIANTS.includes(variant) ? variant : 'primary'` |
| **Event cleanup** | Always return cleanup function from `useEffect` |
| **Stable callbacks** | Use `useCallback` for event handlers passed to effects |
| **Mock data** | Prefix with `MOCK_`: `const MOCK_CANDIDATES = [...]` |
| **File naming** | PascalCase: `Button.jsx`, `InputField.jsx` |
| **Tailwind sorting** | Automatic via Prettier - don't manually reorder |
---
## Current State & Roadmap
### ✅ Implemented
- Component library (Button, Input variants, Badge, Cards, Tables, Tabs, Pagination, etc.)
- Layout system (PageLayout, Navbar, Sidebar, Shortcuts)
- Design token system (460+ variables from Figma)
- Candidates page with Pipeline view
- Component showcase page
### 🔄 Planned
- React Router integration (replace manual state navigation)
- Backend API integration ([VU-WebApp backend](https://github.com/UwUkareem/VU-WebApp.git))
- Global state management (Context/Zustand when needed)
- Remaining pages (Jobs, Mocks, CompanyTeam, Profile)
---
## Troubleshooting
| Issue | Solution |
| ---------------------------- | --------------------------------------------------------------------------------- |
| ESLint config errors | Use flat config format (`eslint.config.js`), not `.eslintrc` |
| Tailwind classes reordering | Normal - Prettier plugin auto-sorts on save |
| Vite HMR not working | Check export consistency (named vs default) |
| Design token not found | Check `src/styles/tokens.css` first; don't hardcode |
| Import not found from barrel | Ensure exported from both `{Component}/index.js` AND `src/components/ui/index.js` |
| `@apply` not working | Tailwind CSS 4 doesn't support `@apply` - use design tokens in CSS instead |
A system prompt system prompt that transforms complex technical content into clear, professional documentation with consistent formatting and progressive disclosure.
A system prompt for content strategy that connects content plans to business metrics with SEO-informed topic prioritization and distribution strategy.
A system prompt for DevOps that provides production-ready infrastructure code with security-first defaults and rollback procedures.
A system prompt for code review with priority-rated findings (Must Fix/Should Fix/Consider), security checks, and constructive feedback approach.
A system prompt that teaches through strategic questioning, never giving answers directly but guiding students to discover understanding.
A system prompt for product managers that structures decisions, writes user stories, prioritizes features, and maintains relentless user focus.