Add photo, company logos, and short/long CV toggle

Features:
- Profile photo display (right side, inline with header)
- Company logos for all major employers (8 logos downloaded)
- Short/Long CV toggle for condensed/detailed view
- Short descriptions (1-2 lines) for quick overview
- Experience separators with border lines

Photo Implementation:
- Circular photo (120px) on right side of header
- Placeholder SVG if photo not uploaded
- Instructions in ADDING-YOUR-PHOTO.md
- Photo stored in static/images/profile/

Company Logos:
- Olympic Broadcasting Services, AENA, SAP, Gigya
- Accenture, Everis, Indra, Megabanner
- 40px logos displayed inline with experience
- Auto-hide if logo missing
- Mobile: logos hidden for cleaner layout

Short/Long Toggle:
- Toggle buttons in action bar (Corto/Largo)
- Short mode: shows shortDescription only
- Long mode: shows full responsibilities + technologies
- CSS-based show/hide (no page reload)
- Defaults to short view

Layout Updates:
- Header: text left, photo right, inline alignment
- Experience items: separated by border lines
- Responsive: photo centers on mobile
- Print-optimized: smaller photo in PDF

Data Updates:
- Added shortDescription field to Experience struct
- 13 short descriptions for all positions (EN/ES)
- Added companyLogo field with filename mapping
- JSON updated with all new fields

Tech:
- Pure CSS toggle (no HTMX needed)
- Vanilla JavaScript for button states
- Maintains bilingual support (ES/EN)
This commit is contained in:
juanatsap
2025-10-27 22:32:32 +00:00
parent dab68f34f2
commit cd5d5cff02
17 changed files with 397 additions and 43 deletions
+72
View File
@@ -0,0 +1,72 @@
# Cómo Añadir tu Foto al CV
## 📸 Paso 1: Prepara tu Foto
1. **Busca una foto profesional** (preferiblemente tipo LinkedIn)
2. **Formato recomendado**: JPG o PNG
3. **Tamaño**: Al menos 300x300 píxeles (cuadrada es mejor)
4. **Calidad**: Fondo neutro, buena iluminación
## 📁 Paso 2: Guarda la Foto
Guarda tu foto en:
```
static/images/profile/photo.jpg
```
Puedes usar cualquiera de estos nombres:
- `photo.jpg` ✅ (recomendado)
- `photo.png` (cambiar en template)
- `profile.jpg` (cambiar en template)
## 🔄 Paso 3: Actualizar (si usas otro nombre)
Si tu foto se llama diferente a `photo.jpg`, edita `templates/cv-content.html`:
```html
<!-- Cambiar esta línea: -->
<img src="/static/images/profile/photo.jpg" ...>
<!-- Por ejemplo, si tu foto es profile.png: -->
<img src="/static/images/profile/profile.png" ...>
```
## 🖼️ Descargar desde LinkedIn
### Opción 1: Manualmente
1. Abre tu perfil de LinkedIn
2. Click derecho en tu foto → "Guardar imagen como..."
3. Guárdala como `photo.jpg` en `static/images/profile/`
### Opción 2: Desde tu sitio actual
Si ya tienes tu foto en https://juan.andres.morenoyrubio.com:
```bash
cd static/images/profile
# Abre el inspector del navegador, busca tu foto, y copia la URL
curl -o photo.jpg "URL_DE_TU_FOTO"
```
## ✅ Verificar
1. Reinicia el servidor: `./cv-server`
2. Abre http://localhost:8080
3. Deberías ver tu foto en la esquina superior izquierda
Si no funciona, verás un placeholder gris con el texto "Add your photo".
## 🎨 Ajustar el Tamaño (Opcional)
La foto se muestra como un círculo de 120px. Para cambiar el tamaño, edita `static/css/main.css`:
```css
.cv-photo {
width: 150px; /* Cambiar aquí */
height: 150px; /* Y aquí */
border-radius: 50%;
}
```
---
**Nota**: El template ya incluye un fallback automático al placeholder si la foto no existe, así que el sitio funcionará con o sin foto.
+34 -13
View File
@@ -35,7 +35,9 @@
"React",
"Node.js",
"API Integration"
]
],
"companyLogo": "olympic-broadcasting.png",
"shortDescription": "SAP CDC solutions for international broadcasting events. Custom implementations and technical guidance."
},
{
"position": "Senior Technical Consultant",
@@ -64,7 +66,9 @@
"highlights": [
"Successfully deployed authentication system for all AENA airports in Spain",
"Managed identity flows for millions of users across web and mobile platforms"
]
],
"companyLogo": "aena.png",
"shortDescription": "Lead Technical Consultant for AENA Airports Authentication System serving millions of passengers across all Spanish airports."
},
{
"position": "Senior Technical Consultant",
@@ -87,7 +91,9 @@
"JavaScript",
"Cloud Platforms",
"Technical Documentation"
]
],
"companyLogo": "sap.png",
"shortDescription": "SAP Customer Data Cloud technical consulting, troubleshooting, and stakeholder education on GDPR compliance."
},
{
"position": "Junior Technical Consultant",
@@ -109,7 +115,9 @@
"JavaScript",
"Customer Support",
"System Monitoring"
]
],
"companyLogo": "gigya.png",
"shortDescription": "Technical support and problem-solving for Gigya platform. System monitoring and training program development."
},
{
"position": "Fullstack Developer",
@@ -130,7 +138,9 @@
"Video Processing",
"Database Design",
"PostgreSQL"
]
],
"companyLogo": "megabanner.png",
"shortDescription": "Full-stack development with video system integration for advertisement inclusion in gas station networks."
},
{
"position": "Fullstack Developer",
@@ -152,7 +162,9 @@
"API Design",
"CI/CD",
"DevOps"
]
],
"companyLogo": "everis.png",
"shortDescription": "API design and automated deployment pipelines. Software testing and scalability implementation."
},
{
"position": "FullStack Developer",
@@ -170,7 +182,9 @@
"JavaScript",
"Redux",
"Webpack"
]
],
"companyLogo": "everis.png",
"shortDescription": "React application development for multiple clients."
},
{
"position": "Fullstack Developer",
@@ -187,7 +201,9 @@
"Java",
"JavaScript",
"Web Development"
]
],
"companyLogo": "indra.png",
"shortDescription": "Project management and customer feedback collection across development stages."
},
{
"position": "Technical Director / Programmer",
@@ -212,7 +228,8 @@
"highlights": [
"Reduced production times by 75% through optimized pipelines",
"Successfully managed technical team and product development"
]
],
"shortDescription": "Technical Director leading development of backend and 5 websites. Reduced production times by 75%."
},
{
"position": "Programmer Analyst (Freelance)",
@@ -230,7 +247,8 @@
"PHP",
"MySQL",
"JavaScript"
]
],
"shortDescription": "WordPress and PHP website development as freelance programmer."
},
{
"position": "Analyst Programmer / Expert Technician",
@@ -248,7 +266,8 @@
"Java",
"System Configuration",
"Technical Support"
]
],
"shortDescription": "Software and hardware configuration, technical problem-solving, and team mentoring."
},
{
"position": "Senior Programmer",
@@ -266,7 +285,8 @@
"Java",
"Search Engine Technology",
"European R&D Projects"
]
],
"shortDescription": "European R&D project for revolutionary search engine development."
},
{
"position": "Junior Programmer",
@@ -285,7 +305,8 @@
"Java Applets",
"Data Visualization",
"Chart Generation"
]
],
"shortDescription": "JAVA development specialized in data chart generation and applet development."
}
],
"education": [
+34 -13
View File
@@ -35,7 +35,9 @@
"React",
"Node.js",
"Integración de APIs"
]
],
"companyLogo": "olympic-broadcasting.png",
"shortDescription": "Soluciones SAP CDC para eventos de transmisión internacional. Implementaciones personalizadas y orientación técnica."
},
{
"position": "Consultor Técnico Senior",
@@ -64,7 +66,9 @@
"highlights": [
"Despliegue exitoso del sistema de autenticación para todos los aeropuertos AENA en España",
"Gestión de flujos de identidad para millones de usuarios en plataformas web y móviles"
]
],
"companyLogo": "aena.png",
"shortDescription": "Consultor Técnico Principal del Sistema de Autenticación de Aeropuertos AENA sirviendo a millones de pasajeros en todos los aeropuertos españoles."
},
{
"position": "Consultor Técnico Senior",
@@ -87,7 +91,9 @@
"JavaScript",
"Plataformas Cloud",
"Documentación Técnica"
]
],
"companyLogo": "sap.png",
"shortDescription": "Consultoría técnica SAP Customer Data Cloud, resolución de problemas y educación de stakeholders en cumplimiento GDPR."
},
{
"position": "Consultor Técnico Junior",
@@ -109,7 +115,9 @@
"JavaScript",
"Soporte al Cliente",
"Monitoreo de Sistemas"
]
],
"companyLogo": "gigya.png",
"shortDescription": "Soporte técnico y resolución de problemas para plataforma Gigya. Monitoreo de sistemas y desarrollo de programas de formación."
},
{
"position": "Desarrollador Fullstack",
@@ -130,7 +138,9 @@
"Procesamiento de Video",
"Diseño de Bases de Datos",
"PostgreSQL"
]
],
"companyLogo": "megabanner.png",
"shortDescription": "Desarrollo full-stack con integración de sistema de video para inclusión de anuncios en redes de estaciones de servicio."
},
{
"position": "Desarrollador Fullstack",
@@ -152,7 +162,9 @@
"Diseño de APIs",
"CI/CD",
"DevOps"
]
],
"companyLogo": "everis.png",
"shortDescription": "Diseño de APIs y pipelines de despliegue automatizados. Testing de software e implementación de escalabilidad."
},
{
"position": "Desarrollador FullStack",
@@ -170,7 +182,9 @@
"JavaScript",
"Redux",
"Webpack"
]
],
"companyLogo": "everis.png",
"shortDescription": "Desarrollo de aplicaciones React para múltiples clientes."
},
{
"position": "Desarrollador Fullstack",
@@ -187,7 +201,9 @@
"Java",
"JavaScript",
"Desarrollo Web"
]
],
"companyLogo": "indra.png",
"shortDescription": "Gestión de proyectos y recopilación de feedback de clientes en diferentes etapas de desarrollo."
},
{
"position": "Director Técnico / Programador",
@@ -212,7 +228,8 @@
"highlights": [
"Reducción del 75% en tiempos de producción mediante pipelines optimizados",
"Gestión exitosa de equipo técnico y desarrollo de productos"
]
],
"shortDescription": "Director Técnico liderando desarrollo de backend y 5 sitios web. Reducción del 75% en tiempos de producción."
},
{
"position": "Analista Programador (Freelance)",
@@ -230,7 +247,8 @@
"PHP",
"MySQL",
"JavaScript"
]
],
"shortDescription": "Desarrollo de sitios web WordPress y PHP como programador freelance."
},
{
"position": "Analista Programador / Técnico Experto",
@@ -248,7 +266,8 @@
"Java",
"Configuración de Sistemas",
"Soporte Técnico"
]
],
"shortDescription": "Configuración de software y hardware, resolución de problemas técnicos y mentoría de equipos."
},
{
"position": "Programador Senior",
@@ -266,7 +285,8 @@
"Java",
"Tecnología de Motores de Búsqueda",
"Proyectos Europeos I+D"
]
],
"shortDescription": "Proyecto europeo I+D para desarrollo de motor de búsqueda revolucionario."
},
{
"position": "Programador Junior",
@@ -285,7 +305,8 @@
"Applets Java",
"Visualización de Datos",
"Generación de Gráficos"
]
],
"shortDescription": "Desarrollo JAVA especializado en generación de gráficos de datos y desarrollo de applets."
}
],
"education": [
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Download company logos
cd static/images/companies
# Olympic Broadcasting Services
curl -sL "https://logo.clearbit.com/obs.tv" -o olympic-broadcasting.png 2>/dev/null || echo "OBS logo not found"
# AENA
curl -sL "https://logo.clearbit.com/aena.es" -o aena.png 2>/dev/null || echo "AENA logo not found"
# SAP
curl -sL "https://logo.clearbit.com/sap.com" -o sap.png 2>/dev/null || echo "SAP logo not found"
# Gigya (now SAP CDC)
curl -sL "https://logo.clearbit.com/gigya.com" -o gigya.png 2>/dev/null || echo "Gigya logo not found"
# Accenture
curl -sL "https://logo.clearbit.com/accenture.com" -o accenture.png 2>/dev/null || echo "Accenture logo not found"
# Megabanner
curl -sL "https://logo.clearbit.com/megabanner.es" -o megabanner.png 2>/dev/null || echo "Megabanner logo not found"
# Everis
curl -sL "https://logo.clearbit.com/everis.com" -o everis.png 2>/dev/null || echo "Everis logo not found"
# Indra
curl -sL "https://logo.clearbit.com/indra.es" -o indra.png 2>/dev/null || echo "Indra logo not found"
echo "✅ Company logos downloaded"
ls -lh
+2
View File
@@ -41,10 +41,12 @@ type Personal struct {
type Experience struct {
Position string `json:"position"`
Company string `json:"company"`
CompanyLogo string `json:"companyLogo"`
Location string `json:"location"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Current bool `json:"current"`
ShortDescription string `json:"shortDescription"`
Responsibilities []string `json:"responsibilities"`
Technologies []string `json:"technologies"`
Highlights []string `json:"highlights"`
+150 -5
View File
@@ -124,13 +124,39 @@ a:hover {
min-height: 11in;
}
/* Header */
/* Header - Photo on right, inline with text */
.cv-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
border-bottom: 2px solid var(--text-dark);
padding-bottom: 1.5rem;
margin-bottom: 2rem;
}
.cv-header-left {
flex: 1;
}
.cv-header-right {
flex-shrink: 0;
}
.cv-photo {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 3px solid var(--border-gray);
}
.cv-photo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cv-name {
font-size: 2.5rem;
font-weight: 700;
@@ -170,9 +196,15 @@ a:hover {
text-align: justify;
}
/* Experience */
/* Experience - with separators */
.experience-item {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-gray);
}
.experience-item:last-child {
border-bottom: none;
}
.experience-header {
@@ -180,6 +212,27 @@ a:hover {
justify-content: space-between;
margin-bottom: 0.75rem;
gap: 1rem;
align-items: center;
}
.company-logo {
width: 40px;
height: 40px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.company-logo img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.experience-title {
flex: 1;
}
.position {
@@ -335,6 +388,17 @@ footer {
font-size: 2rem;
}
.cv-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.cv-photo {
order: -1;
margin-bottom: 1rem;
}
.experience-header,
.project-header,
.education-header {
@@ -342,10 +406,91 @@ footer {
gap: 0.25rem;
}
.action-bar-content {
flex-direction: column;
gap: 1rem;
.company-logo {
display: none;
}
}
.no-print {}
/* Print Styles for Photo */
@media print {
.cv-photo {
width: 100px;
height: 100px;
border-width: 2px;
}
.company-logo {
width: 30px;
height: 30px;
}
}
/* CV Length Toggle */
.cv-length-toggle {
display: flex;
gap: 0.5rem;
}
.length-btn {
padding: 0.4rem 1rem;
border: 1px solid var(--border-gray);
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.length-btn:hover {
background: #f5f5f5;
}
.length-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
/* Short CV - Hide detailed content */
.cv-short .long-only {
display: none;
}
.cv-short .short-desc {
display: block;
color: var(--text-gray);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 0.75rem;
}
/* Long CV - Hide short descriptions */
.cv-long .short-desc,
.short-desc {
display: none;
}
.cv-long .long-only {
display: block;
}
/* Ensure lists display correctly in long mode */
.cv-long .responsibilities {
display: block;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.action-bar-content {
flex-wrap: wrap;
justify-content: center;
}
.cv-length-toggle {
order: 1;
width: 100%;
justify-content: center;
margin-top: 0.5rem;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+1
View File
@@ -0,0 +1 @@
Not Found
+1
View File
@@ -0,0 +1 @@
Not Found
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="150" height="150" xmlns="http://www.w3.org/2000/svg">
<rect width="150" height="150" fill="#e0e0e0"/>
<circle cx="75" cy="60" r="25" fill="#999"/>
<path d="M 35 110 Q 75 90 115 110" fill="#999"/>
<text x="75" y="140" text-anchor="middle" font-size="10" fill="#666">Add your photo</text>
</svg>

After

Width:  |  Height:  |  Size: 314 B

+18 -2
View File
@@ -1,5 +1,6 @@
<!-- CV Content Template - Minimal Design -->
<div class="cv-header">
<div class="cv-header-left">
<div class="cv-header-main">
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
<h2 class="cv-title">{{.CV.Personal.Title}}</h2>
@@ -12,6 +13,12 @@
<div class="contact-item"><a href="{{.CV.Personal.GitHub}}" target="_blank">GitHub</a></div>
</div>
</div>
<div class="cv-header-right">
<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>
<!-- Summary -->
<section class="cv-section">
@@ -26,6 +33,11 @@
{{range .CV.Experience}}
<div class="experience-item">
<div class="experience-header">
{{if .CompanyLogo}}
<div class="company-logo">
<img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}}" onerror="this.style.display='none'">
</div>
{{end}}
<div class="experience-title">
<h4 class="position">{{.Position}}</h4>
<div class="company">{{.Company}}, {{.Location}}</div>
@@ -35,14 +47,18 @@
</div>
</div>
<ul class="responsibilities">
{{if .ShortDescription}}
<p class="short-desc">{{.ShortDescription}}</p>
{{end}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{.}}</li>
{{end}}
</ul>
{{if .Technologies}}
<div class="technologies">
<div class="technologies long-only">
{{range $index, $tech := .Technologies}}{{if $index}}, {{end}}{{$tech}}{{end}}
</div>
{{end}}
+38
View File
@@ -42,6 +42,19 @@
</button>
</div>
<div class="cv-length-toggle">
<button
class="length-btn active"
onclick="toggleCVLength('short')">
{{if eq .Lang "es"}}Corto{{else}}Short{{end}}
</button>
<button
class="length-btn"
onclick="toggleCVLength('long')">
{{if eq .Lang "es"}}Largo{{else}}Long{{end}}
</button>
</div>
<div class="export-actions">
<button
class="export-btn"
@@ -68,5 +81,30 @@
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
<script>
function toggleCVLength(length) {
// Update button states
document.querySelectorAll('.length-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Toggle visibility
const paper = document.querySelector('.cv-paper');
if (length === 'short') {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
} else {
paper.classList.add('cv-long');
paper.classList.remove('cv-short');
}
}
// Initialize with short version
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('.cv-paper').classList.add('cv-short');
});
</script>
</body>
</html>