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