From d7a9936ebac516951b3e1b9f35cd5207ea5c356d Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Fri, 2 Jan 2026 01:54:40 +0000
Subject: [PATCH 1/3] refactor: reorganize components structure and add scroll
restoration on navigation
---
src/App.jsx | 4 +-
src/components/{ui => common}/BackToTop.jsx | 0
.../{ui => common}/BackToTop.test.jsx | 0
src/components/{ui => common}/CTASection.jsx | 0
src/components/{ => common}/SEO.jsx | 0
src/components/common/SEO.test.jsx | 103 ++++++++++++++++++
src/components/common/ScrollRestoration.jsx | 18 +++
.../common/ScrollRestoration.test.jsx | 68 ++++++++++++
.../{portfolio => common}/SocialLinks.jsx | 0
src/components/{portfolio => ui}/Lightbox.jsx | 0
src/pages/ContactPage.jsx | 2 +-
src/pages/FAQPage.jsx | 4 +-
src/pages/HomePage.jsx | 4 +-
src/pages/NotFoundPage.jsx | 2 +-
src/pages/PortfolioPage.jsx | 8 +-
src/pages/ServicesPage.jsx | 4 +-
16 files changed, 204 insertions(+), 13 deletions(-)
rename src/components/{ui => common}/BackToTop.jsx (100%)
rename src/components/{ui => common}/BackToTop.test.jsx (100%)
rename src/components/{ui => common}/CTASection.jsx (100%)
rename src/components/{ => common}/SEO.jsx (100%)
create mode 100644 src/components/common/SEO.test.jsx
create mode 100644 src/components/common/ScrollRestoration.jsx
create mode 100644 src/components/common/ScrollRestoration.test.jsx
rename src/components/{portfolio => common}/SocialLinks.jsx (100%)
rename src/components/{portfolio => ui}/Lightbox.jsx (100%)
diff --git a/src/App.jsx b/src/App.jsx
index 28770f6..a62e927 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -2,7 +2,8 @@ import "./App.css";
import { Routes, Route } from "react-router-dom";
import { Navbar } from "@/components/layout/Navbar";
import Footer from "@/components/layout/Footer";
-import { BackToTop } from "@/components/ui/BackToTop";
+import { BackToTop } from "@/components/common/BackToTop";
+import { ScrollRestoration } from "@/components/common/ScrollRestoration";
import HomePage from "./pages/HomePage";
import ServicesPage from "./pages/ServicesPage";
import PortfolioPage from "./pages/PortfolioPage";
@@ -13,6 +14,7 @@ import NotFoundPage from "./pages/NotFoundPage";
function App() {
return (
+
diff --git a/src/components/ui/BackToTop.jsx b/src/components/common/BackToTop.jsx
similarity index 100%
rename from src/components/ui/BackToTop.jsx
rename to src/components/common/BackToTop.jsx
diff --git a/src/components/ui/BackToTop.test.jsx b/src/components/common/BackToTop.test.jsx
similarity index 100%
rename from src/components/ui/BackToTop.test.jsx
rename to src/components/common/BackToTop.test.jsx
diff --git a/src/components/ui/CTASection.jsx b/src/components/common/CTASection.jsx
similarity index 100%
rename from src/components/ui/CTASection.jsx
rename to src/components/common/CTASection.jsx
diff --git a/src/components/SEO.jsx b/src/components/common/SEO.jsx
similarity index 100%
rename from src/components/SEO.jsx
rename to src/components/common/SEO.jsx
diff --git a/src/components/common/SEO.test.jsx b/src/components/common/SEO.test.jsx
new file mode 100644
index 0000000..4a9ea4a
--- /dev/null
+++ b/src/components/common/SEO.test.jsx
@@ -0,0 +1,103 @@
+import { render } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import { SEO } from "./SEO";
+
+describe("SEO", () => {
+ it("renders title with site name", () => {
+ render(
);
+ expect(document.title).toBe("Contactos | Chrisert");
+ });
+
+ it("renders default title when no title prop provided", () => {
+ render(
);
+ expect(document.title).toBe(
+ "Chrisert - Especialistas em ETICS e Isolamento Térmico"
+ );
+ });
+
+ it("renders meta description", () => {
+ render(
);
+ const metaDescription = document.querySelector('meta[name="description"]');
+ expect(metaDescription).toHaveAttribute("content", "Descrição personalizada");
+ });
+
+ it("renders default description when not provided", () => {
+ render(
);
+ const metaDescription = document.querySelector('meta[name="description"]');
+ expect(metaDescription?.getAttribute("content")).toContain("ETICS");
+ });
+
+ it("renders meta keywords", () => {
+ render(
);
+ const metaKeywords = document.querySelector('meta[name="keywords"]');
+ expect(metaKeywords).toHaveAttribute("content", "isolamento, capoto");
+ });
+
+ it("renders canonical URL with base URL", () => {
+ render(
);
+ const canonicalLink = document.querySelector('link[rel="canonical"]');
+ expect(canonicalLink).toHaveAttribute(
+ "href",
+ "https://chrisert.pt/contactos"
+ );
+ });
+
+ it("renders default canonical URL when not provided", () => {
+ render(
);
+ const canonicalLink = document.querySelector('link[rel="canonical"]');
+ expect(canonicalLink).toHaveAttribute("href", "https://chrisert.pt");
+ });
+
+ it("renders Open Graph meta tags", () => {
+ render(
+
+ );
+
+ const ogTitle = document.querySelector('meta[property="og:title"]');
+ const ogDescription = document.querySelector(
+ 'meta[property="og:description"]'
+ );
+ const ogUrl = document.querySelector('meta[property="og:url"]');
+ const ogType = document.querySelector('meta[property="og:type"]');
+ const ogSiteName = document.querySelector('meta[property="og:site_name"]');
+
+ expect(ogTitle).toHaveAttribute("content", "Portfolio | Chrisert");
+ expect(ogDescription).toHaveAttribute("content", "Os nossos trabalhos");
+ expect(ogUrl).toHaveAttribute("content", "https://chrisert.pt/portfolio");
+ expect(ogType).toHaveAttribute("content", "website");
+ expect(ogSiteName).toHaveAttribute("content", "Chrisert");
+ });
+
+ it("renders custom ogType", () => {
+ render(
);
+ const ogType = document.querySelector('meta[property="og:type"]');
+ expect(ogType).toHaveAttribute("content", "article");
+ });
+
+ it("renders custom ogImage", () => {
+ render(
);
+ const ogImage = document.querySelector('meta[property="og:image"]');
+ expect(ogImage).toHaveAttribute("content", "https://example.com/image.jpg");
+ });
+
+ it("renders Twitter meta tags", () => {
+ render(
);
+
+ const twitterCard = document.querySelector('meta[name="twitter:card"]');
+ const twitterTitle = document.querySelector('meta[name="twitter:title"]');
+ const twitterDescription = document.querySelector(
+ 'meta[name="twitter:description"]'
+ );
+
+ expect(twitterCard).toHaveAttribute("content", "summary_large_image");
+ expect(twitterTitle).toHaveAttribute("content", "FAQ | Chrisert");
+ expect(twitterDescription).toHaveAttribute(
+ "content",
+ "Perguntas frequentes"
+ );
+ });
+});
diff --git a/src/components/common/ScrollRestoration.jsx b/src/components/common/ScrollRestoration.jsx
new file mode 100644
index 0000000..6437ecb
--- /dev/null
+++ b/src/components/common/ScrollRestoration.jsx
@@ -0,0 +1,18 @@
+import { useEffect } from "react";
+import { useLocation } from "react-router-dom";
+
+/**
+ * Component that resets scroll position on route change.
+ * Must be placed inside a Router component.
+ */
+export const ScrollRestoration = () => {
+ const { pathname } = useLocation();
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [pathname]);
+
+ return null;
+};
+
+export default ScrollRestoration;
diff --git a/src/components/common/ScrollRestoration.test.jsx b/src/components/common/ScrollRestoration.test.jsx
new file mode 100644
index 0000000..94f7a7a
--- /dev/null
+++ b/src/components/common/ScrollRestoration.test.jsx
@@ -0,0 +1,68 @@
+import { render } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { MemoryRouter } from "react-router-dom";
+import { ScrollRestoration } from "./ScrollRestoration";
+
+// Helper to render with router at a specific path
+const renderWithRouter = (initialPath = "/") => {
+ return render(
+
+
+
+ );
+};
+
+describe("ScrollRestoration", () => {
+ let scrollToMock;
+
+ beforeEach(() => {
+ scrollToMock = vi.fn();
+ window.scrollTo = scrollToMock;
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("renders nothing (returns null)", () => {
+ const { container } = renderWithRouter("/");
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("scrolls to top on initial render", () => {
+ renderWithRouter("/contactos");
+ expect(scrollToMock).toHaveBeenCalledWith(0, 0);
+ });
+
+ it("scrolls to top when pathname changes", () => {
+ // Verify scroll is called for different paths
+ // Each render at a new path triggers scrollTo
+ renderWithRouter("/");
+ expect(scrollToMock).toHaveBeenCalledWith(0, 0);
+
+ // Reset and render at different path
+ scrollToMock.mockClear();
+ renderWithRouter("/contactos");
+ expect(scrollToMock).toHaveBeenCalledWith(0, 0);
+ });
+
+ it("does not scroll again if pathname remains the same", () => {
+ const { rerender } = render(
+
+
+
+ );
+
+ expect(scrollToMock).toHaveBeenCalledTimes(1);
+
+ // Re-render with same path
+ rerender(
+
+
+
+ );
+
+ // Should still be 1 because pathname didn't change
+ expect(scrollToMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/portfolio/SocialLinks.jsx b/src/components/common/SocialLinks.jsx
similarity index 100%
rename from src/components/portfolio/SocialLinks.jsx
rename to src/components/common/SocialLinks.jsx
diff --git a/src/components/portfolio/Lightbox.jsx b/src/components/ui/Lightbox.jsx
similarity index 100%
rename from src/components/portfolio/Lightbox.jsx
rename to src/components/ui/Lightbox.jsx
diff --git a/src/pages/ContactPage.jsx b/src/pages/ContactPage.jsx
index cc4b46e..d4faa9d 100644
--- a/src/pages/ContactPage.jsx
+++ b/src/pages/ContactPage.jsx
@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Textarea } from "@/components/ui/Textarea";
import ContactErrorDialog from "@/components/contact/ContactErrorDialog";
-import { SEO } from "@/components/SEO";
+import { SEO } from "@/components/common/SEO";
import {
Form,
FormControl,
diff --git a/src/pages/FAQPage.jsx b/src/pages/FAQPage.jsx
index f4aec2f..55128c0 100644
--- a/src/pages/FAQPage.jsx
+++ b/src/pages/FAQPage.jsx
@@ -1,11 +1,11 @@
-import { CTASection } from "@/components/ui/CTASection";
+import { CTASection } from "@/components/common/CTASection";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
-import { SEO } from "@/components/SEO";
+import { SEO } from "@/components/common/SEO";
import { faqCategories, mythsAndFacts } from "@/data/faqData";
const FAQPage = () => {
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 8b7e3a7..354bafe 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -1,8 +1,8 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
-import { CTASection } from "@/components/ui/CTASection";
-import { SEO } from "@/components/SEO";
+import { CTASection } from "@/components/common/CTASection";
+import { SEO } from "@/components/common/SEO";
import { Award, Users, ThermometerSun } from "lucide-react";
const heroImage = new URL("/hero-work.jpg", import.meta.url).href;
diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx
index 5223d5a..34eb50c 100644
--- a/src/pages/NotFoundPage.jsx
+++ b/src/pages/NotFoundPage.jsx
@@ -1,7 +1,7 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/Button";
import { BarsScaleFadeIcon } from "@/components/ui/icons/BarsScaleFadeIcon";
-import { SEO } from "@/components/SEO";
+import { SEO } from "@/components/common/SEO";
import { useEffect, useState } from "react";
const NotFoundPage = () => {
diff --git a/src/pages/PortfolioPage.jsx b/src/pages/PortfolioPage.jsx
index e014300..aa9a1f6 100644
--- a/src/pages/PortfolioPage.jsx
+++ b/src/pages/PortfolioPage.jsx
@@ -6,10 +6,10 @@ import {
CarouselPrevious,
CarouselNext,
} from "@/components/ui/Carousel";
-import { CTASection } from "@/components/ui/CTASection";
-import Lightbox from "@/components/portfolio/Lightbox";
-import SocialLinks from "@/components/portfolio/SocialLinks";
-import { SEO } from "@/components/SEO";
+import { CTASection } from "@/components/common/CTASection";
+import Lightbox from "@/components/ui/Lightbox";
+import SocialLinks from "@/components/common/SocialLinks";
+import { SEO } from "@/components/common/SEO";
import { portfolioImages } from "@/data/portfolioImages";
const PortfolioPage = () => {
diff --git a/src/pages/ServicesPage.jsx b/src/pages/ServicesPage.jsx
index 3180974..bda7003 100644
--- a/src/pages/ServicesPage.jsx
+++ b/src/pages/ServicesPage.jsx
@@ -1,5 +1,5 @@
-import { CTASection } from "@/components/ui/CTASection";
-import { SEO } from "@/components/SEO";
+import { CTASection } from "@/components/common/CTASection";
+import { SEO } from "@/components/common/SEO";
import { services, eticsBenefits, processSteps } from "@/data/servicesData";
const ServicesPage = () => {
From 296928687afab4c095c0d1e417c3d08c7937f931 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Fri, 2 Jan 2026 02:02:25 +0000
Subject: [PATCH 2/3] add test coverage and improve accessibility
---
src/components/ui/Lightbox.jsx | 96 +++++++------
src/components/ui/Lightbox.test.jsx | 210 ++++++++++++++++++++++++++++
2 files changed, 260 insertions(+), 46 deletions(-)
create mode 100644 src/components/ui/Lightbox.test.jsx
diff --git a/src/components/ui/Lightbox.jsx b/src/components/ui/Lightbox.jsx
index da3079a..f73faed 100644
--- a/src/components/ui/Lightbox.jsx
+++ b/src/components/ui/Lightbox.jsx
@@ -44,58 +44,62 @@ const Lightbox = ({ images, currentIndex, onClose, onNavigate }) => {
}, [navigate, onClose]);
return (
- <>
- {/* Backdrop - using button for accessibility */}
+
{
+ if (e.key === "Enter" || e.key === " ") onClose();
+ }}
+ role="button"
+ tabIndex={0}
+ aria-label="Fechar lightbox"
+ >
-
{
- e.stopPropagation();
- navigate("next");
- }}
- className="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
- aria-label="Próxima foto"
- >
-
-
+
{
+ e.stopPropagation();
+ navigate("prev");
+ }}
+ className="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
+ aria-label="Foto anterior"
+ >
+
+
-
-
-
+
{
+ e.stopPropagation();
+ navigate("next");
+ }}
+ className="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
+ aria-label="Próxima foto"
+ >
+
- >
+
+
e.stopPropagation()}
+ >
+
+
+
);
};
diff --git a/src/components/ui/Lightbox.test.jsx b/src/components/ui/Lightbox.test.jsx
new file mode 100644
index 0000000..53d362f
--- /dev/null
+++ b/src/components/ui/Lightbox.test.jsx
@@ -0,0 +1,210 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import Lightbox from "./Lightbox";
+
+const mockImages = [
+ { image: "/image1.jpg", alt: "Image 1" },
+ { image: "/image2.jpg", alt: "Image 2" },
+ { image: "/image3.jpg", alt: "Image 3" },
+];
+
+describe("Lightbox", () => {
+ let onCloseMock;
+ let onNavigateMock;
+
+ beforeEach(() => {
+ onCloseMock = vi.fn();
+ onNavigateMock = vi.fn();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ const renderLightbox = (currentIndex = 0) => {
+ return render(
+
+ );
+ };
+
+ it("renders the current image", () => {
+ renderLightbox(0);
+ const img = screen.getByRole("img");
+ expect(img).toHaveAttribute("src", "/image1.jpg");
+ expect(img).toHaveAttribute("alt", "Image 1");
+ });
+
+ it("renders fallback alt text when alt is not provided", () => {
+ const imagesWithoutAlt = [{ image: "/image1.jpg" }];
+ render(
+
+ );
+ const img = screen.getByRole("img");
+ expect(img).toHaveAttribute("alt", "Projeto 1 de 1");
+ });
+
+ it("renders navigation buttons", () => {
+ renderLightbox();
+ expect(screen.getByLabelText("Foto anterior")).toBeInTheDocument();
+ expect(screen.getByLabelText("Próxima foto")).toBeInTheDocument();
+ expect(screen.getByLabelText("Fechar")).toBeInTheDocument();
+ });
+
+ it("calls onClose when close button is clicked", () => {
+ renderLightbox();
+ fireEvent.click(screen.getByLabelText("Fechar"));
+ expect(onCloseMock).toHaveBeenCalled();
+ });
+
+ it("calls onClose when backdrop is clicked", () => {
+ renderLightbox();
+ fireEvent.click(screen.getByLabelText("Fechar lightbox"));
+ expect(onCloseMock).toHaveBeenCalled();
+ });
+
+ it("navigates to previous image when prev button is clicked", () => {
+ renderLightbox(1);
+ fireEvent.click(screen.getByLabelText("Foto anterior"));
+ expect(onNavigateMock).toHaveBeenCalledWith(0);
+ });
+
+ it("navigates to next image when next button is clicked", () => {
+ renderLightbox(1);
+ fireEvent.click(screen.getByLabelText("Próxima foto"));
+ expect(onNavigateMock).toHaveBeenCalledWith(2);
+ });
+
+ it("wraps to last image when navigating prev from first image", () => {
+ renderLightbox(0);
+ fireEvent.click(screen.getByLabelText("Foto anterior"));
+ expect(onNavigateMock).toHaveBeenCalledWith(2); // Last image index
+ });
+
+ it("wraps to first image when navigating next from last image", () => {
+ renderLightbox(2);
+ fireEvent.click(screen.getByLabelText("Próxima foto"));
+ expect(onNavigateMock).toHaveBeenCalledWith(0); // First image index
+ });
+
+ describe("keyboard navigation", () => {
+ it("closes lightbox on Escape key", () => {
+ renderLightbox();
+ fireEvent.keyDown(document, { key: "Escape" });
+ expect(onCloseMock).toHaveBeenCalled();
+ });
+
+ it("navigates to previous image on ArrowLeft key", () => {
+ renderLightbox(1);
+ fireEvent.keyDown(document, { key: "ArrowLeft" });
+ expect(onNavigateMock).toHaveBeenCalledWith(0);
+ });
+
+ it("navigates to next image on ArrowRight key", () => {
+ renderLightbox(1);
+ fireEvent.keyDown(document, { key: "ArrowRight" });
+ expect(onNavigateMock).toHaveBeenCalledWith(2);
+ });
+
+ it("does not respond to other keys", () => {
+ renderLightbox();
+ fireEvent.keyDown(document, { key: "Enter" });
+ expect(onCloseMock).not.toHaveBeenCalled();
+ expect(onNavigateMock).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("touch/swipe navigation", () => {
+ it("navigates to next image on swipe left", () => {
+ renderLightbox(1);
+ const backdrop = screen.getByLabelText("Fechar lightbox");
+
+ fireEvent.touchStart(backdrop, {
+ touches: [{ clientX: 300 }],
+ });
+ fireEvent.touchEnd(backdrop, {
+ changedTouches: [{ clientX: 100 }],
+ });
+
+ expect(onNavigateMock).toHaveBeenCalledWith(2);
+ });
+
+ it("navigates to previous image on swipe right", () => {
+ renderLightbox(1);
+ const backdrop = screen.getByLabelText("Fechar lightbox");
+
+ fireEvent.touchStart(backdrop, {
+ touches: [{ clientX: 100 }],
+ });
+ fireEvent.touchEnd(backdrop, {
+ changedTouches: [{ clientX: 300 }],
+ });
+
+ expect(onNavigateMock).toHaveBeenCalledWith(0);
+ });
+
+ it("does not navigate on small swipe", () => {
+ renderLightbox(1);
+ const backdrop = screen.getByLabelText("Fechar lightbox");
+
+ fireEvent.touchStart(backdrop, {
+ touches: [{ clientX: 200 }],
+ });
+ fireEvent.touchEnd(backdrop, {
+ changedTouches: [{ clientX: 180 }],
+ });
+
+ expect(onNavigateMock).not.toHaveBeenCalled();
+ });
+
+ it("does not navigate when touchStart was not recorded", () => {
+ renderLightbox(1);
+ const backdrop = screen.getByLabelText("Fechar lightbox");
+
+ // Only fire touchEnd without touchStart
+ fireEvent.touchEnd(backdrop, {
+ changedTouches: [{ clientX: 100 }],
+ });
+
+ expect(onNavigateMock).not.toHaveBeenCalled();
+ });
+ });
+
+ it("stops propagation when clicking navigation buttons", () => {
+ renderLightbox();
+
+ // Click prev button - should not trigger backdrop close
+ fireEvent.click(screen.getByLabelText("Foto anterior"));
+ expect(onNavigateMock).toHaveBeenCalled();
+ // onClose should not be called from the backdrop click
+ expect(onCloseMock).toHaveBeenCalledTimes(0);
+
+ onNavigateMock.mockClear();
+
+ // Click next button - should not trigger backdrop close
+ fireEvent.click(screen.getByLabelText("Próxima foto"));
+ expect(onNavigateMock).toHaveBeenCalled();
+ expect(onCloseMock).toHaveBeenCalledTimes(0);
+ });
+
+ it("cleans up keyboard event listener on unmount", () => {
+ const removeEventListenerSpy = vi.spyOn(document, "removeEventListener");
+ const { unmount } = renderLightbox();
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "keydown",
+ expect.any(Function)
+ );
+ });
+});
From 94eb88482099bfbac5df8fd5e9f6f1f6d8070834 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Fri, 2 Jan 2026 02:13:28 +0000
Subject: [PATCH 3/3] address sonarqube issues
---
.github/workflows/github-pages-deploy.yml | 2 +-
.github/workflows/lint.yml | 2 +-
.github/workflows/netlify-deploy.yml | 2 +-
.github/workflows/security-audit.yml | 2 +-
.github/workflows/sonarqube.yml | 2 +-
.github/workflows/test.yml | 2 +-
src/components/ui/Lightbox.jsx | 47 +++++++++++------------
7 files changed, 28 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/github-pages-deploy.yml b/.github/workflows/github-pages-deploy.yml
index 057d523..401cc57 100644
--- a/.github/workflows/github-pages-deploy.yml
+++ b/.github/workflows/github-pages-deploy.yml
@@ -30,7 +30,7 @@ jobs:
cache: "npm"
- name: Install dependencies
- run: npm ci
+ run: npm ci --ignore-scripts
- name: Build
run: npm run build
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 0521f7c..83f5eb6 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,7 +23,7 @@ jobs:
cache: "npm"
- name: Install dependencies
- run: npm ci
+ run: npm ci --ignore-scripts
- name: Run linting
run: npm run lint
diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml
index 07d7ed3..580a4e2 100644
--- a/.github/workflows/netlify-deploy.yml
+++ b/.github/workflows/netlify-deploy.yml
@@ -33,7 +33,7 @@ jobs:
- name: Install and Build
run: |
- npm ci
+ npm ci --ignore-scripts
npm run build
- name: Deploy to Netlify
diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml
index ce26975..6fb8b9a 100644
--- a/.github/workflows/security-audit.yml
+++ b/.github/workflows/security-audit.yml
@@ -24,7 +24,7 @@ jobs:
cache: "npm"
- name: Install dependencies
- run: npm ci
+ run: npm ci --ignore-scripts
- name: Run security audit
run: npm audit --audit-level=moderate
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 5dd00b9..682e768 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -24,7 +24,7 @@ jobs:
cache: "npm"
- name: Install dependencies
- run: npm ci
+ run: npm ci --ignore-scripts
- name: Run tests with coverage
run: npm run test:coverage
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 72841e3..5dd9d9e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,7 +21,7 @@ jobs:
cache: "npm"
- name: Install dependencies
- run: npm ci
+ run: npm ci --ignore-scripts
- name: Run tests
run: npm test
diff --git a/src/components/ui/Lightbox.jsx b/src/components/ui/Lightbox.jsx
index f73faed..952df5a 100644
--- a/src/components/ui/Lightbox.jsx
+++ b/src/components/ui/Lightbox.jsx
@@ -44,19 +44,20 @@ const Lightbox = ({ images, currentIndex, onClose, onNavigate }) => {
}, [navigate, onClose]);
return (
-
{
- if (e.key === "Enter" || e.key === " ") onClose();
- }}
- role="button"
- tabIndex={0}
- aria-label="Fechar lightbox"
- >
+
+ {/* Backdrop - semantic button element */}
+
+ {/* Close button */}
+
{
+ {/* Previous button */}
{
- e.stopPropagation();
- navigate("prev");
- }}
+ type="button"
+ onClick={() => navigate("prev")}
className="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
aria-label="Foto anterior"
>
+ {/* Next button */}
{
- e.stopPropagation();
- navigate("next");
- }}
+ type="button"
+ onClick={() => navigate("next")}
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
aria-label="Próxima foto"
>
-
e.stopPropagation()}
- >
+ {/* Image container */}
+