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:
+76
-13
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user