8a709c6863
Five complementary improvements to handler layer: 1. Fix Pre-Commit Hook - Remove broken Perl-style regex (unsupported by Go) - Use -short flag to exclude integration tests - Tests now run successfully in pre-commit 2. Extract Duplicate Logic - Remove 100+ lines of duplicate data preparation - Both Home() and CVContent() now use prepareTemplateData() - Reduce cv_pages.go from 290 to 120 lines (58% reduction) 3. Request/Response Types - Create internal/handlers/types.go with structured types - PDFExportRequest, LanguageRequest, PreferenceToggleRequest - Type-safe parameter parsing with centralized validation - Refactor ExportPDF to use typed requests 4. Middleware Extraction - Create internal/middleware/preferences.go - PreferencesMiddleware reads cookies once, stores in context - Automatic migration of old preference values - Ready for integration in routes 5. Handler Tests - Add internal/handlers/cv_pages_test.go (190 lines, 15+ cases) - Add internal/handlers/cv_htmx_test.go (325 lines, 20+ cases) - Test language validation, toggles, cookies, methods - Increase handler test coverage significantly Testing: - All unit tests pass (35+ new test cases) - Pre-commit hook working - Build succeeds - No breaking changes Benefits: - Type safety: Compile-time parameter validation - Code quality: 170 lines of duplication eliminated - Testing: 100% increase in test files - Architecture: Clean middleware pattern - Developer experience: Self-documenting request types Documentation: - Create _go-learning/refactorings/004-handler-improvements.md - Document all five improvements with examples - Include metrics, testing strategy, and future improvements
125 lines
3.9 KiB
Go
125 lines
3.9 KiB
Go
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) {
|
|
// Parse and validate request parameters
|
|
req, err := ParsePDFExportRequest(r)
|
|
if err != nil {
|
|
HandleError(w, r, BadRequestError(err.Error()))
|
|
return
|
|
}
|
|
|
|
log.Printf("PDF export requested: lang=%s, length=%s, icons=%s, version=%s",
|
|
req.Lang, req.Length, req.Icons, req.Version)
|
|
|
|
// Load CV data to get name for filename
|
|
cv, err := cvmodel.LoadCV(req.Lang)
|
|
if err != nil {
|
|
HandleError(w, r, DataLoadError(err, "CV"))
|
|
return
|
|
}
|
|
|
|
// Prepare cookies to set preferences
|
|
cookies := map[string]string{
|
|
"cv-length": req.Length,
|
|
"cv-icons": req.Icons,
|
|
"cv-language": req.Lang,
|
|
}
|
|
|
|
// Set theme cookie based on version parameter
|
|
if req.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, req.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 req.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 req.Version == "clean" {
|
|
filename = fmt.Sprintf("cv-%s-%s-%d-%s.pdf", req.Length, initials, currentYear, req.Lang)
|
|
} else {
|
|
versionForFilename := strings.ReplaceAll(req.Version, "_", "-")
|
|
filename = fmt.Sprintf("cv-%s-%s-%s-%d-%s.pdf", req.Length, versionForFilename, initials, currentYear, req.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))
|
|
}
|