diff --git a/public/img/profile-picture_600x600.jpg b/public/img/profile-picture_600x600.jpg deleted file mode 100644 index 7da4f11..0000000 Binary files a/public/img/profile-picture_600x600.jpg and /dev/null differ diff --git a/public/projects/keyboard-warriors-dark.png b/public/projects/keyboard-warriors-dark.png new file mode 100644 index 0000000..0d6f939 Binary files /dev/null and b/public/projects/keyboard-warriors-dark.png differ diff --git a/public/projects/keyboard-warriors.png b/public/projects/keyboard-warriors.png new file mode 100644 index 0000000..8d742b5 Binary files /dev/null and b/public/projects/keyboard-warriors.png differ diff --git a/public/projects/no-one-asked-dark.png b/public/projects/no-one-asked-dark.png new file mode 100644 index 0000000..5f01050 Binary files /dev/null and b/public/projects/no-one-asked-dark.png differ diff --git a/public/projects/no-one-asked.png b/public/projects/no-one-asked.png new file mode 100644 index 0000000..9272d99 Binary files /dev/null and b/public/projects/no-one-asked.png differ diff --git a/public/projects/ticktock-dark.png b/public/projects/ticktock-dark.png new file mode 100644 index 0000000..de4a1f2 Binary files /dev/null and b/public/projects/ticktock-dark.png differ diff --git a/public/projects/ticktock.png b/public/projects/ticktock.png new file mode 100644 index 0000000..92ee815 Binary files /dev/null and b/public/projects/ticktock.png differ diff --git a/public/projects/where-in-the-world-dark.png b/public/projects/where-in-the-world-dark.png new file mode 100644 index 0000000..e17bb89 Binary files /dev/null and b/public/projects/where-in-the-world-dark.png differ diff --git a/public/projects/where-in-the-world.png b/public/projects/where-in-the-world.png new file mode 100644 index 0000000..615a527 Binary files /dev/null and b/public/projects/where-in-the-world.png differ diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 1d90987..2e7d854 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -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 }>; @@ -45,10 +45,18 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
-
+
+ +
- -
- {imgAlt} -
); }, diff --git a/src/components/sections/hero.tsx b/src/components/sections/hero.tsx index 5ba6243..0a8e24c 100644 --- a/src/components/sections/hero.tsx +++ b/src/components/sections/hero.tsx @@ -5,14 +5,16 @@ import { socialLinks } from '@/data/social-links'; const Hero = () => { return (
- Elmar Chavez - Full Stack Developer +
+ Elmar Chavez - Full Stack Developer +
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

+
+

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', + ); + }); });