pollo/app/routes/room.$roomId.tsx

481 lines
15 KiB
TypeScript
Raw Normal View History

2023-04-20 04:20:00 -06:00
import { getAuth } from "@clerk/remix/ssr.server";
2023-11-22 17:10:44 -07:00
import { LoaderFunction, redirect } from "@remix-run/node";
import { Link, useParams } from "@remix-run/react";
2023-04-20 04:20:00 -06:00
import {
CheckCircleIcon,
CopyIcon,
CrownIcon,
DownloadIcon,
EyeIcon,
EyeOffIcon,
HourglassIcon,
RefreshCwIcon,
SaveIcon,
ShieldIcon,
StarIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import LoadingIndicator from "~/components/LoadingIndicator";
import { useEventSource } from "remix-utils/sse/react";
import { PresenceItem, RoomResponse, VoteResponse } from "~/services/types";
import { isAdmin, jsonToCsv } from "~/services/helpers";
import { useUser } from "@clerk/remix";
2023-11-22 17:06:29 -07:00
import { db } from "~/services/db.server";
import { rooms } from "~/services/schema";
import { eq } from "drizzle-orm";
import { shitList } from "~/services/consts";
2023-04-20 04:20:00 -06:00
2023-11-22 17:06:29 -07:00
// Loader
2023-04-20 04:20:00 -06:00
export const loader: LoaderFunction = async (args) => {
2023-11-22 17:06:29 -07:00
const { userId, sessionClaims } = await getAuth(args);
2023-04-20 04:20:00 -06:00
if (!userId) {
return redirect("/sign-in");
}
2023-11-22 17:06:29 -07:00
const room = await db.query.rooms.findFirst({
where: eq(rooms.id, args.params.roomId as string),
});
if (!room) {
throw new Response(null, {
status: 404,
statusText: "Not Found",
});
}
let isShit = false;
const email = sessionClaims.email as string;
shitList.forEach((shitItem) => {
if (email.includes(shitItem)) {
isShit = true;
}
});
if (isShit) {
2023-11-23 00:36:16 -07:00
throw new Response(null, {
status: 404,
statusText: "Not Found",
});
2023-11-22 17:06:29 -07:00
}
2023-04-20 04:20:00 -06:00
return {};
};
2023-11-22 17:06:29 -07:00
// Checks for 404
export function ErrorBoundary() {
return (
<span className="text-center">
<h1 className="text-5xl font-bold m-2">404</h1>
<h1 className="text-5xl font-bold m-2">
Oops! This room does not appear to exist, or may have been deleted! 😢
</h1>
<Link
about="Back to home."
to="/"
className="btn btn-secondary normal-case text-xl m-2"
>
Back to Home
</Link>
</span>
);
}
2023-04-20 04:20:00 -06:00
export default function Room() {
const { user } = useUser();
const params = useParams();
const roomId = params.roomId;
let roomFromDb = useEventSource(`/api/room/get/${roomId}`, {
event: `room-${params.roomId}`,
});
let votesFromDb = useEventSource(`/api/votes/get/${roomId}`, {
event: `votes-${params.roomId}`,
});
let presenceData = useEventSource(`/api/room/presence/get/${roomId}`, {
event: `${user?.id}-${params.roomId}`,
});
2023-11-22 16:00:27 -07:00
let roomFromDbParsed = (roomFromDb ? JSON.parse(roomFromDb!) : undefined) as
| RoomResponse
2023-11-22 17:06:29 -07:00
| null
2023-11-22 16:00:27 -07:00
| undefined;
2023-11-22 17:06:29 -07:00
2023-04-20 04:20:00 -06:00
let votesFromDbParsed = JSON.parse(votesFromDb!) as VoteResponse | undefined;
let presenceDateParsed = JSON.parse(presenceData!) as
| PresenceItem[]
| undefined;
const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(false);
// Handlers
// =================================
async function setVoteHandler(value: string) {
if (roomFromDb) {
await fetch(`/api/vote/set/${roomId}`, {
cache: "no-cache",
method: "PUT",
body: JSON.stringify({
value,
}),
});
}
}
async function setRoomHandler(data: {
visible: boolean;
reset: boolean | undefined;
log: boolean | undefined;
}) {
if (roomFromDb) {
await fetch(`/api/room/set/${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 (
votesFromDbParsed &&
votesFromDbParsed.find((vote) => vote.userId === user?.id)
);
} else {
return null;
}
};
const downloadLogs = () => {
2023-11-22 16:00:27 -07:00
if (roomFromDbParsed && votesFromDbParsed) {
2023-04-20 04:20:00 -06:00
const jsonObject = roomFromDbParsed?.logs
.map((item) => {
return {
id: item.id,
created_at: item.created_at,
userId: item.userId,
roomId: item.roomId,
roomName: item.roomName,
storyName: item.storyName,
scale: item.scale,
votes: item.votes,
};
})
.concat({
id: "LATEST",
created_at: new Date(),
userId: roomFromDbParsed.userId,
roomId: roomFromDbParsed.id,
roomName: roomFromDbParsed.roomName,
storyName: storyNameText,
scale: roomScale,
votes: votesFromDbParsed?.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 votesFromDbParsed,
presenceItem: PresenceItem
) => {
const matchedVote = votes?.find(
(vote) => vote.userId === presenceItem.userId
);
if (visible) {
if (!!matchedVote) {
return <div>{matchedVote.value}</div>;
} else {
return <HourglassIcon className="text-xl text-error" />;
}
} else if (!!matchedVote) {
return <CheckCircleIcon className="text-xl text-success" />;
} else {
return <HourglassIcon className="text-xl animate-spin text-warning" />;
}
};
// Hooks
// =================================
useEffect(() => {
if (roomFromDb) {
setStoryNameText(roomFromDbParsed?.storyName || "");
setRoomScale(roomFromDbParsed?.scale || "ERROR");
}
}, [roomFromDb]);
// UI
// =================================
// Room is loading
2023-11-22 17:06:29 -07:00
if (!roomFromDbParsed) {
2023-04-20 04:20:00 -06:00
return <LoadingIndicator />;
// Room has been loaded
} else {
2023-11-22 17:06:29 -07:00
return (
2023-04-20 04:20:00 -06:00
<div className="flex flex-col gap-4 text-center justify-center items-center">
2023-11-22 16:00:27 -07:00
<div className="text-2xl">{roomFromDbParsed?.roomName}</div>
2023-04-20 04:20:00 -06:00
<div className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
<div>ID:</div>
2023-11-22 16:00:27 -07:00
<div>{roomFromDbParsed?.id}</div>
2023-04-20 04:20:00 -06:00
<button>
{copied ? (
<CheckCircleIcon className="mx-1 text-success animate-bounce" />
) : (
<CopyIcon
className="mx-1 hover:text-primary"
onClick={copyRoomURLHandler}
/>
)}
</button>
</div>
{roomFromDb && (
<div className="card card-compact bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title mx-auto">
2023-11-22 16:00:27 -07:00
Story: {roomFromDbParsed?.storyName}
2023-04-20 04:20:00 -06:00
</h2>
<ul className="p-0 flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
{presenceData &&
presenceDateParsed
?.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) => presenceItem.userId === value.userId
)
)
.map((presenceItem) => {
return (
<li
key={presenceItem.userId}
className="flex flex-row items-center justify-center gap-2"
>
<div className="w-10 rounded-full avatar">
<img
src={presenceItem.userImageUrl}
alt={`${presenceItem.userFullName}'s Profile Picture`}
height={32}
width={32}
/>
</div>
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
{presenceItem.userFullName}{" "}
{presenceItem.isAdmin && (
<span
className="tooltip tooltip-primary"
data-tip="Admin"
>
<ShieldIcon className="inline-block text-primary" />
</span>
)}{" "}
{presenceItem.isVIP && (
<span
className="tooltip tooltip-secondary"
data-tip="VIP"
>
<StarIcon className="inline-block text-secondary" />
</span>
)}{" "}
{presenceItem.userId ===
roomFromDbParsed?.userId && (
<span
className="tooltip tooltip-warning"
data-tip="Room Owner"
>
2023-11-23 00:36:16 -07:00
<CrownIcon className="inline-block text-warning" />
2023-04-20 04:20:00 -06:00
</span>
)}
{" : "}
</p>
{roomFromDb &&
votesFromDb &&
voteString(
roomFromDbParsed?.visible!,
votesFromDbParsed,
presenceItem
)}
</li>
);
})}
</ul>
<div className="join md:btn-group-horizontal mx-auto">
2023-11-22 16:00:27 -07:00
{roomFromDbParsed?.scale?.split(",").map((scaleItem, index) => {
2023-04-20 04:20:00 -06:00
return (
<button
key={index}
className={`join-item ${
getVoteForCurrentUser()?.value === scaleItem
? "btn btn-active btn-primary"
: "btn"
}`}
onClick={() => void setVoteHandler(scaleItem)}
>
{scaleItem}
</button>
);
})}
</div>
</div>
</div>
)}
{!!roomFromDbParsed &&
(roomFromDbParsed.userId === user?.id ||
isAdmin(user?.publicMetadata)) && (
<>
<div className="card card-compact bg-base-100 shadow-xl">
<div className="card-body flex flex-col flex-wrap">
<h2 className="card-title">Room Settings</h2>
<label className="label">
{"Vote Scale (Comma Separated):"}{" "}
</label>
<input
type="text"
placeholder="Scale (Comma Separated)"
className="input input-bordered"
value={roomScale}
onChange={(event) => {
setRoomScale(event.target.value);
}}
/>
<label className="label">{"Story Name:"} </label>
<input
type="text"
placeholder="Story Name"
className="input input-bordered"
value={storyNameText}
onChange={(event) => {
setStoryNameText(event.target.value);
}}
/>
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
<div>
<button
onClick={() =>
void setRoomHandler({
visible: !roomFromDbParsed?.visible,
reset: false,
log: false,
})
}
className="btn btn-primary inline-flex"
>
{roomFromDbParsed.visible ? (
<>
<EyeOffIcon className="text-xl mr-1" />
Hide
</>
) : (
<>
<EyeIcon className="text-xl mr-1" />
Show
</>
)}
</button>
</div>
<div>
<button
onClick={() =>
void setRoomHandler({
visible: false,
reset: true,
log:
roomFromDbParsed?.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true,
})
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
{roomFromDbParsed?.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
<>
<RefreshCwIcon className="text-xl mr-1" /> Reset
</>
) : (
<>
<SaveIcon className="text-xl mr-1" /> Save
</>
)}
</button>
</div>
{votesFromDb &&
(roomFromDbParsed?.logs.length > 0 ||
votesFromDb.length > 0) && (
<div>
<button
onClick={() => downloadLogs()}
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<DownloadIcon className="text-xl" />
</>
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
);
}
}