|
| 1 | +# Plan: UI Improvements - Logo, NutriScore, Footer |
| 2 | + |
| 3 | +## Contexto |
| 4 | + |
| 5 | +El workshop está completo (5 iteraciones, 13 tests, visual-fixes-v2 aplicado). Este plan |
| 6 | +implementa un lote de mejoras UI: logo de Mercadona en cabecera, extracción del componente |
| 7 | +NutriScore a componente reutilizable, logo clicable con test dedicado, y footer disclaimer |
| 8 | +para demos. |
| 9 | + |
| 10 | +**Decisiones tomadas:** |
| 11 | +- NutriScore: badge simple (extraer a componente, mantener visual actual de pill coloreada) |
| 12 | +- Logo clicable: test dedicado nuevo en iteration-3-solution |
| 13 | +- Footer: sin test, solo para demo |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## F0: Cambios en master |
| 18 | + |
| 19 | +### F0.1 Logo Mercadona en Navigation |
| 20 | +- [x] Reemplazar `<h1 className="navigation__title">TDD Workshop</h1>` por `<img>` |
| 21 | +- [x] Usar `<img src="/mercadona-logo.svg" alt="Mercadona" className="navigation__logo" />` |
| 22 | +- [x] Añadir CSS: `.navigation__logo { max-width: 230px; height: auto; }` |
| 23 | +- [x] Eliminar `.navigation__title` del CSS (ya no se usa) |
| 24 | + |
| 25 | +**Archivos:** |
| 26 | +- `src/components/navigation/Navigation.tsx` |
| 27 | +- `src/components/navigation/Navigation.css` |
| 28 | + |
| 29 | +### F0.2 Footer disclaimer en App |
| 30 | +- [x] Añadir `<footer>` después de `<main>` en `app.tsx` |
| 31 | +- [x] Texto: "La información de los productos ha sido generada con IA y no refleja datos reales. Solo con fines de testing." |
| 32 | +- [x] Crear estilos en `src/styles/globals.css` (estilo discreto, gris, centrado, font-size small) |
| 33 | + |
| 34 | +**Archivos:** |
| 35 | +- `src/app.tsx` |
| 36 | +- `src/styles/globals.css` (añadir estilos de `.disclaimer`) |
| 37 | + |
| 38 | +### F0.3 Commit y verificación |
| 39 | +- [x] `npm run typecheck && npm run lint && npm test` en master |
| 40 | +- [ ] Verificar visualmente que el logo se muestra correctamente |
| 41 | +- [x] Commit |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## F1: Cascade master → todas las ramas |
| 46 | + |
| 47 | +Merge en cadena. En cada paso resolver conflictos si los hay. |
| 48 | + |
| 49 | +- [x] `master` → `iteration-1-start` |
| 50 | +- [x] `iteration-1-start` → `iteration-1-solution` |
| 51 | +- [x] `iteration-1-solution` → `iteration-2-start` |
| 52 | +- [x] `iteration-2-start` → `iteration-2-solution` |
| 53 | +- [x] `iteration-2-solution` → `iteration-3-start` |
| 54 | +- [x] `iteration-3-start` → `iteration-3-solution` |
| 55 | +- [x] `iteration-3-solution` → `iteration-4-start` |
| 56 | +- [x] `iteration-4-start` → `iteration-4-solution` |
| 57 | +- [x] `iteration-4-solution` → `iteration-5-start` |
| 58 | +- [x] `iteration-5-start` → `iteration-5-solution` |
| 59 | + |
| 60 | +**Conflictos esperados:** |
| 61 | +- `app.tsx` en iter-3+ (master tiene `<>header+main+footer</>`, iter-3+ tiene `<AppRoutes />`). |
| 62 | + Resolución: mover footer a `RootLayout.tsx` en iter-3+. |
| 63 | + |
| 64 | +**Verificación por rama:** `npm run typecheck && npm test` tras cada merge. |
| 65 | + |
| 66 | +--- |
| 67 | + |
| 68 | +## F2: Componente NutriScore en iteration-2-solution |
| 69 | + |
| 70 | +### F2.1 Crear componente |
| 71 | +- [x] Crear `src/components/nutri-score/NutriScore.tsx` |
| 72 | +- [x] Crear `src/components/nutri-score/NutriScore.css` (mover estilos de nutriscore desde ProductCard.css) |
| 73 | +- [x] Crear `src/components/nutri-score/index.ts` (barrel export) |
| 74 | + |
| 75 | +**NutriScore.tsx:** |
| 76 | +```tsx |
| 77 | +import classNames from 'classnames' |
| 78 | +import './NutriScore.css' |
| 79 | + |
| 80 | +interface NutriScoreProps { |
| 81 | + score: string |
| 82 | + showLabel?: boolean |
| 83 | +} |
| 84 | + |
| 85 | +export const NutriScore = ({ score, showLabel = false }: NutriScoreProps) => ( |
| 86 | + <span className={classNames('nutri-score', `nutri-score--${score.toLowerCase()}`)}> |
| 87 | + {showLabel && 'Nutriscore: '}{score} |
| 88 | + </span> |
| 89 | +) |
| 90 | +``` |
| 91 | + |
| 92 | +**NutriScore.css** (extraído de ProductCard.css): |
| 93 | +```css |
| 94 | +.nutri-score { |
| 95 | + display: inline-flex; |
| 96 | + align-items: center; |
| 97 | + justify-content: center; |
| 98 | + padding: var(--spacing-xs) var(--spacing-sm); |
| 99 | + border-radius: var(--border-radius-sm); |
| 100 | + font-size: var(--font-size-sm); |
| 101 | + font-weight: 700; |
| 102 | + color: var(--color-white); |
| 103 | + text-transform: uppercase; |
| 104 | +} |
| 105 | + |
| 106 | +.nutri-score--a { background-color: #038141; } |
| 107 | +.nutri-score--b { background-color: #85bb2f; } |
| 108 | +.nutri-score--c { background-color: #fecb02; color: var(--color-text); } |
| 109 | +.nutri-score--d { background-color: #ee8100; } |
| 110 | +.nutri-score--e { background-color: #e63e11; } |
| 111 | +``` |
| 112 | + |
| 113 | +### F2.2 Refactor ProductCard |
| 114 | +- [x] Reemplazar el `<span>` de nutriscore por `<NutriScore score={nutriscore} />` |
| 115 | +- [x] Eliminar estilos `.product-card__nutriscore*` de `ProductCard.css` |
| 116 | + |
| 117 | +**En iter-2 (badge siempre visible):** |
| 118 | +```tsx |
| 119 | +<NutriScore score={nutriscore} /> |
| 120 | +``` |
| 121 | + |
| 122 | +**Archivos:** |
| 123 | +- `src/components/nutri-score/NutriScore.tsx` (nuevo) |
| 124 | +- `src/components/nutri-score/NutriScore.css` (nuevo) |
| 125 | +- `src/components/nutri-score/index.ts` (nuevo) |
| 126 | +- `src/components/product-card/ProductCard.tsx` (refactor) |
| 127 | +- `src/components/product-card/ProductCard.css` (eliminar estilos nutriscore) |
| 128 | + |
| 129 | +### F2.3 Verificación y commit |
| 130 | +- [x] `npm run typecheck && npm test` en iteration-2-solution |
| 131 | +- [x] Commit |
| 132 | + |
| 133 | +--- |
| 134 | + |
| 135 | +## F3: Cascade iteration-2-solution → ramas siguientes |
| 136 | + |
| 137 | +- [x] `iteration-2-solution` → `iteration-3-start` |
| 138 | +- [x] `iteration-3-start` → `iteration-3-solution` |
| 139 | +- [x] `iteration-3-solution` → `iteration-4-start` |
| 140 | +- [x] `iteration-4-start` → `iteration-4-solution` |
| 141 | +- [x] `iteration-4-solution` → `iteration-5-start` |
| 142 | +- [x] `iteration-5-start` → `iteration-5-solution` |
| 143 | + |
| 144 | +**Conflictos esperados:** |
| 145 | +- `ProductCard.tsx` en iter-4+ (iter-4 usa `showLabel` con `isListView`). |
| 146 | + Resolución: usar `<NutriScore score={nutriscore} showLabel />` en la versión condicional. |
| 147 | +- `ProductCard.css` en todas: los estilos nutriscore se eliminaron, resolver a favor de la eliminación. |
| 148 | + |
| 149 | +**Verificación:** `npm run typecheck && npm test` tras cada merge. |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## F4: Logo clicable + test en iteration-3-solution |
| 154 | + |
| 155 | +### F4.1 Link en el logo |
| 156 | +- [x] En `Navigation.tsx` de iter-3-solution, envolver el logo en `<Link to="/">` |
| 157 | +- [x] Añadir CSS para que el link no tenga estilos de anchor |
| 158 | + |
| 159 | +```tsx |
| 160 | +import { Link, NavLink } from 'react-router-dom' |
| 161 | + |
| 162 | +// Dentro del JSX: |
| 163 | +<Link to="/" className="navigation__logo-link"> |
| 164 | + <img src="/mercadona-logo.svg" alt="Mercadona" className="navigation__logo" /> |
| 165 | +</Link> |
| 166 | +``` |
| 167 | + |
| 168 | +**CSS:** |
| 169 | +```css |
| 170 | +.navigation__logo-link { |
| 171 | + display: inline-flex; |
| 172 | + align-items: center; |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +### F4.2 Test dedicado |
| 177 | +- [x] Añadir test en `src/pages/category-detail/__tests__/CategoryDetail.test.tsx` |
| 178 | + (archivo donde viven los tests de navegación en iter-3) |
| 179 | + |
| 180 | +```typescript |
| 181 | +it('should navigate to home when clicking the logo', async () => { |
| 182 | + const user = userEvent.setup() |
| 183 | + render(<App />) |
| 184 | + |
| 185 | + await clickCategory(user, 'Fruta y verdura') |
| 186 | + await screen.findByRole('heading', { name: 'Fruta y verdura' }) |
| 187 | + |
| 188 | + const logo = screen.getByRole('link', { name: 'Mercadona' }) |
| 189 | + await user.click(logo) |
| 190 | + |
| 191 | + expect(screen.queryByRole('heading', { name: 'Fruta y verdura' })).not.toBeInTheDocument() |
| 192 | + expect(await screen.findByText('Fruta y verdura')).toBeVisible() |
| 193 | +}) |
| 194 | +``` |
| 195 | + |
| 196 | +El test: |
| 197 | +1. Navega a una categoría (para estar fuera de home) |
| 198 | +2. Clickea el logo (link con alt="Mercadona") |
| 199 | +3. Verifica que ya no está en la página de categoría (no heading de categoría) |
| 200 | +4. Verifica que vuelve a home (categorías visibles como enlaces, no como heading) |
| 201 | + |
| 202 | +**Nota:** En master el logo NO es clicable (Navigation está fuera del Router). |
| 203 | +Solo en iter-3+ donde RootLayout está dentro del Router se añade el `<Link>`. |
| 204 | + |
| 205 | +### F4.3 Verificación y commit |
| 206 | +- [x] `npm run typecheck && npm test` en iteration-3-solution |
| 207 | +- [x] Commit |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## F5: Cascade iteration-3-solution → ramas siguientes |
| 212 | + |
| 213 | +- [x] `iteration-3-solution` → `iteration-4-start` |
| 214 | +- [x] `iteration-4-start` → `iteration-4-solution` |
| 215 | +- [x] `iteration-4-solution` → `iteration-5-start` |
| 216 | +- [x] `iteration-5-start` → `iteration-5-solution` |
| 217 | + |
| 218 | +**Verificación:** `npm run typecheck && npm test` tras cada merge. |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## F6: NutriScore en ProductDetail (iteration-5-solution) |
| 223 | + |
| 224 | +### F6.1 Integrar NutriScore en ProductDetail |
| 225 | +- [x] Añadir `<NutriScore score={product.nutriscore} showLabel />` en `ProductDetail.tsx` |
| 226 | +- [x] Eliminar los estilos duplicados `.product-detail__nutriscore-badge--*` de `ProductDetail.css` |
| 227 | + (ya no se necesitan, el componente trae sus propios estilos) |
| 228 | + |
| 229 | +```tsx |
| 230 | +import { NutriScore } from 'components/nutri-score' |
| 231 | + |
| 232 | +// Dentro del JSX, después de price: |
| 233 | +{product.nutriscore && <NutriScore score={product.nutriscore} showLabel />} |
| 234 | +``` |
| 235 | + |
| 236 | +### F6.2 Verificación y commit |
| 237 | +- [x] `npm run typecheck && npm test` en iteration-5-solution |
| 238 | +- [x] Commit |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## F7: Push y verificación final |
| 243 | + |
| 244 | +- [x] Push de todas las ramas |
| 245 | +- [ ] Verificar que CI pasa (si hay) |
| 246 | +- [ ] Revisión visual rápida de: |
| 247 | + - master: logo + footer visible |
| 248 | + - iter-3: logo clicable → home |
| 249 | + - iter-2: NutriScore badge en ProductCard |
| 250 | + - iter-4: NutriScore con label en list view |
| 251 | + - iter-5: NutriScore en ProductDetail modal |
| 252 | + |
| 253 | +--- |
| 254 | + |
| 255 | +## Resumen de archivos afectados |
| 256 | + |
| 257 | +| Archivo | Rama origen | Cambio | |
| 258 | +|---------|-------------|--------| |
| 259 | +| `src/components/navigation/Navigation.tsx` | master | Logo `<img>` reemplaza `<h1>` | |
| 260 | +| `src/components/navigation/Navigation.css` | master | Estilos logo, eliminar title | |
| 261 | +| `src/app.tsx` | master | Footer disclaimer | |
| 262 | +| `src/styles/globals.css` | master | Estilos disclaimer | |
| 263 | +| `src/components/nutri-score/NutriScore.tsx` | iter-2-solution | Componente nuevo | |
| 264 | +| `src/components/nutri-score/NutriScore.css` | iter-2-solution | Estilos extraídos | |
| 265 | +| `src/components/nutri-score/index.ts` | iter-2-solution | Barrel export | |
| 266 | +| `src/components/product-card/ProductCard.tsx` | iter-2-solution | Usa `<NutriScore>` | |
| 267 | +| `src/components/product-card/ProductCard.css` | iter-2-solution | Eliminar estilos nutriscore | |
| 268 | +| `src/components/navigation/Navigation.tsx` | iter-3-solution | Link en logo | |
| 269 | +| `src/components/navigation/Navigation.css` | iter-3-solution | Estilos logo-link | |
| 270 | +| `CategoryDetail.test.tsx` | iter-3-solution | Test logo → home | |
| 271 | +| `src/pages/routes/RootLayout.tsx` | iter-3-solution+ | Footer (merge conflict) | |
| 272 | +| `src/components/product-detail/ProductDetail.tsx` | iter-5-solution | Integra `<NutriScore>` | |
| 273 | +| `src/components/product-detail/ProductDetail.css` | iter-5-solution | Eliminar estilos duplicados | |
| 274 | + |
| 275 | +--- |
| 276 | + |
| 277 | +## Notas de contexto para implementación |
| 278 | + |
| 279 | +**Estructura de routing (importante para el logo clicable):** |
| 280 | +- **master**: `app.tsx` tiene `<>header+main</>` con Navigation FUERA del Router |
| 281 | +- **iter-3+**: `app.tsx` tiene solo `<AppRoutes />`, con `RootLayout.tsx` DENTRO del Router |
| 282 | +- Por eso el `<Link>` en el logo solo es posible desde iter-3 en adelante |
| 283 | + |
| 284 | +**NutriScore actual:** |
| 285 | +- iter-2: badge siempre visible (solo letra), usa template literal para clase CSS |
| 286 | +- iter-4: badge condicional solo en list view, muestra "Nutriscore: X" |
| 287 | +- iter-5 ProductDetail: NO tiene nutriscore (CSS skeleton existe pero no se usa) |
| 288 | + |
| 289 | +--- |
| 290 | + |
| 291 | +## Retrospectiva |
| 292 | + |
| 293 | +### Aprendizajes |
| 294 | +- Los merge cascades en cadena son mecánicos pero requieren atención: cada conflicto en Navigation.tsx o ProductCard.tsx tiene un patrón claro (combinar ambas versiones) |
| 295 | +- El footer en iter-3+ va en RootLayout, no en app.tsx (la app ya delegó todo al router) |
| 296 | +- El test del logo clicable necesita `await screen.findByText(...)` antes de `clickCategory` (mismo patrón que los otros tests de navegación — las categorías son async) |
| 297 | +- Import relativo `../../types` en ProductDetail.tsx: corregido de paso a import absoluto |
| 298 | + |
| 299 | +### Fricciones |
| 300 | +- Al inicio de la sesión olvidé actualizar el plan mientras avanzaba — el usuario lo señaló y se corrigió el hábito durante la sesión |
| 301 | +- CLAUDE.md generó conflicto de merge en iter-1-start: resuelto a favor de master (versión más actualizada) |
| 302 | + |
| 303 | +### Decisiones de código |
| 304 | +- `NutriScore` con `showLabel` prop cubre los tres casos: badge solo (iter-2), badge con label (iter-4 list view, iter-5 modal) |
| 305 | +- No se añadió lógica extra en iter-5 para el `../../types` — era un fix obvio de calidad |
| 306 | + |
| 307 | +### CLAUDE.md |
| 308 | +- Sin cambios necesarios: las convenciones ya cubren todo lo implementado |
0 commit comments