A minimal, terminal-inspired portfolio website built with Next.js 16, React 19, Tailwind CSS 4, and Framer Motion. Features real-time Discord presence via Lanyard, a cryptographic text reveal effect, an interactive 3D tile grid background, and dark/light theme support.
- Decrypt Text Effect — Characters scramble and reveal themselves letter-by-letter on mount, with interactive click-to-scale micro-animations.
- 3D Tile Grid Background — A procedurally generated grid that reacts to cursor and touch movement with 3D perspective rotation and proximity-based glow.
- Real-time Discord Activity — Shows current Spotify playback and gaming activity via Lanyard WebSocket, with "last played" fallback.
- Dynamic GitHub Projects — Fetches and displays your latest repos with language, stars, forks, and live-link indicators.
- Hero Slider — Scroll or swipe between intro, contact, collaboration, and activity slides.
- Dark and Light Themes — Instant toggle with localStorage persistence and flash-free initial load.
- Custom Cursor — Smooth dot-and-ring cursor with hover and press states on pointer-fine devices.
- Animated 404 Page — Pixel-art digit grid with auto-redirect countdown.
- Accessibility — Respects
prefers-reduced-motion, keyboard navigation with visible focus rings, and semantic ARIA attributes.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI Library | React 19 |
| Styling | Tailwind CSS 4 |
| Animations | Framer Motion |
| Presence | Lanyard API (WebSocket + REST) |
| Language | TypeScript |
# Clone the repository
git clone https://github.com/westopia/portfolio.git
cd portfolio
# Install dependencies
npm install
# Start the development server
npm run devThe site will be available at http://localhost:3000.
Create a .env.local file in the project root:
# Required — Your Discord user ID for Lanyard presence
NEXT_PUBLIC_DISCORD_ID=your_discord_user_id
# Optional — Lanyard API key for persisting "last played" data to Lanyard KV.
# If not set, the site falls back to client-side localStorage caching.
LANYARD_API_KEY=your_lanyard_api_key
# Optional — Comma-separated origins allowed during local network dev testing.
# Add your machine's local IP with and without the port so your phone can connect.
# Example: ALLOWED_DEV_ORIGINS=192.168.1.5,192.168.1.5:3000
ALLOWED_DEV_ORIGINS=How to get your Discord ID: Enable Developer Mode in Discord settings, then right-click your profile and select "Copy User ID".
Lanyard API Key: Only needed if you want the "last played" data to persist server-side across all visitors. You can get it from the Lanyard dashboard after adding the Lanyard bot to your server. Without it, everything still works — it just uses localStorage per browser.
Testing on mobile: To test on your phone over your local network, find your machine's local IP (run ipconfig on Windows or ifconfig on macOS/Linux), then set ALLOWED_DEV_ORIGINS to both the IP and IP with port, separated by a comma (e.g. 192.168.1.5,192.168.1.5:3000). Both are needed because browsers include the port in the origin header. Then open http://<your-ip>:3000 on your phone.
This portfolio is designed to be forked and personalized. Here is what to change:
Edit app/components/hero-slider.tsx and replace the name, tagline, email, and social links in the slide data.
Edit app/page.tsx and change the GitHub username in getGithubAvatar("westopia").
Edit app/layout.tsx and update metadata.title and metadata.description for SEO.
Edit app/components/projects-list.tsx and change the GitHub username in getGithubProjects("westopia"). You can also adjust LANGUAGE_COLORS to add any languages you use.
Set NEXT_PUBLIC_DISCORD_ID in your .env.local to your own Discord user ID.
Join Lanyard's Discord server so the bot can track your presence.
Edit app/globals.css and modify CSS custom properties in the :root (dark) and html.theme-light blocks. Key variables: --background, --foreground, --accent, --muted, --dim, --line.
Edit app/layout.tsx and replace Geist / Geist_Mono with any Google Font using the next/font module.
app/
├── api/
│ └── activity/
│ └── route.ts API route — syncs activity to Lanyard KV
├── components/
│ ├── activity-slide.tsx Spotify and game activity display
│ ├── app-listeners.tsx Global keyboard and context-menu listeners
│ ├── custom-cursor.tsx Dot-and-ring cursor with lerp animation
│ ├── decrypt-text.tsx Scramble-reveal text effect
│ ├── hero-slider.tsx Main hero section with scroll/swipe slides
│ ├── projects-list.tsx GitHub repos fetched server-side
│ ├── theme-toggle.tsx Dark/light mode toggle
│ └── tile-grid.tsx 3D interactive background grid
├── hooks/
│ └── use-lanyard.ts WebSocket hook for real-time Discord presence
├── types/
│ └── lanyard.ts TypeScript interfaces for Lanyard data
├── globals.css Theme variables, animations, base styles
├── layout.tsx Root layout with theme initializer
├── not-found.tsx Animated 404 page
└── page.tsx Home page
| Command | Description |
|---|---|
npm run dev |
Start development server with hot reload |
npm run build |
Create production build |
npm run start |
Serve the production build |
npm run lint |
Run ESLint |
This project is optimized for Vercel:
- Push your repo to GitHub.
- Import the repository on vercel.com/new.
- Set your environment variables (
NEXT_PUBLIC_DISCORD_ID, optionallyLANYARD_API_KEY). - Deploy — Vercel auto-detects Next.js.
You can also deploy anywhere that supports Node.js:
npm run build
npm run startThis project is open source and available under the MIT License.
Built by Westopia