Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bcfd7da
refactor(hero): update profile picture on landing page
CodingWithJiro Aug 29, 2026
cf2a0ff
chore: remove old profile picture image
CodingWithJiro Aug 29, 2026
f4c63bf
feat(blog): add a back to homepage link to article pages
CodingWithJiro Aug 29, 2026
a9376e8
test(e2e): visitors can go back to homepage using the Back to homepag…
CodingWithJiro Aug 29, 2026
645f0eb
refactor(blog): swap banner image positions to the left side in home …
CodingWithJiro Aug 29, 2026
597ea3c
feat(style): make border color token stronger in light theme
CodingWithJiro Aug 29, 2026
5cc1fb0
fix(blog): adjust size of Back to homepage and Read more articles lin…
CodingWithJiro Aug 29, 2026
a746bf6
feat(projects): add initial projects page and metadata
CodingWithJiro Aug 29, 2026
bc962ee
feat(projects): add view all link to projects section on homepage
CodingWithJiro Aug 29, 2026
3344390
test(e2e): visitor can see the Project section
CodingWithJiro Aug 29, 2026
2d779ac
test(e2e): visitor can view the Projects page
CodingWithJiro Aug 29, 2026
3d61ef0
refactor(test): make one E2E test for projects feature not hardcoded …
CodingWithJiro Aug 29, 2026
8aed70a
feat(a11y): add aria label to each project link in Projects section
CodingWithJiro Aug 29, 2026
63fa28d
refactor(test): update renders every project link integration test
CodingWithJiro Aug 29, 2026
af32af5
feat(projects): add projects page header
CodingWithJiro Aug 30, 2026
ed09126
chore(types): add github url and technologies array to Project type
CodingWithJiro Aug 30, 2026
65a12c0
feat(projects): add more projects in project list
CodingWithJiro Sep 1, 2026
9442f19
chore(projects): add site light and dark theme images for additional …
CodingWithJiro Sep 1, 2026
64be4ea
feat(projects): add initial grid layout to projects page
CodingWithJiro Sep 1, 2026
1422502
feat(projects): style and layout projects list
CodingWithJiro Sep 1, 2026
254d965
feat(a11y): add outline styles to project github and site links when …
CodingWithJiro Sep 2, 2026
4d4d30e
feat(a11y): add aria labels to GitHub and website links in projects page
CodingWithJiro Sep 2, 2026
9623ccb
feat(projects): add view all projects link and back to homepage link …
CodingWithJiro Sep 2, 2026
6a6165e
feat(projects): add more spacing between project title and project de…
CodingWithJiro Sep 2, 2026
e977c81
test(e2e): visitor can see the Projects page
CodingWithJiro Sep 2, 2026
0460050
test(e2e): visitor can open a project website from projects page
CodingWithJiro Sep 2, 2026
4dbc32a
test(e2e): visitor can open a project GitHub repository from projects…
CodingWithJiro Sep 2, 2026
4438cbd
test(e2e): visitor can return to homepage via Back to homepage link
CodingWithJiro Sep 2, 2026
2ed31e3
test(e2e): visitor can view all projects and navigate towards my Proj…
CodingWithJiro Sep 2, 2026
fce5833
test(project-list): update projectList to PROJECTS to test all includ…
CodingWithJiro Sep 2, 2026
12d4c49
test(project-list): every project has a valid GitHub URL
CodingWithJiro Sep 2, 2026
bd3efdd
test(project-list): every project has at least three technologies
CodingWithJiro Sep 2, 2026
2151f14
test(projects): renders view all link
CodingWithJiro Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed public/img/profile-picture_600x600.jpg
Binary file not shown.
Binary file added public/projects/keyboard-warriors-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/keyboard-warriors.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/no-one-asked-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/no-one-asked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/ticktock-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/ticktock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/where-in-the-world-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/projects/where-in-the-world.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BreadcrumbItem } from '@/types/breadcrumb-item';
import ArticleHeader from '@/components/sections/article-header';
import Footer from '@/components/sections/footer';
import Link from 'next/link';
import { ArrowRight } from 'lucide-react';
import { ArrowLeft, ArrowRight } from 'lucide-react';

type ArticlePageProps = {
params: Promise<{ slug: string }>;
Expand Down Expand Up @@ -45,10 +45,18 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
<Article />
</article>

<div className="flex justify-end">
<div className="flex items-center justify-between">
<Link
href="/"
className="focus-visible:outline-muted-foreground flex items-center gap-1 rounded-sm text-[0.85rem] font-normal underline-offset-4 outline-2 outline-offset-4 outline-transparent hover:underline md:text-[0.875rem]"
>
<ArrowLeft aria-hidden="true" size={16} />
<span>Back to homepage</span>
</Link>

<Link
href="/blog"
className="focus-visible:outline-muted-foreground flex w-fit items-center gap-1 rounded-sm text-[0.875rem] font-normal underline-offset-4 outline-2 outline-offset-4 outline-transparent transition-colors duration-150 ease-in-out hover:underline md:text-right"
className="focus-visible:outline-muted-foreground flex items-center gap-1 rounded-sm text-right text-[0.85rem] font-normal underline-offset-4 outline-2 outline-offset-4 outline-transparent hover:underline md:text-[0.875rem]"
>
<span>Read more articles</span>
<ArrowRight aria-hidden="true" size={16} />
Expand Down
5 changes: 4 additions & 1 deletion src/app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ const blogsLatestOrder = BLOGS.slice().reverse();

export default function BlogPage() {
return (
<main id="main" className="mx-auto max-w-130 px-4 py-8 md:max-w-2xl">
<main
id="main"
className="mx-auto min-h-screen max-w-130 px-4 py-8 md:max-w-2xl"
>
<header className="mb-8 flex items-center justify-between">
<Breadcrumb items={[]} currentLabel="Blog" />
<ThemeToggle />
Expand Down
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
--accent: oklch(0.58 0.1 240);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.85 0.02 255);
--border: oklch(30% 0.00002 271.152 / 0.6);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
Expand Down
159 changes: 159 additions & 0 deletions src/app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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 { PROJECTS } from '@/data/project-list';
import Image from 'next/image';
import Link from 'next/link';
import { FaGithub } from 'react-icons/fa';
import { ArrowLeft, ArrowRight, Globe } from 'lucide-react';

const metaDescription =
'List of built projects by Elmar Chavez as a full stack developer.';

export const metadata: Metadata = {
title: 'Elmar Chavez | Projects',
description: metaDescription,
keywords: [
'Elmar Chavez Projects',
'Web Development Projects',
'GitHub Repos',
'Web Applications',
'Software Engineering Projects',
],
openGraph: {
title: 'Projects | Elmar Chavez',
description: metaDescription,
},
twitter: {
card: 'summary_large_image',
title: 'Projects | Elmar Chavez',
description: metaDescription,
},
};

export default function ProjectsPage() {
return (
<main
id="main"
className="mx-auto min-h-screen max-w-130 px-4 py-8 md:max-w-4xl"
>
<header className="mb-8 flex items-center justify-between">
<Breadcrumb items={[]} currentLabel="Projects" />
<ThemeToggle />
</header>

<section>
<h1 className="mb-1 text-lg font-bold md:text-3xl">Projects</h1>
<p className="mb-6 text-sm font-medium md:text-base">
Things I built and worked on.
</p>
</section>

<ul className="mb-8 grid grid-cols-1 gap-y-3 min-[520px]:grid-cols-2 min-[520px]:gap-x-3 md:grid-cols-3">
{PROJECTS.map(
({
id,
title,
description,
imgUrl,
imgUrlDark,
siteUrl,
githubUrl,
technologies,
}) => {
return (
<li
className="border-ring/20 focus-within:border-ring has-hover:border-ring bg-background text-foreground mx-auto flex max-w-60 flex-col overflow-hidden rounded-lg border transition-all duration-150 ease-in-out min-[850px]:max-w-70"
key={id}
>
<div className="relative mb-4 aspect-video max-w-60 shrink-0 overflow-hidden min-[850px]:max-w-70">
<Image
className="border-ring/20 mb-2 h-full w-full rounded-lg border object-cover dark:hidden"
src={imgUrl}
alt=""
width={1440}
height={960}
loading="eager"
/>
<Image
className="border-ring/20 mb-2 hidden h-full w-full rounded-lg border object-cover dark:block"
src={imgUrlDark}
alt={`${title} project preview.`}
width={1440}
height={960}
loading="eager"
/>

<ul className="absolute right-1 bottom-1 flex gap-1 text-[0.50rem] font-light">
{technologies.map((technology) => {
return (
<li
key={technology}
className="bg-background/90 border-border/30 rounded-sm border px-1 py-0.5 text-center"
>
{technology}
</li>
);
})}
</ul>
</div>

<section className="flex h-full flex-col justify-between">
<header className="mb-4 px-4">
<h2 className="mb-2 text-[0.875rem] font-semibold">
{title}
</h2>
<p className="text-[0.75rem]">{description}</p>
</header>

<div className="mb-4 flex items-center justify-center gap-2 px-4 text-[0.75rem]">
<Link
href={githubUrl}
target="_blank"
className="bg-background border-border/50 hover:bg-foreground hover:text-background focus-visible:outline-muted-foreground flex items-center justify-center gap-1 rounded-sm border px-4 py-1 outline-offset-4 outline-transparent transition-colors duration-150 ease-in-out focus-visible:outline-2 min-[850px]:px-6"
aria-label={`Go to ${title}'s GitHub repository.`}
>
<FaGithub size={12} />
<span>GitHub</span>
</Link>
<Link
href={siteUrl}
target="_blank"
className="bg-background border-border/50 text-foreground hover:bg-foreground hover:text-background focus-visible:outline-muted-foreground flex items-center justify-center gap-1 rounded-sm border px-4 py-1 outline-offset-4 outline-transparent transition-colors duration-150 ease-in-out focus-visible:outline-2 min-[850px]:px-6"
aria-label={`Go to ${title}'s live website.`}
>
<Globe size={12} />
<span>Website</span>
</Link>
</div>
</section>
</li>
);
},
)}
</ul>

<div className="flex items-center justify-between">
<Link
href="/"
className="focus-visible:outline-muted-foreground flex items-center gap-1 rounded-sm text-[0.85rem] font-normal underline-offset-4 outline-2 outline-offset-4 outline-transparent hover:underline md:text-[0.875rem]"
>
<ArrowLeft aria-hidden="true" size={16} />
<span>Back to homepage</span>
</Link>

<Link
href="https://github.com/CodingWithJiro/Projects-Hub"
className="focus-visible:outline-muted-foreground flex items-center gap-1 rounded-sm text-[0.85rem] font-normal underline-offset-4 outline-2 outline-offset-4 outline-transparent hover:underline md:text-[0.875rem]"
target="_blank"
>
<span>View all projects</span>
<ArrowRight aria-hidden="true" size={16} />
</Link>
</div>

<Footer />
</main>
);
}
20 changes: 10 additions & 10 deletions src/components/sections/blog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ 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}
>
<div className="border-ring/20 focus-within:border-ring has-hover:border-ring hidden w-55 shrink-0 overflow-hidden rounded-xl border-2 transition-all duration-150 ease-in-out md:block">
<Image
src={imgUrl}
alt={imgAlt}
width={800}
height={400}
className="aspect-video w-full object-cover"
/>
</div>

<article>
<h3 className="mb-2 text-[0.875rem] font-semibold">
{title}
Expand Down Expand Up @@ -60,16 +70,6 @@ const Blog = () => {
</Link>
</div>
</article>

<div className="border-ring/20 focus-within:border-ring has-hover:border-ring hidden w-55 shrink-0 overflow-hidden rounded-xl border-2 transition-all duration-150 ease-in-out md:block">
<Image
src={imgUrl}
alt={imgAlt}
width={800}
height={400}
className="aspect-video w-full object-cover"
/>
</div>
</li>
);
},
Expand Down
18 changes: 10 additions & 8 deletions src/components/sections/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { socialLinks } from '@/data/social-links';
const Hero = () => {
return (
<header className="mb-6 flex flex-row items-center gap-2 md:gap-4">
<Image
src="/img/profile-picture_600x600.jpg"
alt="Elmar Chavez - Full Stack Developer"
className="border-ring/20 h-25 w-25 rounded-lg border md:h-40 md:w-40"
width={600}
height={600}
loading="eager"
/>
<div className="border-ring/20 w-25 shrink-0 overflow-hidden rounded-lg border md:w-30">
<Image
src="/img/author-profile_683x771.png"
alt="Elmar Chavez - Full Stack Developer"
className="aspect-square w-full scale-105 object-cover"
width={600}
height={600}
loading="eager"
/>
</div>

<section className="flex w-full flex-row justify-between">
<div>
Expand Down
9 changes: 8 additions & 1 deletion src/components/sections/projects.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ describe('Projects section', () => {
});
test('renders every project link', () => {
render(<Projects />);
const projectLinks = screen.getAllByRole('link');
const projectLinks = screen.getAllByRole('link', {
name: /^go to project/i,
});
expect(projectLinks).toHaveLength(projectList.length);
});
test('renders view all link', () => {
render(<Projects />);
const viewAllLink = screen.getByRole('link', { name: /view all/i });
expect(viewAllLink).toBeInTheDocument();
});
});
7 changes: 6 additions & 1 deletion src/components/sections/projects.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Image from 'next/image';
import { projectList } from '@/data/project-list';
import ViewAllLink from '../ui/view-all-link';

const Projects = () => {
return (
<section className="border-border/40 bg-card flex w-full flex-col gap-3 rounded-lg border p-4 transition-colors duration-150 ease-in-out">
<h2 className="text-lg font-bold">Projects</h2>
<header className="flex items-center justify-between">
<h2 className="text-lg font-bold">Projects</h2>
<ViewAllLink href="/projects" />
</header>

<ul className="grid grid-cols-1 gap-y-3 min-[488px]:grid-cols-2">
{projectList.map(
Expand Down Expand Up @@ -38,6 +42,7 @@ const Projects = () => {
className="decoration-0 underline-offset-4 opacity-90 hover:underline hover:opacity-100 focus-visible:underline focus-visible:opacity-100 focus-visible:outline-none"
href={siteUrl}
target="_blank"
aria-label={`Go to project ${title}.`}
>
{title}
</a>
Expand Down
22 changes: 16 additions & 6 deletions src/data/project-list.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import { projectList } from './project-list';
import { PROJECTS } from './project-list';
import { isNotEmpty, isValidUrl } from '@/tests/utils';

describe('Project List data', () => {
test('every project uses a valid HTTPS URL', () => {
const siteUrls = projectList.map(({ siteUrl }) => siteUrl);
const siteUrls = PROJECTS.map(({ siteUrl }) => siteUrl);
expect(isValidUrl(siteUrls)).toBe(true);
});
test('every project has a non-empty image path', () => {
const imgUrls = projectList.map(({ imgUrl }) => imgUrl);
const imgUrlsDark = projectList.map(({ imgUrlDark }) => imgUrlDark);
const imgUrls = PROJECTS.map(({ imgUrl }) => imgUrl);
const imgUrlsDark = PROJECTS.map(({ imgUrlDark }) => imgUrlDark);
const allImgUrls = [...imgUrls, ...imgUrlsDark];
expect(isNotEmpty(allImgUrls)).toBe(true);
});
test('every project has a non-empty title', () => {
const titles = projectList.map(({ title }) => title);
const titles = PROJECTS.map(({ title }) => title);
expect(isNotEmpty(titles)).toBe(true);
});
test('every project has a non-empty description', () => {
const descriptions = projectList.map(({ description }) => description);
const descriptions = PROJECTS.map(({ description }) => description);
expect(isNotEmpty(descriptions)).toBe(true);
});
test('every project has a valid GitHub URL', () => {
const githubUrls = PROJECTS.map(({ githubUrl }) => githubUrl);
expect(isValidUrl(githubUrls)).toBe(true);
});
test('every project has at least three technologies', () => {
const technologies = PROJECTS.map(({ technologies }) => technologies);
expect(technologies.every((technology) => technology.length >= 3)).toBe(
true,
);
});
});
Loading
Loading