From 79f234ee53a2a2325df2f928f71223c58ed268f0 Mon Sep 17 00:00:00 2001 From: "leonardo.santos" Date: Wed, 28 Oct 2026 17:48:54 -0300 Subject: [PATCH] =?UTF-8?q?feat(projetos):=20p=C3=A1gina=20s=C3=B3=20com?= =?UTF-8?q?=20lista=20de=20interessados,=20sem=20cargos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../[language]/projetos/[id]/page-content.tsx | 407 ++++-------------- src/services/api/services/projects.ts | 2 +- 2 files changed, 90 insertions(+), 319 deletions(-) diff --git a/src/app/[language]/projetos/[id]/page-content.tsx b/src/app/[language]/projetos/[id]/page-content.tsx index 46c20e6..f25e3b3 100644 --- a/src/app/[language]/projetos/[id]/page-content.tsx +++ b/src/app/[language]/projetos/[id]/page-content.tsx @@ -3,16 +3,12 @@ import { useState } from "react"; import { useParams } from "next/navigation"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Lightbulb, Pencil, Plus, X } from "lucide-react"; +import { ArrowLeft, Lightbulb } from "lucide-react"; import { useDeleteProjectInterestService, - useDeleteProjectParticipantService, - useDeleteProjectRoleService, useGetMyProjectInterestService, useGetProjectIdeaService, useGetProjectParticipantsService, - useGetProjectRolesService, - usePostProjectRoleService, usePutProjectInterestService, } from "@/services/api/services/projects"; import HTTP_CODES_ENUM from "@/services/api/types/http-codes"; @@ -22,7 +18,6 @@ import { ProjectParticipant, ProjectUserSummary, } from "@/services/api/types/project"; -import { RoleEnum } from "@/services/api/types/role"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import Link from "@/components/link"; @@ -30,11 +25,9 @@ import { EmptyState } from "@/components/ui/empty-state"; import useAuth from "@/services/auth/use-auth"; import { useSnackbar } from "@/hooks/use-snackbar"; import { cn, getApiError } from "@/lib/utils"; -import { projectRoleFromLabel, projectUserName } from "@/lib/project-labels"; -import { FIELD_CLASS } from "../project-components"; +import { projectUserName } from "@/lib/project-labels"; -const MAX_STACK = 4; -const MAX_NAMES = 3; +const MAX_STACK = 7; const AVATAR_COLORS = [ "bg-[#2f5fd8]", @@ -79,25 +72,25 @@ function PersonAvatar({ } /** Fotos sobrepostas, no estilo do Instagram; passando do limite vira "+N". */ -function AvatarStack({ participants }: { participants: ProjectParticipant[] }) { - const visible = participants.slice(0, MAX_STACK); - const extra = participants.length - visible.length; +function AvatarStack({ people }: { people: ProjectParticipant[] }) { + const visible = people.slice(0, MAX_STACK); + const extra = people.length - visible.length; return (
- {visible.map((participant, index) => ( + {visible.map((person, index) => ( 0 && "-ml-4" )} /> ))} {extra > 0 && ( - + +{extra} )} @@ -105,57 +98,18 @@ function AvatarStack({ participants }: { participants: ProjectParticipant[] }) { ); } -function EmptyStack() { - return ( -
-
- ); -} - -function TeamSection({ idea }: { idea: ProjectIdea }) { +function InterestSection({ idea }: { idea: ProjectIdea }) { const { user, isLoaded } = useAuth(); const { enqueueSnackbar } = useSnackbar(); const queryClient = useQueryClient(); - const fetchRoles = useGetProjectRolesService(); const fetchParticipants = useGetProjectParticipantsService(); const getMyInterest = useGetMyProjectInterestService(); const putInterest = usePutProjectInterestService(); const deleteInterest = useDeleteProjectInterestService(); - const postRole = usePostProjectRoleService(); - const deleteRole = useDeleteProjectRoleService(); - const deleteParticipant = useDeleteProjectParticipantService(); - const [manage, setManage] = useState(false); - const [newRole, setNewRole] = useState(""); const [processing, setProcessing] = useState(false); - const [confirmRoleId, setConfirmRoleId] = useState(null); - - const userId = user ? Number(user.id) : null; - const roleId = Number(user?.role?.id); - const canManage = - userId !== null && - (userId === idea.authorId || - roleId === RoleEnum.ADMIN || - roleId === RoleEnum.MODERATOR); - - const { data: roles = [], isLoading: loadingRoles } = useQuery({ - queryKey: ["project-idea-roles", idea.id], - queryFn: async () => { - const { status, data } = await fetchRoles({ ideaId: idea.id }); - return status === HTTP_CODES_ENUM.OK ? data : []; - }, - enabled: !!user, - }); - const { data: participants = [] } = useQuery({ + const { data: participants = [], isLoading } = useQuery({ queryKey: ["project-idea-participants", idea.id], queryFn: async () => { const { status, data } = await fetchParticipants({ ideaId: idea.id }); @@ -173,10 +127,14 @@ function TeamSection({ idea }: { idea: ProjectIdea }) { enabled: !!user, }); + // Uma pessoa por linha, mesmo que tenha marcado interesse mais de uma vez. + const people = participants.filter( + (participant, index, list) => + list.findIndex((item) => item.userId === participant.userId) === index + ); + const joined = !!mine?.interested; + const refresh = () => { - queryClient.invalidateQueries({ - queryKey: ["project-idea-roles", idea.id], - }); queryClient.invalidateQueries({ queryKey: ["project-idea-participants", idea.id], }); @@ -186,89 +144,38 @@ function TeamSection({ idea }: { idea: ProjectIdea }) { queryClient.invalidateQueries({ queryKey: ["project-idea", idea.id] }); }; - const run = async ( - action: () => Promise<{ status: number; data?: unknown }>, - okStatuses: number[], - errorMessage: string, - successMessage?: string - ) => { + const handleToggle = async () => { setProcessing(true); try { - const { status, data } = await action(); - if (okStatuses.includes(status)) { - if (successMessage) { - enqueueSnackbar(successMessage, { variant: "success" }); - } + const { status, data } = joined + ? await deleteInterest({ ideaId: idea.id }) + : await putInterest({ ideaId: idea.id }); + const ok = joined + ? status === HTTP_CODES_ENUM.NO_CONTENT + : status === HTTP_CODES_ENUM.OK || status === HTTP_CODES_ENUM.CREATED; + if (ok) { + enqueueSnackbar( + joined ? "Interesse removido." : "Interesse registrado!", + { variant: "success" } + ); refresh(); - return true; + } else { + enqueueSnackbar(getApiError(data, "Erro ao atualizar interesse."), { + variant: "error", + }); } - enqueueSnackbar(getApiError(data, errorMessage), { variant: "error" }); - return false; } finally { setProcessing(false); } }; - const handleToggle = (roleName: string, joined: boolean) => - joined - ? run( - () => deleteInterest({ ideaId: idea.id, role: roleName }), - [HTTP_CODES_ENUM.NO_CONTENT], - "Erro ao sair do cargo.", - "Você saiu do cargo." - ) - : run( - () => putInterest({ ideaId: idea.id, role: roleName }), - [HTTP_CODES_ENUM.OK, HTTP_CODES_ENUM.CREATED], - "Erro ao entrar no cargo.", - "Você entrou no time!" - ); - - const handleAddRole = async () => { - const name = newRole.trim(); - if (name.length < 2) return; - const ok = await run( - () => postRole({ ideaId: idea.id, name }), - [HTTP_CODES_ENUM.OK, HTTP_CODES_ENUM.CREATED], - "Erro ao adicionar cargo.", - "Cargo adicionado." - ); - if (ok) setNewRole(""); - }; - - const handleRemoveRole = (role: { id: string; name: string }) => { - if (confirmRoleId !== role.id) { - setConfirmRoleId(role.id); - return; - } - setConfirmRoleId(null); - run( - () => deleteRole({ ideaId: idea.id, roleId: role.id }), - [HTTP_CODES_ENUM.NO_CONTENT], - "Erro ao remover cargo.", - "Cargo removido." - ); - }; - - const handleRemoveParticipant = (participant: ProjectParticipant) => - run( - () => - deleteParticipant({ - ideaId: idea.id, - userId: participant.userId, - role: participant.role, - }), - [HTTP_CODES_ENUM.NO_CONTENT], - "Erro ao remover participante." - ); - if (!isLoaded) return null; if (!user) { return (

- Time do projeto + Quem tem interesse

Entre na sua conta {" "} - para ver quem está no time e participar de um cargo. + para ver quem tem interesse e marcar o seu.

); } - const joinedRoles = new Set(mine?.roles ?? []); - return (
-
+

- Time do projeto + Quem tem interesse

- Entre em um ou mais cargos. Quem propôs o projeto também pode atuar - nele - {canManage ? ", e você pode criar cargos novos." : "."} + Marque que você quer participar. Quando houver gente suficiente, + quem propôs chama o grupo e vocês se organizam juntos.

- {canManage && ( - - )} +
- {loadingRoles ? ( -
+ {isLoading ? ( +
+ ) : people.length === 0 ? ( +

+ Ninguém ainda. Seja a primeira pessoa a marcar interesse. +

) : ( -
- {roles.map((role) => { - const inRole = participants.filter((p) => p.role === role.name); - const joined = joinedRoles.has(role.name); - const custom = projectRoleFromLabel(role.name) === undefined; - - return ( -
+ +
    + {people.map((person) => ( +
  • -
    -
    -

    - {role.name} -

    - {custom && ( - - Cargo criado por quem propôs - - )} -
    - {manage && ( - - )} -
    - -
    - {inRole.length === 0 ? ( -
    - - - Ninguém ainda. Seja o primeiro. - -
    - ) : ( - <> - -

    - {(manage ? inRole : inRole.slice(0, MAX_NAMES)).map( - (participant, index, list) => ( - - {participant.user?.username ? ( - - {handleOf(participant.user)} - - ) : ( - handleOf(participant.user) - )} - {manage ? ( - - ) : ( - index < list.length - 1 && "," - )} - - ) - )} - {!manage && inRole.length > MAX_NAMES && ( - e mais {inRole.length - MAX_NAMES} - )} -

    - - )} -
    - - -
- ); - })} -
- )} - - {canManage && manage && ( -
- - setNewRole(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - handleAddRole(); - } - }} - maxLength={40} - placeholder="Novo cargo, por exemplo: Mentor" - className={cn(FIELD_CLASS, "min-w-0 flex-[1_1_280px]")} - /> - -
+ + {person.user?.username ? ( + + {handleOf(person.user)} + + ) : ( + + {handleOf(person.user)} + + )} + + ))} + + )}
); @@ -467,8 +257,6 @@ function ProjectIdeaDetailPageContent() { const params = useParams(); const id = params.id as string; const fetchIdea = useGetProjectIdeaService(); - const fetchRoles = useGetProjectRolesService(); - const { user } = useAuth(); const { data: idea, isLoading } = useQuery({ queryKey: ["project-idea", id], @@ -479,15 +267,6 @@ function ProjectIdeaDetailPageContent() { enabled: !!id, }); - const { data: roles } = useQuery({ - queryKey: ["project-idea-roles", id], - queryFn: async () => { - const { status, data } = await fetchRoles({ ideaId: id }); - return status === HTTP_CODES_ENUM.OK ? data : []; - }, - enabled: !!id && !!user, - }); - if (isLoading) { return (
@@ -514,7 +293,7 @@ function ProjectIdeaDetailPageContent() { idea.status === ProjectIdeaStatus.TEAM_FORMED; return ( -
+
- Aberto a participantes + Aberto a interessados )}

@@ -557,16 +336,8 @@ function ProjectIdeaDetailPageContent() { {idea.interestCount} {" "} - {idea.interestCount === 1 ? "participante" : "participantes"} + {idea.interestCount === 1 ? "interessado" : "interessados"} - {roles && ( - - - {roles.length} - {" "} - {roles.length === 1 ? "cargo" : "cargos"} - - )}

{idea.tags.length > 0 && ( @@ -592,7 +363,7 @@ function ProjectIdeaDetailPageContent() {
)} - +

diff --git a/src/services/api/services/projects.ts b/src/services/api/services/projects.ts index bbeb813..9dfb1a4 100644 --- a/src/services/api/services/projects.ts +++ b/src/services/api/services/projects.ts @@ -174,7 +174,7 @@ export function usePutProjectInterestService() { return useCallback( ( - data: { ideaId: string; role: string; message?: string }, + data: { ideaId: string; role?: string; message?: string }, requestConfig?: RequestConfigType ) => fetch(buildUrl(`project-ideas/${data.ideaId}/interest`), {