diff --git a/firestore.rules b/firestore.rules index 68c4e41..a9e8bf0 100644 --- a/firestore.rules +++ b/firestore.rules @@ -110,7 +110,7 @@ service cloud.firestore { && profileId == request.auth.uid && isValidProfile(incoming()) && incoming().createdAt == request.time - && incoming().keys().hasOnly(['userId', 'name', 'photoURL', 'github', 'linkedin', 'bio', 'primaryRole', 'secondaryRoles', 'skills', 'canvas', 'status', 'squadId', 'eventId', 'roast', 'roastBrutal', 'roastMild', 'createdAt', 'updatedAt']); + && incoming().keys().hasOnly(['userId', 'name', 'photoURL', 'github', 'linkedin', 'bio', 'primaryRole', 'secondaryRoles', 'skills', 'canvas', 'status', 'squadId', 'eventId', 'roast', 'roastBrutal', 'roastMild', 'createdAt', 'updatedAt', 'visibility']); allow update: if isSignedIn() && isValidId(profileId) @@ -118,7 +118,7 @@ service cloud.firestore { && incoming().userId == existing().userId && incoming().createdAt == existing().createdAt && ( - (incoming().diff(existing()).affectedKeys().hasOnly(['skills', 'canvas', 'name', 'photoURL', 'github', 'linkedin', 'bio', 'primaryRole', 'secondaryRoles', 'status', 'squadId', 'eventId', 'updatedAt', 'roast', 'roastBrutal', 'roastMild'])) + (incoming().diff(existing()).affectedKeys().hasOnly(['skills', 'canvas', 'name', 'photoURL', 'github', 'linkedin', 'bio', 'primaryRole', 'secondaryRoles', 'status', 'squadId', 'eventId', 'updatedAt', 'roast', 'roastBrutal', 'roastMild', 'visibility'])) || false ); @@ -152,5 +152,31 @@ service cloud.firestore { allow create: if isSignedIn() && userId == request.auth.uid && isValidLike(incoming()); allow delete: if isSignedIn() && userId == request.auth.uid; } + + function isValidMessage(data) { + return data.keys().hasAll(['senderId', 'senderName', 'receiverId', 'receiverName', 'text', 'createdAt', 'read']) + && data.keys().size() == 7 + && data.senderId == request.auth.uid + && data.senderName is string && data.senderName.size() > 0 && data.senderName.size() <= 100 + && data.receiverId is string && data.receiverId.size() > 0 && data.receiverId.size() <= 128 + && data.receiverName is string && data.receiverName.size() > 0 && data.receiverName.size() <= 100 + && data.text is string && data.text.size() > 0 && data.text.size() <= 5000 + && data.read is bool; + } + + match /messages/{messageId} { + allow create: if isSignedIn() + && isValidId(messageId) + && isValidMessage(incoming()) + && incoming().createdAt == request.time; + + allow read, delete: if isSignedIn() + && (resource.data.receiverId == request.auth.uid || resource.data.senderId == request.auth.uid); + + allow update: if isSignedIn() + && (resource.data.receiverId == request.auth.uid) + && incoming().diff(existing()).affectedKeys().hasOnly(['read']) + && incoming().read is bool; + } } } diff --git a/scripts/dump-profiles.ts b/scripts/dump-profiles.ts new file mode 100644 index 0000000..ab69be2 --- /dev/null +++ b/scripts/dump-profiles.ts @@ -0,0 +1,45 @@ +import { cert, initializeApp } from "firebase-admin/app"; +import { getFirestore } from "firebase-admin/firestore"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +const raw = process.env.FIREBASE_SERVICE_ACCOUNT; +if (!raw) { + console.error("Missing FIREBASE_SERVICE_ACCOUNT"); + process.exit(1); +} + +const parsed = JSON.parse(raw) as { + project_id?: string; + projectId?: string; + client_email?: string; + clientEmail?: string; + private_key?: string; + privateKey?: string; +}; + +const projectId = parsed.projectId ?? parsed.project_id; +const clientEmail = parsed.clientEmail ?? parsed.client_email; +const privateKey = parsed.privateKey ?? parsed.private_key; + +const app = initializeApp({ + credential: cert({ + projectId, + clientEmail, + privateKey: privateKey?.replace(/\\n/g, "\n"), + }), +}); + +const db = getFirestore(app, process.env.VITE_FIREBASE_FIRESTORE_DATABASE_ID || "ai-studio-a1333439-9ab3-4356-9f79-ac211cc82b20"); + +async function main() { + const snap = await db.collection("profiles").get(); + console.log(`Encontrados ${snap.size} perfis no Firestore:`); + snap.forEach(doc => { + const data = doc.data(); + console.log(`ID: ${doc.id}, Nome: ${data.name || data.displayName || "Sem nome"}, Visibilidade: ${data.visibility}, Status: ${data.status}`); + }); +} + +main().catch(console.error); diff --git a/scripts/migrate-members-to-profiles.ts b/scripts/migrate-members-to-profiles.ts index 72fd16c..7e0c03c 100644 --- a/scripts/migrate-members-to-profiles.ts +++ b/scripts/migrate-members-to-profiles.ts @@ -34,12 +34,28 @@ function initAdmin() { const raw = process.env.FIREBASE_SERVICE_ACCOUNT; if (raw) { const parsed = JSON.parse(raw) as { - projectId: string; - clientEmail: string; - privateKey: string; + projectId?: string; + project_id?: string; + clientEmail?: string; + client_email?: string; + privateKey?: string; + private_key?: string; }; + + const projectId = parsed.projectId ?? parsed.project_id; + const clientEmail = parsed.clientEmail ?? parsed.client_email; + const privateKey = parsed.privateKey ?? parsed.private_key; + + if (!projectId || !clientEmail || !privateKey) { + throw new Error("Missing projectId, clientEmail, or privateKey in FIREBASE_SERVICE_ACCOUNT env var."); + } + return initializeApp({ - credential: cert({ ...parsed, privateKey: parsed.privateKey.replace(/\\n/g, "\n") }), + credential: cert({ + projectId, + clientEmail, + privateKey: privateKey.replace(/\\n/g, "\n"), + }), }); } @@ -91,7 +107,7 @@ function toProfileDoc(memberId: string, data: MemberDoc): Record void; + onContactClick?: (profile: Profile) => void; currentUserId?: string; } -export function ProfilesGrid({ profiles, onProfileClick, currentUserId }: ProfilesGridProps) { +export function ProfilesGrid({ + profiles, + onProfileClick, + onContactClick, + currentUserId, +}: ProfilesGridProps) { if (profiles.length === 0) { return ; } @@ -25,6 +31,7 @@ export function ProfilesGrid({ profiles, onProfileClick, currentUserId }: Profil profile={p} colorIndex={idx} onClick={isOwn ? () => onProfileClick(p) : undefined} + onContactClick={onContactClick ? () => onContactClick(p) : undefined} /> ); })} diff --git a/src/features/discover/hooks/useProfilesRealtime.ts b/src/features/discover/hooks/useProfilesRealtime.ts index a7a6cba..e2b3fbb 100644 --- a/src/features/discover/hooks/useProfilesRealtime.ts +++ b/src/features/discover/hooks/useProfilesRealtime.ts @@ -10,10 +10,13 @@ export function useProfilesRealtime(currentUserId?: string) { collectionName: "profiles", }); - const profiles = useMemo( - () => (currentUserId ? sortProfiles(data, currentUserId) : []), - [data, currentUserId], - ); + const profiles = useMemo(() => { + console.log("useProfilesRealtime: raw data from Firestore:", data); + console.log("useProfilesRealtime: currentUserId:", currentUserId); + const sorted = currentUserId ? sortProfiles(data, currentUserId) : []; + console.log("useProfilesRealtime: sorted profiles:", sorted); + return sorted; + }, [data, currentUserId]); return { profiles, loading, error }; } diff --git a/src/features/discover/pages/DiscoverPage.tsx b/src/features/discover/pages/DiscoverPage.tsx index 90e86d6..9dd3d95 100644 --- a/src/features/discover/pages/DiscoverPage.tsx +++ b/src/features/discover/pages/DiscoverPage.tsx @@ -1,4 +1,5 @@ import { AnimatePresence, motion } from "motion/react"; +import { useState } from "react"; import { AccessDeniedState } from "../components/AccessDeniedState"; import { DiscoverFilters } from "../components/DiscoverFilters"; @@ -12,6 +13,7 @@ import { useRoastProfile } from "../hooks/useRoastProfile"; import { useToast } from "../hooks/useToast"; import { useAuth } from "@/contexts/useAuth"; +import { SendMessageModal } from "@/features/messages/components/SendMessageModal"; export default function DiscoverPage() { const { user } = useAuth(); @@ -23,6 +25,8 @@ export default function DiscoverPage() { const roast = useRoastProfile({ showToast }); + const [contactTarget, setContactTarget] = useState<{ id: string; name: string } | null>(null); + return ( { + setContactTarget({ + id: profile.id || profile.userId!, + name: profile.name || "Operador Anônimo", + }); + }} /> )} @@ -64,6 +74,17 @@ export default function DiscoverPage() { /> )} + + + {contactTarget && ( + setContactTarget(null)} + onSuccess={() => showToast("Mensagem enviada com sucesso!", "info")} + /> + )} + ); } diff --git a/src/features/guilda/components/GuildAvatar.tsx b/src/features/guilda/components/GuildAvatar.tsx deleted file mode 100644 index 94c2128..0000000 --- a/src/features/guilda/components/GuildAvatar.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useMemo, useState } from "react"; - -import type { AvatarProps } from "../model/guilda.types"; -import { getAvatarSources } from "../utils/guilda.formatters"; - -export function GuildAvatar({ member, currentUser, getGithubUrl }: AvatarProps) { - const [imageIndex, setImageIndex] = useState(0); - - const photoUrlToUse = - member.photoURL || - (currentUser && currentUser.uid === member.id ? currentUser.photoURL || undefined : undefined); - - const sources = useMemo( - () => getAvatarSources(photoUrlToUse, member.github, getGithubUrl), - [photoUrlToUse, member.github, getGithubUrl], - ); - - const currentSrc = sources[imageIndex]; - - if (!currentSrc) { - return ( -
- {member.name?.[0] || "?"} -
- ); - } - - return ( - {member.name} setImageIndex((index) => index + 1)} - /> - ); -} diff --git a/src/features/guilda/components/GuildHeader.tsx b/src/features/guilda/components/GuildHeader.tsx deleted file mode 100644 index de2c78e..0000000 --- a/src/features/guilda/components/GuildHeader.tsx +++ /dev/null @@ -1,31 +0,0 @@ -interface GuildHeaderProps { - totalMembers: number; -} - -export function GuildHeader({ totalMembers }: GuildHeaderProps) { - return ( -
-
-

- A Guilda_ -

-
-

- Esquadrão Operacional Tech Floripa '26 -

-
- - STATUS: PRONTO PARA COMBATE -
-
-
- -
-
- OPERADORES - [{totalMembers}] -
-
-
- ); -} diff --git a/src/features/guilda/components/GuildMemberCard.tsx b/src/features/guilda/components/GuildMemberCard.tsx deleted file mode 100644 index ea74e98..0000000 --- a/src/features/guilda/components/GuildMemberCard.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import type { User } from "firebase/auth"; -import { Github, Linkedin, Skull } from "lucide-react"; -import { motion } from "motion/react"; -import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"; - -import { getCardPalette, getRadarData } from "../model/guilda.selectors"; -import type { GuildMember, RoastStep } from "../model/guilda.types"; -import { getGithubUrl, getLinkedinUrl } from "../utils/guilda.formatters"; - -import { GuildAvatar } from "./GuildAvatar"; - -interface GuildMemberCardProps { - member: GuildMember; - colorIndex: number; - user: User; - roastActiveMember: string | null; - roastStep: RoastStep; - roastLogs: string[]; - onOpenRoastSelection: (memberId: string) => void; - onExecuteRoast: (member: GuildMember, persona: "brutal" | "mild") => void; -} - -export function GuildMemberCard({ - member, - colorIndex, - user, - roastActiveMember, - roastStep, - roastLogs, - onOpenRoastSelection, - onExecuteRoast, -}: GuildMemberCardProps) { - const colors = getCardPalette(colorIndex); - - return ( -
-
-
- - {member.primaryRole || "OPERADOR"} - - {member.secondaryRoles && member.secondaryRoles.length > 0 && ( -
- {member.secondaryRoles.map((role) => ( - - {role} - - ))} -
- )} -
- - ID_{member.id.slice(0, 5)} - -
- -
-
-
-
- -
- -
- -

- {member.name || "NOME_NULO"} -

- -
- {member.github && ( - - HUB - - )} - {member.linkedin && ( - - IN - - )} -
-
- -
-
-
- - - - - -
- -
-
- - PAIXÕES (LOVES) - -
- {member.canvas?.loves && member.canvas.loves.length > 0 ? ( - member.canvas.loves.map((item) => ( - - {item} - - )) - ) : ( - Mistério. - )} -
-
- -
- - OPERO BEM - -
- {member.canvas?.comfort && member.canvas.comfort.length > 0 ? ( - member.canvas.comfort.map((item) => ( - - {item} - - )) - ) : ( - Neutro. - )} -
-
- -
- - NEM F*DENDO - -
- {member.canvas?.veto && member.canvas.veto.length > 0 ? ( - member.canvas.veto.map((item) => ( - - {item} - - )) - ) : ( - Faz qualquer jogo. - )} -
-
-
-
-
- - {user.uid === member.id && ( -
- {roastActiveMember !== member.id ? ( - - ) : roastStep === "selecting" ? ( -
- - -
- ) : ( -
- {roastLogs.map((log, index) => ( - - {">"} {log} - - ))} - - - -
- )} -
- )} -
- ); -} diff --git a/src/features/guilda/components/GuildMembersGrid.tsx b/src/features/guilda/components/GuildMembersGrid.tsx deleted file mode 100644 index 948cea8..0000000 --- a/src/features/guilda/components/GuildMembersGrid.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { User } from "firebase/auth"; - -import type { GuildMember, RoastStep } from "../model/guilda.types"; - -import { GuildMemberCard } from "./GuildMemberCard"; - -interface GuildMembersGridProps { - members: GuildMember[]; - user: User; - roastActiveMember: string | null; - roastStep: RoastStep; - roastLogs: string[]; - onOpenRoastSelection: (memberId: string) => void; - onExecuteRoast: (member: GuildMember, persona: "brutal" | "mild") => void; -} - -export function GuildMembersGrid({ - members, - user, - roastActiveMember, - roastStep, - roastLogs, - onOpenRoastSelection, - onExecuteRoast, -}: GuildMembersGridProps) { - return ( -
- {members.map((member, index) => ( - - ))} -
- ); -} diff --git a/src/features/guilda/components/GuildRoastModal.tsx b/src/features/guilda/components/GuildRoastModal.tsx deleted file mode 100644 index f3cfeaa..0000000 --- a/src/features/guilda/components/GuildRoastModal.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { GuildMember } from "../model/guilda.types"; - -import type { RoastPersona } from "@/domain/entities/Shared"; -import { RoastModal as SharedRoastModal } from "@/shared/components/ui/RoastModal"; - -interface GuildRoastModalProps { - selectedMember: GuildMember; - activePersonaView: RoastPersona | null; - onClose: () => void; - onGeneratePersona: (member: GuildMember, persona: RoastPersona) => void; -} - -export function GuildRoastModal({ - selectedMember, - activePersonaView, - onClose, - onGeneratePersona, -}: GuildRoastModalProps) { - const roastText = - activePersonaView === "brutal" - ? selectedMember.roastBrutal || selectedMember.roast - : selectedMember.roastMild || selectedMember.roastBrutal || selectedMember.roast; - - return ( - { - onClose(); - onGeneratePersona(selectedMember, "brutal"); - }} - onGenerateMild={() => { - onClose(); - onGeneratePersona(selectedMember, "mild"); - }} - /> - ); -} diff --git a/src/features/guilda/components/GuildStates.tsx b/src/features/guilda/components/GuildStates.tsx deleted file mode 100644 index c954eab..0000000 --- a/src/features/guilda/components/GuildStates.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Zap } from "lucide-react"; - -import { AccessDeniedState as SharedAccessDeniedState } from "@/shared/components/states/AccessDeniedState"; - -export function GuildAccessDeniedState() { - return ( - - ); -} - -export function GuildLoadingState() { - return ( -
-

- AGUARDANDO OPERADORES... -

-
- ); -} diff --git a/src/features/guilda/constants/guilda.constants.ts b/src/features/guilda/constants/guilda.constants.ts deleted file mode 100644 index 38455f7..0000000 --- a/src/features/guilda/constants/guilda.constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const GUILD_CARD_PALETTES = [ - { bg: "bg-neo-lime", accent: "bg-neo-pink", text: "text-neo-black" }, - { bg: "bg-neo-pink", accent: "bg-neo-cyan", text: "text-white" }, - { bg: "bg-neo-cyan", accent: "bg-neo-yellow", text: "text-neo-black" }, - { bg: "bg-neo-yellow", accent: "bg-neo-lime", text: "text-neo-black" }, -] as const; - -export const ROAST_LOGS_SEQUENCE = [ - "Iniciando conexão neural...", - "Lendo vetos e paixões...", - "Avaliando nível de vibração AI...", - "Preparando o veredito final...", -] as const; diff --git a/src/features/guilda/hooks/useGuildMembersRealtime.ts b/src/features/guilda/hooks/useGuildMembersRealtime.ts deleted file mode 100644 index ace9771..0000000 --- a/src/features/guilda/hooks/useGuildMembersRealtime.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useMemo } from "react"; - -import { sortMembers } from "../model/guilda.selectors"; -import type { GuildMember } from "../model/guilda.types"; - -import { useFirestoreSubscription } from "@/shared/hooks/useFirestoreSubscription"; - -export function useGuildMembersRealtime(currentUserId?: string) { - const { data, loading, error } = useFirestoreSubscription({ - collectionName: "profiles", - }); - - const members = useMemo( - () => (currentUserId ? sortMembers(data, currentUserId) : []), - [data, currentUserId], - ); - - return { members, loading, error }; -} diff --git a/src/features/guilda/hooks/useGuildRoast.ts b/src/features/guilda/hooks/useGuildRoast.ts deleted file mode 100644 index 2212790..0000000 --- a/src/features/guilda/hooks/useGuildRoast.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { useEffect, useRef } from "react"; - -import { ROAST_LOGS_SEQUENCE } from "../constants/guilda.constants"; -import type { GuildMember, RoastPersona } from "../model/guilda.types"; -import { saveRoast } from "../services/guilda.repository"; -import { useGuildRoastStore } from "../store/guildRoast"; - -import { requestRoast } from "@/shared/services/roast.service"; - -function getRoastByPersona(member: GuildMember, persona: RoastPersona) { - return persona === "brutal" ? member.roastBrutal : member.roastMild; -} - -export function useGuildRoast() { - const store = useGuildRoastStore(); - const logsIntervalRef = useRef(null); - - const clearLogsInterval = () => { - if (logsIntervalRef.current) { - window.clearInterval(logsIntervalRef.current); - logsIntervalRef.current = null; - } - }; - - useEffect(() => clearLogsInterval, []); - - const startLogs = (persona: RoastPersona) => { - const sequence = [`Selecionando persona: ${persona.toUpperCase()}...`, ...ROAST_LOGS_SEQUENCE]; - let currentLog = 0; - store.setRoastLogs([sequence[0]]); - clearLogsInterval(); - logsIntervalRef.current = window.setInterval(() => { - currentLog += 1; - if (currentLog < sequence.length) { - store.appendRoastLog(sequence[currentLog]); - } - }, 1500); - }; - - const roastMutation = useMutation({ - mutationFn: ({ member, persona }: { member: GuildMember; persona: RoastPersona }) => - requestRoast({ memberId: member.id, memberData: member, persona }), - onMutate: ({ persona }) => { - startLogs(persona); - }, - onSuccess: async (data, { member, persona }) => { - if (!data.roast) { - console.error("Erro no backend:", data); - return; - } - let updateData: Partial & { updatedAt?: Date } = {}; - try { - updateData = await saveRoast(member.id, data.roast, persona); - } catch (dbError) { - console.error("Erro ao salvar sina no banco:", dbError); - } - store.setSelectedMember({ ...member, ...updateData }); - }, - onError: (error) => { - console.error("Erro ao chamar o roast:", error); - }, - onSettled: () => { - clearLogsInterval(); - store.setRoastStep(null); - store.setRoastActiveMember(null); - }, - }); - - function executeRoast(member: GuildMember, persona: RoastPersona) { - store.setActivePersonaView(persona); - const existingRoast = getRoastByPersona(member, persona); - if (existingRoast) { - store.setSelectedMember(member); - return; - } - store.setRoastStep("loading"); - roastMutation.mutate({ member, persona }); - } - - return { - ...store, - executeRoast, - }; -} diff --git a/src/features/guilda/model/guilda.selectors.ts b/src/features/guilda/model/guilda.selectors.ts deleted file mode 100644 index 30a36ca..0000000 --- a/src/features/guilda/model/guilda.selectors.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GUILD_CARD_PALETTES } from "../constants/guilda.constants"; - -import type { GuildMember, MemberSkills, RadarDatum } from "./guilda.types"; - -import { sortByCurrentUserAndName } from "@/shared/lib/utils/entity"; - -export function sortMembers(members: GuildMember[], currentUserId: string) { - return sortByCurrentUserAndName(members, currentUserId); -} - -export function getCardPalette(index: number) { - return GUILD_CARD_PALETTES[index % GUILD_CARD_PALETTES.length]; -} - -export function getRadarData(skills?: MemberSkills): RadarDatum[] { - if (!skills) return []; - - return [ - { subject: "Front", A: skills.frontend || 0, fullMark: 10 }, - { subject: "Back", A: skills.backend || 0, fullMark: 10 }, - { subject: "UX", A: skills.ux_ui || 0, fullMark: 10 }, - { subject: "Dados", A: skills.dados || 0, fullMark: 10 }, - { subject: "Hard", A: skills.hardware_android || 0, fullMark: 10 }, - { subject: "AI", A: skills.vibe_coding || 0, fullMark: 10 }, - ]; -} diff --git a/src/features/guilda/model/guilda.types.ts b/src/features/guilda/model/guilda.types.ts deleted file mode 100644 index 325aac2..0000000 --- a/src/features/guilda/model/guilda.types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { User } from "firebase/auth"; - -export type { RoastPersona } from "@/domain/entities/Shared"; -export type RoastStep = "selecting" | "loading" | null; - -export interface MemberSkills { - frontend?: number; - backend?: number; - ux_ui?: number; - dados?: number; - hardware_android?: number; - vibe_coding?: number; -} - -export interface MemberCanvas { - loves?: string[]; - comfort?: string[]; - veto?: string[]; -} - -export interface GuildMember { - id: string; - name?: string; - photoURL?: string; - github?: string; - linkedin?: string; - primaryRole?: string; - secondaryRoles?: string[]; - skills?: MemberSkills; - canvas?: MemberCanvas; - roast?: string; - roastBrutal?: string; - roastMild?: string; -} - -export interface AvatarProps { - member: GuildMember; - currentUser: User | null; - getGithubUrl: (value: string) => string; -} - -export interface RadarDatum { - subject: string; - A: number; - fullMark: number; -} diff --git a/src/features/guilda/pages/GuildaPage.tsx b/src/features/guilda/pages/GuildaPage.tsx deleted file mode 100644 index d59b70e..0000000 --- a/src/features/guilda/pages/GuildaPage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { AnimatePresence, motion } from "motion/react"; - -import { GuildHeader } from "../components/GuildHeader"; -import { GuildMembersGrid } from "../components/GuildMembersGrid"; -import { GuildRoastModal } from "../components/GuildRoastModal"; -import { GuildAccessDeniedState, GuildLoadingState } from "../components/GuildStates"; -import { useGuildMembersRealtime } from "../hooks/useGuildMembersRealtime"; -import { useGuildRoast } from "../hooks/useGuildRoast"; - -import { useAuth } from "@/contexts/useAuth"; - -export default function GuildaPage() { - const { user } = useAuth(); - const { members } = useGuildMembersRealtime(user?.uid); - const roast = useGuildRoast(); - - return ( - - - - {!user ? ( - - ) : members.length === 0 ? ( - - ) : ( - - )} - - - {roast.selectedMember && ( - - )} - - - ); -} diff --git a/src/features/guilda/services/guilda.repository.ts b/src/features/guilda/services/guilda.repository.ts deleted file mode 100644 index f217ee8..0000000 --- a/src/features/guilda/services/guilda.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { doc, updateDoc } from "firebase/firestore"; - -import type { GuildMember } from "../model/guilda.types"; - -import type { RoastPersona } from "@/domain/entities/Shared"; -import { db } from "@/shared/lib/firebase/firebase.client"; - -export async function saveRoast(memberId: string, roast: string, persona: RoastPersona) { - const updateData: Partial & { updatedAt: Date } = { - updatedAt: new Date(), - ...(persona === "brutal" ? { roastBrutal: roast } : { roastMild: roast }), - }; - - await updateDoc(doc(db, "profiles", memberId), updateData); - - return updateData; -} diff --git a/src/features/guilda/store/guildRoast.ts b/src/features/guilda/store/guildRoast.ts deleted file mode 100644 index 5a02692..0000000 --- a/src/features/guilda/store/guildRoast.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { create } from "zustand"; - -import type { GuildMember, RoastPersona, RoastStep } from "../model/guilda.types"; - -interface GuildRoastState { - selectedMember: GuildMember | null; - roastActiveMember: string | null; - roastStep: RoastStep; - roastLogs: string[]; - activePersonaView: RoastPersona | null; - setSelectedMember: (member: GuildMember | null) => void; - setRoastActiveMember: (id: string | null) => void; - setRoastStep: (step: RoastStep) => void; - appendRoastLog: (log: string) => void; - setRoastLogs: (logs: string[]) => void; - setActivePersonaView: (persona: RoastPersona | null) => void; - closeSelectedMember: () => void; - openRoastSelection: (memberId: string) => void; -} - -export const useGuildRoastStore = create((set) => ({ - selectedMember: null, - roastActiveMember: null, - roastStep: null, - roastLogs: [], - activePersonaView: null, - setSelectedMember: (selectedMember) => set({ selectedMember }), - setRoastActiveMember: (roastActiveMember) => set({ roastActiveMember }), - setRoastStep: (roastStep) => set({ roastStep }), - setRoastLogs: (roastLogs) => set({ roastLogs }), - appendRoastLog: (log) => set((state) => ({ roastLogs: [...state.roastLogs, log].slice(-3) })), - setActivePersonaView: (activePersonaView) => set({ activePersonaView }), - closeSelectedMember: () => set({ selectedMember: null }), - openRoastSelection: (roastActiveMember) => set({ roastActiveMember, roastStep: "selecting" }), -})); diff --git a/src/features/guilda/utils/guilda.formatters.ts b/src/features/guilda/utils/guilda.formatters.ts deleted file mode 100644 index 0cab613..0000000 --- a/src/features/guilda/utils/guilda.formatters.ts +++ /dev/null @@ -1,47 +0,0 @@ -export function getGithubUrl(value: string) { - if (!value) return ""; - - const clean = value - .trim() - .replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "") - .replace(/\/$/, "") - .replace(/^@/, ""); - - return `https://github.com/${clean}`; -} - -export function getLinkedinUrl(value: string) { - if (!value) return ""; - - const clean = value - .trim() - .replace(/^(?:https?:\/\/)?(?:[\w-]+\.)?linkedin\.com\/(?:in|profile)\//i, "") - .replace(/\/$/, "") - .replace(/^@/, ""); - - return `https://linkedin.com/in/${clean}`; -} - -export function getAvatarSources( - photoURL: string | undefined, - github: string | undefined, - githubUrlFormatter: (value: string) => string, -) { - const sources: string[] = []; - - if (photoURL) { - let photo = photoURL; - if (photo.includes("googleusercontent.com") && photo.includes("=s96-c")) { - photo = photo.replace("=s96-c", "=s400-c"); - } else if (photo.includes("googleusercontent.com") && !photo.includes("=")) { - photo = `${photo}=s400-c`; - } - sources.push(photo); - } - - if (github) { - sources.push(`${githubUrlFormatter(github)}.png`); - } - - return sources; -} diff --git a/src/features/landing/Landing.tsx b/src/features/landing/Landing.tsx index afcc1d7..2bc095c 100644 --- a/src/features/landing/Landing.tsx +++ b/src/features/landing/Landing.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { CtaSection } from "./components/CtaSection"; import { Footer } from "./components/Footer"; @@ -19,7 +19,7 @@ export default function Landing() { navigate("/onboarding")} - onNavigateGuilda={() => navigate("/guilda")} + onNavigateDiscover={() => navigate("/discover")} /> navigate("/onboarding")} /> diff --git a/src/features/landing/components/HeroSection.tsx b/src/features/landing/components/HeroSection.tsx index 7916663..d05b241 100644 --- a/src/features/landing/components/HeroSection.tsx +++ b/src/features/landing/components/HeroSection.tsx @@ -7,10 +7,10 @@ interface Props { // eslint-disable-next-line user: any; // TODO onNavigateOnboarding: () => void; - onNavigateGuilda: () => void; + onNavigateDiscover: () => void; } -export function HeroSection({ user, onNavigateOnboarding, onNavigateGuilda }: Props) { +export function HeroSection({ user, onNavigateOnboarding, onNavigateDiscover }: Props) { return (
{user && ( - )} diff --git a/src/features/messages/components/SendMessageModal.tsx b/src/features/messages/components/SendMessageModal.tsx new file mode 100644 index 0000000..c209787 --- /dev/null +++ b/src/features/messages/components/SendMessageModal.tsx @@ -0,0 +1,175 @@ +import { Send, AlertTriangle } from "lucide-react"; +import { motion } from "motion/react"; +import { useState } from "react"; + +import { useAuth } from "@/contexts/useAuth"; +import { messageRepository } from "@/infrastructure/firebase/messageRepository"; + +interface SendMessageModalProps { + receiverId: string; + receiverName: string; + onClose: () => void; + onSuccess?: () => void; +} + +export function SendMessageModal({ + receiverId, + receiverName, + onClose, + onSuccess, +}: SendMessageModalProps) { + const { user } = useAuth(); + const [text, setText] = useState(""); + const [isSending, setIsSending] = useState(false); + const [error, setError] = useState(null); + + const handleSend = async (e: React.FormEvent) => { + e.preventDefault(); + if (!user) { + setError("Você precisa estar autenticado para enviar mensagens."); + return; + } + if (!text.trim()) { + setError("Digite sua mensagem antes de enviar."); + return; + } + if (text.length > 2000) { + setError("Mensagem muito longa! Limite de 2000 caracteres."); + return; + } + + try { + setIsSending(true); + setError(null); + + const senderName = user.displayName || "Operador Anônimo"; + + await messageRepository.sendMessage({ + senderId: user.uid, + senderName: senderName, + receiverId, + receiverName, + text: text.trim(), + }); + + setText(""); + if (onSuccess) onSuccess(); + onClose(); + } catch (err) { + setError("Erro ao enviar mensagem. Tente novamente."); + console.error(err); + } finally { + setIsSending(false); + } + }; + + return ( +
+ {/* Backdrop */} + + +
+ {/* Modal Container */} + + {/* Close button */} +
+ +
+ +
+ {/* Header */} +
+
+ +
+

+ Enviar Mensagem_ +

+

+ DESTINATÁRIO: {receiverName} +

+
+
+ + {/* Body Form */} +
+
+ + + Sua mensagem será enviada diretamente para a caixa de entrada interna deste + operador. + +
+ + {error && ( +
+ {error} +
+ )} + +
+ +