diff --git a/src/components/Background.tsx b/src/components/Background.tsx index 2979b62..eb798d7 100644 --- a/src/components/Background.tsx +++ b/src/components/Background.tsx @@ -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 ( ); } diff --git a/src/components/Certifications.tsx b/src/components/Certifications.tsx index 3cca71a..6ba4b55 100644 --- a/src/components/Certifications.tsx +++ b/src/components/Certifications.tsx @@ -87,12 +87,29 @@ export default function Certifications({ certifications, extraCerts = [] }: Prop {/* Large Picture / Certificate Document Preview Container */}
- {cert.title} + {cert.badgeImage ? ( + {cert.title} { + // 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} +
+ + + +
{/* Subtle Preview Indicator overlay */}
@@ -149,6 +166,7 @@ export default function Certifications({ certifications, extraCerts = [] }: Prop {selectedCert.title}
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index f0ea302..b6962a9 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -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 */}
- {/* Backdrop Aura */} - + {/* Subtle Backdrop Aura (Decreased Color Strength, Static) */} +
}> { 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 { 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; diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index 4162c30..453783b 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -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 } diff --git a/src/pages/certifications.astro b/src/pages/certifications.astro index e3ec3e0..ef3cf48 100644 --- a/src/pages/certifications.astro +++ b/src/pages/certifications.astro @@ -18,7 +18,7 @@ const socialLinks = await getSocialLinks();
- +