diff --git a/src/components/sections/projects.test.tsx b/src/components/sections/projects.test.tsx
index 42ff734..4f89f56 100644
--- a/src/components/sections/projects.test.tsx
+++ b/src/components/sections/projects.test.tsx
@@ -15,7 +15,14 @@ describe('Projects section', () => {
});
test('renders every project link', () => {
render(
);
- 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(
);
+ const viewAllLink = screen.getByRole('link', { name: /view all/i });
+ expect(viewAllLink).toBeInTheDocument();
+ });
});
diff --git a/src/components/sections/projects.tsx b/src/components/sections/projects.tsx
index 1ebf4b9..c9697db 100644
--- a/src/components/sections/projects.tsx
+++ b/src/components/sections/projects.tsx
@@ -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 (
- Projects
+
{projectList.map(
@@ -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}
diff --git a/src/data/project-list.test.ts b/src/data/project-list.test.ts
index be5f55e..047eb26 100644
--- a/src/data/project-list.test.ts
+++ b/src/data/project-list.test.ts
@@ -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,
+ );
+ });
});
diff --git a/src/data/project-list.ts b/src/data/project-list.ts
index 8bdf974..1855598 100644
--- a/src/data/project-list.ts
+++ b/src/data/project-list.ts
@@ -1,36 +1,93 @@
import type { Project } from '@/types/project';
-export const projectList: Project[] = [
+export const PROJECTS: Project[] = [
{
id: 1,
title: 'JavaScript Fundamentals',
- description: 'Technical W3Schools JS Tutorial clone site!',
+ description: 'Learn about JavaScript for web development!',
imgUrl: '/projects/javascript-fundamentals.png',
imgUrlDark: '/projects/javascript-fundamentals-dark.png',
siteUrl: 'https://javascript-fundamentals-jiro.netlify.app/',
+ githubUrl:
+ 'https://github.com/CodingWithJiro/w3schools-js-tutorial-clone-site',
+ technologies: ['HTML', 'CSS', 'JavaScript'],
},
{
id: 2,
- title: 'Extensions Manager',
- description: 'Manage your favorite browser extensions!',
- imgUrl: '/projects/extensions-manager.png',
- imgUrlDark: '/projects/extensions-manager-dark.png',
- siteUrl: 'https://browser-extension-fm-jiro.netlify.app/',
- },
- {
- id: 3,
title: 'Veil and Visions',
description: 'Draw three cards to reveal your past, present, and future!',
imgUrl: '/projects/veil-and-visions.png',
imgUrlDark: '/projects/veil-and-visions-dark.png',
siteUrl: 'https://veil-and-visions-fcc-jiro.netlify.app/',
+ githubUrl:
+ 'https://github.com/CodingWithJiro/freecodecamp-js-fortune-teller',
+ technologies: ['HTML', 'CSS', 'JavaScript'],
+ },
+ {
+ id: 3,
+ title: 'No One Asked',
+ description: 'Get a random advice, copy it or share it in X!',
+ imgUrl: '/projects/no-one-asked.png',
+ imgUrlDark: '/projects/no-one-asked-dark.png',
+ siteUrl: 'https://no-one-asked-fcc-jiro.netlify.app/',
+ githubUrl: 'https://github.com/CodingWithJiro/freecodecamp-js-no-one-asked',
+ technologies: ['JavaScript', 'API', 'Figma'],
},
{
id: 4,
+ title: 'TickTock',
+ description: 'Know the time and date from anywhere!',
+ imgUrl: '/projects/ticktock.png',
+ imgUrlDark: '/projects/ticktock-dark.png',
+ siteUrl: 'https://ticktock-jiro.netlify.app/',
+ githubUrl: 'https://github.com/CodingWithJiro/wesbos-js-30-clock',
+ technologies: ['JavaScript', 'API', 'Figma'],
+ },
+ {
+ id: 5,
+ title: 'Keyboard Warrior',
+ description: 'Smash the keys, feel the beat!',
+ imgUrl: '/projects/keyboard-warriors.png',
+ imgUrlDark: '/projects/keyboard-warriors-dark.png',
+ siteUrl: 'https://keyboard-warrior-jiro.netlify.app/',
+ githubUrl: 'https://github.com/CodingWithJiro/wesbos-js-30-drumkit',
+ technologies: ['JavaScript', 'CSS', 'Figma'],
+ },
+ {
+ id: 6,
title: 'Base Apparel',
description: 'Sign-up your email, subscribe to Base Apparel!',
imgUrl: '/projects/base-apparel.png',
imgUrlDark: '/projects/base-apparel-dark.png',
siteUrl: 'https://base-apparel-fm-jiro.netlify.app/',
+ githubUrl:
+ 'https://github.com/CodingWithJiro/frontend-mentor-base-apparel-coming-soon',
+ technologies: ['React', 'Tailwind CSS', 'JavaScript'],
+ },
+ {
+ id: 7,
+ title: 'Extensions Manager',
+ description: 'Manage your favorite browser extensions!',
+ imgUrl: '/projects/extensions-manager.png',
+ imgUrlDark: '/projects/extensions-manager-dark.png',
+ siteUrl: 'https://browser-extension-fm-jiro.netlify.app/',
+ githubUrl:
+ 'https://github.com/CodingWithJiro/frontend-mentor-browser-extension-manager',
+ technologies: ['React', 'Tailwind CSS', 'JavaScript'],
+ },
+ {
+ id: 8,
+ title: 'Where in the World',
+ description: 'Search a country and find relevant info!',
+ imgUrl: '/projects/where-in-the-world.png',
+ imgUrlDark: '/projects/where-in-the-world-dark.png',
+ siteUrl: 'https://where-in-the-world-fm-jiro.netlify.app/',
+ githubUrl:
+ 'https://github.com/CodingWithJiro/frontend-mentor-where-in-the-world',
+ technologies: ['React', 'Tailwind CSS', 'TypeScript'],
},
];
+
+export const projectList: Project[] = PROJECTS.filter(({ id }) =>
+ [1, 2, 7, 8].includes(id),
+);
diff --git a/src/types/project.ts b/src/types/project.ts
index c94d403..08a9090 100644
--- a/src/types/project.ts
+++ b/src/types/project.ts
@@ -5,4 +5,6 @@ export type Project = {
imgUrl: string;
imgUrlDark: string;
siteUrl: string;
+ githubUrl: string;
+ technologies: string[];
};
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index 203c0d4..fc29b12 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -85,4 +85,15 @@ test.describe('Blog', () => {
await blogLink.click();
await expect(page).toHaveURL('/blog');
});
+ test('visitors can go back to homepage using the Back to homepage link', async ({
+ page,
+ }) => {
+ const blogMeta = blogList[0];
+ await page.goto(blogMeta.blogUrl);
+ const backToHomeLink = page.getByRole('link', {
+ name: /back to homepage/i,
+ });
+ await backToHomeLink.click();
+ await expect(page).toHaveURL('/');
+ });
});
diff --git a/tests/projects.spec.ts b/tests/projects.spec.ts
index 50c44fb..331b746 100644
--- a/tests/projects.spec.ts
+++ b/tests/projects.spec.ts
@@ -1,17 +1,87 @@
import { test, expect } from '@playwright/test';
-import { projectList } from '@/data/project-list';
+import { projectList, PROJECTS } from '@/data/project-list';
-test('visitor can open a project in a new tab', async ({ page }) => {
- await page.goto('/');
- const link = page.getByRole('link', {
- name: /javascript fundamentals/i,
- });
- const project = projectList.find(
- ({ title }) => title === 'JavaScript Fundamentals',
- )!;
- expect(project).toBeDefined();
- const newPagePromise = page.waitForEvent('popup');
- await link.click();
- const newPage = await newPagePromise;
- await expect(newPage).toHaveURL(project.siteUrl);
+test.describe('Projects', () => {
+ test('visitor can see the Project section', async ({ page }) => {
+ await page.goto('/');
+ const heading = page.getByRole('heading', { name: /^projects$/i });
+ await expect(heading).toBeVisible();
+ });
+ test('visitor can open a project in a new tab', async ({ page }) => {
+ await page.goto('/');
+ const project = projectList[0];
+ const link = page.getByRole('link', {
+ name: project.title,
+ });
+ const newPagePromise = page.waitForEvent('popup');
+ await link.click();
+ const newPage = await newPagePromise;
+ await expect(newPage).toHaveURL(project.siteUrl);
+ });
+ test('visitor can view the Projects page', async ({ page }) => {
+ await page.goto('/');
+ const viewAllLink = page.locator('a[href="/projects"]');
+ await viewAllLink.click();
+ await expect(page).toHaveURL('/projects');
+ });
+ test('visitor can see the Projects page', async ({ page }) => {
+ await page.goto('/projects');
+ const heading = page.getByRole('heading', { name: /^projects$/i });
+ await expect(heading).toBeVisible();
+ const description = page.getByText(/^things i built and worked on.$/i);
+ await expect(description).toBeVisible();
+ const viewAllProjects = page.getByRole('link', {
+ name: /^view all projects$/i,
+ });
+ await expect(viewAllProjects).toBeVisible();
+ });
+ test('visitor can open a project website from projects page', async ({
+ page,
+ }) => {
+ await page.goto('/projects');
+ const project = PROJECTS[0];
+ const projectLink = page.getByRole('link', {
+ name: `Go to ${project.title}'s live website.`,
+ });
+ const newPagePromise = page.waitForEvent('popup');
+ await projectLink.click();
+ const newPage = await newPagePromise;
+ await expect(newPage).toHaveURL(project.siteUrl);
+ });
+ test('visitor can open a project GitHub repository link from projects page', async ({
+ page,
+ }) => {
+ await page.goto('/projects');
+ const project = PROJECTS[0];
+ const githubLink = page.getByRole('link', {
+ name: `Go to ${project.title}'s GitHub repository.`,
+ });
+ const newPagePromise = page.waitForEvent('popup');
+ await githubLink.click();
+ const newPage = await newPagePromise;
+ await expect(newPage).toHaveURL(project.githubUrl);
+ });
+ test('visitor can return to homepage via Back to homepage link', async ({
+ page,
+ }) => {
+ await page.goto('/projects');
+ const homeLink = page.getByRole('link', { name: /^back to homepage$/i });
+ await expect(homeLink).toHaveAttribute('href', '/');
+ await homeLink.click();
+ await expect(page).toHaveURL('/');
+ });
+ test('visitor can view all projects and navigate towards my Projects Hub GitHub repository', async ({
+ page,
+ }) => {
+ await page.goto('/projects');
+ const viewAllProjects = page.getByRole('link', {
+ name: /^view all projects$/i,
+ });
+ const newPagePromise = page.waitForEvent('popup');
+ await viewAllProjects.click();
+ const newPage = await newPagePromise;
+ await expect(newPage).toHaveURL(
+ 'https://github.com/CodingWithJiro/Projects-Hub',
+ );
+ });
});