feat: polish sections, optimize widgets, and fix chat assistant
This commit is contained in:
1 parent
bb98eabbde
commit
d30e15f3b9
19 files changed
+286
-247
No files matched your search
@@ -41,9 +41,6 @@ export default function About({
|
||||
About & Background
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground-custom max-w-sm">
|
||||
Professional history, educational credentials, and developer profile.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -300,7 +297,11 @@ export default function About({
|
||||
|
||||
{edu.location && (
|
||||
<div className="pt-2 text-[11px] font-mono text-muted-foreground-custom flex items-center gap-1.5">
|
||||
<span>📍 {edu.location}</span>
|
||||
<svg className="w-3.5 h-3.5 text-muted-foreground-custom shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" />
|
||||
</svg>
|
||||
<span>{edu.location}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -331,9 +331,6 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
Verified Credentials & Certifications
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground-custom max-w-sm">
|
||||
Official certifications, diplomas, and digitally verified skill badges.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -386,7 +383,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
: 'text-muted-foreground-custom hover:text-foreground-custom hover:bg-foreground-custom/5'
|
||||
}`}
|
||||
>
|
||||
<span>Skill Badges</span>
|
||||
<span>Badges</span>
|
||||
<span className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${filterMode === 'badges' ? 'bg-white/20 text-white' : 'bg-foreground-custom/10 text-muted-foreground-custom'}`}>
|
||||
{skillBadges.length}
|
||||
</span>
|
||||
@@ -400,11 +397,11 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
<div className="mb-12">
|
||||
{filterMode === 'all' && hasBothTypes && (
|
||||
<div className="flex items-center gap-2.5 mb-5">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-500 animate-pulse" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-primary-custom" />
|
||||
<h3 className="text-xl sm:text-2xl font-bold font-heading text-foreground-custom tracking-tight">
|
||||
Official Certificates & Diplomas
|
||||
Certificates
|
||||
</h3>
|
||||
<span className="text-xs font-mono px-2.5 py-0.5 rounded-full bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 font-semibold">
|
||||
<span className="text-xs font-mono px-2.5 py-0.5 rounded-full bg-foreground-custom/[0.04] text-muted-foreground-custom border border-border-custom font-medium">
|
||||
{documentCerts.length} Document{documentCerts.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -423,7 +420,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedCert(cert); }}
|
||||
aria-label={`View full certificate document for ${cert.title}`}
|
||||
className="bento-card p-5 sm:p-6 flex flex-col justify-between h-full group cursor-pointer hover:border-amber-500/50 hover:-translate-y-1.5 transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500"
|
||||
className="bento-card p-5 sm:p-6 flex flex-col justify-between h-full group cursor-pointer hover:border-primary-custom/40 hover:-translate-y-1 transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-custom"
|
||||
>
|
||||
<div>
|
||||
{/* Top Bar: Code & Official Certificate Pill */}
|
||||
@@ -432,9 +429,9 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
<span className="text-[10px] font-mono text-muted-foreground-custom font-medium">
|
||||
#{cert.code}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono font-semibold px-2 py-0.5 rounded-md bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
||||
<span>Certificate Document</span>
|
||||
<span className="text-[10px] font-mono font-medium px-2 py-0.5 rounded-md bg-foreground-custom/[0.04] text-muted-foreground-custom border border-border-custom flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground-custom/60" />
|
||||
<span>Certificate</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono font-bold text-foreground-custom">
|
||||
@@ -443,7 +440,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
</div>
|
||||
|
||||
{/* Large Document Preview Viewport (Aspect-16/10) */}
|
||||
<div className="w-full aspect-[16/10] my-2 rounded-2xl bg-foreground-custom/[0.02] border border-border-custom/70 flex items-center justify-center p-3 relative overflow-hidden group-hover:border-amber-500/40 transition-all shadow-inner">
|
||||
<div className="w-full aspect-[16/10] my-2 rounded-2xl bg-foreground-custom/[0.02] border border-border-custom/70 flex items-center justify-center p-3 relative overflow-hidden group-hover:border-primary-custom/30 transition-all shadow-inner">
|
||||
{cert.badgeImage ? (
|
||||
<img
|
||||
src={cert.badgeImage}
|
||||
@@ -461,7 +458,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className={`cert-fallback-icon w-16 h-16 rounded-2xl bg-amber-500/10 flex items-center justify-center text-amber-500 ${cert.badgeImage ? 'hidden' : ''}`}>
|
||||
<div className={`cert-fallback-icon w-16 h-16 rounded-2xl bg-foreground-custom/5 flex items-center justify-center text-muted-foreground-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="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
@@ -469,7 +466,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
|
||||
{/* Interactive Zoom & Drag Callout overlay */}
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center gap-2 text-white text-xs font-medium rounded-2xl backdrop-blur-xs">
|
||||
<svg className="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607ZM10.5 7.5v6m3-3h-6" />
|
||||
</svg>
|
||||
<span className="font-semibold">Click to Zoom & Pan Document</span>
|
||||
@@ -477,7 +474,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
</div>
|
||||
|
||||
{/* Title & Metadata */}
|
||||
<h4 className="text-base sm:text-lg font-bold font-heading text-foreground-custom group-hover:text-amber-500 transition-colors leading-snug mt-3">
|
||||
<h4 className="text-base sm:text-lg font-bold font-heading text-foreground-custom group-hover:text-primary-custom transition-colors leading-snug mt-3">
|
||||
{cert.title}
|
||||
</h4>
|
||||
{cert.description && (
|
||||
@@ -492,8 +489,8 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
<span className="text-[11px] font-mono text-muted-foreground-custom">
|
||||
{cert.issueDate ? `Issued ${cert.issueDate}` : 'Verified Credential'}
|
||||
</span>
|
||||
<span className="text-[11px] font-mono text-amber-600 dark:text-amber-400 font-semibold group-hover:underline flex items-center gap-1">
|
||||
<span>Inspect Document</span>
|
||||
<span className="text-[11px] font-mono text-primary-custom font-semibold group-hover:underline flex items-center gap-1">
|
||||
<span>View Certificate</span>
|
||||
<span>↗</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -509,11 +506,11 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
<div>
|
||||
{filterMode === 'all' && hasBothTypes && (
|
||||
<div className="flex items-center gap-2.5 mb-5 pt-4">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-primary-custom" />
|
||||
<h3 className="text-xl sm:text-2xl font-bold font-heading text-foreground-custom tracking-tight">
|
||||
Verified Digital Skill Badges
|
||||
Badges
|
||||
</h3>
|
||||
<span className="text-xs font-mono px-2.5 py-0.5 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 font-semibold">
|
||||
<span className="text-xs font-mono px-2.5 py-0.5 rounded-full bg-foreground-custom/[0.04] text-muted-foreground-custom border border-border-custom font-medium">
|
||||
{skillBadges.length} Badge{skillBadges.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -532,7 +529,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedCert(badge); }}
|
||||
aria-label={`View details for badge ${badge.title}`}
|
||||
className="bento-card p-4 sm:p-5 flex flex-col justify-between items-center text-center h-full group cursor-pointer hover:border-emerald-500/50 hover:-translate-y-1.5 transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
||||
className="bento-card p-4 sm:p-5 flex flex-col justify-between items-center text-center h-full group cursor-pointer hover:border-primary-custom/40 hover:-translate-y-1 transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-custom"
|
||||
>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
{/* Top Bar: Code & Skill Badge Pill */}
|
||||
@@ -540,16 +537,16 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
<span className="text-[10px] font-mono text-muted-foreground-custom font-medium">
|
||||
#{badge.code}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono font-semibold px-2 py-0.5 rounded-md bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
<span>Digital Badge</span>
|
||||
<span className="text-[10px] font-mono font-medium px-2 py-0.5 rounded-md bg-foreground-custom/[0.04] text-muted-foreground-custom border border-border-custom flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground-custom/60" />
|
||||
<span>Badge</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Authentic Digital Badge Viewport (Centered icon with ambient glow) */}
|
||||
<div className="relative w-28 h-28 my-2 flex items-center justify-center">
|
||||
{/* Ambient Backdrop Glow */}
|
||||
<div className="absolute inset-2 rounded-full bg-emerald-500/10 group-hover:bg-emerald-500/20 blur-md transition-all" />
|
||||
<div className="absolute inset-2 rounded-full bg-foreground-custom/[0.02] group-hover:bg-foreground-custom/[0.04] blur-md transition-all" />
|
||||
|
||||
{badge.badgeImage ? (
|
||||
<img
|
||||
@@ -568,7 +565,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className={`badge-fallback-icon w-20 h-20 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-500 ${badge.badgeImage ? 'hidden' : ''}`}>
|
||||
<div className={`badge-fallback-icon w-20 h-20 rounded-full bg-foreground-custom/5 flex items-center justify-center text-muted-foreground-custom ${badge.badgeImage ? 'hidden' : ''}`}>
|
||||
<svg className="w-10 h-10" 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>
|
||||
@@ -576,7 +573,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
</div>
|
||||
|
||||
{/* Badge Title */}
|
||||
<h4 className="text-sm font-bold font-heading text-foreground-custom group-hover:text-emerald-500 transition-colors leading-snug line-clamp-2 mt-2 px-1">
|
||||
<h4 className="text-sm font-bold font-heading text-foreground-custom group-hover:text-primary-custom transition-colors leading-snug line-clamp-2 mt-2 px-1">
|
||||
{badge.title}
|
||||
</h4>
|
||||
|
||||
@@ -592,7 +589,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full py-1.5 px-2.5 rounded-xl bg-foreground-custom/[0.04] hover:bg-emerald-500/10 text-foreground-custom hover:text-emerald-500 border border-border-custom text-[11px] font-mono font-semibold transition-colors flex items-center justify-center gap-1"
|
||||
className="w-full py-1.5 px-2.5 rounded-xl bg-foreground-custom/[0.04] hover:bg-primary-custom/10 text-foreground-custom hover:text-primary-custom border border-border-custom text-[11px] font-mono font-semibold transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
<span>Verify Credential</span>
|
||||
<span>↗</span>
|
||||
@@ -654,7 +651,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
|
||||
{/* Zoom & Drag Callout Badge */}
|
||||
<div className="absolute bottom-3 right-3 px-3 py-1.5 rounded-xl bg-black/75 hover:bg-black/90 text-white text-xs font-mono font-medium backdrop-blur-md border border-white/20 flex items-center gap-1.5 shadow-lg opacity-85 group-hover/preview:opacity-100 transition-all">
|
||||
<svg className="w-3.5 h-3.5 text-primary-custom" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607ZM10.5 7.5v6m3-3h-6" />
|
||||
</svg>
|
||||
<span>Click to Zoom & Drag</span>
|
||||
@@ -664,13 +661,9 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
{/* Title & Metadata */}
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-2">
|
||||
<span className={`text-[10px] font-mono uppercase tracking-wider px-2.5 py-0.5 rounded-full font-semibold flex items-center gap-1.5 ${
|
||||
isDocumentCertificate(selectedCert)
|
||||
? 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${isDocumentCertificate(selectedCert) ? 'bg-amber-500' : 'bg-emerald-500'}`} />
|
||||
{isDocumentCertificate(selectedCert) ? 'OFFICIAL CERTIFICATE' : 'VERIFIED DIGITAL BADGE'}
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider px-2.5 py-0.5 rounded-full font-medium flex items-center gap-1.5 bg-foreground-custom/[0.04] text-muted-foreground-custom border border-border-custom">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground-custom/60" />
|
||||
{isDocumentCertificate(selectedCert) ? 'Certificate' : 'Badge'}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground-custom px-2.5 py-0.5 rounded-full bg-foreground-custom/5 border border-border-custom">
|
||||
Code: #{selectedCert.code}
|
||||
@@ -681,7 +674,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
{selectedCert.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-xs font-mono text-primary-custom font-medium">
|
||||
<p className="text-xs font-mono text-muted-foreground-custom font-medium">
|
||||
Issued by {selectedCert.issuer}
|
||||
{selectedCert.issueDate ? ` • ${selectedCert.issueDate}` : ''}
|
||||
</p>
|
||||
@@ -749,8 +742,8 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col min-w-0 pr-4">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-emerald-400 font-semibold flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-zinc-400 font-medium flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400" />
|
||||
{selectedCert.issuer} • #{selectedCert.code}
|
||||
</span>
|
||||
<h4 className="text-sm sm:text-base font-bold truncate text-zinc-100 mt-0.5">
|
||||
|
||||
@@ -107,6 +107,7 @@ export default function ControlsDropdown() {
|
||||
const nextTier = next ? 'low' : ((navigator.hardwareConcurrency >= 8 && window.devicePixelRatio >= 2) ? 'high' : 'mid');
|
||||
document.documentElement.dataset.tier = nextTier;
|
||||
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: nextTier } }));
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -334,7 +335,10 @@ export default function ControlsDropdown() {
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] font-mono font-semibold uppercase tracking-wider text-muted-foreground-custom flex items-center gap-1.5">
|
||||
<span>⚡</span> Lite Mode (Data Saver)
|
||||
<svg className="w-3.5 h-3.5 text-amber-500 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
Lite Mode (Data Saver)
|
||||
</span>
|
||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${
|
||||
isLiteMode ? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 font-semibold' : 'bg-muted-custom/60 text-muted-foreground-custom'
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function Footer({ fullName = 'Cris Charles Garcia', socialLinks =
|
||||
|
||||
{/* Spotify Widget */}
|
||||
<Suspense fallback={<div className="h-20 w-full rounded-2xl bg-foreground-custom/5 animate-pulse" />}>
|
||||
<SpotifyWidget />
|
||||
<SpotifyWidget isOpen={isSpotifyModalOpen} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,8 @@ export default function LagNotification() {
|
||||
setIsEnabledSuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowNotification(false);
|
||||
}, 1500);
|
||||
window.location.reload();
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
|
||||
@@ -53,9 +53,6 @@ export default function PhotoGallery({ items }: PhotoGalleryProps) {
|
||||
<h2 className="text-2xl sm:text-3xl font-bold font-heading text-foreground-custom tracking-tight mt-1">
|
||||
Photo Gallery
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground-custom mt-1">
|
||||
Visual archive of projects, technical milestones, and university events.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories in top-right */}
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function PortfolioAssistantFAB() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
role: 'assistant',
|
||||
text: "👋 Hello! I'm Cris's Portfolio Assistant.\n\nAsk me anything about Cris's projects, technical skills, certifications, work experience, or contact information."
|
||||
text: "Hello! I'm Cris's Portfolio Assistant.\n\nAsk me anything about Cris's projects, technical skills, certifications, work experience, or contact information."
|
||||
}
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
@@ -202,7 +202,7 @@ export default function PortfolioAssistantFAB() {
|
||||
if (recentSubmits.current.length >= 3) {
|
||||
haptic.tap();
|
||||
setCooldown(8); // 8-second penalty cooldown
|
||||
setSpamNotice("⚠️ Slow down! Too many rapid requests (8s pause).");
|
||||
setSpamNotice("Slow down! Too many rapid requests (8s pause).");
|
||||
setTimeout(() => setSpamNotice(null), 3500);
|
||||
return;
|
||||
}
|
||||
@@ -392,8 +392,11 @@ export default function PortfolioAssistantFAB() {
|
||||
className="p-3 border-t border-border-custom flex flex-col gap-1.5 bg-white dark:bg-[#11131c]"
|
||||
>
|
||||
{spamNotice && (
|
||||
<div className="text-[10px] font-mono font-medium text-rose-500 dark:text-rose-400 px-1 truncate flex items-center gap-1 animate-pulse">
|
||||
<span>⚠️</span> {spamNotice}
|
||||
<div className="text-[10px] font-mono font-medium text-rose-500 dark:text-rose-400 px-1 truncate flex items-center gap-1.5 animate-pulse">
|
||||
<svg className="w-3.5 h-3.5 text-rose-500 dark:text-rose-400 shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
||||
</svg>
|
||||
<span>{spamNotice}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
+62
-123
@@ -80,9 +80,6 @@ export default function Projects({
|
||||
Featured Work
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground-custom max-w-sm">
|
||||
Production systems, institutional database platforms, and full-stack solutions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Pills */}
|
||||
@@ -124,58 +121,36 @@ export default function Projects({
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
|
||||
|
||||
{/* Left: Interactive Browser Mockup Window */}
|
||||
{/* Left: Project Screenshot Viewport */}
|
||||
<div className="lg:col-span-7 flex flex-col justify-between">
|
||||
<div className="browser-frame shadow-xl group/mockup relative">
|
||||
{/* Simulated Browser Chrome Top Header with Lock & .local/dashboard */}
|
||||
<div className="browser-header">
|
||||
{/* Traffic Lights */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#ff5f56]/80 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]/80 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#27c93f]/80 inline-block" />
|
||||
</div>
|
||||
|
||||
{/* Mockup Address Bar */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-1 rounded-md bg-background-custom/80 border border-border-custom text-[11px] font-mono text-muted-foreground-custom truncate max-w-[220px] sm:max-w-[280px]">
|
||||
<svg className="w-2.5 h-2.5 text-primary-custom flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="truncate">{flagshipProject.slug}.local/dashboard</span>
|
||||
</div>
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div className="ml-auto px-2 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-[10px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5 shadow-xs">
|
||||
<div className="relative aspect-[16/10] w-full rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 shadow-lg group/mockup">
|
||||
{flagshipProject.status && (
|
||||
<div className="absolute top-3.5 right-3.5 z-20 px-2.5 py-1 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-[10px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5 shadow-sm">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="hidden sm:inline">{flagshipProject.status || 'LIVE PRODUCTION'}</span>
|
||||
<span className="sm:hidden">LIVE</span>
|
||||
<span>{flagshipProject.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Screenshot Viewport with Hover Overlay */}
|
||||
<div className="relative aspect-[16/10] w-full overflow-hidden bg-muted-custom/30">
|
||||
{flagshipProject.coverImage ? (
|
||||
<img
|
||||
src={flagshipProject.coverImage}
|
||||
alt={flagshipProject.title}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="w-full h-full object-cover group-hover/hero:scale-[1.02] transition-transform duration-500 ease-out"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-6 text-center bg-gradient-to-br from-primary-custom/10 to-transparent">
|
||||
<span className="font-mono text-xs text-muted-foreground-custom">Preview Interface</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Click to inspect overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/hero:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
|
||||
<span className="px-3.5 py-1.5 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-xs font-semibold backdrop-blur-sm shadow-md flex items-center gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 text-primary-custom" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" />
|
||||
</svg>
|
||||
Click to View Details
|
||||
</span>
|
||||
)}
|
||||
{flagshipProject.coverImage ? (
|
||||
<img
|
||||
src={flagshipProject.coverImage}
|
||||
alt={flagshipProject.title}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="w-full h-full object-cover group-hover/hero:scale-[1.02] transition-transform duration-500 ease-out"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-6 text-center bg-gradient-to-br from-primary-custom/10 to-transparent">
|
||||
<span className="font-mono text-xs text-muted-foreground-custom">Preview Interface</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Click to inspect overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/hero:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
|
||||
<span className="px-3.5 py-1.5 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-xs font-semibold backdrop-blur-sm shadow-md flex items-center gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 text-primary-custom" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" />
|
||||
</svg>
|
||||
Click to View Details
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -309,42 +284,27 @@ export default function Projects({
|
||||
className="bento-card p-6 sm:p-7 flex flex-col justify-between h-full group hover:border-primary-custom/40 transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
{/* Browser Mockup Window for Secondary Cards with Lock & .local */}
|
||||
{/* Project Screenshot Viewport for Secondary Cards */}
|
||||
{project.coverImage ? (
|
||||
<div className="browser-frame mb-5 group/submockup">
|
||||
<div className="browser-header py-1.5 px-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-[#ff5f56]/70 inline-block" />
|
||||
<span className="w-2 h-2 rounded-full bg-[#ffbd2e]/70 inline-block" />
|
||||
<span className="w-2 h-2 rounded-full bg-[#27c93f]/70 inline-block" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] font-mono text-muted-foreground-custom/80 ml-2 truncate max-w-[170px] sm:max-w-[210px]">
|
||||
<svg className="w-2 h-2 text-primary-custom flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="truncate">{project.slug}.local</span>
|
||||
</div>
|
||||
{project.status && (
|
||||
<div className="ml-auto px-1.5 py-0.5 rounded-full bg-emerald-500/10 text-[9px] font-mono text-emerald-500 dark:text-emerald-400 flex items-center gap-1">
|
||||
<span className="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" />
|
||||
{project.status}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative aspect-[16/9] w-full overflow-hidden bg-muted-custom/20">
|
||||
<img
|
||||
src={project.coverImage}
|
||||
alt={project.title}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="w-full h-full object-cover group-hover/submockup:scale-[1.03] transition-transform duration-500 ease-out"
|
||||
/>
|
||||
{/* Hover overlay hint */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
|
||||
<span className="px-3 py-1 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-[11px] font-medium backdrop-blur-sm shadow-sm">
|
||||
View Details ↗
|
||||
</span>
|
||||
<div className="relative aspect-[16/9] w-full rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 mb-5 group/submockup shadow-xs">
|
||||
{project.status && (
|
||||
<div className="absolute top-2.5 right-2.5 z-20 px-2 py-0.5 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-[9px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1 shadow-sm">
|
||||
<span className="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span>{project.status}</span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={project.coverImage}
|
||||
alt={project.title}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="w-full h-full object-cover group-hover/submockup:scale-[1.03] transition-transform duration-500 ease-out"
|
||||
/>
|
||||
{/* Hover overlay hint */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
|
||||
<span className="px-3 py-1 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-[11px] font-medium backdrop-blur-sm shadow-sm">
|
||||
View Details ↗
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -533,46 +493,25 @@ export default function Projects({
|
||||
{/* LEFT COLUMN: Screenshot Overview & Gallery */}
|
||||
<div className="w-full md:w-7/12 bg-muted-custom/25 border-b md:border-b-0 md:border-r border-border-custom p-5 sm:p-6 flex flex-col justify-between overflow-y-auto max-h-[45vh] md:max-h-[90vh]">
|
||||
<div>
|
||||
{/* Browser Frame Mockup with lock icon & .local */}
|
||||
<div className="browser-frame shadow-xl overflow-hidden mb-4">
|
||||
<div className="browser-header">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#ff5f56]/80 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]/80 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#27c93f]/80 inline-block" />
|
||||
{/* Clean Project Screenshot Frame */}
|
||||
<div className="relative aspect-[16/10] w-full rounded-2xl overflow-hidden border border-border-custom bg-black/5 mb-4 shadow-md">
|
||||
{selectedProject.status && (
|
||||
<div className="absolute top-3 right-3 z-20 px-2.5 py-1 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-[10px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5 shadow-sm">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span>{selectedProject.status}</span>
|
||||
</div>
|
||||
|
||||
{/* Address bar with lock */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-1 rounded-md bg-background-custom/80 border border-border-custom text-[11px] font-mono text-muted-foreground-custom truncate max-w-[200px] sm:max-w-[260px]">
|
||||
<svg className="w-2.5 h-2.5 text-primary-custom flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="truncate">{selectedProject.slug}.local/dashboard</span>
|
||||
)}
|
||||
{activeImage ? (
|
||||
<img
|
||||
src={activeImage}
|
||||
alt={selectedProject.title}
|
||||
className="w-full h-full object-cover transition-all duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center p-6 text-center text-muted-foreground-custom font-mono text-xs">
|
||||
No Screenshot Preview Available
|
||||
</div>
|
||||
|
||||
{/* Status Pill */}
|
||||
{selectedProject.status && (
|
||||
<div className="ml-auto px-2 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-[10px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span>{selectedProject.status}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main Large Image Viewport */}
|
||||
<div className="relative aspect-[16/10] w-full overflow-hidden bg-black/5">
|
||||
{activeImage ? (
|
||||
<img
|
||||
src={activeImage}
|
||||
alt={selectedProject.title}
|
||||
className="w-full h-full object-cover transition-all duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center p-6 text-center text-muted-foreground-custom font-mono text-xs">
|
||||
No Screenshot Preview Available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Gallery Thumbnails Carousel if multiple images exist */}
|
||||
|
||||
@@ -51,6 +51,49 @@ const getCategoryIcon = (category: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getFaClassByName = (name: string): string => {
|
||||
const n = name.toLowerCase();
|
||||
if (n.includes('react')) return 'fa-brands fa-react';
|
||||
if (n.includes('node')) return 'fa-brands fa-node-js';
|
||||
if (n.includes('php')) return 'fa-brands fa-php';
|
||||
if (n.includes('js') || n.includes('javascript')) return 'fa-brands fa-js';
|
||||
if (n.includes('ts') || n.includes('typescript')) return 'fa-brands fa-js';
|
||||
if (n.includes('html')) return 'fa-brands fa-html5';
|
||||
if (n.includes('css')) return 'fa-brands fa-css3-alt';
|
||||
if (n.includes('python')) return 'fa-brands fa-python';
|
||||
if (n.includes('flutter')) return 'fa-solid fa-mobile-screen-button';
|
||||
if (n.includes('mysql') || n.includes('sql') || n.includes('database') || n.includes('mongo')) return 'fa-solid fa-database';
|
||||
if (n.includes('github') || n.includes('git')) return 'fa-brands fa-github';
|
||||
if (n.includes('docker')) return 'fa-brands fa-docker';
|
||||
if (n.includes('secur') || n.includes('cyber') || n.includes('shield')) return 'fa-solid fa-shield-halved';
|
||||
if (n.includes('linux')) return 'fa-brands fa-linux';
|
||||
if (n.includes('java') && !n.includes('script')) return 'fa-brands fa-java';
|
||||
if (n.includes('vue')) return 'fa-brands fa-vuejs';
|
||||
if (n.includes('angular')) return 'fa-brands fa-angular';
|
||||
if (n.includes('bootstrap')) return 'fa-brands fa-bootstrap';
|
||||
if (n.includes('sass')) return 'fa-brands fa-sass';
|
||||
if (n.includes('aws')) return 'fa-brands fa-aws';
|
||||
if (n.includes('network') || n.includes('telemetry') || n.includes('cisco')) return 'fa-solid fa-network-wired';
|
||||
if (n.includes('cloud')) return 'fa-solid fa-cloud';
|
||||
if (n.includes('terminal') || n.includes('bash') || n.includes('cli')) return 'fa-solid fa-terminal';
|
||||
if (n.includes('ai') || n.includes('ml') || n.includes('bot')) return 'fa-solid fa-robot';
|
||||
return 'fa-solid fa-code';
|
||||
};
|
||||
|
||||
const getSkillFaClass = (skill: Skill): string => {
|
||||
if (skill.icon && skill.icon.trim()) {
|
||||
const raw = skill.icon.trim();
|
||||
if (raw.includes('fa-') && (raw.includes('fa-brands') || raw.includes('fa-solid') || raw.includes('fa-regular') || raw.includes('fab') || raw.includes('fas'))) {
|
||||
return raw;
|
||||
}
|
||||
if (raw.startsWith('fa-')) {
|
||||
return `fa-brands ${raw}`;
|
||||
}
|
||||
return getFaClassByName(raw);
|
||||
}
|
||||
return getFaClassByName(skill.name);
|
||||
};
|
||||
|
||||
export default function Skills({ skills, sectionTag = '03 // Tech Stack' }: SkillsProps) {
|
||||
if (!skills || skills.length === 0) {
|
||||
return null;
|
||||
@@ -98,9 +141,6 @@ export default function Skills({ skills, sectionTag = '03 // Tech Stack' }: Skil
|
||||
Technologies & Tools
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground-custom max-w-sm">
|
||||
Modular bento layout of core languages, database systems, and development toolkits.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -137,9 +177,14 @@ export default function Skills({ skills, sectionTag = '03 // Tech Stack' }: Skil
|
||||
key={skill.name}
|
||||
className="p-3 rounded-xl border border-border-custom/80 bg-foreground-custom/[0.02] hover:bg-foreground-custom/[0.05] hover:border-primary-custom/40 transition-all flex items-center justify-between group/item"
|
||||
>
|
||||
<span className="text-xs font-semibold text-foreground-custom group-hover/item:text-primary-custom transition-colors">
|
||||
{skill.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<span className="w-7 h-7 rounded-lg bg-primary-custom/10 text-primary-custom flex items-center justify-center text-sm shrink-0 group-hover/item:bg-primary-custom group-hover/item:text-white transition-all shadow-xs">
|
||||
<i className={getSkillFaClass(skill)} />
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-foreground-custom group-hover/item:text-primary-custom transition-colors truncate">
|
||||
{skill.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary-custom/40 group-hover/item:bg-primary-custom transition-colors shrink-0 ml-2" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -15,17 +15,26 @@ const defaultTrack: Track = {
|
||||
spotifyUrl: 'https://open.spotify.com'
|
||||
};
|
||||
|
||||
export default function SpotifyWidget() {
|
||||
interface SpotifyWidgetProps {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export default function SpotifyWidget({ isOpen = true }: SpotifyWidgetProps) {
|
||||
const [track, setTrack] = useState<Track>(defaultTrack);
|
||||
const [isPlaying, setIsPlaying] = useState(true);
|
||||
const [isLivePlaying, setIsLivePlaying] = useState(false);
|
||||
const [hasUserManuallyPaused, setHasUserManuallyPaused] = useState(false);
|
||||
const [progress, setProgress] = useState(45);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
let interval: NodeJS.Timeout;
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchNowPlaying = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/now-playing');
|
||||
const response = await fetch('/api/now-playing', { signal: controller.signal });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.currentPlaying) {
|
||||
@@ -35,7 +44,10 @@ export default function SpotifyWidget() {
|
||||
albumArt: data.currentPlaying.albumArt || defaultTrack.albumArt,
|
||||
spotifyUrl: data.currentPlaying.spotifyUrl || defaultTrack.spotifyUrl
|
||||
});
|
||||
setIsPlaying(Boolean(data.currentPlaying.isPlaying));
|
||||
setIsLivePlaying(true);
|
||||
if (!hasUserManuallyPaused) {
|
||||
setIsPlaying(Boolean(data.currentPlaying.isPlaying));
|
||||
}
|
||||
setProgress(data.currentPlaying.progressPercent || 50);
|
||||
return;
|
||||
} else if (data.recentlyPlayed) {
|
||||
@@ -45,24 +57,35 @@ export default function SpotifyWidget() {
|
||||
albumArt: data.recentlyPlayed.albumArt || defaultTrack.albumArt,
|
||||
spotifyUrl: data.recentlyPlayed.spotifyUrl || defaultTrack.spotifyUrl
|
||||
});
|
||||
setIsPlaying(false);
|
||||
setProgress(100);
|
||||
setIsLivePlaying(false);
|
||||
// Fix: Keep vinyl spinning for recently played tracks unless user manually paused!
|
||||
if (!hasUserManuallyPaused) {
|
||||
setIsPlaying(true);
|
||||
}
|
||||
setProgress(60);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback to default
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
// Fallback to default
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchNowPlaying();
|
||||
interval = setInterval(fetchNowPlaying, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => {
|
||||
controller.abort();
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [isOpen, hasUserManuallyPaused]);
|
||||
|
||||
const togglePlay = () => {
|
||||
haptic.tap();
|
||||
setIsPlaying(!isPlaying);
|
||||
const next = !isPlaying;
|
||||
setIsPlaying(next);
|
||||
setHasUserManuallyPaused(!next);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -192,7 +215,7 @@ export default function SpotifyWidget() {
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground-custom/60" />
|
||||
)}
|
||||
<span className="text-[10px] font-mono font-bold uppercase tracking-wider text-[#1DB954]">
|
||||
{isPlaying ? 'Now Playing' : 'Paused / Recent'}
|
||||
{isPlaying ? (isLivePlaying ? 'Now Playing' : 'Recently Played') : 'Paused'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ export default function Testimonials({ testimonials }: TestimonialsProps) {
|
||||
Client & Peer Testimonials
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground-custom">
|
||||
Recommendations, supervisor reviews & colleague feedback.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -138,10 +135,6 @@ export default function Testimonials({ testimonials }: TestimonialsProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm text-muted-foreground-custom hidden sm:block">
|
||||
{testimonials.length} verified recommendations
|
||||
</p>
|
||||
|
||||
{/* Slider Controls (Shown only if more than 3) */}
|
||||
{isCarousel && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
Reference in new issue
Block a user