A premium, cinematic personal portfolio for Elham Rafat Mahmud, a creative multimedia professional bridging design and technology. Built with Next.js 15 (App Router), TypeScript, Tailwind CSS v4, Framer Motion, GSAP, and Lenis.
Design direction: minimal, typography-driven, dark-first with an electric violet → teal accent gradient — inspired by the cinematic feel of sites like toruu.me, but pushed further with glassmorphism, horizontal scroll-jacked project rail, a custom cursor, and per-character text reveals.
- Cinematic hero — per-character animated name reveal, animated gradient rule, ambient gradient-mesh background with floating multimedia icons.
- Smooth scrolling powered by Lenis,
synced with GSAP's
ScrollTrigger. - Horizontal-scroll project rail — vertical page scroll drives a horizontal GSAP-pinned carousel of featured work (desktop); becomes a native swipeable list on mobile/touch.
- Custom cursor with a glassmorphic "lens" trail that reacts to interactive elements (disabled on touch devices & for reduced-motion users).
- Glassmorphic cards with cursor-reactive glow, used across Services, Skills, and elsewhere.
- 3D tilt + image zoom on project cards.
- Dark/light theme toggle, persisted to
localStorage. - Infinite marquee of skills/tools (pure CSS, GPU-cheap, pauses on hover).
- Fully responsive, mobile-first, down to small phones.
- Accessible: visible focus states,
prefers-reduced-motionrespected globally and in every custom animation, semantic landmarks, alt/aria labels. - SEO-ready: full metadata, Open Graph/Twitter cards, auto-generated
sitemap.xmlandrobots.txt. - Performance-conscious: static generation, font
display: swap,next/image-ready config, minimal client JS where possible.
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS v4 (CSS-variable theme tokens) |
| Animation | Framer Motion + GSAP (ScrollTrigger) |
| Smooth scroll | Lenis |
| Icons | lucide-react |
src/
├── app/
│ ├── layout.tsx # Root layout: fonts, metadata, providers
│ ├── page.tsx # Home page — composes all sections
│ ├── globals.css # Design tokens (CSS vars), base styles, marquee keyframes
│ ├── not-found.tsx # Custom 404
│ ├── sitemap.ts # Auto-generated /sitemap.xml
│ └── robots.ts # Auto-generated /robots.txt
│
├── components/
│ ├── layout/
│ │ ├── navbar.tsx # Fixed nav, glass-on-scroll, mobile menu
│ │ ├── footer.tsx # Closing strip + socials
│ │ ├── cursor.tsx # Custom cursor
│ │ ├── theme-provider.tsx # Dark/light context + localStorage
│ │ ├── theme-toggle.tsx # Sun/moon toggle button
│ │ └── smooth-scroll-provider.tsx # Lenis + GSAP ScrollTrigger sync
│ │
│ ├── sections/
│ │ ├── hero.tsx # 1. Hero
│ │ ├── about.tsx # 2. About
│ │ ├── services.tsx # 3. Services / Expertise
│ │ ├── projects.tsx # 4. Featured Projects (horizontal scroll rail)
│ │ ├── project-card.tsx # Single project card (tilt + zoom)
│ │ ├── skills.tsx # 5. Skills & Tools (marquee + grid)
│ │ └── contact.tsx # 6. Contact form + socials
│ │
│ └── ui/
│ ├── split-text.tsx # Per-character/word stagger reveal
│ ├── magnetic-button.tsx # Cursor-following CTA buttons
│ ├── glass-card.tsx # Reusable glassmorphic panel
│ ├── animated-section.tsx # Scroll-triggered fade/rise wrapper
│ ├── profile-photo.tsx # Profile photo with graceful fallback
│ ├── gradient-mesh.tsx # Hero ambient background
│ └── noise-overlay.tsx # Subtle film-grain texture
│
└── lib/
├── data.ts # ALL content lives here: profile, bio, services, projects, skills, socials, nav
└── utils.ts # cn(), clamp(), lerp(), mapRange()
public/
├── images/
│ ├── profile/ # Your profile photo goes here (see README inside)
│ └── projects/ # Real project screenshots go here (see README inside)
└── fonts/ # Custom font files, if you swap in a licensed display font
Why this structure? Content (lib/data.ts) is fully separated from
presentation. To update copy, swap a project, add a skill, or change a
social link, you only ever need to edit src/lib/data.ts — no JSX hunting
required.
# install dependencies
npm install
# run the dev server
npm run dev
# open http://localhost:3000# production build
npm run build
npm run startNote on
lucide-reactversion: this project pinslucide-reactto0.383.0inpackage.json. Versions ≥1.0 removed brand/logo icons (GitHub, LinkedIn, Facebook, etc.) from the package; pinning keeps the social icons working. Because that version predates React 19's official peer-dependency range, an.npmrcwithlegacy-peer-deps=trueis included sonpm installworks without extra flags. If you upgrade lucide-react later, swap those icons for inline SVGs or another icon set and feel free to remove.npmrc.
This Next.js 15 project is fully configured and ready for seamless deployment on Vercel.
- Push your code to a Git repository (GitHub, GitLab, or Bitbucket).
- Go to Vercel Dashboard and click "Add New Project".
- Import your repository.
- Vercel will automatically detect Next.js framework preset and default build settings:
- Framework Preset:
Next.js - Build Command:
npm run build(ornext build) - Output Directory:
.next - Install Command:
npm install(utilizes included.npmrc)
- Framework Preset:
- Click "Deploy".
# Install Vercel CLI globally (if not already installed)
npm i -g vercel
# Log in and deploy preview environment
vercel
# Deploy to production
vercel --prodEverything — name, bio, education, services, projects, skills, social links,
nav items — lives in src/lib/data.ts. Start there.
The About section already has a photo slot built in — you don't need to edit any component code.
- Drop your photo into
public/images/profile/, namedprofile.jpg(see the README inside that folder for size recommendations). - That's it. It picks up automatically via
profile.photoindata.ts.
If you use a different filename or extension (.png, .webp), just update
the photo path in data.ts to match. Until a real photo is added — or if
the file fails to load for any reason — the slot gracefully falls back to a
gradient + your initials, so nothing ever shows a broken image icon.
Project cards (src/components/sections/project-card.tsx) render a real
screenshot automatically whenever a project has an image field — and fall
back to a generated gradient cover otherwise. To add a screenshot:
- Drop the image into
public/images/projects/(see the README inside that folder). - Add an
imagefield to that project's entry indata.ts:{ id: "cv-site", // ...existing fields... image: "/images/projects/cv-site.jpg", }
You can do this one project at a time — any project without an image
field simply keeps its gradient cover, so nothing breaks in the meantime.
The display face is currently Inter Tight (a free Google Font close in spirit to premium geometric sans faces). If you have a license for Clash Display, Satoshi, or similar:
- Drop the font files (
.woff2) intopublic/fonts/. - In
src/app/layout.tsx, replace theInter_Tightimport withnext/font/local:import localFont from "next/font/local"; const display = localFont({ src: "../../public/fonts/ClashDisplay-Variable.woff2", variable: "--font-inter-tight", // keep the same CSS var name });
All colors are CSS custom properties defined in src/app/globals.css under
:root (dark, default) and [data-theme="light"] (light mode). Change
--color-accent and --color-accent-2 to retint the entire site's gradient
and highlight colors in one place.
The form in src/components/sections/contact.tsx is currently a front-end
placeholder (it simulates a send with setTimeout). To make it functional,
either:
- Form service (fastest): point the form at
Formspree, Web3Forms,
or similar — swap the
handleSubmitbody for afetch()POST to their endpoint. - Your own API route: create
src/app/api/contact/route.tsusing Resend or Nodemailer, andfetch("/api/contact", { method: "POST", body: JSON.stringify(form) })fromhandleSubmit.
- All custom animations (cursor, parallax, marquee, GSAP scroll-jacking)
check
prefers-reduced-motionand degrade to static/instant behavior. - The custom cursor is disabled entirely on coarse-pointer (touch) devices.
- Focus states use a visible 2px outline in the secondary accent color — never removed, only restyled.
- Semantic landmarks (
<header>,<main>,<footer>,<nav>) andaria-labels are used throughout interactive icon-only buttons. - Fonts use
display: swapto avoid invisible-text flashes. - The horizontal project rail falls back to native scroll on touch/mobile rather than forcing scroll-jacking on a context where it feels wrong.
This project was generated as a personal portfolio for Elham Rafat Mahmud. Feel free to fork and adapt the structure/components for your own portfolio.