Files
cv-site/templates/partials/modals/pdf-modal.html
T
juanatsap 531433f54c fix: Remove toast notification from PDF download flow
Simplified PDF download UX to use only the modal loading overlay,
removing the redundant toast notification that appeared when the modal
was closed during download. Updated tests to reflect the new behavior.

Changes:
- Removed toast trigger logic from PDF modal download function
- Removed modal close event listener for toast display
- Updated toast notification test expectations
- Fixed recommended card outline styling
2025-11-20 14:35:22 +00:00

368 lines
17 KiB
HTML

{{define "pdf-modal"}}
<!-- PDF Download Modal - Interactive Thumbnails -->
<dialog id="pdf-modal" class="info-modal pdf-download-modal no-print"
_="on click
if event.target is me
call me.close()
end">
<div class="info-modal-content" id="pdf-modal-content">
<!-- Loading Overlay -->
<div class="pdf-loading-overlay" id="pdf-loading-overlay">
<div class="pdf-loading-content">
<div class="pdf-loading-spinner"></div>
<h3 class="pdf-loading-title" id="pdf-loading-title">
{{if eq .Lang "es"}}Preparando PDF...{{else}}Preparing PDF...{{end}}
</h3>
<p class="pdf-loading-message" id="pdf-loading-message">
{{if eq .Lang "es"}}Por favor espera mientras generamos tu CV{{else}}Please wait while we generate your CV{{end}}
</p>
<p class="pdf-loading-estimate" id="pdf-loading-estimate"></p>
</div>
</div>
<!-- Close Button -->
<button class="info-modal-close"
onclick="document.getElementById('pdf-modal').close()"
aria-label="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}">
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
</button>
<!-- Header -->
<div class="info-modal-header">
<iconify-icon icon="catppuccin:pdf" width="40" height="40" style="margin-bottom: 0.5rem;"></iconify-icon>
<h2>{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}</h2>
<p class="pdf-modal-subtitle">
{{if eq .Lang "es"}}Elige tu formato preferido{{else}}Choose your preferred format{{end}}
</p>
</div>
<!-- Body: Three Thumbnail Cards -->
<div class="pdf-options-grid">
<!-- Short CV Card -->
<div class="pdf-option-card"
data-cv-format="short"
role="radio"
aria-checked="false"
aria-label="{{if eq .Lang "es"}}CV Corto - 4 páginas, información esencial{{else}}Short CV - 4 pages, essential information{{end}}"
tabindex="0"
_="on click
-- Remove selected from all cards
set cards to .pdf-option-card in #pdf-modal
for card in cards
remove .selected from card
set card's @aria-checked to 'false'
end
-- Add selected to this card
add .selected to me
set my @aria-checked to 'true'
-- Enable download button
set downloadBtn to .pdf-download-btn in #pdf-modal
remove @disabled from downloadBtn
-- Store selected format
set :selectedFormat to my @data-cv-format
-- Announce to screen readers
set announcement to #pdf-selection-announcement
if :selectedFormat is 'short'
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: CV Corto - Una página{{else}}Selected: Short CV - One page{{end}}'
end
end
on keydown
if event.key is 'Enter' or event.key is ' '
halt the event
trigger click on me
end
end">
<div class="pdf-thumbnail thumbnail-short">
<!-- Header representation -->
<div class="skeleton-block" style="height: 48px;"></div>
<!-- Content sections (compact) -->
<div class="skeleton-block" style="height: 60px;"></div>
<div class="skeleton-block" style="height: 60px;"></div>
<div class="skeleton-block" style="height: 60px;"></div>
<!-- Page count badge -->
<div class="thumbnail-badge">
{{if eq .Lang "es"}}4 Páginas{{else}}4 Pages{{end}}
</div>
</div>
<div class="pdf-option-info">
<h3>{{if eq .Lang "es"}}CV Corto (4 páginas){{else}}Short CV (4 pages){{end}}</h3>
<p>{{if eq .Lang "es"}}Información esencial{{else}}Essential info{{end}}</p>
</div>
<div class="pdf-option-badge">
<iconify-icon icon="mdi:check-circle" width="32" height="32"></iconify-icon>
</div>
</div>
<!-- Default CV Card (Recommended) -->
<div class="pdf-option-card pdf-option-recommended selected"
data-cv-format="default"
role="radio"
aria-checked="true"
aria-label="{{if eq .Lang "es"}}CV Por Defecto - 5 páginas con habilidades (Recomendado){{else}}Default CV - 5 pages with skills (Recommended){{end}}"
tabindex="0"
_="on click
-- Remove selected from all cards
set cards to .pdf-option-card in #pdf-modal
for card in cards
remove .selected from card
set card's @aria-checked to 'false'
end
-- Add selected to this card
add .selected to me
set my @aria-checked to 'true'
-- Enable download button
set downloadBtn to .pdf-download-btn in #pdf-modal
remove @disabled from downloadBtn
-- Store selected format
set :selectedFormat to my @data-cv-format
-- Announce to screen readers
set announcement to #pdf-selection-announcement
if :selectedFormat is 'default'
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: CV Por Defecto (Recomendado){{else}}Selected: Default CV (Recommended){{end}}'
end
end
on keydown
if event.key is 'Enter' or event.key is ' '
halt the event
trigger click on me
end
end">
<div class="pdf-thumbnail thumbnail-default">
<!-- Two-column layout with sidebar -->
<div class="skeleton-block" style="height: 36px; margin-bottom: 6px;"></div>
<div style="display: flex; gap: 4px;">
<div style="width: 25%; display: flex; flex-direction: column; gap: 3px;">
<div class="skeleton-block" style="height: 20px;"></div>
<div class="skeleton-block" style="height: 16px;"></div>
</div>
<div style="width: 75%; display: flex; flex-direction: column; gap: 3px;">
<div class="skeleton-block" style="height: 32px;"></div>
<div class="skeleton-block" style="height: 32px;"></div>
</div>
</div>
<!-- Page count badge with star -->
<div class="thumbnail-badge" style="font-weight: 600;">
⭐ {{if eq .Lang "es"}}5 Páginas{{else}}5 Pages{{end}}
</div>
</div>
<div class="pdf-option-info">
<h3>
{{if eq .Lang "es"}}CV Por Defecto (5 páginas){{else}}Default CV (5 pages){{end}}
<span style="color: #667eea; font-size: 0.9em;"></span>
</h3>
<p style="font-weight: 500;">{{if eq .Lang "es"}}Corto con habilidades - Recomendado{{else}}Short with skills - Recommended{{end}}</p>
</div>
<div class="pdf-option-badge">
<iconify-icon icon="mdi:check-circle" width="32" height="32"></iconify-icon>
</div>
</div>
<!-- Extended CV Card -->
<div class="pdf-option-card"
data-cv-format="long"
role="radio"
aria-checked="false"
aria-label="{{if eq .Lang "es"}}CV Extendido - 9 páginas, versión completa{{else}}Extended CV - 9 pages, full version{{end}}"
tabindex="0"
_="on click
-- Remove selected from all cards
set cards to .pdf-option-card in #pdf-modal
for card in cards
remove .selected from card
set card's @aria-checked to 'false'
end
-- Add selected to this card
add .selected to me
set my @aria-checked to 'true'
-- Enable download button
set downloadBtn to .pdf-download-btn in #pdf-modal
remove @disabled from downloadBtn
-- Store selected format
set :selectedFormat to my @data-cv-format
-- Announce to screen readers
set announcement to #pdf-selection-announcement
if :selectedFormat is 'long'
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: CV Completo - Versión completa{{else}}Selected: Long CV - Full version{{end}}'
end
end
on keydown
if event.key is 'Enter' or event.key is ' '
halt the event
trigger click on me
end
end">
<div class="pdf-thumbnail thumbnail-long">
<!-- Header representation -->
<div class="skeleton-block" style="height: 48px;"></div>
<!-- More content sections (detailed) -->
<div class="skeleton-block" style="height: 40px;"></div>
<div class="skeleton-block" style="height: 40px;"></div>
<div class="skeleton-block" style="height: 40px;"></div>
<div class="skeleton-block" style="height: 40px;"></div>
<div class="skeleton-block" style="height: 40px;"></div>
<!-- Page count badge -->
<div class="thumbnail-badge">
{{if eq .Lang "es"}}9 Páginas{{else}}9 Pages{{end}}
</div>
</div>
<div class="pdf-option-info">
<h3>{{if eq .Lang "es"}}CV Extendido (9 páginas){{else}}Extended CV (9 pages){{end}}</h3>
<p>{{if eq .Lang "es"}}Todos los detalles{{else}}All details{{end}}</p>
</div>
<div class="pdf-option-badge">
<iconify-icon icon="mdi:check-circle" width="32" height="32"></iconify-icon>
</div>
</div>
</div>
<!-- Footer: Download Button -->
<div class="pdf-modal-footer">
<button class="pdf-download-btn"
id="pdf-download-btn"
onclick="downloadPDF()">
<iconify-icon icon="mdi:download" width="20" height="20"></iconify-icon>
{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
</button>
</div>
<script>
// Reset loading state when modal opens to prevent stuck blur effect
(function() {
const modal = document.getElementById('pdf-modal');
const modalContent = document.getElementById('pdf-modal-content');
const overlay = document.getElementById('pdf-loading-overlay');
// Clean up loading state when modal closes
modal.addEventListener('close', function() {
overlay.classList.remove('active');
modalContent.classList.remove('loading-active');
});
// Also clean up when modal opens (in case of stuck state)
// Use MutationObserver to detect when modal becomes visible
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'open') {
if (modal.hasAttribute('open')) {
// Modal just opened - ensure clean state
overlay.classList.remove('active');
modalContent.classList.remove('loading-active');
}
}
});
});
observer.observe(modal, { attributes: true });
})();
function downloadPDF() {
const selectedCard = document.querySelector('#pdf-modal .pdf-option-card.selected');
if (!selectedCard) {
console.error('No card selected');
return;
}
const selectedFormat = selectedCard.getAttribute('data-cv-format');
const lang = '{{.Lang}}';
const isSpanish = lang === 'es';
let url;
let estimatedTime = 4; // Default: 4 seconds
let formatName = '';
console.log('Download requested for format:', selectedFormat);
// Build URL and set estimated time based on format
if (selectedFormat === 'short') {
// Short CV: clean version (no skills), short length
url = `/export/pdf?lang=${lang}&length=short&icons=show&version=clean`;
estimatedTime = 3;
formatName = isSpanish ? 'CV Corto (4 páginas)' : 'Short CV (4 pages)';
} else if (selectedFormat === 'default') {
// Default CV: use shortcut URL (short with skills, 5 pages)
const currentYear = new Date().getFullYear();
url = `/cv-jamr-${currentYear}-${lang}.pdf`;
estimatedTime = 4;
formatName = isSpanish ? 'CV Por Defecto (5 páginas)' : 'Default CV (5 pages)';
} else if (selectedFormat === 'long') {
// Long CV: with skills sidebar, long length
url = `/export/pdf?lang=${lang}&length=long&icons=show&version=with_skills`;
estimatedTime = 8;
formatName = isSpanish ? 'CV Extendido (9 páginas)' : 'Extended CV (9 pages)';
} else if (selectedFormat === 'current') {
// Current view: use localStorage settings
let currentLength = localStorage.getItem('cv-length') || 'short';
const currentIcons = localStorage.getItem('cv-icons') || 'show';
const currentTheme = localStorage.getItem('cv-theme') || 'default';
const version = currentTheme === 'clean' ? 'clean' : 'with_skills';
url = `/export/pdf?lang=${lang}&length=${currentLength}&icons=${currentIcons}&version=${version}`;
estimatedTime = currentLength === 'long' ? 8 : 4;
formatName = isSpanish ? 'CV Actual' : 'Current CV';
}
// Show loading overlay in modal
const overlay = document.getElementById('pdf-loading-overlay');
const modalContent = document.getElementById('pdf-modal-content');
const estimateEl = document.getElementById('pdf-loading-estimate');
const modal = document.getElementById('pdf-modal');
overlay.classList.add('active');
modalContent.classList.add('loading-active');
// Update estimate message
const estimateMsg = isSpanish
? `Generando ${formatName}... Esto puede tardar ~${estimatedTime} segundos`
: `Generating ${formatName}... This may take ~${estimatedTime} seconds`;
estimateEl.textContent = estimateMsg;
console.log('Navigating to:', url);
// Trigger download
window.location.href = url;
// After estimated time: remove loading state and close modal
setTimeout(() => {
overlay.classList.remove('active');
modalContent.classList.remove('loading-active');
// Close modal if still open
modal.close();
}, estimatedTime * 1000);
}
</script>
<!-- Screen Reader Announcement Area -->
<div id="pdf-selection-announcement" class="sr-only" aria-live="polite" aria-atomic="true"></div>
</div>
</dialog>
{{end}}