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

325 lines
8.0 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
}
// Calculate duration for each experience
for i := range cv.Experience {
cv.Experience[i].Duration = calculateDuration(
cv.Experience[i].StartDate,
cv.Experience[i].EndDate,
cv.Experience[i].Current,
lang,
)
}
// Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Get current year
currentYear := time.Now().Year()
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,
"CurrentYear": currentYear,
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
}
// Calculate duration for each experience
for i := range cv.Experience {
cv.Experience[i].Duration = calculateDuration(
cv.Experience[i].StartDate,
cv.Experience[i].EndDate,
cv.Experience[i].Current,
lang,
)
}
// Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Get current year
currentYear := time.Now().Year()
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,
"CurrentYear": currentYear,
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
}
// calculateDuration calculates the duration between two dates in years and months
// Date format expected: "YYYY-MM" (e.g., "2021-01")
// Returns a formatted string like "3 years 6 months" or "6 months"
func calculateDuration(startDate, endDate string, current bool, lang string) string {
// Parse start date
start, err := time.Parse("2006-01", startDate)
if err != nil {
return ""
}
// Determine end date
var end time.Time
if current {
end = time.Now()
} else {
end, err = time.Parse("2006-01", endDate)
if err != nil {
return ""
}
}
// Calculate total months
totalMonths := (end.Year()-start.Year())*12 + int(end.Month()-start.Month())
// If end date is before start date, return empty
if totalMonths < 0 {
return ""
}
years := totalMonths / 12
months := totalMonths % 12
// Format the duration string based on language
var result string
if lang == "es" {
if years > 0 && months > 0 {
yearStr := "años"
if years == 1 {
yearStr = "año"
}
monthStr := "meses"
if months == 1 {
monthStr = "mes"
}
result = fmt.Sprintf("(%d %s %d %s)", years, yearStr, months, monthStr)
} else if years > 0 {
yearStr := "años"
if years == 1 {
yearStr = "año"
}
result = fmt.Sprintf("(%d %s)", years, yearStr)
} else {
monthStr := "meses"
if months == 1 {
monthStr = "mes"
}
result = fmt.Sprintf("(%d %s)", months, monthStr)
}
} else {
if years > 0 && months > 0 {
yearStr := "years"
if years == 1 {
yearStr = "year"
}
monthStr := "months"
if months == 1 {
monthStr = "month"
}
result = fmt.Sprintf("(%d %s %d %s)", years, yearStr, months, monthStr)
} else if years > 0 {
yearStr := "years"
if years == 1 {
yearStr = "year"
}
result = fmt.Sprintf("(%d %s)", years, yearStr)
} else {
monthStr := "months"
if months == 1 {
monthStr = "month"
}
result = fmt.Sprintf("(%d %s)", months, monthStr)
}
}
return result
}