From 495dc2ac37c1f4593ce53dc462f51dac0080b81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Fran=C3=A7a?= Date: Tue, 27 Jan 2026 23:51:24 -0300 Subject: [PATCH 1/4] feat: add new game group api calls and related hooks --- src/api.ts | 171 ++++++++++++++++++++++++++++--- src/hooks/use-group-game-days.ts | 9 ++ src/hooks/use-group-players.ts | 9 ++ src/hooks/use-group.ts | 9 ++ src/hooks/use-groups.ts | 4 + src/hooks/use-players.ts | 15 --- src/types.ts | 10 ++ 7 files changed, 198 insertions(+), 29 deletions(-) create mode 100644 src/hooks/use-group-game-days.ts create mode 100644 src/hooks/use-group-players.ts create mode 100644 src/hooks/use-group.ts create mode 100644 src/hooks/use-groups.ts delete mode 100644 src/hooks/use-players.ts diff --git a/src/api.ts b/src/api.ts index 4fb1a12..fd25965 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,36 +1,162 @@ -import { GameDay, Player } from "./types"; +import { GameDay, GameDayPlayer, GameGroup, Player } from "./types"; const API_URL = window.location.hostname === "localhost" ? "http://localhost:4000" : "https://plankton-app-xoik3.ondigitalocean.app"; -const putPlayer = async (player: Player) => { +const createGroup = async (name: string, players: Player[] = []) => { try { - const res = await fetch(`${API_URL}/players/`, { - body: JSON.stringify(player), + const res = await fetch(`${API_URL}/groups`, { + method: "POST", headers: { "Content-Type": "application/json", }, + body: JSON.stringify({ name, players }), credentials: "include", + }); + if (!res.ok) return null; + return (await res.json()) as { id: string; inviteCode: string }; + } catch (error) { + console.error(error); + return null; + } +}; + +const getGroups = async () => { + try { + const res = await fetch(`${API_URL}/groups`, { + credentials: "include", + }); + if (!res.ok) return []; + return (await res.json()) as GameGroup[]; + } catch (error) { + console.error(error); + return []; + } +}; + +const getGroup = async (groupId: string) => { + try { + const res = await fetch(`${API_URL}/groups/${groupId}`, { + credentials: "include", + }); + if (!res.ok) return null; + return (await res.json()) as GameGroup; + } catch (error) { + console.error(error); + return null; + } +}; + +const joinGroup = async (inviteCode: string) => { + try { + const res = await fetch(`${API_URL}/groups/join/${inviteCode}`, { method: "PUT", + credentials: "include", + }); + if (!res.ok) return null; + return (await res.json()) as GameGroup; + } catch (error) { + console.error(error); + return null; + } +}; + +const deleteGroup = async (groupId: string) => { + // É tratado como delete, mas é apenas um soft + try { + const res = await fetch(`${API_URL}/groups/${groupId}`, { + method: "DELETE", + credentials: "include", + }); + return res.ok; + } catch (error) { + console.error(error); + return false; + } +}; + +const getGroupPlayers = async (groupId: string) => { + try { + const res = await fetch(`${API_URL}/groups/${groupId}/players`, { + credentials: "include", + }); + if (!res.ok) return []; + return (await res.json()) as Player[]; + } catch (error) { + console.error(error); + return []; + } +}; + +const addGroupPlayer = async (groupId: string, player: Player) => { + try { + const res = await fetch(`${API_URL}/groups/${groupId}/players`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(player), + credentials: "include", }); return res.ok; + } catch (error) { + console.error(error); + return false; + } +}; + +const getGroupGameDays = async (groupId: string) => { + try { + const res = await fetch(`${API_URL}/groups/${groupId}/game-days`, { + credentials: "include", + }); + if (!res.ok) return []; + return (await res.json()) as GameDay[]; + } catch (error) { + console.error(error); + return []; + } +}; + +type CreateGroupGameDayParams = { + maxPoints: number; + playersPerTeam: number; + autoSwitchTeamsPoints: number; + isLive: boolean; + playedOn: Date; + players: (Player & Partial)[]; + playingTeams: GameDayPlayer[][]; +}; + +const createGroupGameDay = async (groupId: string, gameDay: CreateGroupGameDayParams) => { + try { + const res = await fetch(`${API_URL}/groups/${groupId}/game-days`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(gameDay), + credentials: "include", + }); + if (!res.ok) return null; + return (await res.json()) as { id: string; courtId: string; joinCode: string }; } catch (error) { console.error(error); return null; } }; -const updatePlayers = async (players: Player[]) => { +const putPlayer = async (player: Player) => { try { - const res = await fetch(`${API_URL}/players/bulk`, { - method: "PUT", + const res = await fetch(`${API_URL}/players/`, { + body: JSON.stringify(player), headers: { "Content-Type": "application/json", }, - body: JSON.stringify(players), credentials: "include", + method: "PUT", }); return res.ok; } catch (error) { @@ -39,16 +165,20 @@ const updatePlayers = async (players: Player[]) => { } }; -const getPlayers = async (params: URLSearchParams) => { +const updatePlayers = async (players: Player[]) => { try { - const res = await fetch(`${API_URL}/players?${params.toString()}`, { + const res = await fetch(`${API_URL}/players/bulk`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(players), credentials: "include", }); - const players = (await res.json()) as Player[]; - return players; + return res.ok; } catch (error) { console.error(error); - return []; + return null; } }; @@ -204,9 +334,22 @@ async function restartGameDay(gameId: string) { } export const api = { - getPlayers, + // Groups + createGroup, + getGroups, + getGroup, + joinGroup, + deleteGroup, + // Group Players + getGroupPlayers, + addGroupPlayer, + // Group Game Days + getGroupGameDays, + createGroupGameDay, + // Players updatePlayers, putPlayer, + // Game Days createGameDay, getActiveGameDay, updateGameDay, diff --git a/src/hooks/use-group-game-days.ts b/src/hooks/use-group-game-days.ts new file mode 100644 index 0000000..3d5118f --- /dev/null +++ b/src/hooks/use-group-game-days.ts @@ -0,0 +1,9 @@ +import useSWR from "swr"; +import { api } from "../api"; + +export const useGroupGameDays = (groupId: string | null) => { + return useSWR( + groupId ? `/groups/${groupId}/game-days` : null, + () => groupId ? api.getGroupGameDays(groupId) : Promise.resolve([]) + ); +}; diff --git a/src/hooks/use-group-players.ts b/src/hooks/use-group-players.ts new file mode 100644 index 0000000..a346723 --- /dev/null +++ b/src/hooks/use-group-players.ts @@ -0,0 +1,9 @@ +import useSWR from "swr"; +import { api } from "../api"; + +export const useGroupPlayers = (groupId: string | null) => { + return useSWR( + groupId ? `/groups/${groupId}/players` : null, + () => (groupId ? api.getGroupPlayers(groupId) : Promise.resolve([])) + ); +}; diff --git a/src/hooks/use-group.ts b/src/hooks/use-group.ts new file mode 100644 index 0000000..642e84c --- /dev/null +++ b/src/hooks/use-group.ts @@ -0,0 +1,9 @@ +import useSWR from "swr"; +import { api } from "../api"; + +export const useGroup = (groupId: string | null) => { + return useSWR( + groupId ? `/groups/${groupId}` : null, + () => (groupId ? api.getGroup(groupId) : Promise.resolve(null)) + ); +}; diff --git a/src/hooks/use-groups.ts b/src/hooks/use-groups.ts new file mode 100644 index 0000000..414d88e --- /dev/null +++ b/src/hooks/use-groups.ts @@ -0,0 +1,4 @@ +import useSWR from "swr"; +import { api } from "../api"; + +export const useGroups = () => useSWR("/groups", api.getGroups); diff --git a/src/hooks/use-players.ts b/src/hooks/use-players.ts deleted file mode 100644 index ac8df6e..0000000 --- a/src/hooks/use-players.ts +++ /dev/null @@ -1,15 +0,0 @@ -import useSWR from "swr"; -import { api } from "../api"; - -export const usePlayers = (filters: { - names?: string[]; -} = {}) => { - const params = new URLSearchParams(); - if(filters.names) { - params.append("names", filters.names.join(",")); - } - return useSWR( - `/players?${params.toString()}`, - () => api.getPlayers(params) - ); -}; diff --git a/src/types.ts b/src/types.ts index 4bc7af2..18c3bab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export type PlayerRating = { export type GameDay = { id: string; + groupId?: string; courtId: string; maxPoints: number; playersPerTeam: number; @@ -36,3 +37,12 @@ export type GameDay = { lastMatch: number; playingTeams: GameDayPlayer[][]; }; + +export type GameGroup = { + id: string; + name: string; + createdAt: string; + inviteCode: string; + inviteCodeExpiration: string; + players: GameDayPlayer[]; +}; From 5a5a0216df6865facb11428e54e3ac5780d02854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Fran=C3=A7a?= Date: Wed, 28 Jan 2026 00:15:12 -0300 Subject: [PATCH 2/4] feat: add overall game group pages --- src/app.tsx | 13 + src/components/court-form-group.tsx | 235 ++++++++++++++++++ src/components/court-form.tsx | 21 +- src/pages/game-day/game-day.tsx | 8 +- .../create-game-day/create-group-game-day.tsx | 87 +++++++ src/pages/groups/create-group.tsx | 97 ++++++++ src/pages/groups/group-detail.tsx | 159 ++++++++++++ src/pages/groups/groups.tsx | 146 +++++++++++ src/pages/groups/history/group-history.tsx | 53 ++++ src/pages/history/history-match.tsx | 29 ++- src/pages/home/home.tsx | 21 +- 11 files changed, 846 insertions(+), 23 deletions(-) create mode 100644 src/components/court-form-group.tsx create mode 100644 src/pages/groups/create-game-day/create-group-game-day.tsx create mode 100644 src/pages/groups/create-group.tsx create mode 100644 src/pages/groups/group-detail.tsx create mode 100644 src/pages/groups/groups.tsx create mode 100644 src/pages/groups/history/group-history.tsx diff --git a/src/app.tsx b/src/app.tsx index 48b491c..9b5106a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -6,6 +6,11 @@ import History from "./pages/history/history"; import Layout from "./pages/layout"; import HistoryMatch from "./pages/history/history-match"; import EditGameDay from "./pages/game-day/edit/edit-game-day"; +import Groups from "./pages/groups/groups"; +import CreateGroup from "./pages/groups/create-group"; +import GroupDetail from "./pages/groups/group-detail"; +import CreateGroupGameDay from "./pages/groups/create-game-day/create-group-game-day"; +import GroupHistory from "./pages/groups/history/group-history"; function App() { return ( @@ -20,6 +25,14 @@ function App() { } /> } /> + + } /> + } /> + } /> + } /> + } /> + } /> + ); diff --git a/src/components/court-form-group.tsx b/src/components/court-form-group.tsx new file mode 100644 index 0000000..9e6879a --- /dev/null +++ b/src/components/court-form-group.tsx @@ -0,0 +1,235 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { api } from "../api"; +import { useGroupPlayers } from "../hooks/use-group-players"; +import { createInitialRating } from "../lib/elo"; +import BackButton from "./back-button"; +import Input from "./input"; +import Select from "./select"; + +const schema = z.object({ + playersPerTeam: z + .number({ coerce: true, message: "Informe um número maior que 0" }) + .min(1, { message: "Informe um número maior que 0" }), + maxPoints: z + .number({ coerce: true, message: "Informe um número maior que 0" }) + .min(1, { message: "Informe um número maior que 0" }), + autoSwitchTeams: z.boolean().optional(), + autoSwitchTeamsPoints: z.number({ coerce: true }).optional(), + players: z.array( + z.object({ + value: z.object({ name: z.string(), mu: z.number(), sigma: z.number() }), + label: z.string(), + __isNew__: z.boolean().optional(), + isFixed: z.boolean().optional(), + }), + { + message: "Informe pelo menos um jogador", + } + ), +}); + +export type CourtFormGroupData = z.infer; + +type Props = { + groupId: string; + onSubmit: (data: CourtFormGroupData) => Promise; + submitButton: (isSubmitting: boolean) => React.ReactNode; + initialValues?: CourtFormGroupData; +}; + +const CourtFormGroup = ({ groupId, onSubmit, submitButton, initialValues }: Props) => { + const { data: groupPlayers = [], mutate: refreshPlayers } = useGroupPlayers(groupId); + const navigate = useNavigate(); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: initialValues, + }); + + const handleSubmit = form.handleSubmit(async (data) => { + try { + if (data.autoSwitchTeams && !data.autoSwitchTeamsPoints) { + form.setError("autoSwitchTeamsPoints", { + type: "manual", + message: "Informe o número de pontos para trocar de time", + }); + return; + } + + if ( + data.autoSwitchTeamsPoints && + data.autoSwitchTeamsPoints >= data.maxPoints + ) { + form.setError("autoSwitchTeamsPoints", { + type: "manual", + message: + "O número de pontos para trocar de time deve ser menor que o máximo de pontos", + }); + return; + } + + if (data.playersPerTeam * 2 > data.players.length) { + form.setError("players", { + type: "manual", + message: + "O número de jogadores é menor que o número de jogadores por time", + }); + return; + } + + const gameDay = await onSubmit(data); + + if (!gameDay) { + form.setError("root", { + type: "manual", + message: "Erro ao criar jogo", + }); + return; + } + + navigate("/pelada"); + } catch (e) { + console.error(e); + } + }); + + const [isCreatingPlayer, setIsCreatingPlayer] = useState(false); + const options = + groupPlayers + ?.map((player) => ({ + value: player, + label: player.name, + })) + .sort((a, b) => a.label.localeCompare(b.label)) ?? []; + const selectedPlayers = form.watch("players"); + + const onCreateOption = async (name: string) => { + try { + form.clearErrors("players"); + setIsCreatingPlayer(true); + const newPlayer = { name, ...createInitialRating() }; + const ok = await api.addGroupPlayer(groupId, newPlayer); + if (!ok) { + form.setError("players", { + type: "manual", + message: `Erro ao criar jogador ${name}`, + }); + return; + } + await refreshPlayers(); + form.setValue("players", [ + ...(selectedPlayers ?? []), + { value: newPlayer, label: newPlayer.name }, + ]); + } finally { + setIsCreatingPlayer(false); + } + }; + + const autoSwitchTeams = form.watch("autoSwitchTeams"); + + return ( +
+ +
+
+ + + {form.formState.errors.playersPerTeam && ( + + {form.formState.errors.playersPerTeam.message} + + )} +
+
+ + + {form.formState.errors.maxPoints && ( + + {form.formState.errors.maxPoints.message} + + )} +
+
+
+
+ + + {form.formState.errors.autoSwitchTeams && ( + + {form.formState.errors.autoSwitchTeams.message} + + )} +
+
+ + + {form.formState.errors.autoSwitchTeamsPoints && ( + + {form.formState.errors.autoSwitchTeamsPoints.message} + + )} +
+
+
+
+ + ( + + {form.formState.errors.name && ( +

+ {form.formState.errors.name.message} +

+ )} +
+ + {form.formState.errors.root && ( +

+ {form.formState.errors.root.message} +

+ )} + + + +
+ + ); +}; + +export default CreateGroup; diff --git a/src/pages/groups/group-detail.tsx b/src/pages/groups/group-detail.tsx new file mode 100644 index 0000000..f4ea105 --- /dev/null +++ b/src/pages/groups/group-detail.tsx @@ -0,0 +1,159 @@ +import { useEffect } from "react"; +import { Link, useNavigate, useParams } from "react-router"; +import { + FaPlus, + FaUsers, + FaCopy, + FaClock, + FaRightToBracket, + FaTrash, +} from "react-icons/fa6"; +import { VscLoading } from "react-icons/vsc"; +import { useGroup } from "../../hooks/use-group"; +import { useGroupPlayers } from "../../hooks/use-group-players"; +import { useGroupGameDays } from "../../hooks/use-group-game-days"; +import { api } from "../../api"; +import { buttonClasses } from "../../components/button"; +import BackButton from "../../components/back-button"; + +const GroupDetail = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { data: group, isLoading } = useGroup(id || null); + const { data: players = [] } = useGroupPlayers(id || null); + const gameDays = useGroupGameDays(id || null); + + // Redirect if group not found + useEffect(() => { + if (!isLoading && !group && id) { + navigate("/grupos"); + } + }, [isLoading, group, id, navigate]); + + const copyInviteCode = () => { + if (!group) return; + navigator.clipboard.writeText(group.inviteCode); + alert("Código copiado: " + group.inviteCode); + }; + + const handleDeleteGroup = async () => { + if (!group) return; + + const confirmed = window.confirm( + "⚠️ ATENÇÃO: Isso irá apagar o grupo e todos os dados associados. Deseja continuar?" + ); + if (confirmed) { + const success = await api.deleteGroup(group.id); + if (success) { + navigate("/grupos"); + } else { + alert("Erro ao apagar o grupo. Tente novamente."); + } + } + }; + + if (isLoading || !group) { + return ( +
+ +
+ ); + } + + return ( + <> + +
+
+

+ {group.name} +

+ +
+
+ + + Criar nova pelada + + + + Histórico + +
+
+ + + Transferir/Entrar em Pelada Existente + +
+
+
+

+ Jogadores ({players.length}) +

+
+ {players.length > 0 ? ( +
    + {players + .sort((a, b) => a.name.localeCompare(b.name)) + .map((player) => ( +
  • + {player.name} +
  • + ))} +
+ ) : ( +

+ Nenhum jogador cadastrado. Crie uma pelada para adicionar jogadores! +

+ )} +
+ {gameDays.data && gameDays.data.length > 0 && ( +
+

Peladas Recentes

+
    + {gameDays.data.slice(0, 5).map((gameDay) => ( +
  • + + {new Date(gameDay.playedOn).toLocaleString()} + + {gameDay.players.length} jogadores + + +
  • + ))} +
+
+ )} + +
+ + ); +}; + +export default GroupDetail; diff --git a/src/pages/groups/groups.tsx b/src/pages/groups/groups.tsx new file mode 100644 index 0000000..64e2feb --- /dev/null +++ b/src/pages/groups/groups.tsx @@ -0,0 +1,146 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router"; +import { FaPlus, FaUsers, FaRightToBracket, FaCopy, FaArrowRight } from "react-icons/fa6"; +import { VscLoading } from "react-icons/vsc"; +import { useGroups } from "../../hooks/use-groups"; +import { api } from "../../api"; +import Button, { buttonClasses } from "../../components/button"; +import Input from "../../components/input"; +import BackButton from "../../components/back-button"; +import { GameGroup } from "../../types"; + +const Groups = () => { + const groups = useGroups(); + const navigate = useNavigate(); + const [inviteCode, setInviteCode] = useState(""); + const [isJoining, setIsJoining] = useState(false); + const [joinError, setJoinError] = useState(null); + + const handleJoinGroup = async (e: React.FormEvent) => { + e.preventDefault(); + if (!inviteCode.trim()) return; + + try { + setIsJoining(true); + setJoinError(null); + const group = await api.joinGroup(inviteCode.toUpperCase()); + if (!group) { + setJoinError("Código inválido ou expirado"); + return; + } + await groups.mutate(); + navigate("/grupos/" + group.id); + } catch { + setJoinError("Erro ao entrar no grupo"); + } finally { + setIsJoining(false); + } + }; + + const handleSelectGroup = (group: GameGroup) => { + navigate(`/grupos/${group.id}`); + }; + + const copyInviteCode = (code: string, e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(code); + alert("Código copiado!"); + }; + + if (groups.error) return
Erro ao carregar grupos
; + + if (groups.isLoading || !groups.data) { + return ( +
+ +
+ ); + } + + return ( + <> + +
+ +

+ Meus Grupos de Pelada +

+ + + Criar novo grupo + +
+
+ +
+ { + setInviteCode(e.target.value.toUpperCase()); + setJoinError(null); + }} + placeholder="Digite o código" + /> + +
+
+ {joinError &&

{joinError}

} +
+ {groups.data.length > 0 ? ( +
+

Selecione um grupo:

+
    + {groups.data.map((group) => ( +
  • + + +
+ + + ))} + +
+ ) : ( +

+ Você ainda não tem nenhum grupo. Crie um ou entre com um código de + convite! +

+ )} + + + ); +}; + +export default Groups; diff --git a/src/pages/groups/history/group-history.tsx b/src/pages/groups/history/group-history.tsx new file mode 100644 index 0000000..b4ece61 --- /dev/null +++ b/src/pages/groups/history/group-history.tsx @@ -0,0 +1,53 @@ +import { FaClock } from "react-icons/fa"; +import { VscLoading } from "react-icons/vsc"; +import { Link, useParams } from "react-router"; +import BackButton from "../../../components/back-button"; +import { useGroupGameDays } from "../../../hooks/use-group-game-days"; + +const GroupHistory = () => { + const { id: groupId } = useParams<{ id: string }>(); + const gameDays = useGroupGameDays(groupId || null); + + if (gameDays.error) return
Erro ao carregar
; + + if (gameDays.isLoading || !gameDays.data) + return ( +
+ +
+ ); + + return ( + <> + +

+ Histórico de Partidas +

+ {gameDays.data.length === 0 ? ( +

+ Nenhuma pelada registrada ainda. +

+ ) : ( +
    + {gameDays.data.map((gameDay) => { + return ( +
  • + + {new Date(gameDay.playedOn).toLocaleString()} + +
  • + ); + })} +
+ )} + + ); +}; + +export default GroupHistory; diff --git a/src/pages/history/history-match.tsx b/src/pages/history/history-match.tsx index 71f4abe..bf3b0c2 100644 --- a/src/pages/history/history-match.tsx +++ b/src/pages/history/history-match.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useGameDays } from '../../hooks/use-game-days' +import { useGroupGameDays } from '../../hooks/use-group-game-days' import { VscLoading } from 'react-icons/vsc'; import { FaRedo } from 'react-icons/fa'; -import { useParams, useSearchParams, useNavigate } from 'react-router'; +import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router'; import PlayersTable from '../../components/players-table'; import BackButton from '../../components/back-button'; import Button from '../../components/button'; @@ -10,8 +11,19 @@ import { api } from '../../api'; const HistoryMatch = () => { - const gameDays = useGameDays(); - const params = useParams(); + // Support both legacy routes (/historico/:id) and group routes (/grupos/:id/historico/:gameDayId) + const params = useParams<{ id?: string; gameDayId?: string }>(); + const location = useLocation(); + const isGroupRoute = location.pathname.includes('/grupos/'); + + // Extract groupId and gameDayId based on route + const groupId = isGroupRoute ? params.id : null; + const gameDayId = isGroupRoute ? params.gameDayId : params.id; + + const legacyGameDays = useGameDays(); + const groupGameDays = useGroupGameDays(groupId ?? null); + + const gameDays = isGroupRoute ? groupGameDays : legacyGameDays; const [searchParams] = useSearchParams() const navigate = useNavigate(); const [isRestarting, setIsRestarting] = useState(false); @@ -22,7 +34,7 @@ const HistoryMatch = () => { } - const gameDay = gameDays.data?.find(gameDay => gameDay.id === params.id); + const gameDay = gameDays.data?.find(gd => gd.id === gameDayId); if(!gameDay) { return
{ } }; + // Determine back navigation path + const getBackPath = () => { + if (searchParams.get('origin') === 'game-day') return '/'; + if (isGroupRoute && groupId) return `/grupos/${groupId}/historico`; + return '/historico'; + }; + return ( <> - + { return (
-
- - - Criar nova pelada - + + + Meus Grupos de Pelada + +
+

+ Ou crie uma pelada avulsa: +

+
+ + + Criar nova pelada + +
Date: Wed, 28 Jan 2026 00:48:40 -0300 Subject: [PATCH 3/4] fix: keep current gameday players within selection until update is actually complete --- src/components/court-form.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/court-form.tsx b/src/components/court-form.tsx index ca952aa..2816af4 100644 --- a/src/components/court-form.tsx +++ b/src/components/court-form.tsx @@ -42,7 +42,9 @@ type Props = { const CourtForm = ({ onSubmit, submitButton, initialValues }: Props) => { const navigate = useNavigate(); - const [createdPlayers, setCreatedPlayers] = useState([]); + const [createdPlayers, setCreatedPlayers] = useState(() => + initialValues?.players?.map(p => p.value) ?? [] + ); const form = useForm({ resolver: zodResolver(schema), defaultValues: initialValues, From 55b64d8a325174bd9c0a5b94b8b64c0752b06cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Fran=C3=A7a?= Date: Wed, 28 Jan 2026 01:18:27 -0300 Subject: [PATCH 4/4] fix: deal with use case where it was possible to remove a player currently playing --- src/pages/game-day/edit/edit-game-day.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pages/game-day/edit/edit-game-day.tsx b/src/pages/game-day/edit/edit-game-day.tsx index 165fb15..898d72b 100644 --- a/src/pages/game-day/edit/edit-game-day.tsx +++ b/src/pages/game-day/edit/edit-game-day.tsx @@ -28,6 +28,20 @@ const EditGameDay = () => { const onSubmit = async (data: CourtFormData) => { if(!gameDay.data) return false; + const newPlayerNames = new Set(data.players.map(p => p.value.name)); + const removedPlayingPlayers = gameDay.data.playingTeams + .flat() + .filter(p => !newPlayerNames.has(p.name)) + .map(p => p.name); + + if (removedPlayingPlayers.length > 0) { + alert( + `Não é possível remover os seguintes jogadores que estão na partida atual: ${removedPlayingPlayers.join(", ")}.\n` + + `Adicione um novo jogador e utilize a função de substituição na tela principal.` + ); + return false; + } + const players = data.players .map((player) => player.value) .map((player, index) => {