feat(cv): add simple in-page modal, fix cross-origin download, and resolve hydration warnings
This commit is contained in:
1 parent
b2a5ef1a31
commit
363fa64faf
6 files changed
+207
-36
No files matched your search
+190
-15
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import type { Education, Experience, SiteSettings } from '../lib/data';
|
import type { Education, Experience, SiteSettings } from '../lib/data';
|
||||||
import ScrollReveal from './ScrollReveal';
|
import ScrollReveal from './ScrollReveal';
|
||||||
import GithubContributions from './GithubContributions';
|
import GithubContributions from './GithubContributions';
|
||||||
@@ -29,6 +30,82 @@ export default function About({
|
|||||||
const hasExperience = experience && experience.length > 0;
|
const hasExperience = experience && experience.length > 0;
|
||||||
const hasEducation = education && education.length > 0;
|
const hasEducation = education && education.length > 0;
|
||||||
|
|
||||||
|
// CV Preview Modal & Download States
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPreviewOpen) return;
|
||||||
|
|
||||||
|
const originalOverflow = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setIsPreviewOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = originalOverflow;
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isPreviewOpen]);
|
||||||
|
|
||||||
|
// Robust File Download (supports Sanity CDN ?dl= attachment and local files)
|
||||||
|
const handleDownload = async (e?: React.MouseEvent) => {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
if (isDownloading) return;
|
||||||
|
setIsDownloading(true);
|
||||||
|
|
||||||
|
const filename = 'CV-Cris-Charles-Garcia.pdf';
|
||||||
|
try {
|
||||||
|
let targetUrl = resumeUrl;
|
||||||
|
if (targetUrl.includes('cdn.sanity.io')) {
|
||||||
|
const sep = targetUrl.includes('?') ? '&' : '?';
|
||||||
|
targetUrl = `${targetUrl}${sep}dl=${encodeURIComponent(filename)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(targetUrl, { mode: 'cors' });
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = blobUrl;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
setTimeout(() => window.URL.revokeObjectURL(blobUrl), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Direct blob download failed, falling back to direct anchor:', err);
|
||||||
|
let fallbackUrl = resumeUrl;
|
||||||
|
if (fallbackUrl.includes('cdn.sanity.io')) {
|
||||||
|
const sep = fallbackUrl.includes('?') ? '&' : '?';
|
||||||
|
fallbackUrl = `${fallbackUrl}${sep}dl=${encodeURIComponent(filename)}`;
|
||||||
|
}
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = fallbackUrl;
|
||||||
|
link.download = filename;
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="about" className="py-12 sm:py-16 relative">
|
<section id="about" className="py-12 sm:py-16 relative">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
|
||||||
@@ -161,29 +238,43 @@ export default function About({
|
|||||||
Curriculum Vitae
|
Curriculum Vitae
|
||||||
</span>
|
</span>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<a
|
<button
|
||||||
href={resumeUrl}
|
type="button"
|
||||||
target="_blank"
|
onClick={() => setIsPreviewOpen(true)}
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 rounded-xl bg-primary-custom text-white text-xs font-medium hover:bg-primary-custom/90 transition-all cursor-pointer text-center shadow-xs"
|
className="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 rounded-xl bg-primary-custom text-white text-xs font-medium hover:bg-primary-custom/90 transition-all cursor-pointer text-center shadow-xs"
|
||||||
|
title="Preview CV"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
</svg>
|
</svg>
|
||||||
View CV ↗
|
View CV ↗
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
href={resumeUrl}
|
type="button"
|
||||||
download="CV-Cris-Charles-Garcia.pdf"
|
onClick={handleDownload}
|
||||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 rounded-xl border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-medium hover:bg-primary-custom/5 transition-all cursor-pointer text-center"
|
disabled={isDownloading}
|
||||||
|
className="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 rounded-xl border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-medium hover:bg-primary-custom/5 transition-all cursor-pointer text-center disabled:opacity-60"
|
||||||
|
title="Download CV file"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
{isDownloading ? (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
<>
|
||||||
</svg>
|
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
Download
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
</a>
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
||||||
|
</svg>
|
||||||
|
Downloading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
Download
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,6 +419,90 @@ export default function About({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Simple CV Preview Modal */}
|
||||||
|
{mounted && isPreviewOpen && createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center p-3 sm:p-5 bg-black/70 backdrop-blur-sm transition-all"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Curriculum Vitae Preview"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-4xl h-[88vh] max-h-[900px] flex flex-col rounded-2xl bg-card-custom border border-border-custom shadow-2xl overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className="px-4 sm:px-6 py-3.5 border-b border-border-custom flex items-center justify-between gap-3 bg-card-custom shrink-0">
|
||||||
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
|
<span className="p-1.5 rounded-lg bg-primary-custom/10 text-primary-custom shrink-0">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" 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.25m2.25 0H5.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>
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-sm font-bold font-heading text-foreground-custom truncate">
|
||||||
|
Curriculum Vitae
|
||||||
|
</h3>
|
||||||
|
<p className="text-[11px] font-mono text-muted-foreground-custom truncate">
|
||||||
|
{settings.fullName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions: Download & Close */}
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={isDownloading}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-primary-custom text-white text-xs font-medium hover:bg-primary-custom/90 transition-all cursor-pointer shadow-xs disabled:opacity-60"
|
||||||
|
title="Download CV"
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:inline">Downloading...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
<span>Download</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
aria-label="Close modal"
|
||||||
|
className="p-1.5 rounded-xl border border-border-custom hover:border-primary-custom/40 text-muted-foreground-custom hover:text-foreground-custom transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body: PDF Preview */}
|
||||||
|
<div className="relative flex-1 w-full h-full bg-zinc-900 overflow-hidden">
|
||||||
|
<iframe
|
||||||
|
src={`${resumeUrl}#toolbar=1&navpanes=0`}
|
||||||
|
title="Curriculum Vitae Document"
|
||||||
|
className="w-full h-full border-0 bg-zinc-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ export default function LocalTimeWeather({
|
|||||||
<span className="font-mono text-muted-foreground-custom">Local Time</span>
|
<span className="font-mono text-muted-foreground-custom">Local Time</span>
|
||||||
<div className="flex items-center gap-1.5 font-mono text-foreground-custom">
|
<div className="flex items-center gap-1.5 font-mono text-foreground-custom">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
<span className="font-semibold text-xs">{dateTime.time}</span>
|
<span className="font-semibold text-xs" suppressHydrationWarning>{dateTime.time}</span>
|
||||||
<span className="text-[10px] text-muted-foreground-custom font-medium">PHT</span>
|
<span className="text-[10px] text-muted-foreground-custom font-medium">PHT</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +192,7 @@ export default function LocalTimeWeather({
|
|||||||
{/* Date */}
|
{/* Date */}
|
||||||
<div className="flex justify-between items-center py-1 border-b border-border-custom/50">
|
<div className="flex justify-between items-center py-1 border-b border-border-custom/50">
|
||||||
<span className="font-mono text-muted-foreground-custom">Date</span>
|
<span className="font-mono text-muted-foreground-custom">Date</span>
|
||||||
<span className="font-mono text-foreground-custom text-xs font-medium">
|
<span className="font-mono text-foreground-custom text-xs font-medium" suppressHydrationWarning>
|
||||||
{dateTime.date}
|
{dateTime.date}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,12 +23,8 @@ export default function ScrollReveal({
|
|||||||
variant = 'fade-up',
|
variant = 'fade-up',
|
||||||
delay = 0
|
delay = 0
|
||||||
}: ScrollRevealProps) {
|
}: ScrollRevealProps) {
|
||||||
const [isLowTier, setIsLowTier] = useState(() => {
|
const isMobile = useIsMobile();
|
||||||
if (typeof document !== 'undefined') {
|
const [isLowTier, setIsLowTier] = useState(false);
|
||||||
return document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkTier = () => {
|
const checkTier = () => {
|
||||||
@@ -52,7 +48,6 @@ export default function ScrollReveal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chosenVariant = variantsMap[variant] || variantsMap['fade-up'];
|
const chosenVariant = variantsMap[variant] || variantsMap['fade-up'];
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -7,14 +7,8 @@ export default function SpotlightLens() {
|
|||||||
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
|
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
|
||||||
const [scroll, setScroll] = useState({ x: 0, y: 0 });
|
const [scroll, setScroll] = useState({ x: 0, y: 0 });
|
||||||
const [magnifierPos, setMagnifierPos] = useState({ clientX: -100, clientY: -100 });
|
const [magnifierPos, setMagnifierPos] = useState({ clientX: -100, clientY: -100 });
|
||||||
const [isDesktop, setIsDesktop] = useState(() => {
|
const [mounted, setMounted] = useState(false);
|
||||||
if (typeof window !== 'undefined') {
|
const [isDesktop, setIsDesktop] = useState(false);
|
||||||
const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
|
||||||
const isLow = document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true';
|
|
||||||
return fine && !isLow;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const [magnifierEnabled, setMagnifierEnabled] = useState(false);
|
const [magnifierEnabled, setMagnifierEnabled] = useState(false);
|
||||||
|
|
||||||
// Direct DOM ref for 120 FPS cursor (0 React re-renders on mouse movement)
|
// Direct DOM ref for 120 FPS cursor (0 React re-renders on mouse movement)
|
||||||
@@ -29,6 +23,7 @@ export default function SpotlightLens() {
|
|||||||
const rafId = useRef<number | null>(null);
|
const rafId = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
const checkDesktop = () => {
|
const checkDesktop = () => {
|
||||||
const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||||
const isLow = document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true';
|
const isLow = document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true';
|
||||||
@@ -215,7 +210,7 @@ export default function SpotlightLens() {
|
|||||||
const pageX = magnifierPos.clientX + scroll.x;
|
const pageX = magnifierPos.clientX + scroll.x;
|
||||||
const pageY = magnifierPos.clientY + scroll.y;
|
const pageY = magnifierPos.clientY + scroll.y;
|
||||||
|
|
||||||
if (!isDesktop) return null;
|
if (!mounted || !isDesktop) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ const links = allLinks.length > 0
|
|||||||
{ platform: 'Facebook', url: 'https://facebook.com', icon: 'facebook', order: 5 },
|
{ platform: 'Facebook', url: 'https://facebook.com', icon: 'facebook', order: 5 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const resumeUrl = settings.resumeUrl || '/CV-Cris-Charles-Garcia.pdf';
|
const resumeUrl = settings.resumeFile && settings.resumeFile !== '#' ? settings.resumeFile : '/CV-Cris-Charles-Garcia.pdf';
|
||||||
|
const downloadUrl = resumeUrl.includes('cdn.sanity.io')
|
||||||
|
? `${resumeUrl}${resumeUrl.includes('?') ? '&' : '?'}dl=CV-Cris-Charles-Garcia.pdf`
|
||||||
|
: resumeUrl;
|
||||||
const profileAvatar = settings.heroImage || settings.logoImage || '/logo.png';
|
const profileAvatar = settings.heroImage || settings.logoImage || '/logo.png';
|
||||||
|
|
||||||
const formatDisplayUrl = (url: string) => {
|
const formatDisplayUrl = (url: string) => {
|
||||||
@@ -124,7 +127,7 @@ const getIconSvg = (platform: string) => {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={resumeUrl}
|
href={downloadUrl}
|
||||||
download="CV-Cris-Charles-Garcia.pdf"
|
download="CV-Cris-Charles-Garcia.pdf"
|
||||||
class="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-xl border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-medium hover:bg-primary-custom/5 transition-all text-center"
|
class="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-xl border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-medium hover:bg-primary-custom/5 transition-all text-center"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { getSiteSettings, getSocialLinks } from '../lib/data';
|
|||||||
const settings = await getSiteSettings();
|
const settings = await getSiteSettings();
|
||||||
const socialLinks = await getSocialLinks();
|
const socialLinks = await getSocialLinks();
|
||||||
const resumeUrl = settings.resumeFile && settings.resumeFile !== '#' ? settings.resumeFile : '/CV-Cris-Charles-Garcia.pdf';
|
const resumeUrl = settings.resumeFile && settings.resumeFile !== '#' ? settings.resumeFile : '/CV-Cris-Charles-Garcia.pdf';
|
||||||
|
const downloadUrl = resumeUrl.includes('cdn.sanity.io')
|
||||||
|
? `${resumeUrl}${resumeUrl.includes('?') ? '&' : '?'}dl=CV-Cris-Charles-Garcia.pdf`
|
||||||
|
: resumeUrl;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`Under Development | ${settings.fullName}`} description="Cris Charles Garcia — Portfolio currently under development. Please check back later.">
|
<Layout title={`Under Development | ${settings.fullName}`} description="Cris Charles Garcia — Portfolio currently under development. Please check back later.">
|
||||||
@@ -87,7 +90,7 @@ const resumeUrl = settings.resumeFile && settings.resumeFile !== '#' ? settings.
|
|||||||
View CV
|
View CV
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={resumeUrl}
|
href={downloadUrl}
|
||||||
download="CV-Cris-Charles-Garcia.pdf"
|
download="CV-Cris-Charles-Garcia.pdf"
|
||||||
class="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-xl border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-medium hover:bg-primary-custom/5 transition-all text-center"
|
class="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-xl border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-medium hover:bg-primary-custom/5 transition-all text-center"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in new issue
Block a user