diff --git a/.github/workflows/portfolio-v2-ci.yml b/.github/workflows/portfolio-v2-ci.yml
index 37b13cb..17ca08b 100644
--- a/.github/workflows/portfolio-v2-ci.yml
+++ b/.github/workflows/portfolio-v2-ci.yml
@@ -3,15 +3,12 @@ name: Portfolio V2 CI
on:
pull_request:
branches: ["main"]
- paths:
- - "portfolio-v2/**"
- - ".github/workflows/portfolio-v2-ci.yml"
- - ".github/workflows/portfolio-project-sync.yml"
- - ".github/prompts/**"
workflow_dispatch:
permissions:
- contents: read
+ contents: write
+ issues: write
+ pull-requests: write
jobs:
build:
@@ -20,6 +17,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
+ with:
+ repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -28,8 +28,77 @@ jobs:
cache: npm
cache-dependency-path: portfolio-v2/package-lock.json
- - name: Build Portfolio V2
+ - name: Install Dependencies
+ working-directory: portfolio-v2
+ run: npm ci
+
+ - name: Format Code
+ working-directory: portfolio-v2
+ run: npm run format
+
+ - name: Project Health and Clean up
+ id: project_health
+ working-directory: portfolio-v2
+ run: |
+ npm run project:health:fix
+ cat project-health-summary.md >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Commit Automated Cleanups
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
+ run: |
+ set -euo pipefail
+
+ if git diff --quiet; then
+ echo "No automated formatting or project cleanup changes were needed." >> "$GITHUB_STEP_SUMMARY"
+ exit 0
+ fi
+
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add -A
+ git commit -m "Apply automated portfolio cleanup"
+ git push
+ echo "Committed automated formatting or project cleanup changes to this PR branch." >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Check Code Formatting
working-directory: portfolio-v2
+ run: npm run format:check
+
+ - name: Comment Project Health Review
+ if: always() && github.event_name == 'pull_request' && steps.project_health.outputs.status == 'needs_review'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
- npm ci
- npm run build
+ set -euo pipefail
+
+ MARKER=""
+ COMMENT_ID="$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
+ --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" | tail -n 1)"
+
+ COMMENT_FILE="$RUNNER_TEMP/project-health-comment.md"
+ {
+ echo "$MARKER"
+ if [ -f portfolio-v2/project-health-summary.md ]; then
+ cat portfolio-v2/project-health-summary.md
+ else
+ echo "Project Health and Clean up did not produce a summary. Please inspect the workflow logs."
+ fi
+ } > "$COMMENT_FILE"
+
+ if [ -n "$COMMENT_ID" ]; then
+ gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \
+ --field "body=$(cat "$COMMENT_FILE")"
+ else
+ gh pr comment "$PR_NUMBER" --body-file "$COMMENT_FILE"
+ fi
+
+ - name: Fail on Project Health Issues
+ if: steps.project_health.outputs.status == 'needs_review'
+ run: |
+ echo "Project Health and Clean up found issues. See the workflow summary or PR comment."
+ exit 1
+
+ - name: Build Portfolio V2
+ working-directory: portfolio-v2
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index 74baf38..16f38ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
linkedin-proof-assets/
+portfolio-v2/node_modules/
+portfolio-v2/dist/
+portfolio-v2/project-health-summary.md
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..24dc651
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,4 @@
+portfolio-v2/dist/
+portfolio-v2/node_modules/
+portfolio-v2/project-health-summary.md
+linkedin-proof-assets/
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..c2ba36e
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "printWidth": 120,
+ "semi": true,
+ "singleQuote": false,
+ "trailingComma": "es5"
+}
diff --git a/portfolio-v2/index.html b/portfolio-v2/index.html
index c83eba6..f34022b 100644
--- a/portfolio-v2/index.html
+++ b/portfolio-v2/index.html
@@ -16,7 +16,7 @@
-
+
Vikram Sharma | Python Backend Engineer
diff --git a/portfolio-v2/package-lock.json b/portfolio-v2/package-lock.json
index f70658e..f5c0b43 100644
--- a/portfolio-v2/package-lock.json
+++ b/portfolio-v2/package-lock.json
@@ -14,6 +14,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
+ "prettier": "^3.8.3",
"vite": "^5.4.11"
}
},
@@ -1478,6 +1479,22 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prettier": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
diff --git a/portfolio-v2/package.json b/portfolio-v2/package.json
index 85082ae..97c5c5f 100644
--- a/portfolio-v2/package.json
+++ b/portfolio-v2/package.json
@@ -6,7 +6,11 @@
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "vite build",
- "preview": "vite preview --host 127.0.0.1"
+ "preview": "vite preview --host 127.0.0.1",
+ "format": "prettier --write --config ../.prettierrc.json --ignore-path ../.prettierignore \"src/**/*.{js,jsx,json,css}\" \"scripts/**/*.mjs\" \"index.html\" \"package.json\" \"docs/**/*.md\" \"../README.md\" \"../.github/**/*.{yml,yaml}\"",
+ "format:check": "prettier --check --config ../.prettierrc.json --ignore-path ../.prettierignore \"src/**/*.{js,jsx,json,css}\" \"scripts/**/*.mjs\" \"index.html\" \"package.json\" \"docs/**/*.md\" \"../README.md\" \"../.github/**/*.{yml,yaml}\"",
+ "project:health": "node scripts/checkProjectHealth.mjs",
+ "project:health:fix": "node scripts/checkProjectHealth.mjs --fix"
},
"dependencies": {
"lucide-react": "^0.468.0",
@@ -15,6 +19,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
+ "prettier": "^3.8.3",
"vite": "^5.4.11"
}
}
diff --git a/portfolio-v2/scripts/applyPortfolioProjectUpdate.mjs b/portfolio-v2/scripts/applyPortfolioProjectUpdate.mjs
index 1e0c174..613f7bc 100644
--- a/portfolio-v2/scripts/applyPortfolioProjectUpdate.mjs
+++ b/portfolio-v2/scripts/applyPortfolioProjectUpdate.mjs
@@ -200,7 +200,9 @@ function labelForLink(href) {
}
function clean(value) {
- return String(value || "").replace(/\s+/g, " ").trim();
+ return String(value || "")
+ .replace(/\s+/g, " ")
+ .trim();
}
function findExistingProject(projects, matchTitle, project) {
diff --git a/portfolio-v2/scripts/checkProjectHealth.mjs b/portfolio-v2/scripts/checkProjectHealth.mjs
new file mode 100644
index 0000000..dda1837
--- /dev/null
+++ b/portfolio-v2/scripts/checkProjectHealth.mjs
@@ -0,0 +1,369 @@
+import { existsSync, readFileSync, writeFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const publicDir = resolve(__dirname, "../public");
+const projectsDataPath = resolve(__dirname, "../src/projects.json");
+const summaryPath = resolve(__dirname, "../project-health-summary.md");
+const shouldFix = process.argv.includes("--fix");
+const skipNetwork = process.argv.includes("--skip-network");
+const timeoutMs = Number(process.env.PROJECT_HEALTH_TIMEOUT_MS || 12000);
+const userAgent = "Mozilla/5.0 (compatible; PortfolioProjectHealth/1.0; +https://github.com/vikramsh2002/My-Portfolio)";
+
+const projects = JSON.parse(readFileSync(projectsDataPath, "utf8"));
+const fixes = [];
+const reviewNotes = [];
+const warnings = [];
+const checkedLinks = [];
+let changed = false;
+
+if (!Array.isArray(projects)) {
+ throw new Error("projects.json must contain a project array.");
+}
+
+const updatedProjects = [];
+
+for (const [projectIndex, rawProject] of projects.entries()) {
+ const project = normalizeProject(rawProject);
+ const projectName = project.title || `Project ${projectIndex + 1}`;
+
+ if (JSON.stringify(project) !== JSON.stringify(rawProject)) {
+ changed = true;
+ fixes.push(`Normalized spacing, tags, or link metadata for "${projectName}".`);
+ }
+
+ validateRequiredText(project, projectName);
+ validateImage(project, projectName);
+
+ const linkResult = await cleanAndCheckLinks(project, projectName);
+ project.links = linkResult.links;
+
+ if (!project.links.length) {
+ reviewNotes.push(`"${projectName}" has no usable project links after cleanup.`);
+ }
+
+ updatedProjects.push(project);
+}
+
+if (!shouldFix && changed) {
+ reviewNotes.push(
+ "Project data has automatic cleanup suggestions. Run `npm run project:health:fix` and commit the result."
+ );
+}
+
+if (shouldFix && changed) {
+ writeFileSync(projectsDataPath, `${JSON.stringify(updatedProjects, null, 2)}\n`, "utf8");
+}
+
+const status = reviewNotes.length ? "needs_review" : changed ? "fixed" : "clean";
+writeSummary({ status });
+writeOutput("status", status);
+writeOutput("changed", String(Boolean(shouldFix && changed)));
+writeOutput("fix_count", String(fixes.length));
+writeOutput("review_count", String(reviewNotes.length));
+writeOutput("warning_count", String(warnings.length));
+writeOutput("summary_path", summaryPath);
+
+function normalizeProject(project) {
+ const normalized = {
+ ...project,
+ title: clean(project.title),
+ subtitle: clean(project.subtitle),
+ image: clean(project.image),
+ description: clean(project.description),
+ impact: clean(project.impact),
+ tags: normalizeTags(project.tags),
+ links: normalizeLinks(project.links),
+ };
+
+ if (Object.hasOwn(project, "featured")) {
+ normalized.featured = Boolean(project.featured);
+ }
+
+ return normalized;
+}
+
+function validateRequiredText(project, projectName) {
+ for (const field of ["title", "subtitle", "description", "impact"]) {
+ if (!project[field]) {
+ reviewNotes.push(`"${projectName}" is missing required field: ${field}.`);
+ }
+ }
+
+ if (!project.tags.length) {
+ reviewNotes.push(`"${projectName}" is missing tags.`);
+ }
+}
+
+function validateImage(project, projectName) {
+ if (!project.image) {
+ reviewNotes.push(`"${projectName}" is missing a project image.`);
+ return;
+ }
+
+ if (/^https?:\/\//i.test(project.image)) {
+ reviewNotes.push(`"${projectName}" uses a remote image. Move it into public/images/Projects for stable builds.`);
+ return;
+ }
+
+ const imagePath = resolve(publicDir, project.image);
+ if (!existsSync(imagePath)) {
+ reviewNotes.push(`"${projectName}" references a missing image: ${project.image}.`);
+ }
+}
+
+async function cleanAndCheckLinks(project, projectName) {
+ const seenLinks = new Set();
+ const keptLinks = [];
+
+ for (const link of project.links) {
+ const normalizedHref = normalizeHref(link.href);
+
+ if (!normalizedHref) {
+ changed = true;
+ fixes.push(`Removed an empty link from "${projectName}".`);
+ continue;
+ }
+
+ if (seenLinks.has(normalizedHref)) {
+ changed = true;
+ fixes.push(`Removed duplicate "${link.label}" link from "${projectName}".`);
+ continue;
+ }
+
+ seenLinks.add(normalizedHref);
+
+ if (!/^https?:\/\//i.test(link.href)) {
+ reviewNotes.push(`"${projectName}" has a non-web project link that needs review: ${link.href}.`);
+ keptLinks.push(link);
+ continue;
+ }
+
+ if (!skipNetwork) {
+ const check = await checkUrl(link.href);
+ checkedLinks.push({ projectName, label: link.label, href: link.href, ...check });
+
+ if (check.status === "dead") {
+ const remainingLinkCount = project.links.filter((candidate) => normalizeHref(candidate.href)).length - 1;
+ const importantSourceLink = isSourceLink(link);
+
+ if (shouldFix && !importantSourceLink && remainingLinkCount > 0) {
+ changed = true;
+ fixes.push(`Removed dead "${link.label}" link from "${projectName}": ${link.href}`);
+ continue;
+ }
+
+ reviewNotes.push(
+ `"${projectName}" has a dead ${importantSourceLink ? "source" : "project"} link that needs review: ${link.href} (${check.reason}).`
+ );
+ } else if (check.status === "blocked") {
+ warnings.push(
+ `"${projectName}" ${link.label} link could not be fully checked in this environment: ${link.href} (${check.reason}).`
+ );
+ } else if (check.status === "transient") {
+ warnings.push(
+ `"${projectName}" ${link.label} link had a transient check issue: ${link.href} (${check.reason}).`
+ );
+ }
+ }
+
+ keptLinks.push(link);
+ }
+
+ return { links: keptLinks };
+}
+
+async function checkUrl(url) {
+ const head = await requestUrl(url, "HEAD");
+
+ if (head.kind === "response" && isHealthyStatus(head.statusCode)) {
+ return { status: "ok", reason: `HTTP ${head.statusCode}` };
+ }
+
+ if (head.kind === "response" && shouldRetryWithGet(head.statusCode)) {
+ const get = await requestUrl(url, "GET");
+ return classifyProbe(get);
+ }
+
+ return classifyProbe(head);
+}
+
+async function requestUrl(url, method) {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
+
+ try {
+ const response = await fetch(url, {
+ method,
+ redirect: "follow",
+ signal: controller.signal,
+ headers: {
+ "user-agent": userAgent,
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ },
+ });
+
+ return { kind: "response", statusCode: response.status };
+ } catch (error) {
+ return {
+ kind: "error",
+ code: error?.cause?.code || error?.code || error?.name || "ERROR",
+ message: error?.cause?.message || error?.message || String(error),
+ };
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+function classifyProbe(probe) {
+ if (probe.kind === "response") {
+ if (isHealthyStatus(probe.statusCode)) {
+ return { status: "ok", reason: `HTTP ${probe.statusCode}` };
+ }
+
+ if ([401, 403, 418, 429, 999].includes(probe.statusCode)) {
+ return { status: "blocked", reason: `HTTP ${probe.statusCode}` };
+ }
+
+ if ([404, 410].includes(probe.statusCode)) {
+ return { status: "dead", reason: `HTTP ${probe.statusCode}` };
+ }
+
+ if (probe.statusCode >= 500) {
+ return { status: "transient", reason: `HTTP ${probe.statusCode}` };
+ }
+
+ return { status: "blocked", reason: `HTTP ${probe.statusCode}` };
+ }
+
+ if (["ENOTFOUND", "ENODATA", "ECONNREFUSED"].includes(probe.code)) {
+ return { status: "dead", reason: probe.code };
+ }
+
+ if (["AbortError", "ETIMEDOUT", "ECONNRESET", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"].includes(probe.code)) {
+ return { status: "transient", reason: probe.code };
+ }
+
+ return { status: "blocked", reason: probe.code };
+}
+
+function isHealthyStatus(statusCode) {
+ return statusCode >= 200 && statusCode < 400;
+}
+
+function shouldRetryWithGet(statusCode) {
+ return [401, 403, 405, 418, 429, 999].includes(statusCode) || statusCode >= 500;
+}
+
+function normalizeTags(tags) {
+ if (!Array.isArray(tags)) {
+ return [];
+ }
+
+ const seen = new Set();
+ const normalized = [];
+
+ for (const tag of tags.map(clean).filter(Boolean)) {
+ const key = tag.toLowerCase();
+ if (!seen.has(key)) {
+ seen.add(key);
+ normalized.push(tag);
+ }
+ }
+
+ return normalized.slice(0, 8);
+}
+
+function normalizeLinks(links) {
+ if (!Array.isArray(links)) {
+ return [];
+ }
+
+ return links.map((link) => ({
+ label: clean(link?.label) || labelForLink(clean(link?.href)),
+ href: clean(link?.href),
+ }));
+}
+
+function labelForLink(href) {
+ if (!href) {
+ return "Link";
+ }
+
+ if (href.includes("github.com")) {
+ return "Source";
+ }
+
+ if (href.includes("linkedin.com")) {
+ return "Demo";
+ }
+
+ return "Live app";
+}
+
+function isSourceLink(link) {
+ return link.label.toLowerCase() === "source" || link.href.includes("github.com");
+}
+
+function normalizeHref(href) {
+ const cleaned = clean(href);
+ if (!cleaned) {
+ return "";
+ }
+
+ try {
+ const url = new URL(cleaned);
+ url.hash = "";
+ url.searchParams.sort();
+ return url.toString().replace(/\/$/, "").toLowerCase();
+ } catch {
+ return cleaned.replace(/\/$/, "").toLowerCase();
+ }
+}
+
+function clean(value) {
+ return String(value || "")
+ .replace(/\s+/g, " ")
+ .trim();
+}
+
+function writeSummary({ status }) {
+ const lines = [
+ "# Project Health and Clean up",
+ "",
+ `- Status: ${status}`,
+ `- Projects checked: ${projects.length}`,
+ `- Links checked: ${checkedLinks.length}`,
+ `- Auto-fixes: ${fixes.length}`,
+ `- Needs review: ${reviewNotes.length}`,
+ `- Warnings: ${warnings.length}`,
+ "",
+ ];
+
+ if (fixes.length) {
+ lines.push("## Auto-fixes", "", ...fixes.map((fix) => `- ${fix}`), "");
+ }
+
+ if (reviewNotes.length) {
+ lines.push("## Needs Review", "", ...reviewNotes.map((note) => `- ${note}`), "");
+ }
+
+ if (warnings.length) {
+ lines.push("## Warnings", "", ...warnings.map((warning) => `- ${warning}`), "");
+ }
+
+ if (!fixes.length && !reviewNotes.length && !warnings.length) {
+ lines.push("No project data, asset, or link health issues were found.", "");
+ }
+
+ writeFileSync(summaryPath, `${lines.join("\n")}\n`, "utf8");
+}
+
+function writeOutput(name, value) {
+ if (!process.env.GITHUB_OUTPUT) {
+ return;
+ }
+
+ writeFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`, { flag: "a" });
+}
diff --git a/portfolio-v2/scripts/prepareProjectThumbnail.mjs b/portfolio-v2/scripts/prepareProjectThumbnail.mjs
index b90b46c..08ab501 100644
--- a/portfolio-v2/scripts/prepareProjectThumbnail.mjs
+++ b/portfolio-v2/scripts/prepareProjectThumbnail.mjs
@@ -233,7 +233,9 @@ function extensionFromUrl(url) {
}
function clean(value) {
- return String(value || "").replace(/\s+/g, " ").trim();
+ return String(value || "")
+ .replace(/\s+/g, " ")
+ .trim();
}
function slug(value) {
diff --git a/portfolio-v2/src/App.jsx b/portfolio-v2/src/App.jsx
index 2bdc824..03664a7 100644
--- a/portfolio-v2/src/App.jsx
+++ b/portfolio-v2/src/App.jsx
@@ -15,15 +15,7 @@ import {
X,
} from "lucide-react";
import { useMemo, useState } from "react";
-import {
- certifications,
- education,
- experience,
- profile,
- projects,
- publications,
- skillGroups,
-} from "./portfolioData";
+import { certifications, education, experience, profile, projects, publications, skillGroups } from "./portfolioData";
const asset = (path) => `${import.meta.env.BASE_URL}${path}`;
const resumeHref = asset("resume/Vikram_Sharma_Python_Backend_Engineer.pdf");
@@ -32,17 +24,27 @@ const defaultProjectImage = "images/Projects/headlines.png";
function App() {
const [menuOpen, setMenuOpen] = useState(false);
const [activeFilter, setActiveFilter] = useState("All");
- const featuredProjects = projects.filter((project) => project.featured);
- const libraryProjects = projects.filter((project) => !project.featured);
const filters = useMemo(() => {
- const tags = projects.flatMap((project) => project.tags);
- return ["All", ...Array.from(new Set(tags)).slice(0, 9)];
+ return [
+ { label: "All" },
+ { label: "Python", tags: ["Python"] },
+ { label: "APIs", tags: ["APIs", "API"] },
+ { label: "ML / AI", tags: ["ML", "TensorFlow", "Deep Learning", "Computer Vision", "NLP", "Regression"] },
+ { label: "React", tags: ["React", "Frontend"] },
+ { label: "Java", tags: ["Java", "Spring Boot", "Microservices"] },
+ { label: "Data", tags: ["Data", "MongoDB", "Recommender"] },
+ ];
}, []);
- const shownProjects = libraryProjects.filter(
- (project) => activeFilter === "All" || project.tags.includes(activeFilter)
- );
+ const activeFilterConfig = filters.find((filter) => filter.label === activeFilter) || filters[0];
+ const shownProjects = projects.filter((project) => {
+ if (!activeFilterConfig.tags) {
+ return true;
+ }
+
+ return project.tags.some((tag) => activeFilterConfig.tags.includes(tag));
+ });
return (
@@ -53,9 +55,7 @@ function App() {
Python Backend Engineer
-
- Building reliable APIs, AWS serverless systems, and production-ready backend services.
-
+
Building reliable APIs, AWS serverless systems, and production-ready backend services.
{profile.summary}
@@ -104,7 +104,7 @@ function App() {
icon={
}
kicker="Experience"
title="Backend work in production environments"
- text="Resume-led experience focused on service reliability, API delivery, cloud systems, and maintainable engineering."
+ text="Experience across service reliability, API delivery, cloud systems, and maintainable engineering practices."
/>
{experience.map((item) => (
@@ -138,7 +138,7 @@ function App() {
icon={
}
kicker="Technical Stack"
title="Backend-first skill set with cloud and testing depth"
- text="Grouped around the way engineering leads review practical production readiness."
+ text="Core tools and technologies used across backend services, cloud workflows, testing, data, and frontend delivery."
/>
{skillGroups.map((group) => (
@@ -158,28 +158,25 @@ function App() {
}
kicker="Projects"
- title="Selected engineering work, with the full project history kept intact"
- text="The first row is intentionally stronger for backend and engineering review; the rest stays available for context."
+ title="Selected software engineering projects"
+ text="Backend, API, cloud, and machine learning projects showing implementation depth, reliability thinking, and end-to-end delivery."
/>
-
- {featuredProjects
- .map((project) => (
-
- ))}
-
-
{filters.map((filter) => (
setActiveFilter(filter)}
+ aria-pressed={filter.label === activeFilter}
+ className={filter.label === activeFilter ? "filter active" : "filter"}
+ key={filter.label}
+ onClick={() => setActiveFilter(filter.label)}
type="button"
>
- {filter}
+ {filter.label}
))}
+
+ {shownProjects.length} {shownProjects.length === 1 ? "project" : "projects"}
+
@@ -248,10 +245,10 @@ function App() {