From dab68f34f257e7238b7f8766af1e1382631d94dd Mon Sep 17 00:00:00 2001
From: juanatsap
` tag (after footer):** + +```html + +
+ + + +``` + +**File:** `/Users/txeo/Git/yo/cv/static/css/main.css` + +**Add at the end of the file:** + +```css +/* Error Toast */ +.error-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: #fee2e2; + color: #dc2626; + padding: 1rem 1.5rem; + border-radius: 8px; + border-left: 4px solid #dc2626; + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: 1rem; + max-width: 400px; + z-index: 1000; + animation: slideIn 0.2s ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.error-toast button { + background: none; + border: none; + font-size: 1.5rem; + color: #dc2626; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.2s; +} + +.error-toast button:hover { + opacity: 0.7; +} + +/* Smooth transitions */ +.cv-paper { + transition: opacity 200ms; +} + +.cv-paper.htmx-swapping { + opacity: 0; +} +``` + +--- + +### 4. HTMX Configuration (5 minutes) + +**File:** `/Users/txeo/Git/yo/cv/templates/index.html` + +**Add in `
` section after meta viewport:** + +```html + + +``` + +--- + +## ⏱️ 1-Hour Enhancement: SEO & Meta Tags + +**File:** `/Users/txeo/Git/yo/cv/templates/index.html` + +**Replace entire `
` section:** + +```html +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +``` + +--- + +## 🔒 2-Hour Enhancement: Security Headers + +**Create file:** `/Users/txeo/Git/yo/cv/middleware/security.go` + +```go +package middleware + +import ( + "net/http" + "os" +) + +// SecurityHeaders adds security headers to all responses +func SecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Prevent clickjacking + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-XSS-Protection", "1; mode=block") + + // Content Security Policy + csp := "default-src 'self'; " + + "script-src 'self' 'unsafe-inline' https://unpkg.com; " + + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + + "font-src 'self' https://fonts.gstatic.com; " + + "img-src 'self' data:; " + + "connect-src 'self'" + w.Header().Set("Content-Security-Policy", csp) + + // Referrer Policy + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Permissions Policy + w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") + + // HTTPS-only in production + if os.Getenv("GO_ENV") == "production" { + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + } + + next.ServeHTTP(w, r) + }) +} + +// CORS allows cross-origin requests (if needed) +func CORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := os.Getenv("ALLOWED_ORIGIN") + if origin == "" { + origin = "*" // Development only + } + + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} +``` + +**Update file:** `/Users/txeo/Git/yo/cv/main.go` + +**Add imports:** +```go +import ( + // ... existing imports + "yourproject/middleware" // Update with your module path +) +``` + +**Update main() function to use middleware:** +```go +func main() { + // ... existing setup code + + // Apply middleware + http.Handle("/", middleware.SecurityHeaders(http.HandlerFunc(handleHome))) + http.Handle("/cv", middleware.SecurityHeaders(http.HandlerFunc(handleCV))) + http.Handle("/export/pdf", middleware.SecurityHeaders(http.HandlerFunc(handlePDFExport))) + + // ... rest of main() +} +``` + +**Or create a middleware chain:** +```go +func main() { + // ... existing setup code + + // Create base handler + mux := http.NewServeMux() + mux.HandleFunc("/", handleHome) + mux.HandleFunc("/cv", handleCV) + mux.HandleFunc("/export/pdf", handlePDFExport) + + // Static files + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + // Apply middleware chain + handler := middleware.SecurityHeaders( + middleware.CORS(mux), + ) + + // ... start server with handler + log.Fatal(http.ListenAndServe(":8080", handler)) +} +``` + +--- + +## ✅ Testing Your Improvements + +### 1. Test Browser History +```bash +# Start server +go run main.go + +# Open browser, click language buttons +# Press browser back button - should work! +``` + +### 2. Test Error Handling +```bash +# Stop the server +# In browser, click language button +# Should see error toast! +``` + +### 3. Test Accessibility +```bash +# Use keyboard only: +# Tab to language buttons +# Press Enter to activate +# Tab to export button +# Press Enter to print +``` + +### 4. Test Security Headers +```bash +curl -I http://localhost:8080/ +# Should see security headers in response +``` + +--- + +## 📊 Before vs After + +### Before (Current) +- ❌ No browser history on language change +- ❌ No error handling +- ❌ Limited accessibility +- ⚠️ Missing SEO meta tags +- ⚠️ No security headers +- ✅ Excellent performance + +### After (30 minutes) +- ✅ Browser history works +- ✅ Error handling with toast +- ✅ ARIA attributes for accessibility +- ✅ Smooth transitions +- ✅ HTMX timeout configured +- ✅ Still excellent performance + +### After (2 hours) +- ✅ All of the above PLUS: +- ✅ Complete SEO meta tags +- ✅ Structured data (JSON-LD) +- ✅ Security headers +- ✅ SRI for external scripts +- ✅ Production-ready! + +--- + +## 🎯 Next Steps + +1. **Apply 30-minute fixes** ← Start here! +2. **Test in browser** +3. **Apply 1-hour SEO enhancements** +4. **Apply 2-hour security enhancements** +5. **Run Lighthouse audit** +6. **Deploy to production!** + +--- + +## 💡 Pro Tips + +1. **Backup first:** + ```bash + cp templates/index.html templates/index.html.backup + cp static/css/main.css static/css/main.css.backup + ``` + +2. **Test incrementally:** + - Apply one fix at a time + - Test in browser + - Commit to git + - Move to next fix + +3. **Use the enhanced templates:** + ```bash + # I've already created fully enhanced versions: + mv templates/index-improved.html templates/index.html + mv static/css/main-enhanced.css static/css/main.css + ``` + +4. **Validate with tools:** + - Lighthouse: `lighthouse http://localhost:8080` + - WAVE: Install browser extension + - axe DevTools: Install browser extension + +--- + +## 🚀 Ready to Go! + +These quick fixes will take you from **85% → 95% production-ready** in just 30 minutes! + +For the complete guide, see: `HTMX-PRODUCTION-RECOMMENDATIONS.md` diff --git a/README.md b/README.md new file mode 100644 index 0000000..02622d1 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# CV Site - Go + HTMX + +**Modern, minimal curriculum vitae website** for Juan Andrés Moreno Rubio built with **Go** and **HTMX**. + +## 🚀 Features + +- ✅ **Bilingual Support** - Spanish and English with instant switching (no page reload) +- ✅ **PDF Export** - Print-optimized design for PDF generation via browser +- ✅ **HTMX Dynamic Updates** - Smooth UX without heavy JavaScript +- ✅ **Paper Design** - Professional CV on elegant white paper with gray background +- ✅ **Responsive** - Mobile, tablet, and desktop friendly +- ✅ **JSON-Based Content** - Easy to update without touching code +- ✅ **AI Development Section** - Showcases modern AI-assisted development skills +- ✅ **Fast & Lightweight** - Go backend, minimal dependencies + +## 📋 Quick Start + +### Prerequisites + +- **Go 1.21+** installed + +### Run + +\`\`\`bash +# Build and run +go build -o cv-server && ./cv-server +\`\`\` + +Open **http://localhost:8080** + +- 🇬🇧 English: http://localhost:8080/?lang=en +- 🇪🇸 Spanish: http://localhost:8080/?lang=es + +## 📄 Updating Your CV + +Edit JSON files in `data/`: +- **English**: `data/cv-en.json` +- **Spanish**: `data/cv-es.json` + +No code changes needed - just refresh browser! + +## 🖨️ Export to PDF + +1. Click **"Download PDF"** button +2. Use browser print (Cmd/Ctrl + P) +3. Save as PDF + +## 🎯 Key Technologies + +- Backend: **Go** (stdlib net/http) +- Frontend: **HTMX** 1.9.10 +- Styling: Custom **CSS** +- Data: **JSON** files + +--- + +**Built with ❤️ using Go, HTMX, and AI assistance** diff --git a/data/cv-en.json b/data/cv-en.json new file mode 100644 index 0000000..ab9a73e --- /dev/null +++ b/data/cv-en.json @@ -0,0 +1,550 @@ +{ + "personal": { + "name": "Juan Andrés Moreno Rubio", + "title": "Lead Technical Consultant, FullStack Developer", + "location": "Arrecife, Las Palmas de Gran Canaria, Spain", + "email": "txeo.msx@gmail.com", + "phone": "+34 676875420", + "dateOfBirth": "1980-03-02", + "placeOfBirth": "Plasencia (Cáceres), Spain", + "citizenship": "Spanish", + "linkedin": "https://www.linkedin.com/in/juan-andres-moreno-rubio", + "github": "https://github.com/juanatsap", + "behance": "https://www.behance.net/txeo", + "website": "https://juan.andres.morenoyrubio.com", + "photo": "/static/images/profile.jpg" + }, + "summary": "Technical Consultant, Fullstack Developer, and AI enthusiast with 18 years of experience in the IT industry, specializing in SAP Customer Data Cloud, web technologies (mainly React and Node ecosystem), and AI integrations. Proven track record of leading technical projects and providing guidance to over 35 international clients. Seeking opportunities to apply and expand my skills in a challenging and rewarding environment.", + "experience": [ + { + "position": "Senior SAP Technical Consultant", + "company": "Olympic Broadcasting Services", + "location": "Madrid, Spain", + "startDate": "2021-01", + "endDate": "present", + "current": true, + "responsibilities": [ + "Assessed business requirements to create focused solutions, mainly with SAP Customer Data Cloud (CDC)", + "Custom implementations and data treatment for international broadcasting events", + "Meetings, guidance and troubleshooting for technical teams", + "Led integration of SAP CDC across multiple Olympic event platforms" + ], + "technologies": [ + "SAP CDC", + "JavaScript", + "React", + "Node.js", + "API Integration" + ] + }, + { + "position": "Senior Technical Consultant", + "company": "AENA (via Accenture Spain)", + "location": "Madrid, Spain", + "startDate": "2021-10", + "endDate": "2023-07", + "current": false, + "responsibilities": [ + "Analyzed client business processes to propose optimal software applications for unique requirements", + "Provided expertise for SAP Customer Data Cloud and integrated the product into AENA ecosystem", + "Enhanced interfaces to promote better functionality for users across all Spanish airports", + "Developed diagrams to describe and lay out logical operational steps", + "Developed software for web and mobile operating systems", + "Lead Technical Consultant & Main Developer for AENA Airports Authentication System (https://usuarios.aena.es)", + "Implemented identity user-related flows for main websites and apps serving millions of passengers" + ], + "technologies": [ + "SAP CDC", + "React", + "Node.js", + "API Development", + "Authentication Systems", + "Mobile Development" + ], + "highlights": [ + "Successfully deployed authentication system for all AENA airports in Spain", + "Managed identity flows for millions of users across web and mobile platforms" + ] + }, + { + "position": "Senior Technical Consultant", + "company": "SAP", + "location": "Barcelona, Spain", + "startDate": "2019-10", + "endDate": "2021-10", + "current": false, + "responsibilities": [ + "Analyzed client business processes to propose optimal software applications for unique requirements", + "Assessed business requirements to create focused solutions", + "Troubleshot incidents reported by end-users to schedule system changes and identify permanent solutions", + "Educated stakeholders on data protection tactics to reduce breaches (GDPR compliance)", + "Offered input for complex documents to support client-ready final versions", + "Provided technical consulting for SAP Customer Data Cloud implementations" + ], + "technologies": [ + "SAP CDC", + "GDPR Compliance", + "JavaScript", + "Cloud Platforms", + "Technical Documentation" + ] + }, + { + "position": "Junior Technical Consultant", + "company": "Gigya", + "location": "Barcelona, Spain", + "startDate": "2017-10", + "endDate": "2019-10", + "current": false, + "responsibilities": [ + "Responded to customer inquiries and provided technical assistance over the phone and in person", + "Monitored system performance to identify potential issues", + "Offered assistance in implementing and developing training programs", + "Researched and identified solutions to technical problems", + "Collaborated with vendors to locate replacement components and resolve advanced problems", + "Assisted in the development of system security protocols" + ], + "technologies": [ + "Gigya Platform", + "JavaScript", + "Customer Support", + "System Monitoring" + ] + }, + { + "position": "Fullstack Developer", + "company": "Megabanner", + "location": "Barcelona, Spain", + "startDate": "2016-12", + "endDate": "2017-08", + "current": false, + "responsibilities": [ + "Rapidly prototyped new data processing capabilities to confirm integration feasibility into existing systems", + "Integrated with a video system for the inclusion of advertisements into gas station networks", + "Built databases and table structures for web applications", + "Translated technical concepts and information into terms parties could easily comprehend" + ], + "technologies": [ + "React", + "Node.js", + "Video Processing", + "Database Design", + "PostgreSQL" + ] + }, + { + "position": "Fullstack Developer", + "company": "Ebantic", + "location": "Barcelona, Spain", + "startDate": "2016-09", + "endDate": "2017-04", + "current": false, + "responsibilities": [ + "Worked with back-end developers to design APIs", + "Oversaw and implemented automated build and deployment pipelines", + "Analyzed existing software implementations to identify areas requiring improvement", + "Tested functional compliance of company products", + "Tested and deployed scalable and highly available software products" + ], + "technologies": [ + "React", + "Node.js", + "API Design", + "CI/CD", + "DevOps" + ] + }, + { + "position": "FullStack Developer", + "company": "Everis", + "location": "Barcelona, Spain", + "startDate": "2016-04", + "endDate": "2016-11", + "current": false, + "responsibilities": [ + "Created two React applications for two different clients", + "Implemented modern frontend architectures with React ecosystem" + ], + "technologies": [ + "React", + "JavaScript", + "Redux", + "Webpack" + ] + }, + { + "position": "Fullstack Developer", + "company": "Indra", + "location": "Barcelona, Spain", + "startDate": "2015-09", + "endDate": "2016-02", + "current": false, + "responsibilities": [ + "Discussed project progress with customers, collected feedback on different stages", + "Directly addressed customer concerns and implemented solutions" + ], + "technologies": [ + "Java", + "JavaScript", + "Web Development" + ] + }, + { + "position": "Technical Director / Programmer", + "company": "Emailing Network S.R.L.", + "location": "Barcelona, Spain", + "startDate": "2012-11", + "endDate": "2015-06", + "current": false, + "responsibilities": [ + "Development of a backend and 5 satellite websites to allow online sales and email marketing communications", + "Guided, coached and led project teams, delegating tasks and evaluating performance", + "Oversaw product pipeline development, reducing production times by 75%", + "Collaborated with leadership staff to determine appropriate budgets" + ], + "technologies": [ + "PHP", + "MySQL", + "JavaScript", + "Email Marketing Systems", + "E-commerce" + ], + "highlights": [ + "Reduced production times by 75% through optimized pipelines", + "Successfully managed technical team and product development" + ] + }, + { + "position": "Programmer Analyst (Freelance)", + "company": "TwenTiC + ALTEN", + "location": "Barcelona, Spain", + "startDate": "2012-05", + "endDate": "2012-10", + "current": false, + "responsibilities": [ + "Construction of several websites using WordPress and PHP", + "Custom theme and plugin development" + ], + "technologies": [ + "WordPress", + "PHP", + "MySQL", + "JavaScript" + ] + }, + { + "position": "Analyst Programmer / Expert Technician", + "company": "Penta MSI", + "location": "Barcelona, Spain", + "startDate": "2010-10", + "endDate": "2011-11", + "current": false, + "responsibilities": [ + "Configured and tested new software and hardware", + "Researched and identified solutions to technical problems", + "Mentored new co-workers" + ], + "technologies": [ + "Java", + "System Configuration", + "Technical Support" + ] + }, + { + "position": "Senior Programmer", + "company": "Homeria + WebRatio S.R.L.", + "location": "Cáceres (Spain) / Como (Italy)", + "startDate": "2008-01", + "endDate": "2008-12", + "current": false, + "responsibilities": [ + "Worked on a European project in a revolutionary search engine", + "Skilled at working independently and collaboratively in a team environment", + "Learned and adapted quickly to new technology and software applications" + ], + "technologies": [ + "Java", + "Search Engine Technology", + "European R&D Projects" + ] + }, + { + "position": "Junior Programmer", + "company": "Insa", + "location": "Cáceres, Spain", + "startDate": "2006-09", + "endDate": "2008-01", + "current": false, + "responsibilities": [ + "Wrote applications in JAVA architecture for various industries, being specialized in data chart generation", + "Developed 3 different types of JAVA applets", + "Debugged and modified JAVA software components" + ], + "technologies": [ + "Java", + "Java Applets", + "Data Visualization", + "Chart Generation" + ] + } + ], + "education": [ + { + "degree": "Computing Engineering, Bachelor's Degree", + "institution": "Universidad de Extremadura", + "location": "Cáceres, Spain", + "startDate": "1999-09", + "endDate": "2009-02", + "field": "Computer Science and Engineering" + } + ], + "skills": { + "technical": [ + { + "category": "AI & Modern Development", + "proficiency": 5, + "items": [ + "AI-Assisted Development (Claude Code, Copilot, GPT-4)", + "Prompt Engineering & AI Workflows", + "HTMX (Hypermedia Applications)", + "Tailwind CSS", + "Go (Golang)", + "OpenAI & Anthropic APIs" + ] + }, + { + "category": "JavaScript Ecosystem", + "proficiency": 5, + "items": [ + "Advanced JavaScript (ES6+)", + "React & React Ecosystem", + "Node.js & Express", + "Webpack, Vite, Modern Build Tools" + ] + }, + { + "category": "Web Development", + "proficiency": 5, + "items": [ + "HTML5, CSS3, Semantic Web", + "REST API Design & Development", + "LESS, SASS, CSS Preprocessors", + "Responsive & Mobile-First Design" + ] + }, + { + "category": "Backend Technologies", + "proficiency": 4, + "items": [ + "Node.js (Express, Modern frameworks)", + "Go (Golang)", + "Java & J2EE", + "Spring Framework, Struts, Hibernate", + "PHP" + ] + }, + { + "category": "Databases", + "proficiency": 4, + "items": [ + "PostgreSQL", + "MySQL", + "Oracle", + "MongoDB (NoSQL)", + "Database Design & Optimization" + ] + }, + { + "category": "SAP Technologies", + "proficiency": 5, + "items": [ + "SAP Customer Data Cloud (CDC)", + "SAP Cloud Platform", + "GDPR Compliance & Data Protection" + ] + }, + { + "category": "DevOps & Tools", + "proficiency": 4, + "items": [ + "Git (Version Control)", + "CI/CD Pipelines", + "Docker", + "Automated Testing", + "Agile Methodologies" + ] + } + ], + "soft_skills": [ + "Leadership & Team Management", + "Technical Documentation", + "Problem-Solving & Critical Thinking", + "Business Consulting", + "On-Site Technical Support", + "Training & Mentoring", + "Client Relationship Management", + "Flexibility & Adaptability", + "Marketing & Resource Management" + ] + }, + "languages": [ + { + "language": "Spanish", + "proficiency": "Native", + "level": 5 + }, + { + "language": "English", + "proficiency": "Professional Working Proficiency", + "level": 4 + }, + { + "language": "Italian", + "proficiency": "Intermediate", + "level": 3 + } + ], + "projects": [ + { + "name": "AENA Airports Authentication System", + "role": "Lead Technical Consultant & Main Developer", + "url": "https://usuarios.aena.es", + "period": "2021-2023", + "description": "Complete authentication and identity management system for all AENA airports in Spain. Handles millions of users across web and mobile platforms.", + "technologies": [ + "SAP CDC", + "React", + "Node.js", + "Authentication", + "Mobile" + ], + "highlights": [ + "Deployed across all Spanish airports", + "Handles millions of user authentications", + "Integrated with multiple AENA digital platforms" + ] + }, + { + "name": "SAP Customer Data Cloud Starter Kit", + "role": "Main Contributor", + "url": "https://github.com/gigya/cdc-starter-kit", + "period": "2019-2021", + "description": "Simple front-end template for building fast, robust, and adaptable web apps or sites, including SAP CDC capabilities. Open-source contribution.", + "technologies": [ + "SAP CDC", + "React", + "JavaScript", + "Template Development" + ], + "highlights": [ + "Open-source contribution to SAP ecosystem", + "Used by developers worldwide", + "Simplifies SAP CDC integration" + ] + }, + { + "name": "AI-Powered Development Workflows", + "role": "Independent Research & Development", + "period": "2023 - Present", + "description": "Pioneered AI-assisted development workflows using Claude Code and modern tools. Successfully experimented with migrating projects from React to HTMX+Go architecture, reducing complexity while maintaining functionality.", + "technologies": [ + "Claude Code", + "HTMX", + "Go", + "Tailwind CSS", + "AI APIs", + "Prompt Engineering" + ], + "highlights": [ + "Reduced development time by 60% using AI-assisted workflows", + "Modernized legacy applications with AI guidance", + "Created reusable patterns for HTMX + Go development" + ] + }, + { + "name": "React & Node.js Projects", + "role": "Technical Lead & Developer", + "period": "2015-2017", + "description": "Multiple projects for clients including Megabanner, Cepsa, Cazatucasa", + "technologies": [ + "React", + "Node.js", + "JavaScript", + "API Development" + ] + }, + { + "name": "Java Enterprise Projects", + "role": "Technical Lead & Developer", + "period": "2008-2015", + "description": "Enterprise applications including Portic.net Regular Lines, III and IV Awards of Music in Extremadura", + "technologies": [ + "Java", + "J2EE", + "Spring", + "Hibernate" + ] + }, + { + "name": "PHP & WordPress Projects", + "role": "Web Developer", + "period": "2012-2015", + "description": "Multiple web projects including Oferting, Emailing Network, Coupon&Go, Clicplan, Lidering, Delivery Bikes BCN, Jorpack, Gourmet Bus, Moreno y Rubio, Mobbeel, Las Peruchas", + "technologies": [ + "PHP", + "WordPress", + "MySQL", + "JavaScript" + ] + } + ], + "awards": [ + { + "title": "Best Comparison Service with Clicplan", + "issuer": "eAwards", + "date": "2013-05", + "description": "Recognition for excellence in comparison service development" + }, + { + "title": "Project Construction Scholarship for drolosoft", + "issuer": "Junta de Extremadura", + "date": "2009-08", + "description": "Scholarship for innovative software project development" + }, + { + "title": "Scholarship to work at TESEO Software Factory", + "issuer": "Universidad de Extremadura", + "date": "2004-04", + "description": "Academic scholarship for software development work" + } + ], + "certifications": [ + { + "name": "SAP CDC Full Training", + "issuer": "SAP", + "date": "2019-05", + "description": "Complete training on SAP Customer Data Cloud platform" + }, + { + "name": "SAP Cloud Platform Learning Program", + "issuer": "SAP", + "date": "2019-02", + "description": "Comprehensive SAP Cloud Platform certification" + }, + { + "name": "GDPR Compliance and Regulations Training", + "issuer": "Gigya", + "date": "2018-03", + "description": "Data protection and GDPR compliance certification" + } + ], + "other": { + "driverLicense": "Type C" + }, + "meta": { + "version": "2024", + "lastUpdated": "2024-10-18", + "format": "JSON Resume Extended", + "language": "en" + } +} \ No newline at end of file diff --git a/data/cv-es.json b/data/cv-es.json new file mode 100644 index 0000000..fe8f47a --- /dev/null +++ b/data/cv-es.json @@ -0,0 +1,550 @@ +{ + "personal": { + "name": "Juan Andrés Moreno Rubio", + "title": "Consultor Técnico Senior, Desarrollador FullStack", + "location": "Arrecife, Las Palmas de Gran Canaria, España", + "email": "txeo.msx@gmail.com", + "phone": "+34 676875420", + "dateOfBirth": "1980-03-02", + "placeOfBirth": "Plasencia (Cáceres), España", + "citizenship": "Española", + "linkedin": "https://www.linkedin.com/in/juan-andres-moreno-rubio", + "github": "https://github.com/juanatsap", + "behance": "https://www.behance.net/txeo", + "website": "https://juan.andres.morenoyrubio.com", + "photo": "/static/images/profile.jpg" + }, + "summary": "Consultor Técnico, Desarrollador Fullstack, y entusiasta de IA con 18 años de experiencia en la industria IT, especializado en SAP Customer Data Cloud, tecnologías web (principalmente React y el ecosistema Node), e integraciones de IA. Historial comprobado liderando proyectos técnicos y proporcionando orientación a más de 35 clientes internacionales. Buscando oportunidades para aplicar y expandir mis habilidades en un entorno desafiante y gratificante.", + "experience": [ + { + "position": "Consultor Técnico Senior SAP", + "company": "Olympic Broadcasting Services", + "location": "Madrid, España", + "startDate": "2021-01", + "endDate": "presente", + "current": true, + "responsibilities": [ + "Evaluación de requisitos de negocio para crear soluciones enfocadas, principalmente con SAP Customer Data Cloud (CDC)", + "Implementaciones personalizadas y tratamiento de datos para eventos de transmisión internacional", + "Reuniones, orientación y resolución de problemas para equipos técnicos", + "Lideré la integración de SAP CDC en múltiples plataformas de eventos olímpicos" + ], + "technologies": [ + "SAP CDC", + "JavaScript", + "React", + "Node.js", + "Integración de APIs" + ] + }, + { + "position": "Consultor Técnico Senior", + "company": "AENA (vía Accenture Spain)", + "location": "Madrid, España", + "startDate": "2021-10", + "endDate": "2023-07", + "current": false, + "responsibilities": [ + "Analicé procesos de negocio del cliente para proponer aplicaciones de software óptimas para requisitos únicos", + "Proporcioné experiencia en SAP Customer Data Cloud e integré el producto en el ecosistema AENA", + "Mejoré interfaces para promover mejor funcionalidad para usuarios en todos los aeropuertos españoles", + "Desarrollé diagramas para describir y detallar pasos operacionales lógicos", + "Desarrollé software para sistemas operativos web y móviles", + "Consultor Técnico Principal y Desarrollador Principal del Sistema de Autenticación de Aeropuertos AENA (https://usuarios.aena.es)", + "Implementé flujos relacionados con identidad de usuarios para sitios web y aplicaciones principales que sirven a millones de pasajeros" + ], + "technologies": [ + "SAP CDC", + "React", + "Node.js", + "Desarrollo de APIs", + "Sistemas de Autenticación", + "Desarrollo Móvil" + ], + "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" + ] + }, + { + "position": "Consultor Técnico Senior", + "company": "SAP", + "location": "Barcelona, España", + "startDate": "2019-10", + "endDate": "2021-10", + "current": false, + "responsibilities": [ + "Analicé procesos de negocio del cliente para proponer aplicaciones de software óptimas para requisitos únicos", + "Evalué requisitos de negocio para crear soluciones enfocadas", + "Resolví incidentes reportados por usuarios finales para programar cambios de sistema e identificar soluciones permanentes", + "Eduqué a las partes interesadas sobre tácticas de protección de datos para reducir brechas (cumplimiento GDPR)", + "Ofrecí aporte para documentos complejos para apoyar versiones finales listas para clientes", + "Proporcioné consultoría técnica para implementaciones de SAP Customer Data Cloud" + ], + "technologies": [ + "SAP CDC", + "Cumplimiento GDPR", + "JavaScript", + "Plataformas Cloud", + "Documentación Técnica" + ] + }, + { + "position": "Consultor Técnico Junior", + "company": "Gigya", + "location": "Barcelona, España", + "startDate": "2017-10", + "endDate": "2019-10", + "current": false, + "responsibilities": [ + "Respondí a consultas de clientes y proporcioné asistencia técnica por teléfono y en persona", + "Monitoricé el rendimiento del sistema para identificar problemas potenciales", + "Ofrecí asistencia en la implementación y desarrollo de programas de formación", + "Investigué e identifiqué soluciones a problemas técnicos", + "Colaboré con proveedores para localizar componentes de reemplazo y resolver problemas avanzados", + "Asistí en el desarrollo de protocolos de seguridad del sistema" + ], + "technologies": [ + "Plataforma Gigya", + "JavaScript", + "Soporte al Cliente", + "Monitoreo de Sistemas" + ] + }, + { + "position": "Desarrollador Fullstack", + "company": "Megabanner", + "location": "Barcelona, España", + "startDate": "2016-12", + "endDate": "2017-08", + "current": false, + "responsibilities": [ + "Prototipé rápidamente nuevas capacidades de procesamiento de datos para confirmar viabilidad de integración en sistemas existentes", + "Integré con un sistema de video para la inclusión de anuncios en redes de estaciones de servicio", + "Construí bases de datos y estructuras de tablas para aplicaciones web", + "Traduje conceptos técnicos e información en términos que las partes pudieran comprender fácilmente" + ], + "technologies": [ + "React", + "Node.js", + "Procesamiento de Video", + "Diseño de Bases de Datos", + "PostgreSQL" + ] + }, + { + "position": "Desarrollador Fullstack", + "company": "Ebantic", + "location": "Barcelona, España", + "startDate": "2016-09", + "endDate": "2017-04", + "current": false, + "responsibilities": [ + "Trabajé con desarrolladores back-end para diseñar APIs", + "Supervisé e implementé pipelines de construcción y despliegue automatizados", + "Analicé implementaciones de software existentes para identificar áreas que requieren mejora", + "Probé el cumplimiento funcional de productos de la empresa", + "Probé y desplegué productos de software escalables y altamente disponibles" + ], + "technologies": [ + "React", + "Node.js", + "Diseño de APIs", + "CI/CD", + "DevOps" + ] + }, + { + "position": "Desarrollador FullStack", + "company": "Everis", + "location": "Barcelona, España", + "startDate": "2016-04", + "endDate": "2016-11", + "current": false, + "responsibilities": [ + "Creé dos aplicaciones React para dos clientes diferentes", + "Implementé arquitecturas frontend modernas con el ecosistema React" + ], + "technologies": [ + "React", + "JavaScript", + "Redux", + "Webpack" + ] + }, + { + "position": "Desarrollador Fullstack", + "company": "Indra", + "location": "Barcelona, España", + "startDate": "2015-09", + "endDate": "2016-02", + "current": false, + "responsibilities": [ + "Discutí el progreso del proyecto con clientes, recopilé comentarios en diferentes etapas", + "Abordé directamente las preocupaciones del cliente e implementé soluciones" + ], + "technologies": [ + "Java", + "JavaScript", + "Desarrollo Web" + ] + }, + { + "position": "Director Técnico / Programador", + "company": "Emailing Network S.R.L.", + "location": "Barcelona, España", + "startDate": "2012-11", + "endDate": "2015-06", + "current": false, + "responsibilities": [ + "Desarrollo de un backend y 5 sitios web satélite para permitir ventas en línea y comunicaciones de email marketing", + "Guié, asesoré y lideré equipos de proyecto, delegando tareas y evaluando el rendimiento", + "Supervisé el desarrollo de pipeline de productos, reduciendo los tiempos de producción en un 75%", + "Colaboré con el personal de liderazgo para determinar presupuestos apropiados" + ], + "technologies": [ + "PHP", + "MySQL", + "JavaScript", + "Sistemas de Email Marketing", + "E-commerce" + ], + "highlights": [ + "Reducción del 75% en tiempos de producción mediante pipelines optimizados", + "Gestión exitosa de equipo técnico y desarrollo de productos" + ] + }, + { + "position": "Analista Programador (Freelance)", + "company": "TwenTiC + ALTEN", + "location": "Barcelona, España", + "startDate": "2012-05", + "endDate": "2012-10", + "current": false, + "responsibilities": [ + "Construcción de varios sitios web usando WordPress y PHP", + "Desarrollo de temas y plugins personalizados" + ], + "technologies": [ + "WordPress", + "PHP", + "MySQL", + "JavaScript" + ] + }, + { + "position": "Analista Programador / Técnico Experto", + "company": "Penta MSI", + "location": "Barcelona, España", + "startDate": "2010-10", + "endDate": "2011-11", + "current": false, + "responsibilities": [ + "Configuré y probé nuevo software y hardware", + "Investigué e identifiqué soluciones a problemas técnicos", + "Asesoré a nuevos compañeros de trabajo" + ], + "technologies": [ + "Java", + "Configuración de Sistemas", + "Soporte Técnico" + ] + }, + { + "position": "Programador Senior", + "company": "Homeria + WebRatio S.R.L.", + "location": "Cáceres (España) / Como (Italia)", + "startDate": "2008-01", + "endDate": "2008-12", + "current": false, + "responsibilities": [ + "Trabajé en un proyecto europeo en un motor de búsqueda revolucionario", + "Habilidad para trabajar independientemente y colaborativamente en un entorno de equipo", + "Aprendí y me adapté rápidamente a nuevas tecnologías y aplicaciones de software" + ], + "technologies": [ + "Java", + "Tecnología de Motores de Búsqueda", + "Proyectos Europeos I+D" + ] + }, + { + "position": "Programador Junior", + "company": "Insa", + "location": "Cáceres, España", + "startDate": "2006-09", + "endDate": "2008-01", + "current": false, + "responsibilities": [ + "Escribí aplicaciones en arquitectura JAVA para varias industrias, especializándome en generación de gráficos de datos", + "Desarrollé 3 tipos diferentes de applets JAVA", + "Depuré y modifiqué componentes de software JAVA" + ], + "technologies": [ + "Java", + "Applets Java", + "Visualización de Datos", + "Generación de Gráficos" + ] + } + ], + "education": [ + { + "degree": "Ingeniería Informática, Grado", + "institution": "Universidad de Extremadura", + "location": "Cáceres, España", + "startDate": "1999-09", + "endDate": "2009-02", + "field": "Ciencias de la Computación e Ingeniería" + } + ], + "skills": { + "technical": [ + { + "category": "IA y Desarrollo Moderno", + "proficiency": 5, + "items": [ + "Desarrollo Asistido por IA (Claude Code, Copilot, GPT-4)", + "Ingeniería de Prompts y Flujos de Trabajo con IA", + "HTMX (Aplicaciones Hipermedia)", + "Tailwind CSS", + "Go (Golang)", + "APIs OpenAI y Anthropic" + ] + }, + { + "category": "Ecosistema JavaScript", + "proficiency": 5, + "items": [ + "JavaScript Avanzado (ES6+)", + "React y Ecosistema React", + "Node.js y Express", + "Webpack, Vite, Herramientas de Build Modernas" + ] + }, + { + "category": "Desarrollo Web", + "proficiency": 5, + "items": [ + "HTML5, CSS3, Web Semántica", + "Diseño y Desarrollo de APIs REST", + "LESS, SASS, Preprocesadores CSS", + "Diseño Responsive y Mobile-First" + ] + }, + { + "category": "Tecnologías Backend", + "proficiency": 4, + "items": [ + "Node.js (Express, frameworks modernos)", + "Go (Golang)", + "Java y J2EE", + "Spring Framework, Struts, Hibernate", + "PHP" + ] + }, + { + "category": "Bases de Datos", + "proficiency": 4, + "items": [ + "PostgreSQL", + "MySQL", + "Oracle", + "MongoDB (NoSQL)", + "Diseño y Optimización de Bases de Datos" + ] + }, + { + "category": "Tecnologías SAP", + "proficiency": 5, + "items": [ + "SAP Customer Data Cloud (CDC)", + "SAP Cloud Platform", + "Cumplimiento GDPR y Protección de Datos" + ] + }, + { + "category": "DevOps y Herramientas", + "proficiency": 4, + "items": [ + "Git (Control de Versiones)", + "Pipelines CI/CD", + "Docker", + "Testing Automatizado", + "Metodologías Ágiles" + ] + } + ], + "soft_skills": [ + "Liderazgo y Gestión de Equipos", + "Documentación Técnica", + "Resolución de Problemas y Pensamiento Crítico", + "Consultoría de Negocio", + "Soporte Técnico On-Site", + "Formación y Mentoría", + "Gestión de Relaciones con Clientes", + "Flexibilidad y Adaptabilidad", + "Marketing y Gestión de Recursos" + ] + }, + "languages": [ + { + "language": "Español", + "proficiency": "Nativo", + "level": 5 + }, + { + "language": "Inglés", + "proficiency": "Profesional Avanzado", + "level": 4 + }, + { + "language": "Italiano", + "proficiency": "Intermedio", + "level": 3 + } + ], + "projects": [ + { + "name": "Sistema de Autenticación de Aeropuertos AENA", + "role": "Consultor Técnico Principal y Desarrollador Principal", + "url": "https://usuarios.aena.es", + "period": "2021-2023", + "description": "Sistema completo de autenticación y gestión de identidad para todos los aeropuertos AENA en España. Gestiona millones de usuarios en plataformas web y móviles.", + "technologies": [ + "SAP CDC", + "React", + "Node.js", + "Autenticación", + "Móvil" + ], + "highlights": [ + "Desplegado en todos los aeropuertos españoles", + "Gestiona millones de autenticaciones de usuarios", + "Integrado con múltiples plataformas digitales AENA" + ] + }, + { + "name": "SAP Customer Data Cloud Starter Kit", + "role": "Contribuidor Principal", + "url": "https://github.com/gigya/cdc-starter-kit", + "period": "2019-2021", + "description": "Plantilla front-end simple para construir aplicaciones o sitios web rápidos, robustos y adaptables, incluyendo capacidades SAP CDC. Contribución de código abierto.", + "technologies": [ + "SAP CDC", + "React", + "JavaScript", + "Desarrollo de Plantillas" + ], + "highlights": [ + "Contribución de código abierto al ecosistema SAP", + "Usado por desarrolladores en todo el mundo", + "Simplifica la integración de SAP CDC" + ] + }, + { + "name": "Flujos de Trabajo de Desarrollo Potenciados por IA", + "role": "Investigación y Desarrollo Independiente", + "period": "2023 - Presente", + "description": "Desarrollo pionero de flujos de trabajo asistidos por IA usando Claude Code y herramientas modernas. Experimentación exitosa con migración de proyectos de arquitectura React a HTMX+Go, reduciendo complejidad mientras se mantiene funcionalidad.", + "technologies": [ + "Claude Code", + "HTMX", + "Go", + "Tailwind CSS", + "APIs IA", + "Ingeniería de Prompts" + ], + "highlights": [ + "Reducción del 60% en tiempo de desarrollo usando flujos de trabajo asistidos por IA", + "Modernización de aplicaciones legacy con guía de IA", + "Creación de patrones reutilizables para desarrollo HTMX + Go" + ] + }, + { + "name": "Proyectos React y Node.js", + "role": "Líder Técnico y Desarrollador", + "period": "2015-2017", + "description": "Múltiples proyectos para clientes incluyendo Megabanner, Cepsa, Cazatucasa", + "technologies": [ + "React", + "Node.js", + "JavaScript", + "Desarrollo de APIs" + ] + }, + { + "name": "Proyectos Java Enterprise", + "role": "Líder Técnico y Desarrollador", + "period": "2008-2015", + "description": "Aplicaciones empresariales incluyendo Portic.net Regular Lines, III y IV Premios de Música en Extremadura", + "technologies": [ + "Java", + "J2EE", + "Spring", + "Hibernate" + ] + }, + { + "name": "Proyectos PHP y WordPress", + "role": "Desarrollador Web", + "period": "2012-2015", + "description": "Múltiples proyectos web incluyendo Oferting, Emailing Network, Coupon&Go, Clicplan, Lidering, Delivery Bikes BCN, Jorpack, Gourmet Bus, Moreno y Rubio, Mobbeel, Las Peruchas", + "technologies": [ + "PHP", + "WordPress", + "MySQL", + "JavaScript" + ] + } + ], + "awards": [ + { + "title": "Mejor Servicio de Comparación con Clicplan", + "issuer": "eAwards", + "date": "2013-05", + "description": "Reconocimiento por excelencia en desarrollo de servicio de comparación" + }, + { + "title": "Beca de Construcción de Proyecto para drolosoft", + "issuer": "Junta de Extremadura", + "date": "2009-08", + "description": "Beca para desarrollo de proyecto de software innovador" + }, + { + "title": "Beca para trabajar en Fábrica de Software TESEO", + "issuer": "Universidad de Extremadura", + "date": "2004-04", + "description": "Beca académica para trabajo de desarrollo de software" + } + ], + "certifications": [ + { + "name": "SAP CDC Full Training", + "issuer": "SAP", + "date": "2019-05", + "description": "Formación completa en plataforma SAP Customer Data Cloud" + }, + { + "name": "SAP Cloud Platform Learning Program", + "issuer": "SAP", + "date": "2019-02", + "description": "Certificación integral de SAP Cloud Platform" + }, + { + "name": "GDPR Compliance and Regulations Training", + "issuer": "Gigya", + "date": "2018-03", + "description": "Certificación de protección de datos y cumplimiento GDPR" + } + ], + "other": { + "driverLicense": "Tipo C" + }, + "meta": { + "version": "2024", + "lastUpdated": "2024-10-18", + "format": "JSON Resume Extended", + "language": "es" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8e80479 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/juanatsap/cv-site + +go 1.25.1 diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e16ebb6 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,89 @@ +package config + +import ( + "fmt" + "os" + "strconv" +) + +// Config holds all application configuration +type Config struct { + Server ServerConfig + Template TemplateConfig + Data DataConfig +} + +// ServerConfig contains server-specific settings +type ServerConfig struct { + Port string + Host string + ReadTimeout int + WriteTimeout int +} + +// TemplateConfig contains template-specific settings +type TemplateConfig struct { + Dir string + PartialsDir string + HotReload bool +} + +// DataConfig contains data directory settings +type DataConfig struct { + Dir string +} + +// Load creates a new Config with values from environment or defaults +func Load() *Config { + return &Config{ + Server: ServerConfig{ + Port: getEnv("PORT", "8080"), + Host: getEnv("HOST", "localhost"), + ReadTimeout: getEnvAsInt("READ_TIMEOUT", 15), + WriteTimeout: getEnvAsInt("WRITE_TIMEOUT", 15), + }, + Template: TemplateConfig{ + Dir: getEnv("TEMPLATE_DIR", "templates"), + PartialsDir: getEnv("PARTIALS_DIR", "templates/partials"), + HotReload: getEnvAsBool("TEMPLATE_HOT_RELOAD", isDevelopment()), + }, + Data: DataConfig{ + Dir: getEnv("DATA_DIR", "data"), + }, + } +} + +// Address returns the server address in host:port format +func (c *Config) Address() string { + return fmt.Sprintf("%s:%s", c.Server.Host, c.Server.Port) +} + +// Helper functions + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getEnvAsInt(key string, defaultValue int) int { + valueStr := os.Getenv(key) + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + return defaultValue +} + +func getEnvAsBool(key string, defaultValue bool) bool { + valueStr := os.Getenv(key) + if value, err := strconv.ParseBool(valueStr); err == nil { + return value + } + return defaultValue +} + +func isDevelopment() bool { + env := getEnv("GO_ENV", "development") + return env == "development" || env == "dev" +} diff --git a/internal/handlers/cv.go b/internal/handlers/cv.go new file mode 100644 index 0000000..ab0109a --- /dev/null +++ b/internal/handlers/cv.go @@ -0,0 +1,117 @@ +package handlers + +import ( + "net/http" + + "github.com/juanatsap/cv-site/internal/models" + "github.com/juanatsap/cv-site/internal/templates" +) + +// CVHandler handles CV-related requests +type CVHandler struct { + templates *templates.Manager +} + +// NewCVHandler creates a new CV handler +func NewCVHandler(tmpl *templates.Manager) *CVHandler { + return &CVHandler{ + templates: tmpl, + } +} + +// Home renders the full CV page +func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) { + // Get language from query parameter, default to English + lang := r.URL.Query().Get("lang") + if lang == "" { + lang = "en" + } + + // Validate language + if lang != "en" && lang != "es" { + HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'")) + return + } + + // Load CV data + cv, err := models.LoadCV(lang) + if err != nil { + HandleError(w, r, DataLoadError(err, "CV")) + return + } + + // Prepare template data + data := map[string]interface{}{ + "CV": cv, + "Lang": lang, + } + + // Render template + tmpl, err := h.templates.Render("index.html") + if err != nil { + HandleError(w, r, TemplateError(err, "index.html")) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, data); err != nil { + HandleError(w, r, TemplateError(err, "index.html")) + return + } +} + +// CVContent renders just the CV content for HTMX swaps +func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) { + // Get language from query parameter + lang := r.URL.Query().Get("lang") + if lang == "" { + lang = "en" + } + + // Validate language + if lang != "en" && lang != "es" { + HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'")) + return + } + + // Load CV data + cv, err := models.LoadCV(lang) + if err != nil { + HandleError(w, r, DataLoadError(err, "CV")) + return + } + + // Prepare template data + data := map[string]interface{}{ + "CV": cv, + "Lang": lang, + } + + // Render template + tmpl, err := h.templates.Render("cv-content.html") + if err != nil { + HandleError(w, r, TemplateError(err, "cv-content.html")) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, data); err != nil { + HandleError(w, r, TemplateError(err, "cv-content.html")) + return + } +} + +// ExportPDF handles PDF export requests +// For now, redirects to print-friendly version +// In production, integrate with chromedp or similar for actual PDF generation +func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) { + // Get language from query parameter + lang := r.URL.Query().Get("lang") + if lang == "" { + 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) +} diff --git a/internal/handlers/errors.go b/internal/handlers/errors.go new file mode 100644 index 0000000..9db06bd --- /dev/null +++ b/internal/handlers/errors.go @@ -0,0 +1,139 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" +) + +// ErrorResponse represents a structured error response +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message,omitempty"` + Code int `json:"code"` +} + +// AppError represents an application error with context +type AppError struct { + Err error + Message string + StatusCode int + Internal bool // If true, don't expose details to client +} + +// Error implements the error interface +func (e *AppError) Error() string { + if e.Err != nil { + return e.Err.Error() + } + return e.Message +} + +// NewAppError creates a new application error +func NewAppError(err error, message string, statusCode int, internal bool) *AppError { + return &AppError{ + Err: err, + Message: message, + StatusCode: statusCode, + Internal: internal, + } +} + +// HandleError handles errors consistently across the application +func HandleError(w http.ResponseWriter, r *http.Request, err error) { + var appErr *AppError + + // Check if it's an AppError + switch e := err.(type) { + case *AppError: + appErr = e + default: + // Unknown error - treat as internal server error + appErr = NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true) + } + + // Log the error + if appErr.Internal { + log.Printf("ERROR [%s %s]: %v", r.Method, r.URL.Path, appErr.Err) + } else { + log.Printf("CLIENT ERROR [%s %s]: %s (status: %d)", r.Method, r.URL.Path, appErr.Message, appErr.StatusCode) + } + + // Determine response based on Accept header + accept := r.Header.Get("Accept") + isJSON := accept == "application/json" + isHTMX := r.Header.Get("HX-Request") != "" + + if isJSON { + // JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(appErr.StatusCode) + + response := ErrorResponse{ + Error: http.StatusText(appErr.StatusCode), + Code: appErr.StatusCode, + } + + // Only include message if not internal + if !appErr.Internal { + response.Message = appErr.Message + } + + json.NewEncoder(w).Encode(response) + return + } + + if isHTMX { + // HTMX response - return simple HTML fragment + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(appErr.StatusCode) + + message := appErr.Message + if appErr.Internal { + message = "An error occurred. Please try again later." + } + + w.Write([]byte("
")) + return + } + + // Standard HTTP error response + message := appErr.Message + if appErr.Internal { + message = "Internal Server Error" + } + + http.Error(w, message, appErr.StatusCode) +} + +// Common error constructors + +func NotFoundError(message string) *AppError { + return NewAppError(nil, message, http.StatusNotFound, false) +} + +func BadRequestError(message string) *AppError { + return NewAppError(nil, message, http.StatusBadRequest, false) +} + +func InternalError(err error) *AppError { + return NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true) +} + +func TemplateError(err error, templateName string) *AppError { + return NewAppError( + err, + "Error rendering template: "+templateName, + http.StatusInternalServerError, + true, + ) +} + +func DataLoadError(err error, dataType string) *AppError { + return NewAppError( + err, + "Error loading "+dataType+" data", + http.StatusInternalServerError, + true, + ) +} diff --git a/internal/handlers/health.go b/internal/handlers/health.go new file mode 100644 index 0000000..9b91494 --- /dev/null +++ b/internal/handlers/health.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "time" +) + +// HealthResponse represents the health check response +type HealthResponse struct { + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Version string `json:"version"` +} + +// HealthHandler handles health check requests +type HealthHandler struct { + version string +} + +// NewHealthHandler creates a new health handler +func NewHealthHandler(version string) *HealthHandler { + return &HealthHandler{ + version: version, + } +} + +// Check performs a health check +func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) { + response := HealthResponse{ + Status: "ok", + Timestamp: time.Now(), + Version: h.version, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go new file mode 100644 index 0000000..3a14e87 --- /dev/null +++ b/internal/middleware/logger.go @@ -0,0 +1,59 @@ +package middleware + +import ( + "log" + "net/http" + "time" +) + +// responseWriter wraps http.ResponseWriter to capture status code +type responseWriter struct { + http.ResponseWriter + status int + written int64 + wroteHeader bool +} + +func (rw *responseWriter) WriteHeader(code int) { + if !rw.wroteHeader { + rw.status = code + rw.ResponseWriter.WriteHeader(code) + rw.wroteHeader = true + } +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusOK) + } + n, err := rw.ResponseWriter.Write(b) + rw.written += int64(n) + return n, err +} + +// Logger logs HTTP requests with method, path, status, and duration +func Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Wrap response writer + wrapped := &responseWriter{ + ResponseWriter: w, + status: http.StatusOK, + } + + // Process request + next.ServeHTTP(wrapped, r) + + // Log request + duration := time.Since(start) + log.Printf( + "[%s] %s %s - %d (%v)", + r.Method, + r.URL.Path, + r.RemoteAddr, + wrapped.status, + duration, + ) + }) +} diff --git a/internal/middleware/recovery.go b/internal/middleware/recovery.go new file mode 100644 index 0000000..98a3c2a --- /dev/null +++ b/internal/middleware/recovery.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "log" + "net/http" + "runtime/debug" +) + +// Recovery recovers from panics and logs the error with stack trace +func Recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + // Log the panic with stack trace + log.Printf("PANIC: %v\n%s", err, debug.Stack()) + + // Return 500 Internal Server Error + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/middleware/security.go b/internal/middleware/security.go new file mode 100644 index 0000000..a505404 --- /dev/null +++ b/internal/middleware/security.go @@ -0,0 +1,30 @@ +package middleware + +import "net/http" + +// SecurityHeaders adds common security headers to responses +func SecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Prevent clickjacking + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + + // Prevent MIME type sniffing + w.Header().Set("X-Content-Type-Options", "nosniff") + + // XSS Protection (legacy but still useful) + w.Header().Set("X-XSS-Protection", "1; mode=block") + + // Referrer policy + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Content Security Policy (adjust as needed) + w.Header().Set("Content-Security-Policy", + "default-src 'self'; "+ + "script-src 'self' 'unsafe-inline' https://unpkg.com; "+ + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "+ + "font-src 'self' https://fonts.gstatic.com; "+ + "connect-src 'self'") + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/models/cv.go b/internal/models/cv.go new file mode 100644 index 0000000..b8ea0c6 --- /dev/null +++ b/internal/models/cv.go @@ -0,0 +1,151 @@ +package models + +import ( + "encoding/json" + "fmt" + "os" +) + +// CV represents the complete curriculum vitae structure +type CV struct { + Personal Personal `json:"personal"` + Summary string `json:"summary"` + Experience []Experience `json:"experience"` + AIDevelopment AIDevelopment `json:"ai_development"` + Education []Education `json:"education"` + Skills Skills `json:"skills"` + Languages []Language `json:"languages"` + Projects []Project `json:"projects"` + Awards []Award `json:"awards"` + Certifications []Certification `json:"certifications"` + Other Other `json:"other"` + Meta Meta `json:"meta"` +} + +type Personal struct { + Name string `json:"name"` + Title string `json:"title"` + Location string `json:"location"` + Email string `json:"email"` + Phone string `json:"phone"` + DateOfBirth string `json:"dateOfBirth"` + PlaceOfBirth string `json:"placeOfBirth"` + Citizenship string `json:"citizenship"` + LinkedIn string `json:"linkedin"` + GitHub string `json:"github"` + Behance string `json:"behance"` + Website string `json:"website"` + Photo string `json:"photo"` +} + +type Experience struct { + Position string `json:"position"` + Company string `json:"company"` + Location string `json:"location"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + Current bool `json:"current"` + Responsibilities []string `json:"responsibilities"` + Technologies []string `json:"technologies"` + Highlights []string `json:"highlights"` +} + +type AIDevelopment struct { + Title string `json:"title"` + Period string `json:"period"` + Description string `json:"description"` + Skills []AISkill `json:"skills"` + Achievements []string `json:"achievements"` +} + +type AISkill struct { + Category string `json:"category"` + Proficiency string `json:"proficiency"` + Items []string `json:"items"` +} + +type Education struct { + Degree string `json:"degree"` + Institution string `json:"institution"` + Location string `json:"location"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + Field string `json:"field"` +} + +type Skills struct { + Technical []SkillCategory `json:"technical"` + SoftSkills []string `json:"soft_skills"` +} + +type SkillCategory struct { + Category string `json:"category"` + Proficiency int `json:"proficiency"` + Items []string `json:"items"` +} + +type Language struct { + Language string `json:"language"` + Proficiency string `json:"proficiency"` + Level int `json:"level"` +} + +type Project struct { + Name string `json:"name"` + Role string `json:"role"` + URL string `json:"url"` + Period string `json:"period"` + Description string `json:"description"` + Technologies []string `json:"technologies"` + Highlights []string `json:"highlights"` +} + +type Award struct { + Title string `json:"title"` + Issuer string `json:"issuer"` + Date string `json:"date"` + Description string `json:"description"` +} + +type Certification struct { + Name string `json:"name"` + Issuer string `json:"issuer"` + Date string `json:"date"` + Description string `json:"description"` +} + +type Other struct { + DriverLicense string `json:"driverLicense"` +} + +type Meta struct { + Version string `json:"version"` + LastUpdated string `json:"lastUpdated"` + Format string `json:"format"` + Language string `json:"language"` +} + +// LoadCV loads CV data from a JSON file for the specified language +func LoadCV(lang string) (*CV, error) { + // Validate language + if lang != "en" && lang != "es" { + return nil, fmt.Errorf("unsupported language: %s", lang) + } + + // Determine which JSON file to load + filename := fmt.Sprintf("data/cv-%s.json", lang) + + // Read the JSON file + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading file %s: %w", filename, err) + } + + // Parse JSON + var cv CV + if err := json.Unmarshal(data, &cv); err != nil { + return nil, fmt.Errorf("error parsing JSON: %w", err) + } + + return &cv, nil +} diff --git a/internal/templates/template.go b/internal/templates/template.go new file mode 100644 index 0000000..83cd046 --- /dev/null +++ b/internal/templates/template.go @@ -0,0 +1,99 @@ +package templates + +import ( + "fmt" + "html/template" + "log" + "path/filepath" + "sync" + + "github.com/juanatsap/cv-site/internal/config" +) + +// Manager handles template parsing and rendering +type Manager struct { + templates *template.Template + config *config.TemplateConfig + mu sync.RWMutex +} + +// NewManager creates a new template manager +func NewManager(cfg *config.TemplateConfig) (*Manager, error) { + m := &Manager{ + config: cfg, + } + + if err := m.loadTemplates(); err != nil { + return nil, fmt.Errorf("failed to load templates: %w", err) + } + + return m, nil +} + +// loadTemplates parses all templates from the configured directory +func (m *Manager) loadTemplates() error { + m.mu.Lock() + defer m.mu.Unlock() + + // Create template with custom functions + funcMap := template.FuncMap{ + "iterate": func(count int) []int { + var result []int + for i := 0; i < count; i++ { + result = append(result, i) + } + return result + }, + "eq": func(a, b string) bool { + return a == b + }, + } + + // Parse main templates + pattern := filepath.Join(m.config.Dir, "*.html") + tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern) + if err != nil { + return fmt.Errorf("error parsing templates from %s: %w", pattern, err) + } + + // Try to parse partials if they exist + partialsPattern := filepath.Join(m.config.PartialsDir, "*.html") + partials, _ := filepath.Glob(partialsPattern) + if len(partials) > 0 { + tmpl, err = tmpl.ParseGlob(partialsPattern) + if err != nil { + log.Printf("Warning: error parsing partials: %v", err) + } + } + + m.templates = tmpl + log.Printf("✓ Templates loaded successfully from %s", m.config.Dir) + + return nil +} + +// Reload reloads all templates (useful for hot-reload in development) +func (m *Manager) Reload() error { + return m.loadTemplates() +} + +// Render executes a template with the given data +func (m *Manager) Render(name string) (*template.Template, error) { + // Hot reload in development mode + if m.config.HotReload { + if err := m.Reload(); err != nil { + log.Printf("Warning: template reload failed: %v", err) + // Continue with cached templates + } + } + + m.mu.RLock() + defer m.mu.RUnlock() + + tmpl := m.templates.Lookup(name) + if tmpl == nil { + return nil, fmt.Errorf("template %q not found", name) + } + + return tmpl, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8d86dd9 --- /dev/null +++ b/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "errors" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/juanatsap/cv-site/internal/config" + "github.com/juanatsap/cv-site/internal/handlers" + "github.com/juanatsap/cv-site/internal/middleware" + "github.com/juanatsap/cv-site/internal/templates" +) + +const version = "1.0.0" + +func main() { + // Initialize logger + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("🚀 Starting CV Server v" + version) + + // Load configuration + cfg := config.Load() + log.Printf("✓ Configuration loaded (env: %s)", os.Getenv("GO_ENV")) + + // Initialize template manager + templateMgr, err := templates.NewManager(&cfg.Template) + if err != nil { + log.Fatalf("❌ Failed to initialize templates: %v", err) + } + + // Initialize handlers + cvHandler := handlers.NewCVHandler(templateMgr) + healthHandler := handlers.NewHealthHandler(version) + + // Setup router + mux := http.NewServeMux() + + // Routes + mux.HandleFunc("/", cvHandler.Home) + mux.HandleFunc("/cv", cvHandler.CVContent) + mux.HandleFunc("/export/pdf", cvHandler.ExportPDF) + mux.HandleFunc("/health", healthHandler.Check) + + // Static files with cache control + staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static"))) + mux.Handle("/static/", cacheControl(staticHandler)) + + // Apply middleware chain + handler := middleware.Recovery( + middleware.Logger( + middleware.SecurityHeaders(mux), + ), + ) + + // Create server with timeouts + server := &http.Server{ + Addr: ":" + cfg.Server.Port, + Handler: handler, + ReadTimeout: time.Duration(cfg.Server.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(cfg.Server.WriteTimeout) * time.Second, + IdleTimeout: 120 * time.Second, + } + + // Start server in goroutine + serverErrors := make(chan error, 1) + go func() { + log.Printf("✓ Server listening on http://%s:%s", cfg.Server.Host, cfg.Server.Port) + log.Printf("📄 English: http://%s:%s/?lang=en", cfg.Server.Host, cfg.Server.Port) + log.Printf("📄 Spanish: http://%s:%s/?lang=es", cfg.Server.Host, cfg.Server.Port) + log.Printf("❤️ Health: http://%s:%s/health", cfg.Server.Host, cfg.Server.Port) + log.Println("Press Ctrl+C to shutdown") + + serverErrors <- server.ListenAndServe() + }() + + // Setup graceful shutdown + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) + + // Wait for shutdown signal or server error + select { + case err := <-serverErrors: + if !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("❌ Server error: %v", err) + } + + case sig := <-shutdown: + log.Printf("🛑 Shutdown signal received: %v", sig) + + // Create shutdown context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Attempt graceful shutdown + if err := server.Shutdown(ctx); err != nil { + log.Printf("⚠️ Graceful shutdown failed, forcing: %v", err) + if err := server.Close(); err != nil { + log.Fatalf("❌ Failed to close server: %v", err) + } + } + + log.Println("✓ Server stopped gracefully") + } +} + +// cacheControl adds cache headers to static files +func cacheControl(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Cache static files for 1 hour in development, 1 day in production + maxAge := "3600" // 1 hour + if os.Getenv("GO_ENV") == "production" { + maxAge = "86400" // 1 day + } + + w.Header().Set("Cache-Control", "public, max-age="+maxAge) + h.ServeHTTP(w, r) + }) +} diff --git a/static/css/main-enhanced.css b/static/css/main-enhanced.css new file mode 100644 index 0000000..4e5308e --- /dev/null +++ b/static/css/main-enhanced.css @@ -0,0 +1,849 @@ +/* Root Variables */ +:root { + --bg-gray: #525659; + --paper-white: #ffffff; + --text-dark: #1a1a1a; + --text-gray: #4a4a4a; + --text-light: #6a6a6a; + --accent-blue: #2563eb; + --accent-blue-hover: #1d4ed8; + --border-gray: #e5e5e5; + --error-red: #dc2626; + --error-bg: #fee2e2; + --success-green: #10b981; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + + /* Transition timings */ + --transition-fast: 150ms; + --transition-base: 200ms; + --transition-slow: 300ms; +} + +/* Reset & Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: var(--bg-gray); + color: var(--text-dark); + line-height: 1.6; + min-height: 100vh; +} + +/* Screen reader only text */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +a { + color: var(--accent-blue); + text-decoration: none; + transition: color var(--transition-base); +} + +a:hover { + color: var(--accent-blue-hover); + text-decoration: underline; +} + +a:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; + border-radius: 2px; +} + +/* Action Bar */ +.action-bar { + background: rgba(255, 255, 255, 0.95); + border-bottom: 1px solid var(--border-gray); + position: sticky; + top: 0; + z-index: 100; + backdrop-filter: blur(10px); + box-shadow: var(--shadow); +} + +.action-bar-content { + max-width: 1200px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; +} + +.language-toggle { + display: flex; + gap: 0.5rem; +} + +.lang-btn { + padding: 0.5rem 1rem; + border: 2px solid var(--border-gray); + background: white; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all var(--transition-base); + position: relative; +} + +.lang-btn:hover:not(.active) { + border-color: var(--accent-blue); + background: #f0f9ff; + transform: translateY(-1px); +} + +.lang-btn:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + +.lang-btn.active { + border-color: var(--accent-blue); + background: var(--accent-blue); + color: white; +} + +/* Loading state for buttons */ +.lang-btn[aria-busy="true"] { + opacity: 0.7; + cursor: wait; + pointer-events: none; +} + +.export-actions { + display: flex; + gap: 0.5rem; +} + +.export-btn { + padding: 0.5rem 1.5rem; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + transition: all var(--transition-base); + box-shadow: var(--shadow); +} + +.export-btn:hover { + background: var(--accent-blue-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +.export-btn:focus-visible { + outline: 2px solid white; + outline-offset: 2px; +} + +/* HTMX Indicator */ +.htmx-indicator { + display: none; + align-items: center; + gap: 0.5rem; +} + +.htmx-indicator.htmx-request { + display: flex; +} + +.loader { + border: 3px solid #f3f3f3; + border-top: 3px solid var(--accent-blue); + border-radius: 50%; + width: 24px; + height: 24px; + animation: spin 1s linear infinite; + display: inline-block; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* CV Container - Paper Effect */ +.cv-container { + max-width: 900px; + margin: 2rem auto; + padding: 0 1rem; +} + +.cv-paper { + background: var(--paper-white); + padding: 3rem; + box-shadow: var(--shadow-lg); + border-radius: 8px; + min-height: 11in; /* A4 height */ + /* HTMX swap transitions */ + transition: opacity var(--transition-base); +} + +/* HTMX swap animation */ +.cv-paper.htmx-swapping { + opacity: 0; + transition: opacity var(--transition-fast); +} + +.cv-paper.htmx-settling { + opacity: 1; + transition: opacity var(--transition-base); +} + +/* Error Toast */ +.error-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--error-bg); + color: var(--error-red); + padding: 1rem 1.5rem; + border-radius: 8px; + border-left: 4px solid var(--error-red); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: 1rem; + max-width: 400px; + z-index: 1000; + animation: slideIn var(--transition-base) ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.error-toast button { + background: none; + border: none; + font-size: 1.5rem; + color: var(--error-red); + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity var(--transition-base); +} + +.error-toast button:hover { + opacity: 0.7; +} + +/* CV Header */ +.cv-header { + border-bottom: 3px solid var(--accent-blue); + padding-bottom: 2rem; + margin-bottom: 2rem; +} + +.cv-header-main { + margin-bottom: 1.5rem; +} + +.cv-name { + font-size: 2.5rem; + font-weight: 700; + color: var(--text-dark); + margin-bottom: 0.5rem; +} + +.cv-title { + font-size: 1.5rem; + font-weight: 400; + color: var(--text-gray); +} + +.cv-contact { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.75rem; + font-size: 0.9rem; +} + +.contact-item { + color: var(--text-gray); +} + +/* Sections */ +.cv-section { + margin-bottom: 2.5rem; +} + +.section-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-dark); + margin-bottom: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--border-gray); +} + +.summary-text { + font-size: 1rem; + line-height: 1.8; + color: var(--text-gray); + text-align: justify; +} + +/* AI Development Section */ +.ai-section { + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + padding: 2rem; + border-radius: 8px; + border-left: 4px solid var(--accent-blue); +} + +.ai-period { + font-size: 0.9rem; + color: var(--text-gray); + margin-bottom: 1rem; + font-style: italic; +} + +.ai-description { + margin-bottom: 1.5rem; + line-height: 1.8; +} + +.ai-skills { + display: grid; + gap: 1.5rem; +} + +.ai-skill-category { + background: white; + padding: 1.5rem; + border-radius: 6px; + box-shadow: var(--shadow); +} + +.ai-skill-title { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-dark); +} + +.proficiency-badge { + background: var(--accent-blue); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + margin-left: 0.5rem; +} + +.ai-skill-list { + list-style: none; + display: grid; + gap: 0.5rem; +} + +.ai-skill-list li:before { + content: "✨ "; + margin-right: 0.5rem; +} + +.ai-achievements { + margin-top: 1.5rem; + background: white; + padding: 1.5rem; + border-radius: 6px; +} + +.ai-achievements h4 { + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.achievement-list { + list-style: none; +} + +.achievement-list li { + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-gray); +} + +.achievement-list li:last-child { + border-bottom: none; +} + +.achievement-list li:before { + content: "🏆 "; + margin-right: 0.5rem; +} + +/* Experience Items */ +.experience-item { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border-gray); +} + +.experience-item:last-child { + border-bottom: none; +} + +.experience-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + gap: 1rem; +} + +.experience-title { + flex: 1; +} + +.position { + font-size: 1.2rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 0.25rem; +} + +.company { + font-size: 0.95rem; + color: var(--text-gray); + font-weight: 500; +} + +.experience-period { + font-size: 0.9rem; + color: var(--text-light); + white-space: nowrap; + font-style: italic; +} + +.responsibilities { + list-style: none; + margin-bottom: 1rem; +} + +.responsibilities li { + padding-left: 1.5rem; + margin-bottom: 0.5rem; + position: relative; +} + +.responsibilities li:before { + content: "▸"; + position: absolute; + left: 0; + color: var(--accent-blue); + font-weight: bold; +} + +.technologies { + font-size: 0.9rem; + color: var(--text-gray); + margin-top: 1rem; +} + +.highlights { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; +} + +.highlight-badge { + background: #fef3c7; + color: #92400e; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 500; +} + +/* Skills Grid */ +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.skill-category { + background: #f9fafb; + padding: 1.5rem; + border-radius: 6px; + border-left: 3px solid var(--accent-blue); +} + +.skill-category-title { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.proficiency-stars { + color: #fbbf24; + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.skill-items { + list-style: none; +} + +.skill-items li { + padding: 0.25rem 0; + padding-left: 1rem; + position: relative; +} + +.skill-items li:before { + content: "•"; + position: absolute; + left: 0; + color: var(--accent-blue); +} + +.soft-skills { + margin-top: 2rem; +} + +.soft-skills h4 { + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.soft-skills-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.soft-skill-tag { + background: #e0e7ff; + color: #3730a3; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; +} + +/* Projects */ +.project-item { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border-gray); +} + +.project-item:last-child { + border-bottom: none; +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.project-name { + font-size: 1.2rem; + font-weight: 600; + color: var(--text-dark); +} + +.project-period { + font-size: 0.9rem; + color: var(--text-light); + font-style: italic; +} + +.project-role { + font-size: 0.9rem; + color: var(--text-gray); + margin-bottom: 0.5rem; + font-weight: 500; +} + +.project-url { + font-size: 0.85rem; + margin-bottom: 0.75rem; +} + +.project-description { + margin-bottom: 1rem; + line-height: 1.7; +} + +.project-highlights { + list-style: none; + margin-top: 1rem; +} + +.project-highlights li { + padding-left: 1.5rem; + margin-bottom: 0.25rem; + position: relative; +} + +.project-highlights li:before { + content: "✓"; + position: absolute; + left: 0; + color: #10b981; + font-weight: bold; +} + +/* Education */ +.education-item { + margin-bottom: 1.5rem; +} + +.education-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.degree { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-dark); +} + +.education-period { + font-size: 0.9rem; + color: var(--text-light); + font-style: italic; +} + +.institution { + font-size: 0.95rem; + color: var(--text-gray); + margin-bottom: 0.25rem; +} + +.field { + font-size: 0.9rem; + color: var(--text-light); +} + +/* Certifications */ +.certifications-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; +} + +.certification-item { + background: #f9fafb; + padding: 1.25rem; + border-radius: 6px; + border-left: 3px solid #10b981; +} + +.certification-name { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-dark); +} + +.certification-issuer { + font-size: 0.9rem; + color: var(--text-gray); + margin-bottom: 0.25rem; +} + +.certification-date { + font-size: 0.85rem; + color: var(--text-light); +} + +/* Awards */ +.award-item { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-gray); +} + +.award-item:last-child { + border-bottom: none; +} + +.award-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 0.5rem; +} + +.award-issuer { + font-size: 0.9rem; + color: var(--text-gray); + margin-bottom: 0.5rem; +} + +.award-description { + font-size: 0.95rem; + color: var(--text-gray); +} + +/* Languages */ +.languages-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; +} + +.language-item { + text-align: center; + padding: 1.5rem; + background: #f9fafb; + border-radius: 6px; +} + +.language-name { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.language-proficiency { + font-size: 0.9rem; + color: var(--text-gray); + margin-bottom: 0.5rem; +} + +/* Footer */ +footer { + text-align: center; + padding: 2rem; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .cv-paper { + padding: 1.5rem; + } + + .cv-name { + font-size: 2rem; + } + + .cv-title { + font-size: 1.2rem; + } + + .action-bar-content { + flex-direction: column; + padding: 1rem; + gap: 1rem; + } + + .language-toggle { + width: 100%; + flex-wrap: wrap; + } + + .lang-btn { + flex: 1; + min-width: 120px; + } + + .export-actions { + width: 100%; + } + + .export-btn { + width: 100%; + } + + .error-toast { + bottom: 1rem; + right: 1rem; + left: 1rem; + max-width: none; + } + + .experience-header, + .project-header, + .education-header { + flex-direction: column; + gap: 0.5rem; + } + + .experience-period, + .project-period, + .education-period { + align-self: flex-start; + } + + .skills-grid, + .certifications-grid, + .languages-grid { + grid-template-columns: 1fr; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Print-specific hiding */ +.no-print { + /* Will be hidden in print.css */ +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .lang-btn { + border-width: 3px; + } + + .lang-btn:focus-visible, + .export-btn:focus-visible, + a:focus-visible { + outline-width: 3px; + } +} diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..21dc13f --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,351 @@ +/* Minimal CV Design - Clean & Professional */ + +:root { + --bg-gray: #525659; + --paper-white: #ffffff; + --text-dark: #2d2d2d; + --text-gray: #555555; + --accent-blue: #0066cc; + --border-gray: #dddddd; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', Arial, sans-serif; + background-color: var(--bg-gray); + color: var(--text-dark); + line-height: 1.6; +} + +a { + color: var(--accent-blue); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Action Bar */ +.action-bar { + background: white; + border-bottom: 1px solid var(--border-gray); + position: sticky; + top: 0; + z-index: 100; +} + +.action-bar-content { + max-width: 900px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.language-toggle { + display: flex; + gap: 0.5rem; +} + +.lang-btn { + padding: 0.4rem 1rem; + border: 1px solid var(--border-gray); + background: white; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + +.lang-btn:hover { + background: #f5f5f5; +} + +.lang-btn.active { + background: var(--accent-blue); + color: white; + border-color: var(--accent-blue); +} + +.export-btn { + padding: 0.4rem 1.2rem; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + +.export-btn:hover { + background: #0052a3; +} + +/* Loading Indicator */ +.htmx-indicator { + display: none; +} + +.htmx-indicator.htmx-request { + display: inline-block; +} + +.loader { + border: 2px solid #f3f3f3; + border-top: 2px solid var(--accent-blue); + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* CV Container - Paper Design */ +.cv-container { + max-width: 900px; + margin: 2rem auto; + padding: 0 1rem; +} + +.cv-paper { + background: var(--paper-white); + padding: 3rem 4rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + min-height: 11in; +} + +/* Header */ +.cv-header { + border-bottom: 2px solid var(--text-dark); + padding-bottom: 1.5rem; + margin-bottom: 2rem; +} + +.cv-name { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.cv-title { + font-size: 1.3rem; + font-weight: 400; + color: var(--text-gray); + margin-bottom: 1rem; +} + +.cv-contact { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.5rem; + font-size: 0.9rem; + color: var(--text-gray); +} + +/* Sections */ +.cv-section { + margin-bottom: 2rem; +} + +.section-title { + font-size: 1.3rem; + font-weight: 700; + margin-bottom: 1rem; + padding-bottom: 0.3rem; + border-bottom: 1px solid var(--border-gray); +} + +.summary-text { + line-height: 1.7; + text-align: justify; +} + +/* Experience */ +.experience-item { + margin-bottom: 1.5rem; +} + +.experience-header { + display: flex; + justify-content: space-between; + margin-bottom: 0.75rem; + gap: 1rem; +} + +.position { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.company { + color: var(--text-gray); + font-size: 0.95rem; +} + +.experience-period { + color: var(--text-gray); + font-size: 0.9rem; + white-space: nowrap; + font-style: italic; +} + +.responsibilities { + list-style: none; + margin-bottom: 0.75rem; +} + +.responsibilities li { + padding-left: 1.2rem; + margin-bottom: 0.4rem; + position: relative; +} + +.responsibilities li:before { + content: "•"; + position: absolute; + left: 0; + font-weight: bold; +} + +.technologies { + font-size: 0.85rem; + color: var(--text-gray); + font-style: italic; +} + +/* Education */ +.education-item { + margin-bottom: 1rem; +} + +.education-header { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.degree { + font-size: 1rem; + font-weight: 600; +} + +.education-period { + color: var(--text-gray); + font-size: 0.9rem; + font-style: italic; +} + +.institution { + color: var(--text-gray); + font-size: 0.95rem; +} + +/* Skills */ +.skill-block { + margin-bottom: 1rem; +} + +.skill-title { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.4rem; +} + +.skill-list { + color: var(--text-gray); + font-size: 0.95rem; +} + +/* Projects */ +.project-item { + margin-bottom: 1.5rem; +} + +.project-header { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.project-name { + font-size: 1.1rem; + font-weight: 600; +} + +.project-period { + color: var(--text-gray); + font-size: 0.9rem; + font-style: italic; +} + +.project-role { + color: var(--text-gray); + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.project-description { + margin-bottom: 0.5rem; +} + +/* Certifications & Awards */ +.cert-item, +.award-item { + margin-bottom: 0.5rem; + font-size: 0.95rem; +} + +/* Languages */ +.languages-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.75rem; +} + +.language-item { + font-size: 0.95rem; +} + +/* Footer */ +footer { + text-align: center; + padding: 2rem; + color: rgba(255,255,255,0.7); + font-size: 0.85rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .cv-paper { + padding: 2rem 1.5rem; + } + + .cv-name { + font-size: 2rem; + } + + .experience-header, + .project-header, + .education-header { + flex-direction: column; + gap: 0.25rem; + } + + .action-bar-content { + flex-direction: column; + gap: 1rem; + } +} + +.no-print {} diff --git a/static/css/print.css b/static/css/print.css new file mode 100644 index 0000000..8878ab4 --- /dev/null +++ b/static/css/print.css @@ -0,0 +1,282 @@ +/* Print Styles - Optimized for PDF Export */ + +@media print { + /* Reset for print */ + * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + color-adjust: exact !important; + } + + /* Page setup */ + @page { + size: A4; + margin: 1.5cm; + } + + body { + background: white; + margin: 0; + padding: 0; + } + + /* Hide non-print elements */ + .no-print, + .action-bar, + footer { + display: none !important; + } + + /* CV Container adjustments */ + .cv-container { + max-width: 100%; + margin: 0; + padding: 0; + } + + .cv-paper { + background: white; + box-shadow: none; + border-radius: 0; + padding: 0; + min-height: auto; + } + + /* Typography */ + body { + font-size: 10pt; + line-height: 1.4; + } + + .cv-name { + font-size: 24pt; + page-break-after: avoid; + } + + .cv-title { + font-size: 14pt; + page-break-after: avoid; + } + + .section-title { + font-size: 14pt; + page-break-after: avoid; + margin-top: 1.5em; + } + + /* Prevent page breaks */ + .cv-header, + .cv-section, + .experience-item, + .project-item, + .education-item, + .award-item { + page-break-inside: avoid; + } + + /* Links */ + a { + color: #2563eb; + text-decoration: none; + } + + a[href]:after { + content: none; /* Don't print URLs */ + } + + /* Compact spacing for print */ + .cv-header { + margin-bottom: 1em; + padding-bottom: 0.5em; + } + + .cv-section { + margin-bottom: 1.5em; + } + + .experience-item, + .project-item { + margin-bottom: 1em; + padding-bottom: 0.75em; + } + + /* Contact info - make it more compact */ + .cv-contact { + grid-template-columns: repeat(2, 1fr); + gap: 0.3em; + font-size: 9pt; + } + + /* AI Section - maintain visibility in print */ + .ai-section { + background: #f0f9ff !important; + border-left: 3px solid #2563eb !important; + padding: 1em !important; + margin: 1em 0 !important; + } + + .ai-skill-category { + background: white !important; + padding: 0.75em !important; + margin-bottom: 0.5em; + box-shadow: none; + border: 1px solid #e5e5e5; + } + + .ai-achievements { + background: white !important; + padding: 0.75em !important; + border: 1px solid #e5e5e5; + } + + /* Skills grid - more compact */ + .skills-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.75em; + } + + .skill-category { + background: #f9fafb !important; + padding: 0.75em !important; + } + + /* Certifications grid */ + .certifications-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.5em; + } + + .certification-item { + background: #f9fafb !important; + padding: 0.75em !important; + } + + /* Languages */ + .languages-grid { + grid-template-columns: repeat(3, 1fr); + gap: 0.5em; + } + + .language-item { + background: #f9fafb !important; + padding: 0.75em !important; + } + + /* Badges and tags */ + .proficiency-badge, + .highlight-badge, + .soft-skill-tag { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + .proficiency-badge { + background: #2563eb !important; + color: white !important; + } + + .highlight-badge { + background: #fef3c7 !important; + color: #92400e !important; + } + + .soft-skill-tag { + background: #e0e7ff !important; + color: #3730a3 !important; + } + + /* Ensure borders print */ + .cv-header { + border-bottom: 2px solid #2563eb !important; + } + + .section-title { + border-bottom: 1px solid #e5e5e5 !important; + } + + .experience-item, + .project-item, + .award-item { + border-bottom: 1px solid #e5e5e5 !important; + } + + /* Reduce list spacing */ + .responsibilities, + .ai-skill-list, + .achievement-list, + .skill-items, + .project-highlights { + margin: 0.5em 0; + } + + .responsibilities li, + .ai-skill-list li, + .achievement-list li, + .skill-items li, + .project-highlights li { + margin-bottom: 0.25em; + } + + /* Font size adjustments for print */ + .summary-text { + font-size: 9.5pt; + line-height: 1.5; + } + + .position { + font-size: 11pt; + } + + .company, + .institution { + font-size: 9.5pt; + } + + .responsibilities, + .project-description { + font-size: 9pt; + } + + .technologies { + font-size: 8.5pt; + } + + /* Optimize spacing */ + .ai-description { + margin-bottom: 0.75em; + } + + .experience-header, + .project-header, + .education-header { + margin-bottom: 0.5em; + } + + /* Ensure stars print correctly */ + .proficiency-stars { + color: #fbbf24 !important; + } + + /* Compact soft skills */ + .soft-skills-list { + gap: 0.25em; + } + + .soft-skill-tag { + padding: 0.25em 0.5em; + font-size: 8pt; + } +} + +/* Print button functionality */ +@media screen { + .print-only { + display: none; + } +} + +@media print { + .print-only { + display: block; + } +} diff --git a/templates/cv-content.html b/templates/cv-content.html new file mode 100644 index 0000000..0ebf2b9 --- /dev/null +++ b/templates/cv-content.html @@ -0,0 +1,137 @@ + +
+
+
+ {{.CV.Summary}}
+ {{range $index, $item := .Items}}{{if $index}}, {{end}}{{$item}}{{end}}
+ {{.Description}}{{if eq .Lang "es"}}Resumen{{else}}Summary{{end}}
+ {{if eq .Lang "es"}}Experiencia Laboral{{else}}Work History{{end}}
+
+ {{range .CV.Experience}}
+ {{.Position}}
+
+ {{range .Responsibilities}}
+
+
+ {{if .Technologies}}
+ {{if eq .Lang "es"}}Formación{{else}}Education{{end}}
+
+ {{range .CV.Education}}
+ {{.Degree}}
+ {{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}
+
+ {{range .CV.Skills.Technical}}
+ {{.Category}}
+ {{if eq .Lang "es"}}Proyectos{{else}}Projects{{end}}
+
+ {{range .CV.Projects}}
+ {{.Name}}
+ {{if eq .Lang "es"}}Certificaciones{{else}}Certifications{{end}}
+
+ {{range .CV.Certifications}}
+ {{if eq .Lang "es"}}Premios{{else}}Awards{{end}}
+
+ {{range .CV.Awards}}
+ {{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}
+
+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+