106 lines
4.8 KiB
TypeScript
106 lines
4.8 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|||
|
|
import { io, type Socket } from "socket.io-client";
|
||
|
|
import type { BoardState } from "./types";
|
||
|
|
import { getModeratorToken, setModeratorToken } from "./identity";
|
||
|
|
|
||
|
|
export type BoardStatus = "connecting" | "ready" | "notfound" | "destroyed";
|
||
|
|
|
||
|
|
export function useBoard(boardId: string, participantId: string, name: string | null) {
|
||
|
|
const [state, setState] = useState<BoardState | null>(null);
|
||
|
|
const [status, setStatus] = useState<BoardStatus>("connecting");
|
||
|
|
const socketRef = useRef<Socket | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!name) return;
|
||
|
|
const socket = io({ path: "/socket.io" });
|
||
|
|
socketRef.current = socket;
|
||
|
|
|
||
|
|
socket.on("connect", () =>
|
||
|
|
socket.emit("board:join", { boardId, participantId, name, moderatorToken: getModeratorToken(boardId) }),
|
||
|
|
);
|
||
|
|
socket.on("board:state", (s: BoardState) => {
|
||
|
|
setState(s);
|
||
|
|
setStatus("ready");
|
||
|
|
});
|
||
|
|
socket.on("board:claimed", (p: { moderatorToken: string }) => {
|
||
|
|
setModeratorToken(boardId, p.moderatorToken);
|
||
|
|
});
|
||
|
|
socket.on("error", (e: { message: string }) => {
|
||
|
|
if (e.message === "board_not_found") setStatus("notfound");
|
||
|
|
});
|
||
|
|
socket.on("board:destroyed", () => setStatus("destroyed"));
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
socket.disconnect();
|
||
|
|
socketRef.current = null;
|
||
|
|
};
|
||
|
|
}, [boardId, participantId, name]);
|
||
|
|
|
||
|
|
const mod = () => getModeratorToken(boardId);
|
||
|
|
const emit = (event: string, payload: Record<string, unknown> = {}) =>
|
||
|
|
socketRef.current?.emit(event, payload);
|
||
|
|
const modEmit = (event: string, payload: Record<string, unknown> = {}) =>
|
||
|
|
emit(event, { ...payload, moderatorToken: mod() });
|
||
|
|
|
||
|
|
return {
|
||
|
|
state,
|
||
|
|
status,
|
||
|
|
isModerator: state?.youAreModerator ?? false,
|
||
|
|
actions: {
|
||
|
|
// notas
|
||
|
|
addNote: (columnId: string, text: string, color?: string) =>
|
||
|
|
emit("note:add", { columnId, text, color }),
|
||
|
|
updateNote: (noteId: string, text: string) => emit("note:update", { noteId, text }),
|
||
|
|
deleteNote: (noteId: string) => emit("note:delete", { noteId }),
|
||
|
|
revealNote: (noteId: string, revealed: boolean) => emit("note:reveal", { noteId, revealed }),
|
||
|
|
publishMine: (columnId: string) => emit("note:publishMine", { columnId }),
|
||
|
|
recolorMine: (color: string) => emit("note:recolorMine", { color }),
|
||
|
|
moveNote: (noteId: string, columnId: string, groupId: string | null, position: number) =>
|
||
|
|
emit("note:move", { noteId, columnId, groupId, position }),
|
||
|
|
// grupos
|
||
|
|
createGroup: (noteId: string, targetNoteId: string) =>
|
||
|
|
emit("group:create", { noteId, targetNoteId }),
|
||
|
|
renameGroup: (groupId: string, title: string) => emit("group:rename", { groupId, title }),
|
||
|
|
moveGroup: (groupId: string, columnId: string, position: number) =>
|
||
|
|
emit("group:move", { groupId, columnId, position }),
|
||
|
|
deleteGroup: (groupId: string) => emit("group:delete", { groupId }),
|
||
|
|
// votos
|
||
|
|
addVote: (targetType: "note" | "group", targetId: string) =>
|
||
|
|
emit("vote:add", { targetType, targetId }),
|
||
|
|
removeVote: (targetId: string) => emit("vote:remove", { targetId }),
|
||
|
|
// ajustes y moderación
|
||
|
|
updateBoard: (patch: {
|
||
|
|
title?: string;
|
||
|
|
votesPerUser?: number;
|
||
|
|
allowMultiVote?: boolean;
|
||
|
|
moderationMode?: "everyone" | "single";
|
||
|
|
showCardCreators?: boolean;
|
||
|
|
locked?: boolean;
|
||
|
|
}) => modEmit("board:update", patch),
|
||
|
|
reveal: (revealed: boolean) => modEmit("board:reveal", { revealed }),
|
||
|
|
revealVotes: (revealed: boolean) => modEmit("board:revealVotes", { revealed }),
|
||
|
|
setPhase: (phase: string) => modEmit("board:phase", { phase }),
|
||
|
|
// columnas (moderador)
|
||
|
|
addColumn: (title: string) => modEmit("column:add", { title }),
|
||
|
|
renameColumn: (columnId: string, title: string) => modEmit("column:rename", { columnId, title }),
|
||
|
|
deleteColumn: (columnId: string) => modEmit("column:delete", { columnId }),
|
||
|
|
reorderColumns: (orderedIds: string[]) => modEmit("column:reorder", { orderedIds }),
|
||
|
|
startTimer: (seconds: number) => modEmit("timer:start", { seconds }),
|
||
|
|
stopTimer: () => modEmit("timer:stop"),
|
||
|
|
claim: () => modEmit("board:claim"),
|
||
|
|
clearVotes: () => modEmit("votes:clear"),
|
||
|
|
removeBoard: () => modEmit("board:remove"),
|
||
|
|
// participante
|
||
|
|
renameMe: (name: string) => emit("participant:rename", { name }),
|
||
|
|
// action items
|
||
|
|
addAction: (text: string) => emit("actionitem:add", { text }),
|
||
|
|
updateAction: (id: string, patch: { text?: string; owner?: string | null }) =>
|
||
|
|
emit("actionitem:update", { id, ...patch }),
|
||
|
|
toggleAction: (id: string, done: boolean) => emit("actionitem:toggle", { id, done }),
|
||
|
|
deleteAction: (id: string) => emit("actionitem:delete", { id }),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export type BoardActions = ReturnType<typeof useBoard>["actions"];
|