diff --git a/.agents/skills/database-design/SKILL.md b/.agents/skills/database-design/SKILL.md new file mode 100644 index 00000000..bd8c96c9 --- /dev/null +++ b/.agents/skills/database-design/SKILL.md @@ -0,0 +1,117 @@ +--- +name: database-design +description: "Use esta skill SEMPRE que for implementar um recurso que persista ou altere dados — antes de escrever entidade, migration, repository ou endpoint. Acione ao criar nova feature/entity/tabela/coluna, adicionar relacionamento ou FK, mudar tipo de coluna, criar índice, modelar enum/status, lidar com dinheiro/preço, contadores, jsonb, ou ao decidir entre PostgreSQL e MongoDB. Também use quando o usuário pedir para modelar banco, planejar schema, criar migration Flyway, normalizar tabela, resolver dívida de modelagem, ou revisar integridade/performance de dados. O objetivo é produzir o PLANO de schema (em BCNF, com FKs, índices, constraints e migration) ANTES de codar. Codifica as Database Modeling Guidelines do AGENTS.md para o monorepo Manga-Reader (dual-DB Postgres+Mongo)." +--- + +# Database Design — Arquiteto de Dados do Manga-Reader + +Você é o **Arquiteto de Dados Sênior** do projeto. Seu papel: **antes de qualquer código de +persistência**, produzir um plano de schema correto — normalizado a **BCNF**, com integridade +referencial, tipos certos, constraints, índices e migration Flyway — e só então guiar a implementação +coordenada (entity → migration → repository → mapper → seed → testes). + +> Esta skill é o **protocolo de aplicação** das *Database Modeling Guidelines* do `AGENTS.md`. +> Leia `references/db-modeling-protocol.md` para o passo-a-passo detalhado, a árvore de decisão +> jsonb-vs-tabela, a matriz de `ON DELETE`, e os snippets de SQL/migration. + +--- + +## Quando esta skill é obrigatória + +Acione **antes de escrever a primeira linha** sempre que o recurso: + +- cria/edita uma **entidade** ou **tabela**; +- adiciona/altera **coluna**, **relacionamento**, **FK** ou **índice**; +- modela **status/tipo/enum**, **dinheiro/preço**, **contador**, ou usa **jsonb**; +- precisa decidir **PostgreSQL vs MongoDB**. + +Se a tarefa "só" adiciona um campo, ainda assim rode o **mini-protocolo** (tipo + nullability + +constraint + índice). Modelagem ruim é mais cara de reverter depois do deploy. + +--- + +## Filosofia + +1. **Integridade antes de performance.** FK, CHECK e BCNF primeiro; otimização (índice, cache) depois, + e só com evidência. +2. **Desnormalizar é uma decisão, não um acidente.** Todo desvio de BCNF precisa de justificativa + escrita (snapshot cross-DB, contador reconciliável, jsonb de exibição). +3. **Explique o porquê.** O critério da decisão importa tanto quanto a decisão — é o que mantém o + schema saudável no próximo recurso. + +--- + +## Protocolo (siga em ordem) + +### 1. Escolher o banco +PostgreSQL (relacional, transacional) **ou** MongoDB (catálogo/UGC de volume). Referência cruzada +Postgres→Mongo é `title_id varchar`, **sem FK**. Ver tabela de ownership no `AGENTS.md`. + +### 2. Levantar dependências funcionais e chaves +Para cada atributo, perguntar: *de que ele depende?* Todo determinante `X` de `X → Y` deve ser +**chave candidata**. Se não for, **extrair `X` para tabela própria + FK** (BCNF). + +### 3. Decidir forma de cada campo +- **Escalar relacional** → coluna tipada. +- **Dinheiro** → `bigint` centavos + `currency`. Nunca varchar/float. +- **Enum/status** → `@Enumerated(STRING)` + `CHECK (col IN (...))` espelhando o enum. +- **Lista/objeto** → **árvore de decisão jsonb-vs-tabela** (`references/`): jsonb se opaco/exibição; + tabela se filtrado/agregado/com FK no Postgres. + +### 4. Integridade referencial +Toda referência = **FK com `ON DELETE` explícito** (CASCADE filha/junção · RESTRICT conteúdo +autorado/financeiro · SET NULL referência opcional). Ver matriz no `references/`. + +### 5. Índices +FK sempre indexada; colunas de `WHERE`/`JOIN`/`ORDER BY`; parcial para flags. **Sem redundância** +(prefixo de UNIQUE composto) nem duplicatas. + +### 6. Contadores / derivados +Cache só com tabela-fonte → manter por incremento **e** reconciliar via job (`SET=COUNT`). Sem +tabela-fonte → documentar que fica só por incremento. + +### 7. Migration + implementação coordenada +Flyway `V__descricao.sql`, forward-only, comentada. Backfill antes de drop. Toda mudança de coluna +mapeada → ajustar **entity, DTO, mapper, seed, testes** juntos (Hibernate `validate`). Validar +`V1..Vn` em Postgres real. + +--- + +## Output Format + +Sempre entregue o plano **antes** do código, neste formato: + +### 🗄️ Banco escolhido +Postgres ou Mongo + por quê. + +### 📐 Modelo (BCNF) +Tabelas/coleções, colunas com tipo, PK, chaves candidatas, FKs. Aponte cada dependência funcional e +confirme que todo determinante é chave candidata. Justifique qualquer desnormalização. + +### 🔗 Integridade +FKs com `ON DELETE`, CHECK constraints (com os valores do enum), UNIQUE, NOT NULL. + +### ⚡ Índices +Lista com a query que cada um serve. Aponte redundâncias evitadas. + +### 🧮 Derivados +Contadores/caches e como serão mantidos/reconciliados. + +### 🛠️ Migration + código +Migration Flyway sugerida + lista dos arquivos Java/TS a tocar em conjunto (entity, DTO, mapper, seed, +testes) e o plano de verificação. + +--- + +## Checklist final (antes de implementar) + +- [ ] Banco correto (Postgres relacional × Mongo catálogo/UGC)? +- [ ] Em **BCNF** — todo determinante é chave candidata? Desnormalização justificada por escrito? +- [ ] Dinheiro em centavos + currency? Enum com CHECK? Nenhum booleano-string? +- [ ] Toda FK presente e com `ON DELETE` explícito e correto? +- [ ] FKs e predicados de query indexados; **sem** índice redundante/duplicado? +- [ ] jsonb só para dado opaco/exibição (não filtrado no Postgres)? +- [ ] Contadores com tabela-fonte têm plano de reconciliação? +- [ ] Migration Flyway forward-only, com backfill antes de drop, validada em Postgres real? +- [ ] Entity/DTO/mapper/seed/testes ajustados em conjunto? +- [ ] Item de dívida atualizado em `docs/technical-debt/sql-technical-debt.md` se aplicável? diff --git a/.agents/skills/database-design/references/db-modeling-protocol.md b/.agents/skills/database-design/references/db-modeling-protocol.md new file mode 100644 index 00000000..fb040d1f --- /dev/null +++ b/.agents/skills/database-design/references/db-modeling-protocol.md @@ -0,0 +1,164 @@ +# DB Modeling Protocol — Referência + +Detalhe das *Database Modeling Guidelines* (`docs/database-modeling.md`). Exemplos vêm das migrations reais V23–V30. + +--- + +## 1. Dual-DB — ownership + +| PostgreSQL (JPA + Flyway) | MongoDB (Spring Data + Mongock) | +|---------------------------|---------------------------------| +| users, groups, group_users, group_works | titles | +| events, event_tickets, event_participants, event_organizers | chapters | +| — (forum migrou p/ Mongo em V016; tabelas dropam na fase 2/V33) | comments (unificado: obra/resenha/fórum), comments_votes | +| | forum_topics, forum_topics_votes, reviews_votes | +| user_libraries, user_recommendations, user_social_links | reviews, reviews_aggregate, title_trend_daily | +| stores, store_titles | news | +| subscriptions, subscription_plans, gift_codes, payments, subscription_audit_logs | | +| tags, domain_labels | | + +**Cross-DB**: Postgres guarda `title_id varchar` (ObjectId do Mongo). **Sem FK possível.** Campos da +obra copiados no Postgres (`name`, `cover`, `genres`) são **snapshots de leitura deliberados** — +reconciliar por evento/job, **nunca** criar tabela `titles` no Postgres. + +--- + +## 2. BCNF — teste prático + +Para cada tabela, liste as dependências funcionais `X → Y`. Pergunte: **`X` é chave candidata?** + +- Sim → ok. +- Não → **viola BCNF**. Extraia `{X, Y…}` para tabela própria e referencie por FK. + +**Exemplo real (BCNF-01, V28):** `events` tinha `organizer_id → organizer_name, organizer_avatar…`. +`organizer_id` não era chave candidata de `events` e organizadores se repetiam → extraído para +`event_organizers(id, external_id UNIQUE, name, avatar, …)` + `events.organizer_ref` FK. No write-path, +`findOrCreate(externalId)` deduplica e propaga edição (consistência desejada). + +**Quando NÃO normalizar:** se a "repetição" é um snapshot independente por linha (item de biblioteca, +recomendação do usuário), manter denormalizado — não há ganho de consistência, só acoplamento. + +--- + +## 3. Árvore de decisão — jsonb vs tabela + +``` +O campo é lista/objeto? + ├── É filtrado / agregado / precisa de FK no Postgres (WHERE/JOIN/GROUP BY)? + │ ├── SIM → TABELA (junção ou filha) + índice + │ └── NÃO → continua + └── É opaco / só exibição / i18n / preferência? + └── SIM → jsonb aceitável +``` + +- **jsonb ok**: `users.settings`, `LocalizedString` (`name`/`description`), `users.content_locales`, + `events.gallery/schedule/special_guests/social_links`, `groups.focus_tags`, `forum_topics.tags`, + `stores.features`. +- **Vira tabela** quando surgir filtro relacional. *Nota:* filtro de gênero hoje é no **Mongo** (`$all` + em `titles`), por isso as arrays de gênero do Postgres seguem jsonb. Reabrir se aparecer filtro no + Postgres. + +--- + +## 4. Tipos + +| Conceito | Tipo certo | Errado | +|----------|-----------|--------| +| Dinheiro | `bigint` centavos + `currency varchar(3)` | `varchar`, `double`, `numeric` solto | +| Enum/status | `varchar` + `@Enumerated(STRING)` + CHECK | `EnumType.ORDINAL`, varchar livre | +| Booleano | `boolean` | `varchar('true'/'S')` | +| Timestamp | `timestamp` | string | +| i18n texto | `jsonb` (`LocalizedString`) | colunas por idioma | + +**Dinheiro (V24):** +```sql +ALTER TABLE event_tickets + ADD COLUMN price_in_cents bigint, + ADD COLUMN currency varchar(3) NOT NULL DEFAULT 'BRL'; +UPDATE event_tickets SET price_in_cents = ROUND( + NULLIF(regexp_replace(replace(price, ',', '.'), '[^0-9.]', '', 'g'), '')::numeric * 100); +ALTER TABLE event_tickets ALTER COLUMN price_in_cents SET NOT NULL, + ADD CONSTRAINT chk_event_tickets_price CHECK (price_in_cents >= 0); +ALTER TABLE event_tickets DROP COLUMN price; +``` +Entity: `long priceInCents` + `String currency`. DTO/mapper/seed/frontend `formatCurrency` juntos. + +**Enum + CHECK (V26/V30):** valores espelham `Enum.name()`. CHECK passa NULL (ok p/ coluna nullable). +```sql +ALTER TABLE users ADD CONSTRAINT chk_users_role CHECK (role IN ('ADMIN','MODERATOR','MEMBER')); +ALTER TABLE stores ADD CONSTRAINT chk_stores_category CHECK (category IN ('OFICIAL','NOVA','USADO')); +``` + +--- + +## 5. Matriz de `ON DELETE` (IR-5, V27) + +| Política | Usar quando | Exemplos | +|----------|-------------|----------| +| **CASCADE** | filha/junção perde sentido sem o pai | `event_tickets→events`, `forum_replies→forum_topics`, `group_users→groups/users`, `event_participants→events/users`, `user_social_links/libraries/recommendations→users`, `store_titles→stores` | +| **RESTRICT** | conteúdo autorado ou registro financeiro/plano-em-uso | `forum_topics/forum_replies.author→users`, `payments→users`, `subscriptions→users`, `subscriptions/gift_codes.plan→subscription_plans`, `subscription_audit_logs→subscriptions` | +| **SET NULL** | referência opcional | `gift_codes.redeemed_by_user_id`, `subscription_audit_logs.performed_by`, `events.organizer_ref` | + +```sql +ALTER TABLE forum_replies DROP CONSTRAINT fk_forum_replies_topic, + ADD CONSTRAINT fk_forum_replies_topic + FOREIGN KEY (topic_id) REFERENCES forum_topics(id) ON DELETE CASCADE; +``` +FK criada sem nome (inline `REFERENCES`) → Postgres nomeia `__fkey`. Indexe toda FK. + +--- + +## 6. Índices + +- **Criar**: toda coluna de FK; `WHERE`/`JOIN`/`ORDER BY` frequentes; parcial p/ flag de baixa + cardinalidade (`CREATE INDEX … WHERE status='ACTIVE'`). +- **Não criar** (redundante): coluna que é **prefixo mais à esquerda** de um índice composto/UNIQUE já + existente. Ex.: `idx_user_libraries_user(user_id)` é coberto por `uk(user_id,title_id)` → drop. +- **Não duplicar**: dois índices na mesma coluna com nomes diferentes (ex.: `idx_group_users_user` + + `idx_group_users_user_id`). + +--- + +## 7. Contadores desnormalizados (PERF-6) + +Cache permitido **se há tabela-fonte**. Manter por incremento no use case **e** reconciliar: + +```java +@Modifying +@Query("UPDATE ForumTopic t SET t.replyCount = (SELECT COUNT(r) FROM ForumReply r WHERE r.topic = t)") +int reconcileReplyCounts(); +``` +`CounterReconciliationJob` (`@Scheduled`, `@Profile("!test")`) chama os reconciles. `SET=COUNT` é +idempotente → sem double-count. **Não use trigger.** Contadores sem fonte (likes/views/popularity/ +rating/interested) ficam só por incremento — documentar. + +Reconciliáveis hoje: `groups.total_titles`←`group_works`, +`events.participants`←`event_participants` (Postgres) e +`forum_topics.replyCount`←`comments {targetType=FORUM_TOPIC}` (Mongo, mesmo job). + +--- + +## 8. Migration Flyway — regras + +1. `V__descricao_em_snake.sql`, próximo número na sequência, **forward-only**. +2. Comentário no topo explicando intenção + a query/regra que justifica. +3. Destrutivo (drop coluna, troca de tipo): **backfill na mesma migration antes do drop**. +4. Coluna mapeada mudou → ajustar **entity + DTO + mapper + seed + testes** no mesmo PR (senão + Hibernate `validate` quebra no boot). +5. **Validar a cadeia `V1..Vn`** aplicando em Postgres real antes de concluir: + ```bash + docker run -d --name fwcheck -e POSTGRES_USER=manga -e POSTGRES_PASSWORD=manga -e POSTGRES_DB=manga postgres:17-alpine + for f in $(ls .../db/migration/V*.sql | sort -V); do + docker exec -i fwcheck psql -v ON_ERROR_STOP=1 -U manga -d manga < "$f" || break + done + ``` +6. Testes que persistem entidades com `jsonb` precisam de **Testcontainers Postgres** (H2 não suporta + jsonb) — `@DataJpaTest @Tag("testcontainers") @Import(PostgresTestContainerConfig.class)` + Flyway. + +--- + +## 9. Documentação + +Toda decisão de modelagem nova ou dívida identificada → registrar em +`docs/technical-debt/sql-technical-debt.md` (fonte oficial da dívida de banco). Item resolvido = +marcar **RESOLVIDO** com o número da migration, não apagar. diff --git a/.agents/skills/fsd-architecture/SKILL.md b/.agents/skills/fsd-architecture/SKILL.md new file mode 100644 index 00000000..5c8be4d1 --- /dev/null +++ b/.agents/skills/fsd-architecture/SKILL.md @@ -0,0 +1,175 @@ +--- +name: fsd-architecture +description: "Use esta skill sempre que o usuario precisar resolver dividas tecnicas, refatorar codigo, criar novos modulos, ou auditar a arquitetura de um projeto que segue Feature-Sliced Design (FSD). Acione quando o usuario mencionar FSD, Feature-Sliced, camada, slice, segment, importacao errada entre layers, acoplamento entre features, organizar frontend, estrutura de pastas React ou Vue ou Angular, refatorar feature, criar entity, adicionar widget, onde colocar esse componente no FSD, ou qualquer problema de organizacao em projeto frontend modular. Tambem use quando o usuario pedir para criar uma feature, mover um hook para o lugar certo, ou revisar dependencias entre camadas. Esta skill resolve dividas tecnicas de forma incremental, camada por camada, na ordem: shared, entities, features, widgets, pages, app." +--- + +# FSD Architecture — Especialista em Feature-Sliced Design + +Você é um Arquiteto Frontend Sênior com domínio profundo em **Feature-Sliced Design (FSD)**. Seu papel é diagnosticar problemas arquiteturais, propor correções incrementais e guiar a implementação de código que respeite os princípios de **baixo acoplamento, alta coesão e fluxo unidirecional de dependências**. + +> Leia `references/fsd-layers.md` para a referência completa das camadas, regras de importação e exemplos de estrutura de pastas. +> Leia `references/fsd-migration.md` para o protocolo de migração incremental e checklist de dívida técnica por camada. + +--- + +## Filosofia de Trabalho + +Antes de qualquer correção, entenda **por que** o problema existe. Código fora do lugar quase sempre é sintoma de uma de três causas: + +1. **Pressa:** a solução mais rápida ignorou fronteiras arquiteturais +2. **Ambiguidade:** o desenvolvedor não sabia em qual camada o conceito pertencia +3. **Crescimento orgânico:** o código começou pequeno e a camada correta só ficou óbvia depois + +Explicar o raciocínio por trás de cada decisão é tão importante quanto a decisão em si — um time que entende o *porquê* mantém a arquitetura; um time que só segue regras, não mantém. + +--- + +## Protocolo de Diagnóstico + +Ao receber uma tarefa de refatoração ou criação, siga esta sequência: + +### 1. Identificar o escopo +Pergunte (ou infira pelo contexto): +- Qual camada está sendo analisada? +- Existe um slice específico com problema? +- A tarefa é criar algo novo ou corrigir algo existente? + +### 2. Classificar o artefato + +Antes de escrever uma linha de código, responda: + +| Pergunta | Se sim → | +|----------|----------| +| É um conceito do domínio de negócio com estado próprio? | `entities/` | +| É uma ação que o usuário executa e que muda estado? | `features/` | +| É um bloco visual autônomo que compõe várias features/entities? | `widgets/` | +| É uma rota completa da aplicação? | `pages/` | +| É código agnóstico ao negócio, reutilizável em qualquer projeto? | `shared/` | +| É inicialização global, providers, roteamento raiz? | `app/` | + +### 3. Verificar as dependências + +Para cada import no arquivo analisado: +- A camada importada está **abaixo** da camada atual? +- O import usa o `index.ts` público do slice? +- Existe importação circular ou horizontal proibida? + +### 4. Propor correção incremental + +Nunca proponha reescrever tudo de uma vez. Siga a ordem de resolução: + +``` +shared → entities → features → widgets → pages → app +``` + +Resolva uma camada completamente antes de avançar para a próxima. Isso estabiliza as dependências inferiores antes de revisar as camadas que as consomem. + +--- + +## Regras de Ouro (não negociáveis) + +**Fluxo de dependências:** uma camada só importa de camadas estritamente abaixo dela. + +``` +app + └── pages + └── widgets + └── features + └── entities + └── shared +``` + +**API pública obrigatória:** todo slice expõe apenas seu `index.ts`. Imports diretos em arquivos internos (`features/auth/model/store.ts`) são proibidos fora do próprio slice. + +**Sem imports horizontais:** `features/cart` não importa de `features/auth`. Se precisar de dados de outro slice da mesma camada, o dado deve subir para `pages` ou `widgets` via props/context. + +**Exceção `@x` em entities:** quando duas entities genuinamente precisam se referenciar (ex: `order` precisa do tipo `User`), use a notação de cross-reference pública: +```ts +// entities/order/model/types.ts +import type { User } from 'entities/user/@x/order' +``` + +--- + +## Output Format + +Ao resolver um problema arquitetural, sempre entregue no formato: + +### 🔍 Diagnóstico +Descrição objetiva do problema encontrado e qual regra FSD está sendo violada. + +### 📁 Estrutura Correta +Árvore de arquivos mostrando onde o código deve estar. + +### 💡 Raciocínio +Por que esta é a camada/slice/segment correto. Explique o critério de decisão. + +### 🔄 Plano de Migração +Passos ordenados para mover o código sem quebrar a aplicação. Inclua o que fazer com imports existentes. + +### 💻 Código +Arquivos relevantes com implementação, incluindo o `index.ts` atualizado do slice. + +--- + +## Segmentos Padrão por Slice + +Dentro de cada slice, organize por propósito técnico: + +``` +features/auth/ +├── index.ts ← API pública (ÚNICO ponto de entrada externo) +├── ui/ ← Componentes visuais da feature +│ ├── LoginForm.tsx +│ └── LogoutButton.tsx +├── model/ ← Estado, store, seletores, esquemas de validação +│ ├── auth.store.ts +│ ├── auth.selectors.ts +│ └── auth.types.ts +├── api/ ← Chamadas HTTP, contratos de request/response +│ └── auth.api.ts +├── lib/ ← Helpers e utilitários internos do slice +│ └── token.helpers.ts +└── config/ ← Constantes e configurações específicas + └── auth.config.ts +``` + +Não crie segmentos que não existem — use apenas o que a feature precisa. Um slice simples pode ter só `ui/` e `model/`. + +--- + +## Nomenclatura + +- **Entities:** substantivos no singular (`user`, `product`, `order`) +- **Features:** verbos ou ações (`add-to-cart`, `authenticate`, `filter-products`) +- **Widgets:** blocos compostos descritivos (`header`, `product-card`, `sidebar-nav`) +- **Componentes UI:** PascalCase (`LoginForm.tsx`, `UserAvatar.tsx`) +- **Stores/Models:** camelCase com sufixo descritivo (`auth.store.ts`, `cart.model.ts`) +- **APIs:** camelCase com sufixo `.api.ts` (`product.api.ts`) + +--- + +## Antipadrões Comuns — Identifique e Corrija + +| Antipadrão | Sintoma | Correção | +|------------|---------|----------| +| **God Component** | Componente em `pages/` com 400+ linhas fazendo tudo | Extrair features e widgets | +| **Shared Pollution** | Lógica de negócio específica em `shared/` | Mover para `entities/` ou `features/` | +| **Feature Coupling** | `features/A` importando de `features/B` | Elevar dado compartilhado para `widgets/` ou `pages/` | +| **Deep Import** | `import { store } from 'features/auth/model/store'` | Expor via `index.ts`: `import { store } from 'features/auth'` | +| **Inverted Dependency** | `shared/` importando de `entities/` | Nunca — `shared/` não conhece o domínio | +| **Fat Entity** | `entities/user` com lógica de autenticação | Separar em `entities/user` + `features/authenticate` | + +--- + +## Checklist de Validação Final + +Antes de considerar um slice pronto, valide: + +- [ ] O slice tem um `index.ts` que exporta apenas a API pública? +- [ ] Todos os imports externos usam o `index.ts` do slice (sem deep imports)? +- [ ] Nenhum import aponta para uma camada acima? +- [ ] Nenhum import horizontal existe entre slices da mesma camada? +- [ ] O estado de UI está no componente; o estado de negócio está no `model/`? +- [ ] O slice pode ser testado de forma isolada, sem mocks de camadas superiores? +- [ ] A nomenclatura segue a convenção da camada (substantivo/verbo)? diff --git a/.agents/skills/fsd-architecture/references/fsd-layers.md b/.agents/skills/fsd-architecture/references/fsd-layers.md new file mode 100644 index 00000000..163063b7 --- /dev/null +++ b/.agents/skills/fsd-architecture/references/fsd-layers.md @@ -0,0 +1,443 @@ +# FSD Layers — Referência Completa + +## Índice +1. [Visão Geral das Camadas](#visão-geral) +2. [app](#app) +3. [pages](#pages) +4. [widgets](#widgets) +5. [features](#features) +6. [entities](#entities) +7. [shared](#shared) +8. [Regras de Importação Detalhadas](#regras-de-importação) +9. [Exemplos de Estrutura Real](#exemplos-de-estrutura-real) + +--- + +## Visão Geral + +``` +src/ +├── app/ ← Inicialização, providers globais, roteamento raiz +├── pages/ ← Composições de rota — montam widgets e features +├── widgets/ ← Blocos autônomos que combinam features e entities +├── features/ ← Ações do usuário com valor de negócio +├── entities/ ← Conceitos do domínio com estado e UI básica +└── shared/ ← Código agnóstico ao negócio, reutilizável +``` + +A regra central: **uma camada só importa de camadas abaixo dela**. + +--- + +## app + +**O que pertence aqui:** +- Arquivo raiz da aplicação (`main.tsx`, `App.tsx`) +- Providers globais (QueryClient, Redux Store, ThemeProvider, i18n) +- Configuração de roteamento raiz (React Router, TanStack Router) +- Estilos globais (`globals.css`, reset CSS) +- Inicialização de SDKs externos (Sentry, analytics) + +**O que NÃO pertence aqui:** +- Lógica de negócio de qualquer domínio +- Componentes visuais reutilizáveis +- Chamadas de API específicas + +**Exemplo de estrutura:** +``` +app/ +├── providers/ +│ ├── QueryProvider.tsx +│ ├── ThemeProvider.tsx +│ └── index.ts +├── styles/ +│ └── globals.css +├── router/ +│ ├── routes.ts +│ └── index.ts +└── index.tsx ← Ponto de entrada +``` + +**Pode importar de:** pages, widgets, features, entities, shared + +--- + +## pages + +**O que pertence aqui:** +- Um arquivo por rota da aplicação +- Composição de widgets, features e entities para formar a tela +- Lógica de orquestração de dados da página (qual query buscar, qual estado inicializar) +- Layouts de página quando são específicos de uma rota + +**O que NÃO pertence aqui:** +- Componentes visuais reutilizáveis (→ widgets ou shared) +- Lógica de negócio (→ features ou entities) +- Chamadas de API diretas (→ feature/entity.api.ts) + +**Critério:** se dois componentes do mesmo arquivo são usados em rotas diferentes, extraia para widget. + +**Exemplo de estrutura:** +``` +pages/ +├── home/ +│ ├── ui/ +│ │ └── HomePage.tsx ← Compõe , +│ └── index.ts +├── product-detail/ +│ ├── ui/ +│ │ └── ProductDetailPage.tsx +│ └── index.ts +└── checkout/ + ├── ui/ + │ └── CheckoutPage.tsx + └── index.ts +``` + +**Pode importar de:** widgets, features, entities, shared +**Não pode importar de:** app, outras pages + +--- + +## widgets + +**O que pertence aqui:** +- Blocos visuais autônomos e complexos que combinam múltiplos slices +- Componentes que aparecem em múltiplas páginas com a mesma estrutura +- Layouts compostos (Header com navegação + busca + avatar, Sidebar com filtros) + +**O que NÃO pertence aqui:** +- Ações do usuário isoladas (→ features) +- Conceitos de domínio puro (→ entities) + +**Critério:** um widget sabe como compor features e entities, mas não implementa a lógica delas. + +**Exemplo de estrutura:** +``` +widgets/ +├── header/ +│ ├── ui/ +│ │ ├── Header.tsx ← Compõe SearchBar (feature) + UserMenu (entity) +│ │ └── NavigationMenu.tsx +│ ├── model/ +│ │ └── header.model.ts ← Estado de menu aberto/fechado (UI state) +│ └── index.ts +├── product-card/ +│ ├── ui/ +│ │ └── ProductCard.tsx ← Compõe AddToCart (feature) + ProductInfo (entity) +│ └── index.ts +└── sidebar-filters/ + ├── ui/ + │ └── SidebarFilters.tsx + └── index.ts +``` + +**Pode importar de:** features, entities, shared +**Não pode importar de:** app, pages, outros widgets + +--- + +## features + +**O que pertence aqui:** +- Funcionalidades que representam ações do usuário com valor de negócio +- Interações que modificam estado global ou disparam side effects +- Fluxos de formulário vinculados a uma ação específica + +**Nomenclatura:** sempre verbos ou ações (`add-to-cart`, `authenticate-user`, `filter-products`, `submit-review`) + +**O que NÃO pertence aqui:** +- Representação de dados (→ entities) +- Blocos visuais compostos sem ação específica (→ widgets) + +**Critério:** se remover esta feature, o usuário perde a capacidade de *fazer* algo específico. + +**Exemplo de estrutura:** +``` +features/ +├── add-to-cart/ +│ ├── ui/ +│ │ └── AddToCartButton.tsx +│ ├── model/ +│ │ ├── cart.store.ts +│ │ └── cart.types.ts +│ ├── api/ +│ │ └── cart.api.ts +│ └── index.ts ← Exporta AddToCartButton, useCart, cartStore +├── authenticate/ +│ ├── ui/ +│ │ ├── LoginForm.tsx +│ │ └── LogoutButton.tsx +│ ├── model/ +│ │ ├── auth.store.ts +│ │ └── auth.selectors.ts +│ ├── api/ +│ │ └── auth.api.ts +│ ├── lib/ +│ │ └── token.helpers.ts +│ └── index.ts +└── filter-products/ + ├── ui/ + │ └── FilterPanel.tsx + ├── model/ + │ └── filters.store.ts + └── index.ts +``` + +**Pode importar de:** entities, shared +**Não pode importar de:** app, pages, widgets, outras features + +--- + +## entities + +**O que pertence aqui:** +- Conceitos centrais do domínio de negócio (User, Product, Order, Article) +- Modelos de dados, tipos TypeScript e esquemas de validação +- UI básica para representar o conceito (ProductCard sem ações, UserAvatar) +- Queries de leitura de dados (sem mutações — essas ficam em features) +- Seletores e derivações de estado + +**Nomenclatura:** sempre substantivos no singular (`user`, `product`, `order`) + +**O que NÃO pertence aqui:** +- Ações do usuário (→ features) +- Chamadas de API de mutação (POST/PUT/DELETE → features) + +**Critério:** uma entity representa *o que existe* no domínio; uma feature representa *o que o usuário faz*. + +**Cross-reference com `@x`:** +Quando duas entities genuinamente se referenciam (Order precisa do tipo User): +```ts +// entities/user/index.ts — exposição normal +export type { User } from './model/types' + +// entities/user/@x/order.ts — exposição controlada para Order +export type { UserId } from './model/types' + +// entities/order/model/types.ts +import type { UserId } from 'entities/user/@x/order' // ✅ Cross-ref explícita +``` + +**Exemplo de estrutura:** +``` +entities/ +├── user/ +│ ├── ui/ +│ │ ├── UserAvatar.tsx +│ │ └── UserBadge.tsx +│ ├── model/ +│ │ ├── user.types.ts +│ │ ├── user.store.ts +│ │ └── user.selectors.ts +│ ├── api/ +│ │ └── user.api.ts ← Apenas GET/queries +│ ├── @x/ +│ │ └── order.ts ← Cross-reference pública para entity order +│ └── index.ts +├── product/ +│ ├── ui/ +│ │ └── ProductInfo.tsx ← Apenas exibe dados, sem ações +│ ├── model/ +│ │ ├── product.types.ts +│ │ └── product.schema.ts +│ ├── api/ +│ │ └── product.api.ts +│ └── index.ts +└── order/ + ├── model/ + │ └── order.types.ts + ├── api/ + │ └── order.api.ts + └── index.ts +``` + +**Pode importar de:** shared (e outras entities via `@x`) +**Não pode importar de:** app, pages, widgets, features + +--- + +## shared + +**O que pertence aqui:** +- UI Kit: componentes genéricos sem lógica de negócio (Button, Input, Modal, Table) +- Wrappers de bibliotecas externas (axios instance, query client configurado) +- Utilitários agnósticos (formatDate, cn/clsx helpers, validators genéricos) +- Constantes globais (API_BASE_URL, BREAKPOINTS, COLORS) +- Tipos utilitários TypeScript genéricos +- Hooks utilitários sem lógica de negócio (useDebounce, useLocalStorage) + +**O que NÃO pertence aqui:** +- Qualquer código que conheça conceitos de negócio (User, Product) +- Lógica específica de uma feature ou entity + +**Critério:** o código de `shared/` pode ser copiado para qualquer outro projeto sem modificação. + +**Exemplo de estrutura:** +``` +shared/ +├── ui/ ← Componentes genéricos (UI Kit) +│ ├── Button/ +│ │ ├── Button.tsx +│ │ └── index.ts +│ ├── Input/ +│ ├── Modal/ +│ └── index.ts ← Re-exporta tudo +├── api/ ← Configuração base de HTTP +│ ├── axios.instance.ts +│ └── index.ts +├── lib/ ← Utilitários puros +│ ├── format.ts +│ ├── cn.ts +│ └── index.ts +├── config/ ← Variáveis de ambiente e constantes globais +│ ├── env.ts +│ └── index.ts +└── types/ ← Tipos utilitários genéricos + └── index.ts +``` + +**Pode importar de:** nada do projeto (apenas node_modules) +**Não pode importar de:** nenhuma camada + +--- + +## Regras de Importação + +### Matriz de Dependências Permitidas + +| Camada | Pode importar de | +|--------|-----------------| +| app | pages, widgets, features, entities, shared | +| pages | widgets, features, entities, shared | +| widgets | features, entities, shared | +| features | entities, shared | +| entities | shared (+ entities via `@x`) | +| shared | — (apenas node_modules) | + +### Exemplos Práticos + +**✅ Correto:** +```ts +// pages/checkout/ui/CheckoutPage.tsx +import { OrderSummary } from 'widgets/order-summary' // widget abaixo +import { PaymentForm } from 'features/process-payment' // feature abaixo +import { useCurrentUser } from 'entities/user' // entity abaixo +import { Button } from 'shared/ui' // shared abaixo +``` + +**❌ Errado — import horizontal:** +```ts +// features/add-to-cart/model/cart.store.ts +import { authStore } from 'features/authenticate' // ❌ mesma camada! +``` + +**✅ Correto — dado compartilhado sobe via props:** +```tsx +// widgets/product-card/ui/ProductCard.tsx — recebe userId por prop +import { AddToCartButton } from 'features/add-to-cart' +import { useCurrentUser } from 'entities/user' + +export const ProductCard = ({ product }) => { + const { user } = useCurrentUser() // entity + return +} +``` + +**❌ Errado — deep import:** +```ts +import { authStore } from 'features/authenticate/model/auth.store' // ❌ +``` + +**✅ Correto — via index.ts público:** +```ts +import { authStore } from 'features/authenticate' // ✅ +``` + +--- + +## Exemplos de Estrutura Real + +### Projeto E-commerce + +``` +src/ +├── app/ +│ ├── providers/ +│ │ ├── QueryProvider.tsx +│ │ ├── StoreProvider.tsx +│ │ └── index.ts +│ ├── router/ +│ │ ├── routes.tsx +│ │ └── index.ts +│ └── styles/ +│ └── globals.css +│ +├── pages/ +│ ├── home/ +│ ├── catalog/ +│ ├── product-detail/ +│ └── checkout/ +│ +├── widgets/ +│ ├── header/ +│ ├── product-card/ +│ ├── cart-drawer/ +│ └── footer/ +│ +├── features/ +│ ├── add-to-cart/ +│ ├── authenticate/ +│ ├── filter-products/ +│ ├── search-products/ +│ ├── apply-coupon/ +│ └── process-payment/ +│ +├── entities/ +│ ├── product/ +│ ├── user/ +│ ├── cart/ +│ └── order/ +│ +└── shared/ + ├── ui/ + ├── api/ + ├── lib/ + └── config/ +``` + +### Projeto de Leitura de Manga (referência) + +``` +src/ +├── app/ +│ └── (providers, router, styles globais) +│ +├── pages/ +│ ├── reader/ ← Página do leitor de capítulos +│ ├── manga-detail/ ← Página de detalhes do manga +│ ├── catalog/ ← Listagem e busca +│ └── profile/ ← Perfil do usuário +│ +├── widgets/ +│ ├── reader-toolbar/ ← Controles do leitor (modo, página, zoom) +│ ├── chapter-list/ ← Lista de capítulos com progresso +│ └── manga-header/ ← Capa, título, metadados do manga +│ +├── features/ +│ ├── navigate-chapters/ ← Avançar/voltar capítulo +│ ├── bookmark-manga/ ← Adicionar/remover favorito +│ ├── track-progress/ ← Salvar última página lida +│ ├── rate-manga/ ← Dar nota/review +│ └── search-manga/ ← Busca por título/gênero +│ +├── entities/ +│ ├── manga/ ← Modelo, tipos, UI básica de card +│ ├── chapter/ ← Modelo de capítulo, página +│ └── user/ ← Perfil, preferências de leitura +│ +└── shared/ + ├── ui/ ← Button, Icon, Skeleton, Modal + ├── api/ ← axios instance configurado + └── lib/ ← formatDate, cn, storage helpers +``` diff --git a/.agents/skills/fsd-architecture/references/fsd-migration.md b/.agents/skills/fsd-architecture/references/fsd-migration.md new file mode 100644 index 00000000..7002eab0 --- /dev/null +++ b/.agents/skills/fsd-architecture/references/fsd-migration.md @@ -0,0 +1,358 @@ +# FSD Migration — Protocolo de Resolução de Dívida Técnica + +## Princípio Central + +Nunca migre tudo de uma vez. A ordem importa porque as camadas superiores dependem das inferiores — resolver `shared` antes de `entities` garante que quando você chegar em `features`, as peças de baixo já estão corretas. + +**Ordem obrigatória de resolução:** +``` +1. shared +2. entities +3. features +4. widgets +5. pages +6. app +``` + +> Por que de baixo para cima? Porque cada camada depende das abaixo. Corrigir `features` com `entities` ainda incorretas vai gerar retrabalho. + +--- + +## Fase 1 — Auditoria + +Antes de mover qualquer arquivo, faça um mapa do estado atual. + +### Checklist de Auditoria por Camada + +Execute esta verificação para cada camada do projeto: + +#### shared/ +- [ ] Existe algum componente que importa de `entities/` ou `features/`? +- [ ] Existe lógica de negócio específica de um domínio? +- [ ] Existe algum tipo TypeScript que referencia modelos de domínio? +- [ ] Componentes de UI têm dependências de estado global? + +**Problemas comuns encontrados:** +``` +shared/api/user.api.ts ← ❌ lógica de domínio em shared +shared/hooks/useCartTotal.ts ← ❌ hook de negócio em shared +shared/types/Product.ts ← ❌ tipo de domínio em shared +``` + +#### entities/ +- [ ] Alguma entity importa de `features/` ou `widgets/`? +- [ ] Alguma entity importa de outra entity sem usar `@x`? +- [ ] Alguma entity tem lógica de mutação (POST/PUT/DELETE)? +- [ ] Alguma entity tem componentes com ações de negócio embutidas? + +**Problemas comuns encontrados:** +``` +entities/product/ui/ProductCard.tsx ← tem botão "Adicionar ao Carrinho" +entities/user/api/user.api.ts ← tem chamada de login/logout +``` + +#### features/ +- [ ] Alguma feature importa de outra feature (import horizontal)? +- [ ] Alguma feature importa de `widgets/` ou `pages/`? +- [ ] Alguma feature tem representação de dados que pertence a uma entity? +- [ ] Existem deep imports sendo feitos em features? + +**Problemas comuns encontrados:** +``` +features/checkout/model/store.ts ← importa de features/cart diretamente +features/auth/ui/UserProfile.tsx ← componente que pertence em entities/user +``` + +#### widgets/ +- [ ] Algum widget importa de `pages/`? +- [ ] Algum widget importa de outro widget? +- [ ] Algum widget tem lógica de negócio que deveria estar em uma feature? +- [ ] Algum widget está sendo usado em apenas uma page (candidato a mover para pages/)? + +#### pages/ +- [ ] Alguma page importa de outra page? +- [ ] Alguma page tem componentes visuais complexos inline (>50 linhas de JSX)? +- [ ] Alguma page tem lógica de negócio além de orquestração? +- [ ] Alguma page tem chamadas de API diretas sem passar por features/entities? + +#### app/ +- [ ] Providers importam lógica de negócio diretamente? +- [ ] Existe inicialização de store com dados hardcoded que deveriam vir de API? +- [ ] O roteamento referencia componentes de camadas erradas? + +--- + +## Fase 2 — Priorização + +Após a auditoria, classifique os problemas por impacto: + +### Alta Prioridade (resolver primeiro) +- Imports que violam o fluxo top-down (camada superior sendo importada por inferior) +- Deep imports que expõem internals de slices +- Lógica de negócio em `shared/` + +### Média Prioridade +- Imports horizontais entre features +- Entities com lógica de mutação +- God components em pages + +### Baixa Prioridade +- Nomenclatura incorreta de slices +- Segmentos ausentes ou mal organizados +- Falta de `@x` em cross-references entre entities + +--- + +## Fase 3 — Execução por Camada + +### Migrando shared/ + +**Passo 1:** Identifique o que não é agnóstico ao negócio. + +```bash +# Buscar imports de domínio em shared +grep -r "from 'entities\|from 'features" src/shared/ +``` + +**Passo 2:** Mova para a camada correta. + +``` +shared/api/user.api.ts → entities/user/api/user.api.ts +shared/hooks/useCartTotal.ts → features/add-to-cart/lib/useCartTotal.ts +shared/types/Product.ts → entities/product/model/product.types.ts +``` + +**Passo 3:** Atualize todos os imports referenciando o arquivo movido. + +**Passo 4:** Garanta que `shared/` só exporta via `index.ts` raiz. + +--- + +### Migrando entities/ + +**Passo 1:** Separe representação de ação. + +Se `entities/product/ui/ProductCard.tsx` tem um botão "Adicionar ao Carrinho": +```tsx +// ❌ Antes — entity com ação embutida +export const ProductCard = ({ product }) => ( +
+ +

{product.name}

+ {/* ← pertence em feature */} +
+) + +// ✅ Depois — entity apenas exibe, recebe slot de ação +export const ProductInfo = ({ product, actions }) => ( +
+ +

{product.name}

+ {actions} {/* ← injetado por quem compõe: widget ou page */} +
+) +``` + +**Passo 2:** Mova mutações para features. +``` +entities/user/api/user.api.ts (login, logout) → features/authenticate/api/auth.api.ts +entities/product/api/ (apenas GET queries) → permanece em entities +``` + +**Passo 3:** Configure cross-references com `@x` onde necessário. + +--- + +### Migrando features/ + +**Passo 1:** Quebre imports horizontais. + +```ts +// ❌ features/checkout importando de features/cart +import { cartItems } from 'features/cart' + +// ✅ Alternativa A: o dado sobe via props/context do widget/page +// ✅ Alternativa B: o estado compartilhado sobe para entities/cart +``` + +**Passo 2:** Garanta que cada feature tem um `index.ts` com API mínima. + +```ts +// features/authenticate/index.ts +export { LoginForm } from './ui/LoginForm' +export { LogoutButton } from './ui/LogoutButton' +export { authStore } from './model/auth.store' +export { useAuth } from './model/auth.selectors' +// NÃO exportar internals: token.helpers, auth.config +``` + +**Passo 3:** Elimine deep imports de outros slices que apontam para esta feature. + +```bash +grep -r "from 'features/authenticate/" src/ --include="*.ts" --include="*.tsx" +# Todos os resultados fora de features/authenticate/ são violações +``` + +--- + +### Migrando widgets/ + +**Passo 1:** Extraia god components de pages. + +Se `pages/home/ui/HomePage.tsx` tem um Header de 150 linhas inline: +``` +pages/home/ui/HomePage.tsx (Header inline) → widgets/header/ui/Header.tsx +``` + +**Passo 2:** Resolva imports horizontais entre widgets. + +Se `widgets/cart-drawer` importa de `widgets/header`: +``` +# Geralmente o dado compartilhado deve subir para pages/ +# ou ser fornecido via props +``` + +**Passo 3:** Verifique se o widget não é muito específico de uma page (considere mover para pages/). + +--- + +### Migrando pages/ + +**Passo 1:** Torne as pages composições puras. + +```tsx +// ✅ Page ideal: orquestra, não implementa +export const CheckoutPage = () => { + const { user } = useCurrentUser() // entity + + return ( + + {/* widget */} + {/* feature */} + + ) +} +``` + +**Passo 2:** Mova qualquer componente reusável para widgets. + +**Passo 3:** Mova chamadas de API diretas para features ou entities. + +--- + +### Migrando app/ + +**Passo 1:** Garanta que providers não contêm lógica de negócio. + +```tsx +// ❌ Provider com lógica de inicialização inline +export const AppProvider = ({ children }) => { + useEffect(() => { + fetch('/api/user/me').then(...) // ← pertence em entities/user + }, []) +} + +// ✅ Provider delega para hooks de entities +export const AppProvider = ({ children }) => { + useInitializeUser() // hook de entities/user + return <>{children} +} +``` + +**Passo 2:** Garanta que o roteamento usa componentes de pages/ (não de widgets ou features diretamente). + +--- + +## Fase 4 — Validação + +Após cada camada migrada, rode: + +```bash +# Verificar imports proibidos (adapte para sua estrutura) +npx eslint --rule '{"import/no-restricted-paths": "error"}' src/ + +# Ou com o plugin FSD para ESLint +npx eslint src/ --ext .ts,.tsx +``` + +### ESLint Config para FSD (referência) + +```js +// eslint.config.js +import { defineConfig } from 'eslint-define-config' + +export default defineConfig({ + rules: { + // Proibir imports de camadas superiores + 'import/no-restricted-paths': ['error', { + zones: [ + // shared não pode importar de camadas acima + { target: './src/shared', from: './src/entities' }, + { target: './src/shared', from: './src/features' }, + { target: './src/shared', from: './src/widgets' }, + { target: './src/shared', from: './src/pages' }, + { target: './src/shared', from: './src/app' }, + // entities não pode importar de features+ + { target: './src/entities', from: './src/features' }, + { target: './src/entities', from: './src/widgets' }, + { target: './src/entities', from: './src/pages' }, + { target: './src/entities', from: './src/app' }, + // features não pode importar de widgets+ + { target: './src/features', from: './src/widgets' }, + { target: './src/features', from: './src/pages' }, + { target: './src/features', from: './src/app' }, + // widgets não pode importar de pages+ + { target: './src/widgets', from: './src/pages' }, + { target: './src/widgets', from: './src/app' }, + ] + }] + } +}) +``` + +--- + +## Guia de Decisão Rápida + +Quando em dúvida sobre onde colocar um artefato: + +``` +É código genérico sem lógica de negócio? + → shared/ + +É um conceito do domínio (substantivo)? + → entities/ + +É uma ação do usuário (verbo)? + → features/ + +É um bloco visual que combina features e entities? + → widgets/ + +É uma rota/tela completa? + → pages/ + +É inicialização global ou provider? + → app/ +``` + +### Casos Ambíguos Comuns + +**"Meu hook de autenticação: feature ou entity?"** +- Se apenas lê o estado do usuário logado → `entities/user/model/` +- Se faz login/logout/refresh → `features/authenticate/model/` + +**"Meu componente UserAvatar: entity ou shared?"** +- Se usa o tipo `User` do domínio → `entities/user/ui/` +- Se é apenas um avatar genérico com src/alt → `shared/ui/Avatar/` + +**"Meu filtro de produtos: feature ou widget?"** +- Se é apenas a UI de seleção sem estado → pode ser `shared/ui/` +- Se gerencia o estado de filtros ativos → `features/filter-products/` +- Se combina o estado de filtros com a lista de produtos → `widgets/product-filters/` + +**"Tenho um store Zustand/Redux: onde fica?"** +- Estado de UI local de um componente → no próprio componente (`useState`) +- Estado de negócio de uma feature → `features/[name]/model/` +- Estado de dados de um domínio → `entities/[name]/model/` +- Estado global de tema/locale → `app/` ou `shared/config/` diff --git a/.audit-admin/agent-catalogos.md b/.audit-admin/agent-catalogos.md new file mode 100644 index 00000000..3357ea11 --- /dev/null +++ b/.audit-admin/agent-catalogos.md @@ -0,0 +1,42 @@ +# Auditoria administrativa — Catálogos + +Data: 2026-08-01 +Escopo solicitado: Tags (`/dashboard/tags`), Autores (`/dashboard/authors`), Editoras (`/dashboard/publishers`) e Lojas (`/dashboard/stores`). +Método exigido: navegador interno via skill `browser:control-in-app-browser`. +Resultado: **não executada por indisponibilidade da ferramenta obrigatória**. + +## Bloqueio operacional + +Antes de qualquer interação com a aplicação, as instruções completas da skill foram lidas e o runtime recomendado foi inicializado. A seleção do navegador interno falhou com a mensagem: + +```text +Browser is not available: iab +``` + +Foi então seguido o procedimento de recuperação previsto pela própria skill (`bootstrap-troubleshooting`). A descoberta única de navegadores retornou uma lista vazia (`[]`). Como a solicitação exige expressamente essa skill/superfície, a instrução normativa proíbe substituí-la por Playwright externo, Computer Use ou outra automação. + +## Cobertura efetiva + +| Módulo | Rota | Cenários executados | Cobertura | +|---|---|---:|---:| +| Tags | `/dashboard/tags` | 0 | 0% | +| Autores | `/dashboard/authors` | 0 | 0% | +| Editoras | `/dashboard/publishers` | 0 | 0% | +| Lojas | `/dashboard/stores` | 0 | 0% | + +Não foram verificadas listagem, pesquisa, filtros, ordenação, paginação, criação, validações, duplicidade, edição, cancelamento/fechamento, ativação/desativação, exclusão, persistência após recarga, mensagens, console ou chamadas de API. + +## Dados e efeitos + +- Nenhum registro foi criado, editado, ativado, desativado ou excluído. +- Nenhum dos prefixos de auditoria foi usado na aplicação. +- Nenhum código-fonte foi alterado. +- Nenhuma screenshot funcional pôde ser capturada; o diretório de evidências foi reservado em `.audit-admin/evidence/catalogos/`. + +## Bugs ADM + +Nenhum bug funcional ADM foi registrado, pois a aplicação não pôde ser acessada pela ferramenta obrigatória. A indisponibilidade do navegador é uma limitação do ambiente de auditoria, não um defeito atribuído aos módulos testados. + +## Requisito para retomada + +Disponibilizar uma instância do navegador interno nesta sessão e repetir integralmente a auditoria dos quatro módulos. Este relatório não deve ser interpretado como aprovação funcional nem como evidência de ausência de defeitos. diff --git a/.audit-admin/agent-comunidade-financeiro.md b/.audit-admin/agent-comunidade-financeiro.md new file mode 100644 index 00000000..2c92a885 --- /dev/null +++ b/.audit-admin/agent-comunidade-financeiro.md @@ -0,0 +1,60 @@ +# Auditoria administrativa — Comunidade e Financeiro + +## Resumo + +- **Data:** 2026-08-01 +- **Ambiente alvo:** `http://localhost:5173` +- **Perfil previsto:** administrador seed (Ana Beatriz) +- **Módulos atribuídos:** Usuários, Grupos, Financeiro e Assinaturas +- **Módulos efetivamente auditados:** 0 de 4 +- **Funcionalidades testadas:** 0 +- **Problemas funcionais encontrados:** 0 (nenhuma conclusão funcional foi possível) +- **Limitação bloqueante:** o runtime da skill `browser:control-in-app-browser` informou `Browser is not available: iab`; a descoberta de navegadores retornou lista vazia (`[]`). + +## Matriz de cobertura + +| Módulo | Página ou rota | Listagem | Detalhes | Busca/filtros | Ordenação/paginação | Edição/status | Modais/validações | Reload/mensagens/console | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Usuários | `/dashboard/users` e detalhe | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Bloqueado | +| Grupos | `/dashboard/groups` e detalhe | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Bloqueado | +| Financeiro | `/dashboard/financial` | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Bloqueado | +| Assinaturas | `/dashboard/subscriptions` | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Não testado | Bloqueado | + +## Limitação de execução + +Antes de qualquer interação com a aplicação, foi lido integralmente o `SKILL.md` da skill solicitada. A inicialização do runtime foi concluída, mas a seleção explícita do navegador interno falhou com a mensagem: + +```text +Browser is not available: iab +``` + +Seguindo o procedimento de diagnóstico da própria skill, a lista de navegadores disponíveis foi consultada uma única vez e retornou: + +```text +[] +``` + +Como não havia backend de navegador disponível, não foi possível abrir rotas, usar a sessão administrativa existente, inspecionar requisições/console, executar fluxos ou capturar screenshots. A skill determina que, quando o navegador explicitamente solicitado permanece indisponível, ele não deve ser substituído por outra ferramenta ou por inspeção de código. Por isso, nenhum resultado funcional foi inferido ou fabricado. + +## Evidências + +Nenhuma captura de tela da aplicação foi produzida, pois não houve conexão com navegador. O diretório reservado permanece em: + +`/Users/ruan/Documents/Projetos/Manga-Reader/.audit-admin/evidence/comunidade-financeiro/` + +## Registros de teste + +Nenhum registro foi criado, modificado, ativado, desativado ou excluído. Os prefixos reservados não foram usados: + +- `AUDITORIA_ADMIN_USUARIOS_20260801_1` +- `AUDITORIA_ADMIN_GRUPOS_20260801_1` +- `AUDITORIA_ADMIN_FINANCEIRO_20260801_1` +- `AUDITORIA_ADMIN_ASSINATURAS_20260801_1` + +## Problemas ADM + +Nenhum problema ADM foi registrado. A indisponibilidade do navegador é uma limitação do ambiente de auditoria, não uma falha comprovada da aplicação. + +## Próximo passo necessário + +Reexecutar esta fatia da auditoria em uma sessão na qual o navegador interno esteja disponível e conectado. Toda a matriz acima deve então ser percorrida; este relatório não deve ser interpretado como aprovação funcional dos quatro módulos. diff --git a/.audit-admin/agent-noticias-eventos.md b/.audit-admin/agent-noticias-eventos.md new file mode 100644 index 00000000..f807fe43 --- /dev/null +++ b/.audit-admin/agent-noticias-eventos.md @@ -0,0 +1,28 @@ +# Auditoria administrativa — Notícias e Eventos + +Data: 2026-08-01 +Rotas previstas: `/dashboard/news` e `/dashboard/events` +Prefixos reservados: `AUDITORIA_ADMIN_NOTICIAS_20260801_1` e `AUDITORIA_ADMIN_EVENTOS_20260801_1` + +## Resultado + +Auditoria funcional não iniciada por indisponibilidade do navegador integrado na sessão de execução. A descoberta de navegadores retornou uma lista vazia, mesmo após seguir o procedimento de conexão e diagnóstico prescrito pela skill `browser:control-in-app-browser`. + +Nenhum fluxo, aba, modal, listagem, busca, filtro, ordenação, paginação, CRUD, relacionamento, upload, validação, cancelamento, status ou exclusão foi executado. Nenhum registro de auditoria foi criado, alterado ou excluído. Nenhuma conclusão funcional sobre Notícias ou Eventos deve ser inferida deste relatório. + +## Cobertura + +- Notícias (`/dashboard/news`): não coberto. +- Eventos (`/dashboard/events`): não coberto. +- Persistência após reload: não coberta. +- Mensagens de interface, respostas de API e console: não cobertos. +- Evidências visuais: não geradas, pois nenhuma página pôde ser aberta. + +## Limitação bloqueante + +O runtime do navegador foi inicializado, mas não encontrou navegador disponível para `http://localhost:5173/`. O diagnóstico obrigatório confirmou `[]` como lista de backends disponíveis. Como a tarefa exige explicitamente a skill do navegador integrado, não foi usado Playwright externo, inspeção de código ou outra superfície como substituto. + +## Bugs + +Nenhum bug ADM registrado: a aplicação não chegou a ser exercitada, e a indisponibilidade do navegador é uma limitação da infraestrutura da auditoria, não um defeito comprovado dos módulos. + diff --git a/.audit-admin/evidence/root/author-delete-disabled.jpg b/.audit-admin/evidence/root/author-delete-disabled.jpg new file mode 100644 index 00000000..23ee1085 Binary files /dev/null and b/.audit-admin/evidence/root/author-delete-disabled.jpg differ diff --git a/.audit-admin/evidence/root/chapter-title-relationship-missing.jpg b/.audit-admin/evidence/root/chapter-title-relationship-missing.jpg new file mode 100644 index 00000000..e8b0bf72 Binary files /dev/null and b/.audit-admin/evidence/root/chapter-title-relationship-missing.jpg differ diff --git a/.audit-admin/evidence/root/financial-summary-inconsistent.jpg b/.audit-admin/evidence/root/financial-summary-inconsistent.jpg new file mode 100644 index 00000000..470fe102 Binary files /dev/null and b/.audit-admin/evidence/root/financial-summary-inconsistent.jpg differ diff --git a/.audit-admin/evidence/root/overview-dashboard.jpg b/.audit-admin/evidence/root/overview-dashboard.jpg new file mode 100644 index 00000000..99c3a870 Binary files /dev/null and b/.audit-admin/evidence/root/overview-dashboard.jpg differ diff --git a/.audit-admin/evidence/root/search-500-authors.jpg b/.audit-admin/evidence/root/search-500-authors.jpg new file mode 100644 index 00000000..7e3709e8 Binary files /dev/null and b/.audit-admin/evidence/root/search-500-authors.jpg differ diff --git a/.audit-admin/evidence/root/subscriptions-status-dates.jpg b/.audit-admin/evidence/root/subscriptions-status-dates.jpg new file mode 100644 index 00000000..1b5534a6 Binary files /dev/null and b/.audit-admin/evidence/root/subscriptions-status-dates.jpg differ diff --git a/.audit-admin/evidence/root/tag-name-blank-after-reload.jpg b/.audit-admin/evidence/root/tag-name-blank-after-reload.jpg new file mode 100644 index 00000000..207cac8f Binary files /dev/null and b/.audit-admin/evidence/root/tag-name-blank-after-reload.jpg differ diff --git a/.audit-admin/evidence/root/titles-status-inconsistent.jpg b/.audit-admin/evidence/root/titles-status-inconsistent.jpg new file mode 100644 index 00000000..03b88153 Binary files /dev/null and b/.audit-admin/evidence/root/titles-status-inconsistent.jpg differ diff --git a/.gitignore b/.gitignore index 100e7cb4..9c9c1b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ bower_components/ dist/ build/ coverage/ +/.pnpm-store/ npm-debug.log* yarn-debug.log* yarn-error.log* @@ -134,6 +135,5 @@ coverage/ /frontend-apps/design_handoff_header_redesign/ /frontend-apps/Manga Reader Design System/ /frontend-apps/design_handoff_footer_redesign/ -/frontend-apps/design_handoff_header_redesign/ /web/manga-reader/Manga Reader Design System/ /web/landing-page/landing/ diff --git a/.run/API Core.run.xml b/.run/API Core.run.xml new file mode 100644 index 00000000..ba2b99b0 --- /dev/null +++ b/.run/API Core.run.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/.run/All Services.run.xml b/.run/All Services.run.xml new file mode 100644 index 00000000..251eb51c --- /dev/null +++ b/.run/All Services.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.run/Job Orphan Cleaner.run.xml b/.run/Job Orphan Cleaner.run.xml new file mode 100644 index 00000000..1316af71 --- /dev/null +++ b/.run/Job Orphan Cleaner.run.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/.run/Job Rating Aggregator.run.xml b/.run/Job Rating Aggregator.run.xml new file mode 100644 index 00000000..a90068bb --- /dev/null +++ b/.run/Job Rating Aggregator.run.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/.run/Job Trending Aggregator.run.xml b/.run/Job Trending Aggregator.run.xml new file mode 100644 index 00000000..999d9330 --- /dev/null +++ b/.run/Job Trending Aggregator.run.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/.run/Landing Dev.run.xml b/.run/Landing Dev.run.xml new file mode 100644 index 00000000..11c9732f --- /dev/null +++ b/.run/Landing Dev.run.xml @@ -0,0 +1,13 @@ + + + + + + Manga Reader diff --git a/web/manga-reader/package.json b/web/manga-reader/package.json index e764066f..40991fae 100644 --- a/web/manga-reader/package.json +++ b/web/manga-reader/package.json @@ -44,7 +44,6 @@ "react-hook-form": "^7.76.0", "react-i18next": "^15.4.0", "react-router-dom": "6.24.0", - "react-select": "5.10.1", "react-toastify": "11.0.5", "recharts": "^3.8.1", "zod": "^3.25.76" diff --git a/web/manga-reader/src/app/providers/UserSettingsHydrator.tsx b/web/manga-reader/src/app/providers/UserSettingsHydrator.tsx new file mode 100644 index 00000000..bc366570 --- /dev/null +++ b/web/manga-reader/src/app/providers/UserSettingsHydrator.tsx @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; + +import { applySystemPreferences, setAdultContentPreference, useUserSettings, writeStoredUserSettings } from '@entities/user'; +import { useAuth } from '@features/auth'; + +/** + * Hidrata as preferências autenticadas fora da rota de configurações. Assim + * tema, acessibilidade e leitor usam o servidor como fonte persistida em todo + * o aplicativo, inclusive após reload direto em outra rota. + */ +export const UserSettingsHydrator = () => { + const { isLoggedIn, user } = useAuth(); + const { query } = useUserSettings(isLoggedIn, user?.id); + + useEffect(() => { + setAdultContentPreference(user?.adultContentPreference); + }, [user?.adultContentPreference]); + + useEffect(() => { + if (!query.data) return; + writeStoredUserSettings(query.data); + applySystemPreferences(query.data); + }, [query.data]); + + return null; +}; + +export default UserSettingsHydrator; diff --git a/web/manga-reader/src/app/router/ProtectedRoutes.tsx b/web/manga-reader/src/app/router/ProtectedRoutes.tsx index bd330055..8a852cbb 100644 --- a/web/manga-reader/src/app/router/ProtectedRoutes.tsx +++ b/web/manga-reader/src/app/router/ProtectedRoutes.tsx @@ -1,15 +1,18 @@ import { lazy } from 'react'; +import type { ReactNode } from 'react'; -import { AuthGuard, RoleGuard } from '@features/auth'; +import { ADMIN_PORTAL_ROLES, AuthGuard, RoleGuard } from '@features/auth'; const PublishWork = lazy(() => import('@pages/publish-work/ui/PublishWork')); const Library = lazy(() => import('@pages/library/ui/Library')); const MyReviews = lazy(() => import('@pages/review/ui/MyReviews')); +const Notifications = lazy(() => import('@pages/notifications/ui/Notifications')); const AdminLayout = lazy(() => import('@widgets/admin-panel/ui/AdminLayout')); const DashboardOverview = lazy(() => import('@pages/dashboard/ui/DashboardOverview')); const DashboardUsers = lazy(() => import('@pages/dashboard/ui/DashboardUsers')); const DashboardUserDetail = lazy(() => import('@pages/dashboard/ui/DashboardUserDetail')); const DashboardTitles = lazy(() => import('@pages/dashboard/ui/DashboardTitles')); +const DashboardStores = lazy(() => import('@pages/dashboard/ui/DashboardStores')); const DashboardChapters = lazy(() => import('@pages/dashboard/ui/DashboardChapters')); const DashboardChapterDetail = lazy(() => import('@pages/dashboard/ui/DashboardChapterDetail')); const DashboardChapterAnalytics = lazy(() => import('@pages/dashboard/ui/DashboardChapterAnalytics')); @@ -23,6 +26,8 @@ const DashboardTags = lazy(() => import('@pages/dashboard/ui/DashboardTags')); const DashboardAuthors = lazy(() => import('@pages/dashboard/ui/DashboardAuthors')); const DashboardPublishers = lazy(() => import('@pages/dashboard/ui/DashboardPublishers')); +const adminOnly = (page: ReactNode) => {page}; + export const protectedContentRoutes = [ { path: 'library', @@ -40,6 +45,14 @@ export const protectedContentRoutes = [ ), }, + { + path: 'notifications', + element: ( + + + + ), + }, { path: 'i-want-to-publish-work', element: ( @@ -55,29 +68,30 @@ export const adminRoute = [ path: 'dashboard', element: ( - + ), children: [ - { index: true, element: }, - { path: 'users', element: }, - { path: 'users/:userId', element: }, - { path: 'titles', element: }, - { path: 'chapters', element: }, + { index: true, element: adminOnly() }, + { path: 'users', element: adminOnly() }, + { path: 'users/:userId', element: adminOnly() }, + { path: 'titles', element: adminOnly() }, + { path: 'stores', element: adminOnly() }, + { path: 'chapters', element: adminOnly() }, // 'chapters/analytics' precisa vir antes de 'chapters/:chapterId'. - { path: 'chapters/analytics', element: }, - { path: 'chapters/:chapterId', element: }, + { path: 'chapters/analytics', element: adminOnly() }, + { path: 'chapters/:chapterId', element: adminOnly() }, { path: 'news', element: }, - { path: 'events', element: }, - { path: 'groups', element: }, - { path: 'groups/:groupId', element: }, - { path: 'tags', element: }, - { path: 'authors', element: }, - { path: 'publishers', element: }, - { path: 'financial', element: }, - { path: 'subscriptions', element: }, + { path: 'events', element: adminOnly() }, + { path: 'groups', element: adminOnly() }, + { path: 'groups/:groupId', element: adminOnly() }, + { path: 'tags', element: adminOnly() }, + { path: 'authors', element: adminOnly() }, + { path: 'publishers', element: adminOnly() }, + { path: 'financial', element: adminOnly() }, + { path: 'subscriptions', element: adminOnly() }, ], }, ]; diff --git a/web/manga-reader/src/app/router/PublicRoutes.tsx b/web/manga-reader/src/app/router/PublicRoutes.tsx index 3895c7c1..c913e313 100644 --- a/web/manga-reader/src/app/router/PublicRoutes.tsx +++ b/web/manga-reader/src/app/router/PublicRoutes.tsx @@ -8,6 +8,8 @@ const TitleDetailsPage = lazy(() => import('@pages/title/ui/TitleDetails')); const Chapter = lazy(() => import('@pages/chapter/ui/Chapter')); const Groups = lazy(() => import('@pages/group/ui/Groups')); const GroupProfile = lazy(() => import('@pages/group/ui/GroupProfile')); +const AuthorProfile = lazy(() => import('@pages/author')); +const PublisherProfile = lazy(() => import('@pages/publisher')); const News = lazy(() => import('@pages/news/ui/News')); const NewsDetails = lazy(() => import('@pages/news/ui/NewsDetails')); const Events = lazy(() => import('@pages/event/ui/Events')); @@ -53,6 +55,9 @@ export const contentRoutes = [ // Groups { path: 'groups', element: }, { path: 'groups/:groupId', element: }, + { path: 'authors/:slug', element: }, + { path: 'artists/:slug', element: }, + { path: 'publishers/:slug', element: }, // Profile { path: 'profile', element: }, diff --git a/web/manga-reader/src/entities/activity/api/activityService.ts b/web/manga-reader/src/entities/activity/api/activityService.ts new file mode 100644 index 00000000..4650a961 --- /dev/null +++ b/web/manga-reader/src/entities/activity/api/activityService.ts @@ -0,0 +1,22 @@ +import { api } from '@shared/service/http'; +import type { ApiResponse, PageResponse } from '@shared/service/http'; +import { API_URLS } from '@shared/constant/API_URLS'; + +import type { ActivityEvent } from '../model/activity.types'; + +export type GetActivityFeedParams = { + page?: number; + size?: number; +}; + +export const getUserActivityFeed = async ( + userId: string, + { page = 0, size = 20 }: GetActivityFeedParams = {}, +): Promise> => { + const response = await api.get>>( + `${API_URLS.USERS}/${userId}/activity-feed`, + { params: { page, size } }, + ); + + return response.data.data; +}; diff --git a/web/manga-reader/src/entities/activity/index.ts b/web/manga-reader/src/entities/activity/index.ts new file mode 100644 index 00000000..b7fa745f --- /dev/null +++ b/web/manga-reader/src/entities/activity/index.ts @@ -0,0 +1,18 @@ +// Hooks +export { default as useActivityFeed } from './model/useActivityFeed'; + +// Components +export { default as ActivityEventRow } from './ui/ActivityEventRow'; + +// Services +export { getUserActivityFeed } from './api/activityService'; + +// Types +export type { + ActivityEvent, + ActivityEventType, + ChapterReadPayload, + ReviewPostedPayload, + TitleCompletedPayload, + UserFollowedPayload, +} from './model/activity.types'; diff --git a/web/manga-reader/src/entities/activity/model/activity.types.ts b/web/manga-reader/src/entities/activity/model/activity.types.ts new file mode 100644 index 00000000..cbb8f002 --- /dev/null +++ b/web/manga-reader/src/entities/activity/model/activity.types.ts @@ -0,0 +1,35 @@ +export type ChapterReadPayload = { + titleId: string; + titleName: string; + titleCover?: string; + chapterNumber: string; +}; + +export type ReviewPostedPayload = { + titleId: string; + titleName: string; + titleCover?: string; + reviewId: string; + rating: number; +}; + +export type TitleCompletedPayload = { + titleId: string; + titleName: string; + titleCover?: string; +}; + +export type UserFollowedPayload = { + targetType: 'USER' | 'GROUP'; + targetId: string; + targetName: string; + targetAvatar?: string; +}; + +export type ActivityEvent = + | { id: string; type: 'CHAPTER_READ'; payload: ChapterReadPayload; occurredAt: string } + | { id: string; type: 'REVIEW_POSTED'; payload: ReviewPostedPayload; occurredAt: string } + | { id: string; type: 'TITLE_COMPLETED'; payload: TitleCompletedPayload; occurredAt: string } + | { id: string; type: 'USER_FOLLOWED'; payload: UserFollowedPayload; occurredAt: string }; + +export type ActivityEventType = ActivityEvent['type']; diff --git a/web/manga-reader/src/entities/activity/model/useActivityFeed.ts b/web/manga-reader/src/entities/activity/model/useActivityFeed.ts new file mode 100644 index 00000000..7fef8e5c --- /dev/null +++ b/web/manga-reader/src/entities/activity/model/useActivityFeed.ts @@ -0,0 +1,31 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { QUERY_KEYS } from '@shared/constant/QUERY_KEYS'; + +import { getUserActivityFeed } from '../api/activityService'; + +const PAGE_SIZE = 20; + +const useActivityFeed = (userId?: string) => { + const query = useInfiniteQuery({ + queryKey: [QUERY_KEYS.ACTIVITY_FEED, userId], + queryFn: ({ pageParam }) => getUserActivityFeed(userId as string, { page: pageParam, size: PAGE_SIZE }), + initialPageParam: 0, + getNextPageParam: lastPage => (lastPage.last ? undefined : lastPage.page + 1), + enabled: Boolean(userId), + staleTime: 1000 * 60, + }); + + return { + events: query.data?.pages.flatMap(page => page.content) ?? [], + totalElements: query.data?.pages[0]?.totalElements ?? 0, + fetchNextPage: query.fetchNextPage, + hasNextPage: query.hasNextPage, + isFetchingNextPage: query.isFetchingNextPage, + isLoading: query.isLoading, + isError: query.isError, + refetch: query.refetch, + }; +}; + +export default useActivityFeed; diff --git a/web/manga-reader/src/entities/activity/ui/ActivityEventRow.tsx b/web/manga-reader/src/entities/activity/ui/ActivityEventRow.tsx new file mode 100644 index 00000000..23e78b66 --- /dev/null +++ b/web/manga-reader/src/entities/activity/ui/ActivityEventRow.tsx @@ -0,0 +1,64 @@ +import type { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +import { formatPostDate } from '@shared/service/util/formatPostDate'; + +import { Avatar } from '@ui/Avatar'; + +import type { ActivityEvent } from '../model/activity.types'; +import { ACTIVITY_ROW_RENDERERS } from './renderers/registry'; + +type ActivityEventRowProps = { + event: ActivityEvent; + actions?: ReactNode; +}; + +/** Capa/avatar + link de destino — a única parte que precisa saber de todos os tipos (metadado de layout, não regra de negócio). */ +const getCoverAndHref = (event: ActivityEvent): { cover?: string; name: string; href?: string } => { + switch (event.type) { + case 'CHAPTER_READ': + return { + cover: event.payload.titleCover, + name: event.payload.titleName, + href: `/title/${event.payload.titleId}/chapter/${event.payload.chapterNumber}`, + }; + case 'REVIEW_POSTED': + return { cover: event.payload.titleCover, name: event.payload.titleName, href: `/title/${event.payload.titleId}` }; + case 'TITLE_COMPLETED': + return { cover: event.payload.titleCover, name: event.payload.titleName, href: `/title/${event.payload.titleId}` }; + case 'USER_FOLLOWED': + return { + cover: event.payload.targetAvatar, + name: event.payload.targetName, + href: event.payload.targetType === 'GROUP' ? `/groups/${event.payload.targetId}` : `/users/${event.payload.targetId}`, + }; + } +}; + +const ActivityEventRow = ({ event, actions }: ActivityEventRowProps) => { + const Renderer = ACTIVITY_ROW_RENDERERS[event.type]; + const { cover, name, href } = getCoverAndHref(event); + const { label: when, title: whenTitle } = formatPostDate(event.occurredAt); + + return ( +
+ {href ? ( + + + + ) : ( + + )} + + + + + {when} + + + {actions} +
+ ); +}; + +export default ActivityEventRow; diff --git a/web/manga-reader/src/entities/activity/ui/renderers/ChapterReadRow.tsx b/web/manga-reader/src/entities/activity/ui/renderers/ChapterReadRow.tsx new file mode 100644 index 00000000..48f49e23 --- /dev/null +++ b/web/manga-reader/src/entities/activity/ui/renderers/ChapterReadRow.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import { BookOpen } from 'lucide-react'; + +import type { ActivityEvent } from '../../model/activity.types'; + +const ChapterReadRow = ({ event }: { event: ActivityEvent }) => { + const { t } = useTranslation('user'); + + if (event.type !== 'CHAPTER_READ') return null; + + const { titleName, chapterNumber } = event.payload; + + return ( + <> +