package handlers import ( "fmt" "log" "net/http" "strings" "time" cvmodel "github.com/juanatsap/cv-site/internal/models/cv" "github.com/juanatsap/cv-site/internal/pdf" ) // ============================================================================== // PDF EXPORT HANDLER // Handles PDF generation with customizable options (length, icons, version, theme) // ============================================================================== // ExportPDF handles PDF export requests using chromedp func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) { // Extract and validate query parameters lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } if lang != "en" && lang != "es" { HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'")) return } length := r.URL.Query().Get("length") if length == "" { length = "short" } if length != "short" && length != "long" { HandleError(w, r, BadRequestError("Unsupported length. Use 'short' or 'long'")) return } icons := r.URL.Query().Get("icons") if icons == "" { icons = "show" } if icons != "show" && icons != "hide" { HandleError(w, r, BadRequestError("Unsupported icons option. Use 'show' or 'hide'")) return } version := r.URL.Query().Get("version") if version == "" { version = "with_skills" } if version != "with_skills" && version != "clean" { HandleError(w, r, BadRequestError("Unsupported version. Use 'with_skills' or 'clean'")) return } log.Printf("PDF export requested: lang=%s, length=%s, icons=%s, version=%s", lang, length, icons, version) // Load CV data to get name for filename cv, err := cvmodel.LoadCV(lang) if err != nil { HandleError(w, r, DataLoadError(err, "CV")) return } // Prepare cookies to set preferences cookies := map[string]string{ "cv-length": length, "cv-icons": icons, "cv-language": lang, } // Set theme cookie based on version parameter if version == "clean" { cookies["cv-theme"] = "clean" } else { cookies["cv-theme"] = "default" } // CRITICAL: ALWAYS force light mode for PDF generation (print-friendly) // This ensures PDFs are NEVER generated in dark mode, regardless of user's preference cookies["color-theme"] = "light" // Construct URL for PDF generation (navigate to home page) targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, lang) // Determine render mode based on version parameter // Clean version: use @media print CSS (print-friendly, no sidebars) // Extended version: use @media screen CSS (full layout with sidebars) var renderMode pdf.RenderMode if version == "clean" { renderMode = pdf.RenderModePrint } else { renderMode = pdf.RenderModeScreen } // Generate PDF with cookies and appropriate render mode ctx := r.Context() pdfData, err := h.pdfGenerator.GenerateFromURLWithOptions(ctx, targetURL, cookies, renderMode) if err != nil { log.Printf("PDF generation failed: %v", err) HandleError(w, r, InternalError(err)) return } // Generate filename based on parameters // Format: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf // Note: {version} is OMITTED when it's "clean" // Examples: // - cv-short-jamr-2025-es.pdf (clean version, no skills) // - cv-short-with_skills-jamr-2025-es.pdf (with skills sidebar) // - cv-long-jamr-2025-en.pdf (clean version, no skills) // - cv-long-with_skills-jamr-2025-en.pdf (with skills sidebar) // Generate initials from name nameParts := strings.Fields(cv.Personal.Name) initials := "" for _, part := range nameParts { if len(part) > 0 { // Take first letter of each name part initials += string([]rune(part)[0]) } } initials = strings.ToLower(initials) // Get current year currentYear := time.Now().Year() // Build filename: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf // Omit version if it's "clean" // Replace underscores with hyphens in version for filename (with_skills → with-skills) var filename string if version == "clean" { filename = fmt.Sprintf("cv-%s-%s-%d-%s.pdf", length, initials, currentYear, lang) } else { versionForFilename := strings.ReplaceAll(version, "_", "-") filename = fmt.Sprintf("cv-%s-%s-%s-%d-%s.pdf", length, versionForFilename, initials, currentYear, lang) } // Set response headers w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) w.Header().Set("Content-Length", fmt.Sprintf("%d", len(pdfData))) // Write PDF data if _, err := w.Write(pdfData); err != nil { log.Printf("Error writing PDF response: %v", err) return } log.Printf("PDF generated successfully: %s (%d bytes)", filename, len(pdfData)) }