Sternboard: free retro board, no signup (initial)
Servidor Fastify + Socket.IO + Drizzle/Postgres; front Vite+React (EN/ES, tema claro/oscuro). Tableros por columnas con plantillas, notas privadas/públicas, agrupación, votos configurables (multivoto), timer compartido con alarma, fases, action items, export Markdown/JSON e import, autodestrucción, gestión de columnas. Imagen única (front servido por el servidor) y CI que la construye y publica.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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"];
|
||||
Reference in New Issue
Block a user