Skip to content

Portafolio del Estudiante

✅ Implementado

El portafolio del estudiante es una página pública y compartible que agrega automáticamente los logros de aprendizaje de un usuario: certificados verificables, badges Open Badge, rutas completadas, habilidades demostradas y una línea de tiempo de actividad.

Cada estudiante tiene su propio portafolio en la URL fluency.nappai.com/u/:username. El portafolio es privado por defecto — el estudiante debe activarlo explícitamente y elegir un username desde los ajustes de su cuenta (/settings, pestaña Portafolio).

Archivos clave:

  • apps/api/src/modules/portfolio/ — módulo NestJS (controller, service, dto)
  • apps/web/src/pages/public/StudentPortfolioPage.tsx — página pública
  • apps/web/src/pages/settings/sections/PortfolioSection.tsx — configuración del estudiante
  • apps/web/src/components/portfolio/ — 14 componentes de UI
  • apps/web/src/store/api/portfolioApi.ts — slice RTK Query
  • apps/web/src/hooks/usePortfolio.ts — hook auxiliar

El portafolio se compone de las siguientes secciones. Cada sección puede activarse o desactivarse individualmente desde los ajustes.

SecciónComponenteControlable
Hero con nivel XP, score y estadísticasPortfolioHeroBannerNo
Habilidades verificadasSkillsSectionSí (showCertificates)
Certificados SHA-256CertificatesSectionSí (showCertificates)
Badges Open Badge 3.0BadgesSectionSí (showBadges)
Rutas completadas o en cursoPathsSectionSí (showPaths)
Línea de tiempo de actividadTimelineSectionSí (showTimeline)
Medidor XP y nivelXPMeterSí (showXP)
Links de perfil (LinkedIn, web, GitHub)ProfileLinksCardNo
Card para reclutadoresRecruiterInfoCardNo
Estándares (Open Badge)StandardsCardNo

GET /u/:username ← Ruta React Router — página pública

El username debe tener entre 3 y 40 caracteres (letras, números, guiones). Los cambios de username están sujetos a un cooldown de 30 días. Nombres reservados (admin, api, verify, dashboard, etc.) no pueden usarse.

El campo isPublic del modelo StudentProfile controla el acceso:

  • isPublic: false → la API devuelve 404 con código PORTFOLIO_PRIVATE
  • isPublic: true → acceso libre sin autenticación

El contador de visitas (viewCount) se incrementa de forma fire-and-forget en cada visualización.


El nivel del estudiante se calcula en tiempo real a partir de dos fuentes:

XP total = Σ xpEvents.xp + Σ badges.tierXP
Tier badgeXP aportado
Bronze (tier 1)25 XP
Silver (tier 2)50 XP
Gold (tier 3)100 XP
Platinum (tier 4)200 XP
Legend (tier 5)500 XP

La progresión de nivel es lineal: 500 XP por nivel, máximo nivel 10.

NivelNombre
1–3Aprendiz IA
4–5Practicante IA
6–7Arquitecto IA
8–9Experto IA
10Maestro IA

Los certificados se integran en el portafolio con verificación en tiempo real.

flowchart LR
C[Certificate\nverificationHash SHA-256] -->|verifyUrl| V[/verify/:uuid]
C -->|linkedinUrl| LI[LinkedIn Add Credential]
C --> P[Portafolio público\n/u/:username]
P -->|verifyAll POST| R[VerificationReport[]]

Cada Certificate expone:

  • verifyUrlhttps://fluency.nappai.com/verify/:id (verificación pública sin auth)
  • linkedinUrl — URL prefabricada para añadir la credencial a LinkedIn (incluye organizationId: 99009889, año/mes de emisión y la certUrl)
  • verificationHash — SHA-256 generado en el backend: SHA-256(userId + pathId + issuedAt + score). Nunca se genera en el cliente.

El endpoint POST /public/portfolio/:username/verify-all devuelve un VerificationReport[] con el estado de validez de todos los certificados del portafolio, sin requerir autenticación.

Los tipos de certificado expuestos en el portafolio:

CertType en BDTipo público
PATH_CERTIFICATEPATH
SPECIALIZATION_CERTIFICATESPECIALIZATION
CORPORATE_CERTIFICATECORPORATE

Las habilidades se derivan automáticamente de los paths en los que el estudiante está matriculado, a partir del campo skills[] de cada Path. Una habilidad queda marcada como verificada (verified: true) si el estudiante tiene un certificado del path que la define.

// Estructura devuelta por la API
interface SkillItem {
name: string // e.g. "Prompt Engineering"
verified: boolean // true si hay certificado del path origen
certId: string | null
}

La línea de tiempo (timeline) agrega los 10 eventos más recientes de tres tipos:

TipoEvento
certificateCertificado obtenido
badgeBadge conseguido
enrollmentInscripción en una ruta

Ruta: apps/web/src/pages/public/StudentPortfolioPage.tsx

Layout de dos columnas en desktop (1fr + 320px sidebar), una columna en móvil (< 860px). Gestiona tres estados:

  • Cargando — spinner Spinner
  • Error / privadoErrorState con CTA al catálogo
  • Portafolio activo — grid completo con todas las secciones

Soporta impresión en PDF (window.print()) mediante media queries @media print que ocultan navegación, botones y sidebar.

Ruta: apps/web/src/pages/settings/sections/PortfolioSection.tsx

Permite al estudiante:

  • Activar/desactivar el portafolio público
  • Elegir o generar automáticamente un username
  • Editar bio, ubicación, links (web, Twitter/X, GitHub)
  • Controlar la visibilidad de cada sección mediante toggles
  • Ver el contador de visitas de su portafolio

El botón “Auto-generar” llama a POST /student/portfolio/generate-username, que produce un slug basado en el fullName del usuario (normalizado, sin acentos, con guiones). Si hay colisión, añade sufijo numérico (name2, name3, etc.).


GET /api/public/portfolio/:username Devuelve el portafolio completo del estudiante (solo si isPublic = true)
POST /api/public/portfolio/:username/verify-all Verifica todos los certificados del portafolio y devuelve un informe de validez

Configuración del estudiante (requiere JWT)

Section titled “Configuración del estudiante (requiere JWT)”
GET /api/student/portfolio/settings Obtiene la configuración actual del portafolio del usuario autenticado
PATCH /api/student/portfolio/settings Actualiza configuración: username, isPublic, bio, links, toggles de visibilidad
GET /api/student/portfolio/check-username?username=:u Comprueba disponibilidad de un username; devuelve {available, suggestion?}
POST /api/student/portfolio/generate-username Auto-genera un username a partir del nombre completo del usuario

model StudentProfile {
id String @id @default(uuid())
userId String @unique
user User @relation(...)
username String @unique // 3-40 chars, [a-zA-Z0-9-]
isPublic Boolean @default(false)
bio String? @db.Text
location String?
websiteUrl String?
twitterUrl String?
githubUrl String?
// Toggles de visibilidad
showCertificates Boolean @default(true)
showBadges Boolean @default(true)
showPaths Boolean @default(true)
showTimeline Boolean @default(true)
showXP Boolean @default(true)
showScore Boolean @default(true)
usernameChangedAt DateTime? // cooldown 30 días para cambios
viewCount Int @default(0)
}

apps/web/src/store/api/portfolioApi.ts
useGetPublicPortfolioQuery(username) // carga portafolio público (cache 120s)
useVerifyAllMutation() // verifica todos los certificados
useGetPortfolioSettingsQuery() // ajustes del usuario autenticado
useUpdatePortfolioSettingsMutation() // guarda ajustes
useCheckUsernameQuery(username) // comprueba disponibilidad
useGenerateUsernameMutation() // auto-genera username