package handlers import ( "fmt" "log" "net/http" "path/filepath" "strings" "time" c "github.com/juanatsap/cv-site/internal/constants" "github.com/juanatsap/cv-site/internal/httputil" "github.com/juanatsap/cv-site/internal/middleware" "github.com/juanatsap/cv-site/internal/pdf" ) // ============================================================================== // PAGE HANDLERS // These handlers render full HTML pages or page sections // ============================================================================== // Home renders the full CV page func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) { // Check if this is a shortcut URL request (cv-jamr-{year}-{lang}.pdf) if strings.HasPrefix(r.URL.Path, "/cv-jamr-") && strings.HasSuffix(r.URL.Path, ".pdf") { h.DefaultCVShortcut(w, r) return } // Detect text-based browsers and serve plain text version if isTextBrowser(r) { h.PlainText(w, r) return } lang, ok := httputil.LangOrError(r) if !ok { HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'")) return } // Prepare template data using shared helper data, err := h.prepareTemplateData(lang) if err != nil { HandleError(w, r, DataLoadError(err, "CV")) return } // Get user preferences from context (set by PreferencesMiddleware) prefs := middleware.GetPreferences(r) // Use preferences from context (already migrated by middleware) cvLength := prefs.CVLength cvIcons := prefs.CVIcons cvTheme := prefs.CVTheme // Add preference-specific fields to template data cvLengthClass := "cv-short" if cvLength == "long" { cvLengthClass = "cv-long" } data["CVLengthClass"] = cvLengthClass data["ShowIcons"] = (cvIcons == "show") data["ThemeClean"] = (cvTheme == "clean") // Render template tmpl, err := h.templates.Render("index.html") if err != nil { HandleError(w, r, TemplateError(err, "index.html")) return } w.Header().Set(c.HeaderContentType, c.ContentTypeHTML) if err := tmpl.Execute(w, data); err != nil { HandleError(w, r, TemplateError(err, "index.html")) return } } // CVContent renders just the CV content for HTMX swaps func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) { lang, ok := httputil.LangOrError(r) if !ok { HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'")) return } // Prepare template data using shared helper data, err := h.prepareTemplateData(lang) if err != nil { HandleError(w, r, DataLoadError(err, "CV")) return } // Render template tmpl, err := h.templates.Render("cv-content.html") if err != nil { HandleError(w, r, TemplateError(err, "cv-content.html")) return } w.Header().Set(c.HeaderContentType, c.ContentTypeHTML) if err := tmpl.Execute(w, data); err != nil { HandleError(w, r, TemplateError(err, "cv-content.html")) return } } // DefaultCVShortcut handles shortcut URLs for default CV downloads // Pattern: /cv-jamr-{year}-{lang}.pdf (e.g., /cv-jamr-2025-en.pdf) // Validates year matches current year and redirects to default PDF export func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) { // Extract path (e.g., "/cv-jamr-2025-en.pdf") path := r.URL.Path log.Printf("DefaultCVShortcut called with path: %s", path) // Parse filename pattern: cv-jamr-{year}-{lang}.pdf parts := strings.Split(strings.TrimPrefix(path, "/"), "-") if len(parts) != 4 { http.NotFound(w, r) return } // Extract year and language yearStr := parts[2] langWithExt := parts[3] // "en.pdf" or "es.pdf" lang := strings.TrimSuffix(langWithExt, ".pdf") // Validate language if !c.SupportedLanguages[lang] { http.NotFound(w, r) return } // Validate year matches current year currentYear := fmt.Sprintf("%d", time.Now().Year()) if yearStr != currentYear { // Return 404 if year doesn't match (old or future URLs) log.Printf("Invalid year in shortcut URL: %s (current: %s)", yearStr, currentYear) http.NotFound(w, r) return } // Generate PDF directly instead of redirecting // This ensures the Content-Disposition filename is respected by browsers log.Printf("Shortcut URL: %s → generating PDF (short + with_skills)", path) // Prepare cookies for PDF generation (short, with_skills, light mode) cookies := map[string]string{ c.CookieCVLength: c.CVLengthShort, c.CookieCVIcons: c.CVIconsShow, c.CookieCVLanguage: lang, c.CookieCVTheme: c.CVThemeDefault, // with_skills = default theme c.CookieColorTheme: c.ColorThemeLight, // Always light for PDFs } // Construct URL for PDF generation targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, lang) // Generate PDF with screen render mode (for sidebar layout) ctx := r.Context() pdfData, err := h.pdfGenerator.GenerateFromURLWithOptions(ctx, targetURL, cookies, pdf.RenderModeScreen) if err != nil { log.Printf("PDF generation failed for shortcut URL: %v", err) HandleError(w, r, InternalError(err)) return } // Use the shortcut filename directly (simple, user-friendly) filename := filepath.Base(path) // cv-jamr-2025-en.pdf // Set response headers with shortcut filename w.Header().Set(c.HeaderContentType, c.ContentTypePDF) w.Header().Set(c.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s", filename)) w.Header().Set(c.HeaderContentLength, 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)) }