"use client"; import { EventTypes } from "@/_utils/types"; import Image from "next/image"; import { useEffect, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import LoadingIndicator from "@/_components/LoadingIndicator"; import type { PresenceItem, RoomResponse, VoteResponse } from "@/_utils/types"; import { useUser } from "@clerk/nextjs"; import { useChannel, usePresence } from "ably/react"; import { isAdmin, isVIP, jsonToCsv } from "app/_utils/helpers"; import { env } from "env.mjs"; import { useParams } from "next/navigation"; import { FaShieldAlt } from "react-icons/fa"; import { GiStarFormation } from "react-icons/gi"; import { IoCheckmarkCircleOutline, IoCopyOutline, IoDownloadOutline, IoEyeOffOutline, IoEyeOutline, IoHourglassOutline, IoReloadOutline, IoSaveOutline, } from "react-icons/io5"; import { RiVipCrownFill } from "react-icons/ri"; import NoRoomUI from "./NoRoomUI"; const VoteUI = () => { // State // ================================= const params = useParams(); const roomId = params?.id as string; const { user } = useUser(); const [storyNameText, setStoryNameText] = useState(""); const [roomScale, setRoomScale] = useState(""); const [copied, setCopied] = useState(false); const queryClient = useQueryClient(); const { data: roomFromDb, isLoading: roomFromDbLoading } = useQuery({ queryKey: ["room"], queryFn: getRoomHandler, retry: false, }); const { data: votesFromDb } = useQuery({ queryKey: ["votes"], queryFn: getVotesHandler, }); const { mutate: setVote } = useMutation({ mutationFn: setVoteHandler, // When mutate is called: onMutate: async (newVote) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ["votes"] }); // Snapshot the previous value const previousVotes = queryClient.getQueryData(["votes"]); // Optimistically update to the new value queryClient.setQueryData(["votes"], (old) => old?.map((vote) => { if (vote.userId === user?.id) { return { ...vote, value: newVote, }; } else { return vote; } }) ); // Return a context object with the snapshotted value return { previousVotes }; }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newVote, context) => { queryClient.setQueryData(["votes"], context?.previousVotes); }, // Always refetch after error or success: onSettled: () => { void queryClient.invalidateQueries({ queryKey: ["votes"] }); }, }); const { mutate: setRoom } = useMutation({ mutationFn: setRoomHandler, // When mutate is called: onMutate: async (data: { visible: boolean; reset: boolean; log: boolean; }) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ["room"] }); // Snapshot the previous value const previousRoom = queryClient.getQueryData(["room"]); // Optimistically update to the new value queryClient.setQueryData(["room"], (old) => { return old?.created_at || old?.id || old?.userId ? { roomName: old?.roomName, created_at: old?.created_at, id: old?.id, userId: old?.userId, logs: old?.logs, storyName: data.reset ? storyNameText : old.storyName, visible: data.visible, scale: data.reset ? roomScale : old.scale, reset: data.reset, log: data.log, } : old; }); // Return a context object with the snapshotted value return { previousRoom }; }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newRoom, context) => { queryClient.setQueryData(["room"], context?.previousRoom); }, // Always refetch after error or success: onSettled: () => { void queryClient.invalidateQueries({ queryKey: ["room"] }); }, }); // Handlers // ================================= async function getRoomHandler() { const response = await fetch(`/api/internal/room/${roomId}`, { cache: "no-cache", method: "GET", }); return (await response.json()) as RoomResponse; } async function getVotesHandler() { const dbVotesResponse = await fetch(`/api/internal/room/${roomId}/votes`, { cache: "no-cache", method: "GET", }); const dbVotes = (await dbVotesResponse.json()) as VoteResponse; return dbVotes; } async function setVoteHandler(value: string) { if (roomFromDb) { await fetch(`/api/internal/room/${roomId}/vote`, { cache: "no-cache", method: "PUT", body: JSON.stringify({ value, }), }); } } async function setRoomHandler(data: { visible: boolean; reset: boolean | undefined; log: boolean | undefined; }) { console.log({ visible: data.visible, reset: data.reset ? data.reset : false, log: data.log ? data.log : false, }); if (roomFromDb) { await fetch(`/api/internal/room/${roomId}`, { cache: "no-cache", method: "PUT", body: JSON.stringify({ name: storyNameText, visible: data.visible, scale: roomScale, reset: data.reset ? data.reset : false, log: data.log ? data.log : false, }), }); } } // Helpers // ================================= const getVoteForCurrentUser = () => { if (roomFromDb) { return ( votesFromDb && votesFromDb.find((vote) => vote.userId === user?.id) ); } else { return null; } }; const downloadLogs = () => { if (roomFromDb && votesFromDb) { const jsonObject = roomFromDb?.logs .map((item) => { return { id: item.id, created_at: item.created_at, userId: item.userId, roomId: item.roomId, roomName: item.roomName, topicName: item.storyName, scale: item.scale, votes: item.votes, }; }) .concat({ id: "LATEST", created_at: new Date(), userId: roomFromDb.userId, roomId: roomFromDb.id, roomName: roomFromDb.roomName, topicName: storyNameText, scale: roomScale, votes: votesFromDb.map((vote) => { return { value: vote.value, }; }), }); jsonToCsv(jsonObject, `sp_${roomId}.csv`); } }; const copyRoomURLHandler = () => { navigator.clipboard .writeText(window.location.href) .then(() => { console.log(`Copied Room Link to Clipboard!`); setCopied(true); setTimeout(() => { setCopied(false); }, 2000); }) .catch(() => { console.log(`Error Copying Room Link to Clipboard!`); }); }; const voteString = ( visible: boolean, votes: typeof votesFromDb, presenceItem: PresenceItem ) => { const matchedVote = votes?.find( (vote) => vote.userId === presenceItem.client_id ); if (visible) { if (!!matchedVote) { return
{matchedVote.value}
; } else { return ; } } else if (!!matchedVote) { return ; } else { return ( ); } }; // Hooks // ================================= useChannel( { channelName: `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, }, ({ name }: { name: string }) => { if (name === EventTypes.ROOM_UPDATE) { void queryClient.invalidateQueries({ queryKey: ["votes"] }); void queryClient.invalidateQueries({ queryKey: ["room"] }); } else if (name === EventTypes.VOTE_UPDATE) { void queryClient.invalidateQueries({ queryKey: ["votes"] }); } } ); const { presenceData } = usePresence( `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, { name: (user?.fullName ?? user?.username) || "", image: user?.imageUrl || "", client_id: user?.id || "unknown", isAdmin: isAdmin(user?.publicMetadata), isVIP: isVIP(user?.publicMetadata), } ); useEffect(() => { if (roomFromDb) { setStoryNameText(roomFromDb.storyName || ""); setRoomScale(roomFromDb.scale || "ERROR"); } }, [roomFromDb]); // UI // ================================= // Room is loading if (roomFromDbLoading) { return ; // Room has been loaded } else { return roomFromDb ? (
{roomFromDb.roomName}
ID:
{roomFromDb.id}
{roomFromDb && (

Topic: {roomFromDb.storyName}

    {presenceData && presenceData .filter( (value, index, self) => index === self.findIndex( (presenceItem) => presenceItem.clientId === value.clientId ) ) .map((presenceItem) => { return (
  • {`${presenceItem.data.name}'s

    {presenceItem.data.name}{" "} {presenceItem.data.isAdmin && ( )}{" "} {presenceItem.data.isVIP && ( )}{" "} {presenceItem.clientId === roomFromDb.userId && ( )} {" : "}

    {roomFromDb && votesFromDb && voteString( roomFromDb.visible, votesFromDb, presenceItem.data )}
  • ); })}
{roomFromDb.scale?.split(",").map((scaleItem, index) => { return ( ); })}
)} {!!roomFromDb && (roomFromDb.userId === user?.id || isAdmin(user?.publicMetadata)) && ( <>

Room Settings

{ setRoomScale(event.target.value); }} /> { setStoryNameText(event.target.value); }} />
{votesFromDb && (roomFromDb.logs.length > 0 || votesFromDb.length > 0) && (
)}
)}
) : ( ); } }; export default VoteUI;