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

479 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getAuth } from "@clerk/remix/ssr.server";
import { LoaderFunction, redirect } from "@remix-run/node";
import { Link, useParams } from "@remix-run/react";
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";
import { db } from "~/services/db.server";
import { rooms } from "~/services/schema";
import { eq } from "drizzle-orm";
import { shitList } from "~/services/consts";
// Loader
export const loader: LoaderFunction = async (args) => {
const { userId, sessionClaims } = await getAuth(args);
if (!userId) {
return redirect("/sign-in");
}
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) {
return redirect(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&pp=ygUXbmV2ZXIgZ29ubmEgZ2l2ZSB5b3UgdXA%3D"
);
}
return {};
};
// 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>
);
}
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}`,
});
let roomFromDbParsed = (roomFromDb ? JSON.parse(roomFromDb!) : undefined) as
| RoomResponse
| null
| undefined;
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 = () => {
if (roomFromDbParsed && votesFromDbParsed) {
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
if (!roomFromDbParsed) {
return <LoadingIndicator />;
// Room has been loaded
} else {
return (
<div className="flex flex-col gap-4 text-center justify-center items-center">
<div className="text-2xl">{roomFromDbParsed?.roomName}</div>
<div className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
<div>ID:</div>
<div>{roomFromDbParsed?.id}</div>
<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">
Story: {roomFromDbParsed?.storyName}
</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"
>
<CrownIcon className="inline-block text-yellow-500" />
</span>
)}
{" : "}
</p>
{roomFromDb &&
votesFromDb &&
voteString(
roomFromDbParsed?.visible!,
votesFromDbParsed,
presenceItem
)}
</li>
);
})}
</ul>
<div className="join md:btn-group-horizontal mx-auto">
{roomFromDbParsed?.scale?.split(",").map((scaleItem, index) => {
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>
);
}
}