51597c074b
- Add dynamic years of experience calculation from April 1, 2005 - Update professional title badges: ANALYST | TECHNICAL CONSULTANT - Add all missing skill categories from React CV (Programming Languages, JavaScript Frameworks, PHP Frameworks, Java Frameworks, Application Servers, CMS, Design Tools, Team Management) - Add complete References section with LinkedIn, Behance, portfolios, and recommendation letters - Refine years-of-experience subtitle styling to match original design - Achieve 100% content parity with old React CV (English: 7→14 skill categories)
210 lines
5.4 KiB
Go
210 lines
5.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/juanatsap/cv-site/internal/models"
|
|
"github.com/juanatsap/cv-site/internal/pdf"
|
|
"github.com/juanatsap/cv-site/internal/templates"
|
|
)
|
|
|
|
// CVHandler handles CV-related requests
|
|
type CVHandler struct {
|
|
templates *templates.Manager
|
|
pdfGenerator *pdf.Generator
|
|
serverAddr string
|
|
}
|
|
|
|
// NewCVHandler creates a new CV handler
|
|
func NewCVHandler(tmpl *templates.Manager, serverAddr string) *CVHandler {
|
|
return &CVHandler{
|
|
templates: tmpl,
|
|
pdfGenerator: pdf.NewGenerator(30 * time.Second),
|
|
serverAddr: serverAddr,
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Prepare template data
|
|
data := map[string]interface{}{
|
|
"CV": cv,
|
|
"Lang": lang,
|
|
"SkillsLeft": skillsLeft,
|
|
"SkillsRight": skillsRight,
|
|
"YearsOfExperience": yearsOfExperience,
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Prepare template data
|
|
data := map[string]interface{}{
|
|
"CV": cv,
|
|
"Lang": lang,
|
|
"SkillsLeft": skillsLeft,
|
|
"SkillsRight": skillsRight,
|
|
"YearsOfExperience": yearsOfExperience,
|
|
}
|
|
|
|
// 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
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|