feat: harden portfolio and add SEO foundations
This commit is contained in:
1 parent
b079afdd73
commit
62bcfbd8fa
121 files changed
+7769
-10469
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user