From a4303a4f2abade2d8e775aab456c5baa18898226 Mon Sep 17 00:00:00 2001 From: tony max Date: Sun, 14 Jun 2026 21:55:55 -0300 Subject: [PATCH 1/7] docs: reestruturar documentacao principal e integrar guia de revisao de prs --- CONTRIBUTING.md | 2 +- README.md | 4 +- docs/ARCHITECTURE.md | 9 ++ docs/CODEBASE_MAP.md | 13 +-- docs/PR_REVIEW_GUIDE.md | 183 ++++++++++++++++++++++++++++++++++++++ docs/VISION_MATCH_TECH.md | 17 ++-- 6 files changed, 212 insertions(+), 16 deletions(-) create mode 100644 docs/PR_REVIEW_GUIDE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0010a93..5595bf5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -233,7 +233,7 @@ Closes #42 # Revisão -Todo Pull Request pode receber comentários e solicitações de alteração. +Todo Pull Request pode receber comentários e solicitações de alteração. Se você for um revisor ou quiser entender como o processo de revisão e aprovação funciona no projeto, consulte o [`docs/PR_REVIEW_GUIDE.md`](docs/PR_REVIEW_GUIDE.md). O processo de revisão tem como objetivo: diff --git a/README.md b/README.md index abdef68..d99e660 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,9 @@ O deploy é automático via **Vercel** ao fazer merge em `main`. ## Contribuindo -Contribuições são bem-vindas! Leia [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) para entender os padrões adotados antes de abrir um PR. +Contribuições são bem-vindas! Leia [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) para entender os padrões de código adotados antes de começar a desenvolver. + +Se você estiver revisando Pull Requests de outros colaboradores ou quiser entender as regras de aceite de código, consulte o [`docs/PR_REVIEW_GUIDE.md`](docs/PR_REVIEW_GUIDE.md). 1. Faça um fork e clone o repositório 2. Crie uma branch: `git checkout -b feat/minha-feature` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index de88493..4bad483 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,6 +24,7 @@ Este documento descreve a arquitetura técnica do projeto, os padrões de design 16. [Deploy e CI/CD](#16-deploy-e-cicd) 17. [Docker e Containers](#17-docker-e-containers) 18. [Scripts Utilitários](#18-scripts-utilitários) +19. [Revisão de Código e PRs](#19-revisão-de-código-e-prs) --- @@ -851,3 +852,11 @@ npm run migrate:members-to-profiles -- --execute ``` **Pré-requisito:** a variável `FIREBASE_SERVICE_ACCOUNT` deve conter o JSON da service account do Firebase Admin, ou `GOOGLE_APPLICATION_CREDENTIALS` deve apontar para o arquivo JSON equivalente. + +--- + +## 19. Revisão de Código e PRs + +Para manter a codebase organizada e estável à medida que mais desenvolvedores contribuem, adotamos um processo estruturado de revisão de Pull Requests. + +O fluxo de trabalho, os critérios de aceitação para merges e os checklists de revisão estão documentados em detalhes no [Guia de Revisão de PRs](./PR_REVIEW_GUIDE.md). diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index c806b3b..81a099b 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -150,12 +150,13 @@ match-tech/ │ └── main.tsx # createRoot + global error listeners │ ├── docs/ -│ ├── ARCHITECTURE.md ← Referência técnica principal (atualizada) -│ ├── CODEBASE_MAP.md ← Este arquivo -│ ├── VISION_MATCH_TECH.md ← Visão de produto e design system -│ ├── TODO_MATCH_TECH.md ← Histórico de desenvolvimento (arquivo) -│ ├── FRONTEND_BLUEPRINT.md ← Blueprint original (depreciado → ver ARCHITECTURE.md) -│ └── hackathon_tech_floripa_2026_strategy.md ← Estratégia do evento +│ ├── [ARCHITECTURE.md](./ARCHITECTURE.md) ← Referência técnica principal (atualizada) +│ ├── [CODEBASE_MAP.md](./CODEBASE_MAP.md) ← Este arquivo +│ ├── [VISION_MATCH_TECH.md](./VISION_MATCH_TECH.md) ← Visão de produto e design system +│ ├── [TODO_MATCH_TECH.md](./TODO_MATCH_TECH.md) ← Histórico de desenvolvimento (histórico) +│ ├── [FRONTEND_BLUEPRINT.md](./FRONTEND_BLUEPRINT.md) ← Blueprint original (depreciado → ver ARCHITECTURE.md) +│ ├── [PR_REVIEW_GUIDE.md](./PR_REVIEW_GUIDE.md) ← Guia de revisão de Pull Requests +│ └── [hackathon_tech_floripa_2026_strategy.md](./hackathon_tech_floripa_2026_strategy.md) ← Estratégia do evento │ ├── .github/workflows/ │ └── ci.yml # typecheck → lint → build (Node 22) diff --git a/docs/PR_REVIEW_GUIDE.md b/docs/PR_REVIEW_GUIDE.md new file mode 100644 index 0000000..9cb3151 --- /dev/null +++ b/docs/PR_REVIEW_GUIDE.md @@ -0,0 +1,183 @@ +# 🧭 Guia de Revisão de Pull Requests — Match Tech + +> Para o Tony: dev júnior, criador do projeto, principal mantenedor do `MatchDock/match-tech`. + +--- + +## O que é um Pull Request (PR)? + +É quando alguém do time faz alterações no código e pede para você **revisar antes de aceitar**. Seu papel como mantenedor é funcionar como um porteiro: decidir se aquela mudança entra ou não no projeto. + +> [!NOTE] +> **Você não precisa entender cada linha de código.** O objetivo da revisão é entender *o que* mudou, *por quê*, e se isso pode quebrar algo. + +--- + +## 🔁 Seu Fluxo de Revisão (Passo a Passo) + +### 1. Leia a descrição do PR primeiro + +Antes de ver qualquer código, leia o texto que a pessoa escreveu no PR. Pergunte-se: + +- ✅ O PR descreve o que mudou? +- ✅ Ele referencia uma issue? (ex: `Closes #42`) +- ✅ A branch está correta? (deve ir para `develop`, não para `main`) +- ❌ Se a pessoa só colocou "atualiza código" sem explicar nada → peça uma descrição melhor + +**Link rápido para ver os PRs:** [github.com/MatchDock/match-tech/pulls](https://github.com/MatchDock/match-tech/pulls) + +--- + +### 2. Entenda o escopo — quantas coisas ele mexeu? + +Na aba **"Files changed"** do PR, veja: + +| Sinal | O que significa | +|-------|-----------------| +| Verde (linhas `+`) | Código **adicionado** | +| Vermelho (linhas `-`) | Código **removido** | +| Poucos arquivos alterados | PR pequeno ✅ mais fácil de revisar | +| Muitos arquivos e linhas | PR gigante ⚠️ pode ser difícil de aprovar de uma vez | + +> [!TIP] +> O `CONTRIBUTING.md` do projeto pede PRs pequenos e focados. Se alguém mandou um PR com 50 arquivos alterados que não tem relação entre si, você pode pedir para dividir. + +--- + +### 3. Verifique se o PR segue as regras do projeto + +Baseado no [CONTRIBUTING.md](../CONTRIBUTING.md): + +- [ ] A branch do PR tem nome correto? (`feat/`, `bug/`, `docs/`, `task/`, `refactor/`) +- [ ] O PR está indo para `develop` (não para `main`)? +- [ ] Os commits seguem Conventional Commits? (`feat:`, `fix:`, `docs:`, `refactor:`) +- [ ] O PR faz **uma coisa só** (não mistura feature nova + bugfix + refactor)? + +--- + +### 4. Perguntas práticas para revisar o código + +Você não precisa ser expert. Faça estas perguntas ao olhar o diff: + +#### 🟢 Perguntas básicas (todo PR) +- O que essa mudança adiciona ou resolve? +- Isso pode quebrar algo que já funcionava? +- O nome dos arquivos e funções faz sentido? + +#### 🟡 Para PRs de feature (nova funcionalidade) +- Isso resolve a issue que está referenciada? +- A feature se encaixa no estilo visual do projeto? +- Mexe no Firebase/Firestore de forma que pode perder dados? + +#### 🔴 Para PRs de refatoração (reorganização do código) +- O comportamento da UI continua igual para o usuário? +- Passou no build? (`npm run build` ou CI do GitHub Actions) +- Passou no typecheck do TypeScript? (`tsc --noEmit`) + +#### 🔵 Para PRs de infra/docs +- A documentação faz sentido e está em português? +- Não quebra nenhuma configuração existente? + +--- + +### 5. Como pedir mudanças sem ser rude + +Quando algo está errado ou precisa de ajuste, você pode comentar assim: + +```text +Oi! Ficou bem legal, mas tenho uma dúvida/sugestão: + +❓ Esse arquivo [X] foi modificado, mas não estava relacionado à issue #42. +Você pode remover essa alteração desse PR? + +💡 Sugestão: [explica o que preferia ver] + +Fora isso, está bem feito! 🚀 +``` + +> [!IMPORTANT] +> No GitHub, você pode comentar linha por linha. Clique no `+` que aparece ao lado das linhas no "Files changed" para deixar um comentário específico. + +--- + +### 6. O CI do projeto já faz parte do trabalho por você! + +O repositório tem um **GitHub Actions CI**. Isso significa que automaticamente, em todo PR, o sistema verifica: + +- ✅ O TypeScript compila sem erros (`tsc --noEmit`) +- ✅ O build do Vite funciona (`npm run build`) +- ✅ Todos os testes passam (`npm test`) + +Se o CI falhar (mostrar ❌ vermelho no PR), **você não precisa aprovar**. Peça para o contribuidor corrigir primeiro. + +--- + +## 🎯 Checklist Rápido — Cole nos comentários do PR + +```text +## Checklist de Review ✅ + +- [ ] Descrição clara do que mudou +- [ ] Referencia a issue correta (ex: `Closes #XX`) +- [ ] Branch correta → develop (não main) +- [ ] Nome da branch no padrão (feat/, bug/, docs/...) +- [ ] Commits em Conventional Commits +- [ ] PR focado em uma única coisa +- [ ] CI passou (TypeScript + Build + Testes) ✅ +- [ ] Não quebra funcionalidades existentes +``` + +--- + +## 🚦 Quando aprovar (Merge), pedir mudanças ou fechar + +| Situação | Ação | +|----------|------| +| Tudo certo, CI passou | ✅ **Aprovar e fazer Merge** | +| Tem erros corrigíveis | 🔄 **Request changes** — pedir ajustes | +| PR gigante sem foco | 📝 Pedir para dividir em PRs menores | +| CI falhou (❌ vermelho) | 🚫 Não aprovar até corrigir | +| PR foi para `main` diretamente | 🚫 Fechar e pedir para reabrir para `develop` | +| Não tem relação com nenhuma issue | ❓ Perguntar o contexto antes de decidir | + +--- + +## 📚 Tipos de PR mais comuns no match-tech + +Com base no histórico do projeto, esses são os tipos que você vai ver: + +### PR de Feature (`feat/`) +Ex: adicionar filtro de perfis, nova página, novo componente UI +- Foco: **funciona? se encaixa no design?** + +### PR de Refatoração (`refactor/`) +Ex: PR #56 foi uma refatoração arquitetural gigante (Clean Architecture, Router v7...) +- Foco: **o comportamento mudou? CI passou? tem testes?** + +### PR de Documentação (`docs/`) +Ex: PR #36 atualizou o CONTRIBUTING.md +- Foco: **está em português? é claro? está correto?** + +### PR de Infra (`infra/`) +Ex: PR #57 adicionou GitHub Actions CI +- Foco: **vai rodar na conta da org? tem segredos expostos?** + +--- + +## 💬 Como pedir análise para mim (Antigravity) + +Se receber um PR complicado, pode me mandar assim no chat: + +```text +Me ajuda a revisar o PR #XX: [link] +``` + +Eu leio o diff, o histórico de commits, e te entrego um resumo em português explicando: +- O que mudou +- Se está correto +- O que pedir para o contribuidor ajustar +- Se você pode aprovar ou não + +--- + +> 🚀 **Você está indo bem!** Gerir um repositório open source com contribuidores externos durante um hackathon é bastante coisa. O importante é manter o ritmo, ser justo nas revisões, e não deixar PRs parados por muito tempo. diff --git a/docs/VISION_MATCH_TECH.md b/docs/VISION_MATCH_TECH.md index b03abf3..0686418 100644 --- a/docs/VISION_MATCH_TECH.md +++ b/docs/VISION_MATCH_TECH.md @@ -4,10 +4,11 @@ **Data de Criação:** 07 de Maio de 2026 **Última Atualização:** 07 de Maio de 2026 -> **ATENÇÃO AGENTE DE IA:** Este é o documento de referência PRIMÁRIO do projeto. -> Sempre que você sentir que está "alucinando" ou se perdendo na direção do código, -> volte aqui. Tudo o que este documento diz sobre identidade visual, arquitetura, -> e funcionalidades é LEI. Não invente funcionalidades que não estejam aqui. +> **ATENÇÃO AGENTE DE IA:** Este é o documento de referência PRIMÁRIO de produto do projeto. +> Sempre que você sentir que está "alucinando" ou se perdendo na direção do código ou regras de negócio, +> volte aqui. Tudo o que este documento diz sobre identidade visual, arquitetura de alto nível, +> e funcionalidades é LEI. +> Para a especificação técnica detalhada, organização de pastas pós-refatoração e padrões de código, consulte sempre o [ARCHITECTURE.md](./ARCHITECTURE.md). --- @@ -334,10 +335,10 @@ interface Squad { ## 11. O QUE ESTE DOCUMENTO NÃO COBRE -- Detalhes de implementação de código (veja `FRONTEND_BLUEPRINT.md`). -- Roadmap detalhado com checklist (veja `TODO_MATCH_TECH.md`). -- Regras de Firestore atualizadas (veja `../firestore.rules`). -- Estratégia pessoal do Tony para o hackathon (veja `hackathon_tech_floripa_2026_strategy.md`). +- Detalhes de implementação de código (consulte a referência atualizada em [ARCHITECTURE.md](./ARCHITECTURE.md) ou o histórico em [FRONTEND_BLUEPRINT.md](./FRONTEND_BLUEPRINT.md)). +- Roadmap detalhado com checklist (veja o arquivo histórico [TODO_MATCH_TECH.md](./TODO_MATCH_TECH.md)). +- Regras de Firestore atualizadas (veja [firestore.rules](../firestore.rules)). +- Estratégia pessoal do Tony para o hackathon (veja [hackathon_tech_floripa_2026_strategy.md](./hackathon_tech_floripa_2026_strategy.md)). --- From 7590807f6b35a7c31e975f926de95f73cd0358c7 Mon Sep 17 00:00:00 2001 From: tony max Date: Sun, 14 Jun 2026 22:36:41 -0300 Subject: [PATCH 2/7] feat: implement FirebaseProfileRepository with Zod validation and data mapping for Firestore operations --- .../firebase/profileRepository.ts | 91 +++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/src/infrastructure/firebase/profileRepository.ts b/src/infrastructure/firebase/profileRepository.ts index 10c7040..37529ca 100644 --- a/src/infrastructure/firebase/profileRepository.ts +++ b/src/infrastructure/firebase/profileRepository.ts @@ -21,8 +21,55 @@ import { db } from "@/shared/lib/firebase/firebase.client"; * Handles all Firestore operations related to member profiles * Uses Zod for runtime validation */ +function mapFirestoreToMemberData( + id: string, + data: Record | undefined, +): Record | undefined { + if (!data) return data; + + const mapped = { ...data }; + + // Map identification fields + mapped.uid = (data.uid ?? data.userId ?? id) as string; + mapped.displayName = (data.displayName ?? data.name ?? "") as string; + + // Map role + mapped.role = (data.role ?? data.primaryRole ?? "") as string; + + // Map squad status + mapped.squadStatus = (data.squadStatus ?? data.status ?? "open") as string; + + // Map tags from canvas if tags is not present + if (!mapped.tags) { + const tags: { name: string; sentiment: "love" | "ok" | "veto" }[] = []; + const canvas = data.canvas as + | { loves?: string[]; comfort?: string[]; veto?: string[] } + | undefined; + if (canvas) { + if (canvas.loves && Array.isArray(canvas.loves)) { + canvas.loves.forEach((name: string) => { + if (name) tags.push({ name, sentiment: "love" }); + }); + } + if (canvas.comfort && Array.isArray(canvas.comfort)) { + canvas.comfort.forEach((name: string) => { + if (name) tags.push({ name, sentiment: "ok" }); + }); + } + if (canvas.veto && Array.isArray(canvas.veto)) { + canvas.veto.forEach((name: string) => { + if (name) tags.push({ name, sentiment: "veto" }); + }); + } + } + mapped.tags = tags; + } + + return mapped; +} + export class FirebaseProfileRepository implements IProfileRepository { - private collectionName = "members"; + private collectionName = "profiles"; async getProfile(uid: string): Promise { try { @@ -34,7 +81,8 @@ export class FirebaseProfileRepository implements IProfileRepository { } const data = docSnap.data(); - return MemberSchema.parse(data); + const mappedData = mapFirestoreToMemberData(uid, data); + return MemberSchema.parse(mappedData); } catch (error) { if (error instanceof AppError) throw error; if (error instanceof Error) { @@ -58,7 +106,8 @@ export class FirebaseProfileRepository implements IProfileRepository { throw new AppError("UNAUTHORIZED", `Profile ${uid} is not public`); } - return PublicMemberSchema.parse(data); + const mappedData = mapFirestoreToMemberData(uid, data); + return PublicMemberSchema.parse(mappedData); } catch (error) { if (error instanceof AppError) throw error; if (error instanceof Error) { @@ -71,8 +120,35 @@ export class FirebaseProfileRepository implements IProfileRepository { async updateProfile(uid: string, data: Partial): Promise { try { const docRef = doc(this.getCollection(), uid); + + const dbData: Record = { ...data } as Record; + + if (data.displayName !== undefined) { + dbData.name = data.displayName; + delete dbData.displayName; + } + if (data.role !== undefined) { + dbData.primaryRole = data.role; + delete dbData.role; + } + if (data.squadStatus !== undefined) { + dbData.status = data.squadStatus; + delete dbData.squadStatus; + } + if (data.tags !== undefined) { + const loves = data.tags.filter((t) => t.sentiment === "love").map((t) => t.name); + const comfort = data.tags.filter((t) => t.sentiment === "ok").map((t) => t.name); + const veto = data.tags.filter((t) => t.sentiment === "veto").map((t) => t.name); + dbData.canvas = { loves, comfort, veto }; + delete dbData.tags; + } + if (data.uid !== undefined) { + dbData.userId = data.uid; + delete dbData.uid; + } + const updateData = { - ...data, + ...dbData, updatedAt: serverTimestamp(), }; @@ -91,16 +167,17 @@ export class FirebaseProfileRepository implements IProfileRepository { const constraints = [where("visibility", "==", "public")]; if (filters?.role) { - constraints.push(where("role", "==", filters.role)); + constraints.push(where("primaryRole", "==", filters.role)); } const q = query(this.getCollection(), ...constraints); const querySnapshot = await getDocs(q); const profiles: Member[] = []; - querySnapshot.forEach((doc) => { + querySnapshot.forEach((docSnap) => { try { - const parsed = MemberSchema.parse(doc.data()); + const mappedData = mapFirestoreToMemberData(docSnap.id, docSnap.data()); + const parsed = MemberSchema.parse(mappedData); profiles.push(parsed); } catch { // Skip profiles that fail validation From 2cbe6add0e945c2f6bbdd6da17dc6e5d88229b8c Mon Sep 17 00:00:00 2001 From: tony max Date: Mon, 15 Jun 2026 19:16:23 -0300 Subject: [PATCH 3/7] feat(onboarding): adjust onboarding language to matchmaker identity (Issue #43, #44) --- scripts/migrate-members-to-profiles.ts | 15 ++++++++++----- .../onboarding/components/ArsenalCalibration.tsx | 10 +++++----- .../onboarding/components/GuildPassport.tsx | 16 ++++++++-------- .../onboarding/components/IdentityCard.tsx | 12 ++++++------ src/features/onboarding/pages/Onboarding.tsx | 12 ++++++------ 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/scripts/migrate-members-to-profiles.ts b/scripts/migrate-members-to-profiles.ts index 72fd16c..e2f3fd8 100644 --- a/scripts/migrate-members-to-profiles.ts +++ b/scripts/migrate-members-to-profiles.ts @@ -34,12 +34,16 @@ function initAdmin() { const raw = process.env.FIREBASE_SERVICE_ACCOUNT; if (raw) { const parsed = JSON.parse(raw) as { - projectId: string; - clientEmail: string; - privateKey: string; + project_id: string; + client_email: string; + private_key: string; }; return initializeApp({ - credential: cert({ ...parsed, privateKey: parsed.privateKey.replace(/\\n/g, "\n") }), + credential: cert({ + projectId: parsed.project_id, + clientEmail: parsed.client_email, + privateKey: parsed.private_key.replace(/\\n/g, "\n"), + }), }); } @@ -91,7 +95,8 @@ function toProfileDoc(memberId: string, data: MemberDoc): Record

- CALIBRAGEM DO ARSENAL + ANÁLISE DE INTERESSES

- Precisamos conhecer seu perfil para gerar sua ID única. + Precisamos conhecer seu perfil para gerar sua identificação de matchmaking.

@@ -55,7 +55,7 @@ export function ArsenalCalibration({ form }: Props) {

POR QUE ISSO? - Para que nossa IA crie um mapeamento justo e te conecte às melhores missões, precisamos + Para que nossa IA crie um mapeamento ideal e te conecte às melhores equipes, precisamos de pelo menos 10 opiniões (Amo ou Veto) sobre as tecnologias abaixo.

@@ -70,8 +70,8 @@ export function ArsenalCalibration({ form }: Props) {

STATUS DO SISTEMA: {isCalibrated - ? "SISTEMA CALIBRADO! VOCÊ JÁ PODE ENTRAR, MAS QUANTO MAIS TAGS MARCAR, MELHOR SERÁ SEU MATCH COM A GUILDA." - : `AGUARDANDO DADOS: MARQUE MAIS ${10 - total} TAGS.`} + ? "PERFIL ALINHADO! VOCÊ JÁ PODE SALVAR, MAS QUANTO MAIS TAGS MARCAR, MELHOR SERÁ SEU MATCH COM OUTROS PARTICIPANTES." + : `AGUARDANDO DADOS: PREENCHA MAIS ${10 - total} TAGS.`}

diff --git a/src/features/onboarding/components/GuildPassport.tsx b/src/features/onboarding/components/GuildPassport.tsx index 4cec925..5d17755 100644 --- a/src/features/onboarding/components/GuildPassport.tsx +++ b/src/features/onboarding/components/GuildPassport.tsx @@ -41,7 +41,7 @@ export function GuildPassport({ {/* Passport header */}
- PASAPORTE_GUILDA + PERFIL_MATCH_TECH Tech_Floripa_2026 @@ -66,7 +66,7 @@ export function GuildPassport({

- NOME_OPERADOR: + NOME_PARTICIPANTE:

{form.name || "Aguardando..."} @@ -74,7 +74,7 @@ export function GuildPassport({

- CLASSE_PRIMÁRIA: + FUNÇÃO_PRINCIPAL:

{form.primaryRole || "AGUARDANDO..."} @@ -83,7 +83,7 @@ export function GuildPassport({ {form.secondaryRoles.length > 0 && (

- CLASSES_SEC: + FUNÇÕES_SECUN:

{form.secondaryRoles.map((r) => ( @@ -104,7 +104,7 @@ export function GuildPassport({

- Análise de Campo + Habilidades

Vibe: {skills.vibe_coding}/10 @@ -143,7 +143,7 @@ export function GuildPassport({ {/* Operator ID row */}

-

ID_OPERADOR

+

ID_USUÁRIO

{user?.uid.slice(0, 10).toUpperCase()}

@@ -170,7 +170,7 @@ export function GuildPassport({ }`} disabled={loading || !isUnlocked} > - {loading ? "PROCESSANDO..." : !isUnlocked ? "BLOQUEADO" : "REGISTRAR OPERADOR"} + {loading ? "PROCESSANDO..." : !isUnlocked ? "BLOQUEADO" : "CRIAR SEU PERFIL"} @@ -200,7 +200,7 @@ export function GuildPassport({
{form.loves.length < 3 && (

- ALERTA: Adicione mais PAIXÕES para calibragem total. + ALERTA: Adicione mais PAIXÕES para alinhamento total.

)}
diff --git a/src/features/onboarding/components/IdentityCard.tsx b/src/features/onboarding/components/IdentityCard.tsx index 80cdcb7..eab6c80 100644 --- a/src/features/onboarding/components/IdentityCard.tsx +++ b/src/features/onboarding/components/IdentityCard.tsx @@ -32,10 +32,10 @@ export function IdentityCard({ form, onChange, onBioChange }: Props) {
-

PONTE DE CONFIANÇA:

+

CONEXÃO DE PONTES:

- O algoritmo da guilda cruza dados do GitHub e LinkedIn para validar xp e sugerir - missões de alto impacto. Sem pontes, você é um fantasma. + O algoritmo cruza dados do GitHub e LinkedIn para validar conexões e sugerir equipes + de alto impacto. Sem pontes, você fica isolado.

@@ -44,7 +44,7 @@ export function IdentityCard({ form, onChange, onBioChange }: Props) { {/* Name */}

- MAPEAR MEMBRO_ + MAPEAR PERFIL_

- Protocolo Floripa 2026 + Match Tech — Floripa 2026

- Nível 01: Identificação + Etapa 1: Mapeamento

@@ -125,10 +125,10 @@ export default function Onboarding() {

- ARSENAL_DE_SKILLS + INTERESSES_E_SKILLS

- Defina seu arsenal: [ ❤️ MEU_FOCO | ✅ OPERO_BEM | 🚫 NEM_FUDENDO ] + Defina seus interesses: [ ❤️ AMO | ✅ CONFORTO | 🚫 VETO ]

From 2e4d8704509b4ed271dc40e1bac14e0df02b7d04 Mon Sep 17 00:00:00 2001 From: tony max Date: Mon, 15 Jun 2026 19:44:28 -0300 Subject: [PATCH 4/7] Revert "feat(onboarding): adjust onboarding language to matchmaker identity (Issue #43, #44)" This reverts commit 2cbe6add0e945c2f6bbdd6da17dc6e5d88229b8c. --- scripts/migrate-members-to-profiles.ts | 15 +++++---------- .../onboarding/components/ArsenalCalibration.tsx | 10 +++++----- .../onboarding/components/GuildPassport.tsx | 16 ++++++++-------- .../onboarding/components/IdentityCard.tsx | 12 ++++++------ src/features/onboarding/pages/Onboarding.tsx | 12 ++++++------ 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/scripts/migrate-members-to-profiles.ts b/scripts/migrate-members-to-profiles.ts index e2f3fd8..72fd16c 100644 --- a/scripts/migrate-members-to-profiles.ts +++ b/scripts/migrate-members-to-profiles.ts @@ -34,16 +34,12 @@ function initAdmin() { const raw = process.env.FIREBASE_SERVICE_ACCOUNT; if (raw) { const parsed = JSON.parse(raw) as { - project_id: string; - client_email: string; - private_key: string; + projectId: string; + clientEmail: string; + privateKey: string; }; return initializeApp({ - credential: cert({ - projectId: parsed.project_id, - clientEmail: parsed.client_email, - privateKey: parsed.private_key.replace(/\\n/g, "\n"), - }), + credential: cert({ ...parsed, privateKey: parsed.privateKey.replace(/\\n/g, "\n") }), }); } @@ -95,8 +91,7 @@ function toProfileDoc(memberId: string, data: MemberDoc): Record

- ANÁLISE DE INTERESSES + CALIBRAGEM DO ARSENAL

- Precisamos conhecer seu perfil para gerar sua identificação de matchmaking. + Precisamos conhecer seu perfil para gerar sua ID única.

@@ -55,7 +55,7 @@ export function ArsenalCalibration({ form }: Props) {

POR QUE ISSO? - Para que nossa IA crie um mapeamento ideal e te conecte às melhores equipes, precisamos + Para que nossa IA crie um mapeamento justo e te conecte às melhores missões, precisamos de pelo menos 10 opiniões (Amo ou Veto) sobre as tecnologias abaixo.

@@ -70,8 +70,8 @@ export function ArsenalCalibration({ form }: Props) {

STATUS DO SISTEMA: {isCalibrated - ? "PERFIL ALINHADO! VOCÊ JÁ PODE SALVAR, MAS QUANTO MAIS TAGS MARCAR, MELHOR SERÁ SEU MATCH COM OUTROS PARTICIPANTES." - : `AGUARDANDO DADOS: PREENCHA MAIS ${10 - total} TAGS.`} + ? "SISTEMA CALIBRADO! VOCÊ JÁ PODE ENTRAR, MAS QUANTO MAIS TAGS MARCAR, MELHOR SERÁ SEU MATCH COM A GUILDA." + : `AGUARDANDO DADOS: MARQUE MAIS ${10 - total} TAGS.`}

diff --git a/src/features/onboarding/components/GuildPassport.tsx b/src/features/onboarding/components/GuildPassport.tsx index 5d17755..4cec925 100644 --- a/src/features/onboarding/components/GuildPassport.tsx +++ b/src/features/onboarding/components/GuildPassport.tsx @@ -41,7 +41,7 @@ export function GuildPassport({ {/* Passport header */}
- PERFIL_MATCH_TECH + PASAPORTE_GUILDA Tech_Floripa_2026 @@ -66,7 +66,7 @@ export function GuildPassport({

- NOME_PARTICIPANTE: + NOME_OPERADOR:

{form.name || "Aguardando..."} @@ -74,7 +74,7 @@ export function GuildPassport({

- FUNÇÃO_PRINCIPAL: + CLASSE_PRIMÁRIA:

{form.primaryRole || "AGUARDANDO..."} @@ -83,7 +83,7 @@ export function GuildPassport({ {form.secondaryRoles.length > 0 && (

- FUNÇÕES_SECUN: + CLASSES_SEC:

{form.secondaryRoles.map((r) => ( @@ -104,7 +104,7 @@ export function GuildPassport({

- Habilidades + Análise de Campo

Vibe: {skills.vibe_coding}/10 @@ -143,7 +143,7 @@ export function GuildPassport({ {/* Operator ID row */}

-

ID_USUÁRIO

+

ID_OPERADOR

{user?.uid.slice(0, 10).toUpperCase()}

@@ -170,7 +170,7 @@ export function GuildPassport({ }`} disabled={loading || !isUnlocked} > - {loading ? "PROCESSANDO..." : !isUnlocked ? "BLOQUEADO" : "CRIAR SEU PERFIL"} + {loading ? "PROCESSANDO..." : !isUnlocked ? "BLOQUEADO" : "REGISTRAR OPERADOR"} @@ -200,7 +200,7 @@ export function GuildPassport({
{form.loves.length < 3 && (

- ALERTA: Adicione mais PAIXÕES para alinhamento total. + ALERTA: Adicione mais PAIXÕES para calibragem total.

)}
diff --git a/src/features/onboarding/components/IdentityCard.tsx b/src/features/onboarding/components/IdentityCard.tsx index eab6c80..80cdcb7 100644 --- a/src/features/onboarding/components/IdentityCard.tsx +++ b/src/features/onboarding/components/IdentityCard.tsx @@ -32,10 +32,10 @@ export function IdentityCard({ form, onChange, onBioChange }: Props) {
-

CONEXÃO DE PONTES:

+

PONTE DE CONFIANÇA:

- O algoritmo cruza dados do GitHub e LinkedIn para validar conexões e sugerir equipes - de alto impacto. Sem pontes, você fica isolado. + O algoritmo da guilda cruza dados do GitHub e LinkedIn para validar xp e sugerir + missões de alto impacto. Sem pontes, você é um fantasma.

@@ -44,7 +44,7 @@ export function IdentityCard({ form, onChange, onBioChange }: Props) { {/* Name */}

- MAPEAR PERFIL_ + MAPEAR MEMBRO_

- Match Tech — Floripa 2026 + Protocolo Floripa 2026

- Etapa 1: Mapeamento + Nível 01: Identificação

@@ -125,10 +125,10 @@ export default function Onboarding() {

- INTERESSES_E_SKILLS + ARSENAL_DE_SKILLS

- Defina seus interesses: [ ❤️ AMO | ✅ CONFORTO | 🚫 VETO ] + Defina seu arsenal: [ ❤️ MEU_FOCO | ✅ OPERO_BEM | 🚫 NEM_FUDENDO ]

From 8988917a32e2d09874986e3d19392201a2747714 Mon Sep 17 00:00:00 2001 From: tony max Date: Mon, 15 Jun 2026 19:56:50 -0300 Subject: [PATCH 5/7] fix(discover): restaurar design original do ProfileCard com bento layout, avatar, radar chart e grid 3 colunas --- .../discover/components/ProfilesGrid.tsx | 40 +-- src/features/discover/model/discover.types.ts | 14 + src/shared/components/ui/ProfileCard.tsx | 338 ++++++++++++------ 3 files changed, 239 insertions(+), 153 deletions(-) diff --git a/src/features/discover/components/ProfilesGrid.tsx b/src/features/discover/components/ProfilesGrid.tsx index 8fe4a45..fa89f22 100644 --- a/src/features/discover/components/ProfilesGrid.tsx +++ b/src/features/discover/components/ProfilesGrid.tsx @@ -1,6 +1,3 @@ -import { useVirtualizer } from "@tanstack/react-virtual"; -import { useRef } from "react"; - import type { Profile } from "../model/discover.types"; import { EmptyProfilesState } from "./EmptyProfilesState"; @@ -13,44 +10,15 @@ interface ProfilesGridProps { } export function ProfilesGrid({ profiles, onProfileClick }: ProfilesGridProps) { - const parentRef = useRef(null); - - const virtualizer = useVirtualizer({ - count: profiles.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 300, - overscan: 5, - }); - if (profiles.length === 0) { return ; } return ( -
-
- {virtualizer.getVirtualItems().map((item) => ( -
-
- onProfileClick(profiles[item.index])} - /> -
-
- ))} -
+
+ {profiles.map((p, idx) => ( + onProfileClick(p)} /> + ))}
); } diff --git a/src/features/discover/model/discover.types.ts b/src/features/discover/model/discover.types.ts index c34c76d..a43149e 100644 --- a/src/features/discover/model/discover.types.ts +++ b/src/features/discover/model/discover.types.ts @@ -7,16 +7,30 @@ export type ProfileStatus = "looking" | "open" | "complete"; export interface ProfileCanvas { loves?: string[]; comfort?: string[]; + veto?: string[]; vetoes?: string[]; } +export interface ProfileSkills { + frontend: number; + backend: number; + ux_ui: number; + dados: number; + hardware_android: number; + vibe_coding: number; +} + export interface Profile { id: string; + userId?: string; name?: string; + photoURL?: string | null; github?: string; + linkedin?: string; bio?: string; primaryRole?: string; secondaryRoles?: string[]; + skills?: ProfileSkills; status?: ProfileStatus; roast?: string; roastBrutal?: string; diff --git a/src/shared/components/ui/ProfileCard.tsx b/src/shared/components/ui/ProfileCard.tsx index 3599b6b..2dd04ec 100644 --- a/src/shared/components/ui/ProfileCard.tsx +++ b/src/shared/components/ui/ProfileCard.tsx @@ -1,148 +1,252 @@ -import { Github, Heart, Sparkles, UserRound } from "lucide-react"; +import { Github, Linkedin } from "lucide-react"; + +import Avatar from "./Avatar"; +import { Card } from "./Card"; +import SkillRadar from "./SkillRadar"; +import StatusBadge from "./StatusBadge"; +import TagBadge from "./TagBadge"; import type { Profile } from "@/features/discover/model/discover.types"; +import { cn } from "@/shared/lib/utils/cn"; interface ProfileCardProps { profile: Profile; + onClick?: () => void; + compact?: boolean; + className?: string; colorIndex?: number; - onClick: () => void; -} - -const cardAccentStyles = [ - "shadow-[8px_8px_0_0_#000]", - "shadow-[8px_8px_0_0_#B8FF29]", - "shadow-[8px_8px_0_0_#00F0FF]", - "shadow-[8px_8px_0_0_#FF2E93]", - "shadow-[8px_8px_0_0_#FFD84D]", -]; - -const statusLabelMap: Record = { - looking: "BUSCANDO EQUIPE", - open: "ABERTO A PROPOSTAS", - complete: "EQUIPE FORMADA", -}; - -const statusStylesMap: Record = { - looking: "bg-neo-yellow text-neo-black", - open: "bg-neo-cyan text-neo-black", - complete: "bg-neo-lime text-neo-black", -}; - -function getCardAccentStyle(colorIndex = 0) { - return cardAccentStyles[colorIndex % cardAccentStyles.length]; -} - -function getDisplayRoles(profile: Profile) { - const roles = [profile.primaryRole, ...(profile.secondaryRoles ?? [])].filter(Boolean); - - return Array.from(new Set(roles)).slice(0, 3); } -function getDisplayTags(profile: Profile) { - const loves = profile.canvas?.loves ?? []; - const comfort = profile.canvas?.comfort ?? []; - - return Array.from(new Set([...loves, ...comfort])).slice(0, 4); -} +export default function ProfileCard({ + profile, + onClick, + compact = false, + className, + colorIndex = 0, +}: ProfileCardProps) { + const { + name, + github, + linkedin, + bio, + primaryRole, + secondaryRoles = [], + skills, + canvas = { loves: [], comfort: [], veto: [] }, + status = "looking", + } = profile; + + // Neo-Brutalist color configurations for variety + const bgColors = ["bg-neo-lime", "bg-neo-pink", "bg-neo-cyan", "bg-neo-yellow"]; + const headerBg = bgColors[colorIndex % bgColors.length]; + + // Decide text colors based on background + const headerText = headerBg === "bg-neo-pink" ? "text-white" : "text-neo-black"; + + const getGithubUrl = (val?: string) => { + if (!val) return ""; + const clean = val + .trim() + .replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "") + .replace(/\/$/, "") + .replace(/^@/, ""); + return `https://github.com/${clean}`; + }; + + const getLinkedinUrl = (val?: string) => { + if (!val) return ""; + const clean = val + .trim() + .replace(/^(?:https?:\/\/)?(?:[\w-]+\.)?linkedin\.com\/(?:in|profile)\//i, "") + .replace(/\/$/, "") + .replace(/^@/, ""); + return `https://linkedin.com/in/${clean}`; + }; + + const vetos = canvas.veto ?? canvas.vetoes ?? []; + + if (compact) { + return ( + + {/* Compact Header */} +
+ {primaryRole || "OPERADOR"} +
-export default function ProfileCard({ profile, colorIndex = 0, onClick }: ProfileCardProps) { - const displayRoles = getDisplayRoles(profile); - const displayTags = getDisplayTags(profile); - const statusLabel = statusLabelMap[profile.status ?? ""] ?? "SEM STATUS"; - const statusStyle = statusStylesMap[profile.status ?? ""] ?? "bg-white text-neo-black"; + {/* Compact Body */} +
+ +
+

+ {name || "NOME_NULO"} +

+
+ +
+
+
+
+ ); + } return ( - + ); } From ad2321ad3369e0fa04fb1d018c30b16eb5f89e27 Mon Sep 17 00:00:00 2001 From: tony max Date: Mon, 15 Jun 2026 20:21:48 -0300 Subject: [PATCH 6/7] feat: add profile discovery UI components including roast modal, profile grid, and radar chart --- .../discover/components/ProfilesGrid.tsx | 17 +- src/features/discover/pages/DiscoverPage.tsx | 12 +- src/shared/components/ui/ProfileCard.tsx | 20 ++- src/shared/components/ui/RoastModal.tsx | 2 +- src/shared/components/ui/SkillRadar.tsx | 145 ++++++++++-------- 5 files changed, 124 insertions(+), 72 deletions(-) diff --git a/src/features/discover/components/ProfilesGrid.tsx b/src/features/discover/components/ProfilesGrid.tsx index fa89f22..82c96d9 100644 --- a/src/features/discover/components/ProfilesGrid.tsx +++ b/src/features/discover/components/ProfilesGrid.tsx @@ -7,18 +7,27 @@ import ProfileCard from "@/shared/components/ui/ProfileCard"; interface ProfilesGridProps { profiles: Profile[]; onProfileClick: (profile: Profile) => void; + currentUserId?: string; } -export function ProfilesGrid({ profiles, onProfileClick }: ProfilesGridProps) { +export function ProfilesGrid({ profiles, onProfileClick, currentUserId }: ProfilesGridProps) { if (profiles.length === 0) { return ; } return (
- {profiles.map((p, idx) => ( - onProfileClick(p)} /> - ))} + {profiles.map((p, idx) => { + const isOwn = p.id === currentUserId; + return ( + onProfileClick(p) : undefined} + /> + ); + })}
); } diff --git a/src/features/discover/pages/DiscoverPage.tsx b/src/features/discover/pages/DiscoverPage.tsx index 66ddfe5..ba89828 100644 --- a/src/features/discover/pages/DiscoverPage.tsx +++ b/src/features/discover/pages/DiscoverPage.tsx @@ -1,4 +1,4 @@ -import { AnimatePresence, motion } from "motion/react"; +import { AnimatePresence, motion } from "motion/react"; import { AccessDeniedState } from "../components/AccessDeniedState"; import { DiscoverFilters } from "../components/DiscoverFilters"; @@ -40,7 +40,15 @@ export default function DiscoverPage() { ) : (
- + { + if (profile.id === user?.uid) { + roast.openProfile(profile); + } + }} + />
)} diff --git a/src/shared/components/ui/ProfileCard.tsx b/src/shared/components/ui/ProfileCard.tsx index 2dd04ec..0bb6f19 100644 --- a/src/shared/components/ui/ProfileCard.tsx +++ b/src/shared/components/ui/ProfileCard.tsx @@ -36,6 +36,8 @@ export default function ProfileCard({ status = "looking", } = profile; + const isClickable = !!onClick; + // Neo-Brutalist color configurations for variety const bgColors = ["bg-neo-lime", "bg-neo-pink", "bg-neo-cyan", "bg-neo-yellow"]; const headerBg = bgColors[colorIndex % bgColors.length]; @@ -72,7 +74,10 @@ export default function ProfileCard({ padding="none" onClick={onClick} className={cn( - "flex flex-col border-4 neo-shadow-hover h-full cursor-pointer group", + "flex flex-col border-4 h-full group", + isClickable + ? "neo-shadow-hover cursor-pointer" + : "active:translate-0 active:shadow-[6px_6px_0px_0px_#000000] cursor-default", className, )} > @@ -116,7 +121,10 @@ export default function ProfileCard({ padding="none" onClick={onClick} className={cn( - "flex flex-col border-4 neo-shadow-hover h-full cursor-pointer group select-none relative", + "flex flex-col border-4 h-full group select-none relative", + isClickable + ? "neo-shadow-hover cursor-pointer" + : "active:translate-0 active:shadow-[6px_6px_0px_0px_#000000] cursor-default", className, )} > @@ -243,9 +251,11 @@ export default function ProfileCard({ status={status} className="shadow-none border-2 text-[9px] py-1 px-2.5 bg-white" /> - - VER SINA → - + {isClickable && ( + + VER SINA → + + )}
); diff --git a/src/shared/components/ui/RoastModal.tsx b/src/shared/components/ui/RoastModal.tsx index f9e3625..8e8525b 100644 --- a/src/shared/components/ui/RoastModal.tsx +++ b/src/shared/components/ui/RoastModal.tsx @@ -104,7 +104,7 @@ export function RoastModal({
- [SISTEMA ROASTED & TOASTED] © 2026 + [SISTEMA ROASTED & TOASTED] © 2026 STRICTLY CONFIDENTIAL diff --git a/src/shared/components/ui/SkillRadar.tsx b/src/shared/components/ui/SkillRadar.tsx index 442fb72..749d340 100644 --- a/src/shared/components/ui/SkillRadar.tsx +++ b/src/shared/components/ui/SkillRadar.tsx @@ -13,70 +13,95 @@ interface SkillRadarProps { size?: "sm" | "md" | "lg"; } -// Wrapped in memo so the SVG only re-renders when skill values actually change. -// RadarChart is one of the most expensive Recharts components — memoisation is critical -// when multiple cards are visible simultaneously (e.g. guilda grid). -const SkillRadar = memo(function SkillRadar({ skills, size = "md" }: SkillRadarProps) { - const data = useMemo( - () => [ - { subject: "Front", A: skills?.frontend || 0 }, - { subject: "Back", A: skills?.backend || 0 }, - { subject: "UX/UI", A: skills?.ux_ui || 0 }, - { subject: "Dados", A: skills?.dados || 0 }, - { subject: "Hard", A: skills?.hardware_android || 0 }, - { subject: "Vibe AI", A: skills?.vibe_coding || 0 }, - ], - [ - skills?.frontend, - skills?.backend, - skills?.ux_ui, - skills?.dados, - skills?.hardware_android, - skills?.vibe_coding, - ], - ); +const DIMS = { sm: 192, md: 256, lg: 320 } as const; +const OUTER_RADIUS = { sm: "65%", md: "70%", lg: "75%" } as const; - // Fixed dims — no ResponsiveContainer to avoid the width/height = -1 Recharts bug - const dims = { sm: 192, md: 256, lg: 320 }; - const chartSize = dims[size]; - const fontSize = size === "sm" ? 9 : size === "md" ? 10 : 12; - const outerRadius = size === "sm" ? "65%" : size === "md" ? "70%" : "75%"; +interface CustomTickProps { + x?: number; + y?: number; + payload?: { value: string }; + fontSize?: number; +} +const CustomTick = memo(function CustomTick({ x, y, payload, fontSize = 10 }: CustomTickProps) { return ( -
-
- - - - - -
+ + {payload?.value} + ); }); +const SkillRadar = memo( + function SkillRadar({ skills, size = "md" }: SkillRadarProps) { + const data = useMemo( + () => [ + { subject: "Front", A: skills?.frontend || 0 }, + { subject: "Back", A: skills?.backend || 0 }, + { subject: "UX/UI", A: skills?.ux_ui || 0 }, + { subject: "Dados", A: skills?.dados || 0 }, + { subject: "Hard", A: skills?.hardware_android || 0 }, + { subject: "Vibe AI", A: skills?.vibe_coding || 0 }, + ], + [ + skills?.frontend, + skills?.backend, + skills?.ux_ui, + skills?.dados, + skills?.hardware_android, + skills?.vibe_coding, + ], + ); + + const chartSize = DIMS[size]; + const outerRadius = OUTER_RADIUS[size]; + const fontSize = size === "sm" ? 9 : size === "md" ? 10 : 12; + + const tickElement = useMemo(() => , [fontSize]); + + return ( +
+
+ + + + + +
+ ); + }, + (prevProps, nextProps) => { + if (prevProps.size !== nextProps.size) return false; + const p = prevProps.skills; + const n = nextProps.skills; + if (!p && !n) return true; + if (!p || !n) return false; + return ( + p.frontend === n.frontend && + p.backend === n.backend && + p.ux_ui === n.ux_ui && + p.dados === n.dados && + p.hardware_android === n.hardware_android && + p.vibe_coding === n.vibe_coding + ); + }, +); + export default SkillRadar; From 9dd4048305fbe5fffe1fce1aa0a8edd715fd78e3 Mon Sep 17 00:00:00 2001 From: tony max Date: Mon, 15 Jun 2026 20:32:05 -0300 Subject: [PATCH 7/7] fix(discover): resolve user ID mismatch in own profile check --- src/features/discover/components/ProfilesGrid.tsx | 4 ++-- src/features/discover/pages/DiscoverPage.tsx | 2 +- src/shared/hooks/useFirestoreSubscription.ts | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/discover/components/ProfilesGrid.tsx b/src/features/discover/components/ProfilesGrid.tsx index 82c96d9..63d2d24 100644 --- a/src/features/discover/components/ProfilesGrid.tsx +++ b/src/features/discover/components/ProfilesGrid.tsx @@ -18,10 +18,10 @@ export function ProfilesGrid({ profiles, onProfileClick, currentUserId }: Profil return (
{profiles.map((p, idx) => { - const isOwn = p.id === currentUserId; + const isOwn = p.id === currentUserId || p.userId === currentUserId; return ( onProfileClick(p) : undefined} diff --git a/src/features/discover/pages/DiscoverPage.tsx b/src/features/discover/pages/DiscoverPage.tsx index ba89828..90e86d6 100644 --- a/src/features/discover/pages/DiscoverPage.tsx +++ b/src/features/discover/pages/DiscoverPage.tsx @@ -44,7 +44,7 @@ export default function DiscoverPage() { profiles={filters.filteredProfiles} currentUserId={user?.uid} onProfileClick={(profile) => { - if (profile.id === user?.uid) { + if (profile.id === user?.uid || profile.userId === user?.uid) { roast.openProfile(profile); } }} diff --git a/src/shared/hooks/useFirestoreSubscription.ts b/src/shared/hooks/useFirestoreSubscription.ts index 4d81222..2d9a409 100644 --- a/src/shared/hooks/useFirestoreSubscription.ts +++ b/src/shared/hooks/useFirestoreSubscription.ts @@ -51,7 +51,10 @@ export function useFirestoreSubscription({ q, (snapshot) => { try { - const docs = snapshot.docs.map((doc) => doc.data() as T); + const docs = snapshot.docs.map((doc) => ({ + id: doc.id, + ...(doc.data() as object), + } as T)); const sorted = sortFn ? docs.sort(sortFn) : docs; setData(sorted); setError(null);