# HuLibraries — Backoffice Reference Este módulo es la implementación de referencia de HuLibraries en el **admin/backoffice**. Sirve como fuente de verdad para entender la arquitectura, patrones y convenciones a seguir en la implementación web. --- ## Estructura de directorios ``` HuLibraries/ ├── Layout/ │ ├── index.tsx # Layout principal con Helmet + HuGoThemeProvider + Sidebar │ ├── RootArticleLayout.tsx # Guard de :rootId, monta debajo de /:rootId │ ├── ChildArticleLayout.tsx # Guard de :childId, monta debajo de /:rootId/child/:childId │ └── articleActionRoutes.tsx # Subárbol de acciones (segmentation, report) reutilizado en root y child ├── Home/ │ └── index.tsx # Página de inicio (placeholder actual) ├── Article/ │ ├── index.tsx # Página de artículo │ └── components/ │ ├── ArticleBreadcrumbs.tsx │ ├── ArticleDetail/ │ │ ├── index.tsx │ │ └── components/ │ │ ├── ArticleTitle.tsx │ │ ├── ArticleBody.tsx │ │ ├── ArticleAttachments.tsx │ │ ├── ArticleChildren.tsx │ │ ├── ArticleNewAlert.tsx │ │ ├── ArticleNotSegmentedAlert.tsx │ │ └── ArticleDetailSkeleton.tsx │ └── ArticleHeader/ │ ├── index.tsx │ ├── DeleteArticleModal.tsx │ ├── DisabledArticleModal.tsx │ ├── ActivateArticleModal.tsx │ ├── NotificationsSettingsDrawer.tsx │ └── DuplicateArticleDrawer/ │ ├── index.tsx │ ├── DuplicateArticleForm.tsx │ └── CancelDuplicateArticleModal.tsx ├── Segmentation/ │ ├── index.tsx │ ├── types.ts │ ├── constants.ts │ ├── services.ts │ ├── components/ │ │ ├── ArticleAudience.tsx │ │ ├── ArticleAudienceForm.tsx │ │ ├── ArticleAudienceFormSkeleton.tsx │ │ └── ArticleAudienceFooter.tsx │ └── hooks/ │ ├── useSaveArticleAudience.ts # artículo-específico │ └── useGetAudienceItems.ts # artículo-específico ├── hooks/ │ ├── useArticleId.ts │ ├── useArticleNavigation.ts │ ├── useNavigateToCurrentArticle.ts │ ├── useArticleBreadcrumb.ts │ ├── useLibraryAccessGuard.ts │ ├── useGetArticle.ts │ ├── useGetArticleTree.ts │ ├── useArticleInformation.ts │ ├── useLibrariesTitle.ts │ ├── useLibraryNotFoundHandler.ts │ ├── useChangeArticleName.ts │ ├── useUpdateArticleStatus.ts │ ├── useUpdateArticleBody.ts │ ├── useDeleteArticle.ts │ ├── useDuplicateArticle.ts │ ├── useCreateArticleAttachment.ts │ ├── useCreateArticleAttachmentBulk.ts │ ├── useDeleteArticleAttachment.ts │ ├── useUpdateArticleAttachmentName.ts │ ├── useUpdateArticleAttachmentsPositions.ts │ ├── useHeaderArticleForm.ts │ ├── useUpdateCoverPicture.ts │ ├── useRemoveCoverPicture.ts │ ├── useGetCollaborators.ts │ ├── useGetEditors.ts │ ├── useUpdateEditors.ts │ ├── useSetArticleQueryData.ts │ ├── useCoverPictureActions/ │ │ ├── index.tsx │ │ └── components/ │ │ └── RemoveCoverPictureModal.tsx │ ├── useEditPermissionsDrawer/ │ │ ├── index.tsx │ │ └── components/ │ │ ├── EditPermissionsFormContent.tsx │ │ └── CancelEditPermissionsModal.tsx │ └── useSharePermissionsActions/ │ └── index.tsx ├── components/ │ └── ArticleInternalHeader.tsx ├── utils/ │ └── libraryErrorUtils.ts ├── types.ts ├── queries.ts ├── constants.ts ├── services.ts ├── utils.ts └── routes.ts ``` --- ## Reglas de estructura 1. **Organización por feature, no por tipo**: cada sub-feature (Article, Segmentation) tiene su propio directorio con sus propios `types.ts`, `services.ts`, `utils.ts`, `hooks/`, `components/`. 2. **Archivos raíz del módulo**: `types.ts`, `queries.ts`, `constants.ts`, `services.ts`, `utils.ts`, `routes.ts` — comparten funcionalidad entre sub-features. 3. **Hooks que renderizan UI** viven en subdirectorio propio con `index.tsx` + `components/` internos. Ej: `useCoverPictureActions/`, `useEditPermissionsDrawer/`. 4. **Componentes puramente presentacionales** van en `components/` sin subcarpeta, a menos que tengan sub-componentes propios. 5. **No crear archivos barrel** (`index.ts`) salvo en casos donde explícitamente se requiera re-exportar desde fuera del módulo. 6. **Utils: archivo único vs directorio**: - Si hay un solo archivo de utils → `utils.ts` en la raíz del scope. - Si hay más de uno → directorio `utils/` con archivos en `camelCase` sin sufijo `Utils` (ej: `utils/libraryError.ts`, no `utils/libraryErrorUtils.ts`). --- ## Reglas de componentes ### Estructura de un componente ```tsx // 1. Imports React/externos // 2. Imports del design system (@material-hu/...) // 3. Imports internos del módulo (hooks, services, utils, types) // 4. Definición de Props (interface) // 5. Componente como arrow function con export nombrado o default // 6. Export default al final interface ArticleTitleProps { article: Article; } const ArticleTitle = ({ article }: ArticleTitleProps) => { // ... }; export default ArticleTitle; ``` ### Patrones de componentes - **Inline save**: Para campos editables (título, body), usar `onBlur` para disparar la mutación. Envolver con `FormProvider` + `useForm`. - **Lazy load de modales**: Los modales de confirmación se importan con `lazy()` dentro del componente que los usa para no inflar el bundle inicial. - **Skeleton como estado de carga**: Cada sección con carga asíncrona tiene su propio componente `*Skeleton.tsx` (ej: `ArticleDetailSkeleton`, `ArticleAudienceFormSkeleton`). - **Alertas contextuales**: Usar `ArticleNewAlert` / `ArticleNotSegmentedAlert` para guiar al usuario según el estado del artículo. - **Renderizado condicional**: Usar flags booleanas explícitas (`showLoading`, `showDetail`, `showError`) en vez de ternarios anidados. ```tsx const showLoading = isLoading; const showError = !isLoading && isError; const showContent = !isLoading && !isError && !!article; return ( <> {showLoading && } {showError && } {showContent && } ); ``` --- ## Reglas de hooks ### Nomenclatura de hooks Los hooks se clasifican en cuatro categorías con convenciones de nombre distintas: | Categoría | Patrón | Ejemplos | |-----------|--------|---------| | **Query** (fetching de datos) | `useGet[Data]` | `useGetArticle`, `useGetAudienceCount`, `useGetEditors` | | **Mutation** (acciones que modifican datos) | `use[Action][Data]` | `useDeleteArticle`, `useUpdateArticleStatus`, `useCreateArticleAttachment` | | **Drawer** (hooks que encapsulan un drawer) | `use[Data]Drawer` | `useEditPermissionsDrawer`, `useDuplicateArticleDrawer` | | **Otros** (utilidades, estado, lógica de UI) | `use[Something]` | `useArticleId`, `useArticleInformation`, `useLibrariesTitle` | ### Hooks de queries (fetching) Si el hook depende de un parámetro opcional (ej: un ID de ruta), se debe validar dentro del `queryFn` y lanzar un error descriptivo. No usar el operador `!` para forzar el tipo. ```ts // hooks/useGetArticleTree.ts import { useQuery } from 'react-query'; import { librariesKeys } from '../queries'; import { getArticleTree } from '../services'; export const useGetArticleTree = (articleId: number | undefined) => { return useQuery({ queryKey: librariesKeys.articleTree(articleId), queryFn: () => { if (!articleId) { throw new Error('[useGetArticleTree] Article ID is required'); } return getArticleTree(articleId); }, select: response => response.data, enabled: !!articleId, }); }; ``` - El `enabled: !!articleId` evita que se ejecute sin el ID. - El guard dentro de `queryFn` protege el tipo en tiempo de ejecución y hace el error trazable. - Usar `select: response => response.data` para exponer directamente el tipo de dominio. - Pasar `options` como último parámetro para permitir overrides desde el consumidor. ### Hooks de mutations ```ts // hooks/useDeleteArticle.ts export const useDeleteArticle = () => { const { enqueueSnackbar } = useHuSnackbar(); const queryClient = useQueryClient(); const navigate = useNavigate(); return useMutation({ mutationFn: (articleId: number) => deleteArticle(articleId), onSuccess: () => { enqueueSnackbar({ title: t('delete.success'), variant: 'success' }); queryClient.invalidateQueries(librariesKeys.all()); navigate(librariesRoutes.base()); }, onError: (err) => { // Manejar errores específicos antes del fallback genérico if (isArticleSpecificError(err)) { enqueueSnackbar({ title: t('specific.error'), variant: 'error' }); return; } enqueueSnackbar({ title: t('delete.error'), variant: 'error' }); }, }); }; ``` - Invalidar queries relacionadas en `onSuccess`. - Manejar errores específicos (por código) antes del fallback genérico. - Mostrar snackbar de éxito/error en el hook, no en el componente. - Navegar en el hook cuando aplica (ej: después de delete). ### Hooks de drawers (con UI) Para hooks que encapsulan un drawer, usar la convención de subdirectorio: ``` hooks/useEditPermissionsDrawer/ ├── index.tsx ← exporta el hook (nombrado, no default) └── components/ ├── EditPermissionsFormContent.tsx └── CancelEditPermissionsModal.tsx ``` El hook retorna un objeto con la UI rendereable y las funciones para mostrarla: ```tsx export const useEditPermissionsDrawer = () => { const { drawer, showDrawer } = useDrawer(EditPermissionsFormContent); const { modal, showModal } = useLazyModal(CancelEditPermissionsModal); return { editPermissionsDrawer: drawer, cancelPermissionsModal: modal, showEditPermissionsDrawer: showDrawer, }; }; ``` ### Hooks de utilidad del módulo - `useArticleId()`: Extrae y parsea los route params. Devuelve `{ rootId, childId, articleId }` donde `articleId = childId ?? rootId` (id del artículo activo). - `useArticleInformation(article)`: Calcula flags de permisos y estado (`canEdit`, `canDelete`, `isActive`, etc.). - `useLibrariesTitle()`: Obtiene el título localizado del módulo con prefijo "HU". - `useLibraryNotFoundHandler()`: Registra handler global de 404 (errores de la API) y navega al base route. Deduplica errores con ventana de 500ms. - `useLibraryAccessGuard({ resetKey, isInvalid })`: Hook genérico usado por los layouts de ruta para validar URL params y redirigir al home si son inválidos. Ver sección **Routes & Navigation**. - `useNavigateToCurrentArticle()`: Devuelve un callback que navega al artículo del contexto actual (útil para botones de "back" desde sub-pantallas como segmentation/report). - `useArticleNavigation()`: Devuelve `navigateBack` con lógica de prioridad search → parent → home. - `useSetArticleQueryData()`: Actualiza el cache de React Query de forma type-safe sin invalidar. --- ## Reglas de queries/mutations/services ### services.ts — Capa de API ```ts // services.ts export const getArticle = (articleId: number) => api.get(`/articles/${articleId}`); export const updateArticleStatus = (payload: UpdateArticleStatusPayload) => api.patch('/articles/status', payload); ``` - Funciones puras que retornan la promesa del HTTP client. - Sin lógica de negocio ni manejo de errores: eso va en los hooks. - Tipar la respuesta con el genérico del cliente HTTP. - Agrupar por entidad en el mismo archivo (artículo, adjuntos, etc.). ### queries.ts — React Query Key Factory ```ts // queries.ts export const librariesKeys = { all: () => ['libraries'] as const, article: (id?: number) => [...librariesKeys.all(), 'article', id] as const, articleTree: (id?: number) => [...librariesKeys.all(), 'tree', id] as const, collaborators: (id?: number, search?: string) => [...librariesKeys.all(), 'collaborators', id, search] as const, editors: (id?: number) => [...librariesKeys.all(), 'editors', id] as const, audience: { all: (id?: number) => [...librariesKeys.all(), 'audience', id] as const, count: (id?: number) => [...librariesKeys.audience.all(id), 'count'] as const, }, }; ``` - Usar `as const` para type safety. - Estructura jerárquica: `all > entidad > id > sub-recurso`. - La jerarquía permite invalidar en cascada: `librariesKeys.all()` invalida todo. - Nunca hardcodear strings de query keys fuera de este archivo. --- ## Reglas de utils/constants ### utils — archivo único vs directorio - **Un solo dominio de utils** → usar `utils.ts` directamente. - **Múltiples dominios** → crear directorio `utils/` con archivos en `camelCase` sin sufijo `Utils`: ``` utils/ ├── breadcrumb.ts # getBreadcrumbItems, etc. ├── editors.ts # splitEditors, formatEditorsPayload, etc. └── libraryError.ts # isLibraryQuery, isNotFoundError, etc. ``` ```ts // utils.ts (caso de un solo archivo) export const getBreadcrumbItems = (tree: ArticleTree[]): BreadcrumbLink[] => { ... }; export const splitEditors = (editors: ArticleCollaborator[]) => { ... }; export const formatEditorsPayload = (editors: ArticleCollaborator[]) => { ... }; export const getPillConfig = (status: ArticleStatus) => { ... }; ``` ```ts // utils/libraryError.ts (caso de directorio — nombre sin sufijo "Utils") export const isLibraryQuery = (queryKey: unknown[]) => { ... }; export const isLibraryMutation = (mutationKey: unknown[]) => { ... }; export const isNotFoundError = (error: unknown) => { ... }; export const isLibraryNotFoundError = (queryKey: unknown[], error: unknown) => { ... }; ``` - Solo transformaciones puras de datos. - Sin side effects, sin llamadas a hooks, sin imports de react. - Testeable unitariamente. ### constants.ts ```ts export const ARTICLE_TITLE_MAX_LENGTH = 240; export const MAX_EDITORS_LENGTH = 150; ``` - Solo valores literales que se usan en múltiples lugares del módulo. - No mezclar con tipos ni funciones. --- ## Reglas de types ### types.ts — Tipos del dominio ```ts // Enums de estado export enum ArticleStatus { ENABLED = 'ENABLED', DISABLED = 'DISABLED', } // Tipos de dominio (respuesta de API) export type Article = { id: number; title: string; body: string; status: ArticleStatus; attachments: ArticleAttachment[]; segmentation: ArticleSegmentation; children: ArticleChild[]; // ... }; // Tipos de payload para mutaciones export type UpdateArticleStatusPayload = { articleId: number; status: ArticleStatus; }; ``` - Separar enums, tipos de dominio y tipos de payload. - Los tipos de payload llevan sufijo `Payload` o `Request`. - Los tipos de respuesta de API son los mismos que los de dominio (se mapean en `select`). - Tipos de sub-feature van en `Segmentation/types.ts`, no en el raíz. --- ## Segmentation — Arquitectura La segmentación es el sub-sistema más complejo. Documenta su propia arquitectura. ### Tipos de condición (`SegmentationConditionType`) | Tipo | Descripción | |------|-------------| | `ALL` | Todos los usuarios | | `USERS` | IDs específicos de usuario | | `ITEMS` | IDs de items de segmentación | | `EXPRESSION` | Condiciones anidadas con operadores AND/OR | | `USERS_OR_ITEMS` | Mezcla de usuarios e items | ### Flujo de datos ``` API (AudienceQuery) → audienceQueryToCriterias() # API → form criteria [src/components/Audience/utils] → [FormState] → criteriasToSegmentationExpression() # form criteria → API payload [src/components/Audience/utils] → API ``` ### Hooks de segmentación Los hooks genéricos de audiencia viven en `src/components/Audience/hooks/` y son compartidos por otros módulos (ServiceManagement, RolesAndPermissions, Competencies). Solo permanecen aquí los hooks específicos del dominio de artículo. **Hooks artículo-específicos** (`Segmentation/hooks/`): | Hook | Propósito | |------|-----------| | `useGetAudienceItems` | Carga grupos e items de segmentación con mapas indexados por ID | | `useSaveArticleAudience` | Mutación para persistir la expresión en el artículo | **Hooks compartidos** (`src/components/Audience/hooks/`): | Hook | Propósito | |------|-----------| | `useGetAudienceCount` | Cuenta usuarios que cumplen una expresión de segmentación | | `useGetAudienceUsers` | Query infinita para búsqueda de usuarios paginada | | `useSelectedUsers` | Fetch datos completos de usuarios seleccionados por ID | | `useAudienceFormCount` | Count reactivo desde criterias del formulario de Audience | | `useSegmentationDrawerCount` | Count para el drawer de segmentación | | `useIndividualDrawerCount` | Count para el drawer de selección individual | | `useSegmentationFieldItems` | Carga dinámica de campos (grupos) con filtro de exclusión | | `useSegmentationValueItems` | Carga dinámica de valores con paginación infinita | | `useSegmentationDrawerService` | Crea search service desde condiciones de segmentación | | `useIndividualDrawerService` | Crea search service desde IDs de usuarios | **Utils compartidas** (`src/components/Audience/utils/`): | Función | Propósito | |---------|-----------| | `criteriasToSegmentationExpression` | Form criteria → `SegmentationExpressionPayload` | | `criteriaToSegmentationExpression` | Un `CriteriaEntry` → `SegmentationExpressionCondition` | | `audienceQueryToCriterias` | `AudienceQuery` (API) → form criteria | | `audienceQueryToSegmentationExpression` | `AudienceQuery` (API) → `SegmentationExpressionPayload` | | `getSegmentationDescription` | Texto legible para una entrada de segmentación | | `getIndividualDescription` | Texto legible para una entrada individual (resuelve nombres) | | `getCollaboratorsService` | Factory de search service vinculado a una expresión | **Archivos compartidos en raíz** (`src/components/Audience/`): | Archivo | Propósito | |---------|-----------| | `services.ts` | `countAudienceByExpression`, `searchAudienceByExpression` | | `queryKeys.ts` | `audienceExpressionKeys` — query key factory para React Query | | `types.ts` | Tipos del dominio: `AudienceQuery`, `SegmentationExpressionPayload`, etc. | --- ## Patrones de modales y drawers ### Modal de confirmación ```tsx // En el componente que dispara el modal const DeleteArticleModal = lazy(() => import('./DeleteArticleModal')); const { isOpen, openModal, closeModal } = useModalState(); return ( <> {isOpen && ( )} ); ``` ### Drawer con formulario ```tsx // Drawer con FormProvider interno const DuplicateArticleDrawer = () => { const form = useForm(); return ( {/* modal anidado para confirmar cierre */} ); }; ``` --- ## Routes & Navigation ### URL shape El módulo soporta dos niveles de artículo en la URL: **root** y **child** (un descendiente cualquiera del root). El `rootId` es siempre el primer segmento porque el sidebar fetchea por root. ``` /library → Home /library/:rootId → Article (root) /library/:rootId/segmentation → Segmentation (root) /library/:rootId/report → Report (root) /library/:rootId/child/:childId → Article (child) /library/:rootId/child/:childId/segmentation → Segmentation (child) /library/:rootId/child/:childId/report → Report (child) ``` ### Estructura de rutas (anidada) Las rutas se declaran en `src/routes.tsx`. La key del diseño: las acciones (`segmentation`, `report`) se declaran **una sola vez** como un array reutilizable (`articleActionRoutes`) y se montan tanto bajo el layout root como bajo el layout child. ```tsx { path: 'library', element: , // sidebar + theme + search children: [ { index: true, element: }, { path: ':rootId', element: , // valida rootId, redirige si inválido children: [ { index: true, element:
}, ...articleActionRoutes, // segmentation, report { path: 'child/:childId', element: , // valida que childId sea descendiente children: [ { index: true, element:
}, ...articleActionRoutes, // mismas acciones, contexto child ], }, ], }, ], } ``` `articleActionRoutes` vive en [`Layout/articleActionRoutes.tsx`](Layout/articleActionRoutes.tsx) dentro del módulo (no en `src/routes.tsx`) para mantener ownership. **Por qué anidado**: en una estructura plana habría que declarar cada acción dos veces (una bajo `:rootId/...` y otra bajo `:rootId/child/:childId/...`). La estructura anidada permite reutilizar el subárbol y agregar acciones nuevas en una sola línea. ### URL builders (`routes.ts`) ```ts // routes.ts export const librariesRoutes = { base, // /library article, // /library/:rootId | .../child/:childId report: (rootId, childId?) => `${article(rootId, childId)}/report`, segmentation: (rootId, childId?) => `${article(rootId, childId)}/segmentation`, }; ``` - Siempre usar estos helpers, nunca strings hardcodeados. - `article(rootId, childId?)` agrega el segmento `/child/:childId` solo cuando `childId` está presente y difiere de `rootId`. Esto significa que el caller no tiene que normalizar `childId === rootId ? undefined : childId` en cada call site — el helper lo hace. ### Layouts como guards Los dos layouts intermedios (`RootArticleLayout` y `ChildArticleLayout`) son guards que validan los params contra los datos cargados, y redirigen al home con un snackbar si la URL es inválida. Renderizan `` siempre, así los hijos manejan su propio loading sin bloquearse. **[`RootArticleLayout`](Layout/RootArticleLayout.tsx)** — monta debajo de `:rootId`: - Valida que `rootId` parsee a número. - Valida que exista en `useGetRootArticles()`. - Espera a que la query no esté `isLoading` ni `isFetching` (importante: tras una mutación que invalida `rootList`, no debe redirigir prematuramente). **[`ChildArticleLayout`](Layout/ChildArticleLayout.tsx)** — monta debajo de `:rootId/child/:childId`: - Valida que `childId` parsee a número. - Valida que el árbol del childId tenga como root el `rootId` (vía `useGetArticleTree`). Ambos delegan la mecánica del redirect (snackbar + dedup + `replace: true`) al hook compartido [`useLibraryAccessGuard`](hooks/useLibraryAccessGuard.ts). ### Hooks de navegación | Hook | Qué hace | |---|---| | [`useArticleId`](hooks/useArticleId.ts) | Parsea `params.rootId` y `params.childId`. Devuelve `{ rootId, childId, articleId }`. `articleId = childId ?? rootId` es el id del artículo activo. | | [`useNavigateToCurrentArticle`](hooks/useNavigateToCurrentArticle.ts) | Devuelve un callback que navega al artículo del contexto actual (`article(rootId, childId)`). Usado en `ArticleInternalHeader.handleBack` y `ArticleAudienceForm` (volver tras guardar/cancelar segmentation). | | [`useArticleNavigation`](hooks/useArticleNavigation.ts) | Devuelve `navigateBack` con prioridad: (1) si `state.search` está seteado → `navigate(-1)` para restaurar resultados de búsqueda; (2) si hay `parentId` → ir al padre en el árbol; (3) fallback al home. | | [`useArticleBreadcrumb`](hooks/useArticleBreadcrumb.ts) | Construye los items del breadcrumb a partir del árbol. Cada click navega vía `librariesRoutes.article(rootId, ancestorId)`. | | [`useLibraryAccessGuard`](hooks/useLibraryAccessGuard.ts) | Hook genérico usado por los dos layouts. Recibe `{ resetKey, isInvalid }`. Cuando `isInvalid` es true, muestra snackbar y redirige al home. Dedupe interno por `resetKey`. | ### Estado de navegación tipado Cuando se navega desde un resultado de búsqueda hacia un artículo, [`SearchItem`](Article/components/ArticleSearch/SearchItem.tsx) pasa el query actual como `state` para que después el back lo restaure. El contrato está tipado en `types.ts`: ```ts export type LibraryNavigationState = { search?: string; }; ``` - **Productor**: `SearchItem.handleSelect` hace `navigate(url, { state: { search } satisfies LibraryNavigationState })`. - **Consumidor**: `useArticleNavigation.navigateBack` lee `location.state as LibraryNavigationState | null` y, si encuentra `search`, hace `navigate(-1)` para que el usuario regrese a los resultados de búsqueda con el query intacto. ### Reglas para agregar features - **Nueva acción de artículo (e.g. `analytics`)**: agregar una entrada en `articleActionRoutes` y un builder en `librariesRoutes`. Funciona automáticamente para root y child. - **Nueva URL del módulo que NO es acción de artículo**: declararla bajo el layout correspondiente en `src/routes.tsx`. - **Cambiar la shape de una URL**: tocar solo `routes.ts` (los builders) y `src/routes.tsx` (los `path` declarados). Mantener ambos sincronizados — son las dos fuentes que React Router consume. - **Nueva navegación desde un componente**: usar `librariesRoutes.*` para construir la URL; nunca strings hardcodeados. --- ## Convenciones de naming | Concepto | Convención | Ejemplo | |----------|-----------|---------| | Componente | PascalCase | `ArticleTitle` | | Hook query | `useGet[Data]` | `useGetArticle`, `useGetAudienceCount` | | Hook mutation | `use[Action][Data]` | `useDeleteArticle`, `useUpdateArticleStatus` | | Hook drawer | `use[Data]Drawer` | `useEditPermissionsDrawer` | | Hook otros | `use[Something]` | `useArticleId`, `useArticleInformation` | | Service function | camelCase | `getArticle`, `updateArticleStatus` | | Query key factory | camelCase + `Keys` | `librariesKeys` | | Enum | PascalCase | `ArticleStatus` | | Enum value | UPPER_SNAKE_CASE | `ArticleStatus.ENABLED` | | Tipo de payload | PascalCase + `Payload` | `UpdateArticleStatusPayload` | | Constante | UPPER_SNAKE_CASE | `ARTICLE_TITLE_MAX_LENGTH` | | Ruta helper | camelCase + `Routes` | `librariesRoutes` | --- ## Stack tecnológico del módulo - **React** — componentes funcionales + hooks - **react-router-dom** — navegación, `useParams`, `useNavigate`, `useMatch` - **react-hook-form** — gestión de formularios con `FormProvider` + `useWatch` - **@tanstack/react-query** — fetching, caching, invalidación - **@material-hu/mui** — componentes UI (Stack, Typography, Box, etc.) - **@material-hu/components** — design system propio (HuSnackbar, HuPills, etc.) - **react-helmet-async** — gestión del `` del documento