fix: resolve certifications visibility with referrerPolicy, merge sanity certs, and refine non-colliding zoom parallax

This commit is contained in:
ssbg04 committed 2026-09-04 00:18:01 +08:00
1 parent 408f968e96
commit 3b7425c5b6
8 files changed
+344 -79

No files matched your search

+291 -52
View File
@@ -5,32 +5,54 @@ export default function Background() {
const [mounted, setMounted] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
// Mouse spring for smooth desktop ambient lighting tracking
const mouseX = useSpring(0, { stiffness: 50, damping: 28 });
const mouseY = useSpring(0, { stiffness: 50, damping: 28 });
// Smooth mouse inertia on desktop
const mouseX = useSpring(0, { stiffness: 40, damping: 24 });
const mouseY = useSpring(0, { stiffness: 40, damping: 24 });
// Native window scroll tracking
const { scrollY } = useScroll();
// Distinct parallax movement across scroll depths
// Layer 1: Dot Matrix Grid (drifts downward with scroll)
const gridY = useTransform(scrollY, [0, 2000], [0, 140]);
// ─── Layer 1: Dot Matrix Grid ───
const gridY = useTransform(scrollY, [0, 3000], [0, 100]);
// Layer 2: Primary Indigo-Blue Orb (moves down and right)
const orb1ScrollY = useTransform(scrollY, [0, 2000], [0, 380]);
const orb1ScrollX = useTransform(scrollY, [0, 2000], [0, 120]);
// ─── Unified Direction Movement (Prevents any collision during scroll) ───
// All shapes drift in the same cohesive direction at gentle depth offsets
const shape1ScrollY = useTransform(scrollY, [0, 3000], [0, 160]);
const shape1ScrollX = useTransform(scrollY, [0, 3000], [0, 30]);
const shape1Rotate = useTransform(scrollY, [0, 3000], [0, 12]);
// Shape 1: Zooms in a little as you scroll
const shape1Scale = useTransform(scrollY, [0, 1500, 3000], [0.95, 1.08, 1.0]);
// Layer 3: Secondary Violet-Cyan Orb (moves up and counter-left)
const orb2ScrollY = useTransform(scrollY, [0, 2000], [0, -320]);
const orb2ScrollX = useTransform(scrollY, [0, 2000], [0, -100]);
const shape2ScrollY = useTransform(scrollY, [0, 3000], [0, 120]);
const shape2ScrollX = useTransform(scrollY, [0, 3000], [0, -25]);
const shape2Rotate = useTransform(scrollY, [0, 3000], [0, -15]);
// Shape 2: Zooms out a little as you scroll
const shape2Scale = useTransform(scrollY, [0, 1500, 3000], [1.06, 0.92, 0.98]);
// Layer 4: Emerald-Teal Accent Glow (floats diagonally)
const orb3ScrollY = useTransform(scrollY, [0, 2000], [0, -220]);
const orb3ScrollX = useTransform(scrollY, [0, 2000], [0, 80]);
const shape3ScrollY = useTransform(scrollY, [0, 3000], [0, 140]);
const shape3ScrollX = useTransform(scrollY, [0, 3000], [0, 30]);
const shape3Rotate = useTransform(scrollY, [0, 3000], [-5, 10]);
// Shape 3: Zooms in a little as you scroll past it
const shape3Scale = useTransform(scrollY, [0, 1500, 3000], [0.92, 1.07, 1.0]);
// Layer 5: Floating Geometric Depth Rings (distinct, noticeable parallax markers)
const ring1Y = useTransform(scrollY, [0, 2000], [0, 450]);
const ring2Y = useTransform(scrollY, [0, 2000], [0, -280]);
const shape4ScrollY = useTransform(scrollY, [0, 3000], [0, 110]);
const shape4ScrollX = useTransform(scrollY, [0, 3000], [0, -25]);
const shape4Rotate = useTransform(scrollY, [0, 3000], [0, 15]);
// Shape 4: Zooms out a little
const shape4Scale = useTransform(scrollY, [0, 1500, 3000], [1.05, 0.93, 1.02]);
// Unified Scroll + Mouse transforms (prevents Framer Motion transform collisions)
const shape1FinalX = useTransform([shape1ScrollX, mouseX], ([sX, mX]) => (sX as number) + (mX as number));
const shape1FinalY = useTransform([shape1ScrollY, mouseY], ([sY, mY]) => (sY as number) + (mY as number));
const shape2FinalX = useTransform([shape2ScrollX, mouseX], ([sX, mX]) => (sX as number) - (mX as number) * 1.1);
const shape2FinalY = useTransform([shape2ScrollY, mouseY], ([sY, mY]) => (sY as number) - (mY as number) * 1.1);
const shape3FinalX = useTransform([shape3ScrollX, mouseX], ([sX, mX]) => (sX as number) + (mX as number) * 0.8);
const shape3FinalY = useTransform([shape3ScrollY, mouseY], ([sY, mY]) => (sY as number) + (mY as number) * 0.8);
const shape4FinalX = useTransform([shape4ScrollX, mouseX], ([sX, mX]) => (sX as number) - (mX as number) * 0.9);
const shape4FinalY = useTransform([shape4ScrollY, mouseY], ([sY, mY]) => (sY as number) + (mY as number) * 0.9);
useEffect(() => {
setMounted(true);
@@ -43,9 +65,8 @@ export default function Background() {
const handleMouseMove = (e: MouseEvent) => {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
// Responsive mouse parallax offset (up to ~55px)
mouseX.set((e.clientX - centerX) * 0.06);
mouseY.set((e.clientY - centerY) * 0.06);
mouseX.set((e.clientX - centerX) * 0.04);
mouseY.set((e.clientY - centerY) * 0.04);
};
window.addEventListener('mousemove', handleMouseMove, { passive: true });
@@ -56,66 +77,284 @@ export default function Background() {
return (
<div className="fixed inset-0 z-0 pointer-events-none overflow-hidden select-none" aria-hidden="true">
{/* ─── Layer 1: Visible Parallax Dot Matrix Grid ─── */}
{/* ─── Layer 1: Dot Matrix Grid (Code Matrix) ─── */}
<motion.div
style={reducedMotion ? {} : { y: gridY }}
className="absolute -top-32 -bottom-32 inset-x-0 bg-[radial-gradient(hsla(var(--foreground)/0.18)_1.2px,transparent_1.2px)] [background-size:28px_28px] [mask-image:radial-gradient(ellipse_85%_85%_at_50%_50%,#000_65%,transparent_100%)] opacity-60 dark:opacity-45 will-change-transform"
className="absolute -top-32 -bottom-32 inset-x-0 bg-[radial-gradient(hsla(var(--foreground)/0.12)_1px,transparent_1px)] [background-size:24px_24px] [mask-image:radial-gradient(ellipse_80%_80%_at_50%_50%,#000_60%,transparent_100%)] opacity-40 will-change-transform"
/>
{/* ─── Layer 2: Vibrant Indigo-Blue Gradient Orb (Top-Left) ─── */}
{/* ─── 1. Top-Left: Stylized Code Brackets & Tag </> (Zooms in subtly on scroll) ─── */}
<motion.div
style={
reducedMotion
? {}
: {
x: orb1ScrollX,
y: orb1ScrollY,
translateX: mouseX,
translateY: mouseY,
x: shape1FinalX,
y: shape1FinalY,
rotate: shape1Rotate,
scale: shape1Scale,
}
}
className="absolute -top-40 -left-40 w-[520px] sm:w-[620px] h-[520px] sm:h-[620px] rounded-full bg-gradient-to-br from-blue-600/25 via-indigo-500/20 to-purple-600/15 dark:from-blue-500/25 dark:via-indigo-500/25 dark:to-cyan-400/20 blur-[85px] will-change-transform"
/>
className="absolute top-[3%] left-[4%] sm:left-[10%] w-[360px] sm:w-[460px] h-[360px] sm:h-[460px] will-change-transform"
>
<svg viewBox="0 0 500 500" className="w-full h-full drop-shadow-xs">
<defs>
<linearGradient id="codeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="var(--primary-color)" stopOpacity="0.08" />
<stop offset="100%" stopColor="var(--secondary-color)" stopOpacity="0.02" />
</linearGradient>
</defs>
{/* Background Outer Hex / Diamond Frame */}
<polygon
points="250,30 450,140 450,360 250,470 50,360 50,140"
fill="url(#codeGrad)"
stroke="var(--primary-color)"
strokeOpacity="0.18"
strokeWidth="1.5"
/>
{/* Inner Dashed Tech Ring */}
<circle
cx="250"
cy="250"
r="170"
fill="none"
stroke="var(--primary-color)"
strokeOpacity="0.10"
strokeWidth="1"
strokeDasharray="6 6"
/>
{/* Left Angle Bracket < */}
<path
d="M 190 170 L 110 250 L 190 330"
fill="none"
stroke="var(--primary-color)"
strokeOpacity="0.30"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Center Forward Slash / */}
<line
x1="275"
y1="160"
x2="225"
y2="340"
stroke="var(--secondary-color)"
strokeOpacity="0.28"
strokeWidth="3.5"
strokeLinecap="round"
/>
{/* Right Angle Bracket > */}
<path
d="M 310 170 L 390 250 L 310 330"
fill="none"
stroke="var(--primary-color)"
strokeOpacity="0.30"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Subtle Dev Watermark text */}
<text
x="250"
y="410"
textAnchor="middle"
fill="var(--primary-color)"
fillOpacity="0.25"
fontSize="13"
fontFamily="var(--font-mono)"
letterSpacing="6"
>
&lt;DEV_ENGINE /&gt;
</text>
</svg>
</motion.div>
{/* ─── Layer 3: Violet-Pink-Cyan Secondary Orb (Center-Right) ─── */}
{/* ─── 2. Upper-Mid Right: Cisco Network Topology & Graph Nodes (Zooms out subtly on scroll) ─── */}
<motion.div
style={
reducedMotion
? {}
: {
x: orb2ScrollX,
y: orb2ScrollY,
translateX: useTransform(mouseX, (v) => -v * 1.3),
translateY: useTransform(mouseY, (v) => -v * 1.3),
x: shape2FinalX,
y: shape2FinalY,
rotate: shape2Rotate,
scale: shape2Scale,
}
}
className="absolute top-1/3 -right-36 w-[500px] sm:w-[580px] h-[500px] sm:h-[580px] rounded-full bg-gradient-to-bl from-purple-600/25 via-fuchsia-500/20 to-cyan-500/15 dark:from-purple-500/25 dark:via-pink-500/20 dark:to-indigo-500/20 blur-[90px] will-change-transform"
/>
className="absolute top-[20%] right-[4%] sm:right-[10%] w-[350px] sm:w-[440px] h-[350px] sm:h-[440px] will-change-transform"
>
<svg viewBox="0 0 500 500" className="w-full h-full drop-shadow-xs">
{/* Connecting Bus Lines */}
<line x1="250" y1="250" x2="120" y2="130" stroke="var(--secondary-color)" strokeOpacity="0.20" strokeWidth="1.5" />
<line x1="250" y1="250" x2="380" y2="120" stroke="var(--secondary-color)" strokeOpacity="0.20" strokeWidth="1.5" />
<line x1="250" y1="250" x2="390" y2="380" stroke="var(--secondary-color)" strokeOpacity="0.20" strokeWidth="1.5" />
<line x1="250" y1="250" x2="110" y2="370" stroke="var(--secondary-color)" strokeOpacity="0.20" strokeWidth="1.5" />
<line x1="120" y1="130" x2="380" y2="120" stroke="var(--secondary-color)" strokeOpacity="0.12" strokeWidth="1" strokeDasharray="4 4" />
<line x1="110" y1="370" x2="390" y2="380" stroke="var(--secondary-color)" strokeOpacity="0.12" strokeWidth="1" strokeDasharray="4 4" />
{/* ─── Layer 4: Emerald-Teal Radiant Orb (Lower Section) ─── */}
{/* Central Hub Switch / Router Node */}
<circle cx="250" cy="250" r="50" fill="var(--secondary-color)" fillOpacity="0.05" stroke="var(--secondary-color)" strokeOpacity="0.28" strokeWidth="1.8" />
<circle cx="250" cy="250" r="68" fill="none" stroke="var(--secondary-color)" strokeOpacity="0.12" strokeWidth="1" strokeDasharray="5 5" />
{/* Core Arrows Symbol in Center */}
<path d="M 235 240 L 250 225 L 265 240 M 250 225 L 250 275 M 235 260 L 250 275 L 265 260" stroke="var(--secondary-color)" strokeOpacity="0.35" strokeWidth="2" strokeLinecap="round" />
{/* Satellite Nodes */}
<circle cx="120" cy="130" r="28" fill="var(--primary-color)" fillOpacity="0.04" stroke="var(--primary-color)" strokeOpacity="0.22" strokeWidth="1.5" />
<circle cx="120" cy="130" r="10" fill="var(--primary-color)" fillOpacity="0.25" />
<circle cx="380" cy="120" r="28" fill="var(--secondary-color)" fillOpacity="0.04" stroke="var(--secondary-color)" strokeOpacity="0.22" strokeWidth="1.5" />
<circle cx="380" cy="120" r="10" fill="var(--secondary-color)" fillOpacity="0.25" />
<circle cx="390" cy="380" r="32" fill="var(--primary-color)" fillOpacity="0.04" stroke="var(--primary-color)" strokeOpacity="0.22" strokeWidth="1.5" />
<circle cx="390" cy="380" r="12" fill="var(--primary-color)" fillOpacity="0.25" />
<circle cx="110" cy="370" r="26" fill="var(--secondary-color)" fillOpacity="0.04" stroke="var(--secondary-color)" strokeOpacity="0.22" strokeWidth="1.5" />
<circle cx="110" cy="370" r="9" fill="var(--secondary-color)" fillOpacity="0.25" />
{/* Network Data Tag */}
<text
x="250"
y="335"
textAnchor="middle"
fill="var(--secondary-color)"
fillOpacity="0.25"
fontSize="12"
fontFamily="var(--font-mono)"
letterSpacing="4"
>
NODE_TOPOLOGY
</text>
</svg>
</motion.div>
{/* ─── 3. Lower-Mid Left: Floating Developer Terminal / IDE Blueprint (Zooms in subtly on scroll) ─── */}
<motion.div
style={
reducedMotion
? {}
: {
x: orb3ScrollX,
y: orb3ScrollY,
translateX: useTransform(mouseX, (v) => v * 0.9),
x: shape3FinalX,
y: shape3FinalY,
rotate: shape3Rotate,
scale: shape3Scale,
}
}
className="absolute -bottom-48 left-1/4 w-[480px] sm:w-[560px] h-[480px] sm:h-[560px] rounded-full bg-gradient-to-tr from-emerald-500/20 via-teal-500/20 to-blue-500/15 dark:from-emerald-400/20 dark:via-cyan-500/20 dark:to-blue-500/20 blur-[85px] will-change-transform"
/>
className="absolute top-[56%] left-[4%] sm:left-[11%] w-[340px] sm:w-[430px] h-[340px] sm:h-[430px] will-change-transform"
>
<svg viewBox="0 0 480 380" className="w-full h-full">
{/* Main Terminal Window Frame */}
<rect
x="30"
y="30"
width="420"
height="320"
rx="20"
fill="var(--primary-color)"
fillOpacity="0.03"
stroke="var(--primary-color)"
strokeOpacity="0.22"
strokeWidth="1.5"
/>
{/* Terminal Window Header Bar */}
<line x1="30" y1="75" x2="450" y2="75" stroke="var(--primary-color)" strokeOpacity="0.18" strokeWidth="1.2" />
{/* 3 Header Window Dots */}
<circle cx="65" cy="53" r="6" fill="var(--primary-color)" fillOpacity="0.30" />
<circle cx="85" cy="53" r="6" fill="var(--primary-color)" fillOpacity="0.20" />
<circle cx="105" cy="53" r="6" fill="var(--primary-color)" fillOpacity="0.20" />
{/* ─── Layer 5: High-Visibility Parallax Depth Markers ─── */}
{/* Subtle floating ambient rings that visibly move faster/slower than cards */}
{/* Terminal Header Title */}
<text x="240" y="58" textAnchor="middle" fill="var(--primary-color)" fillOpacity="0.25" fontSize="11" fontFamily="var(--font-mono)">
portfolio_runtime.sh
</text>
{/* Terminal Prompt >_ */}
<text x="65" y="125" fill="var(--primary-color)" fillOpacity="0.40" fontSize="20" fontFamily="var(--font-mono)" fontWeight="bold">
&gt;_
</text>
{/* Mock Code Line Bars */}
<rect x="110" y="114" width="180" height="12" rx="4" fill="var(--primary-color)" fillOpacity="0.14" />
<rect x="65" y="155" width="260" height="10" rx="4" fill="var(--foreground)" fillOpacity="0.08" />
<rect x="65" y="185" width="200" height="10" rx="4" fill="var(--foreground)" fillOpacity="0.08" />
<rect x="65" y="215" width="310" height="10" rx="4" fill="var(--secondary-color)" fillOpacity="0.12" />
<rect x="65" y="245" width="150" height="10" rx="4" fill="var(--foreground)" fillOpacity="0.08" />
<rect x="65" y="275" width="230" height="10" rx="4" fill="var(--primary-color)" fillOpacity="0.10" />
{/* Blinking Cursor Bar */}
<rect x="302" y="112" width="10" height="16" rx="2" fill="var(--primary-color)" fillOpacity="0.35" />
</svg>
</motion.div>
{/* ─── 4. Bottom-Right: Git Commit Graph & Curly Braces { } (Zooms out subtly on scroll) ─── */}
<motion.div
style={reducedMotion ? {} : { y: ring1Y }}
className="hidden md:block absolute top-1/4 left-[8%] w-72 h-72 rounded-full border border-primary-custom/20 dark:border-primary-custom/25 [mask-image:radial-gradient(circle_at_center,transparent_45%,#000_65%)] will-change-transform"
/>
<motion.div
style={reducedMotion ? {} : { y: ring2Y }}
className="hidden md:block absolute top-2/3 right-[10%] w-96 h-96 rounded-full border border-secondary-custom/20 dark:border-secondary-custom/25 [mask-image:radial-gradient(circle_at_center,transparent_45%,#000_65%)] will-change-transform"
/>
style={
reducedMotion
? {}
: {
x: shape4FinalX,
y: shape4FinalY,
rotate: shape4Rotate,
scale: shape4Scale,
}
}
className="absolute bottom-[4%] right-[5%] sm:right-[12%] w-[360px] sm:w-[450px] h-[360px] sm:h-[450px] will-change-transform"
>
<svg viewBox="0 0 500 500" className="w-full h-full">
{/* Left Large Curly Brace { */}
<path
d="M 140 80 Q 90 80 90 160 Q 90 230 60 250 Q 90 270 90 340 Q 90 420 140 420"
fill="none"
stroke="var(--primary-color)"
strokeOpacity="0.25"
strokeWidth="2.5"
strokeLinecap="round"
/>
{/* Right Large Curly Brace } */}
<path
d="M 360 80 Q 410 80 410 160 Q 410 230 440 250 Q 410 270 410 340 Q 410 420 360 420"
fill="none"
stroke="var(--primary-color)"
strokeOpacity="0.25"
strokeWidth="2.5"
strokeLinecap="round"
/>
{/* Git Commit Branch Graph in Center */}
{/* Main Branch Line */}
<line x1="220" y1="100" x2="220" y2="400" stroke="var(--primary-color)" strokeOpacity="0.25" strokeWidth="2" />
{/* Feature Branch Curve */}
<path
d="M 220 160 C 220 200 280 200 280 250 C 280 300 220 300 220 340"
fill="none"
stroke="var(--secondary-color)"
strokeOpacity="0.28"
strokeWidth="2"
strokeDasharray="5 3"
/>
{/* Commit Nodes */}
<circle cx="220" cy="120" r="9" fill="var(--primary-color)" fillOpacity="0.35" stroke="var(--primary-color)" strokeWidth="2" />
<circle cx="220" cy="180" r="7" fill="var(--primary-color)" fillOpacity="0.30" />
<circle cx="280" cy="250" r="9" fill="var(--secondary-color)" fillOpacity="0.40" stroke="var(--secondary-color)" strokeWidth="2" />
<circle cx="220" cy="320" r="7" fill="var(--primary-color)" fillOpacity="0.30" />
<circle cx="220" cy="380" r="9" fill="var(--primary-color)" fillOpacity="0.35" stroke="var(--primary-color)" strokeWidth="2" />
{/* Git Branch Label */}
<text
x="250"
y="450"
textAnchor="middle"
fill="var(--primary-color)"
fillOpacity="0.25"
fontSize="12"
fontFamily="var(--font-mono)"
letterSpacing="5"
>
git:main [HEAD]
</text>
</svg>
</motion.div>
</div>
);
}
+24 -6
View File
@@ -87,12 +87,29 @@ export default function Certifications({ certifications, extraCerts = [] }: Prop
{/* Large Picture / Certificate Document Preview Container */}
<div className="w-full h-44 sm:h-48 my-3 rounded-2xl bg-foreground-custom/[0.02] border border-border-custom/60 flex items-center justify-center p-3 relative overflow-hidden group-hover:border-primary-custom/30 transition-all">
<img
src={cert.badgeImage}
alt={cert.title}
loading="lazy"
className="max-h-full max-w-full object-contain rounded-lg drop-shadow-sm group-hover:scale-105 transition-transform duration-300"
/>
{cert.badgeImage ? (
<img
src={cert.badgeImage}
alt={cert.title}
loading="lazy"
referrerPolicy="no-referrer"
className="max-h-full max-w-full object-contain rounded-lg drop-shadow-sm group-hover:scale-105 transition-transform duration-300"
onError={(e) => {
// If remote image fails, show subtle fallback badge
const parent = (e.currentTarget as HTMLElement).parentElement;
if (parent) {
(e.currentTarget as HTMLElement).style.display = 'none';
const fallback = parent.querySelector('.cert-fallback-icon');
if (fallback) (fallback as HTMLElement).classList.remove('hidden');
}
}}
/>
) : null}
<div className={`cert-fallback-icon w-16 h-16 rounded-2xl bg-primary-custom/10 flex items-center justify-center text-primary-custom ${cert.badgeImage ? 'hidden' : ''}`}>
<svg className="w-8 h-8" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z" />
</svg>
</div>
{/* Subtle Preview Indicator overlay */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center gap-1.5 text-white text-xs font-medium rounded-2xl backdrop-blur-xs">
@@ -149,6 +166,7 @@ export default function Certifications({ certifications, extraCerts = [] }: Prop
<img
src={selectedCert.badgeImage}
alt={selectedCert.title}
referrerPolicy="no-referrer"
className="max-h-[46vh] w-auto max-w-full object-contain rounded-lg drop-shadow-md"
/>
</div>
+3 -5
View File
@@ -1,5 +1,5 @@
import React, { Suspense, lazy } from 'react';
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { motion, useMotionValue, useTransform, useScroll } from 'framer-motion';
import { useIsMobile } from '../lib/hooks';
import type { SocialLink } from '../lib/data';
import ppDay from '../assets/pp-day.webp';
@@ -308,10 +308,8 @@ export default function Hero({
{/* Right Column: Profile Image Frame */}
<div className="md:col-span-5 flex justify-center order-1 md:order-2">
<div className="relative w-48 h-48 sm:w-64 sm:h-64 md:w-72 md:h-72 lg:w-80 lg:h-80 flex items-center justify-center">
{/* Backdrop Aura */}
<motion.div
className="absolute w-[85%] h-[85%] rounded-[36px] bg-gradient-to-tr from-primary-custom via-secondary-custom to-primary-custom blur-3xl pointer-events-none opacity-30"
/>
{/* Subtle Backdrop Aura (Decreased Color Strength, Static) */}
<div className="absolute w-[85%] h-[85%] rounded-[36px] bg-gradient-to-tr from-primary-custom via-secondary-custom to-primary-custom blur-3xl pointer-events-none opacity-15" />
<Suspense fallback={<div className="w-full h-full rounded-[36px] bg-foreground-custom/5 animate-pulse-slow" />}>
<TiltedCard
imageSrc={heroImage || (ppDay as any).src || ppDay}
+1 -1
View File
@@ -30,7 +30,7 @@ export default function ScrollReveal({
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-8% 0px -8% 0px" }}
viewport={{ once: true, amount: 0.05 }}
variants={chosenVariant}
transition={{
duration: isMobile ? 0.35 : 0.65,
+10 -1
View File
@@ -444,7 +444,7 @@ export async function getCertifications(): Promise<Certification[]> {
if (!sanityClient) return mockCertifications;
const certs = await sanityClient.fetch(`*[_type == "certification"] | order(coalesce(order, 99) asc)`);
if (!certs || certs.length === 0) return mockCertifications;
return certs.map((c: any, idx: number) => {
const sanityCerts = certs.map((c: any, idx: number) => {
const badgeImg = c.badgeImage ? urlFor(c.badgeImage) : (c.badgeImageUrl || '');
return {
id: c._id || `cert-${idx}`,
@@ -461,6 +461,15 @@ export async function getCertifications(): Promise<Certification[]> {
order: typeof c.order === 'number' ? c.order : idx + 1
};
});
// Merge any credentials not yet duplicated in Sanity (e.g. Cisco badges + TESDA NC2)
const combined = [...sanityCerts];
for (const mock of mockCertifications) {
if (!combined.some(c => c.title?.toLowerCase() === mock.title?.toLowerCase() || c.code === mock.code)) {
combined.push(mock);
}
}
return combined;
} catch (error) {
console.error('Error fetching certification from Sanity:', error);
return mockCertifications;
+13 -12
View File
@@ -1,34 +1,35 @@
/**
* Mobile Haptic Feedback Utility using the standard W3C Vibration API.
* Provides tactile micro-vibrations for phone browsers (Android Chrome, Edge, Firefox, Samsung Internet).
* Safely no-ops on desktop and iOS Safari without errors or overhead.
* Provides tactile physical vibrations for mobile phone browsers (Android Chrome, Edge, Firefox, Samsung Internet).
* Note: iOS Safari restricts navigator.vibrate() by Apple system policy.
*/
const canVibrate = (): boolean => {
return typeof window !== 'undefined' && typeof navigator !== 'undefined' && 'vibrate' in navigator;
return typeof window !== 'undefined' && typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
};
export const haptic = {
/**
* Ultra-short 8ms tick for tabs, category filters, and small toggle switches.
* Crisp 35ms pulse for tabs, category filters, and small toggles.
* Long enough for physical vibration motors (ERM & LRA) to overcome inertia.
*/
tick: () => {
if (canVibrate()) {
try {
navigator.vibrate(8);
navigator.vibrate(35);
} catch {
// Safe no-op on devices with restricted permissions
// Safe no-op
}
}
},
/**
* Crisp 15ms tap for standard buttons, theme switcher, and FAB buttons.
* Solid 55ms pulse for standard buttons, theme switcher, and FAB buttons.
*/
tap: () => {
if (canVibrate()) {
try {
navigator.vibrate(15);
navigator.vibrate(55);
} catch {
// Safe no-op
}
@@ -36,12 +37,12 @@ export const haptic = {
},
/**
* 20ms tactile pulse when opening modals, full-screen certificates, or photos.
* 75ms tactile pulse when opening modals, full-screen certificates, or photos.
*/
openModal: () => {
if (canVibrate()) {
try {
navigator.vibrate(20);
navigator.vibrate(75);
} catch {
// Safe no-op
}
@@ -49,12 +50,12 @@ export const haptic = {
},
/**
* Double-pulse [15ms, 40ms, 20ms] confirming actions like copy to clipboard, download, or form submit.
* Distinct double-pulse [45ms, 60ms, 45ms] confirming actions like copy, download, or form submit.
*/
success: () => {
if (canVibrate()) {
try {
navigator.vibrate([15, 40, 20]);
navigator.vibrate([45, 60, 45]);
} catch {
// Safe no-op
}
+1 -1
View File
@@ -18,7 +18,7 @@ const socialLinks = await getSocialLinks();
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<main class="flex-1 min-h-screen pb-16">
<Certifications certifications={certifications} client:visible />
<Certifications certifications={certifications} client:load />
</main>
<Footer fullName={settings.fullName} socialLinks={socialLinks} client:load />
+1 -1
View File
@@ -61,7 +61,7 @@ const socialLinks = await getSocialLinks();
<Skills skills={skills} client:visible />
<Certifications certifications={certifications} client:visible />
<Certifications certifications={certifications} client:load />
<About settings={settings} experience={experience} education={education} client:visible />