From 9a848e8c534b412380d63345caa10ebb1096a095 Mon Sep 17 00:00:00 2001 From: juanatsap Date: Mon, 1 Dec 2025 13:03:06 +0000 Subject: [PATCH] feat: Add CMD+K command palette with ninja-keys integration Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components. --- README.md | 27 +- data/cv-en.json | 317 ++++----- data/cv-es.json | 317 ++++----- data/ui-en.json | 40 +- data/ui-es.json | 40 +- doc/16-CMD-K-API.md | 315 +++++++++ doc/2-MODERN-WEB-TECHNIQUES.md | 140 ++++ doc/3-API.md | 8 + docs/CMD-K-COMMAND-BAR.md | 214 +++++++ docs/CONTACT-FORM-QUICKSTART.md | 2 +- docs/HACK-CHALLENGE.md | 602 ------------------ docs/SECURITY-AUDIT-REPORT.md | 2 +- docs/SECURITY-IMPLEMENTATION-SUMMARY.md | 3 - docs/SECURITY.md | 9 +- internal/handlers/cv_cmdk.go | 104 +++ internal/handlers/cv_cmdk_test.go | 225 +++++++ internal/handlers/cv_contact.go | 4 +- internal/handlers/cv_text.go | 9 + internal/handlers/cv_text_test.go | 266 ++++++++ internal/models/cv.go | 3 + internal/models/cv/cv.go | 3 + internal/models/ui/ui.go | 52 +- internal/routes/routes.go | 5 +- static/css/01-foundation/_themes.css | 25 +- static/css/03-components/_action-bar.css | 51 ++ static/css/04-interactive/_contact-form.css | 23 +- static/hyperscript/utils._hs | 44 +- static/js/main.js | 7 + static/js/ninja-keys-init.js | 497 +++++++++++++++ templates/index.html | 13 +- templates/partials/modals/contact-modal.html | 72 ++- .../partials/modals/shortcuts-modal.html | 6 + .../partials/navigation/action-buttons.html | 12 +- templates/partials/sections/courses.html | 2 +- templates/partials/sections/experience.html | 2 +- templates/partials/sections/projects.html | 2 +- tests/mjs/71-cmd-k-api-scroll.test.mjs | 208 ++++++ tests/mjs/72-cmd-k-button.test.mjs | 143 +++++ tests/mjs/73-contact-form.test.mjs | 329 ++++++++++ tests/mjs/debug-css-cascade.mjs | 48 -- tests/mjs/debug-theme-button.mjs | 69 -- tests/mjs/debug-tooltip.mjs | 134 ---- tests/mjs/test-preference-migration.test.mjs | 133 ---- tests/mjs/verify-migration.test.mjs | 44 -- tests/mjs/verify-theme-button-fix.mjs | 86 --- 45 files changed, 3070 insertions(+), 1587 deletions(-) create mode 100644 doc/16-CMD-K-API.md create mode 100644 docs/CMD-K-COMMAND-BAR.md delete mode 100644 docs/HACK-CHALLENGE.md create mode 100644 internal/handlers/cv_cmdk.go create mode 100644 internal/handlers/cv_cmdk_test.go create mode 100644 internal/handlers/cv_text_test.go create mode 100644 static/js/ninja-keys-init.js create mode 100644 tests/mjs/71-cmd-k-api-scroll.test.mjs create mode 100644 tests/mjs/72-cmd-k-button.test.mjs create mode 100644 tests/mjs/73-contact-form.test.mjs delete mode 100755 tests/mjs/debug-css-cascade.mjs delete mode 100755 tests/mjs/debug-theme-button.mjs delete mode 100755 tests/mjs/debug-tooltip.mjs delete mode 100644 tests/mjs/test-preference-migration.test.mjs delete mode 100644 tests/mjs/verify-migration.test.mjs delete mode 100755 tests/mjs/verify-theme-button-fix.mjs diff --git a/README.md b/README.md index d3b20a2..f944b1d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A professional, bilingual CV site with server-side PDF generation, HTMX interact **Open Source:** The code is MIT licensed and available for educational purposes. You're welcome to use it as a template or reference for your own projects. This repository is maintained as my personal CV site and may be modified without notice. -**Contributions:** This is a personal CV project and is feature-complete. I'm not seeking contributions, but you're welcome to use it as a template! If you find a critical security vulnerability, please follow the [responsible disclosure process](docs/HACK-CHALLENGE.md#-responsible-disclosure). +**Contributions:** This is a personal CV project and is feature-complete. I'm not seeking contributions, but you're welcome to use it as a template! If you find a critical security vulnerability, please report it via email. ## 📑 Table of Contents @@ -81,24 +81,7 @@ This project demonstrates **production-grade security** practices with multiple **Security Rating: A- (Very Good)** -### Try to Hack Me Challenge! 🎯 - -Think you can break through these defenses? **I welcome ethical hackers and security researchers to test this site.** - -**Challenge Categories:** -1. **Browser-Only Bypass** - Submit a contact form using curl or Postman (Hard ⭐⭐⭐) -2. **Rate Limit Bypass** - Exceed the rate limits without detection (Medium ⭐⭐) -3. **Injection Challenge** - Execute code via XSS, command injection, or email header injection (Hard ⭐⭐⭐) -4. **Bot Detection Bypass** - Submit as a bot without getting caught (Medium ⭐⭐) -5. **CSRF Challenge** - Submit without a valid token (Hard ⭐⭐⭐) - -**Documentation:** -- **[SECURITY.md](docs/SECURITY.md)** - Complete security architecture and implementation details -- **[HACK-CHALLENGE.md](docs/HACK-CHALLENGE.md)** - Full hacking challenge rules and guidelines - -**Found a vulnerability?** Follow the [responsible disclosure process](docs/HACK-CHALLENGE.md#-responsible-disclosure). - -**Hall of Fame:** Valid findings will be acknowledged publicly (with your permission). +**Documentation:** See [SECURITY.md](docs/SECURITY.md) for complete security architecture and implementation details. --- @@ -197,7 +180,6 @@ This project includes comprehensive documentation organized by purpose: ### 📋 Policies & Standards - **[SECURITY.md](docs/SECURITY.md)** - Complete security architecture, implementation, and testing guide -- **[HACK-CHALLENGE.md](docs/HACK-CHALLENGE.md)** - "Try to Hack Me!" challenge for security researchers - **[PRIVACY.md](doc/PRIVACY.md)** - Privacy policy template and analytics guidance - **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)** - Community standards (Contributor Covenant) - **[CONTRIBUTING.md](CONTRIBUTING.md)** - Contribution policy (personal project notice) @@ -244,7 +226,7 @@ Deployment guides available for: - `GO_ENV` - Environment (development/production) - `TEMPLATE_HOT_RELOAD` - Enable template hot-reload in development -**Security:** See [SECURITY.md](docs/SECURITY.md) for production deployment best practices and [HACK-CHALLENGE.md](docs/HACK-CHALLENGE.md) for penetration testing guidelines. +**Security:** See [SECURITY.md](docs/SECURITY.md) for production deployment best practices. ## 🎨 Customization @@ -306,8 +288,7 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 💬 Questions or Issues? - **Questions:** Feel free to fork and modify - this is a template! -- **Security Issues:** See [HACK-CHALLENGE.md](docs/HACK-CHALLENGE.md) for reporting security vulnerabilities -- **Security Research:** Read the [Try to Hack Me Challenge](docs/HACK-CHALLENGE.md) if you want to test the security +- **Security Issues:** Report vulnerabilities via email - **Documentation:** Check [CUSTOMIZATION.md](doc/CUSTOMIZATION.md) and [DEPLOYMENT.md](doc/DEPLOYMENT.md) ## 🙏 Acknowledgments diff --git a/data/cv-en.json b/data/cv-en.json index f526ff1..b81f988 100644 --- a/data/cv-en.json +++ b/data/cv-en.json @@ -55,7 +55,8 @@ "API Integration" ], "companyLogo": "olympic-broadcasting.png", - "shortDescription": "SAP CDC solutions for international broadcasting events. Custom implementations and technical guidance." + "shortDescription": "SAP CDC solutions for international broadcasting events. Custom implementations and technical guidance.", + "companyID": "olympic-broadcasting" }, { "position": "Senior SAP/CDC Technical Consultant", @@ -82,7 +83,8 @@ "Authentication Systems" ], "companyLogo": "livgolf.png", - "shortDescription": "Technical consulting for SAP CDC implementation. Created authorization screens, backend endpoints, and comprehensive documentation." + "shortDescription": "Technical consulting for SAP CDC implementation. Created authorization screens, backend endpoints, and comprehensive documentation.", + "companyID": "livgolf" }, { "position": "Senior Technical Consultant", @@ -114,7 +116,8 @@ "Managed identity flows for millions of users across web and mobile platforms" ], "companyLogo": "aena.png", - "shortDescription": "Lead Technical Consultant for AENA Airports Authentication System serving millions of passengers across all Spanish airports." + "shortDescription": "Lead Technical Consultant for AENA Airports Authentication System serving millions of passengers across all Spanish airports.", + "companyID": "aena" }, { "position": "Senior Technical Consultant", @@ -140,7 +143,8 @@ "Technical Documentation" ], "companyLogo": "sap.png", - "shortDescription": "SAP Customer Data Cloud technical consulting, troubleshooting, and stakeholder education on GDPR compliance." + "shortDescription": "SAP Customer Data Cloud technical consulting, troubleshooting, and stakeholder education on GDPR compliance.", + "companyID": "sap" }, { "position": "Junior Technical Consultant", @@ -165,7 +169,8 @@ "System Monitoring" ], "companyLogo": "gigya.png", - "shortDescription": "Technical support and problem-solving for Gigya platform. System monitoring and training program development." + "shortDescription": "Technical support and problem-solving for Gigya platform. System monitoring and training program development.", + "companyID": "gigya" }, { "position": "Director / Freelance Fullstack Developer", @@ -196,7 +201,8 @@ "DevOps" ], "companyLogo": "drosoloft-plain.png", - "shortDescription": "Freelance work for multiple clients (Megabanner, Ebantic, Everis, Indra) developing React applications, designing APIs, integrating video systems and managing projects." + "shortDescription": "Freelance work for multiple clients (Megabanner, Ebantic, Everis, Indra) developing React applications, designing APIs, integrating video systems and managing projects.", + "companyID": "drosoloft" }, { "position": "Technical Director / Programmer", @@ -224,7 +230,8 @@ "Successfully managed technical team and product development" ], "companyLogo": "emailing-network.png", - "shortDescription": "Technical Director leading development of backend and 5 websites. Reduced production times by 75%." + "shortDescription": "Technical Director leading development of backend and 5 websites. Reduced production times by 75%.", + "companyID": "emailing-network" }, { "position": "Programmer Analyst (Freelance)", @@ -245,7 +252,8 @@ "JavaScript" ], "companyLogo": "twentic.png", - "shortDescription": "WordPress and PHP website development as freelance programmer." + "shortDescription": "WordPress and PHP website development as freelance programmer.", + "companyID": "twentic" }, { "position": "Analyst Programmer / Expert Technician", @@ -267,7 +275,8 @@ "System Configuration", "Technical Support" ], - "shortDescription": "Software and hardware configuration, technical problem-solving, and team mentoring." + "shortDescription": "Software and hardware configuration, technical problem-solving, and team mentoring.", + "companyID": "pentamsi" }, { "position": "Senior Programmer", @@ -288,7 +297,8 @@ "Search Engine Technology", "European R&D Projects" ], - "shortDescription": "European R&D project for revolutionary search engine development." + "shortDescription": "European R&D project for revolutionary search engine development.", + "companyID": "webratio" }, { "position": "Junior Programmer", @@ -311,7 +321,8 @@ "Data Visualization", "Chart Generation" ], - "shortDescription": "JAVA development specialized in data chart generation and applet development." + "shortDescription": "JAVA development specialized in data chart generation and applet development.", + "companyID": "insa" } ], "education": [ @@ -533,96 +544,124 @@ ], "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.", + "title": "Somos Una Ola - Beach Cleaning Initiative", + "projectName": "Somos Una Ola", + "projectDesc": "Beach Cleaning Initiative", + "url": "https://somosunaola.org", + "projectLogo": "somosunaola.png", + "location": "La Palma, Canary Islands", + "startDate": "2023-07", + "current": true, "technologies": [ - "SAP CDC", - "React", "Node.js", - "Authentication", - "Mobile" + "Express.js", + "HTMX" ], - "highlights": [ - "Deployed across all Spanish airports", - "Handles millions of user authentications", - "Integrated with multiple AENA digital platforms" - ] + "shortDescription": "Volunteer project promoting beach cleaning on La Palma island. Created their website to publish completed cleanings and schedule future events.", + "responsibilities": [ + "Designed and developed full-stack website using Node.js Express and HTMX", + "Implemented event publishing system for completed and upcoming beach cleanings", + "Supported environmental initiative that has completed 18 cleanings across 12 beaches" + ], + "projectID": "somos-una-ola" }, { - "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.", + "title": "Herrumbre Vivo Arte - Artist Portfolio Website", + "projectName": "Herrumbre Vivo Arte", + "projectDesc": "Artist Portfolio Website", + "url": "https://herrumbrevivoarte.com", + "projectLogo": "herrumbre-vivo.png", + "location": "Fuencaliente, La Palma", + "startDate": "2024", + "current": true, "technologies": [ - "SAP CDC", - "React", - "JavaScript", - "Template Development" + "Web Development", + "Portfolio Design" ], - "highlights": [ - "Open-source contribution to SAP ecosystem", - "Used by developers worldwide", - "Simplifies SAP CDC integration" - ] + "shortDescription": "Portfolio website for Gustavo Díaz, artisan who transforms recycled materials into sculptures. Promotes environmental art and sustainable creativity.", + "responsibilities": [ + "Created online presence for recycled art project focused on sustainability", + "Showcased sculptures made from metal, plastic, glass, and wood waste", + "Highlighted environmental workshops and educational mission aligned with Sustainable Development Goals" + ], + "projectID": "herrumbre-vivo-arte" }, { - "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.", + "title": "La Porra.club - Football Prediction Platform", + "projectName": "La Porra.club", + "projectDesc": "Football Prediction Platform", + "url": "https://laporra.club", + "projectLogo": "laporra.png", + "gitRepoUrl": "/Users/txeo/laporra", + "location": "Online", + "current": true, "technologies": [ - "Claude Code", + "Node.js", + "Hono", "HTMX", - "Go", - "Tailwind CSS", - "AI APIs", - "Prompt Engineering" + "Panini Templates", + "Server-Side Rendering" ], - "highlights": [ - "Reduced development time by 60% using AI-assisted workflows", - "Modernized legacy applications with AI guidance", - "Created reusable patterns for HTMX + Go development" - ] + "shortDescription": "Private invitation-only platform for friends to predict football competition results. Features gamification with digital rewards and competitive scoring system.", + "responsibilities": [ + "Built full-stack application using Node.js, Hono server, and HTMX for reactive frontend", + "Implemented server-side rendering with Panini template engine for optimal performance", + "Designed prediction algorithm and scoring system with gamification mechanics", + "Created private invitation system for exclusive friend group access" + ], + "projectID": "la-porraclub" }, { - "name": "React & Node.js Projects", - "role": "Technical Lead & Developer", - "period": "2015-2017", - "description": "Multiple projects for clients including Megabanner, Cepsa, Cazatucasa", + "title": "CDC Starter Kit - SAP Customer Data Cloud Demo", + "projectName": "CDC Starter Kit", + "projectDesc": "SAP Customer Data Cloud Demo", + "url": "https://gigyademo.com/cdc-starter-kit/", + "projectLogo": "sap.png", + "location": "Online", + "startDate": "2018", + "current": true, + "maintainedBy": "SAP", "technologies": [ + "SAP CDC", + "JavaScript", + "React", + "API Integration", + "Authentication" + ], + "shortDescription": "Comprehensive demonstration and starter kit for SAP Customer Data Cloud. Complete implementation showcase created 100% independently as public GitHub resource. Now maintained by SAP.", + "responsibilities": [ + "Designed and developed complete CDC implementation demonstration from scratch as official SAP resource", + "Created comprehensive starter kit with authentication, user management, and data flow examples", + "Built reusable components and integration patterns for SAP CDC", + "Provided technical documentation and best practices for enterprise identity management", + "Project now maintained by SAP as official public resource" + ], + "projectID": "cdc-starter-kit" + }, + { + "title": "Third Party Contributions", + "url": "", + "projectLogo": "", + "location": "Various", + "startDate": "2015", + "endDate": "2016", + "current": true, + "technologies": [ + "JavaScript", "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" - ] + "Web Development" + ], + "shortDescription": "Collection of client projects and websites including Lidering, Jorpack, Delivery Bikes BCN, and Mobbeel where I contributed to development, implementation, and technical solutions across various industries.", + "responsibilities": [ + "Lidering
Lidering (via Twentic) 2015: Developed and implemented comprehensive real estate and property management platform with advanced search functionality, property listings, and client management features
", + "Jorpack
Jorpack (via Twentic) 2015: Created corporate website and e-commerce solution for industrial packaging company, featuring product catalog, custom quote system, and business process integration
", + "Delivery Bikes BCN
Delivery Bikes BCN 2016: Built web platform for bicycle delivery service in Barcelona, including route optimization, real-time tracking, and customer booking system
", + "
Mobbeel 2015: Designed and developed corporate website for biometric authentication and identity verification solutions provider, showcasing security products and enterprise services
" + ], + "projectID": "third-party-contributions" } ], "awards": [ @@ -694,7 +733,8 @@ "responsibilities": [ "
Intro to AI Transformers Course April 2024: Comprehensive introduction to transformer architecture and AI models, covering attention mechanisms, encoder-decoder structures, and practical applications in natural language processing
", "
Learn React Course March 2022: Complete React framework training covering components, state management, hooks, lifecycle methods, and modern React development practices
" - ] + ], + "courseID": "codecademy-certifications" }, { "title": "LinkedIn Learning Certifications", @@ -710,7 +750,8 @@ "
Learning Android Security February 2020: Android security best practices, encryption methods, secure coding practices, and mobile application security fundamentals
", "
Persuasive UX: Creating Credibility January 2020: User experience design principles focused on building trust, credibility, and persuasive design patterns for web applications
", "
Big Data Foundations: Techniques and Concepts December 2019: Fundamentals of big data technologies, distributed computing, data processing frameworks, and analytics techniques
" - ] + ], + "courseID": "linkedin-learning-certificatio" }, { "title": "Servoy World 2011", @@ -724,7 +765,8 @@ "Attended conferences on Servoy development", "Learned about latest features and platform best practices", "Networked with Servoy developers from around the world" - ] + ], + "courseID": "servoy-world-2011" }, { "title": "Train the Trainers", @@ -738,7 +780,8 @@ "Learned advanced didactic methodologies for professional teaching", "Developed pedagogical skills for technical training delivery", "Obtained official certification as Professional Trainer" - ] + ], + "courseID": "train-the-trainers" }, { "title": "Windows 2003 Server", @@ -752,7 +795,8 @@ "Learned Windows Server 2003 installation and configuration", "Practiced user and permission management in Active Directory", "Developed skills in network services administration" - ] + ], + "courseID": "windows-2003-server" }, { "title": "1st Extremadura Conference on Software Industry", @@ -766,7 +810,8 @@ "Attended presentations on software industry trends", "Participated in practical development workshops", "Networked with regional technology sector professionals" - ] + ], + "courseID": "1st-extremadura-conference-on-" }, { "title": "Web Application Development: Apache, PHP and MySQL", @@ -780,98 +825,8 @@ "Learned Apache web server configuration and administration", "Developed dynamic web applications using PHP", "Designed and implemented MySQL databases for web applications" - ] - } - ], - "projects": [ - { - "title": "Somos Una Ola - Beach Cleaning Initiative", - "projectName": "Somos Una Ola", - "projectDesc": "Beach Cleaning Initiative", - "url": "https://somosunaola.org", - "projectLogo": "somosunaola.png", - "location": "La Palma, Canary Islands", - "startDate": "2023-07", - "current": true, - "technologies": ["Node.js", "Express.js", "HTMX"], - "shortDescription": "Volunteer project promoting beach cleaning on La Palma island. Created their website to publish completed cleanings and schedule future events.", - "responsibilities": [ - "Designed and developed full-stack website using Node.js Express and HTMX", - "Implemented event publishing system for completed and upcoming beach cleanings", - "Supported environmental initiative that has completed 18 cleanings across 12 beaches" - ] - }, - { - "title": "Herrumbre Vivo Arte - Artist Portfolio Website", - "projectName": "Herrumbre Vivo Arte", - "projectDesc": "Artist Portfolio Website", - "url": "https://herrumbrevivoarte.com", - "projectLogo": "herrumbre-vivo.png", - "location": "Fuencaliente, La Palma", - "startDate": "2024", - "current": true, - "technologies": ["Web Development", "Portfolio Design"], - "shortDescription": "Portfolio website for Gustavo Díaz, artisan who transforms recycled materials into sculptures. Promotes environmental art and sustainable creativity.", - "responsibilities": [ - "Created online presence for recycled art project focused on sustainability", - "Showcased sculptures made from metal, plastic, glass, and wood waste", - "Highlighted environmental workshops and educational mission aligned with Sustainable Development Goals" - ] - }, - { - "title": "La Porra.club - Football Prediction Platform", - "projectName": "La Porra.club", - "projectDesc": "Football Prediction Platform", - "url": "https://laporra.club", - "projectLogo": "laporra.png", - "gitRepoUrl": "/Users/txeo/laporra", - "location": "Online", - "current": true, - "technologies": ["Node.js", "Hono", "HTMX", "Panini Templates", "Server-Side Rendering"], - "shortDescription": "Private invitation-only platform for friends to predict football competition results. Features gamification with digital rewards and competitive scoring system.", - "responsibilities": [ - "Built full-stack application using Node.js, Hono server, and HTMX for reactive frontend", - "Implemented server-side rendering with Panini template engine for optimal performance", - "Designed prediction algorithm and scoring system with gamification mechanics", - "Created private invitation system for exclusive friend group access" - ] - }, - { - "title": "CDC Starter Kit - SAP Customer Data Cloud Demo", - "projectName": "CDC Starter Kit", - "projectDesc": "SAP Customer Data Cloud Demo", - "url": "https://gigyademo.com/cdc-starter-kit/", - "projectLogo": "sap.png", - "location": "Online", - "startDate": "2018", - "current": true, - "maintainedBy": "SAP", - "technologies": ["SAP CDC", "JavaScript", "React", "API Integration", "Authentication"], - "shortDescription": "Comprehensive demonstration and starter kit for SAP Customer Data Cloud. Complete implementation showcase created 100% independently as public GitHub resource. Now maintained by SAP.", - "responsibilities": [ - "Designed and developed complete CDC implementation demonstration from scratch as official SAP resource", - "Created comprehensive starter kit with authentication, user management, and data flow examples", - "Built reusable components and integration patterns for SAP CDC", - "Provided technical documentation and best practices for enterprise identity management", - "Project now maintained by SAP as official public resource" - ] - }, - { - "title": "Third Party Contributions", - "url": "", - "projectLogo": "", - "location": "Various", - "startDate": "2015", - "endDate": "2016", - "current": true, - "technologies": ["JavaScript", "React", "Node.js", "PHP", "WordPress", "Web Development"], - "shortDescription": "Collection of client projects and websites including Lidering, Jorpack, Delivery Bikes BCN, and Mobbeel where I contributed to development, implementation, and technical solutions across various industries.", - "responsibilities": [ - "Lidering
Lidering (via Twentic) 2015: Developed and implemented comprehensive real estate and property management platform with advanced search functionality, property listings, and client management features
", - "Jorpack
Jorpack (via Twentic) 2015: Created corporate website and e-commerce solution for industrial packaging company, featuring product catalog, custom quote system, and business process integration
", - "Delivery Bikes BCN
Delivery Bikes BCN 2016: Built web platform for bicycle delivery service in Barcelona, including route optimization, real-time tracking, and customer booking system
", - "
Mobbeel 2015: Designed and developed corporate website for biometric authentication and identity verification solutions provider, showcasing security products and enterprise services
" - ] + ], + "courseID": "web-application-development-ap" } ], "references": [ @@ -935,4 +890,4 @@ "format": "JSON Resume Extended", "language": "en" } -} \ No newline at end of file +} diff --git a/data/cv-es.json b/data/cv-es.json index e395233..6555c1a 100644 --- a/data/cv-es.json +++ b/data/cv-es.json @@ -55,7 +55,8 @@ "Integración de APIs" ], "companyLogo": "olympic-broadcasting.png", - "shortDescription": "Soluciones SAP CDC para eventos de transmisión internacional. Implementaciones personalizadas y orientación técnica." + "shortDescription": "Soluciones SAP CDC para eventos de transmisión internacional. Implementaciones personalizadas y orientación técnica.", + "companyID": "olympic-broadcasting" }, { "position": "Consultor Técnico Senior SAP/CDC", @@ -82,7 +83,8 @@ "Sistemas de Autenticación" ], "companyLogo": "livgolf.png", - "shortDescription": "Consultoría técnica para implementación SAP CDC. Creación de pantallas de autorización, endpoints backend y documentación completa." + "shortDescription": "Consultoría técnica para implementación SAP CDC. Creación de pantallas de autorización, endpoints backend y documentación completa.", + "companyID": "livgolf" }, { "position": "Consultor Técnico Senior", @@ -114,7 +116,8 @@ "Gestión de flujos de identidad para millones de usuarios en plataformas web y móviles" ], "companyLogo": "aena.png", - "shortDescription": "Consultor Técnico Principal del Sistema de Autenticación de Aeropuertos AENA sirviendo a millones de pasajeros en todos los aeropuertos españoles." + "shortDescription": "Consultor Técnico Principal del Sistema de Autenticación de Aeropuertos AENA sirviendo a millones de pasajeros en todos los aeropuertos españoles.", + "companyID": "aena" }, { "position": "Consultor Técnico Senior", @@ -140,7 +143,8 @@ "Documentación Técnica" ], "companyLogo": "sap.png", - "shortDescription": "Consultoría técnica SAP Customer Data Cloud, resolución de problemas y educación de stakeholders en cumplimiento GDPR." + "shortDescription": "Consultoría técnica SAP Customer Data Cloud, resolución de problemas y educación de stakeholders en cumplimiento GDPR.", + "companyID": "sap" }, { "position": "Consultor Técnico Junior", @@ -165,7 +169,8 @@ "Monitoreo de Sistemas" ], "companyLogo": "gigya.png", - "shortDescription": "Soporte técnico y resolución de problemas para plataforma Gigya. Monitoreo de sistemas y desarrollo de programas de formación." + "shortDescription": "Soporte técnico y resolución de problemas para plataforma Gigya. Monitoreo de sistemas y desarrollo de programas de formación.", + "companyID": "gigya" }, { "position": "Director / Desarrollador Fullstack Freelance", @@ -196,7 +201,8 @@ "DevOps" ], "companyLogo": "drosoloft-plain.png", - "shortDescription": "Trabajo freelance para múltiples clientes (Megabanner, Ebantic, Everis, Indra) desarrollando aplicaciones React, diseñando APIs, integrando sistemas de video y gestionando proyectos." + "shortDescription": "Trabajo freelance para múltiples clientes (Megabanner, Ebantic, Everis, Indra) desarrollando aplicaciones React, diseñando APIs, integrando sistemas de video y gestionando proyectos.", + "companyID": "drosoloft" }, { "position": "Director Técnico / Programador", @@ -224,7 +230,8 @@ "Gestión exitosa de equipo técnico y desarrollo de productos" ], "companyLogo": "emailing-network.png", - "shortDescription": "Director Técnico liderando desarrollo de backend y 5 sitios web. Reducción del 75% en tiempos de producción." + "shortDescription": "Director Técnico liderando desarrollo de backend y 5 sitios web. Reducción del 75% en tiempos de producción.", + "companyID": "emailing-network" }, { "position": "Analista Programador (Freelance)", @@ -245,7 +252,8 @@ "JavaScript" ], "companyLogo": "twentic.png", - "shortDescription": "Desarrollo de sitios web WordPress y PHP como programador freelance." + "shortDescription": "Desarrollo de sitios web WordPress y PHP como programador freelance.", + "companyID": "twentic" }, { "position": "Analista Programador / Técnico Experto", @@ -267,7 +275,8 @@ "Configuración de Sistemas", "Soporte Técnico" ], - "shortDescription": "Configuración de software y hardware, resolución de problemas técnicos y mentoría de equipos." + "shortDescription": "Configuración de software y hardware, resolución de problemas técnicos y mentoría de equipos.", + "companyID": "pentamsi" }, { "position": "Programador Senior", @@ -288,7 +297,8 @@ "Tecnología de Motores de Búsqueda", "Proyectos Europeos I+D" ], - "shortDescription": "Proyecto europeo I+D para desarrollo de motor de búsqueda revolucionario." + "shortDescription": "Proyecto europeo I+D para desarrollo de motor de búsqueda revolucionario.", + "companyID": "webratio" }, { "position": "Programador Junior", @@ -311,7 +321,8 @@ "Visualización de Datos", "Generación de Gráficos" ], - "shortDescription": "Desarrollo JAVA especializado en generación de gráficos de datos y desarrollo de applets." + "shortDescription": "Desarrollo JAVA especializado en generación de gráficos de datos y desarrollo de applets.", + "companyID": "insa" } ], "education": [ @@ -538,96 +549,124 @@ ], "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.", + "title": "Somos Una Ola - Iniciativa de Limpieza de Playas", + "projectName": "Somos Una Ola", + "projectDesc": "Iniciativa de Limpieza de Playas", + "url": "https://somosunaola.org", + "projectLogo": "somosunaola.png", + "location": "La Palma, Islas Canarias", + "startDate": "2023-07", + "current": true, "technologies": [ - "SAP CDC", - "React", "Node.js", - "Autenticación", - "Móvil" + "Express.js", + "HTMX" ], - "highlights": [ - "Desplegado en todos los aeropuertos españoles", - "Gestiona millones de autenticaciones de usuarios", - "Integrado con múltiples plataformas digitales AENA" - ] + "shortDescription": "Proyecto de voluntariado que promueve la limpieza de playas en la isla de La Palma. Creación de su sitio web para publicar limpiezas realizadas y programar eventos futuros.", + "responsibilities": [ + "Diseñé y desarrollé sitio web full-stack usando Node.js Express y HTMX", + "Implementé sistema de publicación de eventos para limpiezas realizadas y futuras", + "Apoyé iniciativa ambiental que ha completado 18 limpiezas en 12 playas diferentes" + ], + "projectID": "somos-una-ola" }, { - "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.", + "title": "Herrumbre Vivo Arte - Sitio Web Portfolio de Artista", + "projectName": "Herrumbre Vivo Arte", + "projectDesc": "Sitio Web Portfolio de Artista", + "url": "https://herrumbrevivoarte.com", + "projectLogo": "herrumbre-vivo.png", + "location": "Fuencaliente, La Palma", + "startDate": "2024", + "current": true, "technologies": [ - "SAP CDC", - "React", - "JavaScript", - "Desarrollo de Plantillas" + "Desarrollo Web", + "Diseño de Portfolio" ], - "highlights": [ - "Contribución de código abierto al ecosistema SAP", - "Usado por desarrolladores en todo el mundo", - "Simplifica la integración de SAP CDC" - ] + "shortDescription": "Sitio web portfolio para Gustavo Díaz, artesano que transforma materiales reciclados en esculturas. Promueve arte ambiental y creatividad sostenible.", + "responsibilities": [ + "Creé presencia online para proyecto de arte reciclado enfocado en sostenibilidad", + "Mostré esculturas hechas de desechos metálicos, plásticos, vidrio y madera", + "Destaqué talleres ambientales y misión educativa alineada con Objetivos de Desarrollo Sostenible" + ], + "projectID": "herrumbre-vivo-arte" }, { - "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.", + "title": "La Porra.club - Plataforma de Predicción de Fútbol", + "projectName": "La Porra.club", + "projectDesc": "Plataforma de Predicción de Fútbol", + "url": "https://laporra.club", + "projectLogo": "laporra.png", + "gitRepoUrl": "/Users/txeo/laporra", + "location": "Online", + "current": true, "technologies": [ - "Claude Code", + "Node.js", + "Hono", "HTMX", - "Go", - "Tailwind CSS", - "APIs IA", - "Ingeniería de Prompts" + "Plantillas Panini", + "Renderizado del Lado del Servidor" ], - "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" - ] + "shortDescription": "Plataforma privada de acceso por invitación para amigos para predecir resultados de competiciones de fútbol. Incluye gamificación con recompensas digitales y sistema de puntuación competitivo.", + "responsibilities": [ + "Desarrollé aplicación full-stack usando Node.js, servidor Hono y HTMX para frontend reactivo", + "Implementé renderizado del lado del servidor con motor de plantillas Panini para rendimiento óptimo", + "Diseñé algoritmo de predicción y sistema de puntuación con mecánicas de gamificación", + "Creé sistema de invitación privada para acceso exclusivo del grupo de amigos" + ], + "projectID": "la-porraclub" }, { - "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", + "title": "CDC Starter Kit - Demo de SAP Customer Data Cloud", + "projectName": "CDC Starter Kit", + "projectDesc": "Demo de SAP Customer Data Cloud", + "url": "https://gigyademo.com/cdc-starter-kit/", + "projectLogo": "sap.png", + "location": "Online", + "startDate": "2018", + "current": true, + "maintainedBy": "SAP", "technologies": [ + "SAP CDC", + "JavaScript", + "React", + "Integración de APIs", + "Autenticación" + ], + "shortDescription": "Demostración completa y kit de inicio para SAP Customer Data Cloud. Proyecto de implementación completa creado 100% de forma independiente como recurso público en GitHub. Ahora mantenido por SAP.", + "responsibilities": [ + "Diseñé y desarrollé demostración completa de implementación de CDC desde cero como recurso oficial de SAP", + "Creé kit de inicio integral con autenticación, gestión de usuarios y ejemplos de flujo de datos", + "Desarrollé componentes reutilizables y patrones de integración para SAP CDC", + "Proporcioné documentación técnica y mejores prácticas para gestión empresarial de identidades", + "Proyecto ahora mantenido por SAP como recurso público oficial" + ], + "projectID": "cdc-starter-kit" + }, + { + "title": "Contribuciones a Proyectos de Terceros", + "url": "", + "projectLogo": "", + "location": "Varios", + "startDate": "2015", + "endDate": "2016", + "current": true, + "technologies": [ + "JavaScript", "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" - ] + "Desarrollo Web" + ], + "shortDescription": "Colección de proyectos de clientes y sitios web incluyendo Lidering, Jorpack, Delivery Bikes BCN y Mobbeel donde contribuí al desarrollo, implementación y soluciones técnicas en diversas industrias.", + "responsibilities": [ + "Lidering
Lidering (a través de Twentic) 2015: Desarrollé e implementé plataforma integral de gestión inmobiliaria y propiedades con funcionalidad avanzada de búsqueda, listado de propiedades y gestión de clientes
", + "Jorpack
Jorpack (a través de Twentic) 2015: Creé sitio web corporativo y solución e-commerce para empresa de embalaje industrial, con catálogo de productos, sistema de presupuestos personalizados e integración de procesos de negocio
", + "Delivery Bikes BCN
Delivery Bikes BCN 2016: Construí plataforma web para servicio de entrega en bicicleta en Barcelona, incluyendo optimización de rutas, seguimiento en tiempo real y sistema de reservas para clientes
", + "
Mobbeel 2015: Diseñé y desarrollé sitio web corporativo para proveedor de soluciones de autenticación biométrica y verificación de identidad, mostrando productos de seguridad y servicios empresariales
" + ], + "projectID": "contribuciones-a-proyectos-de-" } ], "awards": [ @@ -699,7 +738,8 @@ "responsibilities": [ "
Intro to AI Transformers Course Abril 2024: Introducción completa a la arquitectura de transformers y modelos de IA, cubriendo mecanismos de atención, estructuras encoder-decoder y aplicaciones prácticas en procesamiento de lenguaje natural
", "
Learn React Course Marzo 2022: Formación completa en React framework cubriendo componentes, gestión de estado, hooks, métodos de ciclo de vida y prácticas modernas de desarrollo con React
" - ] + ], + "courseID": "certificaciones-codecademy" }, { "title": "Certificaciones LinkedIn Learning", @@ -715,7 +755,8 @@ "
Learning Android Security Febrero 2020: Mejores prácticas de seguridad Android, métodos de encriptación, prácticas de codificación segura y fundamentos de seguridad de aplicaciones móviles
", "
Persuasive UX: Creating Credibility Enero 2020: Principios de diseño de experiencia de usuario enfocados en generar confianza, credibilidad y patrones de diseño persuasivo para aplicaciones web
", "
Big Data Foundations: Techniques and Concepts Diciembre 2019: Fundamentos de tecnologías big data, computación distribuida, frameworks de procesamiento de datos y técnicas de análisis
" - ] + ], + "courseID": "certificaciones-linkedin-learn" }, { "title": "Servoy World 2011", @@ -729,7 +770,8 @@ "Asistí a conferencias sobre desarrollo con Servoy", "Aprendí sobre las últimas características y mejores prácticas de la plataforma", "Hice networking con desarrolladores Servoy de todo el mundo" - ] + ], + "courseID": "servoy-world-2011" }, { "title": "Formador de Formadores", @@ -743,7 +785,8 @@ "Aprendí metodologías didácticas avanzadas para la enseñanza profesional", "Desarrollé habilidades pedagógicas para impartir formación técnica", "Obtuve certificación oficial como Formador de Formadores" - ] + ], + "courseID": "formador-de-formadores" }, { "title": "Windows 2003 Server", @@ -757,7 +800,8 @@ "Aprendí instalación y configuración de Windows Server 2003", "Practiqué gestión de usuarios y permisos en Active Directory", "Desarrollé habilidades en administración de servicios de red" - ] + ], + "courseID": "windows-2003-server" }, { "title": "I Jornada Extremeña sobre la Industria del Software", @@ -771,7 +815,8 @@ "Asistí a ponencias sobre tendencias en la industria del software", "Participé en talleres prácticos de desarrollo", "Hice networking con profesionales del sector tecnológico regional" - ] + ], + "courseID": "i-jornada-extremea-sobre-la-in" }, { "title": "Desarrollo de aplicaciones Web: Apache, PHP y MySQL", @@ -785,98 +830,8 @@ "Aprendí configuración y administración del servidor web Apache", "Desarrollé aplicaciones web dinámicas usando PHP", "Diseñé e implementé bases de datos MySQL para aplicaciones web" - ] - } - ], - "projects": [ - { - "title": "Somos Una Ola - Iniciativa de Limpieza de Playas", - "projectName": "Somos Una Ola", - "projectDesc": "Iniciativa de Limpieza de Playas", - "url": "https://somosunaola.org", - "projectLogo": "somosunaola.png", - "location": "La Palma, Islas Canarias", - "startDate": "2023-07", - "current": true, - "technologies": ["Node.js", "Express.js", "HTMX"], - "shortDescription": "Proyecto de voluntariado que promueve la limpieza de playas en la isla de La Palma. Creación de su sitio web para publicar limpiezas realizadas y programar eventos futuros.", - "responsibilities": [ - "Diseñé y desarrollé sitio web full-stack usando Node.js Express y HTMX", - "Implementé sistema de publicación de eventos para limpiezas realizadas y futuras", - "Apoyé iniciativa ambiental que ha completado 18 limpiezas en 12 playas diferentes" - ] - }, - { - "title": "Herrumbre Vivo Arte - Sitio Web Portfolio de Artista", - "projectName": "Herrumbre Vivo Arte", - "projectDesc": "Sitio Web Portfolio de Artista", - "url": "https://herrumbrevivoarte.com", - "projectLogo": "herrumbre-vivo.png", - "location": "Fuencaliente, La Palma", - "startDate": "2024", - "current": true, - "technologies": ["Desarrollo Web", "Diseño de Portfolio"], - "shortDescription": "Sitio web portfolio para Gustavo Díaz, artesano que transforma materiales reciclados en esculturas. Promueve arte ambiental y creatividad sostenible.", - "responsibilities": [ - "Creé presencia online para proyecto de arte reciclado enfocado en sostenibilidad", - "Mostré esculturas hechas de desechos metálicos, plásticos, vidrio y madera", - "Destaqué talleres ambientales y misión educativa alineada con Objetivos de Desarrollo Sostenible" - ] - }, - { - "title": "La Porra.club - Plataforma de Predicción de Fútbol", - "projectName": "La Porra.club", - "projectDesc": "Plataforma de Predicción de Fútbol", - "url": "https://laporra.club", - "projectLogo": "laporra.png", - "gitRepoUrl": "/Users/txeo/laporra", - "location": "Online", - "current": true, - "technologies": ["Node.js", "Hono", "HTMX", "Plantillas Panini", "Renderizado del Lado del Servidor"], - "shortDescription": "Plataforma privada de acceso por invitación para amigos para predecir resultados de competiciones de fútbol. Incluye gamificación con recompensas digitales y sistema de puntuación competitivo.", - "responsibilities": [ - "Desarrollé aplicación full-stack usando Node.js, servidor Hono y HTMX para frontend reactivo", - "Implementé renderizado del lado del servidor con motor de plantillas Panini para rendimiento óptimo", - "Diseñé algoritmo de predicción y sistema de puntuación con mecánicas de gamificación", - "Creé sistema de invitación privada para acceso exclusivo del grupo de amigos" - ] - }, - { - "title": "CDC Starter Kit - Demo de SAP Customer Data Cloud", - "projectName": "CDC Starter Kit", - "projectDesc": "Demo de SAP Customer Data Cloud", - "url": "https://gigyademo.com/cdc-starter-kit/", - "projectLogo": "sap.png", - "location": "Online", - "startDate": "2018", - "current": true, - "maintainedBy": "SAP", - "technologies": ["SAP CDC", "JavaScript", "React", "Integración de APIs", "Autenticación"], - "shortDescription": "Demostración completa y kit de inicio para SAP Customer Data Cloud. Proyecto de implementación completa creado 100% de forma independiente como recurso público en GitHub. Ahora mantenido por SAP.", - "responsibilities": [ - "Diseñé y desarrollé demostración completa de implementación de CDC desde cero como recurso oficial de SAP", - "Creé kit de inicio integral con autenticación, gestión de usuarios y ejemplos de flujo de datos", - "Desarrollé componentes reutilizables y patrones de integración para SAP CDC", - "Proporcioné documentación técnica y mejores prácticas para gestión empresarial de identidades", - "Proyecto ahora mantenido por SAP como recurso público oficial" - ] - }, - { - "title": "Contribuciones a Proyectos de Terceros", - "url": "", - "projectLogo": "", - "location": "Varios", - "startDate": "2015", - "endDate": "2016", - "current": true, - "technologies": ["JavaScript", "React", "Node.js", "PHP", "WordPress", "Desarrollo Web"], - "shortDescription": "Colección de proyectos de clientes y sitios web incluyendo Lidering, Jorpack, Delivery Bikes BCN y Mobbeel donde contribuí al desarrollo, implementación y soluciones técnicas en diversas industrias.", - "responsibilities": [ - "Lidering
Lidering (a través de Twentic) 2015: Desarrollé e implementé plataforma integral de gestión inmobiliaria y propiedades con funcionalidad avanzada de búsqueda, listado de propiedades y gestión de clientes
", - "Jorpack
Jorpack (a través de Twentic) 2015: Creé sitio web corporativo y solución e-commerce para empresa de embalaje industrial, con catálogo de productos, sistema de presupuestos personalizados e integración de procesos de negocio
", - "Delivery Bikes BCN
Delivery Bikes BCN 2016: Construí plataforma web para servicio de entrega en bicicleta en Barcelona, incluyendo optimización de rutas, seguimiento en tiempo real y sistema de reservas para clientes
", - "
Mobbeel 2015: Diseñé y desarrollé sitio web corporativo para proveedor de soluciones de autenticación biométrica y verificación de identidad, mostrando productos de seguridad y servicios empresariales
" - ] + ], + "courseID": "desarrollo-de-aplicaciones-web" } ], "references": [ @@ -940,4 +895,4 @@ "format": "JSON Resume Extended", "language": "es" } -} \ No newline at end of file +} diff --git a/data/ui-en.json b/data/ui-en.json index d2d3a36..d8e4c02 100644 --- a/data/ui-en.json +++ b/data/ui-en.json @@ -121,6 +121,10 @@ }, "actions": { "title": "Actions", + "cmdK": { + "key": "⌘/Ctrl K", + "description": "Open command bar" + }, "print": { "key": "Ctrl / Cmd + P", "description": "Print or save as PDF" @@ -187,6 +191,38 @@ "title": "Error" } }, + "cmdK": { + "placeholder": "Type a command or search...", + "noResults": "No results found", + "sections": { + "navigation": "Navigation", + "shortcuts": "Shortcuts", + "downloads": "Downloads" + }, + "actions": { + "jumpToExperience": "Jump to Experience", + "jumpToEducation": "Jump to Education", + "jumpToSkills": "Jump to Skills", + "jumpToProjects": "Jump to Projects", + "jumpToCourses": "Jump to Courses", + "jumpToLanguages": "Jump to Languages", + "jumpToAwards": "Jump to Awards", + "toggleLength": "Toggle CV Length", + "toggleIcons": "Toggle Icons", + "toggleTheme": "Toggle Theme", + "showShortcuts": "Show Keyboard Shortcuts", + "print": "Print CV", + "downloadPdfShort": "Download PDF (Short)", + "downloadPdfDefault": "Download PDF (Default)", + "downloadPdfExtended": "Download PDF (Extended)", + "viewTextCv": "View Text CV", + "downloadTextCv": "Download Text CV" + }, + "button": { + "tooltip": "Command Bar", + "ariaLabel": "Open command bar (Cmd+K)" + } + }, "widgets": { "backToTop": { "ariaLabel": "Back to top", @@ -232,7 +268,9 @@ "downloadPdf": "Download as PDF", "printFriendly": "Print Friendly", "plainText": "Plain Text", - "contact": "Contact" + "contact": "Contact", + "search": "Search", + "searchAriaLabel": "Open command bar (Cmd+K)" } } } diff --git a/data/ui-es.json b/data/ui-es.json index 3552e1c..17fda97 100644 --- a/data/ui-es.json +++ b/data/ui-es.json @@ -121,6 +121,10 @@ }, "actions": { "title": "Acciones", + "cmdK": { + "key": "⌘/Ctrl K", + "description": "Abrir barra de comandos" + }, "print": { "key": "Ctrl / Cmd + P", "description": "Imprimir o guardar como PDF" @@ -187,6 +191,38 @@ "title": "Error" } }, + "cmdK": { + "placeholder": "Escribe un comando o busca...", + "noResults": "No se encontraron resultados", + "sections": { + "navigation": "Navegación", + "shortcuts": "Atajos", + "downloads": "Descargas" + }, + "actions": { + "jumpToExperience": "Ir a Experiencia", + "jumpToEducation": "Ir a Educación", + "jumpToSkills": "Ir a Habilidades", + "jumpToProjects": "Ir a Proyectos", + "jumpToCourses": "Ir a Cursos", + "jumpToLanguages": "Ir a Idiomas", + "jumpToAwards": "Ir a Premios", + "toggleLength": "Alternar Longitud del CV", + "toggleIcons": "Alternar Iconos", + "toggleTheme": "Alternar Tema", + "showShortcuts": "Mostrar Atajos de Teclado", + "print": "Imprimir CV", + "downloadPdfShort": "Descargar PDF (Corto)", + "downloadPdfDefault": "Descargar PDF (Por Defecto)", + "downloadPdfExtended": "Descargar PDF (Extendido)", + "viewTextCv": "Ver CV en Texto", + "downloadTextCv": "Descargar CV en Texto" + }, + "button": { + "tooltip": "Barra de Comandos", + "ariaLabel": "Abrir barra de comandos (Cmd+K)" + } + }, "widgets": { "backToTop": { "ariaLabel": "Volver arriba", @@ -232,7 +268,9 @@ "downloadPdf": "Descargar como PDF", "printFriendly": "Imprimir amigable", "plainText": "Texto Plano", - "contact": "Contacto" + "contact": "Contacto", + "search": "Buscar", + "searchAriaLabel": "Abrir barra de comandos (Cmd+K)" } } } diff --git a/doc/16-CMD-K-API.md b/doc/16-CMD-K-API.md new file mode 100644 index 0000000..47734ad --- /dev/null +++ b/doc/16-CMD-K-API.md @@ -0,0 +1,315 @@ +# CMD+K Command Palette API Documentation + +## Overview + +The CV application provides a command palette (CMD+K / Ctrl+K) powered by [ninja-keys](https://github.com/nickadam/ninja-keys) web component. Dynamic entries (experiences, projects, courses) are loaded from a backend API endpoint, allowing automatic updates when CV data changes without modifying JavaScript code. + +## Architecture + +### Design Decision + +**API-First Approach**: Rather than hardcoding entries in JavaScript or reading from DOM elements, the command palette fetches its dynamic data from a dedicated API endpoint. This provides: + +1. **Automatic Updates**: New CV entries appear in CMD+K without code changes +2. **Language Support**: API returns localized data based on language parameter +3. **Cache Efficiency**: 1-hour cache headers reduce redundant requests +4. **Separation of Concerns**: Frontend only handles rendering; backend owns data + +### Data Flow + +``` +User opens CMD+K (Ctrl+K / Cmd+K) + ↓ +ninja-keys-init.js initializes + ↓ +fetch('/api/cmd-k?lang={en|es}') + ↓ +Backend loads CV data from JSON files + ↓ +Maps experiences, projects, courses to actions + ↓ +Returns JSON with action arrays + ↓ +Frontend combines with static actions + ↓ +ninja-keys displays searchable command palette +``` + +## API Endpoint + +### GET /api/cmd-k + +Returns dynamic entries for the ninja-keys command palette. + +**URL**: `/api/cmd-k` +**Method**: `GET` +**Authentication**: None (public endpoint) + +#### Query Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `lang` | string | `en` | Language code (`en` or `es`) | + +#### Response + +**Content-Type**: `application/json` +**Cache-Control**: `public, max-age=3600` (1 hour) + +```json +{ + "experiences": [ + { + "id": "exp-olympic-broadcasting", + "title": "Olympic Broadcasting Services", + "section": "Experience", + "keywords": "Olympic Broadcasting Services Senior SAP Technical Consultant" + } + ], + "projects": [ + { + "id": "proj-somos-una-ola", + "title": "Somos Una Ola", + "section": "Projects", + "keywords": "Somos Una Ola Volunteer project promoting beach cleaning..." + } + ], + "courses": [ + { + "id": "course-codecademy-certifications", + "title": "Codecademy Certifications", + "section": "Courses", + "keywords": "Codecademy Certifications Codecademy" + } + ] +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `experiences` | array | Work experience entries | +| `projects` | array | Personal/professional project entries | +| `courses` | array | Course and certification entries | + +Each entry contains: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique identifier (e.g., `exp-{companyId}`, `proj-{projectId}`) | +| `title` | string | Display title for the command palette | +| `section` | string | Section label (`Experience`, `Projects`, `Courses`) | +| `keywords` | string | Searchable keywords for filtering | + +#### Example Requests + +```bash +# English (default) +curl http://localhost:1999/api/cmd-k + +# Spanish +curl http://localhost:1999/api/cmd-k?lang=es + +# With jq formatting +curl -s http://localhost:1999/api/cmd-k | jq '.' + +# Check response headers +curl -I http://localhost:1999/api/cmd-k +``` + +#### Error Responses + +| Status | Description | +|--------|-------------| +| 500 | Failed to load CV data | + +## Frontend Integration + +### ninja-keys-init.js + +The frontend JavaScript fetches from the API and combines with static actions: + +```javascript +// Fetch dynamic entries from API +async function fetchDynamicEntries() { + try { + const response = await fetch(`/api/cmd-k?lang=${lang}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return await response.json(); + } catch (error) { + console.error('Failed to fetch CMD+K data:', error); + return { experiences: [], projects: [], courses: [] }; + } +} + +// Combine with static actions +const dynamicData = await fetchDynamicEntries(); +const actions = [ + ...staticActions, + ...mapExperienceActions(dynamicData.experiences || []), + ...mapProjectActions(dynamicData.projects || []), + ...mapCourseActions(dynamicData.courses || []) +]; + +ninjaKeys.data = actions; +``` + +### Action Mapping + +Dynamic entries are converted to ninja-keys actions with handlers: + +```javascript +function mapExperienceActions(experiences) { + return experiences.map(exp => ({ + id: exp.id, + title: exp.title, + section: exp.section, + keywords: `${exp.keywords} work job career`.toLowerCase(), + icon: '', + handler: () => scrollToSection(exp.id) + })); +} +``` + +## Backend Implementation + +### Handler: cv_cmdk.go + +```go +// CmdKData returns JSON data for the ninja-keys command palette +func (h *CVHandler) CmdKData(w http.ResponseWriter, r *http.Request) { + lang := r.URL.Query().Get("lang") + if lang == "" { + lang = "en" + } + if lang != "en" && lang != "es" { + lang = "en" + } + + cv, err := models.LoadCV(lang) + if err != nil { + http.Error(w, "Failed to load CV data", http.StatusInternalServerError) + return + } + + response := CmdKResponse{ + Experiences: mapExperiences(cv.Experience), + Projects: mapProjects(cv.Projects), + Courses: mapCourses(cv.Courses), + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "public, max-age=3600") + json.NewEncoder(w).Encode(response) +} +``` + +### Route Registration + +```go +// routes/routes.go +// API routes (must be before "/" to avoid catch-all) +mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData) +``` + +## ID Convention + +IDs follow a consistent pattern matching DOM element IDs for scroll targeting: + +| Type | Pattern | Example | +|------|---------|---------| +| Experience | `exp-{companyId}` | `exp-olympic-broadcasting` | +| Project | `proj-{projectId}` | `proj-somos-una-ola` | +| Course | `course-{courseId}` | `course-codecademy-certifications` | + +These IDs correspond to HTML element IDs in the page: +```html +
...
+
...
+
...
+``` + +## Static Actions + +In addition to dynamic entries, the command palette includes static actions: + +### Navigation +- Jump to Top, Experience, Education, Skills, Projects, Courses, Languages, Awards, Other Info + +### Shortcuts +- Toggle CV Length (L key) +- Toggle Icons (I key) +- Toggle Theme (V key) +- Show Shortcuts Help (? key) +- Print CV (Cmd+P) + +### Downloads +- Download PDF (Default, Short, Extended versions) +- View/Download Text CV + +### Actions +- Open Contact Form +- Show Site Info +- Toggle Zoom Controls +- Switch Language (EN/ES) +- Change Color Theme + +### Social Links +- LinkedIn, GitHub, Domestika, Personal Website + +## Testing + +### Unit Tests (Go) + +Located at `internal/handlers/cv_cmdk_test.go`: + +```go +func TestCmdKData(t *testing.T) { + // Tests: Default language, English, Spanish, Invalid language fallback + // Validates: Status code, Content-Type, response structure, counts +} + +func TestCmdKDataCaching(t *testing.T) { + // Validates Cache-Control header +} +``` + +Run with: +```bash +go test ./internal/handlers/ -run TestCmdK -v +``` + +### E2E Tests (Playwright/Bun) + +Located at `tests/mjs/71-cmd-k-api-scroll.test.mjs`: + +Tests: +1. API returns valid JSON with expected structure +2. Experience scroll navigation works +3. Project scroll navigation works +4. Course scroll navigation works +5. Section scroll navigation works +6. Multiple sequential searches work correctly + +Run with: +```bash +HEADLESS=true bun run tests/mjs/71-cmd-k-api-scroll.test.mjs +``` + +## Performance + +- **Cache Duration**: 1 hour (reduces API calls on page refresh) +- **Response Size**: ~2-3 KB (compact JSON) +- **Load Time**: API fetched during page initialization +- **Fallback**: Empty arrays returned on error (graceful degradation) + +## Files + +| File | Purpose | +|------|---------| +| `internal/handlers/cv_cmdk.go` | API handler | +| `internal/handlers/cv_cmdk_test.go` | Unit tests | +| `internal/routes/routes.go` | Route registration | +| `static/js/ninja-keys-init.js` | Frontend integration | +| `tests/mjs/71-cmd-k-api-scroll.test.mjs` | E2E tests | diff --git a/doc/2-MODERN-WEB-TECHNIQUES.md b/doc/2-MODERN-WEB-TECHNIQUES.md index 74bf98f..9d89963 100644 --- a/doc/2-MODERN-WEB-TECHNIQUES.md +++ b/doc/2-MODERN-WEB-TECHNIQUES.md @@ -3602,4 +3602,144 @@ Test structured data: --- +### 15. Dynamic Contact Form - HTMX + Hyperscript Pattern + +**Problem:** Traditional contact forms either require full page reloads (poor UX) or heavy JavaScript frameworks (React, Vue) for dynamic behavior. + +**Solution:** HTMX for server communication + Hyperscript for declarative behavior = dynamic SPA-like experience with zero custom JavaScript. + +#### Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CONTACT FORM FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ [User Input] → [HTMX POST] → [Go Handler] → [HTML Response] │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ Honeypot + hx-post Validation Success/Error │ +│ Timestamp hx-target + Security HTML partial │ +│ hx-swap │ +│ │ +│ [Hyperscript handles UI state based on response content] │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### HTMX Configuration + +```html +
+``` + +**What Each Attribute Does:** +- `hx-post`: Send form data to server without page reload +- `hx-target`: Where to inject the server response +- `hx-swap`: How to replace content (innerHTML = replace contents) +- `hx-indicator`: Element to show during request (loading spinner) +- `hx-headers`: Additional headers for AJAX detection + +#### Hyperscript for Success Detection + +```html +_="on htmx:afterRequest + set responseDiv to document.getElementById('contact-response') + if responseDiv is not null and responseDiv.querySelector('.contact-success') is not null + -- Hide form fields on success + set formFields to me.querySelectorAll('.form-group') + repeat for field in formFields + add .hidden to field + end + add .hidden to me.querySelector('.form-actions') + add .hidden to me.querySelector('.form-note') + -- Auto-close modal after 3 seconds + wait 3s then call document.getElementById('contact-modal').close() + end" +``` + +**Key Insight:** Success is detected by checking for `.contact-success` element in the response, not HTTP status codes. This allows validation errors to return HTTP 200 (avoiding HTMX console errors) while still distinguishing success from error states. + +#### Server Response Pattern + +**Validation Error Response (HTTP 200):** +```html +
+ +
+ Error +

Message is too short (minimum 10 characters)

+
+
+``` + +**Success Response (HTTP 200):** +```html +
+ +
+ Message Sent! +

Thank you for your message. I'll get back to you soon.

+
+
+``` + +#### Bot Protection (Zero JavaScript) + +```html + + + + + +``` + +```go +// Server-side validation +if data.Website != "" { + return fmt.Errorf("spam detected: honeypot field filled") +} + +if elapsed < 2000 { // Less than 2 seconds + return fmt.Errorf("spam detected: form filled too quickly") +} +``` + +#### Benefits of This Approach + +| Aspect | Traditional SPA | HTMX + Hyperscript | +|--------|-----------------|---------------------| +| **JavaScript Size** | ~100KB+ (React/Vue) | ~15KB (HTMX) + ~8KB (Hyperscript) | +| **Build Process** | Webpack, Babel, bundler | None required | +| **Server Rendering** | Complex hydration | Native server templates | +| **Form State** | Complex state management | Declarative behavior | +| **Validation Feedback** | Custom JS handlers | HTML partial swap | +| **SEO** | SSR complexity | Works out of the box | + +#### Complete User Experience + +1. **User opens modal** → Native `` element with `showModal()` +2. **User fills form** → Standard HTML form with browser validation +3. **User submits** → HTMX sends POST, shows spinner +4. **Validation error** → Error HTML swapped into response div (no page reload) +5. **Success** → Success HTML swapped, form fields hidden, modal auto-closes + +**Result:** Full SPA-like dynamic form with: +- Zero page reloads +- Inline validation feedback +- Loading states +- Success animations +- Auto-close behavior + +**All achieved with ~50 lines of declarative code (HTML attributes + Hyperscript) instead of hundreds of lines of JavaScript.** + +--- + *This document serves as both a technical reference and a demonstration of modern web development practices that prioritize web standards, performance, progressive enhancement, AI-era SEO, and superior user experience over JavaScript-heavy solutions.* diff --git a/doc/3-API.md b/doc/3-API.md index c7af440..cc5becc 100644 --- a/doc/3-API.md +++ b/doc/3-API.md @@ -55,6 +55,8 @@ http://localhost:1999 |----------|--------|-------------|------------| | `/?lang={en\|es}` | GET | Full HTML page with CV content | Initial page load | | `/cv?lang={en\|es}` | GET | HTML partial for HTMX swaps | Language switching | +| `/text?lang={en\|es}` | GET | Plain text CV for terminal/AI | curl, text browsers | +| `/api/cmd-k?lang={en\|es}` | GET | CMD+K command palette data (JSON) | ninja-keys integration | | `/export/pdf?lang={en\|es}&length={short\|long}&icons={show\|hide}&version={extended\|clean}` | GET | Download PDF resume with parameters | Export functionality | | `/health` | GET | Health check (JSON) | Monitoring | | `/static/{path}` | GET | Static files (CSS, JS, images) | Assets | @@ -77,6 +79,12 @@ curl "http://localhost:1999/cv?lang=en" # Export PDF (short, clean version) curl -O -J "http://localhost:1999/export/pdf?lang=en&length=short&version=clean" +# CMD+K command palette data (JSON) +curl -s http://localhost:1999/api/cmd-k | jq '.experiences | length' + +# Plain text CV +curl http://localhost:1999/text?lang=en + # Static file with headers curl -I http://localhost:1999/static/css/main.css ``` diff --git a/docs/CMD-K-COMMAND-BAR.md b/docs/CMD-K-COMMAND-BAR.md new file mode 100644 index 0000000..e88ad58 --- /dev/null +++ b/docs/CMD-K-COMMAND-BAR.md @@ -0,0 +1,214 @@ +# CMD+K Command Bar - Implementation Guide + +## Overview + +A keyboard-driven command palette (similar to VS Code, Vercel, and Linear) implemented using the [ninja-keys](https://github.com/nicholascelestin/ninja-keys) Web Component. Provides quick access to all CV navigation, actions, and downloads via keyboard shortcuts. + +## Features Implemented + +### 1. Ninja Keys Integration (`static/js/ninja-keys-init.js`) +- Web Component loaded via CDN (unpkg) +- 50+ searchable actions organized by category +- Smooth animated scrolling to sections +- Modal integration for dialogs +- External link handling + +### 2. Action Categories + +#### Navigation (9 actions) +- Jump to Top, Experience, Education, Skills, Projects, Courses, Languages, Awards, Other + +#### Experience - Companies (7 actions) +- Olympic Broadcasting Services +- LIV Golf +- AENA (Spanish Airports) +- SAP +- Gigya +- Drolosoft (Freelance) +- Emailing Network + +#### Skills by Category (9 actions) +- Programming Languages +- JavaScript Ecosystem +- Go Ecosystem +- Frontend Technologies +- Backend Technologies +- Infrastructure & DevOps +- Databases +- SAP Technologies +- AI-Assisted Development + +#### Social Links (4 actions) +- LinkedIn Profile +- GitHub Profile +- Domestika Portfolio +- Personal Website + +#### Keyboard Shortcuts (5 actions) +- Toggle CV Length (L key) +- Toggle Icons (I key) +- Toggle Theme (V key) +- Show Shortcuts Help (? key) +- Print CV (Cmd/Ctrl+P) + +#### Downloads (5 actions) +- Download PDF (Default - 5 pages) +- Download PDF (Short - 4 pages) +- Download PDF (Extended - 9 pages) +- View Text CV (opens in new tab) +- Download Text CV (.txt file) + +#### Actions (6 actions) +- Open Contact Form +- Show Site Info +- Toggle Zoom Controls +- Switch to English +- Switch to Spanish +- Change Color Theme + +### 3. CSS Styling (`static/css/04-interactive/_buttons.css`) +- Custom ninja-keys theming with CSS variables +- Light and dark mode support +- Matches site design system (Quicksand font, purple accent) +- CMD+K button with hover/active states + +### 4. UI Strings (`data/ui-en.json`, `data/ui-es.json`) +- Internationalized labels for all actions +- Button tooltips and ARIA labels +- Placeholder text and section headers + +### 5. Keyboard Shortcut Conflict Prevention +- Page shortcuts (L, I, V, ?) disabled when ninja-keys is open +- Implemented via hyperscript `ninjaOpen` check in body handler + +## Technical Details + +### Activation Methods +1. **Keyboard**: `Cmd+K` (Mac) or `Ctrl+K` (Windows/Linux) +2. **Button**: Click the magnifying glass button (top-left position) + +### Button Position (Left Column) +1. **CMD+K Button** (top) - `bottom: 30rem` +2. Download PDF Button - `bottom: 26rem` +3. Print Button - `bottom: 22rem` +4. Zoom Toggle - `bottom: 10rem` +5. Shortcuts Help - `bottom: 6rem` + +### Backend Changes + +#### Text CV Download (`internal/handlers/cv_text.go`) +Added `download` query parameter support: +```go +// GET /text?lang=en&download=true +// Returns: Content-Disposition: attachment; filename="cv-jamr-2025-en.txt" +``` + +#### UI Model (`internal/models/ui/ui.go`) +Added CmdK struct for internationalization: +```go +type CmdK struct { + Placeholder string `json:"placeholder"` + NoResults string `json:"noResults"` + Sections CmdKSections `json:"sections"` + Actions CmdKActions `json:"actions"` + Button CmdKButton `json:"button"` +} +``` + +## Usage Examples + +### Search Commands +- Type "SAP" to find SAP experience entry and SAP skills +- Type "download" to see all download options +- Type "contact" to open contact form + +### Quick Navigation +1. Press `Cmd+K` +2. Type section name (e.g., "skills") +3. Press Enter to scroll to section + +### Download PDF +1. Press `Cmd+K` +2. Type "pdf" +3. Select desired format (short/default/extended) + +## CSS Variables + +```css +ninja-keys { + --ninja-font-family: 'Quicksand', sans-serif; + --ninja-accent-color: #667eea; + --ninja-z-index: 10000; + --ninja-width: 640px; + --ninja-backdrop-filter: blur(8px); + --ninja-modal-background: rgba(255, 255, 255, 0.95); + --ninja-selected-background: #667eea; +} +``` + +### Dark Mode +```css +[data-color-theme="dark"] ninja-keys { + --ninja-modal-background: rgba(40, 40, 40, 0.95); + --ninja-text-color: #e0e0e0; + --ninja-actions-background: #2a2a2a; +} +``` + +## Testing + +### Unit Tests +Located in `internal/handlers/cv_text_test.go`: +- Test PlainText handler with various parameters +- Test download filename format +- Test text browser detection + +### Manual Testing +1. Press `Cmd+K` to open command bar +2. Type to search actions +3. Use arrow keys to navigate +4. Press Enter to execute action +5. Press Escape to close + +### Test Commands +```bash +# Run text handler tests +go test ./internal/handlers/ -run TestPlainText -v + +# Test text download endpoint +curl -I "http://localhost:1999/text?lang=en&download=true" +``` + +## Files Modified/Created + +### Created +- `static/js/ninja-keys-init.js` - Action definitions and initialization +- `templates/partials/widgets/cmd-k-button.html` - Button widget +- `docs/CMD-K-COMMAND-BAR.md` - This documentation +- `internal/handlers/cv_text_test.go` - Unit tests + +### Modified +- `templates/index.html` - Added ninja-keys CDN, element, and script +- `static/css/04-interactive/_buttons.css` - Button and ninja-keys styling +- `templates/partials/modals/shortcuts-modal.html` - Added CMD+K entry +- `internal/handlers/cv_text.go` - Added download parameter +- `internal/models/ui/ui.go` - Added CmdK struct +- `data/ui-en.json` - English UI strings +- `data/ui-es.json` - Spanish UI strings + +## Browser Support + +- Chrome 80+ +- Firefox 75+ +- Safari 13.1+ +- Edge 80+ + +Requires ES modules support for ninja-keys Web Component. + +## Accessibility + +- Full keyboard navigation +- ARIA labels on button +- Focus management in modal +- Screen reader compatible +- Respects reduced motion preferences diff --git a/docs/CONTACT-FORM-QUICKSTART.md b/docs/CONTACT-FORM-QUICKSTART.md index ebed9a6..0bfaf3e 100644 --- a/docs/CONTACT-FORM-QUICKSTART.md +++ b/docs/CONTACT-FORM-QUICKSTART.md @@ -517,4 +517,4 @@ You just need to: 3. Add the routes 4. Create the HTML form -**Ready to invite hackers? 😈** +**Your contact form is now production-ready with comprehensive security controls.** diff --git a/docs/HACK-CHALLENGE.md b/docs/HACK-CHALLENGE.md deleted file mode 100644 index 6d19199..0000000 --- a/docs/HACK-CHALLENGE.md +++ /dev/null @@ -1,602 +0,0 @@ -# Try to Hack Me! 🎯 - -**Challenge Site:** https://juan.andres.morenorub.io/ -**Status:** ACTIVE -**Difficulty:** ⭐⭐⭐ (Medium to Hard) - ---- - -## 🎮 Welcome, Security Researcher! - -This CV portfolio site is **intentionally** opening its doors to security researchers, ethical hackers, and curious developers. I believe the best way to prove security isn't through claims, but through **real-world testing**. - -### Why This Challenge? - -As a developer who values security, I've implemented **defense-in-depth** protection across this application. Rather than just documenting these controls, I'm inviting you to **test them yourself**. - -**This is a showcase of:** -- Production-grade security implementation -- Real-world attack prevention -- Transparent security practices -- Confidence in my code - -**Your mission (if you choose to accept it):** Find vulnerabilities, bypass security controls, or break the application in creative ways. - ---- - -## 🎯 Challenge Categories - -### Category 1: Browser-Only Challenge ⭐⭐⭐ - -**Objective:** Submit a contact form message using **anything except a web browser**. - -**What's Protected:** -- The contact form at `/api/contact` ONLY accepts browser requests -- All automation tools are blocked: curl, wget, Postman, HTTPie, Python requests, etc. - -**Your Goal:** -- Successfully submit a contact form using curl, Postman, or any HTTP client -- OR bypass browser-only validation with a crafted request - -**Difficulty:** Hard - -**Why This Matters:** -Browser-only access prevents 95%+ of automated attacks. Can you join the 5% who bypass it? - -**Hints:** -- What headers does a browser send that curl doesn't? -- Can you perfectly impersonate a browser? -- Is there a race condition in the validation? - ---- - -### Category 2: Rate Limit Bypass ⭐⭐ - -**Objective:** Exceed the rate limits without getting blocked. - -**What's Protected:** -- Contact form: 5 requests per hour per IP -- PDF export: 3 requests per minute per IP - -**Your Goal:** -- Submit more than 5 contact forms in 1 hour from a single IP -- Generate more than 3 PDFs in 1 minute from a single IP -- OR find a way to reset the rate limiter - -**Difficulty:** Medium - -**Why This Matters:** -Rate limiting prevents spam and resource exhaustion. Can you find the loophole? - -**Hints:** -- How does the server identify your IP? -- Can you make the server think you're multiple clients? -- Is the rate limiter stateless or stateful? - ---- - -### Category 3: Injection Challenge ⭐⭐⭐ - -**Objective:** Execute code or commands on the server. - -**What's Protected:** -- Email header injection prevention -- XSS protection (HTML escaping) -- Command injection prevention (no shell commands) -- SQL injection (N/A - no database) - -**Your Goal:** -- Inject email headers (Bcc, Cc, Content-Type) -- Execute JavaScript via XSS -- Run shell commands via command injection -- OR find any other injection vulnerability - -**Difficulty:** Hard - -**Why This Matters:** -Injection attacks are the #1 web security threat. Can you find a gap in our input validation? - -**Hints:** -- What characters are allowed in each field? -- How is user input sanitized? -- Are there any differences between client and server validation? - ---- - -### Category 4: Bot Detection Bypass ⭐⭐ - -**Objective:** Submit a contact form as a bot without getting detected. - -**What's Protected:** -- Honeypot field (hidden from humans, visible to bots) -- Timing validation (must take at least 2 seconds) -- Server-side timestamp verification - -**Your Goal:** -- Submit a form with the honeypot filled (bot behavior) -- Submit a form in less than 2 seconds -- OR bypass timing validation without waiting - -**Difficulty:** Medium - -**Why This Matters:** -Bot detection prevents spam and automated abuse. Are you smarter than the bot detector? - -**Hints:** -- Where is the timestamp set? -- Can you manipulate the timestamp? -- Is there a way to predict valid honeypot values? - ---- - -### Category 5: CSRF Challenge ⭐⭐⭐ - -**Objective:** Submit a valid CSRF-protected request from an external site. - -**What's Protected:** -- CSRF tokens (32-byte cryptographically secure) -- Token expiration (24 hours) -- Constant-time comparison (timing attack resistant) - -**Your Goal:** -- Submit a contact form without a valid CSRF token -- Reuse an expired CSRF token -- Predict or forge a CSRF token -- OR exploit a timing attack in token comparison - -**Difficulty:** Hard - -**Why This Matters:** -CSRF allows attackers to perform actions on behalf of users. Can you break the token system? - -**Hints:** -- How are tokens generated? -- Where are tokens stored? -- Are tokens predictable or brute-forceable? - ---- - -### Category 6: Denial of Service ⭐ - -**Objective:** Make the site unavailable to legitimate users. - -**What's Protected:** -- Rate limiting (5 contact forms/hour, 3 PDFs/minute) -- Origin validation (prevents external hotlinking) -- Resource limits (request timeouts, connection limits) - -**Your Goal:** -- Exhaust server resources (CPU, memory, connections) -- Trigger a crash or panic -- Make the site unresponsive to legitimate users -- OR find a resource leak - -**Difficulty:** Easy to Medium - -**Why This Matters:** -DoS attacks can take down services. How robust is this application? - -**Hints:** -- Which endpoints are most resource-intensive? -- Are there any unbounded operations? -- Can you trigger a memory leak? - ---- - -## 🏆 Bonus Challenges - -### Bonus 1: Data Extraction ⭐⭐⭐ - -**Objective:** Extract sensitive data from the server. - -**Examples:** -- Environment variables -- Server file paths -- Configuration details -- Email addresses or contact form submissions - -**Difficulty:** Hard - ---- - -### Bonus 2: Privilege Escalation ⭐⭐⭐⭐ - -**Objective:** Gain unauthorized access or elevated privileges. - -**Examples:** -- Access admin endpoints (if they exist) -- Modify application configuration -- Execute arbitrary code -- Read/write files outside the web root - -**Difficulty:** Very Hard - ---- - -### Bonus 3: Creative Attack ⭐-⭐⭐⭐⭐ - -**Objective:** Surprise me with something I didn't think of! - -**Examples:** -- Novel attack vectors -- Chained exploits -- Social engineering combined with technical attacks -- Zero-day vulnerabilities in dependencies - -**Difficulty:** Variable - -**Why This Matters:** -The best vulnerabilities are the ones nobody thought to test for. - ---- - -## 📋 Rules of Engagement - -### ✅ What's Allowed - -- **Automated scanning** - Use OWASP ZAP, Burp Suite, Nikto, etc. -- **Fuzzing** - Test all inputs with unexpected data -- **Load testing** - Test rate limits and resource exhaustion -- **Source code review** - The code is [open source](https://github.com/juanatsap/cv-site) -- **Social engineering** - Email me attack vectors (no actual exploitation) -- **Creative thinking** - Try anything not explicitly forbidden - -### ❌ What's NOT Allowed - -- **Physical attacks** - Don't attack the server infrastructure -- **Social engineering end users** - Don't phish my site visitors -- **Destructive attacks** - Don't delete data or destroy the site -- **Third-party attacks** - Don't attack my hosting provider or CDN -- **Illegal activity** - Follow all applicable laws -- **Spam** - Don't send actual spam through the contact form - -### 🤝 Good Faith - -This challenge operates on **good faith**: -- Test the security controls, not the infrastructure -- Report findings before exploiting them maliciously -- Don't cause harm to the site or its visitors -- Respect the responsible disclosure process - -**If you're unsure if something is allowed, ask first!** - ---- - -## 🎁 What You Get - -### Recognition - -**Hall of Fame:** Valid findings will be acknowledged in the project repository (with your permission). - -**Categories:** -- 🥇 **Critical Findings** - Remote code execution, data breaches, authentication bypass -- 🥈 **High Severity** - CSRF bypass, XSS, injection attacks, sensitive data exposure -- 🥉 **Medium Severity** - Rate limit bypass, DoS vulnerabilities, information disclosure -- 📝 **Low Severity / Informational** - Security improvements, best practice violations - -### What Qualifies as a Valid Finding? - -**Valid:** -- ✅ Actual security vulnerabilities (reproducible) -- ✅ Bypasses of implemented security controls -- ✅ Data leakage or information disclosure -- ✅ Denial of Service (reproducible, not infrastructure-level) -- ✅ Novel attack vectors I haven't considered - -**Invalid:** -- ❌ Already documented behavior (see [SECURITY.md](SECURITY.md)) -- ❌ Out-of-scope findings (e.g., GitHub account security) -- ❌ Social engineering without technical component -- ❌ Attacks on infrastructure (hosting provider, DNS, etc.) -- ❌ Features, not bugs (e.g., "site allows long names") - ---- - -## 📧 Responsible Disclosure - -Found something? Here's how to report it: - -### 1. Document Your Finding - -Include: -- **Description** - What did you find? -- **Impact** - What can an attacker do with this? -- **Reproduction Steps** - How can I reproduce it? -- **Proof of Concept** - curl commands, screenshots, code samples -- **Suggested Fix** - (Optional) How should this be fixed? - -### 2. Send Your Report - -**Email:** [Create issue on GitHub](https://github.com/juanatsap/cv-site/security/advisories/new) - -**Subject:** `[SECURITY] Brief description of finding` - -**Please DO NOT:** -- ❌ Publicly disclose the vulnerability before I've had a chance to fix it -- ❌ Exploit the vulnerability for personal gain -- ❌ Share the vulnerability with others before resolution - -### 3. What Happens Next? - -**Response Time:** -- **Initial Response:** Within 48 hours -- **Triage:** Within 1 week -- **Fix:** Varies by severity (1 day to 1 month) -- **Public Disclosure:** After fix is deployed (coordinated with you) - -**Severity Timelines:** -- 🔴 **Critical:** 24-48 hours -- 🟠 **High:** 1 week -- 🟡 **Medium:** 2 weeks -- 🟢 **Low:** 1 month - -### 4. Recognition - -If you'd like to be acknowledged: -- **Hall of Fame** entry in repository -- **Thank you** in release notes -- **Social media shoutout** (with your permission) - -**Privacy:** You can choose to remain anonymous! - ---- - -## 🛡️ What You're Up Against - -### Security Layers Implemented - -``` -┌────────────────────────────────────────┐ -│ Layer 1: Browser-Only Access │ -│ Blocks: curl, Postman, automation │ -└────────────────────────────────────────┘ - ▼ -┌────────────────────────────────────────┐ -│ Layer 2: CSRF Protection │ -│ 32-byte cryptographic tokens │ -└────────────────────────────────────────┘ - ▼ -┌────────────────────────────────────────┐ -│ Layer 3: Rate Limiting │ -│ 5 forms/hour, 3 PDFs/minute │ -└────────────────────────────────────────┘ - ▼ -┌────────────────────────────────────────┐ -│ Layer 4: Bot Detection │ -│ Honeypot + Timing validation │ -└────────────────────────────────────────┘ - ▼ -┌────────────────────────────────────────┐ -│ Layer 5: Input Validation │ -│ Email injection, XSS, injection tests │ -└────────────────────────────────────────┘ - ▼ -┌────────────────────────────────────────┐ -│ Layer 6: Security Logging │ -│ All events tracked in structured JSON │ -└────────────────────────────────────────┘ -``` - -### Known Protections - -**You'll have to bypass:** -- Origin/Referer validation -- X-Requested-With header checks -- User-Agent validation -- CSRF token generation & validation -- Rate limiting (per-IP tracking) -- Honeypot field detection -- Timing validation (2-second minimum) -- Email header injection prevention -- HTML escaping (XSS protection) -- Input length limits -- Character whitelist validation - -**See [SECURITY.md](SECURITY.md) for full details on security controls.** - ---- - -## 💡 Hints & Tips - -### Getting Started - -1. **Review the source code:** [GitHub Repository](https://github.com/juanatsap/cv-site) -2. **Read the security documentation:** [SECURITY.md](SECURITY.md) -3. **Inspect the contact form:** View source of the form -4. **Try basic attacks:** XSS, SQL injection, command injection -5. **Use automated tools:** OWASP ZAP, Burp Suite, Nikto - -### Testing Endpoints - -**Primary Targets:** -- `GET /` - Main page -- `POST /api/contact` - Contact form (heavily protected) -- `GET /export/pdf?lang=en` - PDF generation (rate limited) -- `POST /toggle/*` - Preference toggles -- `POST /switch-language` - Language switcher - -### Common Attack Patterns - -**XSS:** -```javascript - - - -``` - -**Email Header Injection:** -``` -test@test.com\nBcc: attacker@evil.com -test@test.com\r\nContent-Type: text/html -``` - -**Command Injection:** -``` -data; ls -la -data | cat /etc/passwd -data`whoami` -``` - -**SQL Injection (N/A but try it):** -``` -' OR 1=1 -- -Robert'; DROP TABLE users; -- -``` - -### Advanced Techniques - -- **Race conditions** - Submit multiple requests simultaneously -- **Unicode tricks** - Use Unicode characters to bypass validation -- **Encoding bypasses** - Try URL encoding, double encoding, hex encoding -- **Header manipulation** - Craft custom headers to bypass validation -- **Timing attacks** - Measure response times to leak information - ---- - -## 🏅 Hall of Fame - -**Status:** No vulnerabilities reported yet! - -**Be the first to find a valid security issue and get recognized here.** - ---- - -### Past Findings - -*This section will be updated as vulnerabilities are found and fixed.* - -**Format:** -``` -[Date] [Severity] [Reporter] - Description -``` - -**Example:** -``` -2025-11-30 | HIGH | @researcher | CSRF bypass via race condition -``` - ---- - -## 📊 Challenge Statistics - -**Current Stats:** - -- **Total Attempts:** Not tracked (privacy-respecting) -- **Valid Findings:** 0 -- **Invalid Reports:** 0 -- **Average Time to First Finding:** N/A - -**Most Tested:** -- Contact form submission -- Rate limit bypass attempts -- Browser-only access bypass - -**Least Tested:** -- Creative/novel attack vectors -- Chained exploits -- Social engineering components - ---- - -## 🤔 FAQ - -### Q: Is this a real production site? - -**A:** Yes! This is my actual CV portfolio site. It's production-ready and serves real traffic. - -### Q: Will you actually fix vulnerabilities I find? - -**A:** Absolutely! Valid findings will be prioritized and fixed according to severity. - -### Q: Can I use automated tools? - -**A:** Yes! OWASP ZAP, Burp Suite, Nikto, and other scanners are welcome. - -### Q: What if I accidentally break something? - -**A:** Don't panic! Just report it immediately. The site has backups and graceful error handling. - -### Q: Can I test in production? - -**A:** Yes, but please be responsible. Don't spam, don't DoS, and don't cause harm to legitimate users. - -### Q: Is there a monetary reward? - -**A:** No cash bounty (this is a personal project), but you'll get recognition and my eternal gratitude! - -### Q: Can I stay anonymous? - -**A:** Absolutely! You can report anonymously and choose whether to be acknowledged publicly. - -### Q: How do I know you won't use my finding maliciously? - -**A:** This is a personal CV site with no sensitive data. The worst case is someone sends me spam emails. I'm committed to transparent, ethical security practices. - -### Q: What if I find something in a dependency, not your code? - -**A:** Still valid! Report it, and I'll coordinate disclosure with the upstream project. - -### Q: Can I write a blog post about my findings? - -**A:** Yes! But please wait until after the fix is deployed. Coordinated disclosure protects everyone. - ---- - -## 🎓 Learning Resources - -New to security testing? Here are some resources to get started: - -### Beginner - -- [OWASP Top 10](https://owasp.org/www-project-top-ten/) -- [PortSwigger Web Security Academy](https://portswigger.net/web-security) -- [HackerOne 101](https://www.hackerone.com/hackers/hacker101) - -### Intermediate - -- [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/) -- [Bug Bounty Bootcamp](https://nostarch.com/bug-bounty-bootcamp) -- [Web Application Hacker's Handbook](https://www.wiley.com/en-us/The+Web+Application+Hacker%27s+Handbook%3A+Finding+and+Exploiting+Security+Flaws%2C+2nd+Edition-p-9781118026472) - -### Advanced - -- [Advanced Penetration Testing](https://nostarch.com/advanced-penetration-testing) -- [The Tangled Web](https://nostarch.com/tangledweb) -- [Real-World Bug Hunting](https://nostarch.com/bughunting) - -### Tools - -- [OWASP ZAP](https://www.zaproxy.org/) - Free web app scanner -- [Burp Suite](https://portswigger.net/burp) - Professional testing toolkit -- [Nikto](https://cirt.net/Nikto2) - Web server scanner -- [SQLMap](https://sqlmap.org/) - SQL injection testing - ---- - -## 🚀 Ready to Hack? - -**Target:** https://juan.andres.morenorub.io/ - -**Source Code:** https://github.com/juanatsap/cv-site - -**Documentation:** [SECURITY.md](SECURITY.md) - -**Report Findings:** [GitHub Security Advisory](https://github.com/juanatsap/cv-site/security/advisories/new) - ---- - -**Good luck, and happy hacking! 🎯** - -**Remember:** -- Be ethical -- Be responsible -- Be creative -- Have fun! - ---- - -**Last Updated:** 2025-11-30 -**Challenge Status:** ACTIVE -**Next Review:** 2026-03-01 - -**P.S.** - If you manage to bypass the browser-only access using curl, I'll be genuinely impressed. That one's tough. 😉 diff --git a/docs/SECURITY-AUDIT-REPORT.md b/docs/SECURITY-AUDIT-REPORT.md index fbb7bca..b625e2a 100644 --- a/docs/SECURITY-AUDIT-REPORT.md +++ b/docs/SECURITY-AUDIT-REPORT.md @@ -1578,7 +1578,7 @@ The CV application demonstrates **strong security fundamentals** with proper XSS **Overall Security Rating: B+ (GOOD)** -With the recommended improvements, the application can achieve an **A+ security rating** and be confidently opened to public hacking challenges. +With the recommended improvements, the application can achieve an **A+ security rating**. --- diff --git a/docs/SECURITY-IMPLEMENTATION-SUMMARY.md b/docs/SECURITY-IMPLEMENTATION-SUMMARY.md index 09bf989..12bd748 100644 --- a/docs/SECURITY-IMPLEMENTATION-SUMMARY.md +++ b/docs/SECURITY-IMPLEMENTATION-SUMMARY.md @@ -623,9 +623,6 @@ Before deploying contact form to production: 4. Create GDPR privacy policy page 5. Configure fail2ban for production -### Ready for "Try to Hack Me!" Challenge? -**YES** - with recommended improvements implemented - --- **Security is a journey, not a destination. Regular audits, updates, and monitoring are essential.** diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 2992ab8..22693ed 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -948,11 +948,10 @@ This CV portfolio site demonstrates that **security and usability can coexist**. **Next Steps:** -1. Review [HACK-CHALLENGE.md](HACK-CHALLENGE.md) for the hacking challenge -2. See [DEPLOYMENT.md](../doc/DEPLOYMENT.md) for production deployment guides -3. Check security logs regularly for anomalies -4. Keep dependencies updated with `go mod tidy` -5. Run `govulncheck ./...` monthly for vulnerability scanning +1. See [DEPLOYMENT.md](../doc/DEPLOYMENT.md) for production deployment guides +2. Check security logs regularly for anomalies +3. Keep dependencies updated with `go mod tidy` +4. Run `govulncheck ./...` monthly for vulnerability scanning **Security is a continuous process, not a destination.** diff --git a/internal/handlers/cv_cmdk.go b/internal/handlers/cv_cmdk.go new file mode 100644 index 0000000..56849da --- /dev/null +++ b/internal/handlers/cv_cmdk.go @@ -0,0 +1,104 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/juanatsap/cv-site/internal/models" +) + +// CmdKAction represents a single action for the ninja-keys command palette +type CmdKAction struct { + ID string `json:"id"` + Title string `json:"title"` + Section string `json:"section"` + Keywords string `json:"keywords"` +} + +// CmdKResponse represents the response for the CMD+K API endpoint +type CmdKResponse struct { + Experiences []CmdKAction `json:"experiences"` + Projects []CmdKAction `json:"projects"` + Courses []CmdKAction `json:"courses"` +} + +// CmdKData returns JSON data for the ninja-keys command palette +// This endpoint provides dynamic entries for experiences, projects, and courses +// that can be searched via CMD+K +func (h *CVHandler) CmdKData(w http.ResponseWriter, r *http.Request) { + // Get language from query parameter, default to "en" + lang := r.URL.Query().Get("lang") + if lang == "" { + lang = "en" + } + if lang != "en" && lang != "es" { + lang = "en" + } + + // Load CV data + cv, err := models.LoadCV(lang) + if err != nil { + log.Printf("ERROR loading CV data: %v", err) + http.Error(w, "Failed to load CV data", http.StatusInternalServerError) + return + } + + // Build response + response := CmdKResponse{ + Experiences: make([]CmdKAction, 0, len(cv.Experience)), + Projects: make([]CmdKAction, 0, len(cv.Projects)), + Courses: make([]CmdKAction, 0, len(cv.Courses)), + } + + // Map experiences + for _, exp := range cv.Experience { + if exp.CompanyID == "" { + continue // Skip entries without ID + } + response.Experiences = append(response.Experiences, CmdKAction{ + ID: "exp-" + exp.CompanyID, + Title: exp.Company, + Section: "Experience", + Keywords: exp.Company + " " + exp.Position, + }) + } + + // Map projects + for _, proj := range cv.Projects { + if proj.ProjectID == "" { + continue // Skip entries without ID + } + title := proj.ProjectName + if title == "" { + title = proj.Title + } + response.Projects = append(response.Projects, CmdKAction{ + ID: "proj-" + proj.ProjectID, + Title: title, + Section: "Projects", + Keywords: title + " " + proj.ShortDescription, + }) + } + + // Map courses + for _, course := range cv.Courses { + if course.CourseID == "" { + continue // Skip entries without ID + } + response.Courses = append(response.Courses, CmdKAction{ + ID: "course-" + course.CourseID, + Title: course.Title, + Section: "Courses", + Keywords: course.Title + " " + course.Institution, + }) + } + + // Set headers and encode response + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "public, max-age=3600") // Cache for 1 hour + + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("ERROR encoding CMD+K response: %v", err) + } +} diff --git a/internal/handlers/cv_cmdk_test.go b/internal/handlers/cv_cmdk_test.go new file mode 100644 index 0000000..e7ac8b4 --- /dev/null +++ b/internal/handlers/cv_cmdk_test.go @@ -0,0 +1,225 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/juanatsap/cv-site/internal/config" + "github.com/juanatsap/cv-site/internal/templates" +) + +// TestCmdKData tests the CmdKData handler +// NOTE: This test requires running from project root due to data file path resolution +// Run with: go test ./internal/handlers/ -run TestCmdKData -v +func TestCmdKData(t *testing.T) { + // Skip if running in short mode (CI) - requires project root + if testing.Short() { + t.Skip("Skipping CmdKData test - requires running from project root") + } + + cfg := &config.TemplateConfig{ + Dir: "../../templates", + PartialsDir: "../../templates/partials", + HotReload: true, + } + tmplManager, err := templates.NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create template manager: %v", err) + } + + handler := NewCVHandler(tmplManager, "localhost:8080") + + tests := []struct { + name string + lang string + expectStatus int + expectExperiences bool // should have experiences + expectProjects bool // should have projects + expectCourses bool // should have courses + expectMinExp int // minimum expected experiences + expectMinProj int // minimum expected projects + expectMinCourses int // minimum expected courses + }{ + { + name: "Default language (English)", + lang: "", + expectStatus: http.StatusOK, + expectExperiences: true, + expectProjects: true, + expectCourses: true, + expectMinExp: 5, + expectMinProj: 3, + expectMinCourses: 2, + }, + { + name: "English language", + lang: "en", + expectStatus: http.StatusOK, + expectExperiences: true, + expectProjects: true, + expectCourses: true, + expectMinExp: 5, + expectMinProj: 3, + expectMinCourses: 2, + }, + { + name: "Spanish language", + lang: "es", + expectStatus: http.StatusOK, + expectExperiences: true, + expectProjects: true, + expectCourses: true, + expectMinExp: 5, + expectMinProj: 3, + expectMinCourses: 2, + }, + { + name: "Invalid language defaults to English", + lang: "fr", + expectStatus: http.StatusOK, + expectExperiences: true, + expectProjects: true, + expectCourses: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build query string + query := "/api/cmd-k" + if tt.lang != "" { + query += "?lang=" + tt.lang + } + + req := httptest.NewRequest(http.MethodGet, query, nil) + rec := httptest.NewRecorder() + + handler.CmdKData(rec, req) + + // Check status code + if rec.Code != tt.expectStatus { + t.Errorf("Expected status %d, got %d", tt.expectStatus, rec.Code) + } + + // If success, validate JSON response + if rec.Code == http.StatusOK { + // Check content type + contentType := rec.Header().Get("Content-Type") + if contentType != "application/json" { + t.Errorf("Expected Content-Type application/json, got %s", contentType) + } + + // Parse JSON response + var response CmdKResponse + if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("Failed to parse JSON response: %v", err) + } + + // Validate experiences + if tt.expectExperiences && len(response.Experiences) == 0 { + t.Error("Expected experiences but got none") + } + if tt.expectMinExp > 0 && len(response.Experiences) < tt.expectMinExp { + t.Errorf("Expected at least %d experiences, got %d", tt.expectMinExp, len(response.Experiences)) + } + + // Validate projects + if tt.expectProjects && len(response.Projects) == 0 { + t.Error("Expected projects but got none") + } + if tt.expectMinProj > 0 && len(response.Projects) < tt.expectMinProj { + t.Errorf("Expected at least %d projects, got %d", tt.expectMinProj, len(response.Projects)) + } + + // Validate courses + if tt.expectCourses && len(response.Courses) == 0 { + t.Error("Expected courses but got none") + } + if tt.expectMinCourses > 0 && len(response.Courses) < tt.expectMinCourses { + t.Errorf("Expected at least %d courses, got %d", tt.expectMinCourses, len(response.Courses)) + } + + // Validate structure of first experience (if present) + if len(response.Experiences) > 0 { + exp := response.Experiences[0] + if exp.ID == "" { + t.Error("Experience ID should not be empty") + } + if exp.Title == "" { + t.Error("Experience Title should not be empty") + } + if exp.Section != "Experience" { + t.Errorf("Experience Section should be 'Experience', got '%s'", exp.Section) + } + } + + // Validate structure of first project (if present) + if len(response.Projects) > 0 { + proj := response.Projects[0] + if proj.ID == "" { + t.Error("Project ID should not be empty") + } + if proj.Title == "" { + t.Error("Project Title should not be empty") + } + if proj.Section != "Projects" { + t.Errorf("Project Section should be 'Projects', got '%s'", proj.Section) + } + } + + // Validate structure of first course (if present) + if len(response.Courses) > 0 { + course := response.Courses[0] + if course.ID == "" { + t.Error("Course ID should not be empty") + } + if course.Title == "" { + t.Error("Course Title should not be empty") + } + if course.Section != "Courses" { + t.Errorf("Course Section should be 'Courses', got '%s'", course.Section) + } + } + + // Log counts for debugging + t.Logf("Response: %d experiences, %d projects, %d courses", + len(response.Experiences), len(response.Projects), len(response.Courses)) + } + }) + } +} + +// TestCmdKDataCaching tests that the response has proper cache headers +func TestCmdKDataCaching(t *testing.T) { + if testing.Short() { + t.Skip("Skipping CmdKDataCaching test - requires running from project root") + } + + cfg := &config.TemplateConfig{ + Dir: "../../templates", + PartialsDir: "../../templates/partials", + HotReload: true, + } + tmplManager, err := templates.NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create template manager: %v", err) + } + + handler := NewCVHandler(tmplManager, "localhost:8080") + + req := httptest.NewRequest(http.MethodGet, "/api/cmd-k", nil) + rec := httptest.NewRecorder() + + handler.CmdKData(rec, req) + + // Check cache header + cacheControl := rec.Header().Get("Cache-Control") + if cacheControl == "" { + t.Error("Expected Cache-Control header to be set") + } + if cacheControl != "public, max-age=3600" { + t.Errorf("Expected Cache-Control 'public, max-age=3600', got '%s'", cacheControl) + } +} diff --git a/internal/handlers/cv_contact.go b/internal/handlers/cv_contact.go index 1e3c3d3..225a120 100644 --- a/internal/handlers/cv_contact.go +++ b/internal/handlers/cv_contact.go @@ -221,8 +221,10 @@ func (h *CVHandler) renderContactError(w http.ResponseWriter, r *http.Request, e } // Render the error template + // Return 200 OK with error content - HTMX 1.9.x logs console.error for non-2xx responses + // Validation errors are expected form feedback, not system errors w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusBadRequest) + w.WriteHeader(http.StatusOK) tmpl, err := h.templates.Render("contact-error") if err != nil { diff --git a/internal/handlers/cv_text.go b/internal/handlers/cv_text.go index 9a8d0ea..9cb46f5 100644 --- a/internal/handlers/cv_text.go +++ b/internal/handlers/cv_text.go @@ -2,12 +2,14 @@ package handlers import ( "bytes" + "fmt" "log" "net/http" "path/filepath" "regexp" "strings" "text/template" + "time" ) // Plain text configuration @@ -161,6 +163,13 @@ func (h *CVHandler) PlainText(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") + // Check if download is requested + if r.URL.Query().Get("download") == "true" { + year := time.Now().Year() + filename := fmt.Sprintf("cv-jamr-%d-%s.txt", year, langCode) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + } + // Write plain text response _, _ = w.Write([]byte(text)) } diff --git a/internal/handlers/cv_text_test.go b/internal/handlers/cv_text_test.go new file mode 100644 index 0000000..b5e9adc --- /dev/null +++ b/internal/handlers/cv_text_test.go @@ -0,0 +1,266 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/juanatsap/cv-site/internal/config" + "github.com/juanatsap/cv-site/internal/templates" +) + +// TestPlainText tests the PlainText handler +// NOTE: This test requires running from project root due to template path resolution +// Run with: go test ./internal/handlers/ -run TestPlainText -v +// Or skip in CI: go test ./internal/handlers/ -run TestPlainText -short +func TestPlainText(t *testing.T) { + // Skip if running in short mode (CI) - requires project root + if testing.Short() { + t.Skip("Skipping PlainText test - requires running from project root") + } + + cfg := &config.TemplateConfig{ + Dir: "../../templates", + PartialsDir: "../../templates/partials", + HotReload: true, + } + tmplManager, err := templates.NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create template manager: %v", err) + } + + handler := NewCVHandler(tmplManager, "localhost:8080") + + tests := []struct { + name string + lang string + icons string + download string + expectStatus int + expectHeader string + expectContains string + }{ + { + name: "Default language (English)", + lang: "", + expectStatus: http.StatusOK, + expectContains: "Juan", + }, + { + name: "English language", + lang: "en", + expectStatus: http.StatusOK, + expectContains: "Juan", + }, + { + name: "Spanish language", + lang: "es", + expectStatus: http.StatusOK, + expectContains: "Juan", + }, + { + name: "Invalid language", + lang: "fr", + expectStatus: http.StatusBadRequest, + }, + { + name: "With icons disabled", + lang: "en", + icons: "false", + expectStatus: http.StatusOK, + expectContains: "Juan", + }, + { + name: "Download mode", + lang: "en", + download: "true", + expectStatus: http.StatusOK, + expectHeader: "attachment", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build query string + query := "/text" + params := []string{} + if tt.lang != "" { + params = append(params, "lang="+tt.lang) + } + if tt.icons != "" { + params = append(params, "icons="+tt.icons) + } + if tt.download != "" { + params = append(params, "download="+tt.download) + } + if len(params) > 0 { + query += "?" + strings.Join(params, "&") + } + + req := httptest.NewRequest(http.MethodGet, query, nil) + w := httptest.NewRecorder() + + handler.PlainText(w, req) + + if w.Code != tt.expectStatus { + t.Errorf("Expected status %d, got %d", tt.expectStatus, w.Code) + } + + // Check Content-Type for successful requests + if tt.expectStatus == http.StatusOK { + contentType := w.Header().Get("Content-Type") + if !strings.HasPrefix(contentType, "text/plain") { + t.Errorf("Expected text/plain content type, got %s", contentType) + } + } + + // Check Content-Disposition header for download mode + if tt.expectHeader != "" { + disposition := w.Header().Get("Content-Disposition") + if !strings.Contains(disposition, tt.expectHeader) { + t.Errorf("Expected Content-Disposition containing '%s', got '%s'", tt.expectHeader, disposition) + } + } + + // Check response body contains expected content (if success) + if tt.expectStatus == http.StatusOK && tt.expectContains != "" { + body := w.Body.String() + if !strings.Contains(body, tt.expectContains) { + t.Errorf("Expected body to contain '%s'", tt.expectContains) + } + } + }) + } +} + +// TestPlainTextDownloadFilename tests that download filename is correctly formatted +// NOTE: This test requires running from project root due to template path resolution +func TestPlainTextDownloadFilename(t *testing.T) { + // Skip if running in short mode (CI) - requires project root + if testing.Short() { + t.Skip("Skipping PlainTextDownloadFilename test - requires running from project root") + } + + cfg := &config.TemplateConfig{ + Dir: "../../templates", + PartialsDir: "../../templates/partials", + HotReload: true, + } + tmplManager, err := templates.NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create template manager: %v", err) + } + + handler := NewCVHandler(tmplManager, "localhost:8080") + + tests := []struct { + name string + lang string + expectPrefix string + }{ + { + name: "English download filename", + lang: "en", + expectPrefix: "cv-jamr-", + }, + { + name: "Spanish download filename", + lang: "es", + expectPrefix: "cv-jamr-", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/text?lang="+tt.lang+"&download=true", nil) + w := httptest.NewRecorder() + + handler.PlainText(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected status OK, got %d", w.Code) + } + + disposition := w.Header().Get("Content-Disposition") + if !strings.Contains(disposition, tt.expectPrefix) { + t.Errorf("Expected filename to contain '%s', got '%s'", tt.expectPrefix, disposition) + } + + // Verify language suffix is in filename + if !strings.Contains(disposition, "-"+tt.lang+".txt") { + t.Errorf("Expected filename to end with '-%s.txt', got '%s'", tt.lang, disposition) + } + }) + } +} + +// TestIsTextBrowser tests the text browser detection +func TestIsTextBrowser(t *testing.T) { + tests := []struct { + name string + userAgent string + accept string + expect bool + }{ + { + name: "Regular browser", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + expect: false, + }, + { + name: "curl", + userAgent: "curl/7.79.1", + expect: true, + }, + { + name: "wget", + userAgent: "Wget/1.21", + expect: true, + }, + { + name: "httpie", + userAgent: "HTTPie/2.6.0", + expect: true, + }, + { + name: "lynx", + userAgent: "Lynx/2.9.0dev.10", + expect: true, + }, + { + name: "w3m", + userAgent: "w3m/0.5.3", + expect: true, + }, + { + name: "Accept text/plain", + userAgent: "Mozilla/5.0", + accept: "text/plain", + expect: true, + }, + { + name: "Accept text/html", + userAgent: "Mozilla/5.0", + accept: "text/html", + expect: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + if tt.userAgent != "" { + req.Header.Set("User-Agent", tt.userAgent) + } + if tt.accept != "" { + req.Header.Set("Accept", tt.accept) + } + + result := isTextBrowser(req) + if result != tt.expect { + t.Errorf("isTextBrowser() = %v, expected %v for User-Agent: %s", result, tt.expect, tt.userAgent) + } + }) + } +} diff --git a/internal/models/cv.go b/internal/models/cv.go index 997950e..be671c7 100644 --- a/internal/models/cv.go +++ b/internal/models/cv.go @@ -45,6 +45,7 @@ type Personal struct { type Experience struct { Position string `json:"position"` Company string `json:"company"` + CompanyID string `json:"companyID,omitempty"` // Unique ID for scrolling/navigation CompanyURL string `json:"companyURL,omitempty"` // Optional URL for company website CompanyLogo string `json:"companyLogo"` Location string `json:"location"` @@ -91,6 +92,7 @@ type Project struct { Title string `json:"title"` ProjectName string `json:"projectName,omitempty"` // Optional: linkable part of title ProjectDesc string `json:"projectDesc,omitempty"` // Optional: non-linkable description part + ProjectID string `json:"projectID,omitempty"` // Unique ID for scrolling/navigation URL string `json:"url"` ProjectLogo string `json:"projectLogo,omitempty"` // Optional logo filename GitRepoUrl string `json:"gitRepoUrl,omitempty"` // Optional git repository URL for dynamic dates @@ -127,6 +129,7 @@ type Certification struct { type Course struct { Title string `json:"title"` Institution string `json:"institution"` + CourseID string `json:"courseID,omitempty"` // Unique ID for scrolling/navigation CourseLogo string `json:"courseLogo,omitempty"` // Optional logo filename Location string `json:"location"` Date string `json:"date"` diff --git a/internal/models/cv/cv.go b/internal/models/cv/cv.go index c556b8d..f4c2d03 100644 --- a/internal/models/cv/cv.go +++ b/internal/models/cv/cv.go @@ -52,6 +52,7 @@ type SEO struct { type Experience struct { Position string `json:"position"` Company string `json:"company"` + CompanyID string `json:"companyID,omitempty"` // Unique ID for scrolling/navigation CompanyURL string `json:"companyURL,omitempty"` // Optional URL for company website CompanyLogo string `json:"companyLogo"` Location string `json:"location"` @@ -98,6 +99,7 @@ type Project struct { Title string `json:"title"` ProjectName string `json:"projectName,omitempty"` // Optional: linkable part of title ProjectDesc string `json:"projectDesc,omitempty"` // Optional: non-linkable description part + ProjectID string `json:"projectID,omitempty"` // Unique ID for scrolling/navigation URL string `json:"url"` ProjectLogo string `json:"projectLogo,omitempty"` // Optional logo filename GitRepoUrl string `json:"gitRepoUrl,omitempty"` // Optional git repository URL for dynamic dates @@ -134,6 +136,7 @@ type Certification struct { type Course struct { Title string `json:"title"` Institution string `json:"institution"` + CourseID string `json:"courseID,omitempty"` // Unique ID for scrolling/navigation CourseLogo string `json:"courseLogo,omitempty"` // Optional logo filename Location string `json:"location"` Date string `json:"date"` diff --git a/internal/models/ui/ui.go b/internal/models/ui/ui.go index 1ca9325..223e09e 100644 --- a/internal/models/ui/ui.go +++ b/internal/models/ui/ui.go @@ -14,6 +14,47 @@ type UI struct { InfoModal InfoModal `json:"infoModal"` ContactModal ContactModal `json:"contactModal"` Widgets Widgets `json:"widgets"` + CmdK CmdK `json:"cmdK"` +} + +// CmdK command bar UI strings +type CmdK struct { + Placeholder string `json:"placeholder"` + NoResults string `json:"noResults"` + Sections CmdKSections `json:"sections"` + Actions CmdKActions `json:"actions"` + Button CmdKButton `json:"button"` +} + +type CmdKSections struct { + Navigation string `json:"navigation"` + Shortcuts string `json:"shortcuts"` + Downloads string `json:"downloads"` +} + +type CmdKActions struct { + JumpToExperience string `json:"jumpToExperience"` + JumpToEducation string `json:"jumpToEducation"` + JumpToSkills string `json:"jumpToSkills"` + JumpToProjects string `json:"jumpToProjects"` + JumpToCourses string `json:"jumpToCourses"` + JumpToLanguages string `json:"jumpToLanguages"` + JumpToAwards string `json:"jumpToAwards"` + ToggleLength string `json:"toggleLength"` + ToggleIcons string `json:"toggleIcons"` + ToggleTheme string `json:"toggleTheme"` + ShowShortcuts string `json:"showShortcuts"` + Print string `json:"print"` + DownloadPdfShort string `json:"downloadPdfShort"` + DownloadPdfDefault string `json:"downloadPdfDefault"` + DownloadPdfExtended string `json:"downloadPdfExtended"` + ViewTextCv string `json:"viewTextCv"` + DownloadTextCv string `json:"downloadTextCv"` +} + +type CmdKButton struct { + Tooltip string `json:"tooltip"` + AriaLabel string `json:"ariaLabel"` } // Navigation labels for hamburger menu @@ -116,6 +157,7 @@ type ShortcutGroup struct { ExpandAll *ShortcutItem `json:"expandAll,omitempty"` CollapseAll *ShortcutItem `json:"collapseAll,omitempty"` ScrollToTop *ShortcutItem `json:"scrollToTop,omitempty"` + CmdK *ShortcutItem `json:"cmdK,omitempty"` Print *ShortcutItem `json:"print,omitempty"` CloseModal *ShortcutItem `json:"closeModal,omitempty"` ShowHelp *ShortcutItem `json:"showHelp,omitempty"` @@ -209,8 +251,10 @@ type PdfToastLabel struct { } type ActionButtonsLabel struct { - DownloadPdf string `json:"downloadPdf"` - PrintFriendly string `json:"printFriendly"` - PlainText string `json:"plainText"` - Contact string `json:"contact"` + DownloadPdf string `json:"downloadPdf"` + PrintFriendly string `json:"printFriendly"` + PlainText string `json:"plainText"` + Contact string `json:"contact"` + Search string `json:"search"` + SearchAriaLabel string `json:"searchAriaLabel"` } diff --git a/internal/routes/routes.go b/internal/routes/routes.go index d8ca5e0..dae5ba3 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -16,10 +16,13 @@ func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) // Pattern: /cv-jamr-{year}-{lang}.pdf (e.g., /cv-jamr-2025-en.pdf) mux.HandleFunc("/cv-jamr-", cvHandler.DefaultCVShortcut) + // API routes (must be before "/" to avoid catch-all) + mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData) // CMD+K command palette data + // Public routes mux.HandleFunc("/", cvHandler.Home) mux.HandleFunc("/cv", cvHandler.CVContent) - mux.HandleFunc("/text", cvHandler.PlainText) // Plain text version for curl/AI + mux.HandleFunc("/text", cvHandler.PlainText) // Plain text version for curl/AI mux.HandleFunc("/health", healthHandler.Check) // HTMX endpoints for interactive controls diff --git a/static/css/01-foundation/_themes.css b/static/css/01-foundation/_themes.css index ee8d608..bf8f59e 100644 --- a/static/css/01-foundation/_themes.css +++ b/static/css/01-foundation/_themes.css @@ -288,13 +288,21 @@ bottom: 1.5rem !important; left: auto !important; right: auto !important; - width: 50px !important; - height: 50px !important; + /* Fluid button size: scales from 36px at 380px to 50px at 900px */ + width: clamp(36px, calc(36px + (50 - 36) * ((100vw - 380px) / (900 - 380))), 50px) !important; + height: clamp(36px, calc(36px + (50 - 36) * ((100vw - 380px) / (900 - 380))), 50px) !important; opacity: 1 !important; /* Full opacity on mobile (no transparency with blur bar) */ transform: none !important; - /* Position in 6-button layout: Download, Print, Shortcuts, Theme, Info, Back-to-top */ - /* Total width: 6 * 50px + 5 * 10px = 350px */ - left: calc(50% + 5px) !important; /* Fourth button */ + /* Position in 8-button layout: Search, Download, Print, Contact, Shortcuts, Theme, Info, Back-to-top */ + /* Fluid positioning: scales from +42px at 380px to +62px at 900px */ + left: calc(50% + clamp(42px, calc(42px + (62 - 42) * ((100vw - 380px) / (900 - 380))), 62px)) !important; /* Sixth button */ + } + + /* Scale theme switcher icon */ + .color-theme-switcher iconify-icon { + width: clamp(18px, calc(18px + (24 - 18) * ((100vw - 380px) / (900 - 380))), 24px) !important; + height: clamp(18px, calc(18px + (24 - 18) * ((100vw - 380px) / (900 - 380))), 24px) !important; + font-size: clamp(18px, calc(18px + (24 - 18) * ((100vw - 380px) / (900 - 380))), 24px) !important; } /* Show theme colors at FULL opacity on mobile (no transparency with blur bar) */ @@ -326,11 +334,10 @@ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; } - /* REAL MOBILE DEVICES: 5 buttons without shortcuts */ - /* Download, Print, Theme, Info, Back-to-top */ - /* Total width: 5 * 50px + 4 * 10px = 290px */ + /* REAL MOBILE DEVICES: 7 buttons without shortcuts */ + /* Fifth position: +33px at 900px, +22px at 380px */ .is-mobile-device .color-theme-switcher { - left: calc(50% - 25px) !important; /* Third button (no gap) */ + left: calc(50% + clamp(22px, calc(22px + (33 - 22) * ((100vw - 380px) / (900 - 380))), 33px)) !important; } } diff --git a/static/css/03-components/_action-bar.css b/static/css/03-components/_action-bar.css index b445363..2ddda8a 100644 --- a/static/css/03-components/_action-bar.css +++ b/static/css/03-components/_action-bar.css @@ -477,6 +477,7 @@ iconify-icon { align-items: stretch; height: 100%; overflow: visible; /* Allow tooltips to extend beyond button container */ + flex-wrap: nowrap; /* Keep buttons in single row */ } .action-buttons-right { @@ -484,6 +485,56 @@ iconify-icon { margin-left: auto; } +/* ======================================== + RESPONSIVE ACTION BUTTONS - Scale to fit + Prevents button overflow on narrow screens + ======================================== */ + +/* Intermediate screens: shrink buttons to fit */ +@media (min-width: 901px) and (max-width: 1400px) { + .action-buttons-right { + flex-shrink: 1; + min-width: 0; /* Allow shrinking */ + } + + .action-buttons-right .action-btn { + flex-shrink: 1; + min-width: 40px; /* Minimum touchable size */ + padding: 0 0.5rem; /* Reduce padding as needed */ + } +} + +/* Narrow desktop: icon-only buttons that scale */ +@media (min-width: 541px) and (max-width: 900px) { + .action-buttons-right { + display: flex !important; /* Show on tablet */ + flex-shrink: 1; + min-width: 0; + } + + .action-buttons-right .action-btn { + width: auto; + min-width: 36px; + padding: 0 0.4rem; + font-size: 0; /* Icon only */ + flex-shrink: 1; + } + + .action-buttons-right .action-btn iconify-icon { + width: 20px; + height: 20px; + } +} + +/* Very narrow: use CSS clamp for fluid button sizing */ +@media (min-width: 901px) and (max-width: 1100px) { + .action-btn { + /* Fluid width that scales with viewport */ + width: clamp(35px, 4vw, 50px) !important; + padding: 0 clamp(0.3rem, 0.8vw, 1rem) !important; + } +} + /* ============================================================================ HTMX Loading Indicators ========================================================================= */ diff --git a/static/css/04-interactive/_contact-form.css b/static/css/04-interactive/_contact-form.css index 4fcbfc4..1df4a17 100644 --- a/static/css/04-interactive/_contact-form.css +++ b/static/css/04-interactive/_contact-form.css @@ -2,6 +2,11 @@ CONTACT FORM - Modal form styling ============================================================================= */ +/* Utility class to hide elements after form submission */ +.hidden { + display: none !important; +} + /* Contact Modal Specific Overrides */ #contact-modal { max-width: 520px; @@ -256,10 +261,10 @@ CONTACT BUTTON (Fixed Position) ============================================================================= */ -/* Contact Button - positioned above bottom buttons */ -.contact-btn { - position: fixed; - bottom: 14rem; /* Above zoom button */ +/* Contact Button - positioned above theme switcher (FIXED sidebar button only) */ +.fixed-btn.contact-btn { + position: fixed !important; /* Override .has-tooltip position: relative */ + bottom: 18rem; /* Above theme switcher (14rem) */ left: 2rem; width: 50px; height: 50px; @@ -277,16 +282,16 @@ opacity: 0.6; } -.contact-btn:hover { +.fixed-btn.contact-btn:hover { opacity: 1; transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); background: #3498db; /* Blue hover */ } -.contact-btn.at-bottom { +.fixed-btn.contact-btn.at-bottom { opacity: 1; - background: #3498db; /* Blue when at bottom */ + background: #3498db !important; /* Blue when at bottom */ } /* ============================================================================= @@ -343,8 +348,8 @@ font-size: 0.75rem; } - /* Contact button mobile */ - .contact-btn { + /* Contact button mobile (FIXED sidebar button only) */ + .fixed-btn.contact-btn { bottom: 13.5rem; /* Adjust for mobile button spacing */ left: 1.5rem; width: 45px; diff --git a/static/hyperscript/utils._hs b/static/hyperscript/utils._hs index 4c5d61d..ae46d99 100644 --- a/static/hyperscript/utils._hs +++ b/static/hyperscript/utils._hs @@ -100,34 +100,38 @@ def handleScroll() end end - -- Back to top button visibility - if currentScroll > 300 - set #back-to-top's *display to 'flex' + -- Back to top button visibility (with null check) + set backToTop to #back-to-top + if backToTop is not null + if currentScroll > 300 + set backToTop's *display to 'flex' + end + if currentScroll <= 300 + set backToTop's *display to 'none' + end end - if currentScroll <= 300 - set #back-to-top's *display to 'none' - end - - -- At-bottom class for fixed buttons + -- At-bottom class for fixed buttons (with null checks) if isAtBottom - add .at-bottom to #back-to-top - add .at-bottom to #info-button - add .at-bottom to #shortcuts-button - add .at-bottom to #download-button - add .at-bottom to #print-friendly-button + if backToTop is not null then add .at-bottom to backToTop end + if #info-button is not null then add .at-bottom to #info-button end + if #shortcuts-button is not null then add .at-bottom to #shortcuts-button end + if #download-button is not null then add .at-bottom to #download-button end + if #print-friendly-button is not null then add .at-bottom to #print-friendly-button end + if #cmd-k-button is not null then add .at-bottom to #cmd-k-button end add .at-bottom to .color-theme-switcher - add .at-bottom to #zoom-toggle-button + if #zoom-toggle-button is not null then add .at-bottom to #zoom-toggle-button end end if not isAtBottom - remove .at-bottom from #back-to-top - remove .at-bottom from #info-button - remove .at-bottom from #shortcuts-button - remove .at-bottom from #download-button - remove .at-bottom from #print-friendly-button + if backToTop is not null then remove .at-bottom from backToTop end + if #info-button is not null then remove .at-bottom from #info-button end + if #shortcuts-button is not null then remove .at-bottom from #shortcuts-button end + if #download-button is not null then remove .at-bottom from #download-button end + if #print-friendly-button is not null then remove .at-bottom from #print-friendly-button end + if #cmd-k-button is not null then remove .at-bottom from #cmd-k-button end remove .at-bottom from .color-theme-switcher - remove .at-bottom from #zoom-toggle-button + if #zoom-toggle-button is not null then remove .at-bottom from #zoom-toggle-button end end -- Update last scroll position diff --git a/static/js/main.js b/static/js/main.js index 5ceef2a..66f6be2 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -421,6 +421,13 @@ // HTMX Global Error Handlers document.addEventListener('htmx:responseError', function(evt) { + // Skip contact form errors - they handle their own validation errors + // Contact form returns 400 for validation which is expected, not a system error + const target = evt.detail.target; + if (target && target.id === 'contact-response') { + return; // Contact form handles its own errors + } + console.error('HTMX Response Error:', evt.detail); console.error('Error details:', { xhr: evt.detail.xhr, diff --git a/static/js/ninja-keys-init.js b/static/js/ninja-keys-init.js new file mode 100644 index 0000000..11920fb --- /dev/null +++ b/static/js/ninja-keys-init.js @@ -0,0 +1,497 @@ +/** + * Ninja Keys Initialization + * CMD+K command palette for CV site navigation and actions + * + * Dynamic entries are fetched from the backend API: + * GET /api/cmd-k?lang={en|es} + * Returns: { experiences: [], projects: [], courses: [] } + */ + +(function() { + 'use strict'; + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initNinjaKeys); + } else { + initNinjaKeys(); + } + + async function initNinjaKeys() { + const ninjaKeys = document.getElementById('cmd-k-bar'); + if (!ninjaKeys) { + console.warn('ninja-keys element not found'); + return; + } + + // Get current language from HTML lang attribute + const lang = document.documentElement.lang || 'en'; + const currentYear = new Date().getFullYear(); + + /** + * Smooth scroll to section + * @param {string} sectionId - The ID of the section to scroll to + */ + function scrollToSection(sectionId) { + const section = document.getElementById(sectionId); + if (section) { + section.scrollIntoView({ behavior: 'smooth', block: 'start' }); + ninjaKeys.close(); + } + } + + /** + * Open a modal dialog + * @param {string} modalId - The ID of the modal to open + */ + function openModal(modalId) { + const modal = document.getElementById(modalId); + if (modal && modal.showModal) { + modal.showModal(); + } + ninjaKeys.close(); + } + + /** + * Trigger a toggle checkbox click + * @param {string} toggleId - The ID of the toggle to click + */ + function clickToggle(toggleId) { + const toggle = document.getElementById(toggleId); + if (toggle) { + toggle.checked = !toggle.checked; + toggle.dispatchEvent(new Event('change', { bubbles: true })); + } + ninjaKeys.close(); + } + + /** + * Download file with specific parameters + * @param {string} url - The URL to navigate to or download from + * @param {boolean} newTab - Whether to open in new tab + */ + function downloadFile(url, newTab = false) { + if (newTab) { + window.open(url, '_blank'); + } else { + window.location.href = url; + } + ninjaKeys.close(); + } + + /** + * Open external link + * @param {string} url - The URL to open + */ + function openLink(url) { + window.open(url, '_blank', 'noopener,noreferrer'); + ninjaKeys.close(); + } + + // ======================================================================== + // DYNAMIC ENTRIES - Fetched from API + // ======================================================================== + + /** + * Fetch dynamic entries from the backend API + * @returns {Promise<{experiences: Array, projects: Array, courses: Array}>} + */ + async function fetchDynamicEntries() { + try { + const response = await fetch(`/api/cmd-k?lang=${lang}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Failed to fetch CMD+K data:', error); + return { experiences: [], projects: [], courses: [] }; + } + } + + /** + * Convert API experience entries to ninja-keys actions + * @param {Array} experiences - Experience entries from API + * @returns {Array} ninja-keys actions + */ + function mapExperienceActions(experiences) { + return experiences.map(exp => ({ + id: exp.id, + title: exp.title, + section: exp.section, + keywords: `${exp.keywords} work job career`.toLowerCase(), + icon: '', + handler: () => scrollToSection(exp.id) + })); + } + + /** + * Convert API project entries to ninja-keys actions + * @param {Array} projects - Project entries from API + * @returns {Array} ninja-keys actions + */ + function mapProjectActions(projects) { + return projects.map(proj => ({ + id: proj.id, + title: proj.title, + section: proj.section, + keywords: `${proj.keywords} project website app`.toLowerCase(), + icon: '', + handler: () => scrollToSection(proj.id) + })); + } + + /** + * Convert API course entries to ninja-keys actions + * @param {Array} courses - Course entries from API + * @returns {Array} ninja-keys actions + */ + function mapCourseActions(courses) { + return courses.map(course => ({ + id: course.id, + title: course.title, + section: course.section, + keywords: `${course.keywords} course training certification`.toLowerCase(), + icon: '', + handler: () => scrollToSection(course.id) + })); + } + + // ======================================================================== + // STATIC ACTIONS - Navigation, Shortcuts, Downloads, etc. + // ======================================================================== + + const staticActions = [ + // ============================================ + // NAVIGATION SECTION + // ============================================ + { + id: 'nav-top', + title: 'Jump to Top', + section: 'Navigation', + keywords: 'scroll up beginning start', + icon: '', + handler: () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + ninjaKeys.close(); + } + }, + { + id: 'nav-experience', + title: 'Jump to Experience', + section: 'Navigation', + keywords: 'work jobs career employment', + icon: '', + handler: () => scrollToSection('experience') + }, + { + id: 'nav-education', + title: 'Jump to Education', + section: 'Navigation', + keywords: 'university degree school college', + icon: '', + handler: () => scrollToSection('education') + }, + { + id: 'nav-skills', + title: 'Jump to Skills', + section: 'Navigation', + keywords: 'technologies abilities competencies', + icon: '', + handler: () => scrollToSection('skills') + }, + { + id: 'nav-projects', + title: 'Jump to Projects', + section: 'Navigation', + keywords: 'portfolio websites apps', + icon: '', + handler: () => scrollToSection('projects') + }, + { + id: 'nav-courses', + title: 'Jump to Courses', + section: 'Navigation', + keywords: 'training certifications learning', + icon: '', + handler: () => scrollToSection('courses') + }, + { + id: 'nav-languages', + title: 'Jump to Languages', + section: 'Navigation', + keywords: 'spanish english portuguese', + icon: '', + handler: () => scrollToSection('languages') + }, + { + id: 'nav-awards', + title: 'Jump to Awards', + section: 'Navigation', + keywords: 'achievements recognition prizes', + icon: '', + handler: () => scrollToSection('awards') + }, + { + id: 'nav-other', + title: 'Jump to Other Info', + section: 'Navigation', + keywords: 'additional references personal', + icon: '', + handler: () => scrollToSection('other') + }, + + // ============================================ + // SOCIAL LINKS + // ============================================ + { + id: 'social-linkedin', + title: 'Open LinkedIn Profile', + section: 'Social', + keywords: 'linkedin professional network connect', + icon: '', + handler: () => openLink('https://www.linkedin.com/in/juan-andres-moreno-rubio') + }, + { + id: 'social-github', + title: 'Open GitHub Profile', + section: 'Social', + keywords: 'github code repositories open source', + icon: '', + handler: () => openLink('https://github.com/juanatsap') + }, + { + id: 'social-domestika', + title: 'Open Domestika Portfolio', + section: 'Social', + keywords: 'domestika portfolio design creative', + icon: '', + handler: () => openLink('https://www.domestika.org/es/txeo/portfolio') + }, + { + id: 'social-website', + title: 'Open Personal Website', + section: 'Social', + keywords: 'website personal portfolio cv', + icon: '', + handler: () => openLink('https://juan.andres.morenorub.io') + }, + + // ============================================ + // KEYBOARD SHORTCUTS SECTION + // ============================================ + { + id: 'shortcut-length', + title: 'Toggle CV Length', + hotkey: 'l', + section: 'Shortcuts', + keywords: 'short long extended compact full', + icon: '', + handler: () => clickToggle('lengthToggle') + }, + { + id: 'shortcut-icons', + title: 'Toggle Icons Visibility', + hotkey: 'i', + section: 'Shortcuts', + keywords: 'icons show hide emoji', + icon: '', + handler: () => clickToggle('iconToggle') + }, + { + id: 'shortcut-theme', + title: 'Toggle Visual Theme', + hotkey: 'v', + section: 'Shortcuts', + keywords: 'theme clean default style visual', + icon: '', + handler: () => clickToggle('themeToggle') + }, + { + id: 'shortcut-help', + title: 'Show Shortcuts Help', + hotkey: '?', + section: 'Shortcuts', + keywords: 'help shortcuts keyboard keys', + icon: '', + handler: () => openModal('shortcuts-modal') + }, + { + id: 'shortcut-print', + title: 'Print CV', + hotkey: 'mod+p', + section: 'Shortcuts', + keywords: 'print pdf paper', + icon: '', + handler: () => { + ninjaKeys.close(); + setTimeout(() => window.print(), 100); + } + }, + + // ============================================ + // PDF DOWNLOADS SECTION + // ============================================ + { + id: 'download-pdf-default', + title: 'Download PDF (Default - 5 pages)', + section: 'Downloads', + keywords: 'pdf download default recommended', + icon: '', + handler: () => downloadFile(`/cv-jamr-${currentYear}-${lang}.pdf`) + }, + { + id: 'download-pdf-short', + title: 'Download PDF (Short - 4 pages)', + section: 'Downloads', + keywords: 'pdf download short compact brief', + icon: '', + handler: () => downloadFile(`/export/pdf?lang=${lang}&length=short&icons=show&version=clean`) + }, + { + id: 'download-pdf-extended', + title: 'Download PDF (Extended - 9 pages)', + section: 'Downloads', + keywords: 'pdf download extended long full complete', + icon: '', + handler: () => downloadFile(`/export/pdf?lang=${lang}&length=long&icons=show&version=with_skills`) + }, + { + id: 'open-pdf-modal', + title: 'Open PDF Options', + section: 'Downloads', + keywords: 'pdf options modal choose select', + icon: '', + handler: () => openModal('pdf-modal') + }, + + // ============================================ + // TEXT CV SECTION + // ============================================ + { + id: 'view-text-cv', + title: 'View Text CV (Plain Text)', + section: 'Downloads', + keywords: 'text plain txt view terminal cli', + icon: '', + handler: () => downloadFile(`/text?lang=${lang}`, true) + }, + { + id: 'download-text-cv', + title: 'Download Text CV (.txt)', + section: 'Downloads', + keywords: 'text download txt file save', + icon: '', + handler: () => downloadFile(`/text?lang=${lang}&download=true`) + }, + + // ============================================ + // ACTIONS SECTION + // ============================================ + { + id: 'action-contact', + title: 'Open Contact Form', + section: 'Actions', + keywords: 'contact email message send hire', + icon: '', + handler: () => openModal('contact-modal') + }, + { + id: 'action-info', + title: 'Show Site Info', + section: 'Actions', + keywords: 'info about site technology stack', + icon: '', + handler: () => openModal('info-modal') + }, + { + id: 'action-zoom', + title: 'Toggle Zoom Controls', + section: 'Actions', + keywords: 'zoom magnify scale size', + icon: '', + handler: () => { + const zoomControl = document.getElementById('zoom-control'); + if (zoomControl) { + const isVisible = zoomControl.style.display !== 'none'; + zoomControl.style.display = isVisible ? 'none' : 'block'; + } + ninjaKeys.close(); + } + }, + { + id: 'action-language-en', + title: 'Switch to English', + section: 'Actions', + keywords: 'english language en switch', + icon: '', + handler: () => { + if (lang !== 'en') { + window.location.href = '/?lang=en'; + } + ninjaKeys.close(); + } + }, + { + id: 'action-language-es', + title: 'Switch to Spanish', + section: 'Actions', + keywords: 'spanish espanol language es switch cambiar idioma', + icon: '', + handler: () => { + if (lang !== 'es') { + window.location.href = '/?lang=es'; + } + ninjaKeys.close(); + } + }, + { + id: 'action-color-theme', + title: 'Change Color Theme', + section: 'Actions', + keywords: 'dark light color theme mode', + icon: '', + handler: () => { + const colorSwitcher = document.querySelector('.color-theme-switcher'); + if (colorSwitcher) { + colorSwitcher.click(); + } + ninjaKeys.close(); + } + } + ]; + + // ======================================================================== + // BUILD FINAL ACTIONS ARRAY + // ======================================================================== + + // Fetch dynamic entries from API and combine with static actions + const dynamicData = await fetchDynamicEntries(); + + const actions = [ + ...staticActions, + ...mapExperienceActions(dynamicData.experiences || []), + ...mapProjectActions(dynamicData.projects || []), + ...mapCourseActions(dynamicData.courses || []) + ]; + + // Assign actions to ninja-keys + ninjaKeys.data = actions; + + // Listen for ninja-keys events + ninjaKeys.addEventListener('selected', (event) => { + console.log('Ninja Keys: Selected action', event.detail); + }); + + // Apply custom styling + ninjaKeys.style.setProperty('--ninja-z-index', '10000'); + ninjaKeys.style.setProperty('--ninja-accent-color', '#667eea'); + ninjaKeys.style.setProperty('--ninja-font-family', 'Quicksand, sans-serif'); + + // Log counts for debugging + const expCount = (dynamicData.experiences || []).length; + const projCount = (dynamicData.projects || []).length; + const courseCount = (dynamicData.courses || []).length; + console.log(`Ninja Keys initialized with ${actions.length} actions (${expCount} experiences, ${projCount} projects, ${courseCount} courses)`); + } +})(); diff --git a/templates/index.html b/templates/index.html index e5b6720..20b413d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -105,6 +105,9 @@ + + + @@ -339,7 +342,9 @@ on scroll from window call handleScroll() on keydown set tag to event.target.tagName - set skip to (tag is 'INPUT' or tag is 'TEXTAREA') + set ninjaKeys to document.getElementById('cmd-k-bar') + set ninjaOpen to (ninjaKeys is not null and ninjaKeys.opened) + set skip to (tag is 'INPUT' or tag is 'TEXTAREA' or ninjaOpen) set noMod to (not event.ctrlKey and not event.metaKey and not event.altKey) if event.key is '?' and noMod and not skip then halt the event then call openModalShortcut('shortcuts-modal') end if (event.key is 'l' or event.key is 'L') and noMod and not skip then halt the event then call handleToggleShortcut('lengthToggle', 'lengthToggleMenu') end @@ -373,16 +378,22 @@ {{template "info-button" .}} {{template "download-button" .}} {{template "print-friendly-button" .}} + {{template "contact-button" .}} {{template "zoom-toggle-button" .}} {{template "shortcuts-button" .}} + {{template "cmd-k-button" .}} {{template "info-modal" .}} {{template "shortcuts-modal" .}} {{template "pdf-modal" .}} {{template "contact-modal" .}} {{template "zoom-control" .}} + + + +
- + {{end}} diff --git a/templates/partials/modals/shortcuts-modal.html b/templates/partials/modals/shortcuts-modal.html index 5917d11..01e9e9d 100644 --- a/templates/partials/modals/shortcuts-modal.html +++ b/templates/partials/modals/shortcuts-modal.html @@ -110,6 +110,12 @@ {{.UI.ShortcutsModal.Sections.Actions.Title}}
+
+
+ / K +
+ {{.UI.ShortcutsModal.Sections.Actions.CmdK.Description}} +
/ P diff --git a/templates/partials/navigation/action-buttons.html b/templates/partials/navigation/action-buttons.html index 8b83b55..bb0857b 100644 --- a/templates/partials/navigation/action-buttons.html +++ b/templates/partials/navigation/action-buttons.html @@ -23,12 +23,12 @@ {{.UI.Widgets.ActionButtons.PrintFriendly}}
{{end}} diff --git a/templates/partials/sections/courses.html b/templates/partials/sections/courses.html index c967ae8..5a4d64a 100644 --- a/templates/partials/sections/courses.html +++ b/templates/partials/sections/courses.html @@ -12,7 +12,7 @@ {{range .CV.Courses}} -
+
{{if .CourseLogo}}
{{.Title}} logo diff --git a/templates/partials/sections/experience.html b/templates/partials/sections/experience.html index ede966e..c6ffae6 100644 --- a/templates/partials/sections/experience.html +++ b/templates/partials/sections/experience.html @@ -12,7 +12,7 @@ {{range .CV.Experience}} -
+