From cdde81eb6e872d751f93aff9a1043dc2e248c218 Mon Sep 17 00:00:00 2001
From: Evandro Fonseca Junior
Date: Sat, 8 Aug 2026 19:42:40 -0300
Subject: [PATCH] fix: stabilize reveals and update contact email
---
app/components/About.tsx | 3 +-
app/components/Contact.tsx | 6 +-
app/components/Experience.tsx | 5 +-
app/components/Footer.test.tsx | 11 ++-
app/components/Footer.tsx | 4 +-
app/components/LighthouseMetrics.tsx | 3 +-
app/components/Logo.tsx | 3 +-
app/components/Projects.tsx | 7 +-
app/components/Reveal.tsx | 3 +-
app/components/RouteTransition.tsx | 5 +-
app/components/Section.tsx | 3 +-
app/components/Skills.tsx | 5 +-
app/components/WorkModalities.tsx | 6 +-
app/components/animations/CounterValue.tsx | 3 +-
app/components/animations/DrawLine.tsx | 3 +-
app/components/animations/LazyRevealImage.tsx | 5 +-
.../TextDiffuseReveal.reducedMotion.test.tsx | 81 +++++++++++++++++++
.../animations/TextDiffuseReveal.tsx | 9 +--
app/components/animations/TopProgressBar.tsx | 3 +-
app/components/cinematic/HeroBackground.tsx | 2 +-
app/components/cinematic/PageIntro.tsx | 3 +-
app/hooks/useReducedMotion.ts | 11 ++-
22 files changed, 144 insertions(+), 40 deletions(-)
create mode 100644 app/components/animations/TextDiffuseReveal.reducedMotion.test.tsx
diff --git a/app/components/About.tsx b/app/components/About.tsx
index 77417e7..5b59183 100644
--- a/app/components/About.tsx
+++ b/app/components/About.tsx
@@ -1,6 +1,7 @@
"use client";
-import { motion, useReducedMotion, type Variants } from "framer-motion";
+import { motion, type Variants } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import profilePhoto from "@/public/edev.jpg";
import { DrawLine } from "./animations/DrawLine";
import { LazyRevealImage } from "./animations/LazyRevealImage";
diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx
index ac67d68..f804ff3 100644
--- a/app/components/Contact.tsx
+++ b/app/components/Contact.tsx
@@ -1,13 +1,13 @@
"use client";
+import { motion, type Variants } from "framer-motion";
import type React from "react";
import { useEffect, useState } from "react";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp";
-import { motion, type Variants, useReducedMotion } from "framer-motion";
-import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
-
+import { Section } from "./Section";
export default function Contact() {
const shouldReduceMotion = useReducedMotion();
diff --git a/app/components/Experience.tsx b/app/components/Experience.tsx
index 5a3c766..950bcfc 100644
--- a/app/components/Experience.tsx
+++ b/app/components/Experience.tsx
@@ -1,9 +1,10 @@
"use client";
+import { motion, type Variants } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { PortfolioExperience } from "@/lib/portfolio-data";
-import { motion, type Variants, useReducedMotion } from "framer-motion";
-import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
+import { Section } from "./Section";
export default function Experience({
experiences,
diff --git a/app/components/Footer.test.tsx b/app/components/Footer.test.tsx
index a85d1f5..392c10d 100644
--- a/app/components/Footer.test.tsx
+++ b/app/components/Footer.test.tsx
@@ -13,7 +13,7 @@ describe("Footer Component - Contact Information", () => {
it("should render email address", () => {
render();
- const emailLink = screen.getByText("evandro@dev.br");
+ const emailLink = screen.getByText("evandrofonsecajunior@gmail.com");
expect(emailLink).toBeInTheDocument();
expect(emailLink.tagName).toBe("A");
});
@@ -21,14 +21,17 @@ describe("Footer Component - Contact Information", () => {
it("should have mailto link for email", () => {
render();
- const emailLink = screen.getByText("evandro@dev.br");
- expect(emailLink).toHaveAttribute("href", "mailto:evandro@dev.br");
+ const emailLink = screen.getByText("evandrofonsecajunior@gmail.com");
+ expect(emailLink).toHaveAttribute(
+ "href",
+ "mailto:evandrofonsecajunior@gmail.com",
+ );
});
it("should apply correct styling classes to email link", () => {
render();
- const emailLink = screen.getByText("evandro@dev.br");
+ const emailLink = screen.getByText("evandrofonsecajunior@gmail.com");
const classes = emailLink.className;
expect(classes).toContain("text-[12px]");
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx
index 40f2a80..8b6f331 100644
--- a/app/components/Footer.tsx
+++ b/app/components/Footer.tsx
@@ -64,11 +64,11 @@ export default function Footer() {
- evandro@dev.br
+ evandrofonsecajunior@gmail.com
diff --git a/app/components/LighthouseMetrics.tsx b/app/components/LighthouseMetrics.tsx
index 1ad6837..11e74fc 100644
--- a/app/components/LighthouseMetrics.tsx
+++ b/app/components/LighthouseMetrics.tsx
@@ -1,6 +1,7 @@
"use client";
-import { motion, useReducedMotion } from "framer-motion";
+import { motion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { Section } from "./Section";
const METRICS = [
diff --git a/app/components/Logo.tsx b/app/components/Logo.tsx
index c3d760c..e431f22 100644
--- a/app/components/Logo.tsx
+++ b/app/components/Logo.tsx
@@ -1,6 +1,7 @@
"use client";
-import { motion, useReducedMotion } from "framer-motion";
+import { motion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface LogoProps {
variant?: "mark" | "wordmark" | "icon";
diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx
index 1180949..1c05c2e 100644
--- a/app/components/Projects.tsx
+++ b/app/components/Projects.tsx
@@ -1,10 +1,11 @@
"use client";
+import { motion, type Variants } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { PortfolioProject } from "@/lib/portfolio-data";
-import { motion, type Variants, useReducedMotion } from "framer-motion";
-import { Section } from "./Section";
-import MagneticButton from "./cinematic/MagneticButton";
import { DrawLine } from "./animations/DrawLine";
+import MagneticButton from "./cinematic/MagneticButton";
+import { Section } from "./Section";
export default function Projects({
projects,
diff --git a/app/components/Reveal.tsx b/app/components/Reveal.tsx
index 9bac1cb..514af96 100644
--- a/app/components/Reveal.tsx
+++ b/app/components/Reveal.tsx
@@ -1,8 +1,9 @@
"use client";
+import { motion } from "framer-motion";
import type { ReactNode } from "react";
import { useState } from "react";
-import { motion, useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface RevealProps {
children: ReactNode;
diff --git a/app/components/RouteTransition.tsx b/app/components/RouteTransition.tsx
index c9079aa..94e2236 100644
--- a/app/components/RouteTransition.tsx
+++ b/app/components/RouteTransition.tsx
@@ -1,8 +1,9 @@
"use client";
-import type { ReactNode } from "react";
-import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
+import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
+import type { ReactNode } from "react";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface RouteTransitionProps {
children: ReactNode;
diff --git a/app/components/Section.tsx b/app/components/Section.tsx
index 2cd195e..1bb6bf5 100644
--- a/app/components/Section.tsx
+++ b/app/components/Section.tsx
@@ -1,7 +1,8 @@
"use client";
+import { motion, type Variants } from "framer-motion";
import type React from "react";
-import { motion, type Variants, useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { DrawLine } from "./animations/DrawLine";
import { TextDiffuseReveal } from "./animations/TextDiffuseReveal";
import { Reveal } from "./Reveal";
diff --git a/app/components/Skills.tsx b/app/components/Skills.tsx
index a26201b..b017ad0 100644
--- a/app/components/Skills.tsx
+++ b/app/components/Skills.tsx
@@ -1,9 +1,10 @@
"use client";
+import { motion, type Variants } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { PortfolioSkillCategory } from "@/lib/portfolio-data";
-import { motion, type Variants, useReducedMotion } from "framer-motion";
-import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
+import { Section } from "./Section";
export default function Skills({
categories,
diff --git a/app/components/WorkModalities.tsx b/app/components/WorkModalities.tsx
index 88e4fba..aa3d111 100644
--- a/app/components/WorkModalities.tsx
+++ b/app/components/WorkModalities.tsx
@@ -1,11 +1,12 @@
"use client";
+import { motion, type Variants } from "framer-motion";
import Link from "next/link";
-import { motion, type Variants, useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { serviceHubLinks } from "@/lib/site-content";
-import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
+import { Section } from "./Section";
interface Tier {
id: string;
@@ -246,4 +247,3 @@ export function WorkModalities() {
);
}
-
diff --git a/app/components/animations/CounterValue.tsx b/app/components/animations/CounterValue.tsx
index 983ee5d..343dabc 100644
--- a/app/components/animations/CounterValue.tsx
+++ b/app/components/animations/CounterValue.tsx
@@ -1,7 +1,8 @@
"use client";
+import { useInView } from "framer-motion";
import { useEffect, useRef, useState } from "react";
-import { useInView, useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface CounterValueProps {
value: number;
diff --git a/app/components/animations/DrawLine.tsx b/app/components/animations/DrawLine.tsx
index 877e518..0d130cc 100644
--- a/app/components/animations/DrawLine.tsx
+++ b/app/components/animations/DrawLine.tsx
@@ -1,7 +1,8 @@
"use client";
+import { motion } from "framer-motion";
import { useState } from "react";
-import { motion, useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface DrawLineProps {
className?: string;
diff --git a/app/components/animations/LazyRevealImage.tsx b/app/components/animations/LazyRevealImage.tsx
index 9b1edaa..d9cbc6a 100644
--- a/app/components/animations/LazyRevealImage.tsx
+++ b/app/components/animations/LazyRevealImage.tsx
@@ -1,8 +1,9 @@
"use client";
-import { useState } from "react";
+import { motion } from "framer-motion";
import Image, { type StaticImageData } from "next/image";
-import { motion, useReducedMotion } from "framer-motion";
+import { useState } from "react";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface LazyRevealImageProps {
src: string | StaticImageData;
diff --git a/app/components/animations/TextDiffuseReveal.reducedMotion.test.tsx b/app/components/animations/TextDiffuseReveal.reducedMotion.test.tsx
new file mode 100644
index 0000000..705f93e
--- /dev/null
+++ b/app/components/animations/TextDiffuseReveal.reducedMotion.test.tsx
@@ -0,0 +1,81 @@
+import { render } from "@testing-library/react";
+import type { ReactNode } from "react";
+
+import { TextDiffuseReveal } from "./TextDiffuseReveal";
+
+let mockReducedMotion = false;
+let mockInView = true;
+let mockAnimations: Array<{ animate?: Record }> = [];
+jest.mock("framer-motion", () => {
+ const React = require("react");
+
+ return {
+ motion: {
+ span: ({
+ children,
+ initial: _initial,
+ animate,
+ transition: _transition,
+ onAnimationComplete: _onAnimationComplete,
+ ...props
+ }: {
+ children?: ReactNode;
+ initial?: unknown;
+ animate?: Record;
+ transition?: unknown;
+ onAnimationComplete?: () => void;
+ } & Record) => {
+ mockAnimations.push({ animate });
+ return React.createElement("span", props, children);
+ },
+ },
+ useInView: () => mockInView,
+ useReducedMotion: () => mockReducedMotion,
+ };
+});
+
+describe("TextDiffuseReveal reduced motion", () => {
+ beforeEach(() => {
+ mockReducedMotion = false;
+ mockInView = true;
+ mockAnimations = [];
+ });
+
+ it("clears blur and scale when reduced motion resolves after mount", () => {
+ const { rerender } = render();
+
+ mockAnimations = [];
+ mockReducedMotion = true;
+ rerender();
+
+ expect(mockAnimations).toHaveLength(6);
+ expect(mockAnimations).toEqual(
+ expect.arrayContaining(
+ Array.from({ length: 6 }, () =>
+ expect.objectContaining({
+ animate: {
+ opacity: 1,
+ filter: "blur(0px)",
+ scale: 1,
+ },
+ }),
+ ),
+ ),
+ );
+ });
+
+ it("keeps the first render independent of the reduced-motion preference", () => {
+ mockReducedMotion = true;
+ mockInView = false;
+
+ render();
+
+ expect(mockAnimations[0]).toEqual({
+ animate: {
+ filter: "blur(12px)",
+ opacity: 0,
+ scale: 0.9,
+ },
+ });
+ });
+});
diff --git a/app/components/animations/TextDiffuseReveal.tsx b/app/components/animations/TextDiffuseReveal.tsx
index 59cacc6..1f44dc7 100644
--- a/app/components/animations/TextDiffuseReveal.tsx
+++ b/app/components/animations/TextDiffuseReveal.tsx
@@ -1,7 +1,8 @@
"use client";
-import { motion, useInView, useReducedMotion } from "framer-motion";
+import { motion, useInView } from "framer-motion";
import { useEffect, useMemo, useRef, useState } from "react";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface TextDiffuseRevealProps {
text: string;
@@ -101,11 +102,9 @@ export function TextDiffuseReveal({
}
animate={
isInView
- ? shouldReduceMotion
- ? { opacity: 1 }
- : { filter: "blur(0px)", opacity: 1, scale: 1 }
+ ? { filter: "blur(0px)", opacity: 1, scale: 1 }
: shouldReduceMotion
- ? { opacity: 0 }
+ ? { filter: "blur(0px)", opacity: 0, scale: 1 }
: { filter: "blur(12px)", opacity: 0, scale: 0.9 }
}
transition={{
diff --git a/app/components/animations/TopProgressBar.tsx b/app/components/animations/TopProgressBar.tsx
index 8997f42..69341c7 100644
--- a/app/components/animations/TopProgressBar.tsx
+++ b/app/components/animations/TopProgressBar.tsx
@@ -1,6 +1,7 @@
"use client";
-import { motion, useReducedMotion } from "framer-motion";
+import { motion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface TopProgressBarProps {
className?: string;
diff --git a/app/components/cinematic/HeroBackground.tsx b/app/components/cinematic/HeroBackground.tsx
index f79af81..2975de0 100644
--- a/app/components/cinematic/HeroBackground.tsx
+++ b/app/components/cinematic/HeroBackground.tsx
@@ -1,7 +1,7 @@
"use client";
import dynamic from "next/dynamic";
-import { useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
const HeroScene = dynamic(() => import("./HeroScene"), { ssr: false });
diff --git a/app/components/cinematic/PageIntro.tsx b/app/components/cinematic/PageIntro.tsx
index 38b6c92..f86a6e2 100644
--- a/app/components/cinematic/PageIntro.tsx
+++ b/app/components/cinematic/PageIntro.tsx
@@ -1,8 +1,9 @@
"use client";
+import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
-import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
+import { useReducedMotion } from "@/app/hooks/useReducedMotion";
const EXIT_EASE: [number, number, number, number] = [0.76, 0, 0.24, 1];
diff --git a/app/hooks/useReducedMotion.ts b/app/hooks/useReducedMotion.ts
index 5587667..9f4ac70 100644
--- a/app/hooks/useReducedMotion.ts
+++ b/app/hooks/useReducedMotion.ts
@@ -1,7 +1,7 @@
"use client";
import { useReducedMotion as useFramerReducedMotion } from "framer-motion";
-
+import { useEffect, useState } from "react";
/**
* Hook que detecta a preferĂȘncia prefers-reduced-motion do sistema operacional.
* Wrapper sobre o hook useReducedMotion do Framer Motion.
@@ -20,5 +20,12 @@ import { useReducedMotion as useFramerReducedMotion } from "framer-motion";
* ```
*/
export function useReducedMotion(): boolean {
- return useFramerReducedMotion() ?? false;
+ const systemPreference = useFramerReducedMotion();
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ return isMounted && systemPreference === true;
}