diff --git a/CHANGELOG.txt b/CHANGELOG.txt index be61c50..e236c2b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,14 @@ +1.1.0 +- Style: Full responsive layout for mobile, tablet, and desktop screens +- Style: Sidebar converts to a slide-in overlay drawer on mobile with backdrop and close button +- Style: Topbar gains a hamburger menu button (hidden on md+ breakpoint) to toggle the sidebar +- Style: Dashboard heading and grid padding adapt to screen size (p-3 on mobile, p-6 on sm+) +- Style: ResourceCard touch targets enlarged (bigger padding on status and open buttons) +- Style: Sidebar nav items have larger tap areas with gap and rounded background on active state +- Fix: Test expectation for ResourceCard "Abrir" link text aligned with component output +- Docs: Add docs/business_context.md — non-technical business context in Spanish +- Chore: Remove docs/ai/implementation-instructions.md and docs/ai/ directory + 1.0.0 - Feat: Complete architectural refactor to Bookmark Manager PWA - Feat: Replace Bootstrap/SCSS stack with TailwindCSS 4 (class-based dark mode via @custom-variant) diff --git a/README.md b/README.md index 370be01..0e253ea 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A fast, offline-capable PWA to explore and manage a curated collection of 1000+ - **Status tracking** — cycle through states: Pendiente → Consumido → Referencia - **Category filtering** — browse resources by category via sidebar navigation - **Dark mode** — toggle between light and dark themes +- **Responsive design** — sidebar converts to a slide-in overlay on mobile; hamburger menu in topbar; optimized touch targets throughout - **PWA** — installable, works offline with service worker - **Virtualization-ready** — designed to handle thousands of resources @@ -51,7 +52,7 @@ pnpm dev ``` src/ -├── app/ # App.tsx (main application shell) +├── app/ # App.tsx (main application shell + sidebar toggle state) ├── components/ # UI components (ResourceCard, SearchBar, Sidebar, Topbar, TagList, CategoryList) ├── hooks/ # Custom hooks (useResources, useFavorites, useStatuses, useSearch) ├── pages/ # Pages (Dashboard, NotFound) @@ -61,6 +62,9 @@ src/ ├── utils/ # Utility functions (url, date) ├── data/ # resources.json (read-only source of truth) └── test/ # Test setup and utilities + +docs/ +└── business_context.md # Non-technical business context (Spanish) ``` ## Architecture diff --git a/docs/ai/implementation-instructions.md b/docs/ai/implementation-instructions.md deleted file mode 100644 index be930f8..0000000 --- a/docs/ai/implementation-instructions.md +++ /dev/null @@ -1,541 +0,0 @@ -# Bookmark Manager PWA — AI Implementation Instructions - -Este documento describe cómo un agente de IA debe implementar la aplicación **Bookmark Manager PWA**. - -La aplicación gestiona miles de enlaces web almacenados en un **archivo JSON estático**. - -⚠️ IMPORTANTE - -La aplicación **NO tiene backend** y **NO permite crear ni editar recursos desde la UI**. - -Los recursos solo pueden agregarse manualmente editando el archivo: - -``` -src/data/resources.json -``` - -La aplicación solo permite al usuario: - -- marcar favoritos -- cambiar estados de consumo -- buscar recursos -- explorar categorías - -Todos los cambios del usuario se almacenan en: - -``` -localStorage -``` - ---- - -# 1. Objetivo del Proyecto - -Crear una **aplicación web PWA** para visualizar y gestionar marcadores web. - -El usuario mantiene una colección de más de **1000 enlaces** almacenados en un archivo JSON. - -La aplicación debe permitir: - -- explorar recursos -- buscar rápidamente -- marcar favoritos -- marcar contenido pendiente -- marcar contenido consumido - -Pero **NO modificar el JSON original**. - ---- - -# 2. Restricción Fundamental - -El archivo JSON es **fuente de verdad de los recursos**. - -El usuario: - -- solo puede editar el JSON manualmente -- la aplicación solo lo lee - -Todos los estados dinámicos se almacenan en: - -``` -localStorage -``` - ---- - -# 3. Stack Tecnológico - -### Base - -- Vite -- React -- TypeScript - -### UI - -- TailwindCSS - -### Búsqueda - -- Fuse.js - -### Estado - -- Zustand - -### Performance - -- react-window o react-virtual - -### PWA - -- vite-plugin-pwa - -### Testing - -- Vitest -- React Testing Library - ---- - -# 4. Principios de Arquitectura - -El código debe seguir: - -- SOLID principles -- Clean Code -- Separation of Concerns -- Functional components -- Custom hooks -- Modular architecture -- DRY -- KISS -- YAGNI - -El objetivo es un código: - -- mantenible -- escalable -- testeable -- legible - ---- - -# 5. Reglas de Idioma - -### Código - -Todo el **código debe estar en inglés**: - -- variables -- funciones -- clases -- interfaces -- no hay comentarios - -### Interfaz - -Todos los **textos visibles al usuario deben estar en español**. - -Ejemplo: - -```ts -const statusLabel = { - pending: "Pendiente", - consumed: "Consumido", - reference: "Referencia" -} -``` - ---- - -# 6. Modelo de Datos - -El archivo JSON contiene únicamente la información base del recurso. - -```ts -export interface Resource { - id: string - title: string - url: string - description?: string - category: string - tags: string[] - createdAt: string -} -``` - -El JSON **no contiene estados dinámicos**. - ---- - -# 7. Estados Dinámicos del Usuario - -Los estados se almacenan en **localStorage**. - -```ts -export type ResourceStatus = - | "pending" - | "consumed" - | "reference" -``` - -Estructura sugerida: - -```ts -interface UserState { - favorites: string[] - statuses: Record -} -``` - -Ejemplo: - -```json -{ - "favorites": ["1", "5"], - "statuses": { - "1": "pending", - "2": "consumed" - } -} -``` - ---- - -# 8. Persistencia - -## Recursos - -Cargar desde: - -``` -src/data/resources.json -``` - -Este archivo es **read-only**. - ---- - -## Estado del usuario - -Guardar en: - -``` -localStorage -``` - -Claves recomendadas: - -``` -bookmark_favorites -bookmark_statuses -``` - ---- - -# 9. Arquitectura del Proyecto - -Estructura recomendada: - -``` -src/ - -app/ -App.tsx -providers.tsx - -components/ -ResourceCard.tsx -SearchBar.tsx -Sidebar.tsx -TagList.tsx -CategoryList.tsx - -features/resources/ -components/ -hooks/ -services/ -selectors/ - -pages/ -Dashboard.tsx -Favorites.tsx -Pending.tsx -Consumed.tsx - -hooks/ -useResources.ts -useFavorites.ts -useStatuses.ts -useSearch.ts - -store/ -resourceStore.ts - -services/ -storageService.ts -searchService.ts -resourceService.ts - -utils/ -url.ts -date.ts - -data/ -resources.json - -types/ -resource.ts -userState.ts - -tests/ -``` -Refactoriza lo existente para seguir esta estructura modular y organizada. -Refactoriza el archivo resources.json actual y elimina pendings.json para que solo contenga la información base de los recursos, sin estados dinámicos. ---- - -# 10. Patrones de Diseño - -El agente debe aplicar estos patrones cuando sea apropiado: - -### Repository Pattern - -Para acceder a los recursos del JSON. - -``` -resourceRepository -``` - ---- - -### Service Layer - -Para lógica de negocio: - -``` -storageService -searchService -resourceService -``` - ---- - -### Custom Hooks Pattern - -Para encapsular lógica React: - -``` -useResources -useFavorites -useStatuses -useSearch -``` - ---- - -### State Management Pattern - -Usar Zustand para estado global. - ---- - -# 11. Componentes Principales - -## ResourceCard - -Debe mostrar: - -- favicon -- title -- domain -- tags -- category -- status -- botón favorito -- botón cambiar estado -- botón abrir enlace - ---- - -## Sidebar - -Navegación: - -``` -Todos -Pendientes -Consumidos -Favoritos -Categorías -``` - ---- - -## SearchBar - -Debe usar: - -``` -Fuse.js -``` - -Buscar sobre: - -- title -- description -- tags -- url -- category - ---- - -# 12. Performance - -La app debe manejar **miles de recursos**. - -Implementar virtualización con: - -``` -react-window -``` - -o - -``` -react-virtual -``` - ---- - -# 13. UI/UX - -Inspiración visual: - -- Notion -- Raindrop -- Linear - -Layout: - -``` -Sidebar -Topbar -Main Content -``` - -Topbar incluye: - -- buscador global - ---- - -# 14. Preview de Sitios - -Mostrar favicon existente. - -# 15. Testing (TDD) - -El agente debe escribir **tests unitarios** para: - -### Servicios - -- storageService -- searchService - -### Hooks - -- useFavorites -- useStatuses - -### Componentes críticos - -- ResourceCard -- SearchBar - -Herramientas: - -``` -Vitest -React Testing Library -``` - -Principios: - -- tests independientes -- mocks cuando sea necesario -- coverage de lógica crítica - ---- - -# 16. Clean Code Rules - -El código debe seguir estas reglas: - -- funciones pequeñas -- nombres descriptivos -- evitar duplicación -- separar lógica de UI -- evitar componentes gigantes -- máximo ~150 líneas por componente - ---- - -# 17. Funciones del Usuario - -El usuario puede: - -### Marcar favorito - -Guardar ID en localStorage. - ---- - -### Cambiar estado - -Estados: - -``` -pending -consumed -reference -``` - ---- - -### Buscar recursos - -Búsqueda fuzzy con Fuse.js. - ---- - -# 18. Funciones NO permitidas - -La aplicación **NO debe permitir**: - -- crear recursos -- editar recursos -- eliminar recursos -- modificar JSON - -El JSON solo se modifica manualmente. - ---- - -# 19. Gitflow - -Crea una rama y PR en ingles para todo lo anterior, u haz atomic commits siguiendo una estructura convencional clara. - -# 20. Objetivo Final - -Crear una **PWA rápida y mantenible** para explorar más de **1000 marcadores web**. - -Debe ser: - -- rápida -- offline -- escalable -- sin backend -- basada en JSON -- mantenible con buenas prácticas de ingeniería. \ No newline at end of file diff --git a/docs/business_context.md b/docs/business_context.md new file mode 100644 index 0000000..61f8af4 --- /dev/null +++ b/docs/business_context.md @@ -0,0 +1,82 @@ +# Gestor de Marcadores Web — Contexto de Negocio + +## ¿Qué es este proyecto? + +Es una aplicación personal para **explorar y organizar una colección de enlaces web** que un desarrollador ha ido guardando a lo largo del tiempo. + +Con el paso de los años, es fácil acumular cientos o miles de enlaces interesantes — tutoriales, herramientas, documentación, recursos de diseño, cursos, referencias técnicas — pero sin un sistema claro para organizarlos, terminan perdiéndose o siendo imposibles de encontrar cuando se necesitan. + +Esta aplicación resuelve ese problema. + +--- + +## ¿Para quién es? + +Para **una sola persona**: el dueño de la colección de marcadores. + +No es una plataforma colaborativa ni tiene usuarios registrados. Es una herramienta personal, como una libreta digital de recursos favoritos. + +--- + +## ¿Qué puede hacer el usuario? + +Una vez abierta la aplicación, el usuario puede: + +- **Explorar todos sus recursos** organizados por categorías (CSS, JavaScript, React, diseño, despliegue, etc.). +- **Buscar rápidamente** cualquier recurso escribiendo palabras clave — el buscador entiende búsquedas aproximadas, por lo que no hace falta escribir exactamente el nombre. +- **Marcar favoritos** para identificar los recursos más valiosos o consultados frecuentemente. +- **Cambiar el estado** de cada recurso: + - **Pendiente** — algo que quiere explorar o aprender próximamente. + - **Consumido** — algo que ya revisó o completó. + - **Referencia** — algo que consulta de forma recurrente, como una guía o documentación oficial. +- **Filtrar por estado** para ver únicamente los recursos pendientes, consumidos o favoritos. +- **Navegar por categorías** para explorar recursos agrupados por tema. +- **Cambiar entre modo claro y oscuro** según su preferencia o condiciones de luz. + +--- + +## ¿Qué NO puede hacer la aplicación? + +La aplicación es solo para **leer y organizar** — no para editar el catálogo de recursos: + +- **No permite agregar nuevos recursos** desde la pantalla. +- **No permite editar ni eliminar recursos** existentes. +- **No tiene un servidor propio** — no guarda datos en una base de datos externa. + +Los recursos del catálogo solo pueden modificarse editando manualmente un archivo de texto en el código del proyecto. + +--- + +## ¿Dónde se guardan los datos? + +Hay dos tipos de información: + +**El catálogo de recursos** — la lista de todos los enlaces — está guardado en un archivo dentro del proyecto. Es fijo y solo cambia cuando el dueño lo edita manualmente. + +**Las preferencias del usuario** — favoritos, estados (pendiente, consumido, referencia) y modo oscuro — se guardan directamente en el navegador del dispositivo que se usa. Esto significa que son privadas, no se comparten ni se sincronizan entre dispositivos. + +--- + +## ¿Cómo se accede? + +La aplicación está publicada en internet y es accesible desde cualquier navegador moderno, ya sea en computador, tableta o teléfono. + +Además, puede **instalarse como una aplicación** en el dispositivo (como si fuera una app del sistema) y funciona incluso **sin conexión a internet**, gracias a que guarda una copia local de los datos. + +--- + +## Valor que aporta + +| Problema | Solución | +|---|---| +| Tengo cientos de marcadores y no los encuentro | Búsqueda rápida y filtros por categoría | +| No sé qué recursos ya revisé | Estados: Pendiente / Consumido / Referencia | +| Quiero marcar mis favoritos del catálogo | Botón de favorito por recurso | +| Quiero usarlo desde el teléfono | Diseño adaptable a cualquier pantalla, instalable como app | +| No quiero depender de internet | Funciona offline una vez cargada | + +--- + +## Mantenimiento + +Agregar o quitar recursos del catálogo es una tarea manual y técnica — requiere editar el archivo de datos y volver a publicar la aplicación. Está pensado para que el dueño del proyecto lo haga de forma ocasional cuando quiera actualizar su colección. diff --git a/src/__tests__/components/ResourceCard.test.tsx b/src/__tests__/components/ResourceCard.test.tsx index 894461d..1c4c3d4 100644 --- a/src/__tests__/components/ResourceCard.test.tsx +++ b/src/__tests__/components/ResourceCard.test.tsx @@ -50,7 +50,7 @@ describe("ResourceCard", () => { it("should render open link with correct URL", () => { render() - const link = screen.getByText("Abrir ↗") + const link = screen.getByText("Abrir") expect(link).toHaveAttribute("href", "https://react.dev") expect(link).toHaveAttribute("target", "_blank") }) diff --git a/src/app/App.tsx b/src/app/App.tsx index 8f962a7..ab56d3a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,3 +1,4 @@ +import { useState } from "react" import { BrowserRouter, Routes, Route } from "react-router-dom" import { Sidebar } from "../components/Sidebar" import { Topbar } from "../components/Topbar" @@ -5,12 +6,14 @@ import { Dashboard } from "../pages/Dashboard" import { NotFound } from "../pages/NotFound" export function App() { + const [sidebarOpen, setSidebarOpen] = useState(false) + return ( -
- +
+ setSidebarOpen(false)} />
- + setSidebarOpen((v) => !v)} /> } /> } /> diff --git a/src/components/ResourceCard.tsx b/src/components/ResourceCard.tsx index fa024fe..00f5d0d 100644 --- a/src/components/ResourceCard.tsx +++ b/src/components/ResourceCard.tsx @@ -57,7 +57,7 @@ export function ResourceCard({
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 465a172..6c24c2d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -7,63 +7,99 @@ const NAV_ITEMS = [ { key: "consumed" as const, label: "Consumidos", icon: "✅" }, ] as const -export function Sidebar() { +interface SidebarProps { + isOpen: boolean + onClose: () => void +} + +export function Sidebar({ isOpen, onClose }: SidebarProps) { const activeFilter = useResourceStore((s) => s.activeFilter) const activeCategory = useResourceStore((s) => s.activeCategory) const categories = useResourceStore((s) => s.categories) const setActiveFilter = useResourceStore((s) => s.setActiveFilter) const setActiveCategory = useResourceStore((s) => s.setActiveCategory) + function handleNavClick(action: () => void) { + action() + onClose() + } + return ( - + +
+

+ Categorías +

+
    + {categories.map((category) => ( +
  • + +
  • + ))} +
+
+ + + ) } diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index 605b631..3dc3d4d 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -16,7 +16,11 @@ function applyDarkMode(enabled: boolean): void { localStorage.setItem("bookmark_dark_mode", String(enabled)) } -export function Topbar() { +interface TopbarProps { + onMenuClick: () => void +} + +export function Topbar({ onMenuClick }: TopbarProps) { const [darkMode, setDarkMode] = useState(getInitialDarkMode) function toggleDarkMode() { @@ -26,11 +30,25 @@ export function Topbar() { } return ( -
- +
+ {/* Hamburger — only on mobile */} + + +
+ +
+