pollo/src/pages/room/[id].tsx

482 lines
15 KiB
TypeScript
Raw Normal View History

2023-07-11 17:22:54 -06:00
import { type GetServerSideProps, type NextPage } from "next";
2023-04-20 04:20:00 -06:00
import Head from "next/head";
2023-07-11 17:22:54 -06:00
import Image from "next/image";
2023-04-20 04:20:00 -06:00
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
2023-07-11 17:22:54 -06:00
import { useRouter } from "next/router";
2023-04-20 04:20:00 -06:00
import {
2023-07-11 18:19:45 -06:00
IoCheckmarkCircleOutline,
IoCopyOutline,
IoDownloadOutline,
IoEyeOffOutline,
IoEyeOutline,
IoHourglassOutline,
IoReloadOutline,
IoSaveOutline,
2023-04-20 04:20:00 -06:00
} from "react-icons/io5";
2023-07-11 17:22:54 -06:00
import { z } from "zod";
import { api } from "~/utils/api";
import { getServerAuthSession } from "../../server/auth";
2023-04-20 04:20:00 -06:00
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
2023-07-11 17:22:54 -06:00
import Link from "next/link";
2023-04-20 04:20:00 -06:00
import { FaShieldAlt } from "react-icons/fa";
import { RiVipCrownFill } from "react-icons/ri";
2023-07-11 17:22:54 -06:00
import { env } from "~/env.mjs";
2023-04-20 04:20:00 -06:00
import { downloadCSV } from "~/utils/helpers";
2023-07-11 17:22:54 -06:00
import type { PresenceItem } from "~/utils/types";
2023-04-20 04:20:00 -06:00
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
// Redirect to login if not signed in
2023-04-20 04:20:00 -06:00
if (!session) {
return {
redirect: {
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
2023-04-20 04:20:00 -06:00
permanent: false,
},
};
}
// Return session if logged in
return {
props: { session },
};
};
const Room: NextPage = () => {
return (
<>
<Head>
<title>Sprint Padawan</title>
<meta name="description" content="Plan. Sprint. Repeat." />
2023-06-28 17:04:11 -06:00
<meta http-equiv="Cache-control" content="no-cache" />
2023-04-20 04:20:00 -06:00
</Head>
<div className="flex flex-col items-center justify-center text-center gap-2">
<RoomBody />
</div>
</>
);
};
export default Room;
const RoomBody: React.FC = () => {
2023-04-20 04:20:00 -06:00
const { data: sessionData } = useSession();
const { query } = useRouter();
const roomId = z.string().parse(query.id);
const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>("");
2023-07-10 00:30:57 -06:00
const [copied, setCopied] = useState<boolean>(false);
2023-04-20 04:20:00 -06:00
const { data: roomFromDb, refetch: refetchRoomFromDb } =
api.room.get.useQuery({ id: roomId });
2023-06-04 15:43:46 -06:00
const { data: votesFromDb, refetch: refetchVotesFromDb } =
api.vote.getAllByRoomId.useQuery({ roomId });
2023-04-20 04:20:00 -06:00
const setVoteInDb = api.vote.set.useMutation({});
const setRoomInDb = api.room.set.useMutation({});
configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
clientId: sessionData?.user.id,
recover: (_, cb) => {
cb(true);
},
});
const [channel] = useChannel(
{
channelName: `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
},
2023-06-04 15:43:46 -06:00
({ name }) => {
if (name === "ROOM_UPDATE") {
void refetchVotesFromDb();
void refetchRoomFromDb();
} else if (name === "VOTE_UPDATE") {
void refetchVotesFromDb();
}
}
2023-04-20 04:20:00 -06:00
);
const [presenceData] = usePresence<PresenceItem>(
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
{
name: sessionData?.user.name || "",
image: sessionData?.user.image || "",
client_id: sessionData?.user.id || "",
role: sessionData?.user.role || "USER",
}
);
// Subscribe on mount and unsubscribe on unmount
useEffect(() => {
window.addEventListener("beforeunload", () => channel.presence.leave());
return () => {
window.removeEventListener("beforeunload", () =>
channel.presence.leave()
);
channel.presence.leave();
};
}, [channel.presence, roomId]);
2023-04-20 04:20:00 -06:00
// Init story name
useEffect(() => {
if (sessionData && roomFromDb) {
2023-06-28 16:35:12 -06:00
setStoryNameText(roomFromDb.storyName || "");
setRoomScale(roomFromDb.scale || "ERROR");
2023-04-20 04:20:00 -06:00
}
}, [roomFromDb, roomId, sessionData]);
2023-04-20 04:20:00 -06:00
// Helper functions
const getVoteForCurrentUser = () => {
if (roomFromDb && sessionData) {
return (
2023-06-04 15:43:46 -06:00
votesFromDb &&
votesFromDb.find((vote) => vote.userId === sessionData.user.id)
2023-04-20 04:20:00 -06:00
);
} else {
return null;
}
};
const setVote = (value: string) => {
if (roomFromDb) {
setVoteInDb.mutate({
roomId: roomFromDb.id,
value: value,
});
}
};
const saveRoom = (visible: boolean, reset = false, log = false) => {
if (roomFromDb) {
setRoomInDb.mutate({
name: storyNameText,
roomId: roomFromDb.id,
scale: roomScale,
visible: visible,
reset: reset,
log: log,
});
}
};
const downloadLogs = () => {
2023-06-04 15:43:46 -06:00
if (roomFromDb && votesFromDb) {
2023-04-20 04:20:00 -06:00
const jsonObject = roomFromDb?.logs
.map((item) => {
return {
...item,
scale: item.scale,
votes: item.votes,
roomName: item.roomName,
storyName: item.storyName,
};
})
.concat({
id: "LATEST",
createdAt: new Date(),
userId: roomFromDb.owner.id,
roomId: roomFromDb.id,
scale: roomScale,
2023-06-04 15:43:46 -06:00
votes: votesFromDb.map((vote) => {
2023-04-20 04:20:00 -06:00
return {
name: vote.owner.name,
value: vote.value,
};
}),
roomName: roomFromDb.roomName,
storyName: storyNameText,
});
downloadCSV(jsonObject, `sprint-padawan-room-${roomId}.csv`);
}
};
const copyRoomURLHandler = () => {
navigator.clipboard
.writeText(window.location.href)
2023-07-10 00:34:01 -06:00
.then(() => {
2023-04-20 04:20:00 -06:00
console.log(`Copied Room Link to Clipboard!`);
2023-07-10 00:30:57 -06:00
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
2023-04-20 04:20:00 -06:00
})
.catch(() => {
console.log(`Error Copying Room Link to Clipboard!`);
});
};
const voteString = (
visible: boolean,
2023-06-04 15:43:46 -06:00
votes: typeof votesFromDb,
2023-04-20 04:20:00 -06:00
presenceItem: PresenceItem
) => {
2023-06-04 15:43:46 -06:00
const matchedVote = votes?.find(
2023-04-20 04:20:00 -06:00
(vote) => vote.userId === presenceItem.client_id
);
if (visible) {
if (!!matchedVote) {
2023-07-11 18:19:45 -06:00
return <p>{ matchedVote.value }</p>;
2023-04-20 04:20:00 -06:00
} else {
return <IoHourglassOutline className="text-xl mx-auto text-red-400" />;
}
} else if (!!matchedVote) {
return (
<IoCheckmarkCircleOutline className="text-xl mx-auto text-green-400" />
);
} else {
return (
<IoHourglassOutline className="text-xl animate-spin mx-auto text-yellow-400" />
);
}
};
// Room is loading
if (roomFromDb === undefined) {
return (
<div className="flex flex-col items-center justify-center text-center">
2023-07-11 18:19:45 -06:00
<span className="loading loading-dots loading-lg"></span>{ " " }
2023-04-20 04:20:00 -06:00
</div>
);
// Room has been loaded
} else if (roomFromDb) {
return (
<span className="text-center">
2023-07-11 18:19:45 -06:00
<div className="text-2xl">{ roomFromDb.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 mx-auto">
<div>ID:</div>
2023-07-11 18:19:45 -06:00
<div>{ roomFromDb.id }</div>
2023-04-20 04:20:00 -06:00
<button>
2023-07-11 18:19:45 -06:00
{ copied ? (
2023-07-10 00:30:57 -06:00
<IoCheckmarkCircleOutline className="mx-1 text-green-400 animate-bounce" />
) : (
<IoCopyOutline
className="mx-1 hover:text-primary"
2023-07-11 18:19:45 -06:00
onClick={ copyRoomURLHandler }
2023-07-10 00:30:57 -06:00
></IoCopyOutline>
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
</button>
</div>
2023-07-11 18:19:45 -06:00
{ roomFromDb && (
2023-04-20 04:20:00 -06:00
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
<div className="card-body">
<h2 className="card-title mx-auto">
2023-07-11 18:19:45 -06:00
Story: { roomFromDb.storyName }
2023-04-20 04:20:00 -06:00
</h2>
<ul className="p-0 mx-auto flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
2023-07-11 18:19:45 -06:00
{ presenceData &&
2023-04-20 04:20:00 -06:00
presenceData
.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) =>
presenceItem.clientId === value.clientId
)
)
.map((presenceItem) => {
return (
<li
2023-07-11 18:19:45 -06:00
key={ presenceItem.clientId }
2023-04-20 04:20:00 -06:00
className="flex flex-row items-center justify-center gap-2"
>
<div className="w-10 rounded-full avatar mx-auto">
<Image
2023-07-11 18:19:45 -06:00
src={ presenceItem.data.image }
alt={ `${presenceItem.data.name}'s Profile Picture` }
height={ 32 }
width={ 32 }
2023-04-20 04:20:00 -06:00
/>
</div>
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md mx-auto">
2023-07-11 18:19:45 -06:00
{ presenceItem.data.name }{ " " }
{ presenceItem.data.role === "ADMIN" && (
2023-04-20 04:20:00 -06:00
<div
className="tooltip tooltip-primary"
data-tip="Admin"
>
<FaShieldAlt className="inline-block text-primary" />
</div>
2023-07-11 18:19:45 -06:00
) }{ " " }
{ presenceItem.clientId === roomFromDb.userId && (
2023-04-20 04:20:00 -06:00
<div
className="tooltip tooltip-warning"
data-tip="Room Owner"
>
<RiVipCrownFill className="inline-block text-yellow-500" />
</div>
2023-07-11 18:19:45 -06:00
) }
{ " : " }
2023-04-20 04:20:00 -06:00
</p>
2023-07-11 18:19:45 -06:00
{ roomFromDb &&
2023-06-04 15:43:46 -06:00
votesFromDb &&
2023-04-20 04:20:00 -06:00
voteString(
roomFromDb.visible,
2023-06-04 15:43:46 -06:00
votesFromDb,
2023-04-20 04:20:00 -06:00
presenceItem.data
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
</li>
);
2023-07-11 18:19:45 -06:00
}) }
2023-04-20 04:20:00 -06:00
</ul>
2023-06-02 23:10:36 -06:00
<div className="join md:btn-group-horizontal mx-auto">
2023-07-11 18:19:45 -06:00
{ roomFromDb.scale.split(",").map((scaleItem, index) => {
2023-04-20 04:20:00 -06:00
return (
<button
2023-07-11 18:19:45 -06:00
key={ index }
className={ `join-item ${getVoteForCurrentUser()?.value === scaleItem
2023-06-02 23:10:36 -06:00
? "btn btn-active btn-primary"
2023-04-20 04:20:00 -06:00
: "btn"
2023-07-11 18:19:45 -06:00
}` }
onClick={ () => setVote(scaleItem) }
2023-04-20 04:20:00 -06:00
>
2023-07-11 18:19:45 -06:00
{ scaleItem }
2023-04-20 04:20:00 -06:00
</button>
);
2023-07-11 18:19:45 -06:00
}) }
2023-04-20 04:20:00 -06:00
</div>
</div>
</div>
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
2023-07-11 18:19:45 -06:00
{ sessionData &&
2023-04-20 04:20:00 -06:00
!!roomFromDb &&
roomFromDb.userId === sessionData.user.id && (
<>
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
<div className="card-body flex flex-col flex-wrap">
<h2 className="card-title mx-auto">Room Settings</h2>
<label className="label mx-auto">
2023-07-11 18:19:45 -06:00
{ "Vote Scale (Comma Separated):" }{ " " }
2023-04-20 04:20:00 -06:00
</label>
<input
type="text"
placeholder="Scale (Comma Separated)"
className="input input-bordered m-auto"
2023-07-11 18:19:45 -06:00
value={ roomScale }
onChange={ (event) => {
2023-05-30 16:44:01 -06:00
setRoomScale(event.target.value);
2023-07-11 18:19:45 -06:00
} }
2023-04-20 04:20:00 -06:00
/>
2023-07-11 18:19:45 -06:00
<label className="label mx-auto">{ "Story Name:" } </label>
2023-04-20 04:20:00 -06:00
<input
type="text"
placeholder="Story Name"
className="input input-bordered m-auto"
2023-07-11 18:19:45 -06:00
value={ storyNameText }
onChange={ (event) => {
2023-05-30 16:44:01 -06:00
setStoryNameText(event.target.value);
2023-07-11 18:19:45 -06:00
} }
2023-04-20 04:20:00 -06:00
/>
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
<div>
<button
2023-07-11 18:19:45 -06:00
onClick={ () => saveRoom(!roomFromDb.visible, false) }
2023-04-20 04:20:00 -06:00
className="btn btn-primary inline-flex"
>
2023-07-11 18:19:45 -06:00
{ roomFromDb.visible ? (
2023-04-20 04:20:00 -06:00
<>
<IoEyeOffOutline className="text-xl mr-1" />
Hide
</>
) : (
<>
<IoEyeOutline className="text-xl mr-1" />
Show
</>
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
</button>
</div>
<div>
<button
2023-07-11 18:19:45 -06:00
onClick={ () =>
2023-04-20 04:20:00 -06:00
saveRoom(
false,
true,
roomFromDb.storyName === storyNameText ||
2023-06-04 15:43:46 -06:00
votesFromDb?.length === 0
2023-04-20 04:20:00 -06:00
? false
: true
)
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
2023-07-11 18:19:45 -06:00
{ roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
2023-04-20 04:20:00 -06:00
<>
2023-06-02 23:10:36 -06:00
<IoReloadOutline className="text-xl mr-1" /> Reset
2023-04-20 04:20:00 -06:00
</>
) : (
<>
<IoSaveOutline className="text-xl mr-1" /> Save
</>
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
</button>
</div>
2023-07-11 18:19:45 -06:00
{ votesFromDb &&
2023-06-04 15:43:46 -06:00
(roomFromDb.logs.length > 0 ||
votesFromDb.length > 0) && (
<div>
<button
2023-07-11 18:19:45 -06:00
onClick={ () => downloadLogs() }
2023-06-04 15:43:46 -06:00
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<IoDownloadOutline className="text-xl" />
</>
</button>
</div>
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
</div>
</div>
</div>
</>
2023-07-11 18:19:45 -06:00
) }
2023-04-20 04:20:00 -06:00
</span>
);
// Room does not exist
} else {
return (
2023-06-05 16:56:28 -06:00
<span className="text-center">
<h1 className="text-5xl font-bold m-2">404</h1>
<h1 className="text-5xl font-bold m-2">
2023-04-20 04:20:00 -06:00
Oops! This room does not appear to exist, or may have been deleted! 😢
</h1>
<Link
about="Back to home."
href="/"
2023-06-05 16:56:28 -06:00
className="btn btn-secondary normal-case text-xl m-2"
2023-04-20 04:20:00 -06:00
>
Back to Home
</Link>
</span>
);
}
};