pollo/app/(client)/room/[id]/VoteUI.tsx

550 lines
18 KiB
TypeScript
Raw Normal View History

2023-09-01 19:43:15 -06:00
"use client";
2023-09-27 12:46:15 -06:00
import { EventTypes } from "@/_utils/types";
2023-09-01 19:43:15 -06:00
import Image from "next/image";
import { useEffect, useState } from "react";
2023-10-11 00:58:00 -06:00
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2023-10-08 13:21:30 -06:00
2023-09-27 12:46:15 -06:00
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";
2023-09-01 19:43:15 -06:00
import { useParams } from "next/navigation";
2023-09-27 12:46:15 -06:00
import { FaShieldAlt } from "react-icons/fa";
import { GiStarFormation } from "react-icons/gi";
2023-09-01 19:43:15 -06:00
import {
2023-10-04 14:45:35 -06:00
IoCheckmarkCircleOutline,
IoCopyOutline,
IoDownloadOutline,
IoEyeOffOutline,
IoEyeOutline,
IoHourglassOutline,
IoReloadOutline,
IoSaveOutline,
2023-09-01 19:43:15 -06:00
} from "react-icons/io5";
import { RiVipCrownFill } from "react-icons/ri";
2023-09-24 23:49:24 -06:00
import NoRoomUI from "./NoRoomUI";
2023-09-06 12:57:13 -06:00
2023-09-06 12:53:48 -06:00
const VoteUI = () => {
2023-10-11 00:58:00 -06:00
// State
// =================================
2023-09-01 19:43:15 -06:00
const params = useParams();
const roomId = params?.id as string;
2023-09-06 13:00:01 -06:00
const { user } = useUser();
2023-09-01 19:43:15 -06:00
const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(false);
2023-10-11 00:58:00 -06:00
const queryClient = useQueryClient();
2023-10-22 22:56:45 -06:00
const { data: roomFromDb, isLoading: roomFromDbLoading } = useQuery({
2023-10-11 00:58:00 -06:00
queryKey: ["room"],
queryFn: getRoomHandler,
2023-10-15 00:44:21 -03:00
retry: false,
2023-10-11 00:58:00 -06:00
});
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<VoteResponse>(["votes"], (old) =>
old?.map((vote) => {
if (vote.userId === user?.id) {
return {
...vote,
value: newVote,
};
} else {
return vote;
}
})
);
2023-09-09 19:25:23 -06:00
2023-10-11 00:58:00 -06:00
// Return a context object with the snapshotted value
return { previousVotes };
},
// If the mutation fails,
// use the context returned from onMutate to roll back
2023-10-22 22:56:45 -06:00
onError: (err, newVote, context) => {
2023-10-11 00:58:00 -06:00
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<RoomResponse>(["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,
2023-10-22 22:56:45 -06:00
storyName: data.reset ? storyNameText : old.storyName,
2023-10-11 00:58:00 -06:00
visible: data.visible,
2023-10-22 22:56:45 -06:00
scale: data.reset ? roomScale : old.scale,
2023-10-11 00:58:00 -06:00
reset: data.reset,
log: data.log,
}
: old;
});
2023-10-08 13:21:30 -06:00
2023-10-11 00:58:00 -06:00
// Return a context object with the snapshotted value
return { previousRoom };
},
// If the mutation fails,
// use the context returned from onMutate to roll back
2023-10-22 22:56:45 -06:00
onError: (err, newRoom, context) => {
2023-10-11 00:58:00 -06:00
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}`, {
2023-09-24 23:49:24 -06:00
cache: "no-cache",
method: "GET",
2023-10-11 00:58:00 -06:00
});
2023-09-01 19:43:15 -06:00
2023-10-11 00:58:00 -06:00
return (await response.json()) as RoomResponse;
}
async function getVotesHandler() {
2023-09-24 23:49:24 -06:00
const dbVotesResponse = await fetch(`/api/internal/room/${roomId}/votes`, {
cache: "no-cache",
method: "GET",
});
const dbVotes = (await dbVotesResponse.json()) as VoteResponse;
2023-10-11 00:58:00 -06:00
return dbVotes;
}
2023-10-08 13:21:30 -06:00
2023-10-11 00:58:00 -06:00
async function setVoteHandler(value: string) {
2023-09-01 19:43:15 -06:00
if (roomFromDb) {
2023-09-24 23:49:24 -06:00
await fetch(`/api/internal/room/${roomId}/vote`, {
cache: "no-cache",
method: "PUT",
body: JSON.stringify({
value,
}),
});
2023-09-01 19:43:15 -06:00
}
2023-10-11 00:58:00 -06:00
}
2023-09-01 19:43:15 -06:00
2023-10-11 00:58:00 -06:00
async function setRoomHandler(data: {
visible: boolean;
reset: boolean | undefined;
log: boolean | undefined;
}) {
2023-09-01 19:43:15 -06:00
if (roomFromDb) {
2023-09-24 23:49:24 -06:00
await fetch(`/api/internal/room/${roomId}`, {
cache: "no-cache",
method: "PUT",
body: JSON.stringify({
name: storyNameText,
2023-10-11 00:58:00 -06:00
visible: data.visible,
2023-09-24 23:49:24 -06:00
scale: roomScale,
2023-10-11 00:58:00 -06:00
reset: data.reset ? data.reset : false,
log: data.log ? data.log : false,
2023-09-24 23:49:24 -06:00
}),
});
2023-09-01 19:43:15 -06:00
}
2023-10-11 00:58:00 -06:00
}
// Helpers
// =================================
const getVoteForCurrentUser = () => {
if (roomFromDb) {
return (
votesFromDb && votesFromDb.find((vote) => vote.userId === user?.id)
);
} else {
return null;
}
2023-09-01 19:43:15 -06:00
};
const downloadLogs = () => {
2023-10-11 00:58:00 -06:00
if (roomFromDb && votesFromDb) {
2023-09-01 19:43:15 -06:00
const jsonObject = roomFromDb?.logs
.map((item) => {
return {
id: item.id,
created_at: item.created_at,
userId: item.userId,
roomId: item.roomId,
roomName: item.roomName,
2023-10-11 00:58:00 -06:00
topicName: item.storyName,
2023-09-01 19:43:15 -06:00
scale: item.scale,
votes: item.votes,
};
})
.concat({
id: "LATEST",
created_at: new Date(),
userId: roomFromDb.userId,
roomId: roomFromDb.id,
roomName: roomFromDb.roomName,
2023-10-11 00:58:00 -06:00
topicName: storyNameText,
2023-09-01 19:43:15 -06:00
scale: roomScale,
2023-10-11 00:58:00 -06:00
votes: votesFromDb.map((vote) => {
2023-09-01 19:43:15 -06:00
return {
value: vote.value,
};
}),
});
2023-09-03 16:04:35 -06:00
jsonToCsv(jsonObject, `sp_${roomId}.csv`);
2023-09-01 19:43:15 -06:00
}
};
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) {
2023-10-04 14:45:35 -06:00
return <div>{matchedVote.value}</div>;
2023-09-01 19:43:15 -06:00
} else {
2023-09-24 23:49:24 -06:00
return <IoHourglassOutline className="text-xl text-error" />;
2023-09-01 19:43:15 -06:00
}
} else if (!!matchedVote) {
2023-09-24 23:49:24 -06:00
return <IoCheckmarkCircleOutline className="text-xl text-success" />;
2023-09-01 19:43:15 -06:00
} else {
return (
2023-09-24 23:49:24 -06:00
<IoHourglassOutline className="text-xl animate-spin text-warning" />
2023-09-01 19:43:15 -06:00
);
}
};
2023-10-11 00:58:00 -06:00
// 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<PresenceItem>(
`${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");
}
2023-10-22 22:56:45 -06:00
}, [roomFromDb]);
2023-10-11 00:58:00 -06:00
// UI
// =================================
2023-09-01 19:43:15 -06:00
// Room is loading
2023-10-15 18:45:08 -03:00
if (roomFromDbLoading) {
2023-09-06 13:00:00 -06:00
return <LoadingIndicator />;
2023-09-01 19:43:15 -06:00
// Room has been loaded
2023-10-15 00:44:21 -03:00
} else {
return roomFromDb ? (
2023-09-24 23:49:24 -06:00
<div className="flex flex-col gap-4 text-center justify-center items-center">
2023-10-04 14:45:35 -06:00
<div className="text-2xl">{roomFromDb.roomName}</div>
2023-09-24 23:49:24 -06:00
<div className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
2023-09-01 19:43:15 -06:00
<div>ID:</div>
2023-10-04 14:45:35 -06:00
<div>{roomFromDb.id}</div>
2023-09-01 19:43:15 -06:00
<button>
2023-10-04 14:45:35 -06:00
{copied ? (
2023-09-01 19:43:15 -06:00
<IoCheckmarkCircleOutline className="mx-1 text-success animate-bounce" />
) : (
<IoCopyOutline
className="mx-1 hover:text-primary"
2023-10-04 14:45:35 -06:00
onClick={copyRoomURLHandler}
2023-09-01 19:43:15 -06:00
></IoCopyOutline>
2023-10-04 14:45:35 -06:00
)}
2023-09-01 19:43:15 -06:00
</button>
</div>
2023-10-04 14:45:35 -06:00
{roomFromDb && (
2023-09-24 23:49:24 -06:00
<div className="card card-compact bg-base-100 shadow-xl">
2023-09-01 19:43:15 -06:00
<div className="card-body">
2023-10-04 14:45:35 -06:00
<h2 className="card-title mx-auto">
2023-10-11 00:58:00 -06:00
Topic: {roomFromDb.storyName}
2023-10-04 14:45:35 -06:00
</h2>
2023-09-01 19:43:15 -06:00
2023-09-24 23:49:24 -06:00
<ul className="p-0 flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
2023-10-04 14:45:35 -06:00
{presenceData &&
2023-09-01 19:43:15 -06:00
presenceData
.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) =>
presenceItem.clientId === value.clientId
)
)
.map((presenceItem) => {
return (
<li
2023-10-04 14:45:35 -06:00
key={presenceItem.clientId}
2023-09-01 19:43:15 -06:00
className="flex flex-row items-center justify-center gap-2"
>
2023-09-24 23:49:24 -06:00
<div className="w-10 rounded-full avatar">
2023-09-01 19:43:15 -06:00
<Image
2023-10-04 14:45:35 -06:00
src={presenceItem.data.image}
alt={`${presenceItem.data.name}'s Profile Picture`}
height={32}
width={32}
2023-09-01 19:43:15 -06:00
/>
</div>
2023-09-24 23:49:24 -06:00
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
2023-10-04 14:45:35 -06:00
{presenceItem.data.name}{" "}
{presenceItem.data.isAdmin && (
2023-09-01 19:43:15 -06:00
<span
className="tooltip tooltip-primary"
data-tip="Admin"
>
<FaShieldAlt className="inline-block text-primary" />
</span>
2023-10-04 14:45:35 -06:00
)}{" "}
{presenceItem.data.isVIP && (
2023-09-01 19:43:15 -06:00
<span
className="tooltip tooltip-secondary"
data-tip="VIP"
>
<GiStarFormation className="inline-block text-secondary" />
</span>
2023-10-04 14:45:35 -06:00
)}{" "}
{presenceItem.clientId === roomFromDb.userId && (
2023-09-01 19:43:15 -06:00
<span
className="tooltip tooltip-warning"
data-tip="Room Owner"
>
<RiVipCrownFill className="inline-block text-yellow-500" />
</span>
2023-10-04 14:45:35 -06:00
)}
{" : "}
2023-09-01 19:43:15 -06:00
</p>
2023-10-04 14:45:35 -06:00
{roomFromDb &&
2023-10-11 00:58:00 -06:00
votesFromDb &&
2023-09-01 19:43:15 -06:00
voteString(
roomFromDb.visible,
2023-10-11 00:58:00 -06:00
votesFromDb,
2023-09-01 19:43:15 -06:00
presenceItem.data
2023-10-04 14:45:35 -06:00
)}
2023-09-01 19:43:15 -06:00
</li>
);
2023-10-04 14:45:35 -06:00
})}
2023-09-01 19:43:15 -06:00
</ul>
2023-10-04 14:45:35 -06:00
<div className="join md:btn-group-horizontal mx-auto">
{roomFromDb.scale?.split(",").map((scaleItem, index) => {
2023-09-01 19:43:15 -06:00
return (
<button
2023-10-04 14:45:35 -06:00
key={index}
className={`join-item ${
getVoteForCurrentUser()?.value === scaleItem
? "btn btn-active btn-primary"
: "btn"
}`}
2023-10-11 00:58:00 -06:00
onClick={() => void setVote(scaleItem)}
2023-09-01 19:43:15 -06:00
>
2023-10-04 14:45:35 -06:00
{scaleItem}
2023-09-01 19:43:15 -06:00
</button>
);
2023-10-04 14:45:35 -06:00
})}
2023-09-01 19:43:15 -06:00
</div>
</div>
</div>
2023-10-04 14:45:35 -06:00
)}
2023-09-01 19:43:15 -06:00
2023-10-04 14:45:35 -06:00
{!!roomFromDb &&
2023-09-06 12:53:48 -06:00
(roomFromDb.userId === user?.id || isAdmin(user?.publicMetadata)) && (
2023-09-01 19:43:15 -06:00
<>
2023-09-24 23:49:24 -06:00
<div className="card card-compact bg-base-100 shadow-xl">
2023-09-01 19:43:15 -06:00
<div className="card-body flex flex-col flex-wrap">
2023-09-24 23:49:24 -06:00
<h2 className="card-title">Room Settings</h2>
2023-09-01 19:43:15 -06:00
2023-09-24 23:49:24 -06:00
<label className="label">
2023-10-04 14:45:35 -06:00
{"Vote Scale (Comma Separated):"}{" "}
2023-09-01 19:43:15 -06:00
</label>
<input
type="text"
placeholder="Scale (Comma Separated)"
2023-09-24 23:49:24 -06:00
className="input input-bordered"
2023-10-04 14:45:35 -06:00
value={roomScale}
onChange={(event) => {
2023-09-01 19:43:15 -06:00
setRoomScale(event.target.value);
2023-10-04 14:45:35 -06:00
}}
2023-09-01 19:43:15 -06:00
/>
2023-10-11 00:58:00 -06:00
<label className="label">{"Topic Name:"} </label>
2023-09-01 19:43:15 -06:00
<input
type="text"
2023-10-11 00:58:00 -06:00
placeholder="Topic Name"
2023-09-24 23:49:24 -06:00
className="input input-bordered"
2023-10-04 14:45:35 -06:00
value={storyNameText}
onChange={(event) => {
2023-09-01 19:43:15 -06:00
setStoryNameText(event.target.value);
2023-10-04 14:45:35 -06:00
}}
2023-09-01 19:43:15 -06:00
/>
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
<div>
<button
2023-10-04 14:45:35 -06:00
onClick={() =>
2023-10-11 00:58:00 -06:00
void setRoom({
visible: !roomFromDb.visible,
reset: false,
log: false,
})
2023-09-09 19:25:23 -06:00
}
2023-09-01 19:43:15 -06:00
className="btn btn-primary inline-flex"
>
2023-10-04 14:45:35 -06:00
{roomFromDb.visible ? (
2023-09-01 19:43:15 -06:00
<>
<IoEyeOffOutline className="text-xl mr-1" />
Hide
</>
) : (
<>
<IoEyeOutline className="text-xl mr-1" />
Show
</>
2023-10-04 14:45:35 -06:00
)}
2023-09-01 19:43:15 -06:00
</button>
</div>
<div>
<button
2023-10-04 14:45:35 -06:00
onClick={() =>
2023-10-11 00:58:00 -06:00
void setRoom({
visible: false,
reset: true,
log:
roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true,
})
2023-09-01 19:43:15 -06:00
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
2023-10-04 14:45:35 -06:00
{roomFromDb.storyName === storyNameText ||
2023-10-11 00:58:00 -06:00
votesFromDb?.length === 0 ? (
2023-09-01 19:43:15 -06:00
<>
<IoReloadOutline className="text-xl mr-1" /> Reset
</>
) : (
<>
<IoSaveOutline className="text-xl mr-1" /> Save
</>
2023-10-04 14:45:35 -06:00
)}
2023-09-01 19:43:15 -06:00
</button>
</div>
2023-10-11 00:58:00 -06:00
{votesFromDb &&
2023-09-01 19:43:15 -06:00
(roomFromDb.logs.length > 0 ||
2023-10-11 00:58:00 -06:00
votesFromDb.length > 0) && (
2023-09-01 19:43:15 -06:00
<div>
<button
2023-10-04 14:45:35 -06:00
onClick={() => downloadLogs()}
2023-09-01 19:43:15 -06:00
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<IoDownloadOutline className="text-xl" />
</>
</button>
</div>
2023-10-04 14:45:35 -06:00
)}
2023-09-01 19:43:15 -06:00
</div>
</div>
</div>
</>
2023-10-04 14:45:35 -06:00
)}
2023-09-24 23:49:24 -06:00
</div>
2023-10-15 00:44:21 -03:00
) : (
<NoRoomUI />
2023-09-01 19:43:15 -06:00
);
}
};
export default VoteUI;