From bad4b337c45bb1e6ddc876eb84f359aeb1a220dc Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sun, 6 Sep 2026 22:42:31 +0800
Subject: [PATCH 01/37] feat(certificates): add Front-End Development library
certificate from freeCodeCamp
---
src/data/experiences.ts | 28 +++++++++++++++++++---------
1 file changed, 19 insertions(+), 9 deletions(-)
diff --git a/src/data/experiences.ts b/src/data/experiences.ts
index 4849326..d30c58a 100644
--- a/src/data/experiences.ts
+++ b/src/data/experiences.ts
@@ -19,14 +19,14 @@ export const workExperiences: WorkExperience[] = [
},
];
-export const certificates: Certificate[] = [
+export const CERTIFICATES: Certificate[] = [
{
- id: 3,
- title: 'Relational Databases',
+ id: 1,
+ title: 'Responsive Web Design',
institution: 'freeCodeCamp',
- dateReceived: 'May 2026',
+ dateReceived: 'Dec 2025',
urlLink:
- 'https://www.freecodecamp.org/certification/codingwithjiro/relational-databases-v9',
+ 'https://www.freecodecamp.org/certification/codingwithjiro/responsive-web-design-v9',
},
{
id: 2,
@@ -37,11 +37,21 @@ export const certificates: Certificate[] = [
'https://www.freecodecamp.org/certification/codingwithjiro/javascript-v9',
},
{
- id: 1,
- title: 'Responsive Web Design',
+ id: 3,
+ title: 'Relational Databases',
institution: 'freeCodeCamp',
- dateReceived: 'Dec 2025',
+ dateReceived: 'May 2026',
urlLink:
- 'https://www.freecodecamp.org/certification/codingwithjiro/responsive-web-design-v9',
+ 'https://www.freecodecamp.org/certification/codingwithjiro/relational-databases-v9',
+ },
+ {
+ id: 4,
+ title: 'Front-End Libraries',
+ institution: 'freeCodeCamp',
+ dateReceived: 'September 2026',
+ urlLink:
+ 'https://www.freecodecamp.org/certification/codingwithjiro/front-end-development-libraries-v9',
},
];
+
+export const certificates: Certificate[] = CERTIFICATES.slice(-3).reverse();
From 600e8fbe8638787ee65556c85322bc3040e6210c Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sun, 6 Sep 2026 23:11:06 +0800
Subject: [PATCH 02/37] chore(meta): update metadata for article slug page to
show blog title and author name
---
src/app/blog/[slug]/page.tsx | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index 2e7d854..ced65af 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -4,11 +4,44 @@ import ArticleHeader from '@/components/sections/article-header';
import Footer from '@/components/sections/footer';
import Link from 'next/link';
import { ArrowLeft, ArrowRight } from 'lucide-react';
+import type { Metadata } from 'next';
+
+const metaDescription =
+ 'Blog posts on full stack development and tech from Elmar Chavez.';
type ArticlePageProps = {
params: Promise<{ slug: string }>;
};
+export async function generateMetadata({
+ params,
+}: ArticlePageProps): Promise {
+ const { slug } = await params;
+ const blog = getBlogMeta(slug);
+ const title = `${blog.title} | Elmar Chavez`;
+ const metadata: Metadata = {
+ title,
+ description: metaDescription,
+ keywords: [
+ 'Elmar Chavez Blog',
+ 'Web Development Blog',
+ 'Tech Articles',
+ 'Tech Community',
+ 'Software Engineering Blog',
+ ],
+ openGraph: {
+ title,
+ description: metaDescription,
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title,
+ description: metaDescription,
+ },
+ };
+ return metadata;
+}
+
export default async function ArticlePage({ params }: ArticlePageProps) {
const { slug } = await params;
const blog = getBlogMeta(slug);
From 58e0d118e4899ebdb052ba063716eeccf51a19e3 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Mon, 7 Sep 2026 23:28:37 +0800
Subject: [PATCH 03/37] test(experiences): update data to use the CERTIFICATES
data instead of only the latest three certificates
---
src/data/experiences.test.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/data/experiences.test.ts b/src/data/experiences.test.ts
index 159aa35..2175fc5 100644
--- a/src/data/experiences.test.ts
+++ b/src/data/experiences.test.ts
@@ -1,4 +1,4 @@
-import { workExperiences, certificates } from './experiences';
+import { workExperiences, CERTIFICATES } from './experiences';
import { isNotEmpty, isValidUrl } from '@/tests/utils';
describe('Work Experiences and Certificates data', () => {
@@ -11,15 +11,15 @@ describe('Work Experiences and Certificates data', () => {
expect(isNotEmpty(companyNames)).toBe(true);
});
test('every certificate has a non-empty title', () => {
- const titles = certificates.map(({ title }) => title);
+ const titles = CERTIFICATES.map(({ title }) => title);
expect(isNotEmpty(titles)).toBe(true);
});
test('every certificate has a non-empty institution', () => {
- const institutions = certificates.map(({ institution }) => institution);
+ const institutions = CERTIFICATES.map(({ institution }) => institution);
expect(isNotEmpty(institutions)).toBe(true);
});
test('every certificate uses a valid HTTPS URL', () => {
- const urls = certificates.map(({ urlLink }) => urlLink);
+ const urls = CERTIFICATES.map(({ urlLink }) => urlLink);
expect(isValidUrl(urls)).toBe(true);
});
});
From caf5f07ca1ea40caa108a6db488ac4391a8359f3 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Mon, 7 Sep 2026 23:58:16 +0800
Subject: [PATCH 04/37] feat(tech-stack): add View all link in the Tech Stack
section of the landing page
---
src/components/sections/tech-stack.tsx | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/components/sections/tech-stack.tsx b/src/components/sections/tech-stack.tsx
index bb32382..eaf3ef9 100644
--- a/src/components/sections/tech-stack.tsx
+++ b/src/components/sections/tech-stack.tsx
@@ -1,3 +1,5 @@
+import ViewAllLink from '../ui/view-all-link';
+
export const FRONTEND: string[] = [
'JavaScript',
'TypeScript',
@@ -11,7 +13,10 @@ export const DEVOPS: string[] = ['GitHub Actions', 'Docker'];
const TechStack = () => {
return (
- Tech Stack
+
Frontend
From 8bff087873c87f2a702efcf4b81f8e8ff64477a3 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Tue, 8 Sep 2026 00:08:37 +0800
Subject: [PATCH 05/37] feat(tech-stack): create the initial tech stack page
---
src/app/tech-stack/page.tsx | 53 +++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
create mode 100644 src/app/tech-stack/page.tsx
diff --git a/src/app/tech-stack/page.tsx b/src/app/tech-stack/page.tsx
new file mode 100644
index 0000000..81810f5
--- /dev/null
+++ b/src/app/tech-stack/page.tsx
@@ -0,0 +1,53 @@
+import type { Metadata } from 'next';
+import Breadcrumb from '@/components/sections/breadcrumb';
+import ThemeToggle from '@/components/theme/theme-toggle';
+import Footer from '@/components/sections/footer';
+
+const metaDescription =
+ 'List of all the technologies Elmar Chavez have experience with.';
+
+export const metadata: Metadata = {
+ title: 'Elmar Chavez | Tech Stack',
+ description: metaDescription,
+ keywords: [
+ 'Elmar Chavez Tech Stack',
+ 'Web Development Technologies',
+ 'Full Stack Development Skills',
+ 'Frontend',
+ 'Backend',
+ ],
+ openGraph: {
+ title: 'Tech Stack | Elmar Chavez',
+ description: metaDescription,
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Tech Stack | Elmar Chavez',
+ description: metaDescription,
+ },
+};
+
+export default function TechStackPage() {
+ return (
+
+
+
+
+ Tech Stack
+
+ List of technologies I used so far.
+
+
+
+ Add list here.
+
+
+
+ );
+}
From 5c1c81e79720d742e6fedbd68bdef72e003507e0 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Tue, 8 Sep 2026 17:16:59 +0800
Subject: [PATCH 06/37] feat(ux): add You are here! label when user hovers over
the current page in the Breadcrumb component
---
src/app/tech-stack/page.tsx | 4 ++--
src/components/sections/breadcrumb.tsx | 18 ++++++++++++------
2 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/src/app/tech-stack/page.tsx b/src/app/tech-stack/page.tsx
index 81810f5..a7b1d65 100644
--- a/src/app/tech-stack/page.tsx
+++ b/src/app/tech-stack/page.tsx
@@ -13,8 +13,8 @@ export const metadata: Metadata = {
'Elmar Chavez Tech Stack',
'Web Development Technologies',
'Full Stack Development Skills',
- 'Frontend',
- 'Backend',
+ 'Frontend Tech',
+ 'Backend Tech',
],
openGraph: {
title: 'Tech Stack | Elmar Chavez',
diff --git a/src/components/sections/breadcrumb.tsx b/src/components/sections/breadcrumb.tsx
index 80fd6a5..6dd8347 100644
--- a/src/components/sections/breadcrumb.tsx
+++ b/src/components/sections/breadcrumb.tsx
@@ -47,12 +47,18 @@ const Breadcrumb = ({ items, currentLabel }: BreadCrumbProps) => {
/
-
- {currentLabel}
-
+
+
+ {currentLabel}
+
+
+
+ You are here!
+
+
From a59fe219e8940d132a5138675e2a2b588bb2cd76 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Wed, 9 Sep 2026 23:22:49 +0800
Subject: [PATCH 07/37] feat(tech-stack): create initial tech stack data
---
src/data/tech-stack.ts | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 src/data/tech-stack.ts
diff --git a/src/data/tech-stack.ts b/src/data/tech-stack.ts
new file mode 100644
index 0000000..b924bc2
--- /dev/null
+++ b/src/data/tech-stack.ts
@@ -0,0 +1,9 @@
+export const FRONTEND: string[] = [
+ 'JavaScript',
+ 'TypeScript',
+ 'React',
+ 'Next.js',
+ 'Tailwind CSS',
+];
+export const BACKEND: string[] = ['Node.js', 'Express.js', 'PostgreSQL'];
+export const DEVOPS: string[] = ['GitHub Actions', 'Docker'];
From 2f62b828627e0c4b10be0ea82f882de46ef223ef Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 00:58:42 +0800
Subject: [PATCH 08/37] feat(tech-stack): complete list of tech used so far and
export landing page data
---
src/data/tech-stack.ts | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/src/data/tech-stack.ts b/src/data/tech-stack.ts
index b924bc2..e892657 100644
--- a/src/data/tech-stack.ts
+++ b/src/data/tech-stack.ts
@@ -4,6 +4,33 @@ export const FRONTEND: string[] = [
'React',
'Next.js',
'Tailwind CSS',
+ 'React Router',
+ 'Styled Components',
+ 'HTML',
+ 'CSS',
+ 'Vite',
];
export const BACKEND: string[] = ['Node.js', 'Express.js', 'PostgreSQL'];
+export const TESTING_AND_QUALITY: string[] = [
+ 'Vitest',
+ 'React Testing Library',
+ 'MSW',
+ 'Playwright',
+ 'Axe DevTools',
+ 'ESLint',
+ 'Prettier',
+];
export const DEVOPS: string[] = ['GitHub Actions', 'Docker'];
+export const CONTENT: string[] = ['MDX', 'Tailwind CSS Typography'];
+export const TOOLS: string[] = [
+ 'Git',
+ 'GitHub',
+ 'VS Code',
+ 'Slack',
+ 'Discord',
+ 'Teams',
+];
+
+export const frontend = FRONTEND.slice(0, 5);
+export const backend = BACKEND.slice(0, 5);
+export const devOps = DEVOPS.slice(0, 5);
From 9d0b17d82498e6156ba700cca7d71a16c62a0222 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 00:59:18 +0800
Subject: [PATCH 09/37] refactor(tech-stack): update data in tech stack section
and test
---
src/components/sections/tech-stack.test.tsx | 4 ++--
src/components/sections/tech-stack.tsx | 17 ++++-------------
2 files changed, 6 insertions(+), 15 deletions(-)
diff --git a/src/components/sections/tech-stack.test.tsx b/src/components/sections/tech-stack.test.tsx
index 5cd57c9..6a6bf70 100644
--- a/src/components/sections/tech-stack.test.tsx
+++ b/src/components/sections/tech-stack.test.tsx
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
-import { FRONTEND, BACKEND, DEVOPS } from './tech-stack';
+import { frontend, backend, devOps } from '@/data/tech-stack';
import TechStack from './tech-stack';
describe('Tech Stack section', () => {
@@ -26,7 +26,7 @@ describe('Tech Stack section', () => {
test('renders every tech in each category', () => {
render();
const techLists = screen.getAllByRole('listitem');
- const totalTech = FRONTEND.length + BACKEND.length + DEVOPS.length;
+ const totalTech = frontend.length + backend.length + devOps.length;
expect(techLists.length).toBe(totalTech);
});
});
diff --git a/src/components/sections/tech-stack.tsx b/src/components/sections/tech-stack.tsx
index eaf3ef9..cf79f7d 100644
--- a/src/components/sections/tech-stack.tsx
+++ b/src/components/sections/tech-stack.tsx
@@ -1,15 +1,6 @@
+import { backend, devOps, frontend } from '@/data/tech-stack';
import ViewAllLink from '../ui/view-all-link';
-export const FRONTEND: string[] = [
- 'JavaScript',
- 'TypeScript',
- 'React',
- 'Next.js',
- 'Tailwind CSS',
-];
-export const BACKEND: string[] = ['Node.js', 'Express.js', 'PostgreSQL'];
-export const DEVOPS: string[] = ['GitHub Actions', 'Docker'];
-
const TechStack = () => {
return (
@@ -22,7 +13,7 @@ const TechStack = () => {
Frontend
- {FRONTEND.map((tech) => {
+ {frontend.map((tech) => {
return (
- {
Backend
- {BACKEND.map((tech) => {
+ {backend.map((tech) => {
return (
- {
DevOps
- {DEVOPS.map((tech) => {
+ {devOps.map((tech) => {
return (
-
Date: Thu, 10 Sep 2026 01:35:11 +0800
Subject: [PATCH 10/37] feat(tech-stack): add more technologies and create a
TECH_STACK object with category name
---
src/data/tech-stack.ts | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/src/data/tech-stack.ts b/src/data/tech-stack.ts
index e892657..abb6e8b 100644
--- a/src/data/tech-stack.ts
+++ b/src/data/tech-stack.ts
@@ -9,6 +9,7 @@ export const FRONTEND: string[] = [
'HTML',
'CSS',
'Vite',
+ 'Responsive Web Design',
];
export const BACKEND: string[] = ['Node.js', 'Express.js', 'PostgreSQL'];
export const TESTING_AND_QUALITY: string[] = [
@@ -17,6 +18,7 @@ export const TESTING_AND_QUALITY: string[] = [
'MSW',
'Playwright',
'Axe DevTools',
+ 'Lighthouse',
'ESLint',
'Prettier',
];
@@ -34,3 +36,30 @@ export const TOOLS: string[] = [
export const frontend = FRONTEND.slice(0, 5);
export const backend = BACKEND.slice(0, 5);
export const devOps = DEVOPS.slice(0, 5);
+
+export const TECH_STACK = [
+ {
+ category: 'Frontend',
+ technologies: FRONTEND,
+ },
+ {
+ category: 'Backend',
+ technologies: BACKEND,
+ },
+ {
+ category: 'DevOps',
+ technologies: DEVOPS,
+ },
+ {
+ category: 'Testing & Quality',
+ technologies: TESTING_AND_QUALITY,
+ },
+ {
+ category: 'Content',
+ technologies: CONTENT,
+ },
+ {
+ category: 'Tools',
+ technologies: TOOLS,
+ },
+];
From 846ff659eb2a539f5f39b03972eaf614634bf75c Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 01:41:52 +0800
Subject: [PATCH 11/37] feat(tech-list): create a TechList component section
---
src/components/sections/tech-list.tsx | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 src/components/sections/tech-list.tsx
diff --git a/src/components/sections/tech-list.tsx b/src/components/sections/tech-list.tsx
new file mode 100644
index 0000000..775f780
--- /dev/null
+++ b/src/components/sections/tech-list.tsx
@@ -0,0 +1,25 @@
+interface TechListProps {
+ category: string;
+ technologies: string[];
+}
+
+export default function TechList({ category, technologies }: TechListProps) {
+ return (
+
+
{category}
+
+
+ {technologies.map((tech) => {
+ return (
+ -
+ {tech}
+
+ );
+ })}
+
+
+ );
+}
From 87d67e2d833d21a8f427c2b5d8f2764cb3779e64 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 01:42:41 +0800
Subject: [PATCH 12/37] refactor(tech-stack): use TechList component
---
src/components/sections/tech-stack.tsx | 55 ++------------------------
1 file changed, 4 insertions(+), 51 deletions(-)
diff --git a/src/components/sections/tech-stack.tsx b/src/components/sections/tech-stack.tsx
index cf79f7d..53b0a51 100644
--- a/src/components/sections/tech-stack.tsx
+++ b/src/components/sections/tech-stack.tsx
@@ -1,5 +1,6 @@
import { backend, devOps, frontend } from '@/data/tech-stack';
import ViewAllLink from '../ui/view-all-link';
+import TechList from './tech-list';
const TechStack = () => {
return (
@@ -8,57 +9,9 @@ const TechStack = () => {
Tech Stack
-
-
- Frontend
-
-
- {frontend.map((tech) => {
- return (
- -
- {tech}
-
- );
- })}
-
-
-
-
- Backend
-
-
- {backend.map((tech) => {
- return (
- -
- {tech}
-
- );
- })}
-
-
-
-
- DevOps
-
-
- {devOps.map((tech) => {
- return (
- -
- {tech}
-
- );
- })}
-
-
+
+
+
);
};
From 5cf656b9d11b77739d503f8731737e4c3d0b8573 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 01:43:38 +0800
Subject: [PATCH 13/37] feat(tech-stack): add lists and content to tech stack
page
---
src/app/tech-stack/page.tsx | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/app/tech-stack/page.tsx b/src/app/tech-stack/page.tsx
index a7b1d65..1e51796 100644
--- a/src/app/tech-stack/page.tsx
+++ b/src/app/tech-stack/page.tsx
@@ -2,9 +2,11 @@ import type { Metadata } from 'next';
import Breadcrumb from '@/components/sections/breadcrumb';
import ThemeToggle from '@/components/theme/theme-toggle';
import Footer from '@/components/sections/footer';
+import TechList from '@/components/sections/tech-list';
+import { TECH_STACK } from '@/data/tech-stack';
const metaDescription =
- 'List of all the technologies Elmar Chavez have experience with.';
+ 'Technologies and tools Elmar Chavez has worked with across his projects.';
export const metadata: Metadata = {
title: 'Elmar Chavez | Tech Stack',
@@ -38,14 +40,22 @@ export default function TechStackPage() {
-
+
Tech Stack
- List of technologies I used so far.
+ Technologies and tools I've worked with so far.
- Add list here.
+
+ {TECH_STACK.map(({ category, technologies }) => {
+ return (
+ -
+
+
+ );
+ })}
+
From 7d3517d5ad413f20dda5b722178bad1238e6071f Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 01:48:06 +0800
Subject: [PATCH 14/37] feat(ux): add color transition and hover border effect
to each tech list item
---
src/app/tech-stack/page.tsx | 4 ++--
src/components/sections/tech-list.tsx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/app/tech-stack/page.tsx b/src/app/tech-stack/page.tsx
index 1e51796..4d88dae 100644
--- a/src/app/tech-stack/page.tsx
+++ b/src/app/tech-stack/page.tsx
@@ -42,9 +42,9 @@ export default function TechStackPage() {
Tech Stack
-
+
Technologies and tools I've worked with so far.
-
+
diff --git a/src/components/sections/tech-list.tsx b/src/components/sections/tech-list.tsx
index 775f780..8b44347 100644
--- a/src/components/sections/tech-list.tsx
+++ b/src/components/sections/tech-list.tsx
@@ -12,7 +12,7 @@ export default function TechList({ category, technologies }: TechListProps) {
{technologies.map((tech) => {
return (
-
{tech}
From 7da8e16a959778b9d95c54ff4562a1108664dbd5 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 02:10:22 +0800
Subject: [PATCH 15/37] refactor(view-all-link): remove color transition
---
src/components/ui/view-all-link.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/ui/view-all-link.tsx b/src/components/ui/view-all-link.tsx
index c022c4e..20e43fe 100644
--- a/src/components/ui/view-all-link.tsx
+++ b/src/components/ui/view-all-link.tsx
@@ -8,10 +8,10 @@ type ViewAllLinkProps = {
const ViewAllLink = ({ href }: ViewAllLinkProps) => {
return (
-
+
View all
From 9e34f5600c05babebaa5457dfc35853e38101e71 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 13:51:29 +0800
Subject: [PATCH 16/37] test(tech-stack): renders view all link
---
src/components/sections/tech-stack.test.tsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/components/sections/tech-stack.test.tsx b/src/components/sections/tech-stack.test.tsx
index 6a6bf70..3e46684 100644
--- a/src/components/sections/tech-stack.test.tsx
+++ b/src/components/sections/tech-stack.test.tsx
@@ -29,4 +29,9 @@ describe('Tech Stack section', () => {
const totalTech = frontend.length + backend.length + devOps.length;
expect(techLists.length).toBe(totalTech);
});
+ test('renders view all link', () => {
+ render();
+ const viewAllLink = screen.getByRole('link', { name: /view all/i });
+ expect(viewAllLink).toBeInTheDocument();
+ });
});
From 5a1dd06a41383189ded992e74e2cdc71067d8910 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 13:57:28 +0800
Subject: [PATCH 17/37] test(e2e): visitor can see the Tech Stack section
---
tests/tech-stack.spec.ts | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 tests/tech-stack.spec.ts
diff --git a/tests/tech-stack.spec.ts b/tests/tech-stack.spec.ts
new file mode 100644
index 0000000..d4a9d50
--- /dev/null
+++ b/tests/tech-stack.spec.ts
@@ -0,0 +1,9 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Tech Stack', () => {
+ test('visitor can see the Tech Stack section', async ({ page }) => {
+ await page.goto('/');
+ const heading = page.getByRole('heading', { name: /^tech stack$/i });
+ await expect(heading).toBeVisible();
+ });
+});
From ecaab479ae4a09993b1d92e8d50e1d7f9a90772e Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 14:01:12 +0800
Subject: [PATCH 18/37] test(e2e): visitor can view the Tech Stack page
---
tests/tech-stack.spec.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/tests/tech-stack.spec.ts b/tests/tech-stack.spec.ts
index d4a9d50..1e72f62 100644
--- a/tests/tech-stack.spec.ts
+++ b/tests/tech-stack.spec.ts
@@ -6,4 +6,11 @@ test.describe('Tech Stack', () => {
const heading = page.getByRole('heading', { name: /^tech stack$/i });
await expect(heading).toBeVisible();
});
+ test('visitor can view the Tech Stack page', async ({ page }) => {
+ await page.goto('/');
+ const viewAllLink = page.locator('a[href="/tech-stack"]');
+ await viewAllLink.click();
+ const heading = page.getByRole('heading', { name: /^tech stack$/i });
+ await expect(heading).toBeVisible();
+ });
});
From 97ab1dbdda2a81f4f453804ca9e1a99c9458dbe6 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 14:06:36 +0800
Subject: [PATCH 19/37] refactor(test): verify page URL to for tech stack page
---
tests/tech-stack.spec.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/tech-stack.spec.ts b/tests/tech-stack.spec.ts
index 1e72f62..55f6754 100644
--- a/tests/tech-stack.spec.ts
+++ b/tests/tech-stack.spec.ts
@@ -10,6 +10,7 @@ test.describe('Tech Stack', () => {
await page.goto('/');
const viewAllLink = page.locator('a[href="/tech-stack"]');
await viewAllLink.click();
+ await expect(page).toHaveURL('/tech-stack');
const heading = page.getByRole('heading', { name: /^tech stack$/i });
await expect(heading).toBeVisible();
});
From 755708033b39405086fb9588ba6fb4459a41fa6f Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 14:19:54 +0800
Subject: [PATCH 20/37] feat(ux): add color transition to view all links when
tabbed into
---
src/components/ui/view-all-link.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/ui/view-all-link.tsx b/src/components/ui/view-all-link.tsx
index 20e43fe..e74f296 100644
--- a/src/components/ui/view-all-link.tsx
+++ b/src/components/ui/view-all-link.tsx
@@ -8,7 +8,7 @@ type ViewAllLinkProps = {
const ViewAllLink = ({ href }: ViewAllLinkProps) => {
return (
From 3007fc6ad6421de644a702fbdb1e69ab8763f096 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Thu, 10 Sep 2026 16:22:49 +0800
Subject: [PATCH 21/37] feat(ux): polish outline offset, color transitions,
hover, and focus states for links
---
src/app/blog/[slug]/page.tsx | 4 ++--
src/app/projects/page.tsx | 22 +++++++++++++---------
src/components/sections/article-header.tsx | 6 +++---
src/components/sections/blog.tsx | 2 +-
src/components/sections/breadcrumb.tsx | 6 +++---
src/components/ui/view-all-link.tsx | 2 +-
6 files changed, 23 insertions(+), 19 deletions(-)
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index ced65af..4ab6961 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -81,7 +81,7 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
Back to homepage
@@ -89,7 +89,7 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
Read more articles
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index da02590..324a47c 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -44,8 +44,10 @@ export default function ProjectsPage() {
- Projects
-
+
+ Projects
+
+
Things I built and worked on.
@@ -64,7 +66,7 @@ export default function ProjectsPage() {
}) => {
return (
-
@@ -101,17 +103,19 @@ export default function ProjectsPage() {
-
+
{title}
-
{description}
+
+ {description}
+
@@ -120,7 +124,7 @@ export default function ProjectsPage() {
@@ -137,7 +141,7 @@ export default function ProjectsPage() {
Back to homepage
@@ -145,7 +149,7 @@ export default function ProjectsPage() {
View all projects
diff --git a/src/components/sections/article-header.tsx b/src/components/sections/article-header.tsx
index 0cfd62b..e931e92 100644
--- a/src/components/sections/article-header.tsx
+++ b/src/components/sections/article-header.tsx
@@ -47,18 +47,18 @@ const ArticleHeader = ({ breadcrumbItems, blogMeta }: ArticleHeaderProps) => {
/>
-
+
{blogMeta.tags.map((tag) => {
return - {tag}
;
})}
-
+
{blogMeta.title}
-
+
{
diff --git a/src/components/sections/breadcrumb.tsx b/src/components/sections/breadcrumb.tsx
index 6dd8347..1722dea 100644
--- a/src/components/sections/breadcrumb.tsx
+++ b/src/components/sections/breadcrumb.tsx
@@ -12,7 +12,7 @@ const Breadcrumb = ({ items, currentLabel }: BreadCrumbProps) => {
-
Home
@@ -30,7 +30,7 @@ const Breadcrumb = ({ items, currentLabel }: BreadCrumbProps) => {
{label}
@@ -49,7 +49,7 @@ const Breadcrumb = ({ items, currentLabel }: BreadCrumbProps) => {
{currentLabel}
diff --git a/src/components/ui/view-all-link.tsx b/src/components/ui/view-all-link.tsx
index e74f296..cfda1e7 100644
--- a/src/components/ui/view-all-link.tsx
+++ b/src/components/ui/view-all-link.tsx
@@ -8,7 +8,7 @@ type ViewAllLinkProps = {
const ViewAllLink = ({ href }: ViewAllLinkProps) => {
return (
From 80fc504248dd5479d3c76ca1cf9c7a3823085f40 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 01:05:59 +0800
Subject: [PATCH 22/37] test(e2e): visitor can tab to the Tech Stack view all
link and open the Tech Stack section
---
tests/keyboard.spec.ts | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/tests/keyboard.spec.ts b/tests/keyboard.spec.ts
index dce755f..10bf83b 100644
--- a/tests/keyboard.spec.ts
+++ b/tests/keyboard.spec.ts
@@ -92,7 +92,7 @@ test.describe('Keyboard Accessibility', () => {
const blog = blogList[0];
const link = page.getByRole('link', { name: blog.title });
await tabUntilFocused(page, link, blog.title);
- expect(link).toBeFocused();
+ await expect(link).toBeFocused();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(blog.blogUrl);
});
@@ -107,10 +107,24 @@ test.describe('Keyboard Accessibility', () => {
const certificate = certificates[0];
const link = page.getByRole('link', { name: certificate.title });
await tabUntilFocused(page, link, certificate.title);
- expect(link).toBeFocused();
+ await expect(link).toBeFocused();
const newPagePromise = page.waitForEvent('popup');
await page.keyboard.press('Enter');
const newPage = await newPagePromise;
await expect(newPage).toHaveURL(certificate.urlLink);
});
+ test('visitor can tab to the Tech Stack view all link and open the Tech Stack section', async ({
+ page,
+ browserName,
+ }) => {
+ test.skip(
+ browserName === 'webkit',
+ 'WebKit on Windows does not traverse focus order correctly.',
+ );
+ const viewAllLink = page.locator('a[href="/tech-stack"]');
+ await tabUntilFocused(page, viewAllLink, 'View all link');
+ await expect(viewAllLink).toBeFocused();
+ await page.keyboard.press('Enter');
+ await expect(page).toHaveURL('/tech-stack');
+ });
});
From 3eceb8ac96088034358d3019414f2780728a3c9d Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 01:12:21 +0800
Subject: [PATCH 23/37] test(e2e): visitor can tab to the Projects view all
link and open the Projects page
---
tests/keyboard.spec.ts | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/tests/keyboard.spec.ts b/tests/keyboard.spec.ts
index 10bf83b..18f2d71 100644
--- a/tests/keyboard.spec.ts
+++ b/tests/keyboard.spec.ts
@@ -113,7 +113,7 @@ test.describe('Keyboard Accessibility', () => {
const newPage = await newPagePromise;
await expect(newPage).toHaveURL(certificate.urlLink);
});
- test('visitor can tab to the Tech Stack view all link and open the Tech Stack section', async ({
+ test('visitor can tab to the Tech Stack view all link and open the Tech Stack page', async ({
page,
browserName,
}) => {
@@ -127,4 +127,18 @@ test.describe('Keyboard Accessibility', () => {
await page.keyboard.press('Enter');
await expect(page).toHaveURL('/tech-stack');
});
+ test.only('visitor can tab to the Projects view all link and open the Projects page', async ({
+ page,
+ browserName,
+ }) => {
+ test.skip(
+ browserName === 'webkit',
+ 'WebKit on Windows does not traverse focus order correctly.',
+ );
+ const viewAllLink = page.locator('a[href="/projects"]');
+ await tabUntilFocused(page, viewAllLink, 'View all link');
+ await expect(viewAllLink).toBeFocused();
+ await page.keyboard.press('Enter');
+ await expect(page).toHaveURL('/projects');
+ });
});
From df6ef39e2e998f3ff74b0404bb44aa7264979a74 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 01:15:05 +0800
Subject: [PATCH 24/37] test(e2e): visitor can tab to the Blog view all link
and open the Blog page
---
tests/keyboard.spec.ts | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/tests/keyboard.spec.ts b/tests/keyboard.spec.ts
index 18f2d71..7ce088d 100644
--- a/tests/keyboard.spec.ts
+++ b/tests/keyboard.spec.ts
@@ -127,7 +127,7 @@ test.describe('Keyboard Accessibility', () => {
await page.keyboard.press('Enter');
await expect(page).toHaveURL('/tech-stack');
});
- test.only('visitor can tab to the Projects view all link and open the Projects page', async ({
+ test('visitor can tab to the Projects view all link and open the Projects page', async ({
page,
browserName,
}) => {
@@ -141,4 +141,18 @@ test.describe('Keyboard Accessibility', () => {
await page.keyboard.press('Enter');
await expect(page).toHaveURL('/projects');
});
+ test('visitor can tab to the Blog view all link and open the Blog page', async ({
+ page,
+ browserName,
+ }) => {
+ test.skip(
+ browserName === 'webkit',
+ 'WebKit on Windows does not traverse focus order correctly.',
+ );
+ const viewAllLink = page.locator('a[href="/blog"]');
+ await tabUntilFocused(page, viewAllLink, 'View all link');
+ await expect(viewAllLink).toBeFocused();
+ await page.keyboard.press('Enter');
+ await expect(page).toHaveURL('/blog');
+ });
});
From a53cbd587f8403378a13d5dfda113053bfcd01dd Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 01:54:50 +0800
Subject: [PATCH 25/37] feat(404): create a not-found page
---
src/app/not-found.tsx | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 src/app/not-found.tsx
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..b6e7441
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,29 @@
+import Footer from '@/components/sections/footer';
+import Breadcrumb from '@/components/sections/breadcrumb';
+import ThemeToggle from '@/components/theme/theme-toggle';
+
+export default function NotFound() {
+ return (
+
+
+
+
+
+ );
+}
From 2bfe01210b437d2cb12dfe0c317a536d898e8285 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 01:57:15 +0800
Subject: [PATCH 26/37] feat(ux): add hover border effect on profile picture
---
src/components/sections/hero.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/sections/hero.tsx b/src/components/sections/hero.tsx
index 0a8e24c..8b52894 100644
--- a/src/components/sections/hero.tsx
+++ b/src/components/sections/hero.tsx
@@ -5,7 +5,7 @@ import { socialLinks } from '@/data/social-links';
const Hero = () => {
return (
-
+
Date: Fri, 11 Sep 2026 02:00:50 +0800
Subject: [PATCH 27/37] feat(ux): update hover color in each project item in
home page
---
src/components/sections/projects.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/sections/projects.tsx b/src/components/sections/projects.tsx
index c9697db..374ea27 100644
--- a/src/components/sections/projects.tsx
+++ b/src/components/sections/projects.tsx
@@ -15,7 +15,7 @@ const Projects = () => {
({ id, title, description, imgUrl, imgUrlDark, siteUrl }) => {
return (
-
From 1794cade33f66ef96ae83c7f24b200f9891f3b8f Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 02:26:11 +0800
Subject: [PATCH 28/37] refactor(projects): use Link from next for Projects
section links
---
src/components/sections/projects.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/components/sections/projects.tsx b/src/components/sections/projects.tsx
index 374ea27..d3b3a63 100644
--- a/src/components/sections/projects.tsx
+++ b/src/components/sections/projects.tsx
@@ -1,4 +1,5 @@
import Image from 'next/image';
+import Link from 'next/link';
import { projectList } from '@/data/project-list';
import ViewAllLink from '../ui/view-all-link';
@@ -38,14 +39,14 @@ const Projects = () => {
{description}
From 885ae201c5ff4ff323bd9864e6a6c71a6bbcda9d Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 02:32:46 +0800
Subject: [PATCH 29/37] refactor(footer): replace anchor tag with Link from
next
---
src/components/sections/footer.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/components/sections/footer.tsx b/src/components/sections/footer.tsx
index 0a4bc9e..eb4a07e 100644
--- a/src/components/sections/footer.tsx
+++ b/src/components/sections/footer.tsx
@@ -1,4 +1,5 @@
import { socialLinks } from '@/data/social-links';
+import Link from 'next/link';
const currentYear = new Date().getFullYear();
@@ -13,14 +14,14 @@ const Footer = () => {
{socialLinks.map(({ id, url, label, Icon }) => {
return (
-
-
-
+
);
})}
From 30eb73babd321c28b7b91d22d7c8604785a4056b Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 11 Sep 2026 02:35:55 +0800
Subject: [PATCH 30/37] feat(ux): update border hover color for banner images
in blog section of the homepage
---
src/components/sections/blog.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/sections/blog.tsx b/src/components/sections/blog.tsx
index 1ab192e..98de716 100644
--- a/src/components/sections/blog.tsx
+++ b/src/components/sections/blog.tsx
@@ -30,7 +30,7 @@ const Blog = () => {
className="border-border/60 border-b pb-4 last:border-b-0 md:flex md:flex-row md:items-center md:justify-between md:gap-4"
key={id}
>
-
+
Date: Fri, 11 Sep 2026 02:38:04 +0800
Subject: [PATCH 31/37] feat(ux): remove scale hover and update border color
hover for banner images in Blog page
---
src/app/blog/page.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
index ff9d84c..95cf212 100644
--- a/src/app/blog/page.tsx
+++ b/src/app/blog/page.tsx
@@ -69,14 +69,14 @@ export default function BlogPage() {
className="border-border/60 border-b pb-4 transition-colors duration-150 ease-in-out last:border-b-0 md:flex md:flex-row md:items-center md:justify-between md:gap-4"
key={id}
>
-
+
From 4c82fab8b4bc2f912c0cd564eb13b7d746a4dcf2 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sat, 12 Sep 2026 00:47:50 +0800
Subject: [PATCH 32/37] test(e2e): visitor sees a 404 Page not found when going
to an unknown page
---
tests/home.spec.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/tests/home.spec.ts b/tests/home.spec.ts
index 5d56289..66ce08b 100644
--- a/tests/home.spec.ts
+++ b/tests/home.spec.ts
@@ -6,3 +6,10 @@ test('visitor can open the portfolio homepage', async ({ page }) => {
page.getByRole('heading', { name: /elmar chavez/i }),
).toBeVisible();
});
+test('visitor sees a 404 Page not found when going to an unknown page', async ({
+ page,
+}) => {
+ await page.goto('/non-existent-page');
+ const heading = page.getByRole('heading', { name: /^page not found$/i });
+ await expect(heading).toBeVisible();
+});
From 1361265d64224cd2721263a2e05a18ef35195f26 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sat, 12 Sep 2026 00:52:35 +0800
Subject: [PATCH 33/37] feat(ux): change hover border color to each project in
Projects page
---
src/app/projects/page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index 324a47c..c419e0f 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -66,7 +66,7 @@ export default function ProjectsPage() {
}) => {
return (
-
From eab1998c7cbef6160260ff1509bbbdc09e6fab5b Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sat, 12 Sep 2026 12:37:19 +0800
Subject: [PATCH 34/37] feat(ux): add hover delay to the tooltip in current
page breadcrumb
---
src/components/sections/breadcrumb.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/sections/breadcrumb.tsx b/src/components/sections/breadcrumb.tsx
index 1722dea..528c586 100644
--- a/src/components/sections/breadcrumb.tsx
+++ b/src/components/sections/breadcrumb.tsx
@@ -55,7 +55,7 @@ const Breadcrumb = ({ items, currentLabel }: BreadCrumbProps) => {
{currentLabel}
-
+
You are here!
From 69deb1002cb257709aa1d3d704a9b3a009b3e82f Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sat, 12 Sep 2026 12:38:54 +0800
Subject: [PATCH 35/37] perf(skip-link): use Link instead of anchor tag
---
src/components/accessibility/skip-link.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/components/accessibility/skip-link.tsx b/src/components/accessibility/skip-link.tsx
index 8521a76..dc5d0c6 100644
--- a/src/components/accessibility/skip-link.tsx
+++ b/src/components/accessibility/skip-link.tsx
@@ -1,11 +1,13 @@
+import Link from 'next/link';
+
const SkipLink = () => {
return (
-
Skip to main content
-
+
);
};
From ea5aaf365c94ca5f56383929c3c62145e2464413 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sat, 12 Sep 2026 12:40:09 +0800
Subject: [PATCH 36/37] perf(experience): use Link instead of anchor tag for
certification links
---
src/components/sections/experience.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/components/sections/experience.tsx b/src/components/sections/experience.tsx
index f84dff1..8d585e1 100644
--- a/src/components/sections/experience.tsx
+++ b/src/components/sections/experience.tsx
@@ -1,5 +1,6 @@
import { FiExternalLink } from 'react-icons/fi';
import { workExperiences, certificates } from '@/data/experiences';
+import Link from 'next/link';
const Experience = () => {
return (
@@ -36,7 +37,7 @@ const Experience = () => {
className="before:border-border before:bg-background first:before:bg-foreground after:bg-border/50 hover:before:bg-foreground relative pl-4.5 before:absolute before:top-1 before:left-0 before:h-3 before:w-3 before:rounded-full before:border-2 before:transition-colors before:duration-150 before:ease-in-out after:absolute after:top-4 after:-bottom-6 after:left-1.5 after:w-px after:transition-colors after:duration-150 after:ease-in-out last:after:hidden md:pl-6"
key={id}
>
- {
>
{title}
-
+
{institution}
From f781c44b09d2a9de8103b65f918db892cc890a96 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Sat, 12 Sep 2026 12:41:08 +0800
Subject: [PATCH 37/37] perf(hero): use Link instead of anchor tag for social
links
---
src/components/sections/hero.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/components/sections/hero.tsx b/src/components/sections/hero.tsx
index 8b52894..e539e73 100644
--- a/src/components/sections/hero.tsx
+++ b/src/components/sections/hero.tsx
@@ -1,4 +1,5 @@
import Image from 'next/image';
+import Link from 'next/link';
import ThemeToggle from '../theme/theme-toggle';
import { socialLinks } from '@/data/social-links';
@@ -28,14 +29,14 @@ const Hero = () => {
{socialLinks.map(({ id, url, label, Icon }) => {
return (
-
-
-
+
);
})}