A bilingual (PT/EN) developer portfolio built with React, Express, and MongoDB. Features a public-facing site with animated pages, dark/light theming, and a protected admin dashboard for managing projects, blog posts, and experience entries — all with drag-and-drop reordering.
This is a template. The placeholder data belongs to the fictional developer "Jon Doe". Fork it, replace the content, and ship your own portfolio.
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite, TypeScript, Tailwind CSS 4, React Router 7 |
| Backend | Express 5, TypeScript, MongoDB/Mongoose 9 |
| Auth | JWT in HttpOnly cookies (SameSite: none; Secure) |
| Validation | Zod schemas on all mutating routes |
| Admin UI extras | @dnd-kit (drag-and-drop), EasyMDE (markdown editor) |
portfolio/
├── frontend/ # React SPA (Vite)
│ ├── public/
│ │ ├── images/ # Hero image and project images
│ │ ├── icons/ # Skill/tech icons (SVG)
│ │ ├── documents/ # CV PDF files
│ │ └── favicon.svg
│ └── src/
│ ├── pages/ # Route-level components (Home, About, CV, Blog, Portfolio)
│ │ └── admin/ # Protected CRUD editors
│ ├── components/ # Shared UI (Navbar, Footer, Projects, …)
│ ├── layouts/ # PublicLayout and AdminLayout wrappers
│ ├── context/ # AppContext — theme (dark/light) and language (pt/en)
│ ├── translations.ts # All static UI strings in both languages
│ └── utils/ # Axios interceptors, progress bar
│
├── backend/ # Express API
│ └── src/
│ ├── models/ # Mongoose schemas (User, Project, BlogPost, Experience)
│ ├── controllers/ # Business logic per resource
│ ├── routes/ # Express routers
│ ├── middleware/ # JWT auth, Zod validation wrapper
│ └── schemas/ # Zod request body schemas
│
├── createAdmin.txt # Script to create the admin user (see below)
├── Deployment.txt # Lessons learned from real deployments
└── LICENSE
After cloning, make these changes to replace the Jon Doe placeholder with your own information.
This file contains all bilingual strings shown on the public pages. Edit both the pt and en objects to match your background:
home.hero.role— your job title or student descriptionhome.hero.tagline— one-sentence pitchabout.p1/about.p1_span— your bio paragraph (supports a{span}highlight)about.p2/about.p2_span— your team or organisation paragraphabout.values— three value cards describing your work stylefooter.desc— footer taglinefooter.rights— copyright line (update the name)
- Name: Change
JONandDOEin the hero<h1>tags to your own name. - Hero image: Replace
frontend/public/images/YourPhoto.jpgwith your own photo (the stub file is already there — just overwrite it). - Backend tools array (
backendTools): Update tool names, descriptions, and icon paths to your actual stack. - Frontend tools array (
codingTools): Same — update to your stack. - Stats section: Edit the three stat cards to reflect your own numbers/jokes.
- CTA email: Change
contact@jondoe.devin the<a href="mailto:...">to your email.
- Name: Change
JON<br/>DOEto your name. - Email: Change
contact@jondoe.dev. - GitHub URL: Change
https://github.com/jondoe-dev. - LinkedIn URL: Change
https://www.linkedin.com/in/jon-doe-dev/. - Current focus label: Edit
BUILDING SCALABLE WEB SYSTEMS. - Location / coordinates: Change
Austin, TXand the GPS coordinates to your own city.
- Name: Change
Jon Doein the<span>inside the logo link. - Logo images: Replace
frontend/public/YourLogo_Light.svg(shown on dark backgrounds) andYourLogo_Dark.svg(shown on light backgrounds) with your own logo. Both stubs are already there — overwrite them.
All content here is hardcoded (not in translations.ts). Edit the bilingual strings inline:
- Professional summary
- Core competencies
- Education section (degree, university, GPA, honours)
- CV download links — overwrite the stub files
frontend/public/documents/YourCV_PT.pdfandYourCV_EN.pdfwith your actual PDFs.
The technical stack grid is hardcoded. Find the array near line 83 and update the four categories and their items to your own skills.
The Professional Journey timeline is fetched from MongoDB via the Experience model — add your entries through the admin dashboard at /admin/experience once the app is running.
Add your own .svg icon files here. Reference them in Home.tsx and About.tsx as /icons/your-tool.svg. Broken icon paths fall back gracefully to a placeholder rectangle.
frontend/index.html— change<title>Jon Doe</title>and update<link rel="icon">if needed.
The following placeholder files ship in frontend/public/. Replace each one with your own content — the filenames must stay the same since they are referenced directly in the code.
| File | What to put there |
|---|---|
public/images/YourPhoto.jpg |
Your hero/profile photo. Used as the full-bleed background on the Home page. Recommended minimum: 1920×1080. |
public/documents/YourCV_EN.pdf |
Your CV in PDF format — English version. |
public/documents/YourCV_PT.pdf |
Your CV in PDF format — Portuguese version (or a second language of your choice). |
public/YourLogo_Light.svg |
Your logo — light variant (white/light colours, shown on dark backgrounds). Referenced in Navbar.tsx. |
public/YourLogo_Dark.svg |
Your logo — dark variant (dark colours, shown on light backgrounds). Referenced in Navbar.tsx. |
The current files are SVG/text stubs so the repo stays clean. Overwrite them with your real files.
Update the Copyright (c) 2026 Jon Doe line to your name and year.
- Go to mongodb.com/atlas and create a free account.
- Create a new free (M0) cluster — choose any cloud region.
- In Database Access, create a database user with a username and strong password.
- In Network Access, add
0.0.0.0/0(allows all IPs — fine for a portfolio; tighten in production if needed). - Click Connect → Drivers and copy the connection string. It looks like:
mongodb+srv://<user>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority
- Append your database name before the
?:
mongodb+srv://<user>:<password>@cluster0.xxxxx.mongodb.net/portfolio?retryWrites=true&w=majority
You do not need to create collections manually. Mongoose creates them automatically on first write. The four collections used are: users, projects, blogposts, experiences.
The file createAdmin.txt in the repo root contains a TypeScript script that creates the admin account used to log into /admin. You need to run it once after setting up your database.
- Copy the script into the backend source folder:
cp createAdmin.txt backend/src/createAdmin.ts- Add the admin credentials to
backend/.env(they are read by the script, never hardcoded):
ADMIN_EMAIL=your@email.com
ADMIN_PASSWORD=a_very_strong_password
- Run the script from inside the
backendfolder:
cd backend
npx ts-node src/createAdmin.ts-
You should see:
✅ Admin user created successfully! -
Delete or gitignore
backend/src/createAdmin.tsafter running it — it's a one-shot utility and should not live in the repo permanently.
You can now log in at /login with those credentials.
- Node.js 18 or later
- npm 9 or later
- A MongoDB Atlas connection string (see above)
# Install backend dependencies
cd backend && npm install
# Install frontend dependencies
cd ../frontend && npm installbackend/.env (copy from backend/.env.example):
MONGODB_URI=your_atlas_connection_string
JWT_SECRET=a_random_64_character_string
FRONTEND_URL=http://localhost:5173
NODE_ENV=development
PORT=5000
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your_admin_password
Generate a strong JWT secret with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"frontend/.env (copy from frontend/.env.example):
VITE_API_URL=http://localhost:5000
Run in two separate terminals:
# Terminal 1 — backend (http://localhost:5000)
cd backend && npm run dev
# Terminal 2 — frontend (http://localhost:5173)
cd frontend && npm run devVisit http://localhost:5173 to see the site and http://localhost:5173/login to access the admin dashboard.
- Push the repo to GitHub.
- Go to vercel.com → Add New Project → import your repo.
- Set the Root Directory to
frontend. - Vercel auto-detects Vite — leave the framework preset as Vite.
- Add the environment variable:
VITE_API_URL=https://your-backend.onrender.com(your Render URL, no trailing slash)
- Deploy.
Gotcha — SPA routing: The frontend/vercel.json already includes the required rewrite rule that sends all paths to index.html. Without it, direct navigation to /admin or /login returns a 404. Do not delete this file.
Gotcha — devDependencies: If the build fails with missing @types/* packages, move them from devDependencies to dependencies in frontend/package.json. Vercel's build environment sometimes skips dev deps.
- Go to render.com → New → Web Service → connect your GitHub repo.
- Set:
- Root Directory:
backend - Build command:
npm install && npm run build - Start command:
npm start - Node version: 18 or later (set in the Environment tab)
- Root Directory:
- Add environment variables (Settings → Environment):
MONGODB_URIJWT_SECRETFRONTEND_URL= your Vercel deployment URL (no trailing slash, e.g.https://your-portfolio.vercel.app)NODE_ENV=productionPORT=5000
- Deploy. Render will build and start the API automatically.
Gotcha — CORS credentials: Cookies only work when credentials: true is set in the backend CORS config AND credentials: 'include' is set on every Axios request. Both are already configured in this repo via utils/progress.ts. If you see auth failures in production, double-check these.
Gotcha — cross-site cookies: The frontend and backend are on different domains, so the auth cookie is set with SameSite: none; Secure: true. This requires HTTPS — which Render and Vercel both provide. Locally, the Secure flag is relaxed when NODE_ENV=development. If cookies work locally but not in production, this is almost certainly the issue.
Gotcha — ENV variable formatting: A trailing slash on FRONTEND_URL or VITE_API_URL will silently break all API requests. Always double-check: no trailing slash, correct protocol (https://), and the exact variable name.
Once deployed, visit /login with the credentials you set up via createAdmin.ts.
The dashboard at /admin lets you:
- Projects — create, edit, delete, and drag-and-drop reorder. Each project has bilingual title, description, full description, features, challenges, image URL, repo URL, live URL, and tags.
- Blog — write posts in Markdown using the embedded editor. Supports bilingual title, excerpt, and content.
- Experience — manage the professional journey timeline entries shown on the About page.
JWT_SECRETmust be at least 64 characters and never committed to git. The.gitignorealready excludes.envfiles.- Login is rate-limited to 5 attempts per 15 minutes.
- All request bodies on mutating routes are validated with Zod before hitting the database.
- MongoDB injection is prevented via
mongo-sanitizemiddleware. - HTTP headers are hardened with
helmet.
MIT — see LICENSE.