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 && (
-
setManage((value) => !value)}
- className="gap-2"
- >
-
- {manage ? "Concluir edição" : "Gerenciar cargos"}
-
- )}
+
+ {joined ? "Remover meu interesse" : "Tenho interesse"}
+
- {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 && (
-
handleRemoveRole(role)}
- disabled={processing}
- aria-label={`Remover cargo ${role.name}`}
- className="text-muted-foreground"
- >
- {confirmRoleId === role.id ? (
-
- Remover?
-
- ) : (
-
- )}
-
- )}
-
-
-
- {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 ? (
-
- handleRemoveParticipant(participant)
- }
- disabled={processing}
- className="rounded p-0.5 text-muted-foreground hover:text-destructive"
- aria-label={`Remover ${handleOf(
- participant.user
- )}`}
- >
-
-
- ) : (
- index < list.length - 1 && ","
- )}
-
- )
- )}
- {!manage && inRole.length > MAX_NAMES && (
- e mais {inRole.length - MAX_NAMES}
- )}
-
- >
- )}
-
-
- handleToggle(role.name, joined)}
- disabled={processing}
- className="h-11 w-full"
- >
- {joined ? "Sair do cargo" : "Participar"}
-
-
- );
- })}
-
- )}
-
- {canManage && manage && (
-
-
- Novo cargo
-
-
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]")}
- />
-
-
- Adicionar cargo
-
-
+
+ {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`), {