useEffect - Side Effects, Data Fetching & Async State
Browser DevTools - The Network Tab
Focus: React Router, Tailwind CSS v4, Vercel deployment
Over the past five weeks you've built a portfolio page in HTML, styled it with CSS, and rebuilt it as a React application. This week you take the final structural step: adding multiple pages with React Router, restyling with Tailwind v4, and deploying it to a live URL on Vercel.
By the end of this assignment, your portfolio will be publicly accessible on the internet: not running on localhost, but at a real URL you can share with anyone! 🎉
This assignment assumes your portfolio is currently a React application from weeks 4–5, with:
useStateIf your week 4–5 project differs from this, adapt the requirements to fit what you have. The core deliverables are the same.
Install React Router:
npm install react-router
Wrap your application in a BrowserRouter in main.tsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
Move your existing content into separate page components. The structure should look like this when you're done:
src/
├── components/
│ ├── Navigation.tsx
│ └── ThemeToggle.tsx
├── pages/
│ ├── HomePage.tsx ← your name, tagline, About section
│ ├── ProjectsPage.tsx ← your projects
│ ├── ContactPage.tsx ← your contact form
│ └── NotFoundPage.tsx ← 404 fallback
└── App.tsx ← route definitions
Define your routes in App.tsx:
import { Routes, Route } from 'react-router';
import { Navigation } from './components/Navigation';
import { HomePage } from './pages/HomePage';
import { ProjectsPage } from './pages/ProjectsPage';
import { ContactPage } from './pages/ContactPage';
import { NotFoundPage } from './pages/NotFoundPage';
export default function App() {
return (
<>
<Navigation />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</>
);
}
Page requirements:
/ — your homepage, including at minimum your name, tagline, and About section/projects — your projects page, displaying at minimum three projects with a title and description each/contact — your contact page, including your contact formNavigation:
Navigation component that links to all three pages using NavLinkNavLink's isActive propApp.tsx outside <Routes> so it stays mounted across all navigationsAfter form submission:
useNavigate to redirect the user to the homepage after the contact form is submittedInstall Tailwind and the Vite plugin:
npm install tailwindcss @tailwindcss/vite
Add the plugin to vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
});
Add the import to src/index.css:
@import "tailwindcss";
Then replace your existing CSS with Tailwind utility classes.
Requirements:
sm:, md:, lg:) instead of writing media queriesfocus-visible statesindex.css must define at least one custom colour token in an @theme block, used somewhere in the design:@import "tailwindcss";
@theme {
--color-brand: #your-chosen-colour;
}
Do not use arbitrary values (text-[17px], mt-[13px]) — use the built-in Tailwind scale.
Your dark mode toggle from weeks 4–5 still works the same way — it needs no changes to the logic. If you haven't already, move the toggle into your Navigation component so it appears on every page.
import { useState } from 'react';
export function ThemeToggle() {
const [dark, setDark] = useState(false);
function toggle() {
setDark(!dark);
document.documentElement.classList.toggle('dark');
}
return (
<button
onClick={toggle}
className="px-3 py-1 rounded-md text-sm font-medium bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
aria-label="Toggle dark mode"
>
{dark ? 'Light mode' : 'Dark mode'}
</button>
);
}
Add dark: variants to your components to make the toggle visually meaningful — at minimum the background and text colour of the page should change.
<aside> ⚠️
Warning
Tailwind's dark: prefix only works if the dark class is on the <html> element. The toggle above handles this via document.documentElement.classList.toggle('dark').
</aside>
Before deploying, verify the build succeeds on your machine:
npm run build
Fix any TypeScript errors or build failures before continuing. A build that passes locally will almost always pass on Vercel.
/, /projects, /contact)npm run build early. The first build often surfaces TypeScript errors the dev server was silently ignoring. Better to find them on your machine than after pushing to Vercel.App.tsx, not in each page. Put it outside <Routes> once and it appears everywhere automatically.NavLink instead of Link for navigation items so you can style the active route differently from the others.npm run build first, then push again./projects directly rather than navigating there) may return a 404. Fix this by adding a vercel.json file to your project root:{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
This tells Vercel to always serve index.html and let React Router handle the routing client-side.