diff --git a/.github/workflows/github-pages-deploy.yml b/.github/workflows/github-pages-deploy.yml
index 40f94c2..401cc57 100644
--- a/.github/workflows/github-pages-deploy.yml
+++ b/.github/workflows/github-pages-deploy.yml
@@ -8,7 +8,7 @@ on:
concurrency:
group: pages
- cancel-in-progress: false
+ cancel-in-progress: true
jobs:
test:
@@ -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 21e1f73..580a4e2 100644
--- a/.github/workflows/netlify-deploy.yml
+++ b/.github/workflows/netlify-deploy.yml
@@ -7,7 +7,7 @@ on:
concurrency:
group: netlify-production
- cancel-in-progress: false
+ cancel-in-progress: true
jobs:
call-test:
@@ -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/eslint.config.js b/eslint.config.js
index b8b8b7d..d89d552 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -5,7 +5,7 @@ import reactRefresh from "eslint-plugin-react-refresh";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
- globalIgnores(["dist"]),
+ globalIgnores(["dist", "coverage"]),
{
files: ["**/*.{js,jsx}"],
extends: [
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 99%
rename from src/components/SEO.jsx
rename to src/components/common/SEO.jsx
index dc7a651..af73b86 100644
--- a/src/components/SEO.jsx
+++ b/src/components/common/SEO.jsx
@@ -60,4 +60,5 @@ SEO.propTypes = {
ogType: PropTypes.string,
};
+export { SEO };
export default SEO;
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/layout/Navbar.jsx b/src/components/layout/Navbar.jsx
index cf9ba67..ac65c8f 100644
--- a/src/components/layout/Navbar.jsx
+++ b/src/components/layout/Navbar.jsx
@@ -112,7 +112,7 @@ export const Navbar = React.forwardRef(
ref={combinedRef}
style={{ colorScheme: "only light" }}
className={cn(
- "sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur @supports-[backdrop-filter]:bg-background/60 px-4 md:px-6 [&_*]:no-underline",
+ "sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur @supports-[backdrop-filter]:bg-background/60 px-4 md:px-6 **:no-underline",
className
)}
{...props}
@@ -138,8 +138,11 @@ export const Navbar = React.forwardRef(
- {navigationLinks.map((link, index) => (
-
+ {navigationLinks.map((link) => (
+
- {navigationLinks.map((link, index) => (
-
+ {navigationLinks.map((link) => (
+
({
+ carouselRef,
+ api: api,
+ opts,
+ orientation:
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
+ scrollPrev,
+ scrollNext,
+ canScrollPrev,
+ canScrollNext,
+ }),
+ [
+ carouselRef,
+ api,
+ opts,
+ orientation,
+ scrollPrev,
+ scrollNext,
+ canScrollPrev,
+ canScrollNext,
+ ]
+ );
+
return (
-
-
+
+
);
}
@@ -134,7 +146,6 @@ function CarouselItem({ className, ...props }) {
return (
{
+ const contextValue = React.useMemo(
+ () => ({ name: props.name }),
+ [props.name]
+ );
+
return (
-
+
);
@@ -51,9 +56,10 @@ const FormItemContext = React.createContext({});
function FormItem({ className, ...props }) {
const id = React.useId();
+ const contextValue = React.useMemo(() => ({ id }), [id]);
return (
-
+
{
}, [navigate, onClose]);
return (
- <>
- {/* Backdrop - using button for accessibility */}
+
+ {/* Backdrop - semantic button element */}
);
};
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)
+ );
+ });
+});
diff --git a/src/main.jsx b/src/main.jsx
index 6cb119c..9cfa7ae 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -4,7 +4,7 @@ import { BrowserRouter } from "react-router-dom";
import "./App.css";
import App from "./App.jsx";
-// Use "/chrisert" for GitHub Pages, "/" for Netlify
+// BASE_URL comes from vite.config base: use "/chrisert" for GitHub Pages, "/" for Netlify
const basename = import.meta.env.BASE_URL.replace(/\/$/, "") || "/";
// Handle GitHub Pages SPA redirect
@@ -12,7 +12,7 @@ const redirect = sessionStorage.getItem("redirect");
if (redirect) {
sessionStorage.removeItem("redirect");
// Redirect to the original path the user requested
- window.history.replaceState(null, "", redirect);
+ globalThis.history.replaceState(null, "", redirect);
}
createRoot(document.getElementById("root")).render(
diff --git a/src/pages/ContactPage.jsx b/src/pages/ContactPage.jsx
index a89d5c0..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,
@@ -21,9 +21,7 @@ const contactFormSchema = z.object({
nome: z.string().min(2, {
message: "O nome deve ter pelo menos 2 caracteres.",
}),
- email: z.string().email({
- message: "Por favor, insira um email válido.",
- }),
+ email: z.email("Por favor, insira um email válido."),
telefone: z
.string()
.min(9, {
diff --git a/src/pages/FAQPage.jsx b/src/pages/FAQPage.jsx
index bec2d6a..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 = () => {
@@ -44,10 +44,10 @@ const FAQPage = () => {
collapsible
className="border rounded-lg px-4"
>
- {category.questions.map((item, index) => (
+ {category.questions.map((item) => (
{item.question}
@@ -78,9 +78,9 @@ const FAQPage = () => {
- {mythsAndFacts.map((item, index) => (
+ {mythsAndFacts.map((item) => (
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index c541062..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 c30d8da..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 = () => {
@@ -10,7 +10,7 @@ const NotFoundPage = () => {
useEffect(() => {
if (count <= 0) {
- window.location.replace(basename);
+ globalThis.location.replace(basename);
return;
}
diff --git a/src/pages/PortfolioPage.jsx b/src/pages/PortfolioPage.jsx
index 915fe92..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 = () => {
@@ -86,9 +86,9 @@ const PortfolioPage = () => {
{/* Dot indicators */}
- {portfolioImages.map((_, index) => (
+ {portfolioImages.map((project, index) => (
api?.scrollTo(index)}
className={`h-2 rounded-full transition-all ${
index === current
diff --git a/src/pages/ServicesPage.jsx b/src/pages/ServicesPage.jsx
index 56f5555..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 = () => {
@@ -28,11 +28,11 @@ const ServicesPage = () => {
- {services.map((service, index) => {
+ {services.map((service) => {
const IconComponent = service.icon;
return (
@@ -48,9 +48,9 @@ const ServicesPage = () => {
{service.description}
- {service.features.map((feature, idx) => (
+ {service.features.map((feature) => (
-
✓
@@ -79,11 +79,11 @@ const ServicesPage = () => {
- {eticsBenefits.map((benefit, index) => {
+ {eticsBenefits.map((benefit) => {
const IconComponent = benefit.icon;
return (
@@ -118,8 +118,8 @@ const ServicesPage = () => {
- {processSteps.map((item, index) => (
-
+ {processSteps.map((item) => (
+