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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+208
-139
@@ -1,149 +1,218 @@
|
||||
<!-- Professional Title Badges - Full Width Top Bar -->
|
||||
<div class="cv-title-badges-header">
|
||||
<span class="title-badge">{{if eq .Lang "es"}}ANALISTA PROGRAMADOR{{else}}ANALYST PROGRAMMER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">NODEJS + REACTJS {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">GO {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Left Sidebar - Skills -->
|
||||
<aside class="cv-sidebar">
|
||||
<!-- Skills Section - Dynamically render all categories -->
|
||||
{{range .CV.Skills.Technical}}
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{.Category}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Languages Section -->
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{range .CV.Languages}}
|
||||
<div class="language-item">
|
||||
<strong>{{.Language}}</strong>: {{.Proficiency}}
|
||||
{{if .Detail}}<br><small style="color: #666;">{{.Detail}}</small>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Courses Section -->
|
||||
{{if .CV.Courses}}
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{if eq .Lang "es"}}Cursos Realizados{{else}}Training Courses{{end}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{range .CV.Courses}}
|
||||
<div class="course-item">
|
||||
<strong>{{.Title}}</strong><br>
|
||||
<small>{{.Institution}} - {{.Location}}</small><br>
|
||||
<small>{{.Date}} ({{.Duration}})</small>
|
||||
{{if .Description}}<p class="course-desc">{{.Description}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- References Section -->
|
||||
{{if .CV.References}}
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{range .CV.References}}
|
||||
<div class="reference-item">
|
||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a>
|
||||
<small class="ref-type">({{.Type}})</small>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Other Section (Driver's License) -->
|
||||
{{if .CV.Other.DriverLicense}}
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{if eq .Lang "es"}}Carnet de conducir {{.CV.Other.DriverLicense}}{{else}}Driver's License {{.CV.Other.DriverLicense}}{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="cv-main">
|
||||
<!-- Header with Name and Photo -->
|
||||
<div class="cv-header">
|
||||
<div class="cv-header-content">
|
||||
<div class="cv-header-left">
|
||||
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
|
||||
<p class="cv-experience-years">{{if eq .Lang "es"}}20 años de experiencia{{else}}20 years of experience{{end}}</p>
|
||||
<!-- Intro/Excerpt Text - No section heading, just the text -->
|
||||
<div class="intro-text">{{.CV.Summary}}</div>
|
||||
</div>
|
||||
<div class="cv-photo">
|
||||
<img src="/static/images/profile/photo.jpg" alt="{{.CV.Personal.Name}}" onerror="this.src='/static/images/profile/placeholder.svg'">
|
||||
</div>
|
||||
</div>
|
||||
<!-- PAGE 1 -->
|
||||
<div class="cv-page page-1">
|
||||
<!-- Professional Title Badges - Full Width Top Bar -->
|
||||
<div class="cv-title-badges-header">
|
||||
<span class="title-badge">{{if eq .Lang "es"}}ANALISTA PROGRAMADOR{{else}}ANALYST PROGRAMMER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">NODEJS + REACTJS {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">GO {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Education -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</h3>
|
||||
{{range .CV.Education}}
|
||||
<div class="education-item">
|
||||
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{if eq $.Lang "es"}}obtenido de{{else}}obtained from the{{end}} <strong>{{.Institution}}</strong> ({{.Location}})
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<!-- Skills Summary -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</h3>
|
||||
<p class="summary-text">
|
||||
{{if eq .Lang "es"}}
|
||||
Amplio conocimiento en entornos web, tanto J2EE como PHP. Experto en tecnologías front-end, aunque con considerable experiencia en sistemas back-end. Receptivo al aprendizaje de nuevas tecnologías, y con una gran dosis de creatividad. Capacidad de analizar problemas y aportar soluciones específicas adaptadas a cada tipo de cliente. Me gusta trabajar tanto solo como en grupos.
|
||||
{{else}}
|
||||
Extensive knowledge in web environments, both J2EE and PHP. Expert in front-end technologies, although with considerable experience in back-end systems. Receptive to learning new technologies, and with a large dose of creativity. Ability to analyze problems and provide specific solutions tailored to each client type. I like to work both alone and in groups.
|
||||
<!-- Page 1 Content Grid: Left Sidebar + Main Content -->
|
||||
<div class="page-content">
|
||||
<!-- Left Sidebar - Skills (first half) -->
|
||||
<aside class="cv-sidebar cv-sidebar-left">
|
||||
{{range $index, $category := .SkillsLeft}}
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{$category.Category}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{range $category.Items}}<div class="skill-item">{{.}}</div>{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
</p>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<!-- Experience -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</h3>
|
||||
|
||||
{{range .CV.Experience}}
|
||||
<div class="experience-item">
|
||||
<div class="experience-header">
|
||||
<div class="experience-title-line">
|
||||
<h4 class="position">{{.Position}} / {{if eq $.Lang "es"}}Analista Programador{{else}}Analyst Programmer{{end}}</h4>
|
||||
<span class="experience-period">{{.StartDate}} / {{if .Current}}{{if eq $.Lang "es"}}presente{{else}}now{{end}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</span>
|
||||
<!-- Main Content Area - Page 1 -->
|
||||
<main class="cv-main">
|
||||
<!-- Header with Name and Photo -->
|
||||
<div class="cv-header">
|
||||
<div class="cv-header-content">
|
||||
<div class="cv-header-left">
|
||||
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
|
||||
<p class="cv-experience-years">{{if eq .Lang "es"}}20 años de experiencia{{else}}20 years of experience{{end}}</p>
|
||||
<!-- Intro/Excerpt Text - No section heading, just the text -->
|
||||
<div class="intro-text">{{.CV.Summary}}</div>
|
||||
</div>
|
||||
<div class="cv-photo">
|
||||
<img src="/static/images/profile/photo.jpg" alt="{{.CV.Personal.Name}}" onerror="this.src='/static/images/profile/placeholder.svg'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .ShortDescription}}
|
||||
<p class="short-desc">{{.ShortDescription}}</p>
|
||||
<!-- Education -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</h3>
|
||||
{{range .CV.Education}}
|
||||
<div class="education-item">
|
||||
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{if eq $.Lang "es"}}obtenido de{{else}}obtained from the{{end}} <strong>{{.Institution}}</strong> ({{.Location}})
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<!-- Skills Summary -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</h3>
|
||||
<p class="summary-text">
|
||||
{{if eq .Lang "es"}}
|
||||
Amplio conocimiento en entornos web, tanto J2EE como PHP. Experto en tecnologías front-end, aunque con considerable experiencia en sistemas back-end. Receptivo al aprendizaje de nuevas tecnologías, y con una gran dosis de creatividad. Capacidad de analizar problemas y aportar soluciones específicas adaptadas a cada tipo de cliente. Me gusta trabajar tanto solo como en grupos.
|
||||
{{else}}
|
||||
Extensive knowledge in web environments, both J2EE and PHP. Expert in front-end technologies, although with considerable experience in back-end systems. Receptive to learning new technologies, and with a large dose of creativity. Ability to analyze problems and provide specific solutions tailored to each client type. I like to work both alone and in groups.
|
||||
{{end}}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Experience -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</h3>
|
||||
|
||||
{{range .CV.Experience}}
|
||||
<div class="experience-item">
|
||||
<div class="experience-header">
|
||||
<div class="experience-title-line">
|
||||
<h4 class="position">{{.Position}} / {{if eq $.Lang "es"}}Analista Programador{{else}}Analyst Programmer{{end}}</h4>
|
||||
<span class="experience-period">{{.StartDate}} / {{if .Current}}{{if eq $.Lang "es"}}presente{{else}}now{{end}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .ShortDescription}}
|
||||
<p class="short-desc">{{.ShortDescription}}</p>
|
||||
{{end}}
|
||||
|
||||
<div class="long-only">
|
||||
<ul class="responsibilities">
|
||||
{{range .Responsibilities}}
|
||||
<li>{{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PAGE 2 -->
|
||||
<div class="cv-page page-2">
|
||||
<!-- Professional Title Badges - Same as Page 1 -->
|
||||
<div class="cv-title-badges-header">
|
||||
<span class="title-badge">{{if eq .Lang "es"}}ANALISTA PROGRAMADOR{{else}}ANALYST PROGRAMMER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">NODEJS + REACTJS {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">GO {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Page 2 Content Grid: Main Content + Right Sidebar -->
|
||||
<div class="page-content">
|
||||
<!-- Main Content Area - Page 2 -->
|
||||
<main class="cv-main">
|
||||
<!-- Awards Section -->
|
||||
{{if .CV.Awards}}
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</h3>
|
||||
{{range .CV.Awards}}
|
||||
<div class="award-item">
|
||||
<strong>{{.Title}}</strong><br>
|
||||
<small>{{.Issuer}} - {{.Date}}</small>
|
||||
{{if .Description}}<p class="award-desc">{{.Description}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<div class="long-only">
|
||||
<ul class="responsibilities">
|
||||
{{range .Responsibilities}}
|
||||
<li>{{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
<!-- Courses Section -->
|
||||
{{if .CV.Courses}}
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</h3>
|
||||
{{range .CV.Courses}}
|
||||
<div class="course-item">
|
||||
<strong>{{.Title}}</strong><br>
|
||||
<small>{{.Institution}} - {{.Location}}</small><br>
|
||||
<small>{{.Date}} ({{.Duration}})</small>
|
||||
{{if .Description}}<p class="course-desc">{{.Description}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
</main>
|
||||
<!-- Languages Section -->
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
|
||||
{{range .CV.Languages}}
|
||||
<div class="language-item">
|
||||
<strong>{{.Language}}</strong>: {{.Proficiency}}
|
||||
{{if .Detail}}<br><small style="color: #666;">{{.Detail}}</small>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<!-- References Section -->
|
||||
{{if .CV.References}}
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</h3>
|
||||
{{range .CV.References}}
|
||||
<div class="reference-item">
|
||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a>
|
||||
<small class="ref-type">({{.Type}})</small>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Other Section (Driver's License) -->
|
||||
{{if .CV.Other.DriverLicense}}
|
||||
<section class="cv-section">
|
||||
<h3 class="section-title">{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</h3>
|
||||
<div class="other-content">
|
||||
{{if eq .Lang "es"}}Carnet de conducir {{.CV.Other.DriverLicense}}{{else}}Driver's License {{.CV.Other.DriverLicense}}{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
<!-- Right Sidebar - Skills (second half) -->
|
||||
<aside class="cv-sidebar cv-sidebar-right">
|
||||
{{range $index, $category := .SkillsRight}}
|
||||
<section class="sidebar-section">
|
||||
<h3 class="sidebar-title">{{$category.Category}}</h3>
|
||||
<div class="sidebar-content">
|
||||
{{range $category.Items}}<div class="skill-item">{{.}}</div>{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Footer - Only on Page 2 -->
|
||||
<footer class="cv-footer">
|
||||
<ul class="footer-content">
|
||||
<li>
|
||||
<div class="footer-label">address_</div>
|
||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||
<div class="footer-value">Carrer Meer, N° 51 4° 2ª, 08003 Barcelona</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="footer-label">phone#</div>
|
||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||
<div class="footer-value">+34 676875420</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="footer-label">email@</div>
|
||||
<div class="footer-separator"><i class="fa fa-circle"></i></div>
|
||||
<div class="footer-value">
|
||||
<a href="mailto:txeo.msx@gmail.com" target="_blank" rel="noopener noreferrer">txeo.msx@gmail.com</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user