refactor: Split monolithic handler into focused files
Split internal/handlers/cv.go (1,001 lines) into 5 focused files: Structure: - cv.go (29 lines) - CVHandler struct + constructor - cv_pages.go (290 lines) - Page handlers (Home, CVContent, DefaultCVShortcut) - cv_pdf.go (153 lines) - PDF export handler (ExportPDF) - cv_htmx.go (218 lines) - HTMX toggle handlers (Length, Icons, Language, Theme) - cv_helpers.go (385 lines) - Helper functions (skills, dates, git, templates, cookies) Benefits: - Single Responsibility: Each file has one clear purpose - Improved Discoverability: Easy to find specific functionality - Reduced Cognitive Load: 200-400 lines per file vs 1,001 - Parallel Development: No conflicts when editing different concerns - Better Organization: Clear section markers and grouping - Maintainability: Trade +74 lines (+7.4%) for better organization Testing: - All Go tests pass (fileutil, handlers, lang, cv, ui) - Server builds and runs correctly - All HTTP endpoints functional - No breaking changes Documentation: - Create _go-learning/refactorings/003-handler-split.md - Document architecture, benefits, and trade-offs - Explain WHY single package vs separate packages
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user