From 026f47943865494f06bc87fe85ef9ad864eb5b34 Mon Sep 17 00:00:00 2001 From: GMNAPI Date: Sun, 29 Mar 2026 14:10:10 +0200 Subject: [PATCH 1/4] feat(projects): replace curated project list with real screenshots - Replace 11 placeholder projects with 4 curated projects (CV + side) - Add gestiono-mi-negocio-facturacion, vita-liber-erp-interno, fastbyte-api-servicios, seedstockers-b2b-platform - Add real WebP screenshots for GestionoMiNegocio and VitaLiber - FastByte and Seedstockers use category gradient fallback (pending screenshots) - Update PROJECT_SLUGS for static route generation - Closes #73 --- apps/portfolio/messages/en.json | 382 +++++++++++------- apps/portfolio/messages/es.json | 382 +++++++++++------- .../images/projects/gestinio-mi-negocio.webp | Bin 0 -> 13732 bytes .../projects/vita-liber-erp-interno.webp | Bin 0 -> 20082 bytes .../src/shared/constants/project-slugs.ts | 15 +- 5 files changed, 478 insertions(+), 301 deletions(-) create mode 100644 apps/portfolio/public/images/projects/gestinio-mi-negocio.webp create mode 100644 apps/portfolio/public/images/projects/vita-liber-erp-interno.webp diff --git a/apps/portfolio/messages/en.json b/apps/portfolio/messages/en.json index 256c9f8..4dc4cbf 100644 --- a/apps/portfolio/messages/en.json +++ b/apps/portfolio/messages/en.json @@ -107,28 +107,44 @@ { "role": "Consultant & Tech Lead", "company": "Fastbyte SL", - "period": { "start": "2019", "end": "2025" }, + "period": { + "start": "2019", + "end": "2025" + }, "headline": "Requirements gathering with clients/teams and translation into practical solutions.", - "achievements": ["Constant communication, task follow-up and phased delivery."] + "achievements": [ + "Constant communication, task follow-up and phased delivery." + ] }, { "role": "Program Specialist", "company": "Vita Liber S.L.U.", - "period": { "start": "2020", "end": "2025" }, + "period": { + "start": "2020", + "end": "2025" + }, "headline": "Managing internal user needs: prioritization, incidents, validation and functional support.", "achievements": [] }, { "role": "Co-founder & Lead Dev", "company": "GestionoMiNegocio", - "period": { "start": "2020", "end": "2025" }, + "period": { + "start": "2020", + "end": "2025" + }, "headline": "Working with beta users: collecting feedback, improvements and product onboarding support.", - "achievements": ["Focus on clarity, quality and user experience."] + "achievements": [ + "Focus on clarity, quality and user experience." + ] }, { "role": "Developer", "company": "240dots", - "period": { "start": "2020", "end": "2023" }, + "period": { + "start": "2020", + "end": "2023" + }, "headline": "Development of headless solutions combining WordPress and Symfony.", "achievements": [ "Building plugins connected to custom APIs.", @@ -138,7 +154,10 @@ { "role": "Webmaster", "company": "Seedstockers (BCG)", - "period": { "start": "2023", "end": "2024" }, + "period": { + "start": "2023", + "end": "2024" + }, "headline": "Multilingual e-commerce management (PrestaShop) and collaboration with payment/logistics areas.", "achievements": [ "Coordination to resolve operational incidents in the purchase flow (order, payment, shipping).", @@ -199,7 +218,12 @@ "usagePercentage": 31, "experienceYears": 6, "experienceProjects": 18, - "keywords": ["Symfony", "Laravel", "OOP", "DDD"] + "keywords": [ + "Symfony", + "Laravel", + "OOP", + "DDD" + ] }, { "name": "Symfony 7", @@ -207,7 +231,11 @@ "usagePercentage": 31, "experienceYears": 5, "experienceProjects": 12, - "keywords": ["API Platform", "Messenger", "Hexagonal"] + "keywords": [ + "API Platform", + "Messenger", + "Hexagonal" + ] }, { "name": "Node.js", @@ -215,7 +243,11 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 8, - "keywords": ["Express", "Serverless", "Workers"] + "keywords": [ + "Express", + "Serverless", + "Workers" + ] }, { "name": "API Platform", @@ -223,7 +255,11 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 6, - "keywords": ["OpenAPI", "JWT", "HATEOAS"] + "keywords": [ + "OpenAPI", + "JWT", + "HATEOAS" + ] } ] }, @@ -239,7 +275,11 @@ "usagePercentage": 29, "experienceYears": 5, "experienceProjects": 15, - "keywords": ["TypeScript", "ESNext", "monorepos"] + "keywords": [ + "TypeScript", + "ESNext", + "monorepos" + ] }, { "name": "React 18/19", @@ -247,7 +287,12 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 12, - "keywords": ["Hooks", "Context", "RSC", "RHF"] + "keywords": [ + "Hooks", + "Context", + "RSC", + "RHF" + ] }, { "name": "Next.js 15", @@ -255,7 +300,12 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": ["App Router", "ISR", "SEO", "next-intl"] + "keywords": [ + "App Router", + "ISR", + "SEO", + "next-intl" + ] }, { "name": "Tailwind CSS / shadcn/ui", @@ -263,7 +313,11 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": ["Design tokens", "Dark mode", "Framer Motion"] + "keywords": [ + "Design tokens", + "Dark mode", + "Framer Motion" + ] } ] }, @@ -279,7 +333,11 @@ "usagePercentage": 90, "experienceYears": 5, "experienceProjects": 15, - "keywords": ["Stakeholders", "Requirements", "Documentation"] + "keywords": [ + "Stakeholders", + "Requirements", + "Documentation" + ] }, { "name": "Incident Management", @@ -287,7 +345,11 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 12, - "keywords": ["Support", "Resolution", "Follow-up"] + "keywords": [ + "Support", + "Resolution", + "Follow-up" + ] }, { "name": "Team Coordination", @@ -295,7 +357,11 @@ "usagePercentage": 80, "experienceYears": 4, "experienceProjects": 10, - "keywords": ["Leadership", "Methodologies", "Deliveries"] + "keywords": [ + "Leadership", + "Methodologies", + "Deliveries" + ] }, { "name": "Client Orientation", @@ -303,7 +369,11 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 15, - "keywords": ["UX", "Feedback", "Continuous improvement"] + "keywords": [ + "UX", + "Feedback", + "Continuous improvement" + ] } ] }, @@ -319,7 +389,11 @@ "usagePercentage": 28, "experienceYears": 5, "experienceProjects": 14, - "keywords": ["Replication", "Query tuning", "Migrations"] + "keywords": [ + "Replication", + "Query tuning", + "Migrations" + ] }, { "name": "PostgreSQL", @@ -327,7 +401,11 @@ "usagePercentage": 17, "experienceYears": 4, "experienceProjects": 7, - "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] + "keywords": [ + "JSONB", + "PL/pgSQL", + "Materialized views" + ] }, { "name": "MongoDB", @@ -335,7 +413,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 4, - "keywords": ["Aggregation", "Atlas", "ODM"] + "keywords": [ + "Aggregation", + "Atlas", + "ODM" + ] }, { "name": "ElasticSearch", @@ -343,7 +425,11 @@ "usagePercentage": 6, "experienceYears": 3, "experienceProjects": 3, - "keywords": ["Full-text", "Kibana", "Ingest pipelines"] + "keywords": [ + "Full-text", + "Kibana", + "Ingest pipelines" + ] } ] }, @@ -359,7 +445,11 @@ "usagePercentage": 40, "experienceYears": 5, "experienceProjects": 20, - "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] + "keywords": [ + "Multi-stage", + "CI/CD", + "Devcontainers" + ] }, { "name": "GitHub Actions", @@ -367,7 +457,11 @@ "usagePercentage": 34, "experienceYears": 4, "experienceProjects": 10, - "keywords": ["Matrix builds", "Secrets", "Artifacts"] + "keywords": [ + "Matrix builds", + "Secrets", + "Artifacts" + ] }, { "name": "AWS / VPS", @@ -375,7 +469,12 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 6, - "keywords": ["EC2", "Lightsail", "S3", "Terraform"] + "keywords": [ + "EC2", + "Lightsail", + "S3", + "Terraform" + ] }, { "name": "Terraform", @@ -383,7 +482,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 3, - "keywords": ["IaC", "Modules", "State management"] + "keywords": [ + "IaC", + "Modules", + "State management" + ] } ] }, @@ -399,7 +502,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 5, - "keywords": ["GPT", "Embeddings", "Function calling"] + "keywords": [ + "GPT", + "Embeddings", + "Function calling" + ] }, { "name": "RAG Systems", @@ -407,7 +514,11 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": ["Vector stores", "Hybrid search", "Context windows"] + "keywords": [ + "Vector stores", + "Hybrid search", + "Context windows" + ] }, { "name": "n8n Workflows", @@ -415,7 +526,11 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 4, - "keywords": ["Automation", "Webhook", "Orchestration"] + "keywords": [ + "Automation", + "Webhook", + "Orchestration" + ] } ] }, @@ -431,7 +546,11 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 10, - "keywords": ["TDD", "Mockery", "Integration tests"] + "keywords": [ + "TDD", + "Mockery", + "Integration tests" + ] }, { "name": "Vitest", @@ -439,7 +558,11 @@ "usagePercentage": 9, "experienceYears": 2, "experienceProjects": 5, - "keywords": ["RTL", "Snapshots", "Monorepo"] + "keywords": [ + "RTL", + "Snapshots", + "Monorepo" + ] }, { "name": "Jest", @@ -447,7 +570,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 6, - "keywords": ["Mocks", "Coverage", "Watch mode"] + "keywords": [ + "Mocks", + "Coverage", + "Watch mode" + ] }, { "name": "Playwright", @@ -455,7 +582,11 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] + "keywords": [ + "Trace viewer", + "CI pipelines", + "Cross-browser" + ] } ] } @@ -463,11 +594,26 @@ "summary": { "totalRepositories": 35, "mainLanguagesShare": [ - { "language": "PHP", "usagePercentage": 31.4 }, - { "language": "JavaScript", "usagePercentage": 20.0 }, - { "language": "Twig", "usagePercentage": 14.3 }, - { "language": "TypeScript", "usagePercentage": 8.6 }, - { "language": "Python", "usagePercentage": 2.9 } + { + "language": "PHP", + "usagePercentage": 31.4 + }, + { + "language": "JavaScript", + "usagePercentage": 20 + }, + { + "language": "Twig", + "usagePercentage": 14.3 + }, + { + "language": "TypeScript", + "usagePercentage": 8.6 + }, + { + "language": "Python", + "usagePercentage": 2.9 + } ], "contributionsLastYear": 1706, "activeRepositories": 9, @@ -537,126 +683,72 @@ ], "items": [ { - "id": "verifactur-gmn", - "title": "Electronic Invoicing System", - "description": "End-to-end electronic invoicing system compliant with VeriFactu/AEAT, offering SaaS flows and fiscal automation.", - "tech": ["Symfony 7", "API Platform", "PostgreSQL", "Docker", "CI/CD"], - "links": { - "github": "https://github.com/GMNAPI/VerifacturGMN" - }, - "categoryId": "facturacion-compliance" - }, - { - "id": "facturae-docusaurus", - "title": "Technical Documentation Portal", - "description": "Technical documentation portal for VeriFactu and Facturae with guides, examples and automated deployment.", - "tech": ["Docusaurus", "TypeScript", "MDX", "GitHub Actions"], + "id": "gestiono-mi-negocio-facturacion", + "title": "GestionoMiNegocio - Billing Software", + "description": "Led development as Co-founder & Lead Dev. Comprehensive platform for basic administrative management, including billing and collection control. Architecture oriented to handle workload peaks while maintaining quality and response times.", + "tech": [ + "Symfony 7", + "API Platform", + "PostgreSQL", + "React", + "TypeScript", + "Docker", + "GitHub Actions" + ], "links": { - "github": "https://github.com/GMNAPI/FacturaeDocusaurus" + "demo": "https://gestionominegocio.com/" }, - "categoryId": "facturacion-compliance" + "categoryId": "facturacion-compliance", + "imageUrl": "/images/projects/gestinio-mi-negocio.webp" }, { - "id": "apigns", - "title": "REST API for Service Management", - "description": "Modern REST API for comprehensive service management with advanced authentication and external dashboards.", - "tech": ["Symfony 7.3", "API Platform", "MariaDB", "Redis", "Docker"], - "links": { - "github": "https://github.com/GMNAPI/apiGns" - }, - "categoryId": "gestion-servicios" + "id": "vita-liber-erp-interno", + "title": "Vita Liber - ERP Rescue & Modernisation", + "description": "Following a critical local server failure that corrupted backups in 2021/2022, I took over the legacy code. Led data recovery and built a new corporate system from scratch using MVC architecture and modern paradigms.", + "tech": [ + "PHP", + "Symfony", + "Laravel", + "MySQL", + "Redis", + "Bootstrap", + "Tailwind CSS", + "JavaScript" + ], + "links": {}, + "categoryId": "erp-transformacion", + "imageUrl": "/images/projects/vita-liber-erp-interno.webp" }, { - "id": "frontend-json", - "title": "Enterprise Frontend", - "description": "Enterprise frontend in Next.js 15 with clean architecture, strict typing and atomic design system.", - "tech": ["Next.js 15", "TypeScript", "Tailwind CSS", "Clean Architecture"], - "links": { - "github": "https://github.com/GMNAPI/FrontendJson" - }, + "id": "fastbyte-api-servicios", + "title": "Fastbyte - Service Management REST API", + "description": "Consultant and Tech Lead focused on requirement gathering with clients and translating them into practical solutions. Robust REST API for service management with phased deliveries and constant client communication.", + "tech": [ + "Symfony 7.3", + "API Platform", + "MariaDB", + "Docker" + ], + "links": {}, "categoryId": "gestion-servicios" }, { - "id": "fynkus", - "title": "Booking Platform", - "description": "Sports booking platform using hexagonal architecture, DDD and CQRS with a scalability focus.", - "tech": ["PHP 8.2", "Symfony 5.4", "CQRS", "React 18", "Docker"], - "links": { - "github": "https://github.com/GMNAPI/FYNKUS" - }, - "categoryId": "arquitectura-avanzada" - }, - { - "id": "gestiono-mi-negocio", - "title": "ERP SaaS Platform", - "description": "Legacy ERP migration to modular SaaS with multi-tenant support, automated billing and analytic dashboards.", - "tech": ["Symfony 7", "Next.js", "RabbitMQ", "PostgreSQL", "Terraform"], - "links": { - "github": "https://github.com/GMNAPI/gestionominegocio" - }, - "categoryId": "erp-transformacion" - }, - { - "id": "vita-liber", - "title": "Enterprise Management Platform", - "description": "Full-stack platform for logistics and academic management integrating Symfony + Next.js.", - "tech": ["Next.js 14", "Symfony 6", "MariaDB", "API Platform", "Jest"], - "links": { - "github": "https://github.com/GMNAPI/vitaliber" - }, - "categoryId": "erp-transformacion" - }, - { - "id": "genesis-enterprise", - "title": "Enterprise Ecosystem", - "description": "Enterprise ecosystem (4M LOC) modernized into microservices with Symfony, messaging and DevOps practices.", - "tech": ["Symfony", "RabbitMQ", "Microservices", "Docker", "CI/CD"], - "links": { - "github": "https://github.com/GMNAPI/genesis" - }, - "categoryId": "erp-transformacion" - }, - { - "id": "streamlit-gmn", - "title": "Business Model Simulator", - "description": "SaaS business model simulator with real-time analytics and interactive dashboards.", - "tech": ["Python", "Streamlit", "Pandas", "Plotly"], - "links": { - "github": "https://github.com/GMNAPI/streamlitGMN" - }, - "categoryId": "herramientas" - }, - { - "id": "zend-laminas-tech-tests", - "title": "Technical Tests Collection", - "description": "Collection of 10 realistic technical tests for Laminas MVC and Mezzio aimed at professional training.", - "tech": ["Laminas", "PHP", "Mezzio", "Docker", "Testing"], - "links": { - "github": "https://github.com/GMNAPI/zend-laminas-tech-tests" - }, - "categoryId": "educacion" - }, - { - "id": "llarjove", - "title": "LlarJove - AI Housing Assistant", - "description": "RAG chatbot helping young people (18-35) in Catalonia find rental subsidies, understand their rights (LAU, deposit rules) and access public housing resources.", + "id": "seedstockers-b2b-platform", + "title": "Seedstockers - B2B Platform & Web Integration", + "description": "Maintenance and development of a React-based B2B platform with complex Web Services integration for Prestashop. Technical coordination with logistics and payment teams to resolve operational incidents.", "tech": [ + "React", "TypeScript", - "React 19", - "Vite 6", - "Tailwind v4", - "Fastify", - "RAG", - "OpenAI GPT-4", - "ChromaDB", - "Docker", - "Railway" + "Prestashop", + "Node.js", + "Express", + "Jest", + "Tailwind CSS" ], "links": { - "github": "https://github.com/GMNAPI/llarjove", - "demo": "https://llarjove-production-754a.up.railway.app/" + "demo": "https://seedstockers.com/" }, - "categoryId": "ia-chatbots" + "categoryId": "erp-transformacion" } ] }, diff --git a/apps/portfolio/messages/es.json b/apps/portfolio/messages/es.json index 73d20dd..8729c9e 100644 --- a/apps/portfolio/messages/es.json +++ b/apps/portfolio/messages/es.json @@ -107,28 +107,44 @@ { "role": "Consultor & Tech Lead", "company": "Fastbyte SL", - "period": { "start": "2019", "end": "2025" }, + "period": { + "start": "2019", + "end": "2025" + }, "headline": "Toma de requisitos con clientes/equipos y traducción a soluciones prácticas.", - "achievements": ["Comunicación constante, seguimiento de tareas y entrega por fases."] + "achievements": [ + "Comunicación constante, seguimiento de tareas y entrega por fases." + ] }, { "role": "Especialista de Programa", "company": "Vita Liber S.L.U.", - "period": { "start": "2020", "end": "2025" }, + "period": { + "start": "2020", + "end": "2025" + }, "headline": "Gestión de necesidades de usuarios internos: priorización, incidencias, validación y soporte funcional.", "achievements": [] }, { "role": "Cofundador & Lead Dev", "company": "GestionoMiNegocio", - "period": { "start": "2020", "end": "2025" }, + "period": { + "start": "2020", + "end": "2025" + }, "headline": "Trabajo con usuarios beta: recogida de feedback, mejoras y acompañamiento en uso del producto.", - "achievements": ["Enfoque en claridad, calidad y experiencia de usuario."] + "achievements": [ + "Enfoque en claridad, calidad y experiencia de usuario." + ] }, { "role": "Developer", "company": "240dots", - "period": { "start": "2020", "end": "2023" }, + "period": { + "start": "2020", + "end": "2023" + }, "headline": "Desarrollo de soluciones headless combinando WordPress y Symfony.", "achievements": [ "Construcción de plugins conectados con APIs personalizadas.", @@ -138,7 +154,10 @@ { "role": "Webmaster", "company": "Seedstockers (BCG)", - "period": { "start": "2023", "end": "2024" }, + "period": { + "start": "2023", + "end": "2024" + }, "headline": "Gestión de e-commerce multilingüe (PrestaShop) y colaboración con áreas de pagos/logística.", "achievements": [ "Coordinación para resolver incidencias operativas en el flujo de compra (pedido, pago, envío).", @@ -199,7 +218,12 @@ "usagePercentage": 31, "experienceYears": 6, "experienceProjects": 18, - "keywords": ["Symfony", "Laravel", "OOP", "DDD"] + "keywords": [ + "Symfony", + "Laravel", + "OOP", + "DDD" + ] }, { "name": "Symfony 7", @@ -207,7 +231,11 @@ "usagePercentage": 31, "experienceYears": 5, "experienceProjects": 12, - "keywords": ["API Platform", "Messenger", "Hexagonal"] + "keywords": [ + "API Platform", + "Messenger", + "Hexagonal" + ] }, { "name": "Node.js", @@ -215,7 +243,11 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 8, - "keywords": ["Express", "Serverless", "Workers"] + "keywords": [ + "Express", + "Serverless", + "Workers" + ] }, { "name": "API Platform", @@ -223,7 +255,11 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 6, - "keywords": ["OpenAPI", "JWT", "HATEOAS"] + "keywords": [ + "OpenAPI", + "JWT", + "HATEOAS" + ] } ] }, @@ -239,7 +275,11 @@ "usagePercentage": 29, "experienceYears": 5, "experienceProjects": 15, - "keywords": ["TypeScript", "ESNext", "monorepos"] + "keywords": [ + "TypeScript", + "ESNext", + "monorepos" + ] }, { "name": "React 18/19", @@ -247,7 +287,12 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 12, - "keywords": ["Hooks", "Context", "RSC", "RHF"] + "keywords": [ + "Hooks", + "Context", + "RSC", + "RHF" + ] }, { "name": "Next.js 15", @@ -255,7 +300,12 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": ["App Router", "ISR", "SEO", "next-intl"] + "keywords": [ + "App Router", + "ISR", + "SEO", + "next-intl" + ] }, { "name": "Tailwind CSS / shadcn/ui", @@ -263,7 +313,11 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": ["Design tokens", "Dark mode", "Framer Motion"] + "keywords": [ + "Design tokens", + "Dark mode", + "Framer Motion" + ] } ] }, @@ -279,7 +333,11 @@ "usagePercentage": 90, "experienceYears": 5, "experienceProjects": 15, - "keywords": ["Stakeholders", "Requisitos", "Documentación"] + "keywords": [ + "Stakeholders", + "Requisitos", + "Documentación" + ] }, { "name": "Gestión de incidencias", @@ -287,7 +345,11 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 12, - "keywords": ["Soporte", "Resolución", "Seguimiento"] + "keywords": [ + "Soporte", + "Resolución", + "Seguimiento" + ] }, { "name": "Coordinación de equipos", @@ -295,7 +357,11 @@ "usagePercentage": 80, "experienceYears": 4, "experienceProjects": 10, - "keywords": ["Liderazgo", "Metodologías", "Entregas"] + "keywords": [ + "Liderazgo", + "Metodologías", + "Entregas" + ] }, { "name": "Orientación al cliente", @@ -303,7 +369,11 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 15, - "keywords": ["UX", "Feedback", "Mejora continua"] + "keywords": [ + "UX", + "Feedback", + "Mejora continua" + ] } ] }, @@ -319,7 +389,11 @@ "usagePercentage": 28, "experienceYears": 5, "experienceProjects": 14, - "keywords": ["Replication", "Query tuning", "Migrations"] + "keywords": [ + "Replication", + "Query tuning", + "Migrations" + ] }, { "name": "PostgreSQL", @@ -327,7 +401,11 @@ "usagePercentage": 17, "experienceYears": 4, "experienceProjects": 7, - "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] + "keywords": [ + "JSONB", + "PL/pgSQL", + "Materialized views" + ] }, { "name": "MongoDB", @@ -335,7 +413,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 4, - "keywords": ["Aggregation", "Atlas", "ODM"] + "keywords": [ + "Aggregation", + "Atlas", + "ODM" + ] }, { "name": "ElasticSearch", @@ -343,7 +425,11 @@ "usagePercentage": 6, "experienceYears": 3, "experienceProjects": 3, - "keywords": ["Full-text", "Kibana", "Ingest pipelines"] + "keywords": [ + "Full-text", + "Kibana", + "Ingest pipelines" + ] } ] }, @@ -359,7 +445,11 @@ "usagePercentage": 40, "experienceYears": 5, "experienceProjects": 20, - "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] + "keywords": [ + "Multi-stage", + "CI/CD", + "Devcontainers" + ] }, { "name": "GitHub Actions", @@ -367,7 +457,11 @@ "usagePercentage": 34, "experienceYears": 4, "experienceProjects": 10, - "keywords": ["Matrix builds", "Secrets", "Artifacts"] + "keywords": [ + "Matrix builds", + "Secrets", + "Artifacts" + ] }, { "name": "AWS / VPS", @@ -375,7 +469,12 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 6, - "keywords": ["EC2", "Lightsail", "S3", "Terraform"] + "keywords": [ + "EC2", + "Lightsail", + "S3", + "Terraform" + ] }, { "name": "Terraform", @@ -383,7 +482,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 3, - "keywords": ["IaC", "Modules", "State management"] + "keywords": [ + "IaC", + "Modules", + "State management" + ] } ] }, @@ -399,7 +502,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 5, - "keywords": ["GPT", "Embeddings", "Function calling"] + "keywords": [ + "GPT", + "Embeddings", + "Function calling" + ] }, { "name": "RAG Systems", @@ -407,7 +514,11 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": ["Vector stores", "Hybrid search", "Context windows"] + "keywords": [ + "Vector stores", + "Hybrid search", + "Context windows" + ] }, { "name": "n8n Workflows", @@ -415,7 +526,11 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 4, - "keywords": ["Automation", "Webhook", "Orchestration"] + "keywords": [ + "Automation", + "Webhook", + "Orchestration" + ] } ] }, @@ -431,7 +546,11 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 10, - "keywords": ["TDD", "Mockery", "Integration tests"] + "keywords": [ + "TDD", + "Mockery", + "Integration tests" + ] }, { "name": "Vitest", @@ -439,7 +558,11 @@ "usagePercentage": 9, "experienceYears": 2, "experienceProjects": 5, - "keywords": ["RTL", "Snapshots", "Monorepo"] + "keywords": [ + "RTL", + "Snapshots", + "Monorepo" + ] }, { "name": "Jest", @@ -447,7 +570,11 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 6, - "keywords": ["Mocks", "Coverage", "Watch mode"] + "keywords": [ + "Mocks", + "Coverage", + "Watch mode" + ] }, { "name": "Playwright", @@ -455,7 +582,11 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] + "keywords": [ + "Trace viewer", + "CI pipelines", + "Cross-browser" + ] } ] } @@ -463,11 +594,26 @@ "summary": { "totalRepositories": 35, "mainLanguagesShare": [ - { "language": "PHP", "usagePercentage": 31.4 }, - { "language": "JavaScript", "usagePercentage": 20.0 }, - { "language": "Twig", "usagePercentage": 14.3 }, - { "language": "TypeScript", "usagePercentage": 8.6 }, - { "language": "Python", "usagePercentage": 2.9 } + { + "language": "PHP", + "usagePercentage": 31.4 + }, + { + "language": "JavaScript", + "usagePercentage": 20 + }, + { + "language": "Twig", + "usagePercentage": 14.3 + }, + { + "language": "TypeScript", + "usagePercentage": 8.6 + }, + { + "language": "Python", + "usagePercentage": 2.9 + } ], "contributionsLastYear": 1706, "activeRepositories": 9, @@ -537,126 +683,72 @@ ], "items": [ { - "id": "verifactur-gmn", - "title": "Sistema de Facturación Electrónica", - "description": "Sistema completo de facturación electrónica compatible con VeriFactu/AEAT con flujos SaaS y automatización fiscal.", - "tech": ["Symfony 7", "API Platform", "PostgreSQL", "Docker", "CI/CD"], - "links": { - "github": "https://github.com/GMNAPI/VerifacturGMN" - }, - "categoryId": "facturacion-compliance" - }, - { - "id": "facturae-docusaurus", - "title": "Portal de Documentación Técnica", - "description": "Portal de documentación técnica para VeriFactu y Facturae con guías, ejemplos y despliegue automatizado.", - "tech": ["Docusaurus", "TypeScript", "MDX", "GitHub Actions"], + "id": "gestiono-mi-negocio-facturacion", + "title": "GestionoMiNegocio - Software de Facturación", + "description": "Desarrollo liderado como Cofundador y Lead Dev. Plataforma integral para la gestión administrativa básica, facturación y control de cobros. Arquitectura orientada a resolver picos de trabajo manteniendo calidad y tiempos de respuesta.", + "tech": [ + "Symfony 7", + "API Platform", + "PostgreSQL", + "React", + "TypeScript", + "Docker", + "GitHub Actions" + ], "links": { - "github": "https://github.com/GMNAPI/FacturaeDocusaurus" + "demo": "https://gestionominegocio.com/" }, - "categoryId": "facturacion-compliance" + "categoryId": "facturacion-compliance", + "imageUrl": "/images/projects/gestinio-mi-negocio.webp" }, { - "id": "apigns", - "title": "API REST de Gestión de Servicios", - "description": "API REST moderna para la gestión integral de servicios con autenticación avanzada y paneles externos.", - "tech": ["Symfony 7.3", "API Platform", "MariaDB", "Redis", "Docker"], - "links": { - "github": "https://github.com/GMNAPI/apiGns" - }, - "categoryId": "gestion-servicios" + "id": "vita-liber-erp-interno", + "title": "Vita Liber - Rescate y Modernización de ERP", + "description": "Tras un fallo crítico en servidores locales que corrompió backups en 2021/2022, asumí el control del código legacy. Lideré la recuperación de datos y desarrollé desde cero un nuevo sistema corporativo con arquitectura MVC y paradigmas modernos.", + "tech": [ + "PHP", + "Symfony", + "Laravel", + "MySQL", + "Redis", + "Bootstrap", + "Tailwind CSS", + "JavaScript" + ], + "links": {}, + "categoryId": "erp-transformacion", + "imageUrl": "/images/projects/vita-liber-erp-interno.webp" }, { - "id": "frontend-json", - "title": "Frontend Empresarial", - "description": "Frontend empresarial en Next.js 15 con arquitectura limpia, tipado estricto y diseño atómico.", - "tech": ["Next.js 15", "TypeScript", "Tailwind CSS", "Clean Architecture"], - "links": { - "github": "https://github.com/GMNAPI/FrontendJson" - }, + "id": "fastbyte-api-servicios", + "title": "Fastbyte - API REST de Gestión de Servicios", + "description": "Consultor y Tech Lead en toma de requisitos con clientes y su traducción a soluciones prácticas. API REST robusta para gestión de servicios con entregas por fases y comunicación constante con el cliente.", + "tech": [ + "Symfony 7.3", + "API Platform", + "MariaDB", + "Docker" + ], + "links": {}, "categoryId": "gestion-servicios" }, { - "id": "fynkus", - "title": "Plataforma de Reservas", - "description": "Plataforma de reservas deportivas con arquitectura hexagonal, DDD y CQRS, enfocada en escalabilidad.", - "tech": ["PHP 8.2", "Symfony 5.4", "CQRS", "React 18", "Docker"], - "links": { - "github": "https://github.com/GMNAPI/FYNKUS" - }, - "categoryId": "arquitectura-avanzada" - }, - { - "id": "gestiono-mi-negocio", - "title": "Plataforma ERP SaaS", - "description": "Migración de ERP legacy a SaaS modular con multi-tenant, automatización de facturación y paneles analíticos.", - "tech": ["Symfony 7", "Next.js", "RabbitMQ", "PostgreSQL", "Terraform"], - "links": { - "github": "https://github.com/GMNAPI/gestionominegocio" - }, - "categoryId": "erp-transformacion" - }, - { - "id": "vita-liber", - "title": "Plataforma de Gestión Empresarial", - "description": "Plataforma full-stack para gestión logística y académica con integración Symfony + Next.js.", - "tech": ["Next.js 14", "Symfony 6", "MariaDB", "API Platform", "Jest"], - "links": { - "github": "https://github.com/GMNAPI/vitaliber" - }, - "categoryId": "erp-transformacion" - }, - { - "id": "genesis-enterprise", - "title": "Ecosistema Enterprise", - "description": "Ecosistema enterprise (4M LOC) modernizado hacia microservicios con Symfony, mensajería y DevOps.", - "tech": ["Symfony", "RabbitMQ", "Microservices", "Docker", "CI/CD"], - "links": { - "github": "https://github.com/GMNAPI/genesis" - }, - "categoryId": "erp-transformacion" - }, - { - "id": "streamlit-gmn", - "title": "Simulador de Modelos de Negocio", - "description": "Simulador de modelos de negocio SaaS con analítica en tiempo real y dashboards interactivos.", - "tech": ["Python", "Streamlit", "Pandas", "Plotly"], - "links": { - "github": "https://github.com/GMNAPI/streamlitGMN" - }, - "categoryId": "herramientas" - }, - { - "id": "zend-laminas-tech-tests", - "title": "Colección de Pruebas Técnicas", - "description": "Colección de 10 pruebas técnicas realistas para Laminas MVC y Mezzio orientadas a formación profesional.", - "tech": ["Laminas", "PHP", "Mezzio", "Docker", "Testing"], - "links": { - "github": "https://github.com/GMNAPI/zend-laminas-tech-tests" - }, - "categoryId": "educacion" - }, - { - "id": "llarjove", - "title": "LlarJove - Asistente de Vivienda con IA", - "description": "Chatbot RAG que ayuda a jóvenes de 18-35 años en Catalunya a encontrar ayudas de alquiler, conocer sus derechos (LAU, fianza) y acceder a recursos de vivienda públicos.", + "id": "seedstockers-b2b-platform", + "title": "Seedstockers - Plataforma B2B & Integración Web", + "description": "Mantenimiento y desarrollo de plataforma B2B en React e integración de Web Services complejos con Prestashop. Coordinación técnica con áreas de logística y pagos para resolución de incidencias operativas.", "tech": [ + "React", "TypeScript", - "React 19", - "Vite 6", - "Tailwind v4", - "Fastify", - "RAG", - "OpenAI GPT-4", - "ChromaDB", - "Docker", - "Railway" + "Prestashop", + "Node.js", + "Express", + "Jest", + "Tailwind CSS" ], "links": { - "github": "https://github.com/GMNAPI/llarjove", - "demo": "https://llarjove-production-754a.up.railway.app/" + "demo": "https://seedstockers.com/" }, - "categoryId": "ia-chatbots" + "categoryId": "erp-transformacion" } ] }, diff --git a/apps/portfolio/public/images/projects/gestinio-mi-negocio.webp b/apps/portfolio/public/images/projects/gestinio-mi-negocio.webp new file mode 100644 index 0000000000000000000000000000000000000000..7b714e986b16ca6340a6c00eec398269e3eb76c0 GIT binary patch literal 13732 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 UlpTN?7k9TIiTmRW*+0_%0TIfBt^fc4 literal 0 HcmV?d00001 diff --git a/apps/portfolio/public/images/projects/vita-liber-erp-interno.webp b/apps/portfolio/public/images/projects/vita-liber-erp-interno.webp new file mode 100644 index 0000000000000000000000000000000000000000..a71c3253b7cb7dfdd7f2f62dbbafa60d9205c897 GIT binary patch literal 20082 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 Zv-~O#Qo|a8#&x=z& literal 0 HcmV?d00001 diff --git a/apps/portfolio/src/shared/constants/project-slugs.ts b/apps/portfolio/src/shared/constants/project-slugs.ts index 36b589f..9f8986b 100644 --- a/apps/portfolio/src/shared/constants/project-slugs.ts +++ b/apps/portfolio/src/shared/constants/project-slugs.ts @@ -6,17 +6,10 @@ */ export const PROJECT_SLUGS = [ - 'apigns', - 'facturae-docusaurus', - 'frontend-json', - 'fynkus', - 'genesis-enterprise', - 'gestiono-mi-negocio', - 'llarjove', - 'streamlit-gmn', - 'verifactur-gmn', - 'vita-liber', - 'zend-laminas-tech-tests', + 'gestiono-mi-negocio-facturacion', + 'vita-liber-erp-interno', + 'fastbyte-api-servicios', + 'seedstockers-b2b-platform', ] as const; export type ProjectSlug = (typeof PROJECT_SLUGS)[number]; From 869aacd126969daa5c769f828a1d9c205642287c Mon Sep 17 00:00:00 2001 From: GMNAPI Date: Sun, 29 Mar 2026 14:21:15 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fix(projects):=20correct=20typo=20in=20Gest?= =?UTF-8?q?ionoMiNegocio=20image=20filename=20(gestinio=20=E2=86=92=20gest?= =?UTF-8?q?iono)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/portfolio/messages/en.json | 2 +- apps/portfolio/messages/es.json | 2 +- ...nio-mi-negocio.webp => gestiono-mi-negocio.webp} | Bin 3 files changed, 2 insertions(+), 2 deletions(-) rename apps/portfolio/public/images/projects/{gestinio-mi-negocio.webp => gestiono-mi-negocio.webp} (100%) diff --git a/apps/portfolio/messages/en.json b/apps/portfolio/messages/en.json index 4dc4cbf..11581b5 100644 --- a/apps/portfolio/messages/en.json +++ b/apps/portfolio/messages/en.json @@ -699,7 +699,7 @@ "demo": "https://gestionominegocio.com/" }, "categoryId": "facturacion-compliance", - "imageUrl": "/images/projects/gestinio-mi-negocio.webp" + "imageUrl": "/images/projects/gestiono-mi-negocio.webp" }, { "id": "vita-liber-erp-interno", diff --git a/apps/portfolio/messages/es.json b/apps/portfolio/messages/es.json index 8729c9e..5bcc271 100644 --- a/apps/portfolio/messages/es.json +++ b/apps/portfolio/messages/es.json @@ -699,7 +699,7 @@ "demo": "https://gestionominegocio.com/" }, "categoryId": "facturacion-compliance", - "imageUrl": "/images/projects/gestinio-mi-negocio.webp" + "imageUrl": "/images/projects/gestiono-mi-negocio.webp" }, { "id": "vita-liber-erp-interno", diff --git a/apps/portfolio/public/images/projects/gestinio-mi-negocio.webp b/apps/portfolio/public/images/projects/gestiono-mi-negocio.webp similarity index 100% rename from apps/portfolio/public/images/projects/gestinio-mi-negocio.webp rename to apps/portfolio/public/images/projects/gestiono-mi-negocio.webp From 976e8566d94c24be6309b202300d4f0c38d037c5 Mon Sep 17 00:00:00 2001 From: GMNAPI Date: Sun, 29 Mar 2026 14:25:51 +0200 Subject: [PATCH 3/4] fix(projects,tests): prettier format JSON files and update tests for new project data - Format messages/es.json and messages/en.json with Prettier - Replace hardcoded Next.js/GitHub tech assertions with data-driven checks using Symfony/TypeScript (present in current projects) - Make GitHub link tests data-driven: find first project with any external link (github or demo) to avoid false negatives --- apps/portfolio/messages/en.json | 191 +++--------------- apps/portfolio/messages/es.json | 191 +++--------------- .../tests/features/projects/Projects.test.tsx | 70 +++++-- 3 files changed, 113 insertions(+), 339 deletions(-) diff --git a/apps/portfolio/messages/en.json b/apps/portfolio/messages/en.json index 11581b5..5a6f5c2 100644 --- a/apps/portfolio/messages/en.json +++ b/apps/portfolio/messages/en.json @@ -112,9 +112,7 @@ "end": "2025" }, "headline": "Requirements gathering with clients/teams and translation into practical solutions.", - "achievements": [ - "Constant communication, task follow-up and phased delivery." - ] + "achievements": ["Constant communication, task follow-up and phased delivery."] }, { "role": "Program Specialist", @@ -134,9 +132,7 @@ "end": "2025" }, "headline": "Working with beta users: collecting feedback, improvements and product onboarding support.", - "achievements": [ - "Focus on clarity, quality and user experience." - ] + "achievements": ["Focus on clarity, quality and user experience."] }, { "role": "Developer", @@ -218,12 +214,7 @@ "usagePercentage": 31, "experienceYears": 6, "experienceProjects": 18, - "keywords": [ - "Symfony", - "Laravel", - "OOP", - "DDD" - ] + "keywords": ["Symfony", "Laravel", "OOP", "DDD"] }, { "name": "Symfony 7", @@ -231,11 +222,7 @@ "usagePercentage": 31, "experienceYears": 5, "experienceProjects": 12, - "keywords": [ - "API Platform", - "Messenger", - "Hexagonal" - ] + "keywords": ["API Platform", "Messenger", "Hexagonal"] }, { "name": "Node.js", @@ -243,11 +230,7 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 8, - "keywords": [ - "Express", - "Serverless", - "Workers" - ] + "keywords": ["Express", "Serverless", "Workers"] }, { "name": "API Platform", @@ -255,11 +238,7 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 6, - "keywords": [ - "OpenAPI", - "JWT", - "HATEOAS" - ] + "keywords": ["OpenAPI", "JWT", "HATEOAS"] } ] }, @@ -275,11 +254,7 @@ "usagePercentage": 29, "experienceYears": 5, "experienceProjects": 15, - "keywords": [ - "TypeScript", - "ESNext", - "monorepos" - ] + "keywords": ["TypeScript", "ESNext", "monorepos"] }, { "name": "React 18/19", @@ -287,12 +262,7 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 12, - "keywords": [ - "Hooks", - "Context", - "RSC", - "RHF" - ] + "keywords": ["Hooks", "Context", "RSC", "RHF"] }, { "name": "Next.js 15", @@ -300,12 +270,7 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": [ - "App Router", - "ISR", - "SEO", - "next-intl" - ] + "keywords": ["App Router", "ISR", "SEO", "next-intl"] }, { "name": "Tailwind CSS / shadcn/ui", @@ -313,11 +278,7 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": [ - "Design tokens", - "Dark mode", - "Framer Motion" - ] + "keywords": ["Design tokens", "Dark mode", "Framer Motion"] } ] }, @@ -333,11 +294,7 @@ "usagePercentage": 90, "experienceYears": 5, "experienceProjects": 15, - "keywords": [ - "Stakeholders", - "Requirements", - "Documentation" - ] + "keywords": ["Stakeholders", "Requirements", "Documentation"] }, { "name": "Incident Management", @@ -345,11 +302,7 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 12, - "keywords": [ - "Support", - "Resolution", - "Follow-up" - ] + "keywords": ["Support", "Resolution", "Follow-up"] }, { "name": "Team Coordination", @@ -357,11 +310,7 @@ "usagePercentage": 80, "experienceYears": 4, "experienceProjects": 10, - "keywords": [ - "Leadership", - "Methodologies", - "Deliveries" - ] + "keywords": ["Leadership", "Methodologies", "Deliveries"] }, { "name": "Client Orientation", @@ -369,11 +318,7 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 15, - "keywords": [ - "UX", - "Feedback", - "Continuous improvement" - ] + "keywords": ["UX", "Feedback", "Continuous improvement"] } ] }, @@ -389,11 +334,7 @@ "usagePercentage": 28, "experienceYears": 5, "experienceProjects": 14, - "keywords": [ - "Replication", - "Query tuning", - "Migrations" - ] + "keywords": ["Replication", "Query tuning", "Migrations"] }, { "name": "PostgreSQL", @@ -401,11 +342,7 @@ "usagePercentage": 17, "experienceYears": 4, "experienceProjects": 7, - "keywords": [ - "JSONB", - "PL/pgSQL", - "Materialized views" - ] + "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] }, { "name": "MongoDB", @@ -413,11 +350,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 4, - "keywords": [ - "Aggregation", - "Atlas", - "ODM" - ] + "keywords": ["Aggregation", "Atlas", "ODM"] }, { "name": "ElasticSearch", @@ -425,11 +358,7 @@ "usagePercentage": 6, "experienceYears": 3, "experienceProjects": 3, - "keywords": [ - "Full-text", - "Kibana", - "Ingest pipelines" - ] + "keywords": ["Full-text", "Kibana", "Ingest pipelines"] } ] }, @@ -445,11 +374,7 @@ "usagePercentage": 40, "experienceYears": 5, "experienceProjects": 20, - "keywords": [ - "Multi-stage", - "CI/CD", - "Devcontainers" - ] + "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] }, { "name": "GitHub Actions", @@ -457,11 +382,7 @@ "usagePercentage": 34, "experienceYears": 4, "experienceProjects": 10, - "keywords": [ - "Matrix builds", - "Secrets", - "Artifacts" - ] + "keywords": ["Matrix builds", "Secrets", "Artifacts"] }, { "name": "AWS / VPS", @@ -469,12 +390,7 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 6, - "keywords": [ - "EC2", - "Lightsail", - "S3", - "Terraform" - ] + "keywords": ["EC2", "Lightsail", "S3", "Terraform"] }, { "name": "Terraform", @@ -482,11 +398,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 3, - "keywords": [ - "IaC", - "Modules", - "State management" - ] + "keywords": ["IaC", "Modules", "State management"] } ] }, @@ -502,11 +414,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 5, - "keywords": [ - "GPT", - "Embeddings", - "Function calling" - ] + "keywords": ["GPT", "Embeddings", "Function calling"] }, { "name": "RAG Systems", @@ -514,11 +422,7 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": [ - "Vector stores", - "Hybrid search", - "Context windows" - ] + "keywords": ["Vector stores", "Hybrid search", "Context windows"] }, { "name": "n8n Workflows", @@ -526,11 +430,7 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 4, - "keywords": [ - "Automation", - "Webhook", - "Orchestration" - ] + "keywords": ["Automation", "Webhook", "Orchestration"] } ] }, @@ -546,11 +446,7 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 10, - "keywords": [ - "TDD", - "Mockery", - "Integration tests" - ] + "keywords": ["TDD", "Mockery", "Integration tests"] }, { "name": "Vitest", @@ -558,11 +454,7 @@ "usagePercentage": 9, "experienceYears": 2, "experienceProjects": 5, - "keywords": [ - "RTL", - "Snapshots", - "Monorepo" - ] + "keywords": ["RTL", "Snapshots", "Monorepo"] }, { "name": "Jest", @@ -570,11 +462,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 6, - "keywords": [ - "Mocks", - "Coverage", - "Watch mode" - ] + "keywords": ["Mocks", "Coverage", "Watch mode"] }, { "name": "Playwright", @@ -582,11 +470,7 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": [ - "Trace viewer", - "CI pipelines", - "Cross-browser" - ] + "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] } ] } @@ -723,12 +607,7 @@ "id": "fastbyte-api-servicios", "title": "Fastbyte - Service Management REST API", "description": "Consultant and Tech Lead focused on requirement gathering with clients and translating them into practical solutions. Robust REST API for service management with phased deliveries and constant client communication.", - "tech": [ - "Symfony 7.3", - "API Platform", - "MariaDB", - "Docker" - ], + "tech": ["Symfony 7.3", "API Platform", "MariaDB", "Docker"], "links": {}, "categoryId": "gestion-servicios" }, @@ -736,15 +615,7 @@ "id": "seedstockers-b2b-platform", "title": "Seedstockers - B2B Platform & Web Integration", "description": "Maintenance and development of a React-based B2B platform with complex Web Services integration for Prestashop. Technical coordination with logistics and payment teams to resolve operational incidents.", - "tech": [ - "React", - "TypeScript", - "Prestashop", - "Node.js", - "Express", - "Jest", - "Tailwind CSS" - ], + "tech": ["React", "TypeScript", "Prestashop", "Node.js", "Express", "Jest", "Tailwind CSS"], "links": { "demo": "https://seedstockers.com/" }, diff --git a/apps/portfolio/messages/es.json b/apps/portfolio/messages/es.json index 5bcc271..ca896fc 100644 --- a/apps/portfolio/messages/es.json +++ b/apps/portfolio/messages/es.json @@ -112,9 +112,7 @@ "end": "2025" }, "headline": "Toma de requisitos con clientes/equipos y traducción a soluciones prácticas.", - "achievements": [ - "Comunicación constante, seguimiento de tareas y entrega por fases." - ] + "achievements": ["Comunicación constante, seguimiento de tareas y entrega por fases."] }, { "role": "Especialista de Programa", @@ -134,9 +132,7 @@ "end": "2025" }, "headline": "Trabajo con usuarios beta: recogida de feedback, mejoras y acompañamiento en uso del producto.", - "achievements": [ - "Enfoque en claridad, calidad y experiencia de usuario." - ] + "achievements": ["Enfoque en claridad, calidad y experiencia de usuario."] }, { "role": "Developer", @@ -218,12 +214,7 @@ "usagePercentage": 31, "experienceYears": 6, "experienceProjects": 18, - "keywords": [ - "Symfony", - "Laravel", - "OOP", - "DDD" - ] + "keywords": ["Symfony", "Laravel", "OOP", "DDD"] }, { "name": "Symfony 7", @@ -231,11 +222,7 @@ "usagePercentage": 31, "experienceYears": 5, "experienceProjects": 12, - "keywords": [ - "API Platform", - "Messenger", - "Hexagonal" - ] + "keywords": ["API Platform", "Messenger", "Hexagonal"] }, { "name": "Node.js", @@ -243,11 +230,7 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 8, - "keywords": [ - "Express", - "Serverless", - "Workers" - ] + "keywords": ["Express", "Serverless", "Workers"] }, { "name": "API Platform", @@ -255,11 +238,7 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 6, - "keywords": [ - "OpenAPI", - "JWT", - "HATEOAS" - ] + "keywords": ["OpenAPI", "JWT", "HATEOAS"] } ] }, @@ -275,11 +254,7 @@ "usagePercentage": 29, "experienceYears": 5, "experienceProjects": 15, - "keywords": [ - "TypeScript", - "ESNext", - "monorepos" - ] + "keywords": ["TypeScript", "ESNext", "monorepos"] }, { "name": "React 18/19", @@ -287,12 +262,7 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 12, - "keywords": [ - "Hooks", - "Context", - "RSC", - "RHF" - ] + "keywords": ["Hooks", "Context", "RSC", "RHF"] }, { "name": "Next.js 15", @@ -300,12 +270,7 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": [ - "App Router", - "ISR", - "SEO", - "next-intl" - ] + "keywords": ["App Router", "ISR", "SEO", "next-intl"] }, { "name": "Tailwind CSS / shadcn/ui", @@ -313,11 +278,7 @@ "usagePercentage": 14, "experienceYears": 4, "experienceProjects": 9, - "keywords": [ - "Design tokens", - "Dark mode", - "Framer Motion" - ] + "keywords": ["Design tokens", "Dark mode", "Framer Motion"] } ] }, @@ -333,11 +294,7 @@ "usagePercentage": 90, "experienceYears": 5, "experienceProjects": 15, - "keywords": [ - "Stakeholders", - "Requisitos", - "Documentación" - ] + "keywords": ["Stakeholders", "Requisitos", "Documentación"] }, { "name": "Gestión de incidencias", @@ -345,11 +302,7 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 12, - "keywords": [ - "Soporte", - "Resolución", - "Seguimiento" - ] + "keywords": ["Soporte", "Resolución", "Seguimiento"] }, { "name": "Coordinación de equipos", @@ -357,11 +310,7 @@ "usagePercentage": 80, "experienceYears": 4, "experienceProjects": 10, - "keywords": [ - "Liderazgo", - "Metodologías", - "Entregas" - ] + "keywords": ["Liderazgo", "Metodologías", "Entregas"] }, { "name": "Orientación al cliente", @@ -369,11 +318,7 @@ "usagePercentage": 85, "experienceYears": 5, "experienceProjects": 15, - "keywords": [ - "UX", - "Feedback", - "Mejora continua" - ] + "keywords": ["UX", "Feedback", "Mejora continua"] } ] }, @@ -389,11 +334,7 @@ "usagePercentage": 28, "experienceYears": 5, "experienceProjects": 14, - "keywords": [ - "Replication", - "Query tuning", - "Migrations" - ] + "keywords": ["Replication", "Query tuning", "Migrations"] }, { "name": "PostgreSQL", @@ -401,11 +342,7 @@ "usagePercentage": 17, "experienceYears": 4, "experienceProjects": 7, - "keywords": [ - "JSONB", - "PL/pgSQL", - "Materialized views" - ] + "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] }, { "name": "MongoDB", @@ -413,11 +350,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 4, - "keywords": [ - "Aggregation", - "Atlas", - "ODM" - ] + "keywords": ["Aggregation", "Atlas", "ODM"] }, { "name": "ElasticSearch", @@ -425,11 +358,7 @@ "usagePercentage": 6, "experienceYears": 3, "experienceProjects": 3, - "keywords": [ - "Full-text", - "Kibana", - "Ingest pipelines" - ] + "keywords": ["Full-text", "Kibana", "Ingest pipelines"] } ] }, @@ -445,11 +374,7 @@ "usagePercentage": 40, "experienceYears": 5, "experienceProjects": 20, - "keywords": [ - "Multi-stage", - "CI/CD", - "Devcontainers" - ] + "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] }, { "name": "GitHub Actions", @@ -457,11 +382,7 @@ "usagePercentage": 34, "experienceYears": 4, "experienceProjects": 10, - "keywords": [ - "Matrix builds", - "Secrets", - "Artifacts" - ] + "keywords": ["Matrix builds", "Secrets", "Artifacts"] }, { "name": "AWS / VPS", @@ -469,12 +390,7 @@ "usagePercentage": 20, "experienceYears": 4, "experienceProjects": 6, - "keywords": [ - "EC2", - "Lightsail", - "S3", - "Terraform" - ] + "keywords": ["EC2", "Lightsail", "S3", "Terraform"] }, { "name": "Terraform", @@ -482,11 +398,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 3, - "keywords": [ - "IaC", - "Modules", - "State management" - ] + "keywords": ["IaC", "Modules", "State management"] } ] }, @@ -502,11 +414,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 5, - "keywords": [ - "GPT", - "Embeddings", - "Function calling" - ] + "keywords": ["GPT", "Embeddings", "Function calling"] }, { "name": "RAG Systems", @@ -514,11 +422,7 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": [ - "Vector stores", - "Hybrid search", - "Context windows" - ] + "keywords": ["Vector stores", "Hybrid search", "Context windows"] }, { "name": "n8n Workflows", @@ -526,11 +430,7 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 4, - "keywords": [ - "Automation", - "Webhook", - "Orchestration" - ] + "keywords": ["Automation", "Webhook", "Orchestration"] } ] }, @@ -546,11 +446,7 @@ "usagePercentage": 20, "experienceYears": 5, "experienceProjects": 10, - "keywords": [ - "TDD", - "Mockery", - "Integration tests" - ] + "keywords": ["TDD", "Mockery", "Integration tests"] }, { "name": "Vitest", @@ -558,11 +454,7 @@ "usagePercentage": 9, "experienceYears": 2, "experienceProjects": 5, - "keywords": [ - "RTL", - "Snapshots", - "Monorepo" - ] + "keywords": ["RTL", "Snapshots", "Monorepo"] }, { "name": "Jest", @@ -570,11 +462,7 @@ "usagePercentage": 9, "experienceYears": 3, "experienceProjects": 6, - "keywords": [ - "Mocks", - "Coverage", - "Watch mode" - ] + "keywords": ["Mocks", "Coverage", "Watch mode"] }, { "name": "Playwright", @@ -582,11 +470,7 @@ "usagePercentage": 6, "experienceYears": 2, "experienceProjects": 3, - "keywords": [ - "Trace viewer", - "CI pipelines", - "Cross-browser" - ] + "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] } ] } @@ -723,12 +607,7 @@ "id": "fastbyte-api-servicios", "title": "Fastbyte - API REST de Gestión de Servicios", "description": "Consultor y Tech Lead en toma de requisitos con clientes y su traducción a soluciones prácticas. API REST robusta para gestión de servicios con entregas por fases y comunicación constante con el cliente.", - "tech": [ - "Symfony 7.3", - "API Platform", - "MariaDB", - "Docker" - ], + "tech": ["Symfony 7.3", "API Platform", "MariaDB", "Docker"], "links": {}, "categoryId": "gestion-servicios" }, @@ -736,15 +615,7 @@ "id": "seedstockers-b2b-platform", "title": "Seedstockers - Plataforma B2B & Integración Web", "description": "Mantenimiento y desarrollo de plataforma B2B en React e integración de Web Services complejos con Prestashop. Coordinación técnica con áreas de logística y pagos para resolución de incidencias operativas.", - "tech": [ - "React", - "TypeScript", - "Prestashop", - "Node.js", - "Express", - "Jest", - "Tailwind CSS" - ], + "tech": ["React", "TypeScript", "Prestashop", "Node.js", "Express", "Jest", "Tailwind CSS"], "links": { "demo": "https://seedstockers.com/" }, diff --git a/apps/portfolio/tests/features/projects/Projects.test.tsx b/apps/portfolio/tests/features/projects/Projects.test.tsx index eeea605..b5e6243 100644 --- a/apps/portfolio/tests/features/projects/Projects.test.tsx +++ b/apps/portfolio/tests/features/projects/Projects.test.tsx @@ -67,21 +67,31 @@ describe('Projects Section', () => { it('should display tech stack in cards', () => { renderProjects(); - const nextjsTags = screen.getAllByText(/Next\.js/i); + // Use tech tags present in the current project data + const symfonyTags = screen.getAllByText(/Symfony/i); const typescriptTags = screen.getAllByText(/TypeScript/i); - expect(nextjsTags.length).toBeGreaterThan(0); + expect(symfonyTags.length).toBeGreaterThan(0); expect(typescriptTags.length).toBeGreaterThan(0); }); }); describe('Links (in modal)', () => { - it('should render GitHub links inside modal after clicking a card', () => { + it('should render external links inside modal after clicking a card', () => { renderProjects(); - // Open the first project modal - const articles = screen.getAllByRole('article'); - fireEvent.click(articles[0]); - const githubLinks = screen.getAllByRole('link', { name: /github/i }); - expect(githubLinks.length).toBeGreaterThan(0); + const projectsData = esMessages.projects.items as unknown as ProjectData[]; + // Find first project with any external link (github or demo) + const linkedIndex = projectsData.findIndex( + (p) => Boolean(p.links.github) || Boolean(p.links.demo) + ); + if (linkedIndex >= 0) { + const articles = screen.getAllByRole('article'); + fireEvent.click(articles[linkedIndex]); + const allLinks = screen.getAllByRole('link'); + const externalLinks = allLinks.filter((l) => + l.getAttribute('href')?.startsWith('http') + ); + expect(externalLinks.length).toBeGreaterThan(0); + } }); it('should render demo links in modal when available for that project', () => { @@ -96,22 +106,44 @@ describe('Projects Section', () => { } }); - it('should have proper href attributes on GitHub links in modal', () => { + it('should have proper href attributes on external links in modal', () => { renderProjects(); - const articles = screen.getAllByRole('article'); - fireEvent.click(articles[0]); - const link = screen.getAllByRole('link', { name: /github/i })[0]; - expect(link).toHaveAttribute('href'); - expect(link.getAttribute('href')).toContain('github.com'); + const projectsData = esMessages.projects.items as unknown as ProjectData[]; + const linkedIndex = projectsData.findIndex( + (p) => Boolean(p.links.github) || Boolean(p.links.demo) + ); + if (linkedIndex >= 0) { + const articles = screen.getAllByRole('article'); + fireEvent.click(articles[linkedIndex]); + const allLinks = screen.getAllByRole('link'); + const externalLinks = allLinks.filter((l) => + l.getAttribute('href')?.startsWith('http') + ); + expect(externalLinks.length).toBeGreaterThan(0); + externalLinks.forEach((link) => { + expect(link).toHaveAttribute('href'); + }); + } }); it('should open external links in new tab', () => { renderProjects(); - const articles = screen.getAllByRole('article'); - fireEvent.click(articles[0]); - const githubLink = screen.getAllByRole('link', { name: /github/i })[0]; - expect(githubLink).toHaveAttribute('target', '_blank'); - expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer'); + const projectsData = esMessages.projects.items as unknown as ProjectData[]; + const linkedIndex = projectsData.findIndex( + (p) => Boolean(p.links.github) || Boolean(p.links.demo) + ); + if (linkedIndex >= 0) { + const articles = screen.getAllByRole('article'); + fireEvent.click(articles[linkedIndex]); + const allLinks = screen.getAllByRole('link'); + const externalLinks = allLinks.filter((l) => + l.getAttribute('href')?.startsWith('http') + ); + if (externalLinks.length > 0) { + expect(externalLinks[0]).toHaveAttribute('target', '_blank'); + expect(externalLinks[0]).toHaveAttribute('rel', 'noopener noreferrer'); + } + } }); it('should render private repository notice in modal', () => { From 9907edb8cbb836bb8e321854453b21dc6e9cbfa6 Mon Sep 17 00:00:00 2001 From: GMNAPI Date: Sun, 29 Mar 2026 14:30:50 +0200 Subject: [PATCH 4/4] style(tests): prettier format Projects.test.tsx --- .../tests/features/projects/Projects.test.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/portfolio/tests/features/projects/Projects.test.tsx b/apps/portfolio/tests/features/projects/Projects.test.tsx index b5e6243..0a79b54 100644 --- a/apps/portfolio/tests/features/projects/Projects.test.tsx +++ b/apps/portfolio/tests/features/projects/Projects.test.tsx @@ -87,9 +87,7 @@ describe('Projects Section', () => { const articles = screen.getAllByRole('article'); fireEvent.click(articles[linkedIndex]); const allLinks = screen.getAllByRole('link'); - const externalLinks = allLinks.filter((l) => - l.getAttribute('href')?.startsWith('http') - ); + const externalLinks = allLinks.filter((l) => l.getAttribute('href')?.startsWith('http')); expect(externalLinks.length).toBeGreaterThan(0); } }); @@ -116,9 +114,7 @@ describe('Projects Section', () => { const articles = screen.getAllByRole('article'); fireEvent.click(articles[linkedIndex]); const allLinks = screen.getAllByRole('link'); - const externalLinks = allLinks.filter((l) => - l.getAttribute('href')?.startsWith('http') - ); + const externalLinks = allLinks.filter((l) => l.getAttribute('href')?.startsWith('http')); expect(externalLinks.length).toBeGreaterThan(0); externalLinks.forEach((link) => { expect(link).toHaveAttribute('href'); @@ -136,9 +132,7 @@ describe('Projects Section', () => { const articles = screen.getAllByRole('article'); fireEvent.click(articles[linkedIndex]); const allLinks = screen.getAllByRole('link'); - const externalLinks = allLinks.filter((l) => - l.getAttribute('href')?.startsWith('http') - ); + const externalLinks = allLinks.filter((l) => l.getAttribute('href')?.startsWith('http')); if (externalLinks.length > 0) { expect(externalLinks[0]).toHaveAttribute('target', '_blank'); expect(externalLinks[0]).toHaveAttribute('rel', 'noopener noreferrer');