e2c4eafda2
**Page 1 Changes:** - Left sidebar with first half of skills categories - Main content: Personal info, Education, Skills summary, Experience - No footer on page 1 **Page 2 Changes:** - Main content: Awards, Courses, Languages, References, Other - Right sidebar with second half of skills categories - Footer with address, phone, email (dark gray #303030 background) - Same header as page 1 **Technical Implementation:** - Split skills between left/right sidebars using midpoint calculation in Go handler - CSS Grid layout: Page 1 (left sidebar + main), Page 2 (main + right sidebar) - Footer styled to match React CV exactly (centered, horizontal layout) - Print CSS with proper page breaks for A4 layout - Mobile responsive: stacks to single column, hides page 2 header - All links in References section are clickable **Data Model:** - Moved Languages, Courses, References, Other from sidebar to page 2 main content - Skills split evenly between SkillsLeft and SkillsRight template variables Matches pixel-perfect design from original React CV screenshot.
181 lines
4.5 KiB
Go
181 lines
4.5 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)
|
|
|
|
// Prepare template data
|
|
data := map[string]interface{}{
|
|
"CV": cv,
|
|
"Lang": lang,
|
|
"SkillsLeft": skillsLeft,
|
|
"SkillsRight": skillsRight,
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Prepare template data
|
|
data := map[string]interface{}{
|
|
"CV": cv,
|
|
"Lang": lang,
|
|
"SkillsLeft": skillsLeft,
|
|
"SkillsRight": skillsRight,
|
|
}
|
|
|
|
// 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
|
|
}
|