@@ -76,6 +94,7 @@ export default function ContactPage() {
name="email"
required
className="w-full px-4 py-3 border border-white bg-black rounded focus:outline-none focus:ring-2 focus:ring-yellow-500"
+ data-test-id="contact-email"
/>
@@ -91,12 +110,20 @@ export default function ContactPage() {
rows={6}
required
className="w-full px-4 py-3 border border-white bg-black rounded focus:outline-none focus:ring-2 focus:ring-yellow-500"
+ data-test-id="contact-message"
>
-
+ {isLoading ? (
+
// Mostra o spinner enquanto está carregando
+ ) : (
+
+ )}
{isSubmitted && (
-
+
Thank you! Your message has been sent successfully.
)}
@@ -104,8 +131,15 @@ export default function ContactPage() {
-
-
+
+
Follow Me
@@ -113,47 +147,72 @@ export default function ContactPage() {
href="https://www.instagram.com/brunomachadors/"
target="_blank"
rel="noopener noreferrer"
- aria-label="INSTAGRAM"
- className="hover:text-yellow-500 transition transform hover:scale-110 uppercase"
+ aria-label="Instagram"
+ data-test-id="social-instagram"
+ className="hover:scale-110 transition transform"
>
-
instagram
+
- linkedin
+
- github
+
- medium
+
- email
+
-
+
);
}
diff --git a/src/app/content/skills.ts b/src/app/content/skills.ts
index 591067b..7182335 100644
--- a/src/app/content/skills.ts
+++ b/src/app/content/skills.ts
@@ -53,12 +53,12 @@ export const SKILLS: SkillCategory[] = [
{
text: 'HTML',
description:
- 'Currently, I teach HTML basics to my mentees as part of a mentorship program. I also offer a course available at https://brunomachadors.github.io/webclass/.',
+ 'Currently, I teach HTML basics to my mentees as part of a mentorship program.',
},
{
text: 'CSS',
description:
- 'CSS is the foundation of web styling. I enjoy creating visually appealing components and also teach CSS as part of my mentorship program, available at https://brunomachadors.github.io/webclass/.',
+ 'CSS is the foundation of web styling. I enjoy creating visually appealing components and also teach CSS as part of my mentorship program.',
},
{
text: 'Redux',
diff --git a/src/app/experience/[companyYear]/page.tsx b/src/app/experience/[companyYear]/page.tsx
index 0f82270..e444abc 100644
--- a/src/app/experience/[companyYear]/page.tsx
+++ b/src/app/experience/[companyYear]/page.tsx
@@ -4,44 +4,97 @@ import Link from 'next/link';
import Image from 'next/image';
import { useParams } from 'next/navigation';
import { EXPERIENCES } from '@/app/content/experiences';
+import LoadingSpinner from '@/app/components/Loading/Loading';
+import { useState, useEffect } from 'react';
export default function ExperiencePage() {
const params = useParams();
const companyYear = params.companyYear;
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Simulating a loading delay for demonstration
+ const timeout = setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+
+ return () => clearTimeout(timeout);
+ }, []);
+
const experience = EXPERIENCES.find(
(exp) =>
`${exp.company.toLowerCase()}-${exp.year.toLowerCase()}` === companyYear
);
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
if (!experience) {
return (
-
Experience not found.
+
+ Experience not found.
+
);
}
return (
-
- {/* Botão para voltar */}
-
+
{/* Título Principal */}
-
+
{experience.company}
{/* Período e Localização */}
-
+
{experience.period}
-
+
Location: {experience.location}
{/* Projetos */}
-
+
{experience.fullDescription.map((project, index) => (
-
-
+
+
{project.project}
@@ -59,8 +112,16 @@ export default function ExperiencePage() {
{/* Galeria */}
{project.gallery && project.gallery.length > 0 && (
-
-
+
+
Gallery
@@ -68,6 +129,7 @@ export default function ExperiencePage() {
Back to Timeline
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4f0c1e9..0835ac6 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,6 +1,32 @@
+'use client';
+import { useState, useEffect } from 'react';
import LinkButton from './components/Button/LinkButton';
+import LoadingSpinner from './components/Loading/Loading';
export default function Home() {
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+
+ return () => clearTimeout(timeout);
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
return (
([]);
- const [selectedTag, setSelectedTag] = useState('All'); // Default filter to 'All'
+ const [feedItems, setFeedItems] = useState(null);
+ const [selectedTag, setSelectedTag] = useState('All');
useEffect(() => {
const fetchFeed = async () => {
@@ -59,27 +59,47 @@ export default function MediumFeed() {
return paragraph?.textContent || '';
};
- // Get unique tags from the posts, sort them alphabetically
+ if (feedItems === null) {
+ return (
+
+
+
+ );
+ }
+
const allTags = Array.from(
new Set(feedItems.flatMap((post) => post.categories))
).sort((a, b) => a.localeCompare(b));
const tags = ['All', ...allTags];
- // Filter posts based on selected tag
const filteredPosts =
selectedTag === 'All'
? feedItems
: feedItems.filter((post) => post.categories.includes(selectedTag));
return (
-
- {/* Title */}
-
+
+
My Medium Posts
- {/* Filter Tabs */}
-
+
{tags.map((tag, index) => (
@@ -97,52 +119,101 @@ export default function MediumFeed() {
- {/* Posts Grid */}
-
+
{filteredPosts.map((post, index) => (
{post.thumbnail && (
-
+
)}
-
-
+
+
{post.title}
-
+
{extractFirstParagraph(post.content)}
-
+
{new Date(post.pubDate).toLocaleDateString()} by {post.author}
-
+
{post.categories.map((category, idx) => (
{category}
))}
-
))}
-
+
+
+ {/* Go to Contacts Button */}
+
+
+
+
);
}
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index c329021..24b5516 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -1,65 +1,116 @@
'use client';
import Image from 'next/image';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { PROJECTS } from '../content/projects';
import { projectStyles } from '../styles/projectStyles';
import LinkButton from '../components/Button/LinkButton';
+import LoadingSpinner from '../components/Loading/Loading';
export default function ProjectsPage() {
+ const [isClient, setIsClient] = useState(false);
const [openSection, setOpenSection] = useState
(null);
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
const toggleSection = (title: string, sectionIndex: number) => {
const key = `${title}-${sectionIndex}`;
setOpenSection(openSection === key ? null : key);
};
+ if (!isClient) {
+ return (
+
+
+
+ );
+ }
+
return (
-
-
- {PROJECTS.map((project) => (
+
+
+ Projects
+
+
+
+ {PROJECTS.map((project, projectIndex) => (
{/* Logo Section */}
-
+
{/* Content Section */}
-
-
+
+
{project.title}
-
+
{project.description}
- {project.sections.map((section, index) => (
+ {project.sections.map((section, sectionIndex) => (
toggleSection(project.title, index)}
+ onClick={() => toggleSection(project.title, sectionIndex)}
+ aria-expanded={
+ openSection === `${project.title}-${sectionIndex}`
+ }
+ aria-controls={`section-content-${projectIndex}-${sectionIndex}`}
+ data-test-id={`section-toggle-${projectIndex}-${sectionIndex}`}
>
{section.title}
- {openSection === `${project.title}-${index}` && (
-
+ {openSection === `${project.title}-${sectionIndex}` && (
+
{section.content}
)}
@@ -80,9 +135,18 @@ export default function ProjectsPage() {
{/* Go to Posts Button */}
-
+
);
}
diff --git a/src/app/resume/page.tsx b/src/app/resume/page.tsx
index e5c87b5..38c1302 100644
--- a/src/app/resume/page.tsx
+++ b/src/app/resume/page.tsx
@@ -1,52 +1,119 @@
'use client';
+
+import { useState, useEffect } from 'react';
import Link from 'next/link';
import { EXPERIENCES } from '../content/experiences';
import LinkButton from '../components/Button/LinkButton';
+import LoadingSpinner from '../components/Loading/Loading';
export default function Resume() {
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => setIsLoading(false), 500);
+ return () => clearTimeout(timeout);
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
return (
-
- {/* Linha do Tempo */}
-
+
+
+ Resume Page
+
+
+ {/* Timeline */}
+
{EXPERIENCES.map((item, index) => (
-
- {/* Ano */}
-
+
+ {/* Year */}
+
{item.year}
-
+
- {/* Detalhes da Vaga */}
-
-
+ {/* Job Details */}
+
+
{item.company}
-
{item.period}
-
+
+ {item.period}
+
+
{item.role}
-
+
{item.shortDescription}
- {/* Centralizar texto e botão */}
-
+ {/* Centralize button */}
+
))}
- {/* Botão Centralizado Abaixo */}
-
-
+
+ {/* Button to go to Skills */}
+
+
-
+
);
}
diff --git a/src/app/skills/page.tsx b/src/app/skills/page.tsx
index 4a4c9af..5fd6fa5 100644
--- a/src/app/skills/page.tsx
+++ b/src/app/skills/page.tsx
@@ -1,13 +1,20 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import SkillButton from '../components/Button/SkillButton';
import { SKILLS } from '../content/skills';
import LinkButton from '../components/Button/LinkButton';
+import LoadingSpinner from '../components/Loading/Loading';
export default function SkillsPage() {
- const [selectedTab, setSelectedTab] = useState
('All'); // Aba "All" selecionada por padrão
+ const [selectedTab, setSelectedTab] = useState('All'); // Default tab
const [activeSkill, setActiveSkill] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => setIsLoading(false), 500); // Simulating data loading
+ return () => clearTimeout(timeout);
+ }, []);
const categories =
selectedTab === 'All'
@@ -21,10 +28,34 @@ export default function SkillsPage() {
setActiveSkill((prev) => (prev === skill ? null : skill));
};
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
return (
-
+
+
+ Skills Page
+
+
{/* Tabs */}
-
+
{tabs.map((tab, index) => (
@@ -43,13 +78,25 @@ export default function SkillsPage() {
{/* Skills */}
-
+
{categories.map((subcategory, subIndex) => (
-
-
+
+
{subcategory.name}
-
+
{subcategory.items.map(({ text, description }, itemIndex) => (
handleSkillClick(text)}
+ testId={`skill-button-${text.toLowerCase()}`}
+ aria-pressed={activeSkill === text}
/>
))}
-
+
))}
- {/* Divisória - Final das Skills */}
-
+ {/* Divider */}
+
{/* Go to Projects Button */}
-
+
);
}
diff --git a/src/app/styles/skillStyles.ts b/src/app/styles/skillStyles.ts
index 14bbf1e..6ab8dd5 100644
--- a/src/app/styles/skillStyles.ts
+++ b/src/app/styles/skillStyles.ts
@@ -62,7 +62,7 @@ export const skillColors: Record
= {
// Interpersonal Skills
'Public Speaking': 'text-purple-500 border-purple-500',
'Didactic Explanations': 'text-purple-500 border-purple-500',
- 'Talks on Accessibility and Security': 'text-purple-500 border-purple-500',
+ Talks: 'text-purple-500 border-purple-500',
'Commitment to Goals': 'text-blue-400 border-blue-400',
'Critical Thinking': 'text-blue-400 border-blue-400',
diff --git a/tests/about.spec.ts b/tests/about.spec.ts
index 10c4502..6902585 100644
--- a/tests/about.spec.ts
+++ b/tests/about.spec.ts
@@ -31,7 +31,7 @@ test.describe('About Page', () => {
const { title, content } = ABOUT_DATA.sections[i];
await test.step(`Section ${i}: ${title}`, async () => {
- await aboutPage.clickToggleIcon(i);
+ await aboutPage.clickSession(i);
await aboutPage.validateSectionTitleVisible(i, title);
await aboutPage.validateSectionContentVisible(i, content);
});
diff --git a/tests/pages/AboutPage.ts b/tests/pages/AboutPage.ts
index a3b3e94..1954e53 100644
--- a/tests/pages/AboutPage.ts
+++ b/tests/pages/AboutPage.ts
@@ -43,18 +43,16 @@ export class AboutPage {
await expect(containerLocator).toBeVisible();
}
- async clickToggleIcon(index: number) {
- const toggleIcon = this.page.locator(
- `[data-test-id="toggle-icon-${index}"]`
- );
- await toggleIcon.click();
+ async clickSession(index: number) {
+ const session = this.page.locator(`[data-test-id="section-${index}"]`);
+ await session.click();
const contentLocator = this.page.locator(
`[data-test-id="section-content-${index}"]`
);
await this.page.waitForTimeout(300);
if (!(await contentLocator.isVisible())) {
- await toggleIcon.click();
+ await session.click();
await this.page.waitForTimeout(300);
}
}
@@ -76,7 +74,7 @@ export class AboutPage {
);
if (!(await contentLocator.isVisible())) {
- await this.clickToggleIcon(index);
+ await this.clickSession(index);
}
await expect(contentLocator).toBeVisible();