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)). --- diff --git a/src/features/discover/components/ProfilesGrid.tsx b/src/features/discover/components/ProfilesGrid.tsx index 8fe4a45..63d2d24 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"; @@ -10,47 +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) { - const parentRef = useRef(null); - - const virtualizer = useVirtualizer({ - count: profiles.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 300, - overscan: 5, - }); - +export function ProfilesGrid({ profiles, onProfileClick, currentUserId }: ProfilesGridProps) { if (profiles.length === 0) { return ; } return ( -
-
- {virtualizer.getVirtualItems().map((item) => ( -
-
- onProfileClick(profiles[item.index])} - /> -
-
- ))} -
+
+ {profiles.map((p, idx) => { + const isOwn = p.id === currentUserId || p.userId === currentUserId; + return ( + onProfileClick(p) : undefined} + /> + ); + })}
); } 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/features/discover/pages/DiscoverPage.tsx b/src/features/discover/pages/DiscoverPage.tsx index 66ddfe5..90e86d6 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 || profile.userId === user?.uid) { + roast.openProfile(profile); + } + }} + />
)} 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 diff --git a/src/shared/components/ui/ProfileCard.tsx b/src/shared/components/ui/ProfileCard.tsx index 3599b6b..0bb6f19 100644 --- a/src/shared/components/ui/ProfileCard.tsx +++ b/src/shared/components/ui/ProfileCard.tsx @@ -1,148 +1,262 @@ -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]; -} +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; -function getDisplayRoles(profile: Profile) { - const roles = [profile.primaryRole, ...(profile.secondaryRoles ?? [])].filter(Boolean); + const isClickable = !!onClick; - return Array.from(new Set(roles)).slice(0, 3); -} + // 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]; -function getDisplayTags(profile: Profile) { - const loves = profile.canvas?.loves ?? []; - const comfort = profile.canvas?.comfort ?? []; + // Decide text colors based on background + const headerText = headerBg === "bg-neo-pink" ? "text-white" : "text-neo-black"; - return Array.from(new Set([...loves, ...comfort])).slice(0, 4); -} + const getGithubUrl = (val?: string) => { + if (!val) return ""; + const clean = val + .trim() + .replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "") + .replace(/\/$/, "") + .replace(/^@/, ""); + return `https://github.com/${clean}`; + }; -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"; + 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}`; + }; - return ( - + ); } 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; 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);