diff --git a/docs/raffle-linkedin.md b/docs/raffle-linkedin.md new file mode 100644 index 0000000..44cef44 --- /dev/null +++ b/docs/raffle-linkedin.md @@ -0,0 +1,194 @@ +## Sorteo de merchandising desde reacciones de LinkedIn + +Este documento recoge el análisis, las limitaciones y la propuesta de implementación para añadir a la web un apartado que permita "sortear" (elegir aleatoriamente) un usuario de LinkedIn que haya reaccionado a un post concreto. + +--- + +## Resumen rápido + +- Objetivo: crear una página que permita introducir la URL de un post de LinkedIn y, al pulsar "Sortear", devuelva aleatoriamente un usuario que haya reaccionado al post. +- Restricción clave: la API oficial de LinkedIn permite recuperar acciones sociales (reacciones, comentarios) solo cuando el post es del miembro autenticado o cuando el miembro autenticado está mencionado. No es posible, en general, consultar las reacciones de cualquier post público sin permisos especiales. +- Recomendación MVP: implementar primero un modo "manual" (front-end) para elegir aleatoriamente entre una lista pegada por el organizador. En 2ª fase, integrar OAuth + backend para automatizar con LinkedIn cuando sea posible. + +--- + +## Hallazgos técnicos (apoyados en la documentación oficial) + +- Endpoint relevante: `GET https://api.linkedin.com/v2/socialActions/{postURN}` — devuelve resumen de acciones sociales del post. +- Condición de acceso habitual: el post debe ser propiedad del miembro autenticado o el miembro autenticado debe estar mencionado en el post. En caso contrario la API devuelve 403. +- Las APIs de comunidad/marketing tienen límites de uso y scopes que requieren registrar una app y posiblemente pasar por revisión de LinkedIn. +- Alternativas no oficiales (scraping, servicios de terceros) existen pero presentan riesgos de TOS y fragilidad. + +--- + +## Opciones de diseño (evaluación) + +1) OAuth + Backend (recomendado para automatización completa) + - Pros: automático, UX superior. + - Contras: requiere registrar app, manejar OAuth, alojar backend serverless o servidor, posible revisión por LinkedIn y permisos adicionales. + - Notas: en muchos casos solo funcionará si el autor del post autoriza la app. + +2) Frontend-only con PKCE + - Pros: evita backend para el flujo de autorización. + - Contras: CORS, tokens en cliente y mismas limitaciones de acceso a posts ajenos. + +3) Modo manual (MVP) + - Pros: inmediato, funciona sin credenciales ni backend. + - Contras: trabajo manual por parte del organizador (copiar/pegar la lista de participantes). + +4) Scraping o terceros (no recomendado) + - Pros: puede recuperar datos sin consentimiento explícito del autor. + - Contras: riesgo legal/terms of service, fragilidad y coste. + +--- + +## Arquitectura propuesta + +MVP (rápido, se puede integrar hoy mismo en `ghspain.github.io`): +- Frontend React (nueva página) en `src/components/RafflePage.tsx`. +- UI: campo para URL del post, modo selección: "Manual" (textarea/CSV) y botón "Sortear". +- Lógica: parsear lista de participantes, validaciones mínimas, seleccionar aleatorio y mostrar resultado. +- No requiere backend; funciona en GitHub Pages. + +Automatización completa (fase 2): +- Backend serverless (Vercel/Netlify/Cloudflare) con endpoints: + - OAuth callbacks y almacenamiento seguro de tokens. + - `POST /api/raffle` que recibe `postUrl` y devuelve participante aleatorio usando `socialActions/{postURN}`. +- Frontend: botones para iniciar OAuth y solicitar sorteo al backend. +- Requisitos: `client_id`, `client_secret`, `REDIRECT_URI`, scopes apropiados y posible revisión de app por LinkedIn. + +--- + +## Contrato (inputs/outputs) + +- Endpoint (backend) — `POST /api/raffle` + - Input JSON: + - `postUrl` (string): URL del post de LinkedIn. + - `mode` ("api" | "manual") + - `participants` (Array) — solo en modo manual: lista de participantes { name?, profileUrl?, id? } + - Output JSON: + - `winner`: { name?, profileUrl?, urn?, avatar? } + - `count`: número de participantes + - `seed?`: opcional, para reproducibilidad + - `error?`: mensaje en caso de fallo + +- Frontend (UI): + - Input: URL del post o textarea con lista manual. + - Output visible: tarjeta con ganador y botón para abrir su perfil. + +--- + +## Casos límite y verificaciones + +- Post no perteneciente al token autenticado → la API devuelve 403. Mostrar mensaje claro y fallback al modo manual. +- Post sin reacciones → informar (no hay participantes). +- Reacciones paginadas → backend debe manejar paginación para recopilar la lista completa. +- Rate limits → aplicar backoff/retries y exponer límites al usuario. +- Privacidad → no exponer tokens en cliente; registrar el mínimo requerido y borrar logs sensibles. + +--- + +## Pasos técnicos para implementar el MVP manual (rápido) + +1. Añadir componente React `src/components/RafflePage.tsx` con: + - Campo `postUrl` (solo informativo) + - Textarea para pegar participantes (una línea por perfil o JSON) + - Botón "Sortear" que valida y selecciona aleatorio + - Mostrar resultado con link al perfil +2. Añadir enlace a `Navigation.tsx` para acceder a la nueva página +3. Añadir test mínimo en `src/__tests__` o usar `App.test.tsx` como referencia +4. Hacer build y comprobar `npm run build` + +--- + +## Implementaciones realizadas (MVP) + +He implementado un MVP manual y adapté la navegación para que el sorteo sea una página separada. Cambios realizados en el repositorio: + +- `src/components/RaffleSection.tsx` + - Nuevo componente React que contiene: + - campo `postUrl` (informativo), + - `textarea` para pegar la lista manual de participantes (una línea por participante), + - botón "Sortear (manual)" que selecciona aleatoriamente un participante y muestra el resultado. + +- `src/components/index.ts` + - Exportado `RaffleSection` desde el índice de componentes para poder importarlo desde otras páginas. + +- `src/pages/RafflePage.tsx` + - Nueva página dedicada que renderiza únicamente `RaffleSection`. Accesible en la ruta `/raffle`. + +- `src/App.tsx` + - Modificado para renderizar la página independiente cuando `window.location.pathname === '/raffle'`. Si no, renderiza la web normal. + +- `src/components/Navigation.tsx` + - El enlace `Sorteo` ahora apunta a `/raffle` y permanece oculto por defecto con `style={{ display: 'none' }}`. Cuando lo actives en la navegación, llevará a la página independiente. + +- `docs/raffle-linkedin.md` + - Actualizado (este archivo) para incluir el resultado del trabajo y la documentación del MVP. + +Resultado de la verificación: + +- Ejecuté `npm run build` después de los cambios y la compilación fue exitosa. Aparecieron advertencias de ESLint ya existentes en otros componentes, pero no hubo errores de compilación. + +Cómo activar la navegación / reactivar el enlace "Sorteo": + +1. Editar el archivo `src/components/Navigation.tsx` y eliminar `style={{ display: 'none' }}` del enlace: + + - Antes: + + ```tsx + Sorteo + ``` + + - Después: + + ```tsx + Sorteo + ``` + +2. (Opcional, recomendado) Alternativa más flexible: renderizar el enlace condicionalmente usando una variable de entorno (por ejemplo `process.env.REACT_APP_ENABLE_RAFFLE === 'true'`). Esto permite activarlo sin tocar el código fuente, solo cambiando la variable y recompilando. + +Notas y próximos pasos recomendados: + +- Si quieres automatizar la obtención de participantes desde LinkedIn, la siguiente fase es preparar el backend/OAuth (ver la sección "Pasos para automatizar con LinkedIn (fase 2)"). +- Puedo ahora: + - hacer el cambio para activar el enlace mediante una variable de entorno (rápido), o + - pulir estilos del componente para que encaje con el resto del sitio, o + - preparar el esqueleto del backend serverless para iniciar el flujo OAuth. + + +## Pasos para automatizar con LinkedIn (fase 2) + +1. Registrar app en LinkedIn Developers (obtener `client_id` y `client_secret`) +2. Definir scopes necesarios (ej. lectura de socialActions — revisar docs y requerimientos de LinkedIn) +3. Implementar backend serverless: + - Endpoint para iniciar OAuth + - Callback que almacena token de manera segura + - Endpoint `POST /api/raffle` que acepta `postUrl`, obtiene `postURN`, llama `GET /v2/socialActions/{postURN}` y recupera reacciones + - Manejar paginación y transformar reacciones a lista de participantes + - Seleccionar aleatorio y devolver resultado +4. Desplegar backend y actualizar frontend para llamar al endpoint +5. Documentar el flujo y permisos necesarios + +--- + +## Requerimientos / Preguntas para el usuario + +- ¿Quieres que implemente primero el MVP manual integrado en este repo? (recomendado — muy rápido y se puede usar ya en GitHub Pages) +- Si prefieres la integración automática: ¿vas a registrar la app de LinkedIn o quieres que te guíe para hacerlo? Necesitaré `client_id`, `client_secret` y la `REDIRECT_URI` para pruebas. +- ¿Deseas que la página esté enlazada desde la navegación principal (`Navigation.tsx`) o solo accesible por URL inicialmente? + +--- + +## Siguientes pasos sugeridos + +- Si eliges MVP manual: implemento `RafflePage.tsx`, añado enlace en `Navigation.tsx`, pruebo `npm run build` y subo los cambios. +- Si eliges automatizar: preparo el esqueleto de la función serverless y documentación para registrar la app en LinkedIn. + +--- + +## Notas finales + +- Evitar scraping: recomendamos la vía OAuth+API o el modo manual para respetar TOS y privacidad. +- Este documento está en `docs/docs/feats/raffle-linkedin.md` dentro del repositorio. Copia o adapta el contenido para añadir instrucciones de despliegue o ejemplos concretos de payload cuando configuremos el backend. + diff --git a/src/App.tsx b/src/App.tsx index eab98bc..7284d26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import '@primer/react-brand/lib/css/main.css' import '@primer/react-brand/fonts/fonts.css' import { ThemeProvider } from '@primer/react-brand'; import { MinimalFooter, Navigation, TimelineSection, HeroSection, CTASection, CardsSection, RiverSection } from './components'; +import RafflePage from './pages/RafflePage'; const designTokenOverrides = ` .custom-colors[data-color-mode='dark'] { @@ -27,20 +28,29 @@ function App() { return ( -
- - - - - - - +
+ {/* Render a standalone raffle page when path is /raffle */} + {typeof window !== 'undefined' && window.location && window.location.pathname === '/raffle' ? ( + + ) : ( + <> + + + + + + + + + )}
) diff --git a/src/components/RaffleSection.tsx b/src/components/RaffleSection.tsx new file mode 100644 index 0000000..91e6919 --- /dev/null +++ b/src/components/RaffleSection.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; + +const parseParticipants = (text: string) => { + // Allow one participant per line. Trim and filter empties. + return text + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); +}; + +const RaffleSection: React.FC = () => { + const [postUrl, setPostUrl] = useState(''); + const [manualList, setManualList] = useState(''); + const [winner, setWinner] = useState(null); + const [error, setError] = useState(null); + + const handleDraw = () => { + setError(null); + setWinner(null); + const participants = parseParticipants(manualList); + if (participants.length === 0) { + setError('No hay participantes en la lista manual. Pega una línea por participante.'); + return; + } + const idx = Math.floor(Math.random() * participants.length); + setWinner(participants[idx]); + }; + + return ( +
+
+

Sorteo de merchandising

+

Introduce la URL del post de LinkedIn (opcional) y pega la lista de participantes abajo (una línea por participante). Usa el modo manual por ahora.

+ + + setPostUrl(e.target.value)} + placeholder="https://www.linkedin.com/posts/..." + style={{ width: '100%', padding: 8, borderRadius: 6, border: '1px solid #ccc' }} + /> + + +