feat: harden portfolio and add SEO foundations

This commit is contained in:
Evandro Fonseca Junior committed 2026-09-04 19:25:30 -03:00
1 parent b079afdd73
commit 62bcfbd8fa
121 files changed
+7769 -10469

No files matched your search

+34 -50
View File
@@ -2,7 +2,7 @@
import createGlobe, { type COBEOptions } from "cobe";
import { useMotionValue, useSpring } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
@@ -47,7 +47,7 @@ export function ControlRoomGlobe({
autoRotate?: boolean;
rotationSpeed?: number;
}) {
const [width, setWidth] = useState(0);
const widthRef = useRef(0);
const phiRef = useRef(config.phi ?? JOINVILLE_LNG_RAD);
// Rotação livre: oscilação de theta (inclinação multi-eixo) + arraste vertical
const thetaPhaseRef = useRef(0);
@@ -55,7 +55,7 @@ export function ControlRoomGlobe({
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerYOrigin = useRef<number | null>(null);
const isVisibleRef = useRef(true);
const intersectsRef = useRef(false);
const r = useMotionValue(0);
const rs = useSpring(r, {
@@ -88,57 +88,44 @@ export function ControlRoomGlobe({
};
useEffect(() => {
const onResize = () => {
if (canvasRef.current) {
setWidth(canvasRef.current.offsetWidth);
}
const canvas = canvasRef.current;
if (!canvas) return undefined;
const dpr = config.devicePixelRatio;
const updateWidth = (entry?: ResizeObserverEntry) => {
const nextWidth =
entry?.contentRect.width || canvas.offsetWidth || config.width / dpr;
if (nextWidth > 0) widthRef.current = nextWidth;
};
updateWidth();
window.addEventListener("resize", onResize);
onResize();
const resizeObserver = new ResizeObserver(([entry]) => updateWidth(entry));
resizeObserver.observe(canvas);
const observer = new IntersectionObserver(
const intersectionObserver = new IntersectionObserver(
([entry]) => {
isVisibleRef.current = entry.isIntersecting;
intersectsRef.current = entry.isIntersecting;
},
{ rootMargin: "100px" },
);
intersectionObserver.observe(canvas);
if (canvasRef.current) {
observer.observe(canvasRef.current);
}
const handleVisibilityChange = () => {
isVisibleRef.current = !document.hidden;
};
document.addEventListener("visibilitychange", handleVisibilityChange);
const canvas = canvasRef.current;
if (!canvas) {
return () => {
window.removeEventListener("resize", onResize);
document.removeEventListener(
"visibilitychange",
handleVisibilityChange,
);
observer.disconnect();
};
}
// Falha suave: sem WebGL (ou canvas já vinculado a outro tipo de contexto) o phenomenon
// lança "reading 'enable' of null" e derrubaria a árvore React inteira — card ficaria preto.
let globe: ReturnType<typeof createGlobe> | null = null;
let globe: { destroy(): void } | null = null;
try {
const supportsWebGL =
canvas.getContext("webgl2") ?? canvas.getContext("webgl");
if (supportsWebGL) {
const initialRenderWidth = widthRef.current * dpr;
globe = createGlobe(canvas, {
...config,
width: width * 2,
height: width * 2,
width: initialRenderWidth,
height: initialRenderWidth,
onRender: (state) => {
if (!isVisibleRef.current || document.hidden) return;
if (autoRotate && !pointerInteracting.current) {
if (!autoRotate || !intersectsRef.current || document.hidden)
return;
if (!pointerInteracting.current) {
phiRef.current += rotationSpeed;
thetaPhaseRef.current += rotationSpeed * 0.9;
}
@@ -148,8 +135,9 @@ export function ControlRoomGlobe({
(config.theta ?? TROPIC_OF_CAPRICORN_RAD) +
Math.sin(thetaPhaseRef.current) * 0.35 +
thetaDragRef.current;
state.width = width * 2;
state.height = width * 2;
const renderWidth = widthRef.current * dpr;
state.width = renderWidth;
state.height = renderWidth;
},
});
}
@@ -161,28 +149,24 @@ export function ControlRoomGlobe({
// Sem globo: fallback visível (gradiente do card permanece), página viva.
canvas.style.opacity = "1";
return () => {
window.removeEventListener("resize", onResize);
document.removeEventListener(
"visibilitychange",
handleVisibilityChange,
);
observer.disconnect();
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}
setTimeout(() => {
const revealTimer = window.setTimeout(() => {
if (canvasRef.current) {
canvasRef.current.style.opacity = "1";
}
}, 0);
return () => {
window.clearTimeout(revealTimer);
globe.destroy();
window.removeEventListener("resize", onResize);
document.removeEventListener("visibilitychange", handleVisibilityChange);
observer.disconnect();
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [rs, config, autoRotate, rotationSpeed, width]);
}, [rs, config, autoRotate, rotationSpeed]);
return (
<div