diff --git a/internal/handlers/cv.go b/internal/handlers/cv.go
index ab0109a..11fb7c7 100644
--- a/internal/handlers/cv.go
+++ b/internal/handlers/cv.go
@@ -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
}
diff --git a/static/css/main.css b/static/css/main.css
index 532e229..25157bd 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -728,3 +728,304 @@ a:focus {
max-width: none;
}
}
+
+/* ===============================================
+ TWO-PAGE LAYOUT STYLES
+ =============================================== */
+
+/* Page Container - Each CV page */
+.cv-page {
+ background: var(--paper-white);
+ max-width: 1200px;
+ margin: 2rem auto;
+ box-shadow: 2px 2px 9px rgba(0,0,0,0.5);
+ border: 1px solid #333;
+ transform: scale(0.95);
+ transform-origin: top center;
+ transition: transform 0.3s ease;
+}
+
+/* Page Content Grid */
+.page-content {
+ display: grid;
+}
+
+/* Page 1: Left sidebar + Main content */
+.page-1 .page-content {
+ grid-template-columns: 300px 1fr;
+}
+
+/* Page 2: Main content + Right sidebar */
+.page-2 .page-content {
+ grid-template-columns: 1fr 300px;
+}
+
+/* Sidebar positioning */
+.cv-sidebar-left {
+ grid-column: 1;
+ grid-row: 1;
+}
+
+.cv-sidebar-right {
+ grid-column: 2;
+ grid-row: 1;
+}
+
+/* Main content positioning */
+.page-1 .cv-main {
+ grid-column: 2;
+ grid-row: 1;
+}
+
+.page-2 .cv-main {
+ grid-column: 1;
+ grid-row: 1;
+}
+
+/* ===============================================
+ FOOTER STYLES
+ =============================================== */
+
+.cv-footer {
+ background: #303030;
+ color: #ccc;
+ padding: 20px 0;
+ margin: 0;
+ grid-column: 1 / -1; /* Span all columns */
+}
+
+.footer-content {
+ list-style: none;
+ text-align: center;
+ margin: 0;
+ padding: 0;
+}
+
+.footer-content li {
+ display: inline-block;
+ margin: 0;
+}
+
+.footer-content li > div {
+ display: inline-block;
+ margin: 0 20px;
+ text-align: left;
+}
+
+.footer-label {
+ width: 200px;
+ font-size: 1.7em;
+}
+
+.footer-value {
+ width: 450px;
+ font-size: 1em;
+}
+
+.footer-value b {
+ font-weight: normal;
+ font-size: 1.7em;
+}
+
+.footer-separator {
+ position: relative;
+ left: -4%;
+ font-size: 0.6em;
+}
+
+.footer-separator i {
+ opacity: 0.3;
+}
+
+.cv-footer a {
+ color: inherit;
+}
+
+.cv-footer a:hover {
+ color: #0275d8;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+/* ===============================================
+ PRINT STYLES - TWO-PAGE LAYOUT
+ =============================================== */
+
+@media print {
+ body {
+ background: white;
+ margin: 0;
+ padding: 0;
+ }
+
+ .action-bar {
+ display: none !important;
+ }
+
+ .cv-page {
+ box-shadow: none;
+ border: none;
+ margin: 0 auto;
+ transform: scale(1);
+ max-width: 100%;
+ page-break-after: always;
+ page-break-inside: avoid;
+ }
+
+ .cv-page.page-2 {
+ page-break-after: auto;
+ }
+
+ .page-content {
+ page-break-inside: avoid;
+ }
+
+ .cv-section {
+ page-break-inside: avoid;
+ }
+
+ /* Ensure footer only on page 2 */
+ .page-1 .cv-footer {
+ display: none !important;
+ }
+
+ .cv-footer {
+ page-break-inside: avoid;
+ background: #ddd !important;
+ color: #333 !important;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
+
+ a {
+ text-decoration: none;
+ font-weight: 800;
+ color: inherit;
+ }
+
+ /* Set up proper A4 page dimensions */
+ @page {
+ size: A4 portrait;
+ margin: 0.5in;
+ }
+}
+
+/* ===============================================
+ SECTION STYLES FOR PAGE 2
+ =============================================== */
+
+.award-item,
+.course-item,
+.language-item,
+.reference-item,
+.other-content {
+ margin-bottom: 1em;
+}
+
+.award-item strong,
+.course-item strong,
+.language-item strong {
+ font-weight: 600;
+ color: var(--text-dark);
+}
+
+.award-item small,
+.course-item small {
+ color: #666;
+ font-size: 0.875em;
+}
+
+.award-desc,
+.course-desc {
+ margin-top: 0.5em;
+ color: var(--text-gray);
+ font-size: 0.95em;
+}
+
+.reference-item {
+ margin-bottom: 0.5em;
+}
+
+.reference-item a {
+ font-weight: 500;
+}
+
+.ref-type {
+ color: #999;
+ margin-left: 0.5em;
+ font-size: 0.875em;
+}
+
+/* ===============================================
+ MOBILE RESPONSIVE - TWO-PAGE LAYOUT
+ =============================================== */
+
+@media (max-width: 900px) {
+ .cv-page {
+ margin: 1rem;
+ transform: scale(1);
+ }
+
+ /* Stack layout on mobile */
+ .page-1 .page-content,
+ .page-2 .page-content {
+ grid-template-columns: 1fr;
+ }
+
+ .cv-sidebar-left,
+ .cv-sidebar-right {
+ grid-column: 1;
+ }
+
+ .page-1 .cv-main,
+ .page-2 .cv-main {
+ grid-column: 1;
+ }
+
+ /* Hide header on page 2 for mobile to merge pages */
+ .page-2 .cv-title-badges-header {
+ display: none;
+ }
+
+ /* Adjust footer for mobile */
+ .footer-content li > div {
+ display: block;
+ margin: 0;
+ text-align: center;
+ width: 100%;
+ }
+
+ .footer-label {
+ font-size: 1em;
+ margin-top: 15px;
+ color: #777;
+ }
+
+ .footer-separator {
+ display: none;
+ }
+
+ .footer-value {
+ font-size: 1.5em;
+ margin-bottom: 0;
+ padding: 0;
+ }
+}
+
+/* ===============================================
+ TABLET RESPONSIVE
+ =============================================== */
+
+@media (max-width: 768px) and (min-width: 577px) {
+ .page-content {
+ gap: 1rem;
+ }
+
+ .footer-label {
+ font-size: 1.2em;
+ }
+
+ .footer-value {
+ font-size: 1em;
+ }
+}
diff --git a/templates/cv-content.html b/templates/cv-content.html
index d93c778..dd46ee6 100644
--- a/templates/cv-content.html
+++ b/templates/cv-content.html
@@ -1,149 +1,218 @@
-
-
-
-
-
-
-
-
-
-