diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4c7314f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,40 @@ +# Auto detect text files and perform LF normalization +* text=auto eol=lf + +# Force LF for all text files +*.js text eol=lf +*.jsx text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.md text eol=lf +*.mdx text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.html text eol=lf +*.svg text eol=lf +*.xml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.sh text eol=lf +*.env text eol=lf +*.env.* text eol=lf +.gitignore text eol=lf +.gitattributes text eol=lf +.prettierrc text eol=lf +.eslintrc text eol=lf +Dockerfile text eol=lf + +# Binary files (don't modify) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.webp binary +*.avif binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary +*.pdf binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c438a13..3b3e20a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,9 @@ jobs: runs-on: ubuntu-latest needs: build if: github.event_name == 'pull_request' + continue-on-error: true + permissions: + actions: write steps: - name: Checkout code @@ -190,7 +193,6 @@ jobs: with: urls: | http://localhost:3000 - uploadArtifacts: true temporaryPublicStorage: true - name: Stop production server @@ -235,6 +237,8 @@ jobs: runs-on: ubuntu-latest needs: [lint, typecheck, test, build] if: always() + permissions: + pull-requests: write steps: - name: Check job status diff --git a/.gitignore b/.gitignore index bdf5419..7707366 100644 --- a/.gitignore +++ b/.gitignore @@ -1,59 +1,59 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -coverage/ - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# AI -CLAUDE.md - -# Personal files -gptPrompt.md - -# Temporary files and helpers -/temp - -# Monorepo -.turbo - -# Docusaurus -.docusaurus -apps/lab/build -apps/lab/.docusaurus -apps/lab/node_modules - -# Next.js -apps/portfolio/.next -apps/portfolio/build -apps/portfolio/node_modules +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +coverage/ + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# AI +CLAUDE.md + +# Personal files +gptPrompt.md + +# Temporary files and helpers +/temp + +# Monorepo +.turbo + +# Docusaurus +.docusaurus +apps/lab/build +apps/lab/.docusaurus +apps/lab/node_modules + +# Next.js +apps/portfolio/.next +apps/portfolio/build +apps/portfolio/node_modules diff --git a/.nvmrc b/.nvmrc index 1d5e644..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24 +24 diff --git a/apps/lab/blog/2025-03-12-bienvenida-al-blog-lab.md b/apps/lab/blog/2025-03-12-bienvenida-al-blog-lab.md new file mode 100644 index 0000000..286420c --- /dev/null +++ b/apps/lab/blog/2025-03-12-bienvenida-al-blog-lab.md @@ -0,0 +1,27 @@ +--- +slug: bienvenida-al-blog-lab +title: Bienvenida al Blog de Lab +authors: [angel] +tags: [typescript, nextjs] +date: 2025-03-12 +--- + +# Bienvenida al Blog de Lab + +Bienvenido al blog técnico del Lab de DevPortfolio. Este espacio está pensado para compartir experimentos, aprendizajes y documentación de desarrollo. + +## ¿Qué encontrarás aquí? + +- **Tutoriales** sobre tecnologías del stack actual (Next.js, TypeScript, Docusaurus) +- **Reflexiones** sobre arquitectura, patrones y buenas prácticas +- **Cómo hacer** (how-tos) y guías prácticas +- **Experimentos** con nuevas herramientas o enfoques + +## El stack del proyecto + +El monorepo DevPortfolio combina: + +- **Portfolio**: Next.js 15, React 19, Tailwind, Framer Motion +- **Lab**: Docusaurus 2.4 para documentación y este blog + +Siéntete libre de explorar las [Guías](/docs/intro) para documentación más estructurada sobre arquitectura, i18n y testing. diff --git a/apps/lab/blog/tags.yml b/apps/lab/blog/tags.yml index bfaa778..f2ad8d7 100644 --- a/apps/lab/blog/tags.yml +++ b/apps/lab/blog/tags.yml @@ -1,19 +1,19 @@ -facebook: - label: Facebook - permalink: /facebook - description: Facebook tag description +typescript: + label: TypeScript + permalink: /typescript + description: Artículos sobre TypeScript y tipado estático -hello: - label: Hello - permalink: /hello - description: Hello tag description +nextjs: + label: Next.js + permalink: /nextjs + description: Contenido sobre Next.js y React Server Components -docusaurus: - label: Docusaurus - permalink: /docusaurus - description: Docusaurus tag description +devops: + label: DevOps + permalink: /devops + description: CI/CD, Docker, despliegues y automatización -hola: - label: Hola - permalink: /hola - description: Hola tag description +clean-architecture: + label: Clean Architecture + permalink: /clean-architecture + description: Patrones, capas y estructura de proyectos diff --git a/apps/lab/docs/intro.md b/apps/lab/docs/intro.md index 883b0d8..5fc2d69 100644 --- a/apps/lab/docs/intro.md +++ b/apps/lab/docs/intro.md @@ -16,9 +16,9 @@ Este es mi espacio de documentación técnica y experimentación, donde comparto Documentación detallada sobre los patrones y estrategias de arquitectura utilizados en mis proyectos: -- **[Clean Architecture](./guides/clean-architecture)**: Implementación de arquitectura limpia en 3 capas con Next.js -- **[Testing Strategy](./guides/testing-strategy)**: Estrategia de testing con Vitest y React Testing Library -- **[i18n Patterns](./guides/i18n-patterns)**: Patrones de internacionalización con next-intl +- **[Clean Architecture](/docs/guides/clean-architecture)**: Implementación de arquitectura limpia en 3 capas con Next.js +- **[Testing Strategy](/docs/guides/testing-strategy)**: Estrategia de testing con Vitest y React Testing Library +- **[i18n Patterns](/docs/guides/i18n-patterns)**: Patrones de internacionalización con next-intl ### ✍️ Blog Técnico diff --git a/apps/lab/docusaurus.config.js b/apps/lab/docusaurus.config.js index 5fb0dd0..a27d3d6 100644 --- a/apps/lab/docusaurus.config.js +++ b/apps/lab/docusaurus.config.js @@ -8,6 +8,7 @@ const config = { url: 'https://desenvolupadormaster.vercel.app', baseUrl: '/', + trailingSlash: false, organizationName: 'GMNAPI', projectName: 'devportfolio-lab', @@ -70,7 +71,7 @@ const config = { position: 'left', label: 'Guías', }, - // { to: '/blog', label: 'Blog', position: 'left' }, // Disabled: no blog posts yet + { to: '/blog', label: 'Blog', position: 'left' }, { type: 'localeDropdown', position: 'right', @@ -92,10 +93,10 @@ const config = { label: 'Guías', to: '/docs/intro', }, - // { - // label: 'Blog', - // to: '/blog', - // }, // Disabled: no blog posts yet + { + label: 'Blog', + to: '/blog', + }, ], }, { diff --git a/apps/lab/i18n/en/code.json b/apps/lab/i18n/en/code.json index e3f2492..a3e809f 100644 --- a/apps/lab/i18n/en/code.json +++ b/apps/lab/i18n/en/code.json @@ -1,4 +1,40 @@ { + "homepage.tagline": { + "message": "Technical experiments, skills, and development documentation", + "description": "Homepage hero tagline" + }, + "homepage.button.guides": { + "message": "View Guides", + "description": "Homepage primary CTA button" + }, + "homepage.button.blog": { + "message": "Go to Blog", + "description": "Homepage secondary CTA button" + }, + "homepage.feature.guides.title": { + "message": "Guides", + "description": "Homepage feature card title for Guides" + }, + "homepage.feature.guides.description": { + "message": "Documentation on Clean Architecture, testing, and i18n patterns for Next.js and TypeScript projects.", + "description": "Homepage feature card description for Guides" + }, + "homepage.feature.blog.title": { + "message": "Blog", + "description": "Homepage feature card title for Blog" + }, + "homepage.feature.blog.description": { + "message": "Technical articles, experiments, and case studies on development, SaaS, and DevOps.", + "description": "Homepage feature card description for Blog" + }, + "homepage.feature.portfolio.title": { + "message": "Portfolio", + "description": "Homepage feature card title for Portfolio" + }, + "homepage.feature.portfolio.description": { + "message": "Production projects, skills, and contact. The main CV linked to this Lab.", + "description": "Homepage feature card description for Portfolio" + }, "theme.ErrorPageContent.title": { "message": "This page crashed.", "description": "The title of the fallback page when the page crashed" diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-blog/2025-03-12-bienvenida-al-blog-lab.md b/apps/lab/i18n/en/docusaurus-plugin-content-blog/2025-03-12-bienvenida-al-blog-lab.md new file mode 100644 index 0000000..19c0194 --- /dev/null +++ b/apps/lab/i18n/en/docusaurus-plugin-content-blog/2025-03-12-bienvenida-al-blog-lab.md @@ -0,0 +1,27 @@ +--- +slug: bienvenida-al-blog-lab +title: Welcome to the Lab Blog +authors: [angel] +tags: [typescript, nextjs] +date: 2025-03-12 +--- + +# Welcome to the Lab Blog + +Welcome to the technical blog of the DevPortfolio Lab. This space is designed for sharing experiments, learnings, and development documentation. + +## What will you find here? + +- **Tutorials** on technologies in the current stack (Next.js, TypeScript, Docusaurus) +- **Reflections** on architecture, patterns, and best practices +- **How-tos** and practical guides +- **Experiments** with new tools or approaches + +## The project stack + +The DevPortfolio monorepo combines: + +- **Portfolio**: Next.js 15, React 19, Tailwind, Framer Motion +- **Lab**: Docusaurus 2.4 for documentation and this blog + +Feel free to explore the [Guides](/en/docs/intro) for more structured documentation on architecture, i18n, and testing. diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-blog/options.json b/apps/lab/i18n/en/docusaurus-plugin-content-blog/options.json index 969f5f4..bd4fd69 100644 --- a/apps/lab/i18n/en/docusaurus-plugin-content-blog/options.json +++ b/apps/lab/i18n/en/docusaurus-plugin-content-blog/options.json @@ -8,7 +8,7 @@ "description": "The description for the blog used in SEO" }, "sidebar.title": { - "message": "Artículos recientes", + "message": "Recent articles", "description": "The label for the left sidebar" } } diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-docs/current.json b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current.json index 51dbac3..769eb12 100644 --- a/apps/lab/i18n/en/docusaurus-plugin-content-docs/current.json +++ b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current.json @@ -3,16 +3,12 @@ "message": "Next", "description": "The label for version current" }, - "sidebar.guidesSidebar.category.Tutorial - Basics": { - "message": "Tutorial - Basics", - "description": "The label for category Tutorial - Basics in sidebar guidesSidebar" + "sidebar.guidesSidebar.category.Guías de Arquitectura": { + "message": "Architecture Guides", + "description": "The label for category Guías de Arquitectura in sidebar guidesSidebar" }, - "sidebar.guidesSidebar.category.Tutorial - Basics.link.generated-index.description": { - "message": "5 minutes to learn the most important Docusaurus concepts.", - "description": "The generated-index page description for category Tutorial - Basics in sidebar guidesSidebar" - }, - "sidebar.guidesSidebar.category.Tutorial - Extras": { - "message": "Tutorial - Extras", - "description": "The label for category Tutorial - Extras in sidebar guidesSidebar" + "sidebar.guidesSidebar.category.Guías de Arquitectura.link.generated-index.description": { + "message": "Detailed guides on Clean Architecture, Testing, and i18n in DevPortfolio.", + "description": "The generated-index page description for Architecture Guides" } } diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/_category_.json b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/_category_.json new file mode 100644 index 0000000..db9646c --- /dev/null +++ b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Architecture Guides", + "position": 2, + "link": { + "type": "generated-index", + "description": "Detailed guides on Clean Architecture, Testing, and i18n in DevPortfolio." + } +} diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/clean-architecture.md b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/clean-architecture.md new file mode 100644 index 0000000..f7a5ad8 --- /dev/null +++ b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/clean-architecture.md @@ -0,0 +1,550 @@ +--- +id: clean-architecture +title: Clean Architecture in DevPortfolio +sidebar_label: Clean Architecture +sidebar_position: 1 +description: Comprehensive guide on implementing Clean Architecture with 3 layers in Next.js +keywords: [clean architecture, next.js, typescript, dependency rule] +--- + +# Clean Architecture - DevPortfolio + +## Introduction + +DevPortfolio implements a simplified version of **Clean Architecture** with 3 main layers. This architecture ensures separation of concerns, testability, and maintainability. + +## Layer Structure + +``` +/src +├── /core # 🎯 Domain Layer +├── /features # 📦 Features Layer +└── /shared # 🔧 Shared Layer +``` + +### Dependency Rule + +**CRITICAL**: Dependencies flow inward only: + +``` +features → can import → core + shared +shared → can import → core +core → CANNOT import anything (pure TypeScript) +``` + +**CORRECT Example**: + +```typescript +// ✅ features/hero/HeroSection.tsx +import { validateEmail } from '@/core/use-cases/validateEmail'; +import { Button } from '@/shared/components/ui/Button'; + +// ✅ shared/hooks/useFormValidation.ts +import { validateEmail } from '@/core/use-cases/validateEmail'; + +// ✅ core/use-cases/validateEmail.ts +export function validateEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} +``` + +**INCORRECT Example**: + +```typescript +// ❌ core/entities/Project.ts +import { useState } from 'react'; // NEVER import React in core/ +import { cn } from '@/shared/utils/cn'; // NEVER import shared in core/ + +// ❌ shared/components/ui/Button.tsx +import { Project } from '@/features/projects/types'; // NEVER import features in shared/ +``` + +## Layer 1: Core (Domain) + +**Location**: `/src/core` + +**Responsibility**: Pure business logic, independent of frameworks. + +**Contents**: + +- **Entities** (`/entities`): Domain models with validation +- **Use Cases** (`/use-cases`): Pure business logic functions + +**Rules**: + +- ✅ Pure TypeScript only +- ✅ 100% testable without mocks +- ❌ NO React, Next.js, Framer Motion, etc. imports +- ❌ NO side effects (API calls, localStorage, etc.) + +### Example: Entity + +```typescript +// src/core/entities/Project.ts +export interface ProjectData { + id: string; + title: string; + description: string; + technologies: string[]; + githubUrl?: string; + liveUrl?: string; + category: ProjectCategory; +} + +export type ProjectCategory = 'fullstack' | 'frontend' | 'backend' | 'devops' | 'mobile' | 'data'; + +export class Project { + readonly id: string; + readonly title: string; + readonly description: string; + readonly technologies: string[]; + readonly githubUrl?: string; + readonly liveUrl?: string; + readonly category: ProjectCategory; + + constructor(data: ProjectData) { + this.validate(data); + Object.assign(this, data); + } + + private validate(data: ProjectData): void { + if (!data.id || data.id.trim().length === 0) { + throw new Error('Project ID is required'); + } + + if (!data.title || data.title.trim().length === 0) { + throw new Error('Project title is required'); + } + + if (data.technologies.length === 0) { + throw new Error('At least one technology is required'); + } + + if (data.githubUrl && !this.isValidUrl(data.githubUrl)) { + throw new Error('Invalid GitHub URL'); + } + + if (data.liveUrl && !this.isValidUrl(data.liveUrl)) { + throw new Error('Invalid live URL'); + } + } + + private isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } + } + + hasLiveDemo(): boolean { + return !!this.liveUrl; + } + + hasSourceCode(): boolean { + return !!this.githubUrl; + } +} +``` + +### Example: Use Case + +```typescript +// src/core/use-cases/filterProjectsByCategory.ts +import type { Project } from '@/core/entities/Project'; +import type { ProjectCategory } from '@/core/entities/Project'; + +export function filterProjectsByCategory( + projects: Project[], + category: ProjectCategory | 'all' +): Project[] { + if (category === 'all') { + return projects; + } + + return projects.filter((project) => project.category === category); +} + +// src/core/use-cases/validateContactForm.ts +export interface ContactFormData { + name: string; + email: string; + message: string; +} + +export interface ValidationResult { + isValid: boolean; + errors: Record; +} + +export function validateContactForm(data: ContactFormData): ValidationResult { + const errors: Record = {}; + + if (!data.name || data.name.trim().length === 0) { + errors.name = 'Name is required'; + } else if (data.name.trim().length < 2) { + errors.name = 'Name must be at least 2 characters'; + } + + if (!data.email || data.email.trim().length === 0) { + errors.email = 'Email is required'; + } else if (!isValidEmail(data.email)) { + errors.email = 'Invalid email format'; + } + + if (!data.message || data.message.trim().length === 0) { + errors.message = 'Message is required'; + } else if (data.message.trim().length < 10) { + errors.message = 'Message must be at least 10 characters'; + } + + return { + isValid: Object.keys(errors).length === 0, + errors, + }; +} + +function isValidEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} +``` + +## Layer 2: Features + +**Location**: `/src/features` + +**Responsibility**: Implementation of specific portfolio features. Each feature is a vertical slice with components, hooks, types, and constants. + +**Contents**: + +- Feature-specific React components +- Feature-specific custom hooks +- Feature-specific TypeScript types +- Feature-specific constants + +**Rules**: + +- ✅ Can import from `@/core` and `@/shared` +- ✅ Components with 'use client' if they use React hooks +- ✅ Translations with `useTranslations('feature-name')` +- ❌ NO importing from other features (avoid coupling) +- ❌ NO complex business logic (move it to core/) + +### Example: Feature Structure + +``` +/src/features/projects/ +├── ProjectsSection.tsx # Main component +├── ProjectCard.tsx # Card component +├── ProjectFilters.tsx # Filters component +├── types.ts # Feature-specific types +└── constants.ts # Feature-specific constants +``` + +### Example: Feature Component + +```typescript +// src/features/projects/ProjectsSection.tsx +'use client'; + +import { useState, useMemo } from 'react'; +import { useTranslations } from 'next-intl'; +import { motion } from 'framer-motion'; +import { ProjectCard } from './ProjectCard'; +import { ProjectFilters } from './ProjectFilters'; +import type { ProjectCategory } from '@/core/entities/Project'; +import { filterProjectsByCategory } from '@/core/use-cases/filterProjectsByCategory'; +import { PROJECTS } from '@/shared/constants/projects'; + +export function ProjectsSection() { + const t = useTranslations('projects'); + const [selectedCategory, setSelectedCategory] = + useState('all'); + + const filteredProjects = useMemo(() => { + return filterProjectsByCategory(PROJECTS, selectedCategory); + }, [selectedCategory]); + + return ( +
+

{t('title')}

+ + + +
+ {filteredProjects.map((project, index) => ( + + + + ))} +
+
+ ); +} +``` + +## Layer 3: Shared + +**Location**: `/src/shared` + +**Responsibility**: Code reusable across multiple features. + +**Contents**: + +- `/components/ui`: Atomic components (Button, Card, Modal, etc.) +- `/components/layout`: Layout components (Navbar, Footer) +- `/hooks`: Reusable custom hooks +- `/utils`: Utility functions +- `/constants`: Global project constants + +**Rules**: + +- ✅ Can import from `@/core` +- ✅ Generic and reusable UI components +- ✅ No dependencies on specific features +- ❌ NO imports from `/features` +- ❌ NO business logic (move it to core/) + +### Example: Shared UI Component + +```typescript +// src/shared/components/ui/Button.tsx +import { ButtonHTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/shared/utils/cn'; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'outline'; + size?: 'sm' | 'md' | 'lg'; +} + +export const Button = forwardRef( + ({ className, variant = 'primary', size = 'md', ...props }, ref) => { + return ( + + + + ); +} +``` + +**Namespace**: The argument to `useTranslations('hero')` must match the root key in the JSON files. + +### getTranslations (Server Components) + +For server components and pages: + +```typescript +import { getTranslations } from 'next-intl/server'; + +export default async function ProjectsPage() { + const t = await getTranslations('projects'); + + return ( +
+

{t('title')}

+

{t('filters.all')}

+
+ ); +} +``` + +### Dynamic Translations + +For translations with variables: + +```typescript +// messages/en.json +{ + "welcome": { + "greeting": "Hello {name}, you have {count} new messages" + } +} + +// Component +const t = useTranslations('welcome'); +

{t('greeting', { name: 'Ángel', count: 5 })}

+// Output: "Hello Ángel, you have 5 new messages" +``` + +### Plural Translations + +```typescript +// messages/en.json +{ + "projects": { + "count": "{count, plural, =0 {No projects} =1 {1 project} other {# projects}}" + } +} + +// Component +const t = useTranslations('projects'); +

{t('count', { count: 0 })}

// "No projects" +

{t('count', { count: 1 })}

// "1 project" +

{t('count', { count: 5 })}

// "5 projects" +``` + +## Common Patterns + +### Pattern 1: Feature Section with i18n + +```typescript +// src/features/projects/ProjectsSection.tsx +'use client'; + +import { useState } from 'react'; +import { useTranslations } from 'next-intl'; +import type { ProjectCategory } from '@/core/entities/Project'; + +export function ProjectsSection() { + const t = useTranslations('projects'); + const [selectedCategory, setSelectedCategory] = + useState('all'); + + return ( +
+

{t('title')}

+ +
+ + + +
+ + {/* Render projects... */} +
+ ); +} +``` + +**Corresponding JSON structure**: + +```json +{ + "projects": { + "title": "Projects", + "filters": { + "all": "All", + "fullstack": "Full Stack", + "frontend": "Frontend" + } + } +} +``` + +### Pattern 2: Validation with i18n Error Messages + +```typescript +// src/features/contact/ContactForm.tsx +'use client'; + +import { useTranslations } from 'next-intl'; +import { validateContactForm } from '@/core/use-cases/validateContactForm'; + +export function ContactForm() { + const t = useTranslations('contact'); + const [errors, setErrors] = useState>({}); + + const handleSubmit = (data: ContactFormData) => { + const validation = validateContactForm(data); + + if (!validation.isValid) { + // Map core validation errors to i18n messages + const translatedErrors: Record = {}; + + if (validation.errors.name) { + translatedErrors.name = t('validation.nameRequired'); + } + if (validation.errors.email === 'Email is required') { + translatedErrors.email = t('validation.emailRequired'); + } + if (validation.errors.email === 'Invalid email format') { + translatedErrors.email = t('validation.emailInvalid'); + } + + setErrors(translatedErrors); + return; + } + + // Submit form... + }; + + return ( +
+ + {errors.name &&

{errors.name}

} + + + {errors.email &&

{errors.email}

} + + +
+ ); +} +``` + +**JSON structure**: + +```json +{ + "contact": { + "form": { + "submit": "Send" + }, + "validation": { + "nameRequired": "Name is required", + "emailRequired": "Email is required", + "emailInvalid": "Invalid email" + } + } +} +``` + +### Pattern 3: Shared Component with Optional i18n + +For shared components that can be reused in multiple contexts: + +```typescript +// src/shared/components/ui/Modal.tsx +'use client'; + +import { useTranslations } from 'next-intl'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; // Allow custom title + children: React.ReactNode; +} + +export function Modal({ isOpen, onClose, title, children }: ModalProps) { + const t = useTranslations('ui'); + + return isOpen ? ( +
+
+

{title || t('modal.defaultTitle')}

+ {children} + +
+
+ ) : null; +} +``` + +**JSON structure**: + +```json +{ + "ui": { + "modal": { + "defaultTitle": "Information", + "close": "Close" + } + } +} +``` + +### Pattern 4: Dynamic Routes with i18n + +For dynamic pages like `/projects/[slug]`: + +```typescript +// app/[locale]/projects/[slug]/page.tsx +import { getTranslations } from 'next-intl/server'; +import { notFound } from 'next/navigation'; +import { PROJECTS } from '@/shared/constants/projects'; + +interface PageProps { + params: { locale: string; slug: string }; +} + +export default async function ProjectDetailPage({ params }: PageProps) { + const t = await getTranslations('projects'); + + const project = PROJECTS.find(p => p.id === params.slug); + + if (!project) { + notFound(); + } + + return ( +
+

{t(`items.${params.slug}.title`)}

+

{t(`items.${params.slug}.description`)}

+ +
+

{t('details.technologies')}

+
    + {project.technologies.map(tech => ( +
  • {tech}
  • + ))} +
+
+
+ ); +} +``` + +**JSON structure for projects**: + +```json +{ + "projects": { + "details": { + "technologies": "Technologies" + }, + "items": { + "devportfolio": { + "title": "DevPortfolio", + "description": "Personal portfolio with Next.js and React" + }, + "ecommerce-platform": { + "title": "E-commerce Platform", + "description": "Scalable e-commerce system" + } + } + } +} +``` + +## Locale Switching + +### Client-Side Locale Switching + +```typescript +// src/shared/components/layout/LocaleSwitcher.tsx +'use client'; + +import { useRouter, usePathname } from 'next/navigation'; +import { useLocale } from 'next-intl'; + +export function LocaleSwitcher() { + const router = useRouter(); + const pathname = usePathname(); + const currentLocale = useLocale(); + + const switchLocale = (newLocale: string) => { + // Remove current locale prefix from pathname + const pathWithoutLocale = pathname.replace(`/${currentLocale}`, ''); + + // Add new locale prefix (except for default locale 'es') + const newPath = newLocale === 'es' + ? pathWithoutLocale || '/' + : `/${newLocale}${pathWithoutLocale || ''}`; + + router.push(newPath); + }; + + return ( +
+ + +
+ ); +} +``` + +### Link Component with Locale + +```typescript +'use client'; + +import { Link as NextIntlLink } from '@/i18n/routing'; + +export function ProjectLink({ slug }: { slug: string }) { + return ( + + View project + + ); +} +``` + +## Testing with i18n + +### Mock next-intl in Tests + +```typescript +// tests/features/hero/HeroSection.test.tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { HeroSection } from '@/features/hero/HeroSection'; + +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => { + const translations: Record = { + 'greeting': 'Hello, I am', + 'name': 'Ángel Hidalgo', + 'tagline': 'Full Stack Developer', + 'cta.projects': 'View Projects', + 'cta.contact': 'Contact', + }; + return translations[key] || key; + }, +})); + +describe('HeroSection', () => { + it('renders translated greeting', () => { + render(); + expect(screen.getByText('Hello, I am')).toBeInTheDocument(); + }); + + it('renders translated CTA buttons', () => { + render(); + expect(screen.getByRole('button', { name: 'View Projects' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Contact' })).toBeInTheDocument(); + }); +}); +``` + +### Testing Multiple Locales + +```typescript +// tests/i18n/messages.test.ts +import { describe, it, expect } from 'vitest'; +import esMessages from '@/messages/es.json'; +import enMessages from '@/messages/en.json'; + +describe('i18n Messages', () => { + it('has same keys in both locale files', () => { + const esKeys = Object.keys(esMessages); + const enKeys = Object.keys(enMessages); + + expect(esKeys).toEqual(enKeys); + }); + + it('has all required sections', () => { + const requiredSections = ['hero', 'about', 'projects', 'contact']; + + requiredSections.forEach((section) => { + expect(esMessages).toHaveProperty(section); + expect(enMessages).toHaveProperty(section); + }); + }); + + it('has matching nested keys for contact form', () => { + const esContactKeys = Object.keys(esMessages.contact.form); + const enContactKeys = Object.keys(enMessages.contact.form); + + expect(esContactKeys).toEqual(enContactKeys); + }); +}); +``` + +## Best Practices + +### 1. Translation Organization + +**DO**: Organize by feature/section + +```json +{ + "hero": { ... }, + "about": { ... }, + "projects": { ... }, + "contact": { ... } +} +``` + +**DON'T**: Organize by content type + +```json +{ + "titles": { "hero": "...", "about": "..." }, + "buttons": { "submit": "...", "cancel": "..." } +} +``` + +### 2. Namespacing + +**DO**: Use specific namespace per feature + +```typescript +const t = useTranslations('projects'); +return

{t('title')}

; +``` + +**DON'T**: Use global namespace + +```typescript +const t = useTranslations(); +return

{t('projects.title')}

; // More verbose +``` + +### 3. Descriptive Keys + +**DO**: Semantic and hierarchical keys + +```json +{ + "contact": { + "form": { + "submit": "Send", + "sending": "Sending..." + }, + "validation": { + "emailRequired": "Email required" + } + } +} +``` + +**DON'T**: Generic or flat keys + +```json +{ + "button1": "Send", + "button2": "Sending...", + "error1": "Email required" +} +``` + +### 4. Keep in Sync + +**CRITICAL**: Always update both files (es.json and en.json) at the same time. + +**Recommended workflow**: + +1. Update `messages/es.json` (source of truth) +2. Update `messages/en.json` with equivalent translations +3. Verify that the keys match +4. Run i18n tests + +### 5. Avoid Hardcoded Strings + +**DO**: Use translations + +```typescript + +``` + +**DON'T**: Hardcode strings + +```typescript + // ❌ Not translatable +``` + +### 6. Consistent Formatting + +**DO**: Consistent format with punctuation + +```json +{ + "validation": { + "nameRequired": "Name is required", + "emailRequired": "Email is required" + } +} +``` + +**DON'T**: Inconsistent format + +```json +{ + "validation": { + "nameRequired": "Name is required.", + "emailRequired": "Email required" // Missing period + } +} +``` + +## Troubleshooting + +### Error: "useTranslations is not a function" + +**Cause**: Missing 'use client' directive in component. + +**Solution**: + +```typescript +'use client'; // ← Add this line + +import { useTranslations } from 'next-intl'; +``` + +### Error: Translation key not found + +**Cause**: Key does not exist in JSON files. + +**Solution**: Verify that the key exists in both `es.json` and `en.json`: + +```json +// messages/en.json +{ + "projects": { + "title": "Projects" // ← Verify it exists + } +} +``` + +### Translations not updating after changes + +**Cause**: Next.js cache. + +**Solution**: + +```bash +rm -rf .next +npm run dev +``` + +## i18n Checklist + +Before committing translation changes: + +- [ ] Both files (`es.json` and `en.json`) are updated +- [ ] Keys match in both files +- [ ] Translations are semantically equivalent +- [ ] No hardcoded strings in components +- [ ] Correct namespace used in `useTranslations()` +- [ ] Components have 'use client' if using `useTranslations` +- [ ] Tests mock `next-intl` correctly +- [ ] Punctuation format is consistent + +## References + +- next-intl Documentation: https://next-intl-docs.vercel.app/ +- Clean Architecture: See `.claude/architecture/clean-architecture.md` +- Testing Strategy: See `.claude/architecture/testing-strategy.md` diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/testing-strategy.md b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/testing-strategy.md new file mode 100644 index 0000000..37a8f5e --- /dev/null +++ b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/guides/testing-strategy.md @@ -0,0 +1,582 @@ +--- +id: testing-strategy +title: Testing Strategy in DevPortfolio +sidebar_label: Testing Strategy +sidebar_position: 2 +description: Testing guide with Vitest and React Testing Library to achieve 80%+ coverage +keywords: [testing, vitest, react testing library, coverage, aaa pattern] +--- + +# Testing Strategy - DevPortfolio + +## Testing Goal + +**Coverage target**: 80%+ (lines, functions, branches, statements) + +**Tools**: + +- **Vitest**: Test runner (Jest replacement, faster) +- **React Testing Library**: Component testing +- **@testing-library/user-event**: User interaction simulation +- **jsdom**: DOM environment for tests + +## Configuration + +### Configuration Files + +**vitest.config.ts**: + +```typescript +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: ['node_modules/', 'tests/', '*.config.*', '.next/', 'app/layout.tsx'], + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@/app': path.resolve(__dirname, './app'), + '@/tests': path.resolve(__dirname, './tests'), + }, + }, +}); +``` + +**tests/setup.ts**: + +```typescript +import '@testing-library/jest-dom'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +afterEach(() => { + cleanup(); +}); +``` + +### Testing Scripts + +```bash +npm test # Run tests in watch mode +npm run test:ui # Run tests with Vitest UI +npm run test:run # Run tests once (CI mode) +npm run test:coverage # Generate coverage report +``` + +## What to Test + +### General Rule + +**Test behavior, not implementation**. Tests should verify that the code does what it's supposed to do, not how it does it. + +### Testing Priorities + +1. **Core Layer (90-100% coverage)**: Critical business logic + - Entities: Validation, business methods + - Use Cases: Pure functions, edge cases + +2. **Features Layer (80-90% coverage)**: Components and functionality + - Components: Rendering, user interactions, states + - Custom hooks: State logic, effects + +3. **Shared Layer (80-90% coverage)**: Reusables + - UI Components: Props, variants, accessibility + - Hooks: Behavior, edge cases + - Utils: Utility functions, edge cases + +4. **App Layer (Optional, less than 50% coverage)**: Layout and pages + - Pages: Basic rendering + - Layouts: Structure, providers + +## Testing by Layer + +### 1. Testing Core Layer (Entities and Use Cases) + +**Goal**: 90-100% coverage, simple tests without mocks. + +#### Testing Entities + +```typescript +// tests/core/entities/Project.test.ts +import { describe, it, expect } from 'vitest'; +import { Project, type ProjectData } from '@/core/entities/Project'; + +describe('Project Entity', () => { + describe('Constructor & Validation', () => { + it('creates project with valid data', () => { + // Arrange + const validData: ProjectData = { + id: 'project-1', + title: 'DevPortfolio', + description: 'Personal portfolio', + technologies: ['Next.js', 'React', 'TypeScript'], + category: 'fullstack', + }; + + // Act + const project = new Project(validData); + + // Assert + expect(project.id).toBe('project-1'); + expect(project.title).toBe('DevPortfolio'); + expect(project.technologies).toHaveLength(3); + }); + + it('throws error when ID is missing', () => { + const invalidData = { + id: '', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend' as const, + }; + + expect(() => new Project(invalidData)).toThrow('Project ID is required'); + }); + + it('throws error when title is missing', () => { + const invalidData = { + id: 'test-1', + title: '', + description: 'Test', + technologies: ['React'], + category: 'frontend' as const, + }; + + expect(() => new Project(invalidData)).toThrow('Project title is required'); + }); + + it('throws error when technologies array is empty', () => { + const invalidData = { + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: [], + category: 'frontend' as const, + }; + + expect(() => new Project(invalidData)).toThrow('At least one technology is required'); + }); + + it('throws error for invalid GitHub URL', () => { + const invalidData = { + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend' as const, + githubUrl: 'not-a-url', + }; + + expect(() => new Project(invalidData)).toThrow('Invalid GitHub URL'); + }); + }); + + describe('Business Methods', () => { + it('hasLiveDemo returns true when liveUrl exists', () => { + const project = new Project({ + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend', + liveUrl: 'https://example.com', + }); + + expect(project.hasLiveDemo()).toBe(true); + }); + + it('hasLiveDemo returns false when liveUrl is missing', () => { + const project = new Project({ + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend', + }); + + expect(project.hasLiveDemo()).toBe(false); + }); + + it('hasSourceCode returns true when githubUrl exists', () => { + const project = new Project({ + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend', + githubUrl: 'https://github.com/user/repo', + }); + + expect(project.hasSourceCode()).toBe(true); + }); + }); +}); +``` + +#### Testing Use Cases + +```typescript +// tests/core/use-cases/filterProjectsByCategory.test.ts +import { describe, it, expect } from 'vitest'; +import { filterProjectsByCategory } from '@/core/use-cases/filterProjectsByCategory'; +import { Project } from '@/core/entities/Project'; + +describe('filterProjectsByCategory', () => { + const mockProjects = [ + new Project({ id: '1', title: 'Project 1', description: 'Test', technologies: ['React'], category: 'frontend' }), + new Project({ id: '2', title: 'Project 2', description: 'Test', technologies: ['Node.js'], category: 'backend' }), + new Project({ id: '3', title: 'Project 3', description: 'Test', technologies: ['Next.js'], category: 'fullstack' }), + ]; + + it('returns all projects when category is "all"', () => { + const result = filterProjectsByCategory(mockProjects, 'all'); + expect(result).toHaveLength(3); + }); + + it('filters projects by frontend category', () => { + const result = filterProjectsByCategory(mockProjects, 'frontend'); + expect(result).toHaveLength(1); + expect(result[0].category).toBe('frontend'); + }); + + it('returns empty array when no projects match category', () => { + const result = filterProjectsByCategory(mockProjects, 'mobile'); + expect(result).toHaveLength(0); + }); + + it('handles empty projects array', () => { + const result = filterProjectsByCategory([], 'frontend'); + expect(result).toHaveLength(0); + }); +}); +``` + +### 2. Testing Features Layer (Components) + +**Goal**: 80-90% coverage, testing with React Testing Library. + +#### AAA Pattern (Arrange-Act-Assert) + +All tests must follow the AAA pattern: + +1. **Arrange**: Prepare data and mocks +2. **Act**: Execute the action to test +3. **Assert**: Verify the result + +#### Testing Components + +```typescript +// tests/features/projects/ProjectCard.test.tsx +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ProjectCard } from '@/features/projects/ProjectCard'; +import { Project } from '@/core/entities/Project'; + +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => key, +})); + +describe('ProjectCard', () => { + it('renders project title and description', () => { + // Arrange + const mockProject = new Project({ + id: 'test-1', + title: 'Test Project', + description: 'This is a test project', + technologies: ['React', 'TypeScript'], + category: 'frontend', + }); + + // Act + render(); + + // Assert + expect(screen.getByText('Test Project')).toBeInTheDocument(); + expect(screen.getByText('This is a test project')).toBeInTheDocument(); + }); + + it('shows live demo link when liveUrl exists', () => { + const mockProject = new Project({ + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend', + liveUrl: 'https://example.com', + }); + + render(); + + const liveLink = screen.getByRole('link', { name: /live demo/i }); + expect(liveLink).toHaveAttribute('href', 'https://example.com'); + }); + + it('does not show live demo link when liveUrl is missing', () => { + const mockProject = new Project({ + id: 'test-1', + title: 'Test', + description: 'Test', + technologies: ['React'], + category: 'frontend', + }); + + render(); + + expect(screen.queryByRole('link', { name: /live demo/i })).not.toBeInTheDocument(); + }); +}); +``` + +#### Testing User Interactions + +```typescript +// tests/features/contact/ContactForm.test.tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ContactForm } from '@/features/contact/ContactForm'; + +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => key, +})); + +describe('ContactForm', () => { + it('submits form with valid data', async () => { + const user = userEvent.setup(); + const mockSubmit = vi.fn(); + render(); + + await user.type(screen.getByLabelText(/name/i), 'John Doe'); + await user.type(screen.getByLabelText(/email/i), 'john@example.com'); + await user.type(screen.getByLabelText(/message/i), 'Hello, this is a test message'); + await user.click(screen.getByRole('button', { name: /send/i })); + + await waitFor(() => { + expect(mockSubmit).toHaveBeenCalledWith({ + name: 'John Doe', + email: 'john@example.com', + message: 'Hello, this is a test message', + }); + }); + }); + + it('shows validation errors for empty fields', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /send/i })); + + expect(await screen.findByText(/name is required/i)).toBeInTheDocument(); + expect(await screen.findByText(/email is required/i)).toBeInTheDocument(); + expect(await screen.findByText(/message is required/i)).toBeInTheDocument(); + }); + + it('disables submit button while submitting', async () => { + const user = userEvent.setup(); + const mockSubmit = vi.fn(() => new Promise(resolve => setTimeout(resolve, 1000))); + render(); + + await user.type(screen.getByLabelText(/name/i), 'John Doe'); + await user.type(screen.getByLabelText(/email/i), 'john@example.com'); + await user.type(screen.getByLabelText(/message/i), 'Test message here'); + + const submitButton = screen.getByRole('button', { name: /send/i }); + await user.click(submitButton); + + expect(submitButton).toBeDisabled(); + }); +}); +``` + +### 3. Testing Shared Layer (UI Components and Hooks) + +#### Testing UI Components + +```typescript +// tests/shared/components/ui/Button.test.tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Button } from '@/shared/components/ui/Button'; + +describe('Button', () => { + it('renders button with text', () => { + render(); + expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); + }); + + it('applies primary variant styles by default', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('bg-accent'); + }); + + it('applies secondary variant styles', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('bg-background'); + }); + + it('applies outline variant styles', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('border-2', 'border-accent'); + }); + + it('handles click events', async () => { + const user = userEvent.setup(); + const handleClick = vi.fn(); + render(); + + await user.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('can be disabled', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('forwards ref correctly', () => { + const ref = vi.fn(); + render(); + expect(ref).toHaveBeenCalled(); + }); +}); +``` + +#### Testing Custom Hooks + +```typescript +// tests/shared/hooks/useScrollSpy.test.ts +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useScrollSpy } from '@/shared/hooks/useScrollSpy'; + +describe('useScrollSpy', () => { + beforeEach(() => { + // Setup DOM elements for test + const section1 = document.createElement('div'); + section1.id = 'section-1'; + const section2 = document.createElement('div'); + section2.id = 'section-2'; + document.body.appendChild(section1); + document.body.appendChild(section2); + }); + + afterEach(() => { + document.body.replaceChildren(); + }); + + it('returns empty string initially', () => { + const { result } = renderHook(() => useScrollSpy(['section-1', 'section-2'])); + expect(result.current).toBe(''); + }); + + it('updates active section when element intersects', () => { + const { result } = renderHook(() => useScrollSpy(['section-1', 'section-2'])); + + const mockCallback = vi.fn(); + global.IntersectionObserver = vi.fn().mockImplementation((callback) => { + mockCallback.mockImplementation(callback); + return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() }; + }); + + mockCallback([{ isIntersecting: true, target: { id: 'section-1' } }]); + + expect(result.current).toBe('section-1'); + }); +}); +``` + +## Mocking in Tests + +### Mock next-intl + +```typescript +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => key, +})); +``` + +### Mock framer-motion + +```typescript +vi.mock('framer-motion', () => ({ + motion: { + div: 'div', + button: 'button', + section: 'section', + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => children, +})); +``` + +### Mock next/navigation + +```typescript +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + prefetch: vi.fn(), + }), + usePathname: () => '/en/projects', + useSearchParams: () => new URLSearchParams(), +})); +``` + +## Useful Commands + +```bash +# Run specific test file +npm test -- ProjectCard.test.tsx + +# Run tests matching pattern +npm test -- --grep "validates email" + +# Run tests with UI (debugging) +npm run test:ui + +# Generate coverage report +npm run test:coverage + +# Watch mode (default) +npm test +``` + +## Testing Checklist + +Before considering a feature complete: + +- [ ] Core entities have 90%+ coverage +- [ ] Core use cases have 90%+ coverage +- [ ] Main components have 80%+ coverage +- [ ] User interactions are tested +- [ ] Edge cases are covered +- [ ] Validations are tested +- [ ] Tests follow the AAA pattern +- [ ] Mocks are minimal and necessary +- [ ] Tests are readable and maintainable +- [ ] `npm run test:coverage` passes with 80%+ + +## References + +- Testing Library: https://testing-library.com/docs/react-testing-library/intro/ +- Vitest: https://vitest.dev/ +- Clean Architecture Testing: See `.claude/architecture/clean-architecture.md` diff --git a/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/intro.md b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/intro.md new file mode 100644 index 0000000..53b4ffb --- /dev/null +++ b/apps/lab/i18n/en/docusaurus-plugin-content-docs/current/intro.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 1 +id: intro +title: Welcome to the Lab +description: Documentation and technical experimentation hub by Ángel Hidalgo +keywords: [lab, documentation, development, architecture, testing] +--- + +# Welcome to the Lab 🧪 + +This is my technical documentation and experimentation space, where I share guides, architecture patterns, and learnings about software development. + +## What will you find here? + +### 📚 Architecture Guides + +Detailed documentation on the patterns and architecture strategies used in my projects: + +- **[Clean Architecture](/docs/guides/clean-architecture)**: Implementation of clean architecture in 3 layers with Next.js +- **[Testing Strategy](/docs/guides/testing-strategy)**: Testing strategy with Vitest and React Testing Library +- **[i18n Patterns](/docs/guides/i18n-patterns)**: Internationalization patterns with next-intl + +### ✍️ Technical Blog + +Articles on development, Claude Code skills, and real-project case studies. + +## About the Portfolio + +This Lab is linked to my [main portfolio](https://desenvolupadormaster.vercel.app), where you can: + +- View my **[projects](https://desenvolupadormaster.vercel.app/projects)** in production +- Learn about my **[experience](https://desenvolupadormaster.vercel.app#about)** +- **[Contact me](https://desenvolupadormaster.vercel.app#contact)** for collaborations + +## Lab Technologies + +This site is built with: + +- **Docusaurus 2.4.3** - Documentation framework +- **React 17** - UI library +- **Markdown** - Content format +- **TypeScript** - Type safety (optional) + +## Navigation + +Use the left sidebar to explore the **Architecture Guides**. + +--- + +**Questions or suggestions?** Connect with me on [GitHub](https://github.com/GMNAPI) or [LinkedIn](https://linkedin.com/in/angel-hidalgo). diff --git a/apps/lab/i18n/en/docusaurus-theme-classic/footer.json b/apps/lab/i18n/en/docusaurus-theme-classic/footer.json index 62ad511..a1151a4 100644 --- a/apps/lab/i18n/en/docusaurus-theme-classic/footer.json +++ b/apps/lab/i18n/en/docusaurus-theme-classic/footer.json @@ -1,6 +1,6 @@ { "link.title.Contenido": { - "message": "Contenido", + "message": "Content", "description": "The title of the footer links column with title=Contenido in the footer" }, "link.title.Portfolio": { @@ -11,8 +11,12 @@ "message": "Social", "description": "The title of the footer links column with title=Social in the footer" }, + "link.item.label.Inicio": { + "message": "Home", + "description": "The label of footer link linking to portfolio home" + }, "link.item.label.Guías": { - "message": "Guías", + "message": "Guides", "description": "The label of footer link with label=Guías linking to /docs/intro" }, "link.item.label.Blog": { @@ -20,11 +24,11 @@ "description": "The label of footer link with label=Blog linking to /blog" }, "link.item.label.Proyectos": { - "message": "Proyectos", + "message": "Projects", "description": "The label of footer link with label=Proyectos linking to https://angelhidalgo.dev/projects" }, "link.item.label.Contacto": { - "message": "Contacto", + "message": "Contact", "description": "The label of footer link with label=Contacto linking to https://angelhidalgo.dev/#contact" }, "link.item.label.GitHub": { diff --git a/apps/lab/i18n/en/docusaurus-theme-classic/navbar.json b/apps/lab/i18n/en/docusaurus-theme-classic/navbar.json index 08b2615..03285c9 100644 --- a/apps/lab/i18n/en/docusaurus-theme-classic/navbar.json +++ b/apps/lab/i18n/en/docusaurus-theme-classic/navbar.json @@ -8,7 +8,7 @@ "description": "The alt text of navbar logo" }, "item.label.Guías": { - "message": "Guías", + "message": "Guides", "description": "Navbar item with label Guías" }, "item.label.Blog": { diff --git a/apps/lab/src/components/HomepageFeatures/index.tsx b/apps/lab/src/components/HomepageFeatures/index.tsx index 2c3dd93..d46335b 100644 --- a/apps/lab/src/components/HomepageFeatures/index.tsx +++ b/apps/lab/src/components/HomepageFeatures/index.tsx @@ -1,57 +1,66 @@ import React, { type ReactNode } from 'react'; import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import Translate from '@docusaurus/Translate'; import Heading from '@theme/Heading'; import styles from './styles.module.css'; type FeatureItem = { - title: string; + title: ReactNode; Svg: React.ComponentType>; description: ReactNode; + to?: string; + href?: string; }; const FeatureList: FeatureItem[] = [ { - title: 'Easy to Use', + title: Guías, + to: '/docs/intro', Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, description: ( - <> - Docusaurus was designed from the ground up to be easily installed and - used to get your website up and running quickly. - + + Documentación sobre Clean Architecture, testing y patrones de i18n para + proyectos Next.js y TypeScript. + ), }, { - title: 'Focus on What Matters', + title: Blog, + to: '/blog', Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, description: ( - <> - Docusaurus lets you focus on your docs, and we'll do the chores. Go - ahead and move your docs into the docs directory. - + + Artículos técnicos, experimentos y casos de estudio sobre desarrollo, + SaaS y DevOps. + ), }, { - title: 'Powered by React', + title: Portfolio, + href: 'https://desenvolupadormaster.vercel.app', Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, description: ( - <> - Extend or customize your website layout by reusing React. Docusaurus can - be extended while reusing the same header and footer. - + + Proyectos en producción, skills y contacto. El CV principal del Lab. + ), }, ]; -function Feature({title, Svg, description}: FeatureItem) { +function Feature({title, to, href, Svg, description}: FeatureItem) { + const linkProps = to ? {to} : {href}; return (
-
- -
-
- {title} -

{description}

-
+ +
+ +
+
+ {title} +

{description}

+
+
); } diff --git a/apps/lab/src/components/HomepageFeatures/styles.module.css b/apps/lab/src/components/HomepageFeatures/styles.module.css index b248eb2..677ffb0 100644 --- a/apps/lab/src/components/HomepageFeatures/styles.module.css +++ b/apps/lab/src/components/HomepageFeatures/styles.module.css @@ -5,6 +5,19 @@ width: 100%; } +.featureLink { + display: block; + color: inherit; + text-decoration: none; + transition: opacity 0.2s ease; +} + +.featureLink:hover { + color: inherit; + text-decoration: none; + opacity: 0.85; +} + .featureSvg { height: 200px; width: 200px; diff --git a/apps/lab/src/pages/index.module.css b/apps/lab/src/pages/index.module.css index 9f71a5d..4c6d72e 100644 --- a/apps/lab/src/pages/index.module.css +++ b/apps/lab/src/pages/index.module.css @@ -20,4 +20,9 @@ display: flex; align-items: center; justify-content: center; + gap: 1rem; +} + +.buttonsColumn { + flex-wrap: wrap; } diff --git a/apps/lab/src/pages/index.tsx b/apps/lab/src/pages/index.tsx index f6cde46..920e677 100644 --- a/apps/lab/src/pages/index.tsx +++ b/apps/lab/src/pages/index.tsx @@ -2,6 +2,7 @@ import React, { type ReactNode } from 'react'; import clsx from 'clsx'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Translate from '@docusaurus/Translate'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; import Heading from '@theme/Heading'; @@ -16,12 +17,19 @@ function HomepageHeader() { {siteConfig.title} -

{siteConfig.tagline}

-
+

+ {siteConfig.tagline} +

+
- Docusaurus Tutorial - 5min ⏱️ + Ver Guías + + + Ir al Blog
@@ -33,8 +41,8 @@ export default function Home(): ReactNode { const {siteConfig} = useDocusaurusContext(); return ( + title={siteConfig.title} + description={siteConfig.tagline}>
diff --git a/apps/portfolio/.nvmrc b/apps/portfolio/.nvmrc index 1d5e644..a45fd52 100644 --- a/apps/portfolio/.nvmrc +++ b/apps/portfolio/.nvmrc @@ -1 +1 @@ -24 +24 diff --git a/apps/portfolio/messages/en.json b/apps/portfolio/messages/en.json index a7f7f04..256c9f8 100644 --- a/apps/portfolio/messages/en.json +++ b/apps/portfolio/messages/en.json @@ -1,724 +1,736 @@ -{ - "metadata": { - "title": "{name} | {tagline}", - "description": "Tech Lead with experience collaborating with users and clients, accustomed to handling incidents and real needs with clear communication and a solution-oriented attitude. Specialized in translating business requirements into technical solutions.", - "ogImageAlt": "{name} - Professional Portfolio", - "keywords": [ - "Angel Hidalgo Barreiro", - "tech lead", - "Barcelona", - "technical management", - "communication", - "team coordination", - "SaaS", - "PHP", - "Symfony", - "Node.js", - "React", - "Next.js", - "TypeScript", - "technical leadership", - "portfolio", - "incident resolution", - "stakeholder management" - ] - }, - "navigation": { - "brand": "Dev Portfolio", - "ariaLabel": "Main navigation", - "items": { - "home": "Home", - "about": "About", - "projects": "Projects", - "contact": "Contact" - }, - "cta": "Let's talk →", - "theme": { - "toggle": "Toggle theme" - }, - "menu": { - "open": "Open menu", - "close": "Close menu" - }, - "locale": { - "label": "Language", - "switchTo": "Switch language to {locale}", - "current": "Current language: {locale}", - "options": { - "es": "Spanish", - "en": "English" - } - } - }, - "hero": { - "greeting": "👋 Hi, I'm", - "location": "📍 {city}, {country}", - "asciiAlt": "Professional portrait of {name} in ASCII terminal style", - "buttons": { - "projects": "View projects →", - "cv": "📄 Download CV", - "contact": "Contact" - } - }, - "about": { - "eyebrow": "Experience", - "title": "About me", - "intro": "Tech Lead with experience collaborating with users and clients, accustomed to handling incidents and real needs with clear communication and a solution-oriented attitude. Focused on translating business requirements into practical solutions with constant follow-up and iterative delivery.", - "sections": { - "timeline": "Professional timeline", - "activity": "Recent activity", - "certifications": "Certifications", - "techStack": "Tech stack" - }, - "stats": { - "contributions": { - "label": "Contributions last year", - "detail": "Commits, PRs and private activity on GitHub" - }, - "repositoriesAnalysed": { - "label": "Repositories analysed", - "detail": "Private portfolio across SaaS, tooling and documentation" - }, - "repositoriesActive": { - "label": "Active repositories", - "detail": "Projects under maintenance with an active roadmap" - }, - "repositoriesWithStars": { - "label": "Repositories with stars", - "detail": "Highlighted repos with community feedback" - }, - "accountCreated": { - "label": "Account created", - "detail": "Since {detailDate} contributing to open/private projects" - } - }, - "technologies": [ - "Next.js", - "React", - "TypeScript", - "Tailwind CSS", - "Node.js", - "Git", - "Vitest", - "Clean Architecture" - ], - "career": { - "experiences": [ - { - "role": "Co-founder & Lead Dev", - "company": "GestionoMiNegocio", - "period": { "start": "2020", "end": "Present" }, - "headline": "Working with beta users: collecting feedback, improvements and product onboarding support.", - "achievements": [ - "Focus on clarity, quality and user experience.", - "Onboarded 8+ beta users with personalized support.", - "Constant communication with stakeholders to prioritize features." - ] - }, - { - "role": "Program Specialist", - "company": "Vita Liber S.L.U.", - "period": { "start": "2020", "end": "Present" }, - "headline": "Managing internal user needs: prioritization, incidents, validation and functional support.", - "achievements": [ - "Personalized attention and incident resolution with effective communication.", - "Coordination with internal teams for task prioritization.", - "Delivery follow-up and functional validation with end users." - ] - }, - { - "role": "Consultant & Tech Lead", - "company": "Fastbyte SL", - "period": { "start": "2019", "end": "Present" }, - "headline": "Requirements gathering with clients/teams and translation into practical solutions.", - "achievements": [ - "Constant communication, task follow-up and phased delivery.", - "Technical coordination with client teams to align expectations.", - "Proactive resolution of blockers and technical incidents." - ] - }, - { - "role": "Developer", - "company": "240dots", - "period": { "start": "2020", "end": "2023" }, - "headline": "Delivered headless solutions combining WordPress and Symfony.", - "achievements": [ - "Built plugins connected to custom APIs.", - "Optimized performance and content pipelines." - ] - }, - { - "role": "Webmaster", - "company": "Seedstockers (BCG)", - "period": { "start": "2023", "end": "2024" }, - "headline": "Multilingual e-commerce management and coordination with payment/logistics areas.", - "achievements": [ - "Coordination to resolve operational incidents in the purchase flow (order, payment, shipping).", - "Continuous improvement of user experience in key processes.", - "Cross-functional collaboration with business and operations teams." - ] - }, - { - "role": "AI Projects", - "company": "Self-employed", - "period": { "start": "2023", "end": "Present" }, - "headline": "Research and delivery of AI solutions applied to business processes.", - "achievements": [ - "Designed RAG systems and workflows with n8n for intelligent automation.", - "Experimented with local models and integrations with OpenAI Platform." - ] - } - ], - "certifications": [ - { - "name": "Zend Certificate of E-Learning – PHP OOP & Software Patterns", - "issuer": "Zend", - "year": "2025", - "description": "Advanced program on object-oriented design patterns applied in PHP." - } - ] - } - }, - "skills": { - "eyebrow": "Core stack", - "title": "Technical skills", - "intro": "Overview based on the analysis of 35 private repositories and recent contributions. Highlights technologies with the highest adoption, experience years and current focus on intelligent automation.", - "languages": { - "title": "Most used languages" - }, - "visualization": { - "title": "Stack distribution", - "subtitle": "Relative share per language across analysed repositories", - "centerLabel": "Repos analysed", - "legendLabel": "{percentage}% of stack" - }, - "stats": { - "contributions": { - "label": "Annual contributions", - "value": "{count} private commits & PRs" - }, - "activeRepositories": { - "label": "Active repositories", - "value": "{count} repos under ongoing maintenance" - } - }, - "experienceLabel": "{years}+ years", - "usageLabel": "{percentage}% repo usage", - "projectsLabel": "{count} projects", - "categories": [ - { - "id": "backend", - "name": "Backend", - "summary": "ERP-to-SaaS migrations, REST APIs and service platforms with a focus on resilience and business domain expertise.", - "experienceYears": 6, - "skills": [ - { - "name": "PHP", - "description": "Primary language across 11 repositories focusing on clean architecture.", - "usagePercentage": 31, - "experienceYears": 6, - "experienceProjects": 18, - "keywords": ["Symfony", "Laravel", "OOP", "DDD"] - }, - { - "name": "Symfony 7", - "description": "Flagship framework for APIs and multi-tenant backoffice apps.", - "usagePercentage": 31, - "experienceYears": 5, - "experienceProjects": 12, - "keywords": ["API Platform", "Messenger", "Hexagonal"] - }, - { - "name": "Node.js", - "description": "Supplementary services, workers and automation with TypeScript.", - "usagePercentage": 20, - "experienceYears": 4, - "experienceProjects": 8, - "keywords": ["Express", "Serverless", "Workers"] - }, - { - "name": "API Platform", - "description": "Rich REST contracts for B2B ecosystems.", - "usagePercentage": 14, - "experienceYears": 4, - "experienceProjects": 6, - "keywords": ["OpenAPI", "JWT", "HATEOAS"] - } - ] - }, - { - "id": "frontend", - "name": "Frontend", - "summary": "Enterprise interfaces with Next.js 15/React 19, bespoke components and performance tuning.", - "experienceYears": 5, - "skills": [ - { - "name": "JavaScript / TypeScript", - "description": "Combined usage in a modern stack for strict typing and superior DX.", - "usagePercentage": 29, - "experienceYears": 5, - "experienceProjects": 15, - "keywords": ["TypeScript", "ESNext", "monorepos"] - }, - { - "name": "React 18/19", - "description": "SPA/SSR with advanced hooks, Suspense and RSC patterns.", - "usagePercentage": 20, - "experienceYears": 5, - "experienceProjects": 12, - "keywords": ["Hooks", "Context", "RSC", "RHF"] - }, - { - "name": "Next.js 15", - "description": "App Router, ISR and nested layouts for portfolios and SaaS products.", - "usagePercentage": 14, - "experienceYears": 4, - "experienceProjects": 9, - "keywords": ["App Router", "ISR", "SEO", "next-intl"] - }, - { - "name": "Tailwind CSS / shadcn/ui", - "description": "Minimalist design system with accessible components.", - "usagePercentage": 14, - "experienceYears": 4, - "experienceProjects": 9, - "keywords": ["Design tokens", "Dark mode", "Framer Motion"] - } - ] - }, - { - "id": "leadership", - "name": "Leadership & Communication", - "summary": "Team management, stakeholder communication and effective incident resolution.", - "experienceYears": 5, - "skills": [ - { - "name": "Technical Communication", - "description": "Translating business requirements into practical solutions.", - "usagePercentage": 90, - "experienceYears": 5, - "experienceProjects": 15, - "keywords": ["Stakeholders", "Requirements", "Documentation"] - }, - { - "name": "Incident Management", - "description": "Prioritization, validation and functional support to users.", - "usagePercentage": 85, - "experienceYears": 5, - "experienceProjects": 12, - "keywords": ["Support", "Resolution", "Follow-up"] - }, - { - "name": "Team Coordination", - "description": "Task follow-up and phased delivery.", - "usagePercentage": 80, - "experienceYears": 4, - "experienceProjects": 10, - "keywords": ["Leadership", "Methodologies", "Deliveries"] - }, - { - "name": "Client Orientation", - "description": "Personalized attention, empathy and continuous feedback.", - "usagePercentage": 85, - "experienceYears": 5, - "experienceProjects": 15, - "keywords": ["UX", "Feedback", "Continuous improvement"] - } - ] - }, - { - "id": "databases", - "name": "Databases", - "summary": "Modelling and optimization for high-availability systems with reporting and ETL.", - "experienceYears": 5, - "skills": [ - { - "name": "MariaDB / MySQL", - "description": "Primary engines powering ERP solutions and SaaS platforms.", - "usagePercentage": 28, - "experienceYears": 5, - "experienceProjects": 14, - "keywords": ["Replication", "Query tuning", "Migrations"] - }, - { - "name": "PostgreSQL", - "description": "Used in multi-service architectures and advanced analytics.", - "usagePercentage": 17, - "experienceYears": 4, - "experienceProjects": 7, - "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] - }, - { - "name": "MongoDB", - "description": "Supports microservices and flexible ingestion pipelines.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 4, - "keywords": ["Aggregation", "Atlas", "ODM"] - }, - { - "name": "ElasticSearch", - "description": "Faceted search and log observability.", - "usagePercentage": 6, - "experienceYears": 3, - "experienceProjects": 3, - "keywords": ["Full-text", "Kibana", "Ingest pipelines"] - } - ] - }, - { - "id": "devops", - "name": "DevOps & Cloud", - "summary": "Deployment automation, infrastructure as code and observability for SaaS.", - "experienceYears": 5, - "skills": [ - { - "name": "Docker & Compose", - "description": "Service packaging, CI pipelines and reproducible environments.", - "usagePercentage": 40, - "experienceYears": 5, - "experienceProjects": 20, - "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] - }, - { - "name": "GitHub Actions", - "description": "CI/CD workflows covering lint, tests, build and deployments.", - "usagePercentage": 34, - "experienceYears": 4, - "experienceProjects": 10, - "keywords": ["Matrix builds", "Secrets", "Artifacts"] - }, - { - "name": "AWS / VPS", - "description": "Hybrid infrastructure with focus on cost, security and monitoring.", - "usagePercentage": 20, - "experienceYears": 4, - "experienceProjects": 6, - "keywords": ["EC2", "Lightsail", "S3", "Terraform"] - }, - { - "name": "Terraform", - "description": "Declarative provisioning for multi-stage environments.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 3, - "keywords": ["IaC", "Modules", "State management"] - } - ] - }, - { - "id": "ai-automation", - "name": "AI & Automation", - "summary": "Model integrations and intelligent workflow orchestration for process automation.", - "experienceYears": 3, - "skills": [ - { - "name": "OpenAI Platform", - "description": "Conversational integrations and copilots for support.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 5, - "keywords": ["GPT", "Embeddings", "Function calling"] - }, - { - "name": "RAG Systems", - "description": "Designing Retrieval Augmented Generation pipelines for internal knowledge.", - "usagePercentage": 6, - "experienceYears": 2, - "experienceProjects": 3, - "keywords": ["Vector stores", "Hybrid search", "Context windows"] - }, - { - "name": "n8n Workflows", - "description": "Business process automation with low-code integrations.", - "usagePercentage": 6, - "experienceYears": 2, - "experienceProjects": 4, - "keywords": ["Automation", "Webhook", "Orchestration"] - } - ] - }, - { - "id": "testing", - "name": "Testing & QA", - "summary": "Comprehensive coverage with unit, integration and end-to-end suites for continuous quality.", - "experienceYears": 5, - "skills": [ - { - "name": "PHPUnit", - "description": "Testing complex domains in Symfony and backend services.", - "usagePercentage": 20, - "experienceYears": 5, - "experienceProjects": 10, - "keywords": ["TDD", "Mockery", "Integration tests"] - }, - { - "name": "Vitest", - "description": "Unit and component testing in frontend with TypeScript.", - "usagePercentage": 9, - "experienceYears": 2, - "experienceProjects": 5, - "keywords": ["RTL", "Snapshots", "Monorepo"] - }, - { - "name": "Jest", - "description": "Legacy and Node.js services with advanced mocks.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 6, - "keywords": ["Mocks", "Coverage", "Watch mode"] - }, - { - "name": "Playwright", - "description": "End-to-end for critical flows with CI and visual reporting.", - "usagePercentage": 6, - "experienceYears": 2, - "experienceProjects": 3, - "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] - } - ] - } - ], - "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 } - ], - "contributionsLastYear": 1706, - "activeRepositories": 9, - "repositoriesWithStars": 3, - "accountCreatedAt": "2024-04-22" - } - }, - "projects": { - "title": "Projects", - "subtitle": "Selection of SaaS products, internal tools and technical documentation created for clients and personal initiatives. Organized into 6 strategic categories.", - "filters": { - "all": "All ({count})" - }, - "aria": { - "tech": "Technologies used" - }, - "labels": { - "github": "GitHub", - "demo": "Demo →", - "detail": "View detail", - "categoryFallback": "Project" - }, - "privateNotice": { - "text": "Private repository — request access on {linkLabel}", - "linkLabel": "LinkedIn" - }, - "categories": [ - { - "id": "facturacion-compliance", - "name": "Billing & Compliance", - "description": "Fiscal solutions and technical documentation for VeriFactu and ERP environments." - }, - { - "id": "gestion-servicios", - "name": "Service Management", - "description": "SaaS applications to automate bookings, operations and back-office processes." - }, - { - "id": "arquitectura-avanzada", - "name": "Advanced Architecture", - "description": "Platforms focused on hexagonal architecture, DDD and scalability." - }, - { - "id": "erp-transformacion", - "name": "ERP & Digital Transformation", - "description": "Modernizing legacy systems into maintainable SaaS infrastructures." - }, - { - "id": "herramientas", - "name": "Tools & Automation", - "description": "Internal products for analytics, automation and intelligent support." - }, - { - "id": "educacion", - "name": "Education & Community", - "description": "Training material and technical challenges to share knowledge." - } - ], - "items": [ - { - "id": "verifactur-gmn", - "title": "VerifacturGMN", - "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": "FacturaeDocusaurus", - "description": "Technical documentation portal for VeriFactu and Facturae with guides, examples and automated deployment.", - "tech": ["Docusaurus", "TypeScript", "MDX", "GitHub Actions"], - "links": { - "github": "https://github.com/GMNAPI/FacturaeDocusaurus" - }, - "categoryId": "facturacion-compliance" - }, - { - "id": "apigns", - "title": "apiGns", - "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": "frontend-json", - "title": "FrontendJson", - "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" - }, - "categoryId": "gestion-servicios" - }, - { - "id": "fynkus", - "title": "FYNKUS", - "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": "GestiónoMiNegocio", - "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": "Vita Liber 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": "Genesis Enterprise", - "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": "Streamlit GMN", - "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": "Zend Laminas Tech Tests", - "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" - } - ] - }, - "contact": { - "title": "Contact", - "subtitle": "Have a project in mind? Let's talk", - "form": { - "name": { - "label": "Name", - "placeholder": "Your name" - }, - "email": { - "label": "Email", - "placeholder": "your@email.com" - }, - "message": { - "label": "Message", - "placeholder": "Tell me about your project..." - } - }, - "validation": { - "nameRequired": "Name is required", - "emailInvalid": "A valid email is required", - "messageRequired": "Message is required", - "messageMinLength": "Message must be at least 10 characters" - }, - "buttons": { - "submit": "Send message", - "submitting": "Sending..." - }, - "messages": { - "success": "Message sent successfully. I'll get back to you soon!", - "error": "Could not send message. Please try again." - } - }, - "footer": { - "tagline": "I build things", - "findMe": "Find me", - "socialLabel": "Follow me on {network}", - "copyright": "All rights reserved" - }, - "social": { - "github": { - "label": "GitHub", - "aria": "Visit my GitHub profile" - }, - "linkedin": { - "label": "LinkedIn", - "aria": "Visit my LinkedIn profile" - } - }, - "personal": { - "location": { - "city": "Barcelona", - "country": "Spain" - }, - "tagline": "Tech Lead | Technical Communication | SaaS | Node.js | React | PHP/Symfony", - "bio": { - "short": "Tech Lead with experience collaborating with users and clients, clear communication and a solution-oriented attitude.", - "full": "Tech Lead specialized in translating business requirements into technical solutions. Experience leading teams, coordinating with stakeholders, and managing incidents with effective communication. Focused on delivering value through clear communication, task follow-up and iterative delivery." - }, - "availability": { - "open": "Open to new opportunities" - }, - "focusAreas": [ - "Technical Leadership", - "Stakeholder Communication", - "Requirements Analysis", - "Team Coordination", - "Incident Resolution", - "Iterative Delivery", - "User Support & Training", - "Technical Strategy" - ] - }, - "career": {} -} +{ + "metadata": { + "title": "{name} | {tagline}", + "description": "Tech Lead with experience collaborating with users and clients, accustomed to handling incidents and real needs with clear communication and a solution-oriented attitude. Specialized in translating business requirements into technical solutions.", + "ogImageAlt": "{name} - Professional Portfolio", + "keywords": [ + "Angel Hidalgo Barreiro", + "tech lead", + "Barcelona", + "technical management", + "communication", + "team coordination", + "SaaS", + "PHP", + "Symfony", + "Node.js", + "React", + "Next.js", + "TypeScript", + "technical leadership", + "portfolio", + "incident resolution", + "stakeholder management" + ] + }, + "navigation": { + "brand": "Dev Portfolio", + "ariaLabel": "Main navigation", + "items": { + "home": "Home", + "about": "About", + "projects": "Projects", + "contact": "Contact" + }, + "cta": "Let's talk →", + "theme": { + "toggle": "Toggle theme" + }, + "menu": { + "open": "Open menu", + "close": "Close menu" + }, + "locale": { + "label": "Language", + "switchTo": "Switch language to {locale}", + "current": "Current language: {locale}", + "options": { + "es": "Spanish", + "en": "English" + } + } + }, + "hero": { + "greeting": "👋 Hi, I'm", + "location": "📍 {city}, {country}", + "asciiAlt": "Professional portrait of {name} in ASCII terminal style", + "buttons": { + "projects": "View projects →", + "cv": "📄 Download CV", + "contact": "Contact" + } + }, + "about": { + "eyebrow": "Experience", + "title": "About me", + "intro": "Tech Lead with experience collaborating with users and clients, accustomed to handling incidents and real needs with clear communication and a solution-oriented attitude. Focused on translating business requirements into practical solutions with constant follow-up and iterative delivery.", + "sections": { + "timeline": "Professional timeline", + "activity": "Recent activity", + "certifications": "Certifications", + "techStack": "Tech stack" + }, + "stats": { + "contributions": { + "label": "Contributions last year", + "detail": "Commits, PRs and private activity on GitHub" + }, + "repositoriesAnalysed": { + "label": "Repositories analysed", + "detail": "Private portfolio across SaaS, tooling and documentation" + }, + "repositoriesActive": { + "label": "Active repositories", + "detail": "Projects under maintenance with an active roadmap" + }, + "repositoriesWithStars": { + "label": "Repositories with stars", + "detail": "Highlighted repos with community feedback" + }, + "accountCreated": { + "label": "Account created", + "detail": "Since {detailDate} contributing to open/private projects" + } + }, + "technologies": [ + "Next.js", + "React", + "TypeScript", + "Tailwind CSS", + "Node.js", + "Git", + "Vitest", + "Clean Architecture" + ], + "career": { + "experiences": [ + { + "role": "Consultant & Tech Lead", + "company": "Fastbyte SL", + "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."] + }, + { + "role": "Program Specialist", + "company": "Vita Liber S.L.U.", + "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" }, + "headline": "Working with beta users: collecting feedback, improvements and product onboarding support.", + "achievements": ["Focus on clarity, quality and user experience."] + }, + { + "role": "Developer", + "company": "240dots", + "period": { "start": "2020", "end": "2023" }, + "headline": "Development of headless solutions combining WordPress and Symfony.", + "achievements": [ + "Building plugins connected to custom APIs.", + "Performance optimization and content pipelines." + ] + }, + { + "role": "Webmaster", + "company": "Seedstockers (BCG)", + "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).", + "Continuous improvement of user experience in key processes." + ] + } + ], + "certifications": [ + { + "name": "Zend Certificate of E-Learning – PHP OOP & Software Patterns", + "issuer": "Zend", + "year": "2025", + "description": "Advanced program on object-oriented design patterns applied in PHP." + } + ] + } + }, + "skills": { + "eyebrow": "Core stack", + "title": "Technical skills", + "intro": "Overview based on the analysis of 35 private repositories and recent contributions. Highlights technologies with the highest adoption, experience years and current focus on intelligent automation.", + "languages": { + "title": "Most used languages" + }, + "visualization": { + "title": "Stack distribution", + "subtitle": "Relative share per language across analysed repositories", + "centerLabel": "Repos analysed", + "legendLabel": "{percentage}% of stack" + }, + "stats": { + "contributions": { + "label": "Annual contributions", + "value": "{count} private commits & PRs" + }, + "activeRepositories": { + "label": "Active repositories", + "value": "{count} repos under ongoing maintenance" + } + }, + "experienceLabel": "{years}+ years", + "usageLabel": "{percentage}% repo usage", + "projectsLabel": "{count} projects", + "aria": { + "openCategory": "Open details for {name}", + "closeModal": "Close modal" + }, + "categories": [ + { + "id": "backend", + "name": "Backend", + "summary": "ERP-to-SaaS migrations, REST APIs and service platforms with a focus on resilience and business domain expertise.", + "experienceYears": 6, + "skills": [ + { + "name": "PHP", + "description": "Primary language across 11 repositories focusing on clean architecture.", + "usagePercentage": 31, + "experienceYears": 6, + "experienceProjects": 18, + "keywords": ["Symfony", "Laravel", "OOP", "DDD"] + }, + { + "name": "Symfony 7", + "description": "Flagship framework for APIs and multi-tenant backoffice apps.", + "usagePercentage": 31, + "experienceYears": 5, + "experienceProjects": 12, + "keywords": ["API Platform", "Messenger", "Hexagonal"] + }, + { + "name": "Node.js", + "description": "Supplementary services, workers and automation with TypeScript.", + "usagePercentage": 20, + "experienceYears": 4, + "experienceProjects": 8, + "keywords": ["Express", "Serverless", "Workers"] + }, + { + "name": "API Platform", + "description": "Rich REST contracts for B2B ecosystems.", + "usagePercentage": 14, + "experienceYears": 4, + "experienceProjects": 6, + "keywords": ["OpenAPI", "JWT", "HATEOAS"] + } + ] + }, + { + "id": "frontend", + "name": "Frontend", + "summary": "Enterprise interfaces with Next.js 15/React 19, bespoke components and performance tuning.", + "experienceYears": 5, + "skills": [ + { + "name": "JavaScript / TypeScript", + "description": "Combined usage in a modern stack for strict typing and superior DX.", + "usagePercentage": 29, + "experienceYears": 5, + "experienceProjects": 15, + "keywords": ["TypeScript", "ESNext", "monorepos"] + }, + { + "name": "React 18/19", + "description": "SPA/SSR with advanced hooks, Suspense and RSC patterns.", + "usagePercentage": 20, + "experienceYears": 5, + "experienceProjects": 12, + "keywords": ["Hooks", "Context", "RSC", "RHF"] + }, + { + "name": "Next.js 15", + "description": "App Router, ISR and nested layouts for portfolios and SaaS products.", + "usagePercentage": 14, + "experienceYears": 4, + "experienceProjects": 9, + "keywords": ["App Router", "ISR", "SEO", "next-intl"] + }, + { + "name": "Tailwind CSS / shadcn/ui", + "description": "Minimalist design system with accessible components.", + "usagePercentage": 14, + "experienceYears": 4, + "experienceProjects": 9, + "keywords": ["Design tokens", "Dark mode", "Framer Motion"] + } + ] + }, + { + "id": "leadership", + "name": "Leadership & Communication", + "summary": "Team management, stakeholder communication and effective incident resolution.", + "experienceYears": 5, + "skills": [ + { + "name": "Technical Communication", + "description": "Translating business requirements into practical solutions.", + "usagePercentage": 90, + "experienceYears": 5, + "experienceProjects": 15, + "keywords": ["Stakeholders", "Requirements", "Documentation"] + }, + { + "name": "Incident Management", + "description": "Prioritization, validation and functional support to users.", + "usagePercentage": 85, + "experienceYears": 5, + "experienceProjects": 12, + "keywords": ["Support", "Resolution", "Follow-up"] + }, + { + "name": "Team Coordination", + "description": "Task follow-up and phased delivery.", + "usagePercentage": 80, + "experienceYears": 4, + "experienceProjects": 10, + "keywords": ["Leadership", "Methodologies", "Deliveries"] + }, + { + "name": "Client Orientation", + "description": "Personalized attention, empathy and continuous feedback.", + "usagePercentage": 85, + "experienceYears": 5, + "experienceProjects": 15, + "keywords": ["UX", "Feedback", "Continuous improvement"] + } + ] + }, + { + "id": "databases", + "name": "Databases", + "summary": "Modelling and optimization for high-availability systems with reporting and ETL.", + "experienceYears": 5, + "skills": [ + { + "name": "MariaDB / MySQL", + "description": "Primary engines powering ERP solutions and SaaS platforms.", + "usagePercentage": 28, + "experienceYears": 5, + "experienceProjects": 14, + "keywords": ["Replication", "Query tuning", "Migrations"] + }, + { + "name": "PostgreSQL", + "description": "Used in multi-service architectures and advanced analytics.", + "usagePercentage": 17, + "experienceYears": 4, + "experienceProjects": 7, + "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] + }, + { + "name": "MongoDB", + "description": "Supports microservices and flexible ingestion pipelines.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 4, + "keywords": ["Aggregation", "Atlas", "ODM"] + }, + { + "name": "ElasticSearch", + "description": "Faceted search and log observability.", + "usagePercentage": 6, + "experienceYears": 3, + "experienceProjects": 3, + "keywords": ["Full-text", "Kibana", "Ingest pipelines"] + } + ] + }, + { + "id": "devops", + "name": "DevOps & Cloud", + "summary": "Deployment automation, infrastructure as code and observability for SaaS.", + "experienceYears": 5, + "skills": [ + { + "name": "Docker & Compose", + "description": "Service packaging, CI pipelines and reproducible environments.", + "usagePercentage": 40, + "experienceYears": 5, + "experienceProjects": 20, + "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] + }, + { + "name": "GitHub Actions", + "description": "CI/CD workflows covering lint, tests, build and deployments.", + "usagePercentage": 34, + "experienceYears": 4, + "experienceProjects": 10, + "keywords": ["Matrix builds", "Secrets", "Artifacts"] + }, + { + "name": "AWS / VPS", + "description": "Hybrid infrastructure with focus on cost, security and monitoring.", + "usagePercentage": 20, + "experienceYears": 4, + "experienceProjects": 6, + "keywords": ["EC2", "Lightsail", "S3", "Terraform"] + }, + { + "name": "Terraform", + "description": "Declarative provisioning for multi-stage environments.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 3, + "keywords": ["IaC", "Modules", "State management"] + } + ] + }, + { + "id": "ai-automation", + "name": "AI & Automation", + "summary": "Model integrations and intelligent workflow orchestration for process automation.", + "experienceYears": 3, + "skills": [ + { + "name": "OpenAI Platform", + "description": "Conversational integrations and copilots for support.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 5, + "keywords": ["GPT", "Embeddings", "Function calling"] + }, + { + "name": "RAG Systems", + "description": "Designing Retrieval Augmented Generation pipelines for internal knowledge.", + "usagePercentage": 6, + "experienceYears": 2, + "experienceProjects": 3, + "keywords": ["Vector stores", "Hybrid search", "Context windows"] + }, + { + "name": "n8n Workflows", + "description": "Business process automation with low-code integrations.", + "usagePercentage": 6, + "experienceYears": 2, + "experienceProjects": 4, + "keywords": ["Automation", "Webhook", "Orchestration"] + } + ] + }, + { + "id": "testing", + "name": "Testing & QA", + "summary": "Comprehensive coverage with unit, integration and end-to-end suites for continuous quality.", + "experienceYears": 5, + "skills": [ + { + "name": "PHPUnit", + "description": "Testing complex domains in Symfony and backend services.", + "usagePercentage": 20, + "experienceYears": 5, + "experienceProjects": 10, + "keywords": ["TDD", "Mockery", "Integration tests"] + }, + { + "name": "Vitest", + "description": "Unit and component testing in frontend with TypeScript.", + "usagePercentage": 9, + "experienceYears": 2, + "experienceProjects": 5, + "keywords": ["RTL", "Snapshots", "Monorepo"] + }, + { + "name": "Jest", + "description": "Legacy and Node.js services with advanced mocks.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 6, + "keywords": ["Mocks", "Coverage", "Watch mode"] + }, + { + "name": "Playwright", + "description": "End-to-end for critical flows with CI and visual reporting.", + "usagePercentage": 6, + "experienceYears": 2, + "experienceProjects": 3, + "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] + } + ] + } + ], + "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 } + ], + "contributionsLastYear": 1706, + "activeRepositories": 9, + "repositoriesWithStars": 3, + "accountCreatedAt": "2024-04-22" + } + }, + "projects": { + "title": "Projects", + "subtitle": "Selection of SaaS products, internal tools and technical documentation created in professional projects. Organized into 6 strategic categories.", + "filters": { + "all": "All ({count})" + }, + "aria": { + "tech": "Technologies used", + "openProject": "Open details for {title}", + "closeModal": "Close modal" + }, + "labels": { + "github": "GitHub", + "demo": "Demo →", + "detail": "View detail", + "categoryFallback": "Project", + "close": "Close", + "moretech": "+{count} more" + }, + "privateNotice": { + "text": "Private repository — request access on {linkLabel}", + "linkLabel": "LinkedIn" + }, + "categories": [ + { + "id": "facturacion-compliance", + "name": "Billing & Compliance", + "description": "Fiscal solutions and technical documentation for VeriFactu and ERP environments." + }, + { + "id": "gestion-servicios", + "name": "Service Management", + "description": "SaaS applications to automate bookings, operations and back-office processes." + }, + { + "id": "arquitectura-avanzada", + "name": "Advanced Architecture", + "description": "Platforms focused on hexagonal architecture, DDD and scalability." + }, + { + "id": "erp-transformacion", + "name": "ERP & Digital Transformation", + "description": "Modernizing legacy systems into maintainable SaaS infrastructures." + }, + { + "id": "herramientas", + "name": "Tools & Automation", + "description": "Internal products for analytics, automation and intelligent support." + }, + { + "id": "educacion", + "name": "Education & Community", + "description": "Training material and technical challenges to share knowledge." + }, + { + "id": "ia-chatbots", + "name": "AI & Chatbots", + "description": "Conversational assistants and RAG systems powered by generative AI." + } + ], + "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"], + "links": { + "github": "https://github.com/GMNAPI/FacturaeDocusaurus" + }, + "categoryId": "facturacion-compliance" + }, + { + "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": "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" + }, + "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.", + "tech": [ + "TypeScript", + "React 19", + "Vite 6", + "Tailwind v4", + "Fastify", + "RAG", + "OpenAI GPT-4", + "ChromaDB", + "Docker", + "Railway" + ], + "links": { + "github": "https://github.com/GMNAPI/llarjove", + "demo": "https://llarjove-production-754a.up.railway.app/" + }, + "categoryId": "ia-chatbots" + } + ] + }, + "contact": { + "title": "Contact", + "subtitle": "Have a project in mind? Let's talk", + "form": { + "name": { + "label": "Name", + "placeholder": "Your name" + }, + "email": { + "label": "Email", + "placeholder": "your@email.com" + }, + "message": { + "label": "Message", + "placeholder": "Tell me about your project..." + } + }, + "validation": { + "nameRequired": "Name is required", + "emailInvalid": "A valid email is required", + "messageRequired": "Message is required", + "messageMinLength": "Message must be at least 10 characters" + }, + "buttons": { + "submit": "Send message", + "submitting": "Sending..." + }, + "messages": { + "success": "Message sent successfully. I'll get back to you soon!", + "error": "Could not send message. Please try again." + } + }, + "footer": { + "tagline": "I build things", + "findMe": "Find me", + "socialLabel": "Follow me on {network}", + "copyright": "All rights reserved" + }, + "social": { + "github": { + "label": "GitHub", + "aria": "Visit my GitHub profile" + }, + "linkedin": { + "label": "LinkedIn", + "aria": "Visit my LinkedIn profile" + } + }, + "personal": { + "location": { + "city": "Barcelona", + "country": "Spain" + }, + "tagline": "Tech Lead | Technical Communication | SaaS | Node.js | React | PHP/Symfony", + "bio": { + "short": "Tech Lead with experience collaborating with users and clients, clear communication and a solution-oriented attitude.", + "full": "Tech Lead specialized in translating business requirements into technical solutions. Experience leading teams, coordinating with stakeholders, and managing incidents with effective communication. Focused on delivering value through clear communication, task follow-up and iterative delivery." + }, + "availability": { + "open": "Open to new opportunities" + }, + "focusAreas": [ + "Technical Leadership", + "Stakeholder Communication", + "Requirements Analysis", + "Team Coordination", + "Incident Resolution", + "Iterative Delivery", + "User Support & Training", + "Technical Strategy" + ] + }, + "career": {} +} diff --git a/apps/portfolio/messages/es.json b/apps/portfolio/messages/es.json index 13438f0..73d20dd 100644 --- a/apps/portfolio/messages/es.json +++ b/apps/portfolio/messages/es.json @@ -1,724 +1,736 @@ -{ - "metadata": { - "title": "{name} | {tagline}", - "description": "Tech Lead con experiencia colaborando con usuarios y clientes, acostumbrado a tratar incidencias y necesidades reales con comunicación clara y actitud resolutiva. Especializado en traducir requisitos de negocio a soluciones técnicas.", - "ogImageAlt": "{name} - Portfolio Profesional", - "keywords": [ - "Ángel Hidalgo Barreiro", - "tech lead", - "Barcelona", - "gestión técnica", - "comunicación", - "coordinación equipos", - "SaaS", - "PHP", - "Symfony", - "Node.js", - "React", - "Next.js", - "TypeScript", - "liderazgo técnico", - "portfolio", - "resolución incidencias", - "gestión stakeholders" - ] - }, - "navigation": { - "brand": "Dev Portfolio", - "ariaLabel": "Navegación principal", - "items": { - "home": "Inicio", - "about": "Sobre mí", - "projects": "Proyectos", - "contact": "Contacto" - }, - "cta": "Hablemos →", - "theme": { - "toggle": "Cambiar tema" - }, - "menu": { - "open": "Abrir menú", - "close": "Cerrar menú" - }, - "locale": { - "label": "Idioma", - "switchTo": "Cambiar idioma a {locale}", - "current": "Idioma actual: {locale}", - "options": { - "es": "Español", - "en": "Inglés" - } - } - }, - "hero": { - "greeting": "👋 Hola, soy", - "location": "📍 {city}, {country}", - "asciiAlt": "Retrato profesional de {name} en arte ASCII con estilo terminal", - "buttons": { - "projects": "Ver proyectos →", - "cv": "📄 Descargar CV", - "contact": "Contactar" - } - }, - "about": { - "eyebrow": "Experiencia", - "title": "Sobre mí", - "intro": "Tech Lead con experiencia colaborando con usuarios y clientes, acostumbrado a tratar incidencias y necesidades reales con comunicación clara y actitud resolutiva. Enfocado en traducir requisitos de negocio a soluciones prácticas, con seguimiento constante y entregas iterativas.", - "sections": { - "timeline": "Timeline profesional", - "activity": "Actividad reciente", - "certifications": "Certificaciones", - "techStack": "Stack tecnológico" - }, - "stats": { - "contributions": { - "label": "Contribuciones último año", - "detail": "Commits, PRs y actividad privada en GitHub" - }, - "repositoriesAnalysed": { - "label": "Repositorios analizados", - "detail": "Portafolio privado entre SaaS, herramientas y documentación" - }, - "repositoriesActive": { - "label": "Repositorios activos", - "detail": "Proyectos con mantenimiento y roadmap en curso" - }, - "repositoriesWithStars": { - "label": "Repositorios con stars", - "detail": "Repos destacados con feedback de la comunidad" - }, - "accountCreated": { - "label": "Cuenta creada", - "detail": "Desde {detailDate} contribuyendo en proyectos open/private" - } - }, - "technologies": [ - "Next.js", - "React", - "TypeScript", - "Tailwind CSS", - "Node.js", - "Git", - "Vitest", - "Clean Architecture" - ], - "career": { - "experiences": [ - { - "role": "Cofundador & Lead Dev", - "company": "GestionoMiNegocio", - "period": { "start": "2020", "end": "Actualidad" }, - "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.", - "Activación de más de 8 usuarios beta con acompañamiento personalizado.", - "Comunicación constante con stakeholders para priorizar funcionalidades." - ] - }, - { - "role": "Especialista de programa", - "company": "Vita Liber S.L.U.", - "period": { "start": "2020", "end": "Actualidad" }, - "headline": "Gestión de necesidades de usuarios internos: priorización, incidencias, validación y soporte funcional.", - "achievements": [ - "Atención personalizada y resolución de incidencias con comunicación efectiva.", - "Coordinación con equipos internos para priorización de tareas.", - "Seguimiento de entregas y validación funcional con usuarios finales." - ] - }, - { - "role": "Consultor & Tech Lead", - "company": "Fastbyte SL", - "period": { "start": "2019", "end": "Actualidad" }, - "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.", - "Coordinación técnica con equipos cliente para alinear expectativas.", - "Resolución proactiva de bloqueos e incidencias técnicas." - ] - }, - { - "role": "Developer", - "company": "240dots", - "period": { "start": "2020", "end": "2023" }, - "headline": "Desarrollo de soluciones headless combinando WordPress y Symfony.", - "achievements": [ - "Construcción de plugins conectados con APIs personalizadas.", - "Optimización de rendimiento y pipelines de contenido." - ] - }, - { - "role": "Webmaster", - "company": "Seedstockers (BCG)", - "period": { "start": "2023", "end": "2024" }, - "headline": "Gestión de e-commerce multilingüe y coordinación con áreas de pagos/logística.", - "achievements": [ - "Coordinación para resolver incidencias operativas en el flujo de compra (pedido, pago, envío).", - "Mejora continua de experiencia de usuario en procesos clave.", - "Colaboración transversal con equipos de negocio y operaciones." - ] - }, - { - "role": "AI Projects", - "company": "Autónomo", - "period": { "start": "2023", "end": "Actualidad" }, - "headline": "Investigación y entrega de soluciones IA aplicadas a procesos de negocio.", - "achievements": [ - "Diseño de sistemas RAG y workflows con n8n para automatización inteligente.", - "Experimentación con modelos locales e integración con OpenAI Platform." - ] - } - ], - "certifications": [ - { - "name": "Zend Certificate of E-Learning – PHP OOP & Software Patterns", - "issuer": "Zend", - "year": "2025", - "description": "Programa avanzado sobre patrones de diseño orientados a objetos aplicados en PHP." - } - ] - } - }, - "skills": { - "eyebrow": "Stack principal", - "title": "Habilidades técnicas", - "intro": "Panorama basado en el análisis de 35 repositorios privados y contribuciones recientes. Destaca tecnologías con mayor adopción, años de experiencia y foco actual en automatización inteligente.", - "languages": { - "title": "Lenguajes más utilizados" - }, - "visualization": { - "title": "Distribución del stack", - "subtitle": "Participación relativa por lenguaje en los repositorios analizados", - "centerLabel": "Repos analizados", - "legendLabel": "{percentage}% del stack" - }, - "stats": { - "contributions": { - "label": "Contribuciones anuales", - "value": "{count} commits y PRs privados" - }, - "activeRepositories": { - "label": "Repositorios activos", - "value": "{count} repos en mantenimiento continuo" - } - }, - "experienceLabel": "{years}+ años", - "usageLabel": "{percentage}% uso repos", - "projectsLabel": "{count} proyectos", - "categories": [ - { - "id": "backend", - "name": "Backend", - "summary": "Migraciones ERP a SaaS, APIs REST y plataformas de servicios con foco en resiliencia y dominio del negocio.", - "experienceYears": 6, - "skills": [ - { - "name": "PHP", - "description": "Lenguaje principal en 11 repositorios con foco en arquitectura limpia.", - "usagePercentage": 31, - "experienceYears": 6, - "experienceProjects": 18, - "keywords": ["Symfony", "Laravel", "OOP", "DDD"] - }, - { - "name": "Symfony 7", - "description": "Framework estrella para APIs y backoffice multi-tenant.", - "usagePercentage": 31, - "experienceYears": 5, - "experienceProjects": 12, - "keywords": ["API Platform", "Messenger", "Hexagonal"] - }, - { - "name": "Node.js", - "description": "Servicios suplementarios, workers y automatización con TypeScript.", - "usagePercentage": 20, - "experienceYears": 4, - "experienceProjects": 8, - "keywords": ["Express", "Serverless", "Workers"] - }, - { - "name": "API Platform", - "description": "Contratos REST enriquecidos para ecosistemas B2B.", - "usagePercentage": 14, - "experienceYears": 4, - "experienceProjects": 6, - "keywords": ["OpenAPI", "JWT", "HATEOAS"] - } - ] - }, - { - "id": "frontend", - "name": "Frontend", - "summary": "Interfaces empresariales con Next.js 15/React 19, componentes a medida y rendimiento optimizado.", - "experienceYears": 5, - "skills": [ - { - "name": "JavaScript / TypeScript", - "description": "Uso combinado en stack moderno para tipado estricto y DX superior.", - "usagePercentage": 29, - "experienceYears": 5, - "experienceProjects": 15, - "keywords": ["TypeScript", "ESNext", "monorepos"] - }, - { - "name": "React 18/19", - "description": "SPA/SSR con hooks avanzados, Suspense y patrones RSC.", - "usagePercentage": 20, - "experienceYears": 5, - "experienceProjects": 12, - "keywords": ["Hooks", "Context", "RSC", "RHF"] - }, - { - "name": "Next.js 15", - "description": "App Router, ISR y layouts anidados para portfolio y productos SaaS.", - "usagePercentage": 14, - "experienceYears": 4, - "experienceProjects": 9, - "keywords": ["App Router", "ISR", "SEO", "next-intl"] - }, - { - "name": "Tailwind CSS / shadcn/ui", - "description": "Design system minimalista con componentes accesibles.", - "usagePercentage": 14, - "experienceYears": 4, - "experienceProjects": 9, - "keywords": ["Design tokens", "Dark mode", "Framer Motion"] - } - ] - }, - { - "id": "leadership", - "name": "Liderazgo & Comunicación", - "summary": "Gestión de equipos, comunicación con stakeholders y resolución efectiva de incidencias.", - "experienceYears": 5, - "skills": [ - { - "name": "Comunicación técnica", - "description": "Traducción de requisitos de negocio a soluciones prácticas.", - "usagePercentage": 90, - "experienceYears": 5, - "experienceProjects": 15, - "keywords": ["Stakeholders", "Requisitos", "Documentación"] - }, - { - "name": "Gestión de incidencias", - "description": "Priorización, validación y soporte funcional a usuarios.", - "usagePercentage": 85, - "experienceYears": 5, - "experienceProjects": 12, - "keywords": ["Soporte", "Resolución", "Seguimiento"] - }, - { - "name": "Coordinación de equipos", - "description": "Seguimiento de tareas y entrega por fases.", - "usagePercentage": 80, - "experienceYears": 4, - "experienceProjects": 10, - "keywords": ["Liderazgo", "Metodologías", "Entregas"] - }, - { - "name": "Orientación al cliente", - "description": "Atención personalizada, empatía y feedback continuo.", - "usagePercentage": 85, - "experienceYears": 5, - "experienceProjects": 15, - "keywords": ["UX", "Feedback", "Mejora continua"] - } - ] - }, - { - "id": "databases", - "name": "Bases de Datos", - "summary": "Modelado y optimización para sistemas de alta disponibilidad con reporting y ETL.", - "experienceYears": 5, - "skills": [ - { - "name": "MariaDB / MySQL", - "description": "Motores principales en soluciones ERP y plataformas SaaS.", - "usagePercentage": 28, - "experienceYears": 5, - "experienceProjects": 14, - "keywords": ["Replication", "Query tuning", "Migrations"] - }, - { - "name": "PostgreSQL", - "description": "Implementado en arquitecturas multi-servicio y analítica avanzada.", - "usagePercentage": 17, - "experienceYears": 4, - "experienceProjects": 7, - "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] - }, - { - "name": "MongoDB", - "description": "Soporte a microservicios y pipelines de ingesta flexible.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 4, - "keywords": ["Aggregation", "Atlas", "ODM"] - }, - { - "name": "ElasticSearch", - "description": "Buscadores facetados y observabilidad de logs.", - "usagePercentage": 6, - "experienceYears": 3, - "experienceProjects": 3, - "keywords": ["Full-text", "Kibana", "Ingest pipelines"] - } - ] - }, - { - "id": "devops", - "name": "DevOps & Cloud", - "summary": "Automatización de despliegues, infraestructura como código y observabilidad para SaaS.", - "experienceYears": 5, - "skills": [ - { - "name": "Docker & Compose", - "description": "Empaquetado de servicios, pipelines de CI y entornos reproducibles.", - "usagePercentage": 40, - "experienceYears": 5, - "experienceProjects": 20, - "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] - }, - { - "name": "GitHub Actions", - "description": "Workflows CI/CD para lint, tests, build y despliegues.", - "usagePercentage": 34, - "experienceYears": 4, - "experienceProjects": 10, - "keywords": ["Matrix builds", "Secrets", "Artifacts"] - }, - { - "name": "AWS / VPS", - "description": "Infraestructura híbrida con focos en coste, seguridad y monitoreo.", - "usagePercentage": 20, - "experienceYears": 4, - "experienceProjects": 6, - "keywords": ["EC2", "Lightsail", "S3", "Terraform"] - }, - { - "name": "Terraform", - "description": "Provisionamiento declarativo para entornos multi-stage.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 3, - "keywords": ["IaC", "Modules", "State management"] - } - ] - }, - { - "id": "ai-automation", - "name": "IA & Automatización", - "summary": "Integración de modelos y orquestación de workflows inteligentes para automatización de procesos.", - "experienceYears": 3, - "skills": [ - { - "name": "OpenAI Platform", - "description": "Integraciones conversacionales y copilotos para soporte.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 5, - "keywords": ["GPT", "Embeddings", "Function calling"] - }, - { - "name": "RAG Systems", - "description": "Diseño de pipelines Retrieval Augmented Generation para conocimiento interno.", - "usagePercentage": 6, - "experienceYears": 2, - "experienceProjects": 3, - "keywords": ["Vector stores", "Hybrid search", "Context windows"] - }, - { - "name": "n8n Workflows", - "description": "Automatización de procesos de negocio con integraciones low-code.", - "usagePercentage": 6, - "experienceYears": 2, - "experienceProjects": 4, - "keywords": ["Automation", "Webhook", "Orchestration"] - } - ] - }, - { - "id": "testing", - "name": "Testing & QA", - "summary": "Cobertura integral con suites unitarias, de integración y end-to-end para asegurar calidad continua.", - "experienceYears": 5, - "skills": [ - { - "name": "PHPUnit", - "description": "Testing para dominios complejos en Symfony y servicios backend.", - "usagePercentage": 20, - "experienceYears": 5, - "experienceProjects": 10, - "keywords": ["TDD", "Mockery", "Integration tests"] - }, - { - "name": "Vitest", - "description": "Pruebas unitarias y de componentes en frontend con TypeScript.", - "usagePercentage": 9, - "experienceYears": 2, - "experienceProjects": 5, - "keywords": ["RTL", "Snapshots", "Monorepo"] - }, - { - "name": "Jest", - "description": "Legacy y servicios Node.js con mocks avanzados.", - "usagePercentage": 9, - "experienceYears": 3, - "experienceProjects": 6, - "keywords": ["Mocks", "Coverage", "Watch mode"] - }, - { - "name": "Playwright", - "description": "End-to-end para flujos críticos con CI y reporting visual.", - "usagePercentage": 6, - "experienceYears": 2, - "experienceProjects": 3, - "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] - } - ] - } - ], - "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 } - ], - "contributionsLastYear": 1706, - "activeRepositories": 9, - "repositoriesWithStars": 3, - "accountCreatedAt": "2024-04-22" - } - }, - "projects": { - "title": "Proyectos", - "subtitle": "Selección de productos SaaS, herramientas internas y documentación técnica desarrollada para clientes y proyectos propios. Organizados en 6 categorías estratégicas.", - "filters": { - "all": "Todas ({count})" - }, - "aria": { - "tech": "Tecnologías utilizadas" - }, - "labels": { - "github": "GitHub", - "demo": "Demo →", - "detail": "Ver detalle", - "categoryFallback": "Proyecto" - }, - "privateNotice": { - "text": "Repositorio privado — solicita acceso en {linkLabel}", - "linkLabel": "LinkedIn" - }, - "categories": [ - { - "id": "facturacion-compliance", - "name": "Facturación & Compliance", - "description": "Soluciones fiscales y documentación técnica para entornos VeriFactu y ERP." - }, - { - "id": "gestion-servicios", - "name": "Gestión de Servicios", - "description": "Aplicaciones SaaS para automatizar reservas, operaciones y backoffice." - }, - { - "id": "arquitectura-avanzada", - "name": "Arquitectura Avanzada", - "description": "Plataformas con enfoque en arquitectura hexagonal, DDD y escalabilidad." - }, - { - "id": "erp-transformacion", - "name": "ERP & Transformación Digital", - "description": "Modernización de sistemas legacy a infraestructuras SaaS mantenibles." - }, - { - "id": "herramientas", - "name": "Herramientas & Automatización", - "description": "Productos internos para analítica, automatización y soporte inteligente." - }, - { - "id": "educacion", - "name": "Educación & Comunidad", - "description": "Material formativo y pruebas técnicas para compartir conocimiento." - } - ], - "items": [ - { - "id": "verifactur-gmn", - "title": "VerifacturGMN", - "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": "FacturaeDocusaurus", - "description": "Portal de documentación técnica para VeriFactu y Facturae con guías, ejemplos y despliegue automatizado.", - "tech": ["Docusaurus", "TypeScript", "MDX", "GitHub Actions"], - "links": { - "github": "https://github.com/GMNAPI/FacturaeDocusaurus" - }, - "categoryId": "facturacion-compliance" - }, - { - "id": "apigns", - "title": "apiGns", - "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": "frontend-json", - "title": "FrontendJson", - "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" - }, - "categoryId": "gestion-servicios" - }, - { - "id": "fynkus", - "title": "FYNKUS", - "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": "GestiónoMiNegocio", - "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": "Vita Liber Platform", - "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": "Genesis 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": "Streamlit GMN", - "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": "Zend Laminas Tech Tests", - "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" - } - ] - }, - "contact": { - "title": "Contacto", - "subtitle": "¿Tienes un proyecto en mente? Hablemos", - "form": { - "name": { - "label": "Nombre", - "placeholder": "Tu nombre" - }, - "email": { - "label": "Email", - "placeholder": "tu@email.com" - }, - "message": { - "label": "Mensaje", - "placeholder": "Cuéntame sobre tu proyecto..." - } - }, - "validation": { - "nameRequired": "El nombre es requerido", - "emailInvalid": "Un email válido es requerido", - "messageRequired": "El mensaje es requerido", - "messageMinLength": "El mensaje debe tener al menos 10 caracteres" - }, - "buttons": { - "submit": "Enviar mensaje", - "submitting": "Enviando..." - }, - "messages": { - "success": "Mensaje enviado correctamente. ¡Te responderé pronto!", - "error": "No se pudo enviar el mensaje. Intenta de nuevo." - } - }, - "footer": { - "tagline": "Desarrollo cosas", - "findMe": "Encuéntrame", - "socialLabel": "Sígueme en {network}", - "copyright": "Todos los derechos reservados" - }, - "social": { - "github": { - "label": "GitHub", - "aria": "Visita mi perfil de GitHub" - }, - "linkedin": { - "label": "LinkedIn", - "aria": "Visita mi perfil de LinkedIn" - } - }, - "personal": { - "location": { - "city": "Barcelona", - "country": "España" - }, - "tagline": "Tech Lead | Comunicación técnica | SaaS | Node.js | React | PHP/Symfony", - "bio": { - "short": "Tech Lead con experiencia colaborando con usuarios y clientes, comunicación clara y actitud resolutiva.", - "full": "Tech Lead especializado en traducir requisitos de negocio a soluciones técnicas. Experiencia liderando equipos, coordinando con stakeholders, y gestionando incidencias con comunicación efectiva. Enfocado en entregar valor a través de comunicación clara, seguimiento de tareas y entregas iterativas." - }, - "availability": { - "open": "Abierto a nuevas oportunidades" - }, - "focusAreas": [ - "Technical Leadership", - "Stakeholder Communication", - "Requirements Analysis", - "Team Coordination", - "Incident Resolution", - "Iterative Delivery", - "User Support & Training", - "Technical Strategy" - ] - }, - "career": {} -} +{ + "metadata": { + "title": "{name} | {tagline}", + "description": "Tech Lead con experiencia colaborando con usuarios y clientes, acostumbrado a tratar incidencias y necesidades reales con comunicación clara y actitud resolutiva. Especializado en traducir requisitos de negocio a soluciones técnicas.", + "ogImageAlt": "{name} - Portfolio Profesional", + "keywords": [ + "Ángel Hidalgo Barreiro", + "tech lead", + "Barcelona", + "gestión técnica", + "comunicación", + "coordinación equipos", + "SaaS", + "PHP", + "Symfony", + "Node.js", + "React", + "Next.js", + "TypeScript", + "liderazgo técnico", + "portfolio", + "resolución incidencias", + "gestión stakeholders" + ] + }, + "navigation": { + "brand": "Dev Portfolio", + "ariaLabel": "Navegación principal", + "items": { + "home": "Inicio", + "about": "Sobre mí", + "projects": "Proyectos", + "contact": "Contacto" + }, + "cta": "Hablemos →", + "theme": { + "toggle": "Cambiar tema" + }, + "menu": { + "open": "Abrir menú", + "close": "Cerrar menú" + }, + "locale": { + "label": "Idioma", + "switchTo": "Cambiar idioma a {locale}", + "current": "Idioma actual: {locale}", + "options": { + "es": "Español", + "en": "Inglés" + } + } + }, + "hero": { + "greeting": "👋 Hola, soy", + "location": "📍 {city}, {country}", + "asciiAlt": "Retrato profesional de {name} en arte ASCII con estilo terminal", + "buttons": { + "projects": "Ver proyectos →", + "cv": "📄 Descargar CV", + "contact": "Contactar" + } + }, + "about": { + "eyebrow": "Experiencia", + "title": "Sobre mí", + "intro": "Tech Lead con experiencia colaborando con usuarios y clientes, acostumbrado a tratar incidencias y necesidades reales con comunicación clara y actitud resolutiva. Enfocado en traducir requisitos de negocio a soluciones prácticas, con seguimiento constante y entregas iterativas.", + "sections": { + "timeline": "Timeline profesional", + "activity": "Actividad reciente", + "certifications": "Certificaciones", + "techStack": "Stack tecnológico" + }, + "stats": { + "contributions": { + "label": "Contribuciones último año", + "detail": "Commits, PRs y actividad privada en GitHub" + }, + "repositoriesAnalysed": { + "label": "Repositorios analizados", + "detail": "Portafolio privado entre SaaS, herramientas y documentación" + }, + "repositoriesActive": { + "label": "Repositorios activos", + "detail": "Proyectos con mantenimiento y roadmap en curso" + }, + "repositoriesWithStars": { + "label": "Repositorios con stars", + "detail": "Repos destacados con feedback de la comunidad" + }, + "accountCreated": { + "label": "Cuenta creada", + "detail": "Desde {detailDate} contribuyendo en proyectos open/private" + } + }, + "technologies": [ + "Next.js", + "React", + "TypeScript", + "Tailwind CSS", + "Node.js", + "Git", + "Vitest", + "Clean Architecture" + ], + "career": { + "experiences": [ + { + "role": "Consultor & Tech Lead", + "company": "Fastbyte SL", + "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."] + }, + { + "role": "Especialista de Programa", + "company": "Vita Liber S.L.U.", + "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" }, + "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."] + }, + { + "role": "Developer", + "company": "240dots", + "period": { "start": "2020", "end": "2023" }, + "headline": "Desarrollo de soluciones headless combinando WordPress y Symfony.", + "achievements": [ + "Construcción de plugins conectados con APIs personalizadas.", + "Optimización de rendimiento y pipelines de contenido." + ] + }, + { + "role": "Webmaster", + "company": "Seedstockers (BCG)", + "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).", + "Mejora continua de experiencia de usuario en procesos clave." + ] + } + ], + "certifications": [ + { + "name": "Zend Certificate of E-Learning – PHP OOP & Software Patterns", + "issuer": "Zend", + "year": "2025", + "description": "Programa avanzado sobre patrones de diseño orientados a objetos aplicados en PHP." + } + ] + } + }, + "skills": { + "eyebrow": "Stack principal", + "title": "Habilidades técnicas", + "intro": "Panorama basado en el análisis de 35 repositorios privados y contribuciones recientes. Destaca tecnologías con mayor adopción, años de experiencia y foco actual en automatización inteligente.", + "languages": { + "title": "Lenguajes más utilizados" + }, + "visualization": { + "title": "Distribución del stack", + "subtitle": "Participación relativa por lenguaje en los repositorios analizados", + "centerLabel": "Repos analizados", + "legendLabel": "{percentage}% del stack" + }, + "stats": { + "contributions": { + "label": "Contribuciones anuales", + "value": "{count} commits y PRs privados" + }, + "activeRepositories": { + "label": "Repositorios activos", + "value": "{count} repos en mantenimiento continuo" + } + }, + "experienceLabel": "{years}+ años", + "usageLabel": "{percentage}% uso repos", + "projectsLabel": "{count} proyectos", + "aria": { + "openCategory": "Abrir detalles de {name}", + "closeModal": "Cerrar modal" + }, + "categories": [ + { + "id": "backend", + "name": "Backend", + "summary": "Migraciones ERP a SaaS, APIs REST y plataformas de servicios con foco en resiliencia y dominio del negocio.", + "experienceYears": 6, + "skills": [ + { + "name": "PHP", + "description": "Lenguaje principal en 11 repositorios con foco en arquitectura limpia.", + "usagePercentage": 31, + "experienceYears": 6, + "experienceProjects": 18, + "keywords": ["Symfony", "Laravel", "OOP", "DDD"] + }, + { + "name": "Symfony 7", + "description": "Framework estrella para APIs y backoffice multi-tenant.", + "usagePercentage": 31, + "experienceYears": 5, + "experienceProjects": 12, + "keywords": ["API Platform", "Messenger", "Hexagonal"] + }, + { + "name": "Node.js", + "description": "Servicios suplementarios, workers y automatización con TypeScript.", + "usagePercentage": 20, + "experienceYears": 4, + "experienceProjects": 8, + "keywords": ["Express", "Serverless", "Workers"] + }, + { + "name": "API Platform", + "description": "Contratos REST enriquecidos para ecosistemas B2B.", + "usagePercentage": 14, + "experienceYears": 4, + "experienceProjects": 6, + "keywords": ["OpenAPI", "JWT", "HATEOAS"] + } + ] + }, + { + "id": "frontend", + "name": "Frontend", + "summary": "Interfaces empresariales con Next.js 15/React 19, componentes a medida y rendimiento optimizado.", + "experienceYears": 5, + "skills": [ + { + "name": "JavaScript / TypeScript", + "description": "Uso combinado en stack moderno para tipado estricto y DX superior.", + "usagePercentage": 29, + "experienceYears": 5, + "experienceProjects": 15, + "keywords": ["TypeScript", "ESNext", "monorepos"] + }, + { + "name": "React 18/19", + "description": "SPA/SSR con hooks avanzados, Suspense y patrones RSC.", + "usagePercentage": 20, + "experienceYears": 5, + "experienceProjects": 12, + "keywords": ["Hooks", "Context", "RSC", "RHF"] + }, + { + "name": "Next.js 15", + "description": "App Router, ISR y layouts anidados para portfolio y productos SaaS.", + "usagePercentage": 14, + "experienceYears": 4, + "experienceProjects": 9, + "keywords": ["App Router", "ISR", "SEO", "next-intl"] + }, + { + "name": "Tailwind CSS / shadcn/ui", + "description": "Design system minimalista con componentes accesibles.", + "usagePercentage": 14, + "experienceYears": 4, + "experienceProjects": 9, + "keywords": ["Design tokens", "Dark mode", "Framer Motion"] + } + ] + }, + { + "id": "leadership", + "name": "Liderazgo & Comunicación", + "summary": "Gestión de equipos, comunicación con stakeholders y resolución efectiva de incidencias.", + "experienceYears": 5, + "skills": [ + { + "name": "Comunicación técnica", + "description": "Traducción de requisitos de negocio a soluciones prácticas.", + "usagePercentage": 90, + "experienceYears": 5, + "experienceProjects": 15, + "keywords": ["Stakeholders", "Requisitos", "Documentación"] + }, + { + "name": "Gestión de incidencias", + "description": "Priorización, validación y soporte funcional a usuarios.", + "usagePercentage": 85, + "experienceYears": 5, + "experienceProjects": 12, + "keywords": ["Soporte", "Resolución", "Seguimiento"] + }, + { + "name": "Coordinación de equipos", + "description": "Seguimiento de tareas y entrega por fases.", + "usagePercentage": 80, + "experienceYears": 4, + "experienceProjects": 10, + "keywords": ["Liderazgo", "Metodologías", "Entregas"] + }, + { + "name": "Orientación al cliente", + "description": "Atención personalizada, empatía y feedback continuo.", + "usagePercentage": 85, + "experienceYears": 5, + "experienceProjects": 15, + "keywords": ["UX", "Feedback", "Mejora continua"] + } + ] + }, + { + "id": "databases", + "name": "Bases de Datos", + "summary": "Modelado y optimización para sistemas de alta disponibilidad con reporting y ETL.", + "experienceYears": 5, + "skills": [ + { + "name": "MariaDB / MySQL", + "description": "Motores principales en soluciones ERP y plataformas SaaS.", + "usagePercentage": 28, + "experienceYears": 5, + "experienceProjects": 14, + "keywords": ["Replication", "Query tuning", "Migrations"] + }, + { + "name": "PostgreSQL", + "description": "Implementado en arquitecturas multi-servicio y analítica avanzada.", + "usagePercentage": 17, + "experienceYears": 4, + "experienceProjects": 7, + "keywords": ["JSONB", "PL/pgSQL", "Materialized views"] + }, + { + "name": "MongoDB", + "description": "Soporte a microservicios y pipelines de ingesta flexible.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 4, + "keywords": ["Aggregation", "Atlas", "ODM"] + }, + { + "name": "ElasticSearch", + "description": "Buscadores facetados y observabilidad de logs.", + "usagePercentage": 6, + "experienceYears": 3, + "experienceProjects": 3, + "keywords": ["Full-text", "Kibana", "Ingest pipelines"] + } + ] + }, + { + "id": "devops", + "name": "DevOps & Cloud", + "summary": "Automatización de despliegues, infraestructura como código y observabilidad para SaaS.", + "experienceYears": 5, + "skills": [ + { + "name": "Docker & Compose", + "description": "Empaquetado de servicios, pipelines de CI y entornos reproducibles.", + "usagePercentage": 40, + "experienceYears": 5, + "experienceProjects": 20, + "keywords": ["Multi-stage", "CI/CD", "Devcontainers"] + }, + { + "name": "GitHub Actions", + "description": "Workflows CI/CD para lint, tests, build y despliegues.", + "usagePercentage": 34, + "experienceYears": 4, + "experienceProjects": 10, + "keywords": ["Matrix builds", "Secrets", "Artifacts"] + }, + { + "name": "AWS / VPS", + "description": "Infraestructura híbrida con focos en coste, seguridad y monitoreo.", + "usagePercentage": 20, + "experienceYears": 4, + "experienceProjects": 6, + "keywords": ["EC2", "Lightsail", "S3", "Terraform"] + }, + { + "name": "Terraform", + "description": "Provisionamiento declarativo para entornos multi-stage.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 3, + "keywords": ["IaC", "Modules", "State management"] + } + ] + }, + { + "id": "ai-automation", + "name": "IA & Automatización", + "summary": "Integración de modelos y orquestación de workflows inteligentes para automatización de procesos.", + "experienceYears": 3, + "skills": [ + { + "name": "OpenAI Platform", + "description": "Integraciones conversacionales y copilotos para soporte.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 5, + "keywords": ["GPT", "Embeddings", "Function calling"] + }, + { + "name": "RAG Systems", + "description": "Diseño de pipelines Retrieval Augmented Generation para conocimiento interno.", + "usagePercentage": 6, + "experienceYears": 2, + "experienceProjects": 3, + "keywords": ["Vector stores", "Hybrid search", "Context windows"] + }, + { + "name": "n8n Workflows", + "description": "Automatización de procesos de negocio con integraciones low-code.", + "usagePercentage": 6, + "experienceYears": 2, + "experienceProjects": 4, + "keywords": ["Automation", "Webhook", "Orchestration"] + } + ] + }, + { + "id": "testing", + "name": "Testing & QA", + "summary": "Cobertura integral con suites unitarias, de integración y end-to-end para asegurar calidad continua.", + "experienceYears": 5, + "skills": [ + { + "name": "PHPUnit", + "description": "Testing para dominios complejos en Symfony y servicios backend.", + "usagePercentage": 20, + "experienceYears": 5, + "experienceProjects": 10, + "keywords": ["TDD", "Mockery", "Integration tests"] + }, + { + "name": "Vitest", + "description": "Pruebas unitarias y de componentes en frontend con TypeScript.", + "usagePercentage": 9, + "experienceYears": 2, + "experienceProjects": 5, + "keywords": ["RTL", "Snapshots", "Monorepo"] + }, + { + "name": "Jest", + "description": "Legacy y servicios Node.js con mocks avanzados.", + "usagePercentage": 9, + "experienceYears": 3, + "experienceProjects": 6, + "keywords": ["Mocks", "Coverage", "Watch mode"] + }, + { + "name": "Playwright", + "description": "End-to-end para flujos críticos con CI y reporting visual.", + "usagePercentage": 6, + "experienceYears": 2, + "experienceProjects": 3, + "keywords": ["Trace viewer", "CI pipelines", "Cross-browser"] + } + ] + } + ], + "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 } + ], + "contributionsLastYear": 1706, + "activeRepositories": 9, + "repositoriesWithStars": 3, + "accountCreatedAt": "2024-04-22" + } + }, + "projects": { + "title": "Proyectos", + "subtitle": "Selección de productos SaaS, herramientas internas y documentación técnica desarrollada en proyectos profesionales. Organizados en 6 categorías estratégicas.", + "filters": { + "all": "Todas ({count})" + }, + "aria": { + "tech": "Tecnologías utilizadas", + "openProject": "Abrir detalles de {title}", + "closeModal": "Cerrar modal" + }, + "labels": { + "github": "GitHub", + "demo": "Demo →", + "detail": "Ver detalle", + "categoryFallback": "Proyecto", + "close": "Cerrar", + "moretech": "+{count} más" + }, + "privateNotice": { + "text": "Repositorio privado — solicita acceso en {linkLabel}", + "linkLabel": "LinkedIn" + }, + "categories": [ + { + "id": "facturacion-compliance", + "name": "Facturación & Compliance", + "description": "Soluciones fiscales y documentación técnica para entornos VeriFactu y ERP." + }, + { + "id": "gestion-servicios", + "name": "Gestión de Servicios", + "description": "Aplicaciones SaaS para automatizar reservas, operaciones y backoffice." + }, + { + "id": "arquitectura-avanzada", + "name": "Arquitectura Avanzada", + "description": "Plataformas con enfoque en arquitectura hexagonal, DDD y escalabilidad." + }, + { + "id": "erp-transformacion", + "name": "ERP & Transformación Digital", + "description": "Modernización de sistemas legacy a infraestructuras SaaS mantenibles." + }, + { + "id": "herramientas", + "name": "Herramientas & Automatización", + "description": "Productos internos para analítica, automatización y soporte inteligente." + }, + { + "id": "educacion", + "name": "Educación & Comunidad", + "description": "Material formativo y pruebas técnicas para compartir conocimiento." + }, + { + "id": "ia-chatbots", + "name": "IA & Chatbots", + "description": "Asistentes conversacionales y sistemas RAG con inteligencia artificial generativa." + } + ], + "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"], + "links": { + "github": "https://github.com/GMNAPI/FacturaeDocusaurus" + }, + "categoryId": "facturacion-compliance" + }, + { + "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": "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" + }, + "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.", + "tech": [ + "TypeScript", + "React 19", + "Vite 6", + "Tailwind v4", + "Fastify", + "RAG", + "OpenAI GPT-4", + "ChromaDB", + "Docker", + "Railway" + ], + "links": { + "github": "https://github.com/GMNAPI/llarjove", + "demo": "https://llarjove-production-754a.up.railway.app/" + }, + "categoryId": "ia-chatbots" + } + ] + }, + "contact": { + "title": "Contacto", + "subtitle": "¿Tienes un proyecto en mente? Hablemos", + "form": { + "name": { + "label": "Nombre", + "placeholder": "Tu nombre" + }, + "email": { + "label": "Email", + "placeholder": "tu@email.com" + }, + "message": { + "label": "Mensaje", + "placeholder": "Cuéntame sobre tu proyecto..." + } + }, + "validation": { + "nameRequired": "El nombre es requerido", + "emailInvalid": "Un email válido es requerido", + "messageRequired": "El mensaje es requerido", + "messageMinLength": "El mensaje debe tener al menos 10 caracteres" + }, + "buttons": { + "submit": "Enviar mensaje", + "submitting": "Enviando..." + }, + "messages": { + "success": "Mensaje enviado correctamente. ¡Te responderé pronto!", + "error": "No se pudo enviar el mensaje. Intenta de nuevo." + } + }, + "footer": { + "tagline": "Desarrollo cosas", + "findMe": "Encuéntrame", + "socialLabel": "Sígueme en {network}", + "copyright": "Todos los derechos reservados" + }, + "social": { + "github": { + "label": "GitHub", + "aria": "Visita mi perfil de GitHub" + }, + "linkedin": { + "label": "LinkedIn", + "aria": "Visita mi perfil de LinkedIn" + } + }, + "personal": { + "location": { + "city": "Barcelona", + "country": "España" + }, + "tagline": "Tech Lead | Comunicación técnica | SaaS | Node.js | React | PHP/Symfony", + "bio": { + "short": "Tech Lead con experiencia colaborando con usuarios y clientes, comunicación clara y actitud resolutiva.", + "full": "Tech Lead especializado en traducir requisitos de negocio a soluciones técnicas. Experiencia liderando equipos, coordinando con stakeholders, y gestionando incidencias con comunicación efectiva. Enfocado en entregar valor a través de comunicación clara, seguimiento de tareas y entregas iterativas." + }, + "availability": { + "open": "Abierto a nuevas oportunidades" + }, + "focusAreas": [ + "Technical Leadership", + "Stakeholder Communication", + "Requirements Analysis", + "Team Coordination", + "Incident Resolution", + "Iterative Delivery", + "User Support & Training", + "Technical Strategy" + ] + }, + "career": {} +} diff --git a/apps/portfolio/package.json b/apps/portfolio/package.json index 94b86a3..a1c58f8 100644 --- a/apps/portfolio/package.json +++ b/apps/portfolio/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "next dev --port 3002", + "dev:lab": "pnpm --filter lab dev", "build": "next build", "start": "next start", "start:dev": "next start --port 3002", diff --git a/apps/portfolio/public/cvEn.pdf b/apps/portfolio/public/cvEn.pdf index 2787df1..e95382f 100644 Binary files a/apps/portfolio/public/cvEn.pdf and b/apps/portfolio/public/cvEn.pdf differ diff --git a/apps/portfolio/public/cvEs.pdf b/apps/portfolio/public/cvEs.pdf index 9cfc792..f5926ab 100644 Binary files a/apps/portfolio/public/cvEs.pdf and b/apps/portfolio/public/cvEs.pdf differ diff --git a/apps/portfolio/public/images/avatar-placeholder.svg b/apps/portfolio/public/images/avatar-placeholder.svg index a48eadb..ddbd204 100644 --- a/apps/portfolio/public/images/avatar-placeholder.svg +++ b/apps/portfolio/public/images/avatar-placeholder.svg @@ -11,4 +11,4 @@ Ángel Hidalgo Barreiro - + diff --git a/apps/portfolio/public/images/projects/.gitkeep b/apps/portfolio/public/images/projects/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/portfolio/src/core/entities/Project.ts b/apps/portfolio/src/core/entities/Project.ts index d00098d..6e485d1 100644 --- a/apps/portfolio/src/core/entities/Project.ts +++ b/apps/portfolio/src/core/entities/Project.ts @@ -20,12 +20,13 @@ export interface ProjectLinks { } export type ProjectCategoryId = - | 'facturacion-compliance' - | 'gestion-servicios' | 'arquitectura-avanzada' + | 'educacion' | 'erp-transformacion' + | 'facturacion-compliance' + | 'gestion-servicios' | 'herramientas' - | 'educacion'; + | 'ia-chatbots'; export interface ProjectData { id: string; @@ -35,6 +36,7 @@ export interface ProjectData { links: ProjectLinks; categoryId: ProjectCategoryId; detailSlug?: string; + imageUrl?: string; } export class Project { @@ -45,6 +47,7 @@ export class Project { readonly links: ProjectLinks; readonly categoryId: ProjectCategoryId; readonly detailSlug: string; + readonly imageUrl: string | undefined; constructor(data: ProjectData) { this.validate(data); @@ -56,6 +59,7 @@ export class Project { this.links = data.links; this.categoryId = data.categoryId; this.detailSlug = data.detailSlug ?? data.id; + this.imageUrl = data.imageUrl; } /** @@ -95,6 +99,7 @@ export class Project { links: { ...this.links }, categoryId: this.categoryId, detailSlug: this.detailSlug, + imageUrl: this.imageUrl, }; } } diff --git a/apps/portfolio/src/features/about/About.tsx b/apps/portfolio/src/features/about/About.tsx index fee9f31..142996b 100644 --- a/apps/portfolio/src/features/about/About.tsx +++ b/apps/portfolio/src/features/about/About.tsx @@ -116,16 +116,18 @@ export function About() { -
    - {experience.achievements.map((achievement) => ( - - - {achievement} - - ))} -
+ {experience.achievements.length > 0 && ( +
    + {experience.achievements.map((achievement) => ( + + + {achievement} + + ))} +
+ )} ))} diff --git a/apps/portfolio/src/features/projects/Projects.tsx b/apps/portfolio/src/features/projects/Projects.tsx index 8c442bb..3fb8582 100644 --- a/apps/portfolio/src/features/projects/Projects.tsx +++ b/apps/portfolio/src/features/projects/Projects.tsx @@ -2,31 +2,23 @@ * Projects Section * * Portfolio projects showcase. - * Displays project cards with tech stack and links. + * Displays compact project cards; clicking opens a detail modal. */ 'use client'; -import Link from 'next/link'; import { useMemo, useState } from 'react'; import { m } from 'framer-motion'; -import { useLocale, useTranslations } from 'next-intl'; +import { useTranslations } from 'next-intl'; -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, -} from '@/shared/components/ui/Card'; import { Button } from '@/shared/components/ui/Button'; import { Project } from '@/core/entities/Project'; import type { ProjectCategory, ProjectMessageItem } from '@/shared/constants/projects'; -import { personalInfo } from '@/shared/constants/personal'; import { fadeInUp, staggerContainer } from '@/shared/utils/motion'; +import { ProjectCard } from './components/ProjectCard'; +import { ProjectModal } from './components/ProjectModal'; export function Projects() { - const locale = useLocale(); const tProjects = useTranslations('projects'); const rawProjects = useMemo(() => tProjects.raw('items') as ProjectMessageItem[], [tProjects]); @@ -44,7 +36,9 @@ export function Projects() { ), [categories] ); + const [activeCategory, setActiveCategory] = useState<'all' | Project['categoryId']>('all'); + const [selectedProject, setSelectedProject] = useState(null); const categoryCounts = useMemo(() => { return projects.reduce>( @@ -60,7 +54,6 @@ export function Projects() { if (activeCategory === 'all') { return projects; } - return projects.filter((project) => project.categoryId === activeCategory); }, [activeCategory, projects]); @@ -118,100 +111,33 @@ export function Projects() { {/* Projects Grid */} {filteredProjects.map((project) => { const category = categoriesById[project.categoryId]; - return ( - - - -
- - {category?.name ?? 'Proyecto'} - -
-
- {project.title} - - {project.description} - -
-
- - {/* Tech Stack */} -
- {project.tech.map((tech) => ( - - {tech} - - ))} -
- - {/* Links */} -
- {project.links.github && ( - - {tProjects('labels.github')} - - )} - {project.links.demo && ( - - {tProjects('labels.demo')} - - )} - - {tProjects('labels.detail')} - -
-

- - - {tProjects.rich('privateNotice.text', { - linkLabel: tProjects('privateNotice.linkLabel'), - link: (chunks) => ( - - {chunks} - - ), - })} - -

-
-
-
+ project={project} + categoryName={category?.name ?? tProjects('labels.categoryFallback')} + onOpen={setSelectedProject} + /> ); })}
+ + {/* Detail modal */} + {selectedProject && ( + setSelectedProject(null)} + /> + )} ); } diff --git a/apps/portfolio/src/features/projects/components/ProjectCard.tsx b/apps/portfolio/src/features/projects/components/ProjectCard.tsx new file mode 100644 index 0000000..250071a --- /dev/null +++ b/apps/portfolio/src/features/projects/components/ProjectCard.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { m } from 'framer-motion'; +import { useTranslations } from 'next-intl'; + +import { cn } from '@/shared/utils/cn'; +import { fadeInUp } from '@/shared/utils/motion'; +import type { Project } from '@/core/entities/Project'; +import { CATEGORY_GRADIENTS } from '../utils/categoryColors'; + +const MAX_VISIBLE_TECH = 3; + +interface ProjectCardProps { + project: Project; + categoryName: string; + onOpen: (project: Project) => void; +} + +export function ProjectCard({ project, categoryName, onOpen }: ProjectCardProps) { + const t = useTranslations('projects'); + + const visibleTech = project.tech.slice(0, MAX_VISIBLE_TECH); + const overflowCount = project.tech.length - MAX_VISIBLE_TECH; + + return ( + onOpen(project)} + role="article" + aria-label={t('aria.openProject', { title: project.title })} + > + {/* Image / Gradient area */} +
+ {project.imageUrl ? ( + {project.title} + ) : ( +
+ + {project.title.charAt(0)} + +
+ )} +
+ + {/* Default card content */} +
+ + {categoryName} + +

+ {project.title} +

+
+ {visibleTech.map((tech) => ( + + {tech} + + ))} + {overflowCount > 0 && ( + + {t('labels.moretech', { count: overflowCount })} + + )} +
+
+ + {/* Hover overlay */} +
+
+ + {categoryName} + +

+ {project.title} +

+

+ {project.description} +

+
+ {project.tech.map((tech) => ( + + {tech} + + ))} +
+
+

{t('labels.detail')} →

+
+
+ ); +} diff --git a/apps/portfolio/src/features/projects/components/ProjectModal.tsx b/apps/portfolio/src/features/projects/components/ProjectModal.tsx new file mode 100644 index 0000000..623f535 --- /dev/null +++ b/apps/portfolio/src/features/projects/components/ProjectModal.tsx @@ -0,0 +1,135 @@ +'use client'; + +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; + +import { Modal } from '@/shared/components/ui/Modal'; +import { cn } from '@/shared/utils/cn'; +import type { Project } from '@/core/entities/Project'; +import { personalInfo } from '@/shared/constants/personal'; +import { CATEGORY_GRADIENTS } from '../utils/categoryColors'; + +interface ProjectModalProps { + project: Project | null; + categoryName: string; + onClose: () => void; +} + +export function ProjectModal({ project, categoryName, onClose }: ProjectModalProps) { + const t = useTranslations('projects'); + + if (!project) return null; + + return ( + + {/* Header gradient / image */} +
+ {project.imageUrl ? ( + {project.title} + ) : ( +
+ + {project.title.charAt(0)} + +
+ )} + + {/* Close button */} + +
+ + {/* Content */} +
+ {/* Category + links row */} + + + {/* Title */} +

{project.title}

+ + {/* Description */} +

{project.description}

+ + {/* Tech stack */} +
+ {project.tech.map((tech) => ( + + {tech} + + ))} +
+ + {/* Detail link */} + + {t('labels.detail')} → + + + {/* Private notice */} +

+ + + {t.rich('privateNotice.text', { + linkLabel: t('privateNotice.linkLabel'), + link: (chunks) => ( + e.stopPropagation()} + className="text-accent underline-offset-2 hover:underline" + > + {chunks} + + ), + })} + +

+
+
+ ); +} diff --git a/apps/portfolio/src/features/projects/utils/categoryColors.ts b/apps/portfolio/src/features/projects/utils/categoryColors.ts new file mode 100644 index 0000000..a24ed27 --- /dev/null +++ b/apps/portfolio/src/features/projects/utils/categoryColors.ts @@ -0,0 +1,11 @@ +import type { ProjectCategoryId } from '@/core/entities/Project'; + +export const CATEGORY_GRADIENTS: Record = { + 'arquitectura-avanzada': 'from-blue-900/60 to-blue-700/40', + educacion: 'from-green-900/60 to-green-700/40', + 'erp-transformacion': 'from-orange-900/60 to-orange-700/40', + 'facturacion-compliance': 'from-red-900/60 to-red-700/40', + 'gestion-servicios': 'from-purple-900/60 to-purple-700/40', + herramientas: 'from-yellow-900/60 to-yellow-700/40', + 'ia-chatbots': 'from-cyan-900/60 to-cyan-700/40', +}; diff --git a/apps/portfolio/src/features/skills/Skills.tsx b/apps/portfolio/src/features/skills/Skills.tsx index 34047a1..3839a8c 100644 --- a/apps/portfolio/src/features/skills/Skills.tsx +++ b/apps/portfolio/src/features/skills/Skills.tsx @@ -1,20 +1,13 @@ 'use client'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { m } from 'framer-motion'; import { useLocale, useTranslations } from 'next-intl'; import type { SkillCategory, SkillSummary } from '@/shared/constants/skills'; import { fadeInUp, staggerContainer } from '@/shared/utils/motion'; - -const CATEGORY_ICONS: Record = { - backend: '🛠️', - frontend: '🎨', - databases: '🗄️', - devops: '⚙️', - 'ai-automation': '🤖', - testing: '✅', -}; +import { SkillCard } from './components/SkillCard'; +import { SkillModal } from './components/SkillModal'; const SEGMENT_COLORS = ['#38bdf8', '#34d399', '#fbbf24', '#f97316', '#a855f7', '#ef4444']; @@ -23,6 +16,8 @@ export function Skills() { const tSkills = useTranslations('skills'); const numberLocale = locale === 'es' ? 'es-ES' : 'en-US'; + const [selectedCategory, setSelectedCategory] = useState(null); + const categories = tSkills.raw('categories') as SkillCategory[]; const summary = tSkills.raw('summary') as SkillSummary; const languages = summary.mainLanguagesShare; @@ -192,96 +187,16 @@ export function Skills() { - {/* Skill categories */} + {/* Skill category cards */} {categories.map((category) => ( - -
-
-

- {CATEGORY_ICONS[category.id]} {category.name} -

-

{category.summary}

-
- - {tSkills('experienceLabel', { years: category.experienceYears })} - -
- -
    - {category.skills.map((skill) => { - const usageLabel = - skill.usagePercentage !== undefined - ? tSkills('usageLabel', { - percentage: percentFormatter.format(skill.usagePercentage), - }) - : null; - - return ( -
  • -
    -

    {skill.name}

    - {usageLabel && ( - - {usageLabel} - - )} -
    -

    {skill.description}

    - {(skill.usagePercentage !== undefined || - skill.experienceYears !== undefined || - skill.experienceProjects !== undefined) && ( -
    - {skill.usagePercentage !== undefined && ( -
    -
    -
    -
    - )} - {skill.experienceYears !== undefined && ( - - {tSkills('experienceLabel', { years: skill.experienceYears })} - - )} - {skill.experienceProjects !== undefined && ( - - {tSkills('projectsLabel', { count: skill.experienceProjects })} - - )} -
    - )} - {skill.keywords && ( -
    - {skill.keywords.map((keyword) => ( - - {keyword} - - ))} -
    - )} -
  • - ); - })} -
-
+ ))}
+ + {/* Detail modal */} + setSelectedCategory(null)} /> ); } diff --git a/apps/portfolio/src/features/skills/components/SkillCard.tsx b/apps/portfolio/src/features/skills/components/SkillCard.tsx new file mode 100644 index 0000000..2194668 --- /dev/null +++ b/apps/portfolio/src/features/skills/components/SkillCard.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { m } from 'framer-motion'; +import { useTranslations } from 'next-intl'; + +import { fadeInUp } from '@/shared/utils/motion'; +import type { SkillCategory } from '@/shared/constants/skills'; + +export const CATEGORY_ICONS: Record = { + backend: '🛠️', + frontend: '🎨', + databases: '🗄️', + devops: '⚙️', + 'ai-automation': '🤖', + testing: '✅', + leadership: '🤝', +}; + +const MAX_VISIBLE_SKILLS = 3; + +interface SkillCardProps { + category: SkillCategory; + onOpen: (category: SkillCategory) => void; +} + +export function SkillCard({ category, onOpen }: SkillCardProps) { + const t = useTranslations('skills'); + + const visibleSkills = category.skills.slice(0, MAX_VISIBLE_SKILLS); + const overflowCount = category.skills.length - MAX_VISIBLE_SKILLS; + + return ( + onOpen(category)} + role="article" + aria-label={t('aria.openCategory', { name: category.name })} + > + {/* Default card content */} +
+ {/* Header: icon + name + experience badge */} +
+

+ {CATEGORY_ICONS[category.id]} {category.name} +

+ + {t('experienceLabel', { years: category.experienceYears })} + +
+ + {/* Summary truncated to 2 lines */} +

+ {category.summary} +

+ + {/* Skill name pills: first 3 + overflow */} +
+ {visibleSkills.map((skill) => ( + + {skill.name} + + ))} + {overflowCount > 0 && ( + + +{overflowCount} + + )} +
+
+ + {/* Hover overlay */} +
+

+ {CATEGORY_ICONS[category.id]} {category.name} +

+
    + {category.skills.map((skill) => ( +
  • +
    + {skill.name} + {skill.usagePercentage !== undefined && ( + + {skill.usagePercentage}% + + )} +
    + {skill.usagePercentage !== undefined && ( +
    +
    + )} +
  • + ))} +
+

Ver detalles →

+
+
+ ); +} diff --git a/apps/portfolio/src/features/skills/components/SkillModal.tsx b/apps/portfolio/src/features/skills/components/SkillModal.tsx new file mode 100644 index 0000000..a0c9139 --- /dev/null +++ b/apps/portfolio/src/features/skills/components/SkillModal.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useMemo } from 'react'; +import { useLocale, useTranslations } from 'next-intl'; + +import { Modal } from '@/shared/components/ui/Modal'; +import type { SkillCategory } from '@/shared/constants/skills'; +import { CATEGORY_ICONS } from './SkillCard'; + +interface SkillModalProps { + category: SkillCategory | null; + onClose: () => void; +} + +export function SkillModal({ category, onClose }: SkillModalProps) { + const t = useTranslations('skills'); + const locale = useLocale(); + const numberLocale = locale === 'es' ? 'es-ES' : 'en-US'; + + const percentFormatter = useMemo( + () => + new Intl.NumberFormat(numberLocale, { + maximumFractionDigits: 1, + minimumFractionDigits: 0, + }), + [numberLocale] + ); + + if (!category) return null; + + return ( + + {/* Header */} +
+
+

+ {CATEGORY_ICONS[category.id]} {category.name} +

+ + {t('experienceLabel', { years: category.experienceYears })} + +
+ +
+ + {/* Summary */} +
+

{category.summary}

+
+ + {/* Skill list */} +
    + {category.skills.map((skill) => { + const usageLabel = + skill.usagePercentage !== undefined + ? t('usageLabel', { + percentage: percentFormatter.format(skill.usagePercentage), + }) + : null; + + return ( +
  • +
    +

    {skill.name}

    + {usageLabel && ( + {usageLabel} + )} +
    + + {skill.usagePercentage !== undefined && ( +
    +
    +
    +
    + )} + +

    {skill.description}

    + + {(skill.experienceYears !== undefined || skill.experienceProjects !== undefined) && ( +
    + {skill.experienceYears !== undefined && ( + {t('experienceLabel', { years: skill.experienceYears })} + )} + {skill.experienceProjects !== undefined && ( + {t('projectsLabel', { count: skill.experienceProjects })} + )} +
    + )} + + {skill.keywords && skill.keywords.length > 0 && ( +
    + {skill.keywords.map((keyword) => ( + + {keyword} + + ))} +
    + )} +
  • + ); + })} +
+
+ ); +} diff --git a/apps/portfolio/src/shared/components/layout/Navigation.tsx b/apps/portfolio/src/shared/components/layout/Navigation.tsx index 6e218f7..01b0e91 100644 --- a/apps/portfolio/src/shared/components/layout/Navigation.tsx +++ b/apps/portfolio/src/shared/components/layout/Navigation.tsx @@ -1,283 +1,283 @@ -/** - * Navigation Component - * - * Sticky navigation header with scroll spy, theme toggle, and mobile menu. - * Uses Intersection Observer to highlight the active section. - * - * Features: - * - Responsive design (mobile menu for < md breakpoint) - * - Theme toggle (light/dark mode) - * - Smooth scroll navigation - * - Active section highlighting - * - CTA button - */ - -'use client'; - -import { useEffect, useMemo, useState } from 'react'; -import { useTheme } from 'next-themes'; -import { AnimatePresence, m } from 'framer-motion'; -import { useLocale, useTranslations } from 'next-intl'; - -import { locales, type Locale } from '@/i18n/config'; -import { usePathname, useRouter } from '@/i18n/navigation'; -import { NAV_SECTIONS } from '@/shared/constants/navigation'; -import { useScrollSpy } from '@/shared/hooks/useScrollSpy'; -import { cn } from '@/shared/utils/cn'; -import { fadeInUp } from '@/shared/utils/motion'; - -export function Navigation() { - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [mounted, setMounted] = useState(false); - const { theme, setTheme } = useTheme(); - - // Prevent hydration mismatch by only rendering theme icon after mount - useEffect(() => { - setMounted(true); - }, []); - const locale = useLocale() as Locale; - const router = useRouter(); - const pathname = usePathname(); - - const tNavigation = useTranslations('navigation'); - const navItems = useMemo( - () => - NAV_SECTIONS.map((section) => ({ - ...section, - label: tNavigation(`items.${section.key}` as const), - })), - [tNavigation] - ); - - const sectionIds = useMemo(() => navItems.map((item) => item.sectionId), [navItems]); - const activeSection = useScrollSpy(sectionIds); - - const handleNavClick = (e: React.MouseEvent, sectionId: string) => { - e.preventDefault(); - const section = document.getElementById(sectionId); - if (section) { - section.scrollIntoView({ behavior: 'smooth' }); - } - // Close mobile menu after navigation - setIsMobileMenuOpen(false); - }; - - const toggleTheme = () => { - setTheme(theme === 'dark' ? 'light' : 'dark'); - }; - - const localeOptions = useMemo( - () => - locales.map((code) => ({ - code, - label: tNavigation(`locale.options.${code}` as const), - })), - [tNavigation] - ); - - const handleLocaleChange = (nextLocale: Locale) => { - if (nextLocale === locale) return; - - // Get current hash for preservation - const hash = typeof window !== 'undefined' ? window.location.hash : ''; - - // router.replace from next-intl automatically handles locale prefixes - // based on the localePrefix: 'as-needed' configuration - const targetPath = `${pathname}${hash}`; - router.replace(targetPath, { locale: nextLocale }); - - setIsMobileMenuOpen(false); - }; - - const localeLabel = tNavigation(`locale.options.${locale}` as const); - - const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen); - }; - - return ( - -
-
- {/* Brand Name */} - - handleNavClick(e, 'hero')} - > - {tNavigation('brand')} - - - - {/* Desktop Navigation */} - - {navItems.map((item) => ( -
  • - handleNavClick(e, item.sectionId)} - className={cn( - 'text-sm font-mono transition-colors', - activeSection === item.sectionId - ? 'text-accent border-b-2 border-accent' - : 'text-foreground-secondary hover:text-foreground' - )} - > - {item.label} - -
  • - ))} -
    - - {/* Right Side: CTA + Theme Toggle + Mobile Menu */} -
    - {/* CTA Button - Hidden on mobile */} - handleNavClick(e, 'contact')} - > - {tNavigation('cta')} - - - {/* Locale Switcher */} - - - - {/* Theme Toggle */} - - - {/* Mobile Menu Button */} - -
    -
    - - {/* Mobile Menu Dropdown */} - - {isMobileMenuOpen && ( - - - - )} - -
    -
    - ); -} +/** + * Navigation Component + * + * Sticky navigation header with scroll spy, theme toggle, and mobile menu. + * Uses Intersection Observer to highlight the active section. + * + * Features: + * - Responsive design (mobile menu for < md breakpoint) + * - Theme toggle (light/dark mode) + * - Smooth scroll navigation + * - Active section highlighting + * - CTA button + */ + +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { useTheme } from 'next-themes'; +import { AnimatePresence, m } from 'framer-motion'; +import { useLocale, useTranslations } from 'next-intl'; + +import { locales, type Locale } from '@/i18n/config'; +import { usePathname, useRouter } from '@/i18n/navigation'; +import { NAV_SECTIONS } from '@/shared/constants/navigation'; +import { useScrollSpy } from '@/shared/hooks/useScrollSpy'; +import { cn } from '@/shared/utils/cn'; +import { fadeInUp } from '@/shared/utils/motion'; + +export function Navigation() { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const { theme, setTheme } = useTheme(); + + // Prevent hydration mismatch by only rendering theme icon after mount + useEffect(() => { + setMounted(true); + }, []); + const locale = useLocale() as Locale; + const router = useRouter(); + const pathname = usePathname(); + + const tNavigation = useTranslations('navigation'); + const navItems = useMemo( + () => + NAV_SECTIONS.map((section) => ({ + ...section, + label: tNavigation(`items.${section.key}` as const), + })), + [tNavigation] + ); + + const sectionIds = useMemo(() => navItems.map((item) => item.sectionId), [navItems]); + const activeSection = useScrollSpy(sectionIds); + + const handleNavClick = (e: React.MouseEvent, sectionId: string) => { + e.preventDefault(); + const section = document.getElementById(sectionId); + if (section) { + section.scrollIntoView({ behavior: 'smooth' }); + } + // Close mobile menu after navigation + setIsMobileMenuOpen(false); + }; + + const toggleTheme = () => { + setTheme(theme === 'dark' ? 'light' : 'dark'); + }; + + const localeOptions = useMemo( + () => + locales.map((code) => ({ + code, + label: tNavigation(`locale.options.${code}` as const), + })), + [tNavigation] + ); + + const handleLocaleChange = (nextLocale: Locale) => { + if (nextLocale === locale) return; + + // Get current hash for preservation + const hash = typeof window !== 'undefined' ? window.location.hash : ''; + + // router.replace from next-intl automatically handles locale prefixes + // based on the localePrefix: 'as-needed' configuration + const targetPath = `${pathname}${hash}`; + router.replace(targetPath, { locale: nextLocale }); + + setIsMobileMenuOpen(false); + }; + + const localeLabel = tNavigation(`locale.options.${locale}` as const); + + const toggleMobileMenu = () => { + setIsMobileMenuOpen(!isMobileMenuOpen); + }; + + return ( + +
    +
    + {/* Brand Name */} + + handleNavClick(e, 'hero')} + > + {tNavigation('brand')} + + + + {/* Desktop Navigation */} + + {navItems.map((item) => ( +
  • + handleNavClick(e, item.sectionId)} + className={cn( + 'text-sm font-mono transition-colors', + activeSection === item.sectionId + ? 'text-accent border-b-2 border-accent' + : 'text-foreground-secondary hover:text-foreground' + )} + > + {item.label} + +
  • + ))} +
    + + {/* Right Side: CTA + Theme Toggle + Mobile Menu */} +
    + {/* CTA Button - Hidden on mobile */} + handleNavClick(e, 'contact')} + > + {tNavigation('cta')} + + + {/* Locale Switcher */} + + + + {/* Theme Toggle */} + + + {/* Mobile Menu Button */} + +
    +
    + + {/* Mobile Menu Dropdown */} + + {isMobileMenuOpen && ( + + + + )} + +
    +
    + ); +} diff --git a/apps/portfolio/src/shared/components/ui/Modal.tsx b/apps/portfolio/src/shared/components/ui/Modal.tsx new file mode 100644 index 0000000..cecdeda --- /dev/null +++ b/apps/portfolio/src/shared/components/ui/Modal.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { AnimatePresence, m } from 'framer-motion'; + +export interface ModalProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; + ariaLabel?: string; +} + +export function Modal({ isOpen, onClose, children, ariaLabel }: ModalProps) { + const [mounted, setMounted] = useState(false); + const panelRef = useRef(null); + const onCloseRef = useRef(onClose); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + onCloseRef.current = onClose; + }); + + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCloseRef.current(); + }; + + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = ''; + }; + }, [isOpen]); + + if (!mounted) return null; + + return createPortal( + + {isOpen && ( + + {/* Backdrop */} +