Personal portfolio site for Lina Rawas, Full Stack Engineer and Founder of The Agile Labs. Built with a "Constructivist Control Room" design language — dark editorial, dense information architecture, intentional asymmetry, operational framing.
Live: portfolio.theagilelabs.com
| Layer | Technology |
|---|---|
| UI Framework | React 19 + TypeScript |
| Build Tool | Vite 7 |
| Styling | Tailwind CSS v4 (CSS-first config) |
| Animations | Framer Motion 12 |
| Routing | Wouter 3 |
| UI Primitives | shadcn/ui (new-york style) + Radix UI |
| Server | Express 4 (SPA host, production only) |
| Server Bundle | esbuild |
| Package Manager | pnpm |
| Node Requirement | ≥ 20 (project uses Vite 7 and Tailwind v4 Oxide engine) |
lina-portfolio/
├── client/
│ ├── index.html # Entry HTML — OG/Twitter meta, fonts, analytics
│ └── src/
│ ├── main.tsx # React root mount
│ ├── App.tsx # Router + providers (ThemeProvider, TooltipProvider)
│ ├── index.css # Tailwind v4 CSS-first config, design tokens, base styles
│ ├── lib/
│ │ └── portfolioData.ts # Single source of truth for all content
│ ├── pages/
│ │ ├── Home.tsx # Main portfolio page (all sections)
│ │ ├── TrxPage.tsx # TRX case study deep-dive
│ │ └── NotFound.tsx # 404 fallback
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives (button, dialog, etc.)
│ │ ├── ErrorBoundary.tsx
│ │ └── ...
│ ├── contexts/
│ │ └── ThemeContext.tsx # Dark-mode provider (default: dark)
│ └── hooks/
│ └── useMobile.tsx
├── server/
│ └── index.ts # Express static server for production SPA serving
├── package.json
├── vite.config.ts
├── tsconfig.json
└── patches/
└── wouter@3.7.1.patch # pnpm patch for wouter
| Path | Component | Title |
|---|---|---|
/ |
Home.tsx |
Lina Rawas — Full Stack Engineer & Founder |
/trx |
TrxPage.tsx |
TRX Case Study — Lina Rawas |
* |
NotFound.tsx |
— |
Document titles are set per-route via useEffect + document.title. TrxPage restores the previous title on unmount.
All content lives in client/src/lib/portfolioData.ts. This is the only file to edit when updating copy, projects, roles, or skills.
| Export | Used In | Description |
|---|---|---|
heroSubtitles |
Home hero |
Rotating subtitle lines |
navLinks |
Home nav |
Section anchor links |
aboutLayers |
Home about |
Four-layer stack summary (Product, Frontend, Backend, Infra) |
projects |
Home work |
All project entries (featured + supporting) |
featuredProject |
Home |
projects[0] — TRX |
supportingProjects |
Home |
projects[1–5] |
experiences |
Home experience |
Work history entries |
skillGroups |
Home skills |
Skill categories with primary/secondary distinction |
trxPainPoints |
TrxPage |
Problem statement cards |
trxDecisions |
TrxPage |
Architecture decision records |
trxResults |
TrxPage |
Outcome metrics |
trxStackLayers |
TrxPage |
Stack diagram layers |
contactLinks |
Home contact |
Email, LinkedIn, CV anchor |
siteMeta |
Home, index.html |
Title, description, domain |
{
category: string; // display name, e.g. "Frontend"
short: string; // orbit node label, e.g. "FE"
description: string; // shown in the detail panel when the node is active
skills: {
name: string;
primary: boolean; // true = amber pill, false = muted pill
}[];
}{
key: string;
name: string;
description: string;
type?: "featured"; // only set on TRX
badges?: string[]; // e.g. ["Production", "SaaS"]
metrics?: string[]; // e.g. ["3k+ txn/mo"]
stack?: string[]; // tech stack tags
actions?: {
label: string;
href: string;
kind: "primary" | "ghost";
external?: boolean;
}[];
backgroundImage?: string; // CloudFront URL for featured card
tags?: string[]; // used on supporting cards
visual?: string; // visual variant key for supporting card illustrations
repoUrl?: string;
liveUrl?: string;
collaboratorNote?: string;
}"Constructivist Control Room" — expose system structure through dense, intentional information blocks. Use asymmetry, editorial framing, and operational language. Every content grouping feels like a panel inside a living control room.
| Role | Value |
|---|---|
| Background | #0a0a0f |
| Primary accent | rgba(245, 158, 11) — amber |
| Secondary accent | rgba(99, 102, 241) — indigo |
| Muted text | slate-400 / slate-500 |
| Borders | white/8 – white/15 |
| Role | Font |
|---|---|
| Display / headings | Syne (Google Fonts) |
| Body / UI | DM Sans (Google Fonts) |
| Code / mono labels | JetBrains Mono (Google Fonts) |
/* Global flex min-size reset (prevents overflow shrink issues) */
.flex { min-width: 0; min-height: 0; }
/* Container */
/* mobile: 16px padding — 288px usable on 320px screen */
/* sm: 24px padding */
/* lg: max-width 1280px, 32px padding */Tailwind v4 is configured entirely in client/src/index.css — there is no tailwind.config.ts.
- Node.js ≥ 20 (Vite 7 and Tailwind v4 Oxide engine require it)
- pnpm (declared as package manager) — or npm/yarn also work
pnpm install
# or
npm installpnpm dev
# or
npm run devThe Vite dev server starts on http://localhost:5173 with --host (LAN accessible).
pnpm check
# or
npm run checkpnpm build
# or
npm run buildThis runs two steps in sequence:
vite build— bundles the React SPA intodist/public/esbuild server/index.ts— bundles the Express server intodist/index.js
pnpm start
# or
npm startStarts the Express server (NODE_ENV=production node dist/index.js). It serves dist/public as static files and returns index.html for all unmatched routes (SPA catch-all).
Default port: 3000 (override with PORT env var).
pnpm previewAll vars are optional — the site functions without them (some features degrade gracefully).
| Variable | Used In | Effect if unset |
|---|---|---|
VITE_ANALYTICS_ENDPOINT |
index.html Umami script |
Analytics not loaded |
VITE_ANALYTICS_WEBSITE_ID |
index.html Umami script |
Analytics not loaded |
PORT |
server/index.ts |
Defaults to 3000 |
Create a .env file at the project root for local overrides:
VITE_ANALYTICS_ENDPOINT=https://your-umami-instance.com/script.js
VITE_ANALYTICS_WEBSITE_ID=your-website-id
PORT=3000All sections are implemented as named sub-components inside Home.tsx.
| Section | Component | Notes |
|---|---|---|
| Navigation | NavBar |
Sticky, smooth-scroll anchors, mobile hamburger |
| Hero | HeroSection |
Rotating subtitles, animated badge, stats panel (desktop only) |
| About | AboutSection |
Four-layer stack grid |
| Work | WorkSection |
Featured TRX card + supporting project grid |
| Experience | ExperienceSection |
Desktop: horizontal scroll timeline with progress bar. Mobile: accordion |
| Skills | SkillsSection |
Desktop: interactive orbit (click nodes to expand). Mobile: accordion by category |
| Contact | ContactSection |
Email + LinkedIn links |
A full engineering case study covering:
- Problem — pain points in Lebanese water distribution (manual reconciliation, dual-currency complexity, field connectivity)
- Architecture decisions — exchange rate snapshotting, read-time aggregation, offline-first sync queue, partial indexing
- Stack — layered diagram from Testing down to Infrastructure
- Results — 3 hrs/day saved, $1,200/mo in salaries, 3,000+ txn/mo
All TRX content is driven by trxPainPoints, trxDecisions, trxResults, and trxStackLayers from portfolioData.ts.
Edit the relevant entry in the experiences array in portfolioData.ts. Fields: role, company, location, dates, symbol (2-letter badge), accent, bullets.
Add an entry to the projects array. Set type: "featured" only for the primary featured card (currently TRX). Supporting projects use tags and visual for their grid cards.
Edit skillGroups. Set primary: true on the 2–4 core skills per category (rendered as amber pills). Add a description sentence — it appears in the desktop orbit detail panel.
Edit trxPainPoints, trxDecisions, trxResults, or trxStackLayers arrays. No component changes needed.
- Node 18: The project runs with warnings on Node 18 but requires Node 20+ for a clean build. Upgrade Node before deploying to production.
- Private project links: Supporting projects (I-Stay, CarsLB, Alumni CMS, LMS, Gutenberg) have empty
repoUrlandliveUrl. They render a "Not public" badge by design. - Bundle size: The client bundle exceeds Vite's 500KB advisory due to Framer Motion and Recharts. This is expected — no action needed unless performance degrades.
- pnpm patch:
wouter@3.7.1has a pnpm patch inpatches/. If upgrading wouter, verify or remove the patch.