Files
cv-site/internal/handlers/cv.go
T

210 lines
5.4 KiB
Go
Raw Normal View History

2025-10-20 08:54:21 +01:00
package handlers
import (
"fmt"
"log"
2025-10-20 08:54:21 +01:00
"net/http"
"time"
2025-10-20 08:54:21 +01:00
"github.com/juanatsap/cv-site/internal/models"
"github.com/juanatsap/cv-site/internal/pdf"
2025-10-20 08:54:21 +01:00
"github.com/juanatsap/cv-site/internal/templates"
)
// CVHandler handles CV-related requests
type CVHandler struct {
templates *templates.Manager
pdfGenerator *pdf.Generator
serverAddr string
2025-10-20 08:54:21 +01:00
}
// NewCVHandler creates a new CV handler
func NewCVHandler(tmpl *templates.Manager, serverAddr string) *CVHandler {
2025-10-20 08:54:21 +01:00
return &CVHandler{
templates: tmpl,
pdfGenerator: pdf.NewGenerator(30 * time.Second),
serverAddr: serverAddr,
2025-10-20 08:54:21 +01:00
}
}
// Home renders the full CV page
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Get language from query parameter, default to English
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
// Validate language
if lang != "en" && lang != "es" {
HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'"))
return
}
// Load CV data
cv, err := models.LoadCV(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
2025-10-20 08:54:21 +01:00
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
2025-10-20 08:54:21 +01:00
}
// Render template
tmpl, err := h.templates.Render("index.html")
if err != nil {
HandleError(w, r, TemplateError(err, "index.html"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
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) {
// Get language from query parameter
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
// Validate language
if lang != "en" && lang != "es" {
HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'"))
return
}
// Load CV data
cv, err := models.LoadCV(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
2025-10-20 08:54:21 +01:00
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
2025-10-20 08:54:21 +01:00
}
// 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("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
}
// ExportPDF handles PDF export requests using chromedp
2025-10-20 08:54:21 +01:00
func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) {
// Get language from query parameter
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
// Validate language
if lang != "en" && lang != "es" {
HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'"))
return
}
// Construct URL to generate PDF from
// Use localhost instead of the actual server address to avoid network overhead
url := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, lang)
log.Printf("Generating PDF from URL: %s", url)
// Generate PDF
pdfData, err := h.pdfGenerator.GenerateFromURL(r.Context(), url)
if err != nil {
log.Printf("PDF generation failed: %v", err)
HandleError(w, r, InternalError(err))
return
}
// Set response headers
filename := fmt.Sprintf("CV-Juan-Andres-Moreno-Rubio-%s.pdf", lang)
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("Failed to write PDF response: %v", err)
return
}
log.Printf("Successfully generated PDF: %s (%d bytes)", filename, len(pdfData))
}
// splitSkills splits skill categories between left (page 1) and right (page 2) sidebars
// The split is done at the midpoint to evenly distribute skills
func splitSkills(skills []models.SkillCategory) (left, right []models.SkillCategory) {
if len(skills) == 0 {
return nil, nil
}
// Calculate midpoint
mid := len(skills) / 2
// Split at midpoint
left = skills[:mid]
right = skills[mid:]
return left, right
2025-10-20 08:54:21 +01:00
}
// calculateYearsOfExperience calculates years of experience since April 1, 2005
// This matches the original React implementation that calculated from 01/04/2005
func calculateYearsOfExperience() int {
// First day at work: April 1, 2005
firstDay := time.Date(2005, time.April, 1, 9, 0, 0, 0, time.UTC)
// Current date
now := time.Now()
// Calculate the difference in years
years := now.Year() - firstDay.Year()
// Adjust if we haven't reached the anniversary this year yet
if now.Month() < firstDay.Month() ||
(now.Month() == firstDay.Month() && now.Day() < firstDay.Day()) {
years--
}
return years
}