diff --git a/AGENTS.md b/AGENTS.md index 561184b..f9aa570 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,16 +2,15 @@ ## Project Summary -**Portfolio Website** for Adrián Montesinos López +**Portfolio Website** for Adrián Montes Linares - **URL**: https://adrianmonteslinares.com/ - **Purpose**: Personal developer portfolio showcasing skills, projects, and CV -- **Status**: Active development — Hero section complete, other sections marked "under construction" +- **Status**: Live — all landing sections shipped (hero, profile, projects, skills, journey, references, certifications, contact). Content is kept in sync with the latest CV in `public/cv/` (September 2026 revision). ### Current Goals -1. **Preserve UI/UX**: Keep the current landing experience identical (hero, navbar, language switch, scroll guard modal) -2. **Refactor Architecture**: Migrate from ad-hoc structure to clean, scalable frontend architecture -3. **Migrate i18n**: Replace custom i18n with `next-intl` for better maintainability -4. **Prepare for Growth**: Structure supports adding blog, more projects, sections, etc. +1. **Keep content truthful**: every claim on the site must match the latest CV (`public/cv/*_NTF.pdf`) and the Code-XR changelog; never expose private data (phone, date of birth, certificate numbers) +2. **Preserve UI/UX**: keep the landing experience stable while content evolves; every user-visible string lives in both `src/i18n/messages/en.json` and `es.json` +3. **Prepare for Growth**: structure supports adding blog, more projects, sections, etc. --- diff --git a/README.md b/README.md index 998d6b6..b543113 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# amontesl.github.io +# adrianmonteslinares.com Personal portfolio website for **Adrián Montes Linares**. @@ -53,7 +53,8 @@ npm run dev # Development server npm run lint # ESLint npm run typecheck # TypeScript strict check npm run check-translations # i18n coverage sanity check -npm run check # lint + typecheck + translations +npm run check # lint + typecheck + translations + journey timeline +npm run gen:og # Regenerate the social share card (run on Windows for Segoe UI) npm run build # Static export to ./out npm run preview # Serve ./out locally npm run ship "message" # Check, build, commit, and push current branch diff --git a/package.json b/package.json index ac62140..6d34145 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "ship": "node scripts/deploy-with-check.mjs", "gen:content": "node scripts/generate-project-images.mjs && node scripts/generate-marketplace-stats.mjs", "gen:images": "node scripts/generate-images.mjs", + "gen:og": "node scripts/generate-og.mjs", "translate": "node scripts/translate.mjs", "check-translations": "node scripts/check-translations.mjs" }, diff --git a/public/cv/CV_Color_Adrian_Montes_Linares_EN_NTF.pdf b/public/cv/CV_Color_Adrian_Montes_Linares_EN_NTF.pdf index ec19f66..1d49fe6 100644 Binary files a/public/cv/CV_Color_Adrian_Montes_Linares_EN_NTF.pdf and b/public/cv/CV_Color_Adrian_Montes_Linares_EN_NTF.pdf differ diff --git a/public/cv/CV_Color_Adrian_Montes_Linares_NTF.pdf b/public/cv/CV_Color_Adrian_Montes_Linares_NTF.pdf index f8d68b6..6fc275e 100644 Binary files a/public/cv/CV_Color_Adrian_Montes_Linares_NTF.pdf and b/public/cv/CV_Color_Adrian_Montes_Linares_NTF.pdf differ diff --git a/public/cv/README.md b/public/cv/README.md index c7e0a4a..19c317a 100644 --- a/public/cv/README.md +++ b/public/cv/README.md @@ -14,3 +14,4 @@ When ready, upload each PDF with the exact filename to enable download functiona - Directory: ✅ Created - Expected filenames: ✅ Defined for ES and EN - Links configured: ✅ Locale-based CV URLs ready +- Served revision: September 2026 (NTF = no phone number) — English C1 (Oxford Test of English Advanced), UPM master's in progress, SATEC Cloud & Systems N2 internship ongoing diff --git a/public/images/og/portfolio.png b/public/images/og/portfolio.png index 8c47caf..44fa186 100644 Binary files a/public/images/og/portfolio.png and b/public/images/og/portfolio.png differ diff --git a/scripts/check-journey-timeline.mjs b/scripts/check-journey-timeline.mjs index dd32463..b703bfd 100644 --- a/scripts/check-journey-timeline.mjs +++ b/scripts/check-journey-timeline.mjs @@ -119,10 +119,17 @@ const baseEntries = [ id: 'masterTelecomUPM', startYear: 2026, startMonth: 9, + startDay: 7, + endYear: null, + }, + { + id: 'oxfordC1', + startYear: 2026, + startMonth: 3, startDay: 1, endYear: 2026, endMonth: 9, - endDay: 1, + endDay: 4, }, ]; diff --git a/scripts/generate-og.mjs b/scripts/generate-og.mjs new file mode 100644 index 0000000..60af9b7 --- /dev/null +++ b/scripts/generate-og.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node +/** + * Regenerates the social share card public/images/og/portfolio.png (1200x630). + * + * The card is drawn as SVG from the site's design tokens (src/app/globals.css) and the + * profile photo is composited on top with sharp. Run it locally on Windows so the text + * renders in Segoe UI like the original card (`npm run gen:og`); a Linux runner would + * pick a different fallback font, which is why this is not part of the build. + */ +import { stat } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import sharp from 'sharp' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const OUTPUT = path.join(ROOT, 'public/images/og/portfolio.png') +const AVATAR_SOURCE = path.join(ROOT, 'public/images/profile/hero-320@2x.jpg') + +const WIDTH = 1200 +const HEIGHT = 630 +const TEXT_X = 88 +const AVATAR = { cx: 946, cy: 316, radius: 126, ring: 4 } +const FONT_FAMILY = "'Segoe UI', Inter, 'Helvetica Neue', Arial, sans-serif" + +// Design tokens (src/app/globals.css) +const COLORS = { + bg: '#040304', + fg: '#EFD2BC', + fgMuted: '#D2B6A1', + accent: '#DCA293', + copper: '#A66B57', +} + +const LINES = [ + { text: 'Adrián Montes Linares', y: 236, size: 64, weight: 700, fill: COLORS.fg }, + { text: 'Telematics Engineer', y: 298, size: 38, weight: 400, fill: COLORS.fgMuted }, + { text: "Master's student in Telecommunications Engineering", y: 342, size: 28, weight: 400, fill: COLORS.fgMuted }, + { text: 'Cloud & Systems N2 · Full-Stack · XR · Code-XR', y: 388, size: 28, weight: 400, fill: COLORS.accent }, + { text: 'Portfolio · adrianmonteslinares.com', y: 540, size: 28, weight: 400, fill: COLORS.fgMuted }, +] +const RULE = { y: 418, x1: TEXT_X, x2: 560 } + +function escapeXml(value) { + return value.replace(/&/g, '&').replace(//g, '>') +} + +function buildCardSvg() { + const texts = LINES.map( + (line) => + `${escapeXml(line.text)}`, + ).join('\n ') + const ringRadius = AVATAR.radius + AVATAR.ring / 2 + + return ` + + + + + + + + + + + + + + + ${texts} + + + +` +} + +async function buildAvatar() { + const size = AVATAR.radius * 2 + const mask = Buffer.from( + ``, + ) + + return sharp(AVATAR_SOURCE) + .resize(size, size, { fit: 'cover', position: 'centre' }) + .composite([{ input: mask, blend: 'dest-in' }]) + .png() + .toBuffer() +} + +/** Right edge of the lit pixels in a text band, to make sure nothing runs into the photo. */ +async function measureTextEdges() { + const { data, info } = await sharp(OUTPUT).raw().toBuffer({ resolveWithObject: true }) + // Left edge of the photo ring; pixels from there on belong to the avatar, not the text. + const limitX = AVATAR.cx - AVATAR.radius - AVATAR.ring - 4 + + const rightEdge = (y0, y1) => { + let max = 0 + for (let y = Math.max(0, y0); y < Math.min(info.height, y1); y += 1) { + for (let x = 0; x < limitX; x += 1) { + const i = (y * info.width + x) * info.channels + const luminance = (data[i] + data[i + 1] + data[i + 2]) / 3 + if (luminance > 70 && x > max) max = x + } + } + return max + } + + return LINES.map((line) => { + const edge = rightEdge(line.y - line.size, line.y + 10) + return { text: line.text, rightEdge: edge, fits: edge < limitX - 24 } + }) +} + +async function main() { + const avatar = await buildAvatar() + + // sharp rasterises SVG at 72 dpi, so the 1200x630 viewBox maps 1:1 to pixels. + await sharp(Buffer.from(buildCardSvg())) + .composite([{ input: avatar, left: AVATAR.cx - AVATAR.radius, top: AVATAR.cy - AVATAR.radius }]) + .png({ compressionLevel: 9 }) + .toFile(OUTPUT) + + const [metadata, fileStat, edges] = await Promise.all([sharp(OUTPUT).metadata(), stat(OUTPUT), measureTextEdges()]) + console.log(`og: wrote ${path.relative(ROOT, OUTPUT)} ${metadata.width}x${metadata.height} (${Math.round(fileStat.size / 1024)} KB)`) + for (const edge of edges) { + console.log(`og: ${edge.fits ? 'ok ' : 'WIDE'} right edge x=${edge.rightEdge} — ${edge.text}`) + } + if (edges.some((edge) => !edge.fits)) { + process.exitCode = 1 + } +} + +await main() diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d743c69..d433964 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,7 +10,7 @@ export const metadata: Metadata = { template: `%s | ${SITE.author}`, }, description: 'Portfolio of Adrián Montes Linares, Telematics & Software Engineer focused on TypeScript, React, Node.js, DevTools and XR.', - keywords: ['Adrián Montes Linares', 'Adrián Montes', 'Telematics Engineer', 'Software Engineer', 'React', 'TypeScript', 'Node.js', 'XR', 'WebXR', 'Code-XR', 'VISSOFT', 'ICSME 2025', 'Portfolio'], + keywords: ['Adrián Montes Linares', 'Adrián Montes', 'Telematics Engineer', 'Software Engineer', 'React', 'TypeScript', 'Node.js', 'XR', 'WebXR', 'Code-XR', 'VISSOFT', 'ICSME 2025', 'Universidad Politécnica de Madrid', 'UPM', 'Machine Learning', 'Big Data', 'Cloud', 'Systems N2', 'Model Context Protocol', 'MCP', 'Oxford Test of English C1', 'Portfolio'], authors: [{ name: SITE.author }], creator: SITE.author, manifest: '/favicons/site.webmanifest', diff --git a/src/components/common/Footer.tsx b/src/components/common/Footer.tsx index bb34c60..cf3fbaa 100644 --- a/src/components/common/Footer.tsx +++ b/src/components/common/Footer.tsx @@ -46,7 +46,7 @@ export function Footer() {
-

{SITE.author}

+

{SITE.author}

{t('brand')}

@@ -129,7 +129,7 @@ export function Footer() {

- © {currentYear} {SITE.author}. {t('copyright')} + © {currentYear} {SITE.author}. {t('copyright')}

{t('by')} diff --git a/src/components/common/LocalizedShell.tsx b/src/components/common/LocalizedShell.tsx index 441b6db..f317ff8 100644 --- a/src/components/common/LocalizedShell.tsx +++ b/src/components/common/LocalizedShell.tsx @@ -2,8 +2,8 @@ import type { ReactNode } from 'react' import { Footer } from '@/components/common/Footer' -import { I18nProvider, type Locale } from '@/i18n' -import { LocalePreferenceGate } from '@/features/language' +import { I18nProvider, useTranslations, type Locale } from '@/i18n' +import { BrowserTranslateHint, LocalePreferenceGate } from '@/features/language' export interface LocalizedShellProps { children: ReactNode @@ -11,17 +11,26 @@ export interface LocalizedShellProps { showFooter?: boolean } +function SkipToContentLink() { + const t = useTranslations('language') + + return ( + + {t('skipToContent')} + + ) +} + export function LocalizedShell({ children, locale, showFooter = true }: LocalizedShellProps) { return ( - - Skip to main content - +

{children}
{showFooter &&
} + ) } diff --git a/src/content/certifications.ts b/src/content/certifications.ts index 1f191f5..d22d1d8 100644 --- a/src/content/certifications.ts +++ b/src/content/certifications.ts @@ -3,6 +3,8 @@ * Professional certifications, awards, and achievements */ +import { LINKS } from '@/lib/constants' + export interface Certification { id: string name: string // i18n key for name @@ -16,6 +18,16 @@ export interface Certification { } export const CERTIFICATIONS: Certification[] = [ + { + id: 'oxford-c1', + name: 'oxfordC1', + issuer: 'oxfordC1Issuer', + date: '2026-09-04', + link: LINKS.oxfordC1Verify, + linkType: 'external', + tags: ['English', 'CEFR C1', 'Oxford'], + status: 'completed', + }, { id: 'telematics-degree', name: 'telematicsDegree', @@ -54,20 +66,12 @@ export const CERTIFICATIONS: Certification[] = [ tags: ['Kotlin', 'Android', 'Maps', 'GPX'], status: 'completed', }, - { - id: 'cambridge-c1', - name: 'cambridgeC1', - issuer: 'Cambridge University', - date: '2026-Q2', - tags: ['English', 'Professional Development'], - status: 'in-progress', - }, { id: 'master-telecom-upm', name: 'masterTelecomUPM', issuer: 'masterTelecomUPMIssuer', date: '2026-09', - tags: ['Telecommunications', 'UPM', 'Systems'], - status: 'planned', + tags: ['UPM', 'Telecommunications', 'Machine Learning', 'Big Data'], + status: 'in-progress', }, ] diff --git a/src/content/journey.ts b/src/content/journey.ts index a54b10e..eb378b0 100644 --- a/src/content/journey.ts +++ b/src/content/journey.ts @@ -1,4 +1,5 @@ import { JourneyEntry, LaneConfig } from '@/features/journey/types' +import { LINKS } from '@/lib/constants' /** * Lane configuration - defines colors for each track @@ -57,6 +58,20 @@ export const JOURNEY_ENTRIES: JourneyEntry[] = [ ], tags: ['Software Engineering', 'Networks', 'XR'], }, + { + id: 'masterTelecomUPM', + type: 'education', + lane: 'education', + roleKey: 'journey.entries.masterTelecomUPM.role', + orgKey: 'journey.entries.masterTelecomUPM.org', + descKey: 'journey.entries.masterTelecomUPM.desc', + startYear: 2026, + startMonth: 9, + startDay: 7, + endYear: null, + tags: ['Telecommunications', 'Machine Learning', 'Big Data', 'UPM'], + link: LINKS.upm, + }, // === WORK (Lane 2) === { @@ -131,6 +146,22 @@ export const JOURNEY_ENTRIES: JourneyEntry[] = [ }, // === ACHIEVEMENTS (Lane 4 - Discrete events) === + { + id: 'hackathonUrjc', + type: 'achievement', + lane: 'achievement', + roleKey: 'journey.entries.hackathonUrjc.role', + orgKey: 'journey.entries.hackathonUrjc.org', + descKey: 'journey.entries.hackathonUrjc.desc', + startYear: 2024, + startMonth: 3, + startDay: 1, + endYear: 2024, + endMonth: 3, + endDay: 1, + tags: ['Hackathon', 'Circular economy', 'Pitch'], + link: LINKS.hackathonUrjc, + }, { id: 'vissoft', type: 'achievement', @@ -147,34 +178,22 @@ export const JOURNEY_ENTRIES: JourneyEntry[] = [ link: 'https://vissoft.info', }, - // === LEARNING GOALS (Lane 5 - Future objectives) === + // === LEARNING (Lane 5 - Certifications and learning goals) === { - id: 'cambridgeC1', + id: 'oxfordC1', type: 'learning', lane: 'learning', - roleKey: 'journey.entries.cambridgeC1.role', - orgKey: 'journey.entries.cambridgeC1.org', - descKey: 'journey.entries.cambridgeC1.desc', + roleKey: 'journey.entries.oxfordC1.role', + orgKey: 'journey.entries.oxfordC1.org', + descKey: 'journey.entries.oxfordC1.desc', startYear: 2026, startMonth: 3, startDay: 1, - endYear: null, - tags: ['English', 'Certification', 'Professional Development'], - }, - { - id: 'masterTelecomUPM', - type: 'learning', - lane: 'learning', - roleKey: 'journey.entries.masterTelecomUPM.role', - orgKey: 'journey.entries.masterTelecomUPM.org', - descKey: 'journey.entries.masterTelecomUPM.desc', - startYear: 2026, - startMonth: 9, - startDay: 1, endYear: 2026, endMonth: 9, - endDay: 1, - tags: ['Telecommunications', 'Networks', 'Cloud', 'Systems'], + endDay: 4, + tags: ['English', 'CEFR C1', 'Oxford Test of English'], + link: LINKS.oxfordC1Verify, }, ] @@ -183,8 +202,8 @@ export const TIMELINE_START = 2020 export const TIMELINE_END = 2026 export const CURRENT_DATE = { year: 2026, - month: 8, - day: 29, + month: 9, + day: 7, } as const /** Convert a date to a decimal year (for precise positioning) */ diff --git a/src/content/marketplaceStats.generated.ts b/src/content/marketplaceStats.generated.ts index 424012e..7202ba1 100644 --- a/src/content/marketplaceStats.generated.ts +++ b/src/content/marketplaceStats.generated.ts @@ -3,9 +3,9 @@ export const marketplaceStats = { "codeXr": { - "downloadCount": 727, + "downloadCount": 735, "displayDownloads": "700+", - "updatedAt": "2026-08-29T13:25:10.641Z", + "updatedAt": "2026-09-06T18:40:32.417Z", "sourceUrl": "https://marketplace.visualstudio.com/items?itemName=aMonteSl.code-xr" } } as const diff --git a/src/content/marketplaceStats.json b/src/content/marketplaceStats.json index ae40df7..6a9ea73 100644 --- a/src/content/marketplaceStats.json +++ b/src/content/marketplaceStats.json @@ -1,8 +1,8 @@ { "codeXr": { - "downloadCount": 727, + "downloadCount": 735, "displayDownloads": "700+", - "updatedAt": "2026-08-29T13:25:10.641Z", + "updatedAt": "2026-09-06T18:40:32.417Z", "sourceUrl": "https://marketplace.visualstudio.com/items?itemName=aMonteSl.code-xr" } } diff --git a/src/content/projects.json b/src/content/projects.json index cb8b96f..d8dd389 100644 --- a/src/content/projects.json +++ b/src/content/projects.json @@ -38,12 +38,12 @@ "highlights_en": [ "Problem -> Static dashboards make it difficult to understand large codebases, compare software metrics, and explain structural hotspots during review or research work.", "Solution -> A VS Code workflow that analyses the code live, projects metrics into an XR scene, and supports shared rooms, virtual screens, and safer chart remapping from inside the immersive workspace.", - "Impact -> Accepted at VISSOFT @ ICSME 2025, published on the VS Code Marketplace, and maintained as an open-source developer tool with 300+ downloads." + "Impact -> Paper published at IEEE VISSOFT 2025 (co-located with ICSME 2025) and recognized with the Distinguished Artifact Award; the extension is available on the VS Code Marketplace and maintained as an open-source developer tool." ], "highlights_es": [ "Problema -> Los dashboards estáticos dificultan entender bases de código grandes, comparar métricas de software y explicar hotspots estructurales durante revisión o investigación.", "Solución -> Un flujo integrado en VS Code que analiza el código en vivo, proyecta métricas en una escena XR y permite salas compartidas, pantallas virtuales y remapeo seguro de gráficos desde el propio entorno inmersivo.", - "Impacto -> Aceptado en VISSOFT @ ICSME 2025, publicado en VS Code Marketplace y mantenido como herramienta open source para desarrolladores con más de 300 descargas." + "Impacto -> Artículo publicado en IEEE VISSOFT 2025 (junto a ICSME 2025) y reconocido con el Distinguished Artifact Award; la extensión está disponible en VS Code Marketplace y se mantiene como herramienta open source para desarrolladores." ], "role_en": "Sole author and end-to-end developer. Built the VS Code extension, Python analysis pipeline, JSON persistence layer, XR scene generation with A-Frame/BabiaXR, marketplace packaging, and documentation site. Version 1.1.0 focused on performance, shared rooms, virtual-screen controls, and safer metric remapping. Version 1.2.0 added the per-language dependency adapters, safe Git snapshot materialization that never runs checkout or fetch, the Cloudflare tunnel and six-digit pairing flow behind cross-network sessions, and the in-room user guide. Academic supervision by David Moreno Lumbreras (PhD, TFG supervisor).", "role_es": "Autor único y desarrollador end-to-end. Construí la extensión de VS Code, el pipeline de análisis en Python, la persistencia en JSON, la generación de escenas XR con A-Frame/BabiaXR, el empaquetado para Marketplace y la web de documentación. La versión 1.1.0 se centró en rendimiento, salas compartidas, controles de pantallas virtuales y remapeo seguro de métricas. La versión 1.2.0 añadió los adaptadores de dependencias por lenguaje, la materialización segura de snapshots de Git sin ejecutar checkout ni fetch, el túnel de Cloudflare y el emparejamiento por código de seis dígitos detrás de las sesiones entre redes, y la guía de uso dentro de la escena. Supervisión académica de David Moreno Lumbreras (PhD, supervisor del TFG).", @@ -244,13 +244,13 @@ { "slug": "obt", "title": "OBT — Online Booking Tool", - "period": "Nov 2025–Feb 2026", + "period": "Nov 2025–Jan 2026", "featured": true, "type": "internal", "summary_en": "Internal SaaS platform developed during my VBGroup internship to manage travel booking requests for client companies. The system supports request intake, agent-managed cost estimates, approval flows, reservation processing, automated email notifications, and role-based access across five user tiers.", "summary_es": "Plataforma SaaS interna desarrollada durante mis prácticas en VBGroup para gestionar solicitudes de reserva de viajes de empresas cliente. El sistema cubre la recepción de solicitudes, presupuestos gestionados por agentes, flujos de aprobación, procesamiento de reservas, notificaciones automáticas por email y control de acceso por roles en cinco niveles de usuario.", "tags": [ - "Nest.js", + "Express.js", "TypeScript", "React", "Azure CosmosDB", @@ -271,10 +271,10 @@ "Implementé acciones interactivas por email aseguradas con JWT para aprobaciones de presupuestos, notificaciones de viaje y confirmaciones de reserva.", "Empecé con backend y diseño de base de datos, y después amplié mi contribución al frontend React, gestión de estado, flujos de reserva e integración con APIs REST." ], - "role_en": "Initially focused on backend architecture and development, then evolved into a full-stack role as the platform grew. Designed REST API architecture with Nest.js, TypeScript, and Swagger; implemented JWT-based email security, RBAC middleware, and MVC patterns; and structured Cosmos DB collections and Azure Blob Storage containers for travel data, cost estimates, and user documents. Later contributed to the React + Vite frontend, multi-step booking workflows, state management, Tailwind UI implementation, and API integration.", - "role_es": "Inicialmente me enfoqué en arquitectura y desarrollo backend, y después evolucioné hacia un rol full-stack a medida que la plataforma creció. Diseñé la arquitectura de API REST con Nest.js, TypeScript y Swagger; implementé seguridad en emails basada en JWT, middleware RBAC y patrones MVC; y estructuré colecciones de Cosmos DB y contenedores de Azure Blob Storage para datos de viajes, presupuestos y documentos de usuario. Posteriormente contribuí al frontend React + Vite, flujos de reserva multipaso, gestión de estado, implementación UI con Tailwind e integración con APIs.", + "role_en": "Initially focused on backend architecture and development, then evolved into a full-stack role as the platform grew. Designed REST API architecture with Express.js, TypeScript, and Swagger; implemented JWT-based email security, RBAC middleware, and MVC patterns; and structured Cosmos DB collections and Azure Blob Storage containers for travel data, cost estimates, and user documents. Later contributed to the React + Vite frontend, multi-step booking workflows, state management, Tailwind UI implementation, and API integration.", + "role_es": "Inicialmente me enfoqué en arquitectura y desarrollo backend, y después evolucioné hacia un rol full-stack a medida que la plataforma creció. Diseñé la arquitectura de API REST con Express.js, TypeScript y Swagger; implementé seguridad en emails basada en JWT, middleware RBAC y patrones MVC; y estructuré colecciones de Cosmos DB y contenedores de Azure Blob Storage para datos de viajes, presupuestos y documentos de usuario. Posteriormente contribuí al frontend React + Vite, flujos de reserva multipaso, gestión de estado, implementación UI con Tailwind e integración con APIs.", "tech": [ - "Nest.js", + "Express.js", "TypeScript", "Swagger", "TSDoc", @@ -388,12 +388,12 @@ }, { "slug": "honor-course-2", - "title": "StepByStep — LSMO High Distinction", + "title": "StepByStep — LSMU High Distinction", "period": "May 2025", "featured": true, "type": "academic", - "summary_en": "Android/Kotlin walking-route app developed as the final project for LSMO. StepByStep records routes in real time, imports GPX files, persists route data, calculates distance, time, elevation and gain metrics, and presents profile statistics with light and dark themes.", - "summary_es": "App Android/Kotlin para rutas andando desarrollada como proyecto final de LSMO. StepByStep graba rutas en tiempo real, importa ficheros GPX, persiste datos de rutas, calcula métricas de distancia, tiempo, altitud y desnivel, y muestra estadísticas de perfil con tema claro y oscuro.", + "summary_en": "Android/Kotlin walking-route app developed as the final project for LSMU. StepByStep records routes in real time, imports GPX files, persists route data, calculates distance, time, elevation and gain metrics, and presents profile statistics with light and dark themes.", + "summary_es": "App Android/Kotlin para rutas andando desarrollada como proyecto final de LSMU. StepByStep graba rutas en tiempo real, importa ficheros GPX, persiste datos de rutas, calcula métricas de distancia, tiempo, altitud y desnivel, y muestra estadísticas de perfil con tema claro y oscuro.", "tags": [ "Kotlin", "Android", @@ -425,8 +425,8 @@ "Flujo de importación GPX con parser XML propio, cálculo de estadísticas de ruta y persistencia Room para rutas y puntos.", "Interfaz móvil con listado de rutas, mapa de grabación en vivo, estadísticas de perfil, temas claro/oscuro y estilo de mapa." ], - "role_en": "Android project developer for the final LSMO assignment. Designed and implemented the Kotlin app structure, Room entities and repositories, GPX parser, route recording service, Google Maps integration, metrics calculation, profile statistics, theme support, and screen flows for listing, importing, recording, saving, and reviewing walking routes.", - "role_es": "Desarrollador Android del proyecto final de LSMO. Diseñé e implementé la estructura de la app en Kotlin, entidades y repositorios Room, parser GPX, servicio de grabación de rutas, integración con Google Maps, cálculo de métricas, estadísticas de perfil, soporte de temas y flujos de pantalla para listar, importar, grabar, guardar y revisar rutas andando.", + "role_en": "Android project developer for the final LSMU assignment. Designed and implemented the Kotlin app structure, Room entities and repositories, GPX parser, route recording service, Google Maps integration, metrics calculation, profile statistics, theme support, and screen flows for listing, importing, recording, saving, and reviewing walking routes.", + "role_es": "Desarrollador Android del proyecto final de LSMU. Diseñé e implementé la estructura de la app en Kotlin, entidades y repositorios Room, parser GPX, servicio de grabación de rutas, integración con Google Maps, cálculo de métricas, estadísticas de perfil, soporte de temas y flujos de pantalla para listar, importar, grabar, guardar y revisar rutas andando.", "tech": [ "Kotlin", "Android", @@ -439,12 +439,12 @@ "MVVM", "XML Layouts" ], - "title_en": "StepByStep — LSMO High Distinction", - "title_es": "StepByStep — Matrícula de Honor en LSMO", + "title_en": "StepByStep — LSMU High Distinction", + "title_es": "StepByStep — Matrícula de Honor en LSMU", "status_en": "High Distinction", "status_es": "Matrícula de Honor", - "detailSummary_en": "StepByStep was my final project for LSMO, a mobile and ubiquitous systems course focused on Android development. Unlike AST, this was not a project grown through many separate mini-projects: the goal was to deliver a complete mobile application for walking routes, from route creation and GPX import to live tracking, metrics, persistence, profile statistics, and theme support.\nThe core of the app is real-time route recording. A ForegroundService keeps tracking active while the app can move to the background, using FusedLocationProviderClient for high-accuracy updates, a persistent notification so the user can return to the active route, START_STICKY behavior, and a PARTIAL_WAKE_LOCK to keep the CPU available during recording. The service calculates distance from consecutive location points, elapsed time, current elevation, and positive elevation gain, then broadcasts updates to the recording screen.\nThe project also includes GPX import through a custom parser that reads track points, elevation and timestamps, calculates route statistics, and stores the result with Room. Routes and points are modeled as separate entities with cascade behavior, allowing the app to list recorded/imported routes, show profile-level statistics, and persist the walking history. Visually, the app combines Google Maps, a dark map style, light/dark Android themes, and dedicated screens for route creation, importing, profile metrics, saving, and route detail review.", - "detailSummary_es": "StepByStep fue mi proyecto final de LSMO, una asignatura de sistemas móviles y ubicuos centrada en desarrollo Android. A diferencia de AST, no fue un proyecto que creciera a partir de muchos mini-proyectos separados: el objetivo era entregar una aplicación móvil completa para rutas andando, desde creación e importación GPX hasta seguimiento en vivo, métricas, persistencia, estadísticas de perfil y soporte de temas.\nEl núcleo de la app es la grabación de rutas en tiempo real. Un ForegroundService mantiene el seguimiento activo aunque la app pase a segundo plano, usando FusedLocationProviderClient para actualizaciones de alta precisión, una notificación persistente para volver a la ruta activa, comportamiento START_STICKY y un PARTIAL_WAKE_LOCK para mantener la CPU disponible durante la grabación. El servicio calcula distancia a partir de puntos consecutivos, tiempo transcurrido, altitud actual y desnivel positivo, y envía actualizaciones a la pantalla de grabación mediante broadcasts.\nEl proyecto también incluye importación GPX mediante un parser propio que lee puntos de track, altitud y timestamps, calcula estadísticas de ruta y guarda el resultado con Room. Las rutas y puntos están modelados como entidades separadas con borrado en cascada, permitiendo listar rutas grabadas/importadas, mostrar estadísticas globales de perfil y persistir el historial de caminatas. Visualmente, la app combina Google Maps, estilo oscuro de mapa, temas Android claro/oscuro y pantallas específicas para creación de ruta, importación, métricas de perfil, guardado y revisión de detalle.", + "detailSummary_en": "StepByStep was my final project for LSMU (Laboratorio de Sistemas Móviles y Ubicuos), a mobile and ubiquitous systems course focused on Android development. Unlike AST, this was not a project grown through many separate mini-projects: the goal was to deliver a complete mobile application for walking routes, from route creation and GPX import to live tracking, metrics, persistence, profile statistics, and theme support.\nThe core of the app is real-time route recording. A ForegroundService keeps tracking active while the app can move to the background, using FusedLocationProviderClient for high-accuracy updates, a persistent notification so the user can return to the active route, START_STICKY behavior, and a PARTIAL_WAKE_LOCK to keep the CPU available during recording. The service calculates distance from consecutive location points, elapsed time, current elevation, and positive elevation gain, then broadcasts updates to the recording screen.\nThe project also includes GPX import through a custom parser that reads track points, elevation and timestamps, calculates route statistics, and stores the result with Room. Routes and points are modeled as separate entities with cascade behavior, allowing the app to list recorded/imported routes, show profile-level statistics, and persist the walking history. Visually, the app combines Google Maps, a dark map style, light/dark Android themes, and dedicated screens for route creation, importing, profile metrics, saving, and route detail review.", + "detailSummary_es": "StepByStep fue mi proyecto final de LSMU (Laboratorio de Sistemas Móviles y Ubicuos), una asignatura centrada en desarrollo Android. A diferencia de AST, no fue un proyecto que creciera a partir de muchos mini-proyectos separados: el objetivo era entregar una aplicación móvil completa para rutas andando, desde creación e importación GPX hasta seguimiento en vivo, métricas, persistencia, estadísticas de perfil y soporte de temas.\nEl núcleo de la app es la grabación de rutas en tiempo real. Un ForegroundService mantiene el seguimiento activo aunque la app pase a segundo plano, usando FusedLocationProviderClient para actualizaciones de alta precisión, una notificación persistente para volver a la ruta activa, comportamiento START_STICKY y un PARTIAL_WAKE_LOCK para mantener la CPU disponible durante la grabación. El servicio calcula distancia a partir de puntos consecutivos, tiempo transcurrido, altitud actual y desnivel positivo, y envía actualizaciones a la pantalla de grabación mediante broadcasts.\nEl proyecto también incluye importación GPX mediante un parser propio que lee puntos de track, altitud y timestamps, calcula estadísticas de ruta y guarda el resultado con Room. Las rutas y puntos están modelados como entidades separadas con borrado en cascada, permitiendo listar rutas grabadas/importadas, mostrar estadísticas globales de perfil y persistir el historial de caminatas. Visualmente, la app combina Google Maps, estilo oscuro de mapa, temas Android claro/oscuro y pantallas específicas para creación de ruta, importación, métricas de perfil, guardado y revisión de detalle.", "milestones": [ { "version": "BRIEF", @@ -480,8 +480,8 @@ "version": "FINAL", "date_en": "May 2025", "date_es": "Mayo de 2025", - "title_en": "High Distinction in LSMO", - "title_es": "Matrícula de Honor en LSMO", + "title_en": "High Distinction in LSMU", + "title_es": "Matrícula de Honor en LSMU", "description_en": "Final delivery with route recording, import, metrics, profile statistics, themes, and map-based screens.", "description_es": "Entrega final con grabación de rutas, importación, métricas, estadísticas de perfil, temas y pantallas basadas en mapa.", "status": "awarded" diff --git a/src/content/skills.ts b/src/content/skills.ts index c7de55a..5cec198 100644 --- a/src/content/skills.ts +++ b/src/content/skills.ts @@ -60,6 +60,8 @@ | 'agileScrum' | 'mvvm' | 'githubCopilot' + | 'claudeCode' + | 'mcp' export type CategoryId = | 'languages' @@ -127,7 +129,9 @@ export type SkillIconKey = | 'security' | 'ai' | 'azuredevops' - | 'jira' + | 'jira' + | 'claude' + | 'mcp' export interface SkillUsedIn { id: string @@ -331,9 +335,9 @@ export const SKILLS: Record = { id: 'express', labelKey: 'items.express.label', iconKey: 'express', - experienceTags: ['regular_use', 'open_source'], + experienceTags: ['internship', 'regular_use'], proficiency: 'intermediate', - purposeTag: 'open_source', + purposeTag: 'internship', purposeKey: 'items.express.purpose', summaryKey: 'items.express.summary', highlightsKeys: ['items.express.h1', 'items.express.h2'], @@ -343,13 +347,13 @@ export const SKILLS: Record = { id: 'nestjs', labelKey: 'items.nestjs.label', iconKey: 'nestjs', - experienceTags: ['internship', 'production_like'], + experienceTags: ['regular_use'], proficiency: 'intermediate', - purposeTag: 'internship', + purposeTag: 'learning', purposeKey: 'items.nestjs.purpose', summaryKey: 'items.nestjs.summary', highlightsKeys: ['items.nestjs.h1', 'items.nestjs.h2'], - usedIn: [USED_IN.projects], + usedIn: [], }, swagger: { id: 'swagger', @@ -883,7 +887,31 @@ export const SKILLS: Record = { summaryKey: 'items.githubCopilot.summary', highlightsKeys: ['items.githubCopilot.h1', 'items.githubCopilot.h2'], usedIn: [USED_IN.projects], - } + }, + claudeCode: { + id: 'claudeCode', + labelKey: 'items.claudeCode.label', + iconKey: 'claude', + experienceTags: ['regular_use'], + proficiency: 'intermediate', + purposeTag: 'production_like', + purposeKey: 'items.claudeCode.purpose', + summaryKey: 'items.claudeCode.summary', + highlightsKeys: ['items.claudeCode.h1', 'items.claudeCode.h2'], + usedIn: [USED_IN.projects], + }, + mcp: { + id: 'mcp', + labelKey: 'items.mcp.label', + iconKey: 'mcp', + experienceTags: ['regular_use'], + proficiency: 'intermediate', + purposeTag: 'production_like', + purposeKey: 'items.mcp.purpose', + summaryKey: 'items.mcp.summary', + highlightsKeys: ['items.mcp.h1', 'items.mcp.h2'], + usedIn: [USED_IN.projects], + }, } export const SKILL_CATEGORIES: SkillCategory[] = [ @@ -933,7 +961,7 @@ export const SKILL_CATEGORIES: SkillCategory[] = [ id: 'other', titleKey: 'categories.other.label', descriptionKey: 'categories.other.desc', - skills: ['promptEngineering', 'aiModels', 'githubCopilot'], + skills: ['promptEngineering', 'aiModels', 'githubCopilot', 'claudeCode', 'mcp'], } ] diff --git a/src/content/testimonials.ts b/src/content/testimonials.ts index caffeb9..f8b46fd 100644 --- a/src/content/testimonials.ts +++ b/src/content/testimonials.ts @@ -6,7 +6,7 @@ export type RecommendationType = 'academic' | 'professional' | 'formal' export interface Recommendation { - id: 'vbgroup-abner' | 'david-moreno' | 'vbgroup-formal' + id: 'vbgroup-abner' | 'david-moreno' | 'vbgroup-formal' | 'satec-luis' name: string roleKey: string organization: string @@ -18,6 +18,22 @@ export interface Recommendation { } export const RECOMMENDATIONS: Recommendation[] = [ + { + id: 'satec-luis', + name: 'Luis del Otero Sevillano', + roleKey: 'recommendations.satec-luis.role', + organization: 'SATEC', + relationshipKey: 'recommendations.satec-luis.relationship', + date: 'Jul 2026', + type: 'professional', + summaryKey: 'recommendations.satec-luis.summary', + strengthKeys: [ + 'recommendations.satec-luis.strengths.learning', + 'recommendations.satec-luis.strengths.teamwork', + 'recommendations.satec-luis.strengths.adaptability', + 'recommendations.satec-luis.strengths.attitude', + ], + }, { id: 'vbgroup-abner', name: 'Abner Alejandro Magaña H.', diff --git a/src/content/uses.ts b/src/content/uses.ts index 4da5d68..29bd885 100644 --- a/src/content/uses.ts +++ b/src/content/uses.ts @@ -11,6 +11,7 @@ export const USES_ITEMS: UsesItem[] = [ // Editor & IDE { id: 'vscode', category: 'editor', url: 'https://code.visualstudio.com' }, { id: 'copilot', category: 'editor', url: 'https://github.com/features/copilot' }, + { id: 'claudeCode', category: 'editor', url: 'https://claude.com/product/claude-code' }, // Terminal & Shell { id: 'windowsTerminal', category: 'terminal', url: 'https://github.com/microsoft/terminal' }, diff --git a/src/features/certifications/CertificationsSection.tsx b/src/features/certifications/CertificationsSection.tsx index 9174bf1..5ac7683 100644 --- a/src/features/certifications/CertificationsSection.tsx +++ b/src/features/certifications/CertificationsSection.tsx @@ -38,7 +38,14 @@ function getTagIcon(tag: string): IconType { const normalized = tag.toLowerCase() if (normalized.includes('urjc') || normalized.includes('upm')) return FaUniversity - if (normalized.includes('english') || normalized.includes('professional development')) return FaLanguage + if ( + normalized.includes('english') || + normalized.includes('cefr') || + normalized.includes('oxford') || + normalized.includes('professional development') + ) { + return FaLanguage + } if (normalized.includes('vissoft') || normalized.includes('icsme') || normalized.includes('code-xr')) return FaAward const TechIcon = getTechIcon(tag) @@ -83,7 +90,7 @@ export function CertificationsSection() {
- + {statusOrder - .filter((status) => status !== 'completed') + .filter((status) => status !== 'completed' && groupedByStatus[status].length > 0) .map((status) => ( ))} @@ -171,7 +178,8 @@ function CertificationCard({ const config = statusConfig[cert.status] const translatableCertKeys = [ - 'cambridgeC1', + 'oxfordC1', + 'oxfordC1Issuer', 'masterTelecomUPM', 'masterTelecomUPMIssuer', 'telematicsDegree', diff --git a/src/features/journey/ParallelStreamsSection.tsx b/src/features/journey/ParallelStreamsSection.tsx index ccd21a8..6ea80d8 100644 --- a/src/features/journey/ParallelStreamsSection.tsx +++ b/src/features/journey/ParallelStreamsSection.tsx @@ -1421,6 +1421,47 @@ export function ParallelStreamsSection() { }) )} + {/* Standalone achievements (hackathon, VISSOFT, ...) that fall in this year */} + {JOURNEY_ENTRIES.filter((entry) => entry.lane === 'achievement' && entry.startYear === drillDownYear).map((entry) => { + const percent = monthToPercent(entry.startMonth ?? 1, entry.startDay ?? 1) + const glowIntensity = getIntensity(entry.id) + const haloColor = LANE_COLORS.achievement.hex + + const haloStyle = glowIntensity > 0 ? { + boxShadow: `0 0 ${20 * glowIntensity}px ${haloColor}`, + filter: `brightness(${1 + 0.4 * glowIntensity})`, + } : { + boxShadow: `0 0 14px rgba(245, 158, 11, 0.3)` + } + + return ( + handleEntryHoverStart(entry.id)} + onPointerMove={() => handleEntryHoverStart(entry.id)} + onPointerLeave={() => handleEntryHoverEnd(entry.id)} + onClick={(event) => handleEntryClick(entry.id, event)} + title={t(`entries.${entry.id}.role`)} + aria-label={t(`entries.${entry.id}.role`)} + /> + ) + })} +
diff --git a/src/features/landing/Hero.tsx b/src/features/landing/Hero.tsx index aa77c63..1dd64af 100644 --- a/src/features/landing/Hero.tsx +++ b/src/features/landing/Hero.tsx @@ -102,6 +102,7 @@ export function Hero() { {t('name')} diff --git a/src/features/language/BrowserTranslateHint.tsx b/src/features/language/BrowserTranslateHint.tsx new file mode 100644 index 0000000..6f03adf --- /dev/null +++ b/src/features/language/BrowserTranslateHint.tsx @@ -0,0 +1,64 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useTranslations } from '@/i18n' +import { getBrowserLanguages, hasSupportedLanguage } from './negotiate' + +export const BROWSER_TRANSLATE_HINT_DISMISSED_KEY = 'browser-translate-hint-dismissed' + +/** + * One-line note for visitors whose browser languages include neither English nor Spanish: + * the site is written in those two languages and their browser can translate it. + * Rendered only after mount, so server and client markup never differ. + */ +export function BrowserTranslateHint() { + const t = useTranslations('language') + const [visible, setVisible] = useState(false) + + useEffect(() => { + try { + if (window.localStorage.getItem(BROWSER_TRANSLATE_HINT_DISMISSED_KEY)) { + return + } + } catch { + // Storage unavailable: still show the hint once for this page view. + } + + const languages = getBrowserLanguages() + if (languages.length === 0 || hasSupportedLanguage(languages)) { + return + } + + setVisible(true) + }, []) + + if (!visible) { + return null + } + + const dismiss = () => { + setVisible(false) + try { + window.localStorage.setItem(BROWSER_TRANSLATE_HINT_DISMISSED_KEY, '1') + } catch { + // Ignore: the note simply reappears on the next visit. + } + } + + return ( +
+

{t('browserHint')}

+ +
+ ) +} diff --git a/src/features/language/LocalePreferenceGate.tsx b/src/features/language/LocalePreferenceGate.tsx index 5704280..ae4a774 100644 --- a/src/features/language/LocalePreferenceGate.tsx +++ b/src/features/language/LocalePreferenceGate.tsx @@ -1,7 +1,8 @@ 'use client' import { useEffect } from 'react' -import { useLocale, localizePath, type Locale } from '@/i18n' +import { useLocale, localizePath } from '@/i18n' +import { getBrowserLanguages, negotiateLocale } from './negotiate' export const PREFERRED_LOCALE_STORAGE_KEY = 'preferred-locale' @@ -9,13 +10,6 @@ function isRootPath(pathname: string): boolean { return pathname === '/' || pathname === '/index.html' } -function getPreferredBrowserLocale(): Locale { - const languages = navigator.languages?.length ? navigator.languages : [navigator.language] - const primaryLanguage = languages.find(Boolean)?.toLowerCase() ?? '' - - return primaryLanguage.startsWith('es') ? 'es' : 'en' -} - export function LocalePreferenceGate() { const { locale } = useLocale() @@ -33,7 +27,7 @@ export function LocalePreferenceGate() { return } - const preferredLocale = getPreferredBrowserLocale() + const preferredLocale = negotiateLocale(getBrowserLanguages()) if (preferredLocale === locale) { return } diff --git a/src/features/language/index.ts b/src/features/language/index.ts index 1fd69ce..4d80a43 100644 --- a/src/features/language/index.ts +++ b/src/features/language/index.ts @@ -1,2 +1,4 @@ export { LanguageSwitcher } from './LanguageSwitcher' export { LocalePreferenceGate, PREFERRED_LOCALE_STORAGE_KEY } from './LocalePreferenceGate' +export { BrowserTranslateHint, BROWSER_TRANSLATE_HINT_DISMISSED_KEY } from './BrowserTranslateHint' +export { getBrowserLanguages, hasSupportedLanguage, negotiateLocale } from './negotiate' diff --git a/src/features/language/negotiate.ts b/src/features/language/negotiate.ts new file mode 100644 index 0000000..ab27198 --- /dev/null +++ b/src/features/language/negotiate.ts @@ -0,0 +1,49 @@ +import { defaultLocale, locales, type Locale } from '@/i18n' + +const BROWSER_LANGUAGES_QUERY_PARAM = 'browserLanguages' + +function baseLanguage(tag: string): string { + return tag.trim().toLowerCase().split('-')[0] +} + +/** + * Picks the first supported locale from a list of BCP-47 tags (most preferred first). + * `fr-CA, es-MX, en` -> `es`; `fr, de` -> the default locale. + */ +export function negotiateLocale( + tags: readonly string[], + available: readonly Locale[] = locales, + fallback: Locale = defaultLocale, +): Locale { + for (const tag of tags) { + if (!tag) continue + const base = baseLanguage(tag) + const match = available.find((locale) => locale === base) + if (match) return match + } + + return fallback +} + +/** True when at least one of the tags is a language the site is written in. */ +export function hasSupportedLanguage(tags: readonly string[], available: readonly Locale[] = locales): boolean { + return tags.some((tag) => Boolean(tag) && available.some((locale) => locale === baseLanguage(tag))) +} + +/** + * The visitor's preferred languages. `?browserLanguages=fr-FR,fr` overrides them for local testing, + * mirroring the `?journeyToday=` hook used by the journey timeline. + */ +export function getBrowserLanguages(): string[] { + if (typeof window === 'undefined' || typeof navigator === 'undefined') { + return [] + } + + const override = new URLSearchParams(window.location.search).get(BROWSER_LANGUAGES_QUERY_PARAM) + if (override) { + return override.split(',').map((tag) => tag.trim()).filter(Boolean) + } + + const languages = navigator.languages?.length ? [...navigator.languages] : [navigator.language] + return languages.filter(Boolean) +} diff --git a/src/features/profile/ProfileBio.tsx b/src/features/profile/ProfileBio.tsx index 7fc52e5..7bcc49a 100644 --- a/src/features/profile/ProfileBio.tsx +++ b/src/features/profile/ProfileBio.tsx @@ -17,7 +17,7 @@ export function ProfileBio({ className }: ProfileBioProps) { { label: t('pills.satec'), tone: 'success' as const }, { label: t('pills.vbgroup'), tone: 'accent' as const }, { label: t('pills.upm'), tone: 'xr' as const }, - { label: t('pills.english'), tone: 'muted' as const }, + { label: t('pills.english'), tone: 'success' as const }, ] const stats = [ @@ -32,7 +32,7 @@ export function ProfileBio({ className }: ProfileBioProps) {
{t('operatingKicker')} -

+

{t('name')}

diff --git a/src/features/projects/components/TechTag.tsx b/src/features/projects/components/TechTag.tsx index 381101c..47b7514 100644 --- a/src/features/projects/components/TechTag.tsx +++ b/src/features/projects/components/TechTag.tsx @@ -55,6 +55,8 @@ const techIconMap: Record = { 'nest.js': SiNestjs, nestjs: SiNestjs, express: SiExpress, + 'express.js': SiExpress, + expressjs: SiExpress, 'a-frame': SiAframe, aframe: SiAframe, @@ -148,7 +150,10 @@ export function TechTag({ tech, className = '' }: TechTagProps) { className="w-4 h-4 text-[var(--accent)] opacity-80 group-hover:opacity-100 transition-opacity" aria-hidden="true" /> - + {tech}

diff --git a/src/features/skills/components/SkillChip.tsx b/src/features/skills/components/SkillChip.tsx index 24ff6d0..e0fe321 100644 --- a/src/features/skills/components/SkillChip.tsx +++ b/src/features/skills/components/SkillChip.tsx @@ -61,7 +61,10 @@ export function SkillChip({ )} aria-hidden="true" /> - + {label} diff --git a/src/features/skills/skillIconMap.tsx b/src/features/skills/skillIconMap.tsx index 91f0eaf..6c6c2b5 100644 --- a/src/features/skills/skillIconMap.tsx +++ b/src/features/skills/skillIconMap.tsx @@ -8,6 +8,7 @@ import { FaRobot, FaBrain, FaCloud, + FaPlug, } from 'react-icons/fa' import { SiC, @@ -43,6 +44,7 @@ import { SiOpenssl, SiGithub, SiJira, + SiClaude, } from 'react-icons/si' import { VscAzureDevops } from 'react-icons/vsc' @@ -87,6 +89,8 @@ const skillIconMap: Record = { xr: FaCube, security: FaShieldAlt, ai: FaBrain, + claude: SiClaude, + mcp: FaPlug, } export function getSkillIcon(iconKey?: SkillIconKey): IconType { diff --git a/src/features/testimonials/TestimonialsSection.tsx b/src/features/testimonials/TestimonialsSection.tsx index 002ab25..37e1373 100644 --- a/src/features/testimonials/TestimonialsSection.tsx +++ b/src/features/testimonials/TestimonialsSection.tsx @@ -7,6 +7,9 @@ import { fadeInUp } from '@/lib/motion' import { RECOMMENDATIONS, type Recommendation } from '@/content/testimonials' import { cn } from '@/lib/utils' +/** Letters shown at full size: the academic supervisor and the current internship. */ +const FEATURED_IDS: Recommendation['id'][] = ['david-moreno', 'satec-luis'] + const typeTone = { professional: 'border-emerald-300/25 bg-emerald-300/8 text-emerald-200', academic: 'border-sky-300/25 bg-sky-300/8 text-sky-200', @@ -90,8 +93,12 @@ function RecommendationCard({ export function TestimonialsSection() { const t = useTranslations('testimonials') - const featuredRecommendation = RECOMMENDATIONS.find((recommendation) => recommendation.id === 'david-moreno') - const secondaryRecommendations = RECOMMENDATIONS.filter((recommendation) => recommendation.id !== 'david-moreno') + const featuredRecommendations = FEATURED_IDS.map((id) => + RECOMMENDATIONS.find((recommendation) => recommendation.id === id), + ).filter((recommendation): recommendation is Recommendation => Boolean(recommendation)) + const secondaryRecommendations = RECOMMENDATIONS.filter( + (recommendation) => !FEATURED_IDS.includes(recommendation.id), + ) return ( @@ -114,9 +121,11 @@ export function TestimonialsSection() { }} >
- {featuredRecommendation && ( - - )} +
+ {featuredRecommendations.map((recommendation) => ( + + ))} +
{secondaryRecommendations.map((recommendation) => ( diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 39cd91a..da5f6a7 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1,7 +1,7 @@ { "meta": { "title": "Adrián Montes - Portfolio", - "description": "Telematics Engineering student focusing on clean architectures, XR/3D/AR visualizations of software metrics, and pragmatic UX." + "description": "Telematics Engineer and Master's student in Telecommunications Engineering at UPM. Author of Code-XR, an open-source VS Code extension for XR software visualization published at IEEE VISSOFT 2025." }, "nav": { "home": "Portfolio", @@ -20,41 +20,19 @@ }, "hero": { "name": "Adrián Montes Linares", - "headline": "Telematics Engineer & Systems N2 / Cloud Intern", + "headline": "Telematics Engineer · Master's student in Telecommunications Engineering", "headlineSub": "Systems, Cloud, Full-Stack and XR developer tools", - "aboutMe": "Telematics Engineer from URJC, graduated in January 2026, with experience in software development, systems, and data visualization. Author of Code-XR, an open-source VS Code extension published at IEEE VISSOFT 2025, focused on visual analysis of code metrics in XR environments. Currently finishing an extracurricular internship in Cloud & Systems N2 (March-August 2026) and incoming student of the Master's Degree in Telecommunication Engineering at UPM, specializing in Machine Learning and Big Data, with interest in software analytics, code repositories, and applied research.", - "title": "Adrián Montes Linares", - "subtitle1": "Telematics Engineering — Student", - "subtitle2": "Backend • Frontend • Software Engineering • XR", - "tagline": "Author of Code-XR, an open-source VS Code extension for XR visualization of software metrics. Focused on clean architectures and developer experience.", + "aboutMe": "Telematics Engineer from URJC, graduated in January 2026, with experience in software development, systems, and data visualization. Author of Code-XR, an open-source VS Code extension published at IEEE VISSOFT 2025, focused on visual analysis of code metrics in XR environments. Currently enrolled in the Master's Degree in Telecommunications Engineering at UPM, specializing in Machine Learning and Big Data, while continuing an extracurricular Cloud & Systems N2 internship at SATEC. Interested in software analytics, code repositories, and applied research.", "ctaProjects": "View Projects", "ctaResume": "Download CV", - "ctaContact": "Contact", - "alternativeTitle": "Telematics Engineering", "availabilityLabel": "Currently active", - "availabilityText": "SATEC internship, Mar-Aug 2026", + "availabilityText": "SATEC internship, since Mar 2026", "location": "Madrid, ES", - "language": "English B2 (TOEIC)", - "metaLine": "Madrid, ES - English B2 (TOEIC)", - "highlights": { - "vissoft": "VISSOFT - @ - ICSME - 2025 - (accepted) - · - Code-XR - (OSS)", - "downloads": "Code-XR: 300+ downloads", - "availability": "Available: internships (up to 550h) or full-time" - }, - "topStack": { - "typescript": "TypeScript", - "nodejs": "Node.js", - "express": "Express", - "react": "React", - "tailwind": "Tailwind", - "azure": "Azure" - }, "featuredProject": { "badge": "Featured", "codeXr": { "title": "Code-XR", "subtitle": "VS Code extension: your codebase as a 3D city you walk through as you edit", - "metric": "300+ downloads", "vissoft": "Linked to VISSOFT @ ICSME 2025" }, "obt": { @@ -71,17 +49,12 @@ "doi": "DOI" }, "publication": "Publication", - "credentials": { - "vissoft": "VISSOFT 2025", - "codeXr": "Code-XR (Open Source)", - "stack": "TypeScript - · - Next.js - · - Tailwind" - }, "kicker": "Portfolio", "metrics": { "current": { "label": "Current", "value": "Cloud & Systems N2", - "detail": "Mar-Aug 2026" + "detail": "SATEC · since Mar 2026" }, "research": { "label": "Research", @@ -89,46 +62,40 @@ "detail": "Code-XR publication" }, "next": { - "label": "Next", - "value": "UPM 2026", - "detail": "Telecom Engineering master" + "label": "Master's", + "value": "UPM · in progress", + "detail": "Telecommunications · ML & Big Data" } } }, - "about": { - "title": "About me", - "p1": "I am a Telematics Engineering student (URJC) and the author of Code-XR, an ecosystem that connects static analysis with immersive visualization of metrics.", - "p2": "Our work has been accepted for VISSOFT 2025 (co-located with ICSME 2025). I am preparing the thesis and consolidating documentation and tooling for its evolution.", - "p3": "I care about clean code, modular architecture, and a strong DX/UX. I aim for systems that scale and can be maintained." - }, "profile": { "title": "About Me", "subtitle": "A clearer view of where my engineering profile is heading", "imageAlt": "Adrián Montes profile photo", "name": "Adrián Montes Linares", - "role": "Telematics Engineer with experience in Cloud, Systems N2, Full-Stack development, and XR software visualization", + "role": "Telematics Engineer and Master's student in Telecommunications Engineering (UPM), with experience in Cloud, Systems N2, Full-Stack development, and XR software visualization", "bio1": "I am a Telematics Engineer from Universidad Rey Juan Carlos, graduated in January 2026. My profile combines software development, systems, cloud operations, and data visualization, with a practical focus on maintainable tools and clear documentation.", - "bio2": "I am currently finishing an extracurricular internship at SATEC in Cloud & Systems N2, working around support requests, incidents, infrastructure operation, virtual machines, vulnerability patching, SSH/RDP access, and tools such as Nagios, AMS, Veeam Backup, NetApp, and VMware.", + "bio2": "I am currently combining the master's degree at UPM with an extracurricular internship at SATEC in Cloud & Systems N2, where I work on support requests, incidents, infrastructure operation, virtual machines, vulnerability patching, SSH/RDP access, and tools such as Nagios, AMS, Veeam Backup, NetApp, and VMware.", "stats": { "experience": "Experience", "experienceValue": "SATEC Cloud & Systems N2, VBGroup Full-Stack internship, Code-XR, VISSOFT 2025", "education": "Education", - "educationValue": "Telematics Engineering (URJC, Jan 2026), Telecom Engineering master at UPM from Sep 2026", + "educationValue": "Telematics Engineering (URJC, Jan 2026) · Master's in Telecommunications Engineering at UPM, in progress since Sep 2026", "focus": "Current Focus", "focusValue": "Systems, cloud, developer tooling, XR visualization, Machine Learning, and Big Data", "languages": "Languages", - "languagesValue": "Spanish native, English B2 TOEIC, preparing C1", + "languagesValue": "Spanish native, English C1 (Oxford Test of English Advanced)", "mindset": "Mindset", "mindsetValue": "Technical curiosity, Quality-driven approach, Continuous learning" }, "kicker": "Profile", "operatingKicker": "Operating profile", - "bio3": "Before that, during my curricular internship at VBGroup, I worked as a Junior Full-Stack Developer on OBT, an internal platform built with Express.js, TypeScript, Cosmos DB, Azure Blob Storage, React, Vite, and Tailwind. My next academic step is the Master's Degree in Telecommunication Engineering at UPM, with a specialization path in Machine Learning and Big Data.", + "bio3": "Before that, during my curricular internship at VBGroup, I worked as a Junior Full-Stack Developer on OBT, an internal platform built with Express.js, TypeScript, Cosmos DB, Azure Blob Storage, React, Vite, and Tailwind. I am now enrolled in the Master's Degree in Telecommunications Engineering at UPM, following the Machine Learning and Big Data specialization.", "pills": { "satec": "SATEC Cloud & Systems N2", "vbgroup": "VBGroup Full-Stack internship", - "upm": "UPM master: ML & Big Data", - "english": "Working towards English C1" + "upm": "UPM Master's · ML & Big Data", + "english": "English C1 (Oxford)" }, "snapshotTitle": "Current direction", "snapshotBody": "Systems and cloud give me operational depth; full-stack development gives me product delivery; XR, software analytics, Machine Learning, and Big Data define the research and tooling direction I want to keep building.", @@ -142,8 +109,8 @@ "value": "Code-XR / VISSOFT 2025" }, "english": { - "label": "Language goal", - "value": "English C1 in progress" + "label": "Language certificate", + "value": "English C1 · Oxford Test of English" } }, "transition": { @@ -153,13 +120,6 @@ "skills": { "title": "Skills", "subtitle": "A practical map of technologies grouped by use, context, and current depth.", - "topStack": { - "typescript": "TypeScript", - "react": "React", - "express": "Express", - "postgresql": "PostgreSQL", - "azure": "Azure" - }, "actions": { "viewDetails": "View details", "close": "Close", @@ -179,13 +139,6 @@ "nextGroup": "Next category", "prevGroup": "Previous category" }, - "tags": { - "academic": "Academic", - "internship": "Internship", - "open_source": "Open Source", - "regular_use": "Regular use", - "production_like": "Production-like" - }, "proficiency": { "basic": "Basic", "intermediate": "Intermediate", @@ -193,18 +146,6 @@ }, "proficiencyLabel": "Level", "highlights": "Key points", - "purposeTags": { - "learning": "Learning", - "academic": "Academic", - "internship": "Internship", - "open_source": "Open Source", - "production_like": "Production-like" - }, - "usedIn": { - "projects": "Projects", - "journey": "Journey", - "codeXr": "Code-XR" - }, "categories": { "languages": { "label": "Languages", @@ -236,7 +177,7 @@ }, "other": { "label": "Other", - "desc": "Workflow accelerators and AI-assisted practices." + "desc": "AI-assisted development, prompt engineering, and MCP integrations." } }, "items": { @@ -347,10 +288,10 @@ }, "express": { "label": "Express.js", - "summary": "Used to prototype lightweight APIs for web projects.", - "purpose": "Fast Node.js API prototyping.", - "h1": "Built middleware pipelines for validation.", - "h2": "Integrated services with client applications." + "summary": "Node.js framework behind the OBT backend at VBGroup and lightweight APIs in personal projects.", + "purpose": "REST APIs for internal tools and prototypes.", + "h1": "Built the OBT REST API with TypeScript, Swagger docs, and JWT-secured flows.", + "h2": "Composed middleware pipelines for validation and role-based access." }, "react": { "label": "React", @@ -520,6 +461,20 @@ "h1": "Generated scaffolds and refactors quickly.", "h2": "Reviewed suggestions for accuracy and style." }, + "claudeCode": { + "label": "Claude Code", + "summary": "Anthropic's agentic coding tool, used for AI-assisted development from the terminal and the editor.", + "purpose": "AI-assisted development with an agentic workflow.", + "h1": "Used it alongside GitHub Copilot for refactors, reviews, and repetitive engineering tasks.", + "h2": "Reviewed every generated change before merging it." + }, + "mcp": { + "label": "MCP (Model Context Protocol)", + "summary": "Open protocol for connecting AI assistants to external tools, data, and services through MCP servers.", + "purpose": "Integrate MCP servers into AI-assisted development workflows.", + "h1": "Integrated MCP servers so assistants reach project tools and data in a controlled way.", + "h2": "Configured and maintained server setups for day-to-day development." + }, "azureDevOps": { "label": "Microsoft Azure DevOps", "summary": "CI/CD and project-delivery platform used for pipelines, repos and boards.", @@ -543,10 +498,10 @@ }, "nestjs": { "label": "Nest.js", - "summary": "Structured Node.js framework used in the OBT backend.", - "purpose": "Backend architecture with modules, controllers, and services.", - "h1": "Designed REST API modules with a clean server-side structure.", - "h2": "Connected documentation, validation, and cloud-facing services." + "summary": "Structured Node.js framework with modules, controllers, and services for typed APIs.", + "purpose": "Modular, typed backend architecture.", + "h1": "Familiar with its module, controller, and service layering.", + "h2": "Complements Express.js for REST API work in TypeScript." }, "swagger": { "label": "Swagger", @@ -698,7 +653,7 @@ }, "backend": { "summary": "Backend skills focused on API design, Node.js ecosystems, documentation, and practical delivery in project work.", - "h1": "Express, Nest.js, REST APIs, Swagger, JWT, and Django cover the main service patterns represented in projects.", + "h1": "Express, REST APIs, Swagger, JWT, and Django cover the main service patterns represented in projects, with Nest.js as a complementary framework.", "h2": "The emphasis is maintainable endpoints, clear contracts, and integration with frontend and storage layers." }, "frontend": { @@ -785,19 +740,6 @@ "gallery": "Project gallery", "kicker": "Selected work" }, - "experience": { - "title": "Experience / Collaborations", - "intro": "My focus has been academic-applied development and personal projects with measurable impact." - }, - "education": { - "title": "Education", - "urjc": "URJC — Telematics Engineering (2020–2025)", - "bach": "Baccalaureate — Science & Technology (prior to university entry)" - }, - "publications": { - "title": "Publications / Merits", - "item1": "Paper accepted for VISSOFT 2025 (co-located with ICSME 2025): visualization of software metrics in XR. Preparing camera-ready." - }, "journey": { "title": "Professional Journey", "subtitle": "Experience, education, and key milestones", @@ -812,7 +754,7 @@ "work": "Work Experience", "project": "Featured Projects", "achievement": "Achievements & Publications", - "learning": "Learning Goals" + "learning": "Learning & Certifications" }, "entries": { "urjc": { @@ -849,24 +791,29 @@ } }, "vissoft": { - "role": "Paper Accepted — VISSOFT 2025", + "role": "Distinguished Artifact Award — VISSOFT 2025", "org": "IEEE VISSOFT @ ICSME 2025", - "desc": "Research paper on XR visualization of software metrics accepted for presentation at international conference." + "desc": "Research paper on XR visualization of software metrics, published at IEEE VISSOFT 2025 (co-located with ICSME 2025) and recognized with the Distinguished Artifact Award." + }, + "hackathonUrjc": { + "role": "Hackathon URJC 2024 — circular-economy challenge", + "org": "Universidad Rey Juan Carlos (URJC)", + "desc": "First Hackathon URJC (March 1–2, 2024, Móstoles campus). I worked on the ideation, prototyping, and final pitch of a university token model for the circular-economy challenge: students earn tokens by contributing (tutoring, notes, activities) and spend them on campus services." }, - "cambridgeC1": { - "role": "Cambridge English C1 Certification - in progress", - "org": "Cambridge University", - "desc": "Working toward professional English fluency across technical and business contexts, with reading, writing, speaking, and listening preparation in progress." + "oxfordC1": { + "role": "Oxford Test of English Advanced — C1 certified", + "org": "Oxford University Press · certified by the University of Oxford", + "desc": "Exam taken on September 4, 2026: overall CEFR C1 (146/170). Verifiable by third parties through the official Oxford University Press service." }, "satecCloud": { "role": "Systems N2 / Cloud Engineer", "org": "SATEC (Extracurricular Internship)", - "desc": "Extracurricular internship focused on systems, N2 support, cloud environments, infrastructure operation, and professional documentation. Started on March 14, 2026." + "desc": "Extracurricular internship focused on systems, N2 support, cloud environments, infrastructure operation, and professional documentation. Started on March 14, 2026, renewed in September 2026, and currently combined with the master's degree at UPM." }, "masterTelecomUPM": { - "role": "Master's Degree in Telecommunication Engineering - starts Sep 2026", - "org": "Universidad Politecnica de Madrid (UPM)", - "desc": "Official master's degree starting in September 2026 at UPM, with a path toward Machine Learning and Big Data within Telecommunication Engineering." + "role": "Master's Degree in Telecommunications Engineering", + "org": "Universidad Politécnica de Madrid (UPM)", + "desc": "Official master's degree at UPM, started in September 2026, following the Machine Learning and Big Data specialization within Telecommunications Engineering. Combined with the extracurricular Cloud & Systems N2 internship at SATEC." } }, "kicker": "Career stream", @@ -938,6 +885,10 @@ "name": "GitHub Copilot", "desc": "AI pair programmer that helps me write code faster and explore new patterns." }, + "claudeCode": { + "name": "Claude Code", + "desc": "Anthropic's agentic coding tool. AI-assisted development from the terminal, extended with MCP servers." + }, "windowsTerminal": { "name": "Windows Terminal", "desc": "Modern terminal with tabs, panes, and GPU-accelerated rendering." @@ -999,6 +950,17 @@ "formal": "Formal reference" }, "recommendations": { + "satec-luis": { + "role": "Director of Cloud Services", + "relationship": "Reference from SATEC's Cloud Services management", + "summary": "Luis endorses my work during the Cloud & Systems N2 internship at SATEC and recommends me for any position. His letter highlights a proactive, responsible, and committed attitude, a strong capacity to learn and adapt quickly to changes and challenges, adaptability to different contexts and ways of working, and a natural integration into the team, fostering collaboration, respect, and effective communication.", + "strengths": { + "learning": "Fast learner", + "teamwork": "Teamwork", + "adaptability": "Adaptability", + "attitude": "Proactive attitude" + } + }, "vbgroup-abner": { "role": "Development Leader", "relationship": "Direct supervisor during VBGroup internship", @@ -1036,26 +998,32 @@ "certificates": { "title": "Certifications & Achievements", "subtitle": "Professional credentials, publications, and recognitions", + "kicker": "Professional record", "completed": "Completed", "inProgress": "In Progress", "planned": "Planned", "statusDescriptions": { "completed": "Degrees, awards, and confirmed academic recognitions.", - "in-progress": "Credentials currently being prepared.", - "planned": "Next academic step already planned." + "in-progress": "Degrees and credentials currently in progress.", + "planned": "Upcoming credentials on the roadmap." }, - "cambridgeC1": "Cambridge English C1 Certificate", + "oxfordC1": "Oxford Test of English Advanced — CEFR C1", + "oxfordC1Issuer": "Oxford University Press · certified by the University of Oxford", "verify": "Verify", - "learnMore": "Learn more", - "masterTelecomUPM": "Master's Degree in Telecommunication Engineering", - "masterTelecomUPMIssuer": "Universidad Politecnica de Madrid (UPM)", + "masterTelecomUPM": "Master's Degree in Telecommunications Engineering", + "masterTelecomUPMIssuer": "Universidad Politécnica de Madrid (UPM)", "telematicsDegree": "BSc in Telematics Engineering", "telematicsDegreeIssuer": "Universidad Rey Juan Carlos (URJC)", - "codeXrAward": "Code-XR — VISSOFT 2025 Poster Track Award", - "codeXrAwardIssuer": "ICSME 2025 Awards", + "codeXrAward": "Code-XR — Distinguished Artifact Award, VISSOFT 2025", + "codeXrAwardIssuer": "IEEE VISSOFT 2025 (co-located with ICSME 2025)", "greenhouseHighDistinction": "GreenHouse — AST High Distinction", - "greenhouseHighDistinctionIssuer": "Ampliacion de Sistemas Telematicos (URJC)", - "stepByStepHighDistinction": "StepByStep — LSMO High Distinction", - "stepByStepHighDistinctionIssuer": "Laboratorio de Sistemas Moviles y Ubicuos (URJC)" + "greenhouseHighDistinctionIssuer": "Ampliación de Sistemas Telemáticos (URJC)", + "stepByStepHighDistinction": "StepByStep — LSMU High Distinction", + "stepByStepHighDistinctionIssuer": "Laboratorio de Sistemas Móviles y Ubicuos (URJC)" + }, + "language": { + "browserHint": "This site is written in English and Spanish. Your browser can translate it into your language.", + "dismiss": "Dismiss", + "skipToContent": "Skip to main content" } } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 2821624..a469f82 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1,7 +1,7 @@ { "meta": { "title": "Adrián Montes - Portfolio", - "description": "Estudiante de Ingeniería Telemática enfocado en arquitecturas limpias, visualizaciones XR/3D/AR de métricas de software y UX pragmática." + "description": "Ingeniero Telemático y estudiante del Máster en Ingeniería de Telecomunicación en la UPM. Autor de Code-XR, extensión open source de VS Code para la visualización XR de métricas de software, publicada en IEEE VISSOFT 2025." }, "nav": { "home": "Portfolio", @@ -20,41 +20,19 @@ }, "hero": { "name": "Adrián Montes Linares", - "headline": "Ingeniero Telematico y Sistemas N2 / Cloud", + "headline": "Ingeniero Telemático · Estudiante del Máster en Ingeniería de Telecomunicación", "headlineSub": "Sistemas, Cloud, Full-Stack y herramientas XR", - "aboutMe": "Ingeniero Telemático por la URJC, titulado en enero de 2026, con experiencia en desarrollo software, sistemas y visualización de datos. Autor de Code-XR, extensión open source para VS Code publicada en IEEE VISSOFT 2025, orientada al análisis visual de métricas de código en entornos XR. Actualmente finalizando prácticas extracurriculares en Cloud & Sistemas N2 (marzo-agosto de 2026) y próximo estudiante del Máster Universitario en Ingeniería de Telecomunicación en la UPM, intensificación en Aprendizaje Automático y Big Data, con interés en analítica de software, repositorios de código e investigación aplicada.", - "title": "Adrián Montes Linares", - "subtitle1": "Ingeniería Telemática — Estudiante", - "subtitle2": "Backend • Frontend • Ingeniería de Software • XR", - "tagline": "Autor de Code-XR, una extensión open-source de VS Code para visualización XR de métricas de software. Enfocado en arquitecturas limpias y experiencia de desarrollador.", + "aboutMe": "Ingeniero Telemático por la URJC, titulado en enero de 2026, con experiencia en desarrollo software, sistemas y visualización de datos. Autor de Code-XR, extensión open source para VS Code publicada en IEEE VISSOFT 2025, orientada al análisis visual de métricas de código en entornos XR. Actualmente cursando el Máster Universitario en Ingeniería de Telecomunicación en la UPM, con intensificación en Aprendizaje Automático y Big Data, compaginándolo con prácticas extracurriculares en Cloud & Sistemas N2 de SATEC, con interés en analítica de software, repositorios de código e investigación aplicada.", "ctaProjects": "Ver Proyectos", "ctaResume": "Descargar CV", - "ctaContact": "Contacto", - "alternativeTitle": "Ingeniería Telemática", "availabilityLabel": "Actualmente activo", - "availabilityText": "Prácticas en SATEC, mar-ago 2026", + "availabilityText": "Prácticas en SATEC, desde mar 2026", "location": "Madrid, ES", - "language": "Ingles B2 (TOEIC)", - "metaLine": "Madrid, ES - Ingles B2 (TOEIC)", - "highlights": { - "vissoft": "VISSOFT - @ - ICSME - 2025 - (aceptado) - · - Code-XR - (OSS)", - "downloads": "Code-XR: +300 descargas", - "availability": "Disponible: prácticas (hasta 550h) o contrato" - }, - "topStack": { - "typescript": "TypeScript", - "nodejs": "Node.js", - "express": "Express", - "react": "React", - "tailwind": "Tailwind", - "azure": "Azure" - }, "featuredProject": { "badge": "Destacado", "codeXr": { "title": "Code-XR", "subtitle": "Extensión de VS Code: tu código como una ciudad 3D que recorres mientras editas", - "metric": "+300 descargas", "vissoft": "Vinculado a VISSOFT @ ICSME 2025" }, "obt": { @@ -71,17 +49,12 @@ "doi": "DOI" }, "publication": "Publicación", - "credentials": { - "vissoft": "VISSOFT 2025", - "codeXr": "Code-XR (Open Source)", - "stack": "TypeScript - · - Next.js - · - Tailwind" - }, "kicker": "Portfolio", "metrics": { "current": { "label": "Actual", "value": "Cloud & Sistemas N2", - "detail": "mar-ago 2026" + "detail": "SATEC · desde mar 2026" }, "research": { "label": "Investigación", @@ -89,46 +62,40 @@ "detail": "Publicación Code-XR" }, "next": { - "label": "Siguiente", - "value": "UPM 2026", - "detail": "Máster de Telecomunicación" + "label": "Máster", + "value": "UPM · en curso", + "detail": "Telecomunicación · ML y Big Data" } } }, - "about": { - "title": "Sobre mí", - "p1": "Soy estudiante de Ingeniería Telemática (URJC) y autor de Code-XR, un ecosistema que conecta análisis estático con visualización inmersiva de métricas.", - "p2": "Nuestro trabajo ha sido aceptado para VISSOFT 2025 (co-localizado con ICSME 2025). Estoy preparando la tesis y consolidando documentación y herramientas para su evolución.", - "p3": "Me preocupo por el código limpio, la arquitectura modular y un DX/UX sólido. Busco sistemas que escalen y puedan mantenerse." - }, "profile": { "title": "Sobre mí", "subtitle": "Una lectura más clara de hacia dónde evoluciona mi perfil técnico", "imageAlt": "Foto de perfil de Adrián Montes", "name": "Adrián Montes Linares", - "role": "Ingeniero Telemático con experiencia en Cloud, Sistemas N2, desarrollo Full-Stack y visualización XR de software", + "role": "Ingeniero Telemático y estudiante del Máster en Ingeniería de Telecomunicación (UPM), con experiencia en Cloud, Sistemas N2, desarrollo Full-Stack y visualización XR de software", "bio1": "Soy Ingeniero Telemático por la Universidad Rey Juan Carlos, titulado en enero de 2026. Mi perfil combina desarrollo software, sistemas, operación cloud y visualización de datos, con un enfoque práctico en herramientas mantenibles y documentación clara.", - "bio2": "Actualmente estoy finalizando prácticas extracurriculares en SATEC dentro de Cloud & Sistemas N2, trabajando con requests, incidencias, operación de infraestructura, máquinas virtuales, parcheo de vulnerabilidades, acceso SSH/RDP y herramientas como Nagios, AMS, Veeam Backup, NetApp y VMware.", + "bio2": "Actualmente compagino el máster en la UPM con prácticas extracurriculares en SATEC dentro de Cloud & Sistemas N2, donde trabajo con requests, incidencias, operación de infraestructura, máquinas virtuales, parcheo de vulnerabilidades, acceso SSH/RDP y herramientas como Nagios, AMS, Veeam Backup, NetApp y VMware.", "stats": { "experience": "Experiencia", "experienceValue": "SATEC Cloud & Sistemas N2, prácticas Full-Stack en VBGroup, Code-XR, VISSOFT 2025", "education": "Formación", - "educationValue": "Ingeniería Telemática (URJC, ene 2026), máster de Telecomunicación en UPM desde sep 2026", + "educationValue": "Ingeniería Telemática (URJC, ene 2026) · Máster en Ingeniería de Telecomunicación en la UPM, en curso desde sep 2026", "focus": "Enfoque actual", "focusValue": "Sistemas, cloud, developer tooling, visualización XR, Machine Learning y Big Data", "languages": "Idiomas", - "languagesValue": "Español nativo, inglés B2 TOEIC, preparando C1", + "languagesValue": "Español nativo, inglés C1 (Oxford Test of English Advanced)", "mindset": "Mentalidad", "mindsetValue": "Curiosidad técnica, orientación a calidad, aprendizaje continuo" }, "kicker": "Perfil", "operatingKicker": "Perfil operativo", - "bio3": "Antes, durante mis prácticas curriculares en VBGroup, trabajé como Desarrollador Full-Stack Junior en OBT, una plataforma interna construida con Express.js, TypeScript, Cosmos DB, Azure Blob Storage, React, Vite y Tailwind. Mi siguiente paso académico es el Máster Universitario en Ingeniería de Telecomunicación en la UPM, con intensificación en Aprendizaje Automático y Big Data.", + "bio3": "Antes, durante mis prácticas curriculares en VBGroup, trabajé como Desarrollador Full-Stack Junior en OBT, una plataforma interna construida con Express.js, TypeScript, Cosmos DB, Azure Blob Storage, React, Vite y Tailwind. En paralelo curso el Máster Universitario en Ingeniería de Telecomunicación en la UPM, con intensificación en Aprendizaje Automático y Big Data.", "pills": { "satec": "SATEC Cloud & Sistemas N2", "vbgroup": "Prácticas Full-Stack en VBGroup", - "upm": "Máster UPM: ML & Big Data", - "english": "Preparando inglés C1" + "upm": "Máster UPM · ML y Big Data", + "english": "Inglés C1 (Oxford)" }, "snapshotTitle": "Dirección actual", "snapshotBody": "Sistemas y cloud me dan profundidad operativa; el full-stack me da capacidad de entrega de producto; XR, analítica de software, Machine Learning y Big Data marcan la línea de investigación y tooling que quiero seguir construyendo.", @@ -142,8 +109,8 @@ "value": "Code-XR / VISSOFT 2025" }, "english": { - "label": "Objetivo de idioma", - "value": "Inglés C1 en progreso" + "label": "Certificado de idioma", + "value": "Inglés C1 · Oxford Test of English" } }, "transition": { @@ -153,13 +120,6 @@ "skills": { "title": "Competencias", "subtitle": "Mapa práctico de tecnologías agrupadas por uso, contexto y profundidad actual.", - "topStack": { - "typescript": "TypeScript", - "react": "React", - "express": "Express", - "postgresql": "PostgreSQL", - "azure": "Azure" - }, "actions": { "viewDetails": "Ver detalles", "close": "Cerrar", @@ -179,13 +139,6 @@ "nextGroup": "Siguiente categoría", "prevGroup": "Categoría anterior" }, - "tags": { - "academic": "Académico", - "internship": "Prácticas", - "open_source": "Código abierto", - "regular_use": "Uso habitual", - "production_like": "Similar a producción" - }, "proficiency": { "basic": "Básico", "intermediate": "Intermedio", @@ -193,18 +146,6 @@ }, "proficiencyLabel": "Nivel", "highlights": "Puntos clave", - "purposeTags": { - "learning": "Aprendizaje", - "academic": "Académico", - "internship": "Prácticas", - "open_source": "Código abierto", - "production_like": "Similar a producción" - }, - "usedIn": { - "projects": "Proyectos", - "journey": "Trayectoria", - "codeXr": "Code-XR" - }, "categories": { "languages": { "label": "Lenguajes", @@ -236,7 +177,7 @@ }, "other": { "label": "Otros", - "desc": "Aceleradores de flujo de trabajo y prácticas con IA." + "desc": "Desarrollo asistido por IA, ingeniería de prompts e integraciones MCP." } }, "items": { @@ -347,10 +288,10 @@ }, "express": { "label": "Express.js", - "summary": "Usado para prototipar APIs ligeras.", - "purpose": "Prototipado rápido de APIs Node.", - "h1": "Construí middlewares de validación.", - "h2": "Integré servicios con el cliente." + "summary": "Framework de Node.js usado en el backend de OBT (VBGroup) y en APIs ligeras de proyectos personales.", + "purpose": "APIs REST para herramientas internas y prototipos.", + "h1": "Construí la API REST de OBT con TypeScript, documentación Swagger y flujos asegurados con JWT.", + "h2": "Encadené middlewares de validación y de acceso por roles." }, "react": { "label": "React", @@ -520,6 +461,20 @@ "h1": "Generé scaffolds y refactors rápido.", "h2": "Revisé sugerencias por precisión." }, + "claudeCode": { + "label": "Claude Code", + "summary": "Herramienta de programación agéntica de Anthropic, usada para desarrollo asistido por IA desde la terminal y el editor.", + "purpose": "Desarrollo asistido por IA con flujo agéntico.", + "h1": "Lo combiné con GitHub Copilot en refactors, revisiones y tareas repetitivas de ingeniería.", + "h2": "Revisé cada cambio generado antes de integrarlo." + }, + "mcp": { + "label": "MCP (Model Context Protocol)", + "summary": "Protocolo abierto para conectar asistentes de IA con herramientas, datos y servicios externos mediante servidores MCP.", + "purpose": "Integrar servidores MCP en flujos de desarrollo asistido por IA.", + "h1": "Integré servidores MCP para que los asistentes accedan de forma controlada a herramientas y datos del proyecto.", + "h2": "Configuré y mantuve servidores MCP para el desarrollo del día a día." + }, "azureDevOps": { "label": "Microsoft Azure DevOps", "summary": "Plataforma de CI/CD y entrega para pipelines, repos y planificación.", @@ -543,10 +498,10 @@ }, "nestjs": { "label": "Nest.js", - "summary": "Framework Node.js estructurado usado en el backend de OBT.", - "purpose": "Arquitectura backend con módulos, controladores y servicios.", - "h1": "Diseñé módulos de API REST con estructura limpia.", - "h2": "Conecté documentación, validación y servicios cloud." + "summary": "Framework estructurado de Node.js con módulos, controladores y servicios para APIs tipadas.", + "purpose": "Arquitectura backend modular y tipada.", + "h1": "Conozco su organización en módulos, controladores y servicios.", + "h2": "Complementa a Express.js en APIs REST con TypeScript." }, "swagger": { "label": "Swagger", @@ -698,7 +653,7 @@ }, "backend": { "summary": "Backend centrado en diseño de APIs, ecosistema Node.js, documentación y entrega práctica en proyectos.", - "h1": "Express, Nest.js, REST APIs, Swagger, JWT y Django cubren los principales patrones de servicios.", + "h1": "Express, REST APIs, Swagger, JWT y Django cubren los principales patrones de servicios de los proyectos, con Nest.js como framework complementario.", "h2": "El foco está en endpoints mantenibles, contratos claros e integración con frontend y almacenamiento." }, "frontend": { @@ -727,7 +682,7 @@ "h2": "El valor está menos en las etiquetas y más en estructura, trazabilidad e iteración." }, "other": { - "summary": "Prácticas de IA asistida y aceleración de flujo utilizadas como apoyo al trabajo de ingeniería.", + "summary": "Prácticas de desarrollo asistido por IA y de aceleración del flujo de trabajo, utilizadas como apoyo al trabajo de ingeniería.", "h1": "Estas herramientas ayudan a explorar, revisar y avanzar más rápido sin sustituir criterio técnico.", "h2": "El foco está en asistencia práctica, validación clara y mantener los resultados anclados al contexto real." } @@ -785,19 +740,6 @@ "gallery": "Galería del proyecto", "kicker": "Trabajo seleccionado" }, - "experience": { - "title": "Experiencia / Colaboraciones", - "intro": "Mi enfoque ha sido desarrollo académico-aplicado y proyectos personales con impacto medible." - }, - "education": { - "title": "Formación", - "urjc": "URJC — Ingeniería Telemática (2020–2025)", - "bach": "Bachillerato — Ciencias y Tecnología (previo al ingreso universitario)" - }, - "publications": { - "title": "Publicaciones / Méritos", - "item1": "Artículo aceptado para VISSOFT 2025 (co-localizado con ICSME 2025): visualización de métricas de software en XR. Preparando camera-ready." - }, "journey": { "title": "Trayectoria Profesional", "subtitle": "Experiencia, formación y logros clave", @@ -812,7 +754,7 @@ "work": "Experiencia laboral", "project": "Proyectos destacados", "achievement": "Logros y publicaciones", - "learning": "Objetivos de Aprendizaje" + "learning": "Aprendizaje y certificaciones" }, "entries": { "urjc": { @@ -849,24 +791,29 @@ } }, "vissoft": { - "role": "Paper Aceptado — VISSOFT 2025", + "role": "Distinguished Artifact Award — VISSOFT 2025", "org": "IEEE VISSOFT @ ICSME 2025", - "desc": "Artículo de investigación sobre visualización XR de métricas de software aceptado para presentación en conferencia internacional." + "desc": "Artículo de investigación sobre visualización XR de métricas de software, publicado en IEEE VISSOFT 2025 (junto a ICSME 2025) y reconocido con el Distinguished Artifact Award." + }, + "hackathonUrjc": { + "role": "Hackatón URJC 2024 — reto de economía circular", + "org": "Universidad Rey Juan Carlos (URJC)", + "desc": "Primer Hackatón URJC (1 y 2 de marzo de 2024, campus de Móstoles). Trabajé en la ideación, el prototipado y el pitch final de un modelo de tokens universitarios para el reto de economía circular: los estudiantes ganan tokens por contribuir (tutorías, apuntes, actividades) y los gastan en servicios del campus." }, - "cambridgeC1": { - "role": "Certificación Cambridge English C1 - en progreso", - "org": "Universidad de Cambridge", - "desc": "Trabajo en progreso para avanzar hacia fluidez profesional en inglés en contextos técnicos y de negocio, preparando lectura, escritura, expresión oral y comprensión auditiva." + "oxfordC1": { + "role": "Oxford Test of English Advanced — C1 certificado", + "org": "Oxford University Press · certificado por la Universidad de Oxford", + "desc": "Examen realizado el 4 de septiembre de 2026: nivel global C1 del MCER (146/170). Verificable por terceros a través del servicio oficial de Oxford University Press." }, "satecCloud": { "role": "Ingeniero de Sistemas N2 / Cloud", - "org": "SATEC (Practicas extracurriculares)", - "desc": "Practicas extracurriculares centradas en sistemas, soporte N2, entornos cloud, operacion de infraestructura y documentacion profesional. Inicio el 14 de marzo de 2026." + "org": "SATEC (Prácticas extracurriculares)", + "desc": "Prácticas extracurriculares centradas en sistemas, soporte N2, entornos cloud, operación de infraestructura y documentación profesional. Comenzaron el 14 de marzo de 2026, se renovaron en septiembre de 2026 y actualmente las compagino con el máster en la UPM." }, "masterTelecomUPM": { - "role": "Máster Universitario en Ingeniería de Telecomunicación - comienza sep 2026", - "org": "Universidad Politecnica de Madrid (UPM)", - "desc": "Máster oficial con inicio en septiembre de 2026 en la UPM, con camino de intensificación hacia Aprendizaje Automático y Big Data dentro de Ingeniería de Telecomunicación." + "role": "Máster Universitario en Ingeniería de Telecomunicación", + "org": "Universidad Politécnica de Madrid (UPM)", + "desc": "Máster oficial en la UPM, iniciado en septiembre de 2026, con intensificación en Aprendizaje Automático y Big Data dentro de Ingeniería de Telecomunicación. Lo compagino con las prácticas extracurriculares en Cloud & Sistemas N2 de SATEC." } }, "kicker": "Trayectoria técnica", @@ -938,6 +885,10 @@ "name": "GitHub Copilot", "desc": "Programador IA que me ayuda a escribir código más rápido y explorar nuevos patrones." }, + "claudeCode": { + "name": "Claude Code", + "desc": "Herramienta de programación agéntica de Anthropic. Desarrollo asistido por IA desde la terminal, ampliado con servidores MCP." + }, "windowsTerminal": { "name": "Windows Terminal", "desc": "Terminal moderna con pestañas, paneles y renderizado acelerado por GPU." @@ -999,6 +950,17 @@ "formal": "Referencia formal" }, "recommendations": { + "satec-luis": { + "role": "Director de Servicios Cloud", + "relationship": "Referencia de la dirección de Servicios Cloud de SATEC", + "summary": "Luis avala mi desempeño profesional durante las prácticas en Cloud & Sistemas N2 de SATEC y me recomienda para cualquier puesto. Su carta destaca una actitud proactiva, responsable y comprometida; una gran capacidad de aprendizaje y de adaptación rápida a los cambios y desafíos; adaptabilidad a distintos contextos y dinámicas de trabajo; y una integración natural en el equipo, fomentando la colaboración, el respeto y la comunicación efectiva.", + "strengths": { + "learning": "Aprendizaje rápido", + "teamwork": "Trabajo en equipo", + "adaptability": "Adaptabilidad", + "attitude": "Actitud proactiva" + } + }, "vbgroup-abner": { "role": "Responsable de Desarrollo", "relationship": "Supervisor directo durante las prácticas en VBGroup", @@ -1036,26 +998,32 @@ "certificates": { "title": "Certificaciones y Logros", "subtitle": "Credenciales profesionales, publicaciones y reconocimientos", + "kicker": "Historial profesional", "completed": "Completado", - "inProgress": "En Progreso", + "inProgress": "En curso", "planned": "Planeado", "statusDescriptions": { - "completed": "Grado, premios y reconocimientos academicos confirmados.", - "in-progress": "Credenciales actualmente en preparacion.", - "planned": "Siguiente paso academico ya planificado." + "completed": "Grado, premios y reconocimientos académicos confirmados.", + "in-progress": "Titulaciones y credenciales actualmente en curso.", + "planned": "Próximas credenciales en el horizonte." }, - "cambridgeC1": "Certificado Cambridge English C1", + "oxfordC1": "Oxford Test of English Advanced — C1 (MCER)", + "oxfordC1Issuer": "Oxford University Press · certificado por la Universidad de Oxford", "verify": "Verificar", - "learnMore": "Más información", - "masterTelecomUPM": "Master Universitario en Ingenieria de Telecomunicacion", - "masterTelecomUPMIssuer": "Universidad Politecnica de Madrid (UPM)", - "telematicsDegree": "Grado en Ingenieria Telematica", + "masterTelecomUPM": "Máster Universitario en Ingeniería de Telecomunicación", + "masterTelecomUPMIssuer": "Universidad Politécnica de Madrid (UPM)", + "telematicsDegree": "Grado en Ingeniería Telemática", "telematicsDegreeIssuer": "Universidad Rey Juan Carlos (URJC)", - "codeXrAward": "Code-XR — Premio Poster Track VISSOFT 2025", - "codeXrAwardIssuer": "ICSME 2025 Awards", - "greenhouseHighDistinction": "GreenHouse — Matricula de Honor en AST", - "greenhouseHighDistinctionIssuer": "Ampliacion de Sistemas Telematicos (URJC)", - "stepByStepHighDistinction": "StepByStep — Matricula de Honor en LSMO", - "stepByStepHighDistinctionIssuer": "Laboratorio de Sistemas Moviles y Ubicuos (URJC)" + "codeXrAward": "Code-XR — Distinguished Artifact Award en VISSOFT 2025", + "codeXrAwardIssuer": "IEEE VISSOFT 2025 (junto a ICSME 2025)", + "greenhouseHighDistinction": "GreenHouse — Matrícula de Honor en AST", + "greenhouseHighDistinctionIssuer": "Ampliación de Sistemas Telemáticos (URJC)", + "stepByStepHighDistinction": "StepByStep — Matrícula de Honor en LSMU", + "stepByStepHighDistinctionIssuer": "Laboratorio de Sistemas Móviles y Ubicuos (URJC)" + }, + "language": { + "browserHint": "Este sitio está escrito en inglés y español. Tu navegador puede traducirlo a tu idioma.", + "dismiss": "Cerrar", + "skipToContent": "Saltar al contenido" } } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d74037a..d3fb083 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -18,6 +18,10 @@ export const LINKS = { codeXrAwardCertificate: '/documents/distinghuished_artifact_award.pdf', vissoftPaper: 'https://doi.org/10.1109/VISSOFT67405.2025.00034', + + oxfordC1Verify: 'https://go.oup.com/oxfordtestofenglish/verify', + upm: 'https://www.upm.es', + hackathonUrjc: 'https://eventos.urjc.es/111906/detail/hackathon-urjc.html', } as const export const CV_FILES = { @@ -29,12 +33,6 @@ export function getCvUrl(locale: string): string { return locale === 'es' ? CV_FILES.es : CV_FILES.en } -export const CREDENTIALS = [ - { key: 'vissoft', label: 'VISSOFT 2025' }, - { key: 'codeXr', label: 'Code-XR (Open Source)' }, - { key: 'techStack', label: 'TypeScript - Next.js - Tailwind' }, -] as const - export const NAV_ITEMS = [ { key: 'home', href: '#home' }, { key: 'profile', href: '#profile' }, diff --git a/src/lib/seo.ts b/src/lib/seo.ts index f2e599e..903ae78 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -11,7 +11,7 @@ const messages = { } as const export const OG_IMAGE = '/images/og/portfolio.png' -export const LAST_MODIFIED = '2026-08-29' +export const LAST_MODIFIED = '2026-09-07' const localeMeta = { en: { @@ -177,8 +177,9 @@ export function buildHomeJsonLd(locale: Locale): Record { ], givenName: 'Adrián', familyName: 'Montes Linares', - jobTitle: ['Telecommunications Engineer', 'Software Engineer'], - description: 'Full-stack developer focused on TypeScript, React, Node.js, DevTools, and XR software visualization.', + jobTitle: ['Telematics Engineer', 'Software Engineer'], + description: + "Telematics Engineer (URJC, 2026) and Master's student in Telecommunications Engineering at UPM, specializing in Machine Learning and Big Data. Author of Code-XR, an open-source VS Code extension for XR software visualization published at IEEE VISSOFT 2025. Cloud & Systems N2 intern at SATEC.", url: SITE.url, email: LINKS.email, image: absoluteUrl('/images/profile/hero-320.jpg'), @@ -188,6 +189,39 @@ export function buildHomeJsonLd(locale: Locale): Record { name: 'Universidad Rey Juan Carlos', url: 'https://www.urjc.es', }, + affiliation: { + '@type': 'EducationalOrganization', + name: 'Universidad Politécnica de Madrid', + alternateName: 'UPM', + url: LINKS.upm, + }, + hasCredential: [ + { + '@type': 'EducationalOccupationalCredential', + name: 'Oxford Test of English Advanced - CEFR C1', + credentialCategory: 'Language certificate', + educationalLevel: 'CEFR C1', + dateCreated: '2026-09-04', + recognizedBy: { + '@type': 'Organization', + name: 'Oxford University Press', + url: 'https://www.oxfordtestofenglish.com', + }, + }, + { + '@type': 'EducationalOccupationalCredential', + name: "Bachelor's Degree in Telematics Engineering", + credentialCategory: 'degree', + dateCreated: '2026-01', + recognizedBy: { + '@type': 'EducationalOrganization', + name: 'Universidad Rey Juan Carlos', + url: 'https://www.urjc.es', + }, + }, + ], + knowsLanguage: ['es', 'en'], + award: ['Distinguished Artifact Award — IEEE VISSOFT 2025 (Code-XR)'], knowsAbout: [ 'TypeScript', 'React', @@ -197,6 +231,12 @@ export function buildHomeJsonLd(locale: Locale): Record { 'Software Engineering', 'Data Visualization', 'Azure', + 'Machine Learning', + 'Big Data', + 'Cloud Infrastructure', + 'Linux', + 'Prompt Engineering', + 'Model Context Protocol (MCP)', ], }, { @@ -245,6 +285,7 @@ export function buildProjectJsonLd( operatingSystem: 'Visual Studio Code', downloadUrl: LINKS.codeXrMarketplace, citation: LINKS.codeXrDoi, + award: 'Distinguished Artifact Award, IEEE VISSOFT 2025', } : {}), }