fix: stabilize reveals and update contact email

This commit is contained in:
Evandro Fonseca Junior committed 2026-08-08 19:42:40 -03:00
1 parent aad56ad885
commit cdde81eb6e
22 files changed
+144 -40

No files matched your search

+2 -1
View File
@@ -1,6 +1,7 @@
"use client"; "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 profilePhoto from "@/public/edev.jpg";
import { DrawLine } from "./animations/DrawLine"; import { DrawLine } from "./animations/DrawLine";
import { LazyRevealImage } from "./animations/LazyRevealImage"; import { LazyRevealImage } from "./animations/LazyRevealImage";
+3 -3
View File
@@ -1,13 +1,13 @@
"use client"; "use client";
import { motion, type Variants } from "framer-motion";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp"; import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine"; import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton"; import MagneticButton from "./cinematic/MagneticButton";
import { Section } from "./Section";
export default function Contact() { export default function Contact() {
const shouldReduceMotion = useReducedMotion(); const shouldReduceMotion = useReducedMotion();
+3 -2
View File
@@ -1,9 +1,10 @@
"use client"; "use client";
import { motion, type Variants } from "framer-motion";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { PortfolioExperience } from "@/lib/portfolio-data"; 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 { DrawLine } from "./animations/DrawLine";
import { Section } from "./Section";
export default function Experience({ export default function Experience({
experiences, experiences,
+7 -4
View File
@@ -13,7 +13,7 @@ describe("Footer Component - Contact Information", () => {
it("should render email address", () => { it("should render email address", () => {
render(<Footer />); render(<Footer />);
const emailLink = screen.getByText("evandro@dev.br"); const emailLink = screen.getByText("evandro[email protected]");
expect(emailLink).toBeInTheDocument(); expect(emailLink).toBeInTheDocument();
expect(emailLink.tagName).toBe("A"); expect(emailLink.tagName).toBe("A");
}); });
@@ -21,14 +21,17 @@ describe("Footer Component - Contact Information", () => {
it("should have mailto link for email", () => { it("should have mailto link for email", () => {
render(<Footer />); render(<Footer />);
const emailLink = screen.getByText("evandro@dev.br"); const emailLink = screen.getByText("evandro[email protected]");
expect(emailLink).toHaveAttribute("href", "mailto:[email protected]"); expect(emailLink).toHaveAttribute(
"href",
"mailto:[email protected]",
);
}); });
it("should apply correct styling classes to email link", () => { it("should apply correct styling classes to email link", () => {
render(<Footer />); render(<Footer />);
const emailLink = screen.getByText("evandro@dev.br"); const emailLink = screen.getByText("evandro[email protected]");
const classes = emailLink.className; const classes = emailLink.className;
expect(classes).toContain("text-[12px]"); expect(classes).toContain("text-[12px]");
+2 -2
View File
@@ -64,11 +64,11 @@ export default function Footer() {
</p> </p>
<MagneticButton <MagneticButton
as="a" as="a"
href="mailto:evandro@dev.br" href="mailto:evandro[email protected]"
className="inline-block text-[18px] font-bold text-[var(--ink)] transition-colors duration-200 hover:text-[var(--slash)]" className="inline-block text-[18px] font-bold text-[var(--ink)] transition-colors duration-200 hover:text-[var(--slash)]"
strength={0.2} strength={0.2}
> >
evandro@dev.br evandrofonsecajunior@gmail.com
</MagneticButton> </MagneticButton>
</div> </div>
</div> </div>
+2 -1
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { motion, useReducedMotion } from "framer-motion"; import { motion } from "framer-motion";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { Section } from "./Section"; import { Section } from "./Section";
const METRICS = [ const METRICS = [
+2 -1
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { motion, useReducedMotion } from "framer-motion"; import { motion } from "framer-motion";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface LogoProps { interface LogoProps {
variant?: "mark" | "wordmark" | "icon"; variant?: "mark" | "wordmark" | "icon";
+4 -3
View File
@@ -1,10 +1,11 @@
"use client"; "use client";
import { motion, type Variants } from "framer-motion";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { PortfolioProject } from "@/lib/portfolio-data"; 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 { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
import { Section } from "./Section";
export default function Projects({ export default function Projects({
projects, projects,
+2 -1
View File
@@ -1,8 +1,9 @@
"use client"; "use client";
import { motion } from "framer-motion";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useState } from "react"; import { useState } from "react";
import { motion, useReducedMotion } from "framer-motion"; import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface RevealProps { interface RevealProps {
children: ReactNode; children: ReactNode;
+3 -2
View File
@@ -1,8 +1,9 @@
"use client"; "use client";
import type { ReactNode } from "react"; import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import type { ReactNode } from "react";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface RouteTransitionProps { interface RouteTransitionProps {
children: ReactNode; children: ReactNode;
+2 -1
View File
@@ -1,7 +1,8 @@
"use client"; "use client";
import { motion, type Variants } from "framer-motion";
import type React from "react"; 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 { DrawLine } from "./animations/DrawLine";
import { TextDiffuseReveal } from "./animations/TextDiffuseReveal"; import { TextDiffuseReveal } from "./animations/TextDiffuseReveal";
import { Reveal } from "./Reveal"; import { Reveal } from "./Reveal";
+3 -2
View File
@@ -1,9 +1,10 @@
"use client"; "use client";
import { motion, type Variants } from "framer-motion";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { PortfolioSkillCategory } from "@/lib/portfolio-data"; 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 { DrawLine } from "./animations/DrawLine";
import { Section } from "./Section";
export default function Skills({ export default function Skills({
categories, categories,
+3 -3
View File
@@ -1,11 +1,12 @@
"use client"; "use client";
import { motion, type Variants } from "framer-motion";
import Link from "next/link"; 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 { serviceHubLinks } from "@/lib/site-content";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine"; import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton"; import MagneticButton from "./cinematic/MagneticButton";
import { Section } from "./Section";
interface Tier { interface Tier {
id: string; id: string;
@@ -246,4 +247,3 @@ export function WorkModalities() {
</Section> </Section>
); );
} }
+2 -1
View File
@@ -1,7 +1,8 @@
"use client"; "use client";
import { useInView } from "framer-motion";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useInView, useReducedMotion } from "framer-motion"; import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface CounterValueProps { interface CounterValueProps {
value: number; value: number;
+2 -1
View File
@@ -1,7 +1,8 @@
"use client"; "use client";
import { motion } from "framer-motion";
import { useState } from "react"; import { useState } from "react";
import { motion, useReducedMotion } from "framer-motion"; import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface DrawLineProps { interface DrawLineProps {
className?: string; className?: string;
@@ -1,8 +1,9 @@
"use client"; "use client";
import { useState } from "react"; import { motion } from "framer-motion";
import Image, { type StaticImageData } from "next/image"; 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 { interface LazyRevealImageProps {
src: string | StaticImageData; src: string | StaticImageData;
@@ -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<string, unknown> }> = [];
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<string, unknown>;
transition?: unknown;
onAnimationComplete?: () => void;
} & Record<string, unknown>) => {
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(<TextDiffuseReveal text="Reveal" />);
mockAnimations = [];
mockReducedMotion = true;
rerender(<TextDiffuseReveal text="Reveal" />);
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(<TextDiffuseReveal text="Reveal" />);
expect(mockAnimations[0]).toEqual({
animate: {
filter: "blur(12px)",
opacity: 0,
scale: 0.9,
},
});
});
});
@@ -1,7 +1,8 @@
"use client"; "use client";
import { motion, useInView, useReducedMotion } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface TextDiffuseRevealProps { interface TextDiffuseRevealProps {
text: string; text: string;
@@ -101,11 +102,9 @@ export function TextDiffuseReveal({
} }
animate={ animate={
isInView isInView
? shouldReduceMotion ? { filter: "blur(0px)", opacity: 1, scale: 1 }
? { opacity: 1 }
: { filter: "blur(0px)", opacity: 1, scale: 1 }
: shouldReduceMotion : shouldReduceMotion
? { opacity: 0 } ? { filter: "blur(0px)", opacity: 0, scale: 1 }
: { filter: "blur(12px)", opacity: 0, scale: 0.9 } : { filter: "blur(12px)", opacity: 0, scale: 0.9 }
} }
transition={{ transition={{
+2 -1
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { motion, useReducedMotion } from "framer-motion"; import { motion } from "framer-motion";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
interface TopProgressBarProps { interface TopProgressBarProps {
className?: string; className?: string;
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useReducedMotion } from "framer-motion"; import { useReducedMotion } from "@/app/hooks/useReducedMotion";
const HeroScene = dynamic(() => import("./HeroScene"), { ssr: false }); const HeroScene = dynamic(() => import("./HeroScene"), { ssr: false });
+2 -1
View File
@@ -1,8 +1,9 @@
"use client"; "use client";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createPortal } from "react-dom"; 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]; const EXIT_EASE: [number, number, number, number] = [0.76, 0, 0.24, 1];
+9 -2
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useReducedMotion as useFramerReducedMotion } from "framer-motion"; import { useReducedMotion as useFramerReducedMotion } from "framer-motion";
import { useEffect, useState } from "react";
/** /**
* Hook que detecta a preferência prefers-reduced-motion do sistema operacional. * Hook que detecta a preferência prefers-reduced-motion do sistema operacional.
* Wrapper sobre o hook useReducedMotion do Framer Motion. * Wrapper sobre o hook useReducedMotion do Framer Motion.
@@ -20,5 +20,12 @@ import { useReducedMotion as useFramerReducedMotion } from "framer-motion";
* ``` * ```
*/ */
export function useReducedMotion(): boolean { export function useReducedMotion(): boolean {
return useFramerReducedMotion() ?? false; const systemPreference = useFramerReducedMotion();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted && systemPreference === true;
} }