Skip to content
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 51 additions & 49 deletions src/app/certificates/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Breadcrumb from '@/components/sections/breadcrumb';
import ThemeToggle from '@/components/theme/theme-toggle';
import Footer from '@/components/sections/footer';
import CertificateImage from '@/components/ui/certificate-image';
import { CERTIFICATES } from '@/data/experiences';
import { CERTIFICATES } from '@/data/certificates';
import { FiExternalLink } from 'react-icons/fi';
import type { Metadata } from 'next';
import Link from 'next/link';
Expand Down Expand Up @@ -30,7 +30,7 @@ export const metadata: Metadata = {
},
};

export default function ExperiencePage() {
export default function CertificatesPage() {
return (
<main
id="main"
Expand All @@ -49,56 +49,58 @@ export default function ExperiencePage() {
</section>

<ul className="grid grid-cols-1 gap-x-3 gap-y-3 min-[520px]:grid-cols-2 md:grid-cols-3 md:gap-y-6">
{CERTIFICATES.map(
({
id,
title,
institution,
dateReceived,
urlLink,
imgUrl,
description,
}) => {
return (
<li
className="border-ring/80 focus-within:border-foreground has-hover:border-foreground bg-background text-foreground mx-auto flex max-w-57.5 flex-col rounded-lg border p-2 transition-colors duration-150 ease-in-out md:max-w-65"
key={id}
>
<CertificateImage
src={imgUrl}
title={title}
alt={`Elmar Chavez's certificate for ${title}.`}
/>

<Link
className="group mb-1 flex items-center gap-1 outline-0"
href={urlLink}
target="_blank"
aria-label={`See Elmar Chavez's ${title} certificate in new tab`}
{CERTIFICATES.slice()
.reverse()
.map(
({
id,
title,
institution,
dateReceived,
urlLink,
imgUrl,
description,
}) => {
return (
<li
className="border-ring/80 focus-within:border-foreground has-hover:border-foreground bg-background text-foreground mx-auto flex max-w-57.5 flex-col rounded-lg border p-2 transition-colors duration-150 ease-in-out md:max-w-65"
key={id}
>
<h3 className="text-foreground text-sm font-semibold decoration-1 underline-offset-4 transition-colors duration-150 ease-in-out group-hover:underline group-focus-visible:underline md:text-base">
{title}
</h3>
<FiExternalLink
className="text-muted-foreground group-hover:text-foreground group-focus-visible:text-foreground relative bottom-px transition-colors duration-150 ease-in-out"
size={14}
aria-hidden="true"
<CertificateImage
src={imgUrl}
title={title}
alt={`Elmar Chavez's certificate for ${title}.`}
/>
</Link>

<p className="text-foreground text-[0.75rem] transition-colors duration-150 ease-in-out md:text-sm">
{institution}
</p>
<p className="text-muted-foreground mb-2 text-[0.70rem] transition-colors duration-150 ease-in-out md:text-[0.75rem]">
Issued {dateReceived}
</p>
<p className="text-muted-foreground mb-2 text-[0.70rem] transition-colors duration-150 ease-in-out md:text-[0.75rem]">
{description}
</p>
</li>
);
},
)}
<Link
className="group mb-1 flex items-center gap-1 outline-0"
href={urlLink}
target="_blank"
aria-label={`See Elmar Chavez's ${title} certificate in new tab`}
>
<h3 className="text-foreground text-sm font-semibold decoration-1 underline-offset-4 transition-colors duration-150 ease-in-out group-hover:underline group-focus-visible:underline md:text-base">
{title}
</h3>
<FiExternalLink
className="text-muted-foreground group-hover:text-foreground group-focus-visible:text-foreground relative bottom-px transition-colors duration-150 ease-in-out"
size={14}
aria-hidden="true"
/>
</Link>

<p className="text-foreground text-[0.75rem] transition-colors duration-150 ease-in-out md:text-sm">
{institution}
</p>
<p className="text-muted-foreground mb-2 text-[0.70rem] transition-colors duration-150 ease-in-out md:text-[0.75rem]">
Issued {dateReceived}
</p>
<p className="text-muted-foreground mb-2 text-[0.70rem] transition-colors duration-150 ease-in-out md:text-[0.75rem]">
{description}
</p>
</li>
);
},
)}
</ul>

<Footer />
Expand Down
2 changes: 1 addition & 1 deletion src/components/sections/certificates.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { certificates } from '@/data/experiences';
import { certificates } from '@/data/certificates';
import Certificates from './certificates';

describe('Certificates section', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/sections/certificates.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { certificates } from '@/data/experiences';
import { certificates } from '@/data/certificates';
import { FiExternalLink } from 'react-icons/fi';
import ViewAllLink from '../ui/view-all-link';
import Link from 'next/link';
Expand Down
17 changes: 17 additions & 0 deletions src/data/certificates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CERTIFICATES } from './certificates';
import { isNotEmpty, isValidUrl } from '@/tests/utils';

describe('Certificats data', () => {
test('every certificate has a non-empty 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);
expect(isNotEmpty(institutions)).toBe(true);
});
test('every certificate uses a valid HTTPS URL', () => {
const urls = CERTIFICATES.map(({ urlLink }) => urlLink);
expect(isValidUrl(urls)).toBe(true);
});
});
61 changes: 61 additions & 0 deletions src/data/certificates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Certificate } from '@/types/experience';

export const CERTIFICATES: Certificate[] = [
{
id: 1,
title: 'Responsive Web Design',
institution: 'freeCodeCamp',
dateReceived: 'Dec 2025',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/responsive-web-design-v9',
imgUrl: '/certificates/responsive-web-design-certificate.webp',
description:
'Covers HTML, CSS, accessibility, responsive design, and modern web layouts.',
},
{
id: 2,
title: 'JavaScript',
institution: 'freeCodeCamp',
dateReceived: 'Dec 2025',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/javascript-v9',
imgUrl: '/certificates/javascript-certificate.webp',
description:
'Covers JavaScript fundamentals, data structures, algorithms, DOM, and programming concepts.',
},
{
id: 3,
title: 'Relational Databases',
institution: 'freeCodeCamp',
dateReceived: 'May 2026',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/relational-databases-v9',
imgUrl: '/certificates/relational-database-certificate.webp',
description:
'Covers Bash, SQL, PostgreSQL, relational databases, scripting, and Git workflows.',
},
{
id: 4,
title: 'Front-End Libraries',
institution: 'freeCodeCamp',
dateReceived: 'September 2026',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/front-end-development-libraries-v9',
imgUrl: '/certificates/frontend-developement-libraries-certificate.webp',
description:
'Covers React, state management, routing, testing, performance, CSS frameworks, and TypeScript.',
},
{
id: 5,
title: 'Back-End & API',
institution: 'freeCodeCamp',
dateReceived: 'October 2026',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/back-end-development-and-apis-v9',
imgUrl: '/certificates/backend-development-and-apis.webp',
description:
'Covers Node.js, npm, HTTP, Express, middleware, REST APIs, WebSockets, JWT, security, and authentication.',
},
];

export const certificates: Certificate[] = CERTIFICATES.slice(-3).reverse();
22 changes: 5 additions & 17 deletions src/data/experiences.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { workExperiences, CERTIFICATES } from './experiences';
import { isNotEmpty, isValidUrl } from '@/tests/utils';
import { WORK_EXPERIENCES } from './experiences';
import { isNotEmpty } from '@/tests/utils';

describe('Work Experiences and Certificates data', () => {
describe('Work Experiences data', () => {
test('every work experience has a non-empty position', () => {
const positions = workExperiences.map(({ position }) => position);
const positions = WORK_EXPERIENCES.map(({ position }) => position);
expect(isNotEmpty(positions)).toBe(true);
});
test('every work experience has a non-empty company name', () => {
const companyNames = workExperiences.map(({ companyName }) => companyName);
const companyNames = WORK_EXPERIENCES.map(({ companyName }) => companyName);
expect(isNotEmpty(companyNames)).toBe(true);
});
test('every certificate has a non-empty 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);
expect(isNotEmpty(institutions)).toBe(true);
});
test('every certificate uses a valid HTTPS URL', () => {
const urls = CERTIFICATES.map(({ urlLink }) => urlLink);
expect(isValidUrl(urls)).toBe(true);
});
});
51 changes: 1 addition & 50 deletions src/data/experiences.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WorkExperience, Certificate } from '@/types/experience';
import { WorkExperience } from '@/types/experience';

export const WORK_EXPERIENCES: WorkExperience[] = [
{
Expand Down Expand Up @@ -35,52 +35,3 @@ export const WORK_EXPERIENCES: WorkExperience[] = [

export const workExperiences: WorkExperience[] =
WORK_EXPERIENCES.slice(-2).reverse();

export const CERTIFICATES: Certificate[] = [
{
id: 1,
title: 'Responsive Web Design',
institution: 'freeCodeCamp',
dateReceived: 'Dec 2025',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/responsive-web-design-v9',
imgUrl: '/certificates/responsive-web-design-certificate.webp',
description:
'Covers HTML, CSS, accessibility, responsive design, and modern web layouts.',
},
{
id: 2,
title: 'JavaScript',
institution: 'freeCodeCamp',
dateReceived: 'Dec 2025',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/javascript-v9',
imgUrl: '/certificates/javascript-certificate.webp',
description:
'Covers JavaScript fundamentals, data structures, algorithms, DOM, and programming concepts.',
},
{
id: 3,
title: 'Relational Databases',
institution: 'freeCodeCamp',
dateReceived: 'May 2026',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/relational-databases-v9',
imgUrl: '/certificates/relational-database-certificate.webp',
description:
'Covers Bash, SQL, PostgreSQL, relational databases, scripting, and Git workflows.',
},
{
id: 4,
title: 'Front-End Libraries',
institution: 'freeCodeCamp',
dateReceived: 'September 2026',
urlLink:
'https://www.freecodecamp.org/certification/codingwithjiro/front-end-development-libraries-v9',
imgUrl: '/certificates/frontend-developement-libraries-certificate.webp',
description:
'Covers React, state management, routing, testing, performance, CSS frameworks, and TypeScript.',
},
];

export const certificates: Certificate[] = CERTIFICATES.slice(-3).reverse();
2 changes: 1 addition & 1 deletion tests/certificates.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test';
import { certificates, CERTIFICATES } from '@/data/experiences';
import { certificates, CERTIFICATES } from '@/data/certificates';

test.describe('Certificates', () => {
test('visitor can see the Certificates section', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion tests/keyboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import { tabUntilFocused } from './utils/keyboard';
import { projectList } from '@/data/project-list';
import { blogList } from '@/data/blog-list';
import { certificates } from '@/data/experiences';
import { certificates } from '@/data/certificates';

test.describe('Keyboard Accessibility', () => {
test.beforeEach(async ({ page }) => {
Expand Down
Loading