refactor: standardize port to 1999 across all files

- Updated default port from 8080 to 1999 in config, Docker, and documentation files
- Modified example URLs and test commands to use new port
- Ensured consistent port references in environment configs and deployment examples
- Updated health check endpoints in Docker and testing scripts

The port change aligns with LIV Golf port allocation standards for staging environments (5000-9999 range).
This commit is contained in:
juanatsap
2025-10-29 14:04:24 +00:00
parent 4ec966591d
commit ee354d1d35
12 changed files with 336 additions and 458 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# Copy this file to .env and customize as needed
# Server Configuration
PORT=8080
PORT=1999
HOST=localhost
GO_ENV=development
+1 -1
View File
@@ -50,7 +50,7 @@ curl -o photo.jpg "URL_DE_TU_FOTO"
## ✅ Verificar
1. Reinicia el servidor: `./cv-server`
2. Abre http://localhost:8080
2. Abre http://localhost:1999
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".
+7 -7
View File
@@ -114,7 +114,7 @@ handler := middleware.Recovery(
```go
cfg := config.Load() // Reads from env vars
cfg.Server.Port // Defaults to "8080"
cfg.Server.Port // Defaults to "1999"
cfg.Template.HotReload // Auto-detects development mode
```
@@ -278,19 +278,19 @@ if r.Header.Get("HX-Request") != "" {
```bash
# 1. Health check
curl http://localhost:8080/health
curl http://localhost:1999/health
# 2. Happy path
curl "http://localhost:8080/?lang=en"
curl "http://localhost:1999/?lang=en"
# 3. Error cases
curl "http://localhost:8080/?lang=invalid" # 400 Bad Request
curl "http://localhost:1999/?lang=invalid" # 400 Bad Request
# 4. HTMX requests
curl -H "HX-Request: true" "http://localhost:8080/cv?lang=es"
curl -H "HX-Request: true" "http://localhost:1999/cv?lang=es"
# 5. Security headers
curl -I http://localhost:8080/
curl -I http://localhost:1999/
```
### Future: Automated Tests
@@ -325,7 +325,7 @@ go build -o cv-server -ldflags="-s -w" .
```bash
docker build -t cv-server .
docker run -p 8080:8080 cv-server
docker run -p 1999:1999 cv-server
```
### 3. Cloud Platforms
+3 -3
View File
@@ -32,15 +32,15 @@ COPY data data/
COPY static static/
# Expose port
EXPOSE 8080
EXPOSE 1999
# Set production environment
ENV GO_ENV=production
ENV PORT=8080
ENV PORT=1999
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD wget --no-verbose --tries=1 --spider http://localhost:1999/health || exit 1
# Run the binary
CMD ["./cv-server"]
+6 -6
View File
@@ -631,7 +631,7 @@ terser static/js/main.js -o static/js/main.min.js -c -m
**Create `.env.production`:**
```env
GO_ENV=production
PORT=8080
PORT=1999
HOST=0.0.0.0
ALLOWED_ORIGINS=https://yoursite.com
CACHE_CONTROL_MAX_AGE=86400
@@ -802,13 +802,13 @@ go run main.go
### Test HTMX endpoints
```bash
# Test initial load
curl -s 'http://localhost:8080/?lang=en' | head -50
curl -s 'http://localhost:1999/?lang=en' | head -50
# Test HTMX partial
curl -s 'http://localhost:8080/cv?lang=es' | head -50
curl -s 'http://localhost:1999/cv?lang=es' | head -50
# Test performance
curl -o /dev/null -s -w "Time: %{time_total}s\n" 'http://localhost:8080/cv?lang=en'
curl -o /dev/null -s -w "Time: %{time_total}s\n" 'http://localhost:1999/cv?lang=en'
```
### Run Lighthouse audit
@@ -817,7 +817,7 @@ curl -o /dev/null -s -w "Time: %{time_total}s\n" 'http://localhost:8080/cv?lang=
npm install -g lighthouse
# Run audit
lighthouse http://localhost:8080/?lang=en --view
lighthouse http://localhost:1999/?lang=en --view
```
### Test accessibility
@@ -826,7 +826,7 @@ lighthouse http://localhost:8080/?lang=en --view
npm install -g @axe-core/cli
# Run audit
axe http://localhost:8080/?lang=en
axe http://localhost:1999/?lang=en
```
---
+6 -6
View File
@@ -24,20 +24,20 @@ run: build
test:
@echo "🧪 Testing endpoints..."
@echo "\n1. Health check:"
@curl -s http://localhost:8080/health | jq .
@curl -s http://localhost:1999/health | jq .
@echo "\n2. English CV (first 50 chars):"
@curl -s "http://localhost:8080/?lang=en" | head -c 50
@curl -s "http://localhost:1999/?lang=en" | head -c 50
@echo "\n\n3. Spanish CV content (first 50 chars):"
@curl -s "http://localhost:8080/cv?lang=es" | head -c 50
@curl -s "http://localhost:1999/cv?lang=es" | head -c 50
@echo "\n\n4. Security headers:"
@curl -I http://localhost:8080/ 2>&1 | grep -E "^(X-|Content-Security)"
@curl -I http://localhost:1999/ 2>&1 | grep -E "^(X-|Content-Security)"
@echo "\n✓ All tests complete"
# Test error handling
test-errors:
@echo "🧪 Testing error handling..."
@echo "\n1. Invalid language:"
@curl -i "http://localhost:8080/?lang=invalid" 2>&1 | head -15
@curl -i "http://localhost:1999/?lang=invalid" 2>&1 | head -15
@echo "\n2. Error logging check"
@echo "✓ Error tests complete"
@@ -55,7 +55,7 @@ docker-build:
docker-run:
@echo "🐳 Running Docker container..."
docker run -p 8080:8080 cv-server:latest
docker run -p 1999:1999 cv-server:latest
# Help
help:
+3 -3
View File
@@ -401,7 +401,7 @@ func main() {
)
// ... start server with handler
log.Fatal(http.ListenAndServe(":8080", handler))
log.Fatal(http.ListenAndServe(":1999", handler))
}
```
@@ -436,7 +436,7 @@ go run main.go
### 4. Test Security Headers
```bash
curl -I http://localhost:8080/
curl -I http://localhost:1999/
# Should see security headers in response
```
@@ -503,7 +503,7 @@ curl -I http://localhost:8080/
```
4. **Validate with tools:**
- Lighthouse: `lighthouse http://localhost:8080`
- Lighthouse: `lighthouse http://localhost:1999`
- WAVE: Install browser extension
- axe DevTools: Install browser extension
+3 -3
View File
@@ -26,10 +26,10 @@
go build -o cv-server && ./cv-server
\`\`\`
Open **http://localhost:8080**
Open **http://localhost:1999**
- 🇬🇧 English: http://localhost:8080/?lang=en
- 🇪🇸 Spanish: http://localhost:8080/?lang=es
- 🇬🇧 English: http://localhost:1999/?lang=en
- 🇪🇸 Spanish: http://localhost:1999/?lang=es
## 📄 Updating Your CV
+5 -5
View File
@@ -23,9 +23,9 @@ type ServerConfig struct {
// TemplateConfig contains template-specific settings
type TemplateConfig struct {
Dir string
Dir string
PartialsDir string
HotReload bool
HotReload bool
}
// DataConfig contains data directory settings
@@ -37,15 +37,15 @@ type DataConfig struct {
func Load() *Config {
return &Config{
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
Port: getEnv("PORT", "1999"),
Host: getEnv("HOST", "localhost"),
ReadTimeout: getEnvAsInt("READ_TIMEOUT", 15),
WriteTimeout: getEnvAsInt("WRITE_TIMEOUT", 15),
},
Template: TemplateConfig{
Dir: getEnv("TEMPLATE_DIR", "templates"),
Dir: getEnv("TEMPLATE_DIR", "templates"),
PartialsDir: getEnv("PARTIALS_DIR", "templates/partials"),
HotReload: getEnvAsBool("TEMPLATE_HOT_RELOAD", isDevelopment()),
HotReload: getEnvAsBool("TEMPLATE_HOT_RELOAD", isDevelopment()),
},
Data: DataConfig{
Dir: getEnv("DATA_DIR", "data"),
+167 -263
View File
@@ -1,17 +1,14 @@
/* Minimal CV Design - A4 Page Simulation */
/* CV Design - Original Style Recreation */
:root {
--bg-gray: #525659;
--sidebar-gray: #d9d9d9;
--black-bar: #2b2b2b;
--paper-white: #ffffff;
--text-dark: #2d2d2d;
--text-gray: #555555;
--accent-blue: #0066cc;
--border-gray: #dddddd;
/* A4 dimensions */
--a4-width: 210mm;
--a4-height: 297mm;
--page-padding: 20mm;
}
* {
@@ -21,7 +18,7 @@
}
body {
font-family: 'Inter', Arial, sans-serif;
font-family: Arial, Helvetica, sans-serif;
background-color: var(--bg-gray);
color: var(--text-dark);
line-height: 1.6;
@@ -36,21 +33,23 @@ a:hover {
text-decoration: underline;
}
/* Action Bar */
/* Single Black Top Bar */
.action-bar {
background: white;
border-bottom: 1px solid var(--border-gray);
background: var(--black-bar);
color: white;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.action-bar-content {
max-width: var(--a4-width);
margin: 0 auto;
max-width: 100%;
margin: 0;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 2rem;
align-items: center;
}
@@ -61,35 +60,64 @@ a:hover {
.lang-btn {
padding: 0.4rem 1rem;
border: 1px solid var(--border-gray);
background: white;
border: 1px solid rgba(255,255,255,0.3);
background: transparent;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.lang-btn:hover {
background: #f5f5f5;
background: rgba(255,255,255,0.1);
}
.lang-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
background: rgba(255,255,255,0.2);
border-color: white;
}
.export-btn {
padding: 0.4rem 1.2rem;
background: var(--accent-blue);
background: transparent;
color: white;
border: none;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.export-btn:hover {
background: #0052a3;
background: rgba(255,255,255,0.1);
}
/* Title badges in center of bar */
.title-badges {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.8rem;
}
.title-badge {
font-size: 0.75rem;
letter-spacing: 1px;
font-weight: 400;
color: white;
white-space: nowrap;
}
.title-separator {
color: rgba(255,255,255,0.4);
font-size: 0.75rem;
}
/* Action buttons on right */
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Loading Indicator */
@@ -103,7 +131,7 @@ a:hover {
.loader {
border: 2px solid #f3f3f3;
border-top: 2px solid var(--accent-blue);
border-top: 2px solid white;
border-radius: 50%;
width: 20px;
height: 20px;
@@ -115,27 +143,26 @@ a:hover {
100% { transform: rotate(360deg); }
}
/* A4 Page Container */
/* Main CV Container */
.cv-container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 2rem 0;
max-width: 1200px;
margin: 0 auto;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
/* A4 Paper - Exact dimensions */
/* CV Paper - Two-column layout with shadow */
.cv-paper {
width: var(--a4-width);
min-height: var(--a4-height);
width: 100%;
background: var(--paper-white);
padding: var(--page-padding);
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin: 0 auto;
box-shadow: 0 0 30px rgba(0,0,0,0.4);
margin: 0;
position: relative;
display: grid;
grid-template-columns: 300px 1fr;
min-height: 100vh;
}
/* Page break helpers */
@@ -149,31 +176,55 @@ a:hover {
break-inside: avoid;
}
/* 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;
/* Sidebar - Left column */
.cv-sidebar {
background: var(--sidebar-gray);
padding: 2rem 1.5rem;
font-size: 0.9rem;
}
.sidebar-section {
margin-bottom: 2rem;
}
.cv-header-left {
flex: 1;
.sidebar-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.8rem;
color: var(--text-dark);
}
.cv-header-right {
flex-shrink: 0;
.sidebar-content {
line-height: 1.8;
}
.skill-item {
margin-bottom: 0.3rem;
color: var(--text-dark);
}
/* Main Content - Right column */
.cv-main {
background: var(--paper-white);
padding: 2rem 2.5rem;
}
/* Header with photo and name */
.cv-header {
margin-bottom: 2rem;
}
.cv-header-content {
display: flex;
align-items: flex-start;
gap: 1.5rem;
}
.cv-photo {
width: 100px;
height: 100px;
border-radius: 50%;
width: 150px;
height: 200px;
flex-shrink: 0;
overflow: hidden;
border: 3px solid var(--border-gray);
}
.cv-photo img {
@@ -183,230 +234,117 @@ a:hover {
}
.cv-name {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.cv-title {
font-size: 1.1rem;
font-size: 2.5rem;
font-weight: 400;
color: var(--text-gray);
margin-bottom: 1rem;
margin-bottom: 0.5rem;
color: var(--text-dark);
}
.cv-contact {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem;
font-size: 0.85rem;
.cv-experience-years {
font-size: 1rem;
color: var(--text-gray);
margin: 0;
}
/* Sections */
.cv-section {
margin-bottom: 1.5rem;
margin-bottom: 2rem;
page-break-inside: avoid;
}
.section-title {
font-size: 1.2rem;
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 1rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--border-gray);
color: var(--text-dark);
}
.summary-text {
line-height: 1.6;
text-align: justify;
font-size: 0.9rem;
font-size: 0.95rem;
color: var(--text-dark);
}
/* Experience - with separators */
/* Experience */
.experience-item {
margin-bottom: 1.2rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid var(--border-gray);
margin-bottom: 1.5rem;
page-break-inside: avoid;
}
.experience-item:last-child {
border-bottom: none;
.experience-header {
margin-bottom: 0.6rem;
}
.experience-header {
.experience-title-line {
display: flex;
justify-content: space-between;
margin-bottom: 0.6rem;
align-items: baseline;
gap: 1rem;
align-items: center;
}
.company-logo {
width: 35px;
height: 35px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.75rem;
}
.company-logo img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.experience-title {
flex: 1;
flex-wrap: wrap;
}
.position {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.2rem;
}
.company {
color: var(--text-gray);
font-size: 0.85rem;
margin: 0;
color: var(--text-dark);
}
.experience-period {
color: var(--text-gray);
font-size: 0.8rem;
white-space: nowrap;
font-size: 0.85rem;
font-style: italic;
}
.short-desc {
color: var(--text-gray);
font-size: 0.85rem;
line-height: 1.5;
margin-bottom: 0.6rem;
color: var(--text-dark);
font-size: 0.9rem;
line-height: 1.6;
margin-top: 0.5rem;
}
.responsibilities {
list-style: none;
margin-bottom: 0.6rem;
margin-top: 0.5rem;
padding-left: 0;
}
.responsibilities li {
padding-left: 1rem;
margin-bottom: 0.3rem;
padding-left: 1.2rem;
margin-bottom: 0.4rem;
position: relative;
font-size: 0.85rem;
font-size: 0.9rem;
color: var(--text-dark);
line-height: 1.5;
}
.responsibilities li:before {
content: "•";
position: absolute;
left: 0;
font-weight: bold;
}
.technologies {
font-size: 0.8rem;
color: var(--text-gray);
font-style: italic;
}
/* Education */
.education-item {
margin-bottom: 0.8rem;
font-size: 0.9rem;
}
.education-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.3rem;
}
.degree {
font-size: 0.95rem;
font-weight: 600;
}
.education-period {
color: var(--text-gray);
font-size: 0.8rem;
font-style: italic;
}
.institution {
color: var(--text-gray);
font-size: 0.85rem;
}
/* Skills */
.skill-block {
margin-bottom: 0.8rem;
}
.skill-title {
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.skill-list {
color: var(--text-gray);
font-size: 0.85rem;
}
/* Projects */
.project-item {
margin-bottom: 1rem;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-dark);
}
.project-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.3rem;
}
.project-name {
font-size: 1rem;
font-weight: 600;
}
.project-period {
color: var(--text-gray);
font-size: 0.8rem;
font-style: italic;
}
.project-role {
color: var(--text-gray);
font-size: 0.85rem;
margin-bottom: 0.3rem;
}
.project-description {
margin-bottom: 0.3rem;
font-size: 0.85rem;
}
/* Certifications & Awards */
.cert-item,
.award-item {
margin-bottom: 0.4rem;
font-size: 0.85rem;
}
/* Languages */
.languages-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.language-item {
font-size: 0.85rem;
font-size: 0.9rem;
color: var(--text-dark);
}
/* Footer */
@@ -417,30 +355,6 @@ footer {
font-size: 0.85rem;
}
/* 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 {
@@ -468,57 +382,47 @@ footer {
/* Responsive - tablet/mobile */
@media (max-width: 900px) {
.cv-paper {
width: 100%;
min-height: auto;
padding: 15mm;
grid-template-columns: 1fr;
box-shadow: none;
}
.cv-container {
padding: 1rem;
.cv-sidebar {
padding: 1.5rem 1rem;
}
.cv-main {
padding: 1.5rem 1rem;
}
.cv-name {
font-size: 1.8rem;
}
.cv-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.cv-photo {
order: -1;
margin-bottom: 1rem;
}
.cv-contact {
grid-template-columns: 1fr;
text-align: center;
}
.experience-header,
.project-header,
.education-header {
flex-direction: column;
gap: 0.25rem;
}
.company-logo {
display: none;
width: 120px;
height: 150px;
}
.action-bar-content {
flex-wrap: wrap;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
.language-toggle,
.title-badges,
.action-buttons {
justify-content: center;
}
.cv-length-toggle {
order: 1;
width: 100%;
justify-content: center;
margin-top: 0.5rem;
.title-badges {
order: -1;
}
.experience-title-line {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
}
+106 -139
View File
@@ -1,153 +1,120 @@
<!-- 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>
</div>
<div class="cv-contact">
<div class="contact-item">{{.CV.Personal.Location}}</div>
<div class="contact-item"><a href="mailto:{{.CV.Personal.Email}}">{{.CV.Personal.Email}}</a></div>
<div class="contact-item">{{.CV.Personal.Phone}}</div>
<div class="contact-item"><a href="{{.CV.Personal.LinkedIn}}" target="_blank">LinkedIn</a></div>
<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">
<h3 class="section-title">{{if eq .Lang "es"}}Resumen{{else}}Summary{{end}}</h3>
<p class="summary-text">{{.CV.Summary}}</p>
</section>
<!-- Experience -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia Laboral{{else}}Work History{{end}}</h3>
{{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>
</div>
<div class="experience-period">
{{.StartDate}} - {{if .Current}}{{if eq $.Lang "es"}}Presente{{else}}Present{{end}}{{else}}{{.EndDate}}{{end}}
</div>
</div>
{{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 long-only">
{{range $index, $tech := .Technologies}}{{if $index}}, {{end}}{{$tech}}{{end}}
<!-- Left Sidebar - Skills -->
<aside class="cv-sidebar">
<!-- Skills Section -->
<section class="sidebar-section">
<h3 class="sidebar-title">{{if eq .Lang "es"}}Lenguajes de Programación{{else}}Programming Languages{{end}}</h3>
{{range .CV.Skills.Technical}}
{{if eq .Category "Programming Languages"}}
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
</div>
{{end}}
</div>
{{end}}
</section>
{{end}}
</section>
<!-- Education -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Formación{{else}}Education{{end}}</h3>
{{range .CV.Education}}
<div class="education-item">
<div class="education-header">
<h4 class="degree">{{.Degree}}</h4>
<div class="education-period">{{.StartDate}} - {{.EndDate}}</div>
<section class="sidebar-section">
<h3 class="sidebar-title">{{if eq .Lang "es"}}Desarrollo Web{{else}}Web Development{{end}}</h3>
{{range .CV.Skills.Technical}}
{{if eq .Category "Web Development"}}
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
</div>
{{end}}
{{end}}
</section>
<section class="sidebar-section">
<h3 class="sidebar-title">{{if eq .Lang "es"}}Frameworks JavaScript{{else}}Javascript Frameworks{{end}}</h3>
{{range .CV.Skills.Technical}}
{{if eq .Category "JavaScript Frameworks"}}
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
</div>
{{end}}
{{end}}
</section>
</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-photo">
<img src="/static/images/profile/photo.jpg" alt="{{.CV.Personal.Name}}" onerror="this.src='/static/images/profile/placeholder.svg'">
</div>
<div>
<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>
</div>
</div>
<div class="institution">{{.Institution}}, {{.Location}}</div>
</div>
{{end}}
</section>
<!-- Skills -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</h3>
<!-- Summary -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Resumen{{else}}Training{{end}}</h3>
<p class="summary-text">{{.CV.Summary}}</p>
</section>
{{range .CV.Skills.Technical}}
<div class="skill-block">
<h4 class="skill-title">{{.Category}}</h4>
<p class="skill-list">
{{range $index, $item := .Items}}{{if $index}}, {{end}}{{$item}}{{end}}
<!-- 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>
</div>
{{end}}
</section>
</section>
<!-- Projects -->
{{if .CV.Projects}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Proyectos{{else}}Projects{{end}}</h3>
<!-- Experience -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</h3>
{{range .CV.Projects}}
<div class="project-item">
<div class="project-header">
<h4 class="project-name">{{.Name}}</h4>
<div class="project-period">{{.Period}}</div>
</div>
<div class="project-role">{{.Role}}</div>
<p class="project-description">{{.Description}}</p>
</div>
{{end}}
</section>
{{end}}
{{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>
<!-- Certifications -->
{{if .CV.Certifications}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Certificaciones{{else}}Certifications{{end}}</h3>
{{if .ShortDescription}}
<p class="short-desc">{{.ShortDescription}}</p>
{{end}}
{{range .CV.Certifications}}
<div class="cert-item">
<strong>{{.Name}}</strong> - {{.Issuer}} ({{.Date}})
</div>
{{end}}
</section>
{{end}}
<!-- Awards -->
{{if .CV.Awards}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Premios{{else}}Awards{{end}}</h3>
{{range .CV.Awards}}
<div class="award-item">
<strong>{{.Title}}</strong> - {{.Issuer}} ({{.Date}})
</div>
{{end}}
</section>
{{end}}
<!-- Languages -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
<div class="languages-list">
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}</strong>: {{.Proficiency}}
<div class="long-only">
<ul class="responsibilities">
{{range .Responsibilities}}
<li>{{.}}</li>
{{end}}
</ul>
</div>
</div>
{{end}}
</div>
</section>
</section>
<!-- Languages -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
<div class="languages-list">
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}</strong>: {{.Proficiency}}
</div>
{{end}}
</div>
</section>
</main>
+23 -16
View File
@@ -20,9 +20,10 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Language & Export Bar (hidden in print) -->
<!-- Single Black Bar with Everything -->
<div class="action-bar no-print">
<div class="action-bar-content">
<!-- Left: Language buttons -->
<div class="language-toggle">
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
@@ -30,7 +31,7 @@
hx-target="#cv-content"
hx-swap="innerHTML"
hx-indicator="#loading">
🇬🇧 English
English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
@@ -38,28 +39,34 @@
hx-target="#cv-content"
hx-swap="innerHTML"
hx-indicator="#loading">
🇪🇸 Español
Español
</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>
<!-- Center: Title badges -->
<div class="title-badges">
<span class="title-badge">ANALYST PROGRAMMER</span>
<span class="title-separator">|</span>
<span class="title-badge">NODEJS + REACTJS DEVELOPER</span>
<span class="title-separator">|</span>
<span class="title-badge">WEB DEVELOPER</span>
<span class="title-separator">|</span>
<span class="title-badge">JAVA DEVELOPER</span>
<span class="title-separator">|</span>
<span class="title-badge">PHP DEVELOPER</span>
</div>
<div class="export-actions">
<!-- Right: Action buttons -->
<div class="action-buttons">
<button
class="export-btn"
onclick="window.print()">
📄 {{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
📥 {{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}
</button>
<button
class="export-btn"
onclick="window.print()">
🖨️ {{if eq .Lang "es"}}Imprimir{{else}}Print Friendly{{end}}
</button>
</div>