feat: implement proper two-page CV layout matching original React design

**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.
This commit is contained in:
juanatsap
2025-11-04 19:07:34 +00:00
parent 7003694e4d
commit e2c4eafda2
3 changed files with 585 additions and 152 deletions
+76 -13
View File
@@ -1,21 +1,29 @@
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
templates *templates.Manager
pdfGenerator *pdf.Generator
serverAddr string
}
// NewCVHandler creates a new CV handler
func NewCVHandler(tmpl *templates.Manager) *CVHandler {
func NewCVHandler(tmpl *templates.Manager, serverAddr string) *CVHandler {
return &CVHandler{
templates: tmpl,
templates: tmpl,
pdfGenerator: pdf.NewGenerator(30 * time.Second),
serverAddr: serverAddr,
}
}
@@ -40,10 +48,15 @@ func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
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,
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
}
// Render template
@@ -81,10 +94,15 @@ func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
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,
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
}
// Render template
@@ -101,9 +119,7 @@ func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
}
}
// ExportPDF handles PDF export requests
// For now, redirects to print-friendly version
// In production, integrate with chromedp or similar for actual PDF generation
// 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")
@@ -111,7 +127,54 @@ func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) {
lang = "en"
}
// Redirect to print-friendly version
// The browser's print dialog will handle PDF generation
http.Redirect(w, r, "/?lang="+lang+"&print=true", http.StatusSeeOther)
// 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
}