Updated to Atash's latest

This commit is contained in:
Atridad Lahiji 2023-09-01 19:43:15 -06:00 committed by atridadl
parent 7405286b7a
commit 922eea06a0
No known key found for this signature in database
7 changed files with 527 additions and 577 deletions

View file

@ -11,7 +11,7 @@ interface NavbarProps {
} }
const Navbar = ({ title }: NavbarProps) => { const Navbar = ({ title }: NavbarProps) => {
const { isLoaded, isSignedIn } = useUser(); const { isSignedIn } = useUser();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
@ -57,13 +57,7 @@ const Navbar = ({ title }: NavbarProps) => {
</Link> </Link>
</div> </div>
{!isLoaded ? ( {navigationMenu()}
<div className="flex items-center justify-center">
<span className="loading loading-dots loading-lg"></span>
</div>
) : (
navigationMenu()
)}
<UserButton afterSignOutUrl="/" /> <UserButton afterSignOutUrl="/" />
</nav> </nav>

View file

@ -0,0 +1,7 @@
"use client";
const Loading = () => {
return <Loading />;
};
export default Loading;

View file

@ -2,36 +2,37 @@
import Link from "next/link"; import Link from "next/link";
import { configureAbly, useChannel } from "@ably-labs/react-hooks"; import { configureAbly, useChannel } from "@ably-labs/react-hooks";
import { useState } from "react"; import { useEffect, useState } from "react";
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5"; import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { useUser } from "@clerk/nextjs"; import { useOrganization } from "@clerk/nextjs";
import { trpc } from "../_trpc/client"; import { trpc } from "../_trpc/client";
import Loading from "./Loading";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 0;
export const fetchCache = "force-no-store";
const RoomList = () => { const RoomList = ({ userId }: { userId: string }) => {
const { isSignedIn, user } = useUser(); const { organization } = useOrganization();
configureAbly({ configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY, key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
clientId: user?.id, clientId: userId,
recover: (_, cb) => { recover: (_, cb) => {
cb(true); cb(true);
}, },
}); });
const [] = useChannel( useChannel(
`${env.NEXT_PUBLIC_APP_ENV}-${user?.id}`, `${env.NEXT_PUBLIC_APP_ENV}-${organization ? organization.id : userId}`,
() => void refetchRoomsFromDb() () => void refetchRoomsFromDb()
); );
const [roomName, setRoomName] = useState<string>(""); const [roomName, setRoomName] = useState<string>("");
const { data: roomsFromDb, refetch: refetchRoomsFromDb } = const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
trpc.room.getAll.useQuery(undefined, { trpc.room.getAll.useQuery(undefined);
enabled: isSignedIn,
});
const createRoom = trpc.room.create.useMutation({}); const createRoom = trpc.room.create.useMutation({});
@ -46,11 +47,13 @@ const RoomList = () => {
const deleteRoom = trpc.room.delete.useMutation({}); const deleteRoom = trpc.room.delete.useMutation({});
const deleteRoomHandler = (roomId: string) => { const deleteRoomHandler = (roomId: string) => {
if (isSignedIn) { deleteRoom.mutate({ id: roomId });
deleteRoom.mutate({ id: roomId });
}
}; };
useEffect(() => {
void refetchRoomsFromDb();
}, [organization]);
return ( return (
<div className="flex flex-col items-center justify-center gap-8"> <div className="flex flex-col items-center justify-center gap-8">
{/* Modal for Adding Rooms */} {/* Modal for Adding Rooms */}
@ -98,6 +101,7 @@ const RoomList = () => {
{roomsFromDb && roomsFromDb.length > 0 && ( {roomsFromDb && roomsFromDb.length > 0 && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="table text-center"> <table className="table text-center">
{/* head */}
<thead> <thead>
<tr className="border-white"> <tr className="border-white">
<th>Room Name</th> <th>Room Name</th>
@ -116,7 +120,7 @@ const RoomList = () => {
className="m-2 no-underline" className="m-2 no-underline"
href={`/room/${room.id}`} href={`/room/${room.id}`}
> >
<IoEnterOutline className="text-xl inline-block hover:text-secondary" /> <IoEnterOutline className="text-xl inline-block hover:text-primary" />
</Link> </Link>
<button <button
@ -133,15 +137,11 @@ const RoomList = () => {
</table> </table>
</div> </div>
)} )}
<label htmlFor="new-room-modal" className="btn btn-secondary"> <label htmlFor="new-room-modal" className="btn btn-primary">
New Room New Room
</label> </label>
{roomsFromDb === undefined && ( {roomsFromDb === undefined && <Loading />}
<div className="flex items-center justify-center">
<span className="loading loading-dots loading-lg"></span>
</div>
)}
</div> </div>
); );
}; };

View file

@ -0,0 +1,464 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import { EventTypes } from "@/utils/types";
import { useParams } from "next/navigation";
import {
IoCheckmarkCircleOutline,
IoCopyOutline,
IoDownloadOutline,
IoEyeOffOutline,
IoEyeOutline,
IoHourglassOutline,
IoReloadOutline,
IoSaveOutline,
} from "react-icons/io5";
import { GiStarFormation } from "react-icons/gi";
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
import Link from "next/link";
import { FaShieldAlt } from "react-icons/fa";
import { RiVipCrownFill } from "react-icons/ri";
import { env } from "@/env.mjs";
import { isAdmin, isVIP } from "@/utils/helpers";
import type { PresenceItem } from "@/utils/types";
import { trpc } from "@/app/_trpc/client";
import Loading from "@/app/_components/Loading";
import { parse } from "json2csv";
import { User } from "@clerk/nextjs/dist/types/server";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export const fetchCache = "force-no-store";
const VoteUI = ({ user }: { user: Partial<User> }) => {
const params = useParams();
const roomId = params?.id as string;
const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(false);
const { data: roomFromDb, refetch: refetchRoomFromDb } =
trpc.room.get.useQuery({ id: roomId });
const { data: votesFromDb, refetch: refetchVotesFromDb } =
trpc.vote.getAllByRoomId.useQuery({ roomId });
const setVoteInDb = trpc.vote.set.useMutation({});
const setRoomInDb = trpc.room.set.useMutation({});
configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
clientId: user ? user.id : "unknown",
recover: (_, cb) => {
cb(true);
},
});
const [channel] = useChannel(
{
channelName: `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
},
({ name }) => {
if (name === EventTypes.ROOM_UPDATE) {
void refetchVotesFromDb();
void refetchRoomFromDb();
} else if (name === EventTypes.VOTE_UPDATE) {
void refetchVotesFromDb();
}
}
);
const [presenceData] = usePresence<PresenceItem>(
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
{
name: `${user?.firstName} ${user?.lastName}` || "",
image: user?.imageUrl || "",
client_id: user?.id || "unknown",
isAdmin: isAdmin(user?.publicMetadata),
isVIP: isVIP(user?.publicMetadata),
}
);
// 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]);
// Init story name
useEffect(() => {
if (roomFromDb) {
setStoryNameText(roomFromDb.storyName || "");
setRoomScale(roomFromDb.scale || "ERROR");
}
}, [roomFromDb, roomId, user]);
// Helper functions
const getVoteForCurrentUser = () => {
if (roomFromDb) {
return votesFromDb && votesFromDb.find((vote) => vote.userId === user.id);
} 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 = () => {
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,
storyName: item.storyName,
scale: item.scale,
votes: item.votes,
};
})
.concat({
id: "LATEST",
created_at: new Date(),
userId: roomFromDb.userId,
roomId: roomFromDb.id,
roomName: roomFromDb.roomName,
storyName: storyNameText,
scale: roomScale,
votes: votesFromDb.map((vote) => {
return {
value: vote.value,
};
}),
});
const csv = parse(jsonObject);
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `sp_${roomId}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
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 <div>{matchedVote.value}</div>;
} else {
return <IoHourglassOutline className="text-xl mx-auto text-error" />;
}
} else if (!!matchedVote) {
return (
<IoCheckmarkCircleOutline className="text-xl mx-auto text-success" />
);
} else {
return (
<IoHourglassOutline className="text-xl animate-spin mx-auto text-warning" />
);
}
};
// Room is loading
if (roomFromDb === undefined) {
return <Loading />;
// Room has been loaded
} else if (roomFromDb) {
return (
<span className="text-center">
<div className="text-2xl">{roomFromDb.roomName}</div>
<div className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md mx-auto">
<div>ID:</div>
<div>{roomFromDb.id}</div>
<button>
{copied ? (
<IoCheckmarkCircleOutline className="mx-1 text-success animate-bounce" />
) : (
<IoCopyOutline
className="mx-1 hover:text-primary"
onClick={copyRoomURLHandler}
></IoCopyOutline>
)}
</button>
</div>
{roomFromDb && (
<div className="card card-compact bg-base-100 shadow-xl mx-auto m-4">
<div className="card-body">
<h2 className="card-title mx-auto">
Story: {roomFromDb.storyName}
</h2>
<ul className="p-0 mx-auto flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
{presenceData &&
presenceData
.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) =>
presenceItem.clientId === value.clientId
)
)
.map((presenceItem) => {
return (
<li
key={presenceItem.clientId}
className="flex flex-row items-center justify-center gap-2"
>
<div className="w-10 rounded-full avatar mx-auto">
<Image
src={presenceItem.data.image}
alt={`${presenceItem.data.name}'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 mx-auto">
{presenceItem.data.name}{" "}
{presenceItem.data.isAdmin && (
<span
className="tooltip tooltip-primary"
data-tip="Admin"
>
<FaShieldAlt className="inline-block text-primary" />
</span>
)}{" "}
{presenceItem.data.isVIP && (
<span
className="tooltip tooltip-secondary"
data-tip="VIP"
>
<GiStarFormation className="inline-block text-secondary" />
</span>
)}{" "}
{presenceItem.clientId === roomFromDb.userId && (
<span
className="tooltip tooltip-warning"
data-tip="Room Owner"
>
<RiVipCrownFill className="inline-block text-yellow-500" />
</span>
)}
{" : "}
</p>
{roomFromDb &&
votesFromDb &&
voteString(
roomFromDb.visible,
votesFromDb,
presenceItem.data
)}
</li>
);
})}
</ul>
<div className="join md:btn-group-horizontal mx-auto">
{roomFromDb.scale.split(",").map((scaleItem, index) => {
return (
<button
key={index}
className={`join-item ${
getVoteForCurrentUser()?.value === scaleItem
? "btn btn-active btn-primary"
: "btn"
}`}
onClick={() => setVote(scaleItem)}
>
{scaleItem}
</button>
);
})}
</div>
</div>
</div>
)}
{!!roomFromDb &&
(roomFromDb.userId === user.id || isAdmin(user?.publicMetadata)) && (
<>
<div className="card card-compact bg-base-100 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">
{"Vote Scale (Comma Separated):"}{" "}
</label>
<input
type="text"
placeholder="Scale (Comma Separated)"
className="input input-bordered m-auto"
value={roomScale}
onChange={(event) => {
setRoomScale(event.target.value);
}}
/>
<label className="label mx-auto">{"Story Name:"} </label>
<input
type="text"
placeholder="Story Name"
className="input input-bordered m-auto"
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={() => saveRoom(!roomFromDb.visible, false)}
className="btn btn-primary inline-flex"
>
{roomFromDb.visible ? (
<>
<IoEyeOffOutline className="text-xl mr-1" />
Hide
</>
) : (
<>
<IoEyeOutline className="text-xl mr-1" />
Show
</>
)}
</button>
</div>
<div>
<button
onClick={() =>
saveRoom(
false,
true,
roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true
)
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
{roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
<>
<IoReloadOutline className="text-xl mr-1" /> Reset
</>
) : (
<>
<IoSaveOutline className="text-xl mr-1" /> Save
</>
)}
</button>
</div>
{votesFromDb &&
(roomFromDb.logs.length > 0 ||
votesFromDb.length > 0) && (
<div>
<button
onClick={() => downloadLogs()}
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<IoDownloadOutline className="text-xl" />
</>
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</span>
);
// Room does not exist
} else {
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."
href="/"
className="btn btn-secondary normal-case text-xl m-2"
>
Back to Home
</Link>
</span>
);
}
};
export default VoteUI;

View file

@ -1,44 +1,20 @@
"use client";
import RoomList from "@/app/_components/RoomList"; import RoomList from "@/app/_components/RoomList";
import Link from "next/link";
import { useEffect, useState } from "react";
import { FaShieldAlt } from "react-icons/fa"; import { FaShieldAlt } from "react-icons/fa";
import { GiStarFormation } from "react-icons/gi"; import { GiStarFormation } from "react-icons/gi";
import { useUser } from "@clerk/nextjs";
import { isAdmin, isVIP } from "@/utils/helpers"; import { isAdmin, isVIP } from "@/utils/helpers";
import { currentUser } from "@clerk/nextjs";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 0;
export const fetchCache = "force-no-store";
export default async function Dashboard() {
const user = await currentUser();
const Home = () => {
return ( return (
<div className="flex flex-col text-center items-center justify-center px-4 py-16 gap-4"> <div className="flex flex-col text-center items-center justify-center px-4 py-16 gap-4">
<HomePageBody />
</div>
);
};
export default Home;
const HomePageBody = () => {
const { isLoaded, user } = useUser();
const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>("");
const [tabIndex, setTabIndex] = useState<number>();
useEffect(() => {
const tabIndexLocal = localStorage.getItem(`dashboardTabIndex`);
setTabIndex(tabIndexLocal !== null ? Number(tabIndexLocal) : 0);
}, [tabIndex, user]);
return !isLoaded ? (
<div className="flex items-center justify-center">
<span className="loading loading-dots loading-lg"></span>
</div>
) : (
<>
<h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto"> <h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto">
Hi, {user?.fullName}!{" "} Hi, {user?.firstName}!{" "}
{isAdmin(user?.publicMetadata) && ( {isAdmin(user?.publicMetadata) && (
<FaShieldAlt className="inline-block text-primary" /> <FaShieldAlt className="inline-block text-primary" />
)} )}
@ -46,52 +22,8 @@ const HomePageBody = () => {
<GiStarFormation className="inline-block text-secondary" /> <GiStarFormation className="inline-block text-secondary" />
)} )}
</h1> </h1>
<div className="tabs tabs-boxed border-2 border-cyan-500 mb-4">
<a
className={
tabIndex === 0 ? "tab no-underline tab-active" : "tab no-underline"
}
onClick={() => {
setTabIndex(0);
localStorage.setItem("dashboardTabIndex", "0");
}}
>
Join a Room
</a>
<a
className={
tabIndex === 1 ? "tab no-underline tab-active" : "tab no-underline"
}
onClick={() => {
setTabIndex(1);
localStorage.setItem("dashboardTabIndex", "1");
}}
>
Room List
</a>
</div>
{tabIndex === 0 && ( {user && <RoomList userId={user?.id} />}
<> </div>
<input
type="text"
placeholder="Enter Room ID"
className="input input-bordered input-primary mb-4"
onChange={(event) => {
console.log(event.target.value);
setJoinRoomTextBox(event.target.value);
}}
/>
<Link
href={joinRoomTextBox.length > 0 ? `/room/${joinRoomTextBox}` : "/"}
className="btn btn-secondary"
>
Join Room
</Link>
</>
)}
{tabIndex === 1 && <RoomList />}
</>
); );
}; }

View file

@ -1,4 +1,4 @@
import { ClerkProvider } from "@clerk/nextjs"; import { ClerkLoaded, ClerkProvider } from "@clerk/nextjs";
import Footer from "@/app/_components/Footer"; import Footer from "@/app/_components/Footer";
import Header from "@/app/_components/Header"; import Header from "@/app/_components/Header";
import "@/styles/globals.css"; import "@/styles/globals.css";
@ -18,11 +18,13 @@ export default function RootLayout({
<ClerkProvider> <ClerkProvider>
<html lang="en" className="h-[100%] w-[100%] fixed overflow-y-auto"> <html lang="en" className="h-[100%] w-[100%] fixed overflow-y-auto">
<body className="h-[100%] w-[100%] fixed overflow-y-auto"> <body className="h-[100%] w-[100%] fixed overflow-y-auto">
<Header title="Sprint Padawan" /> <ClerkLoaded>
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]"> <Header title="Sprint Padawan" />
<Provider>{children}</Provider> <div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
</div> <Provider>{children}</Provider>
<Footer /> </div>
<Footer />
</ClerkLoaded>
</body> </body>
</html> </html>
</ClerkProvider> </ClerkProvider>

View file

@ -1,472 +1,23 @@
"use client"; import { currentUser } from "@clerk/nextjs";
import Loading from "@/app/_components/Loading";
import Image from "next/image"; import VoteUI from "@/app/_components/VoteUI";
import { useEffect, useState } from "react";
import { EventTypes } from "@/utils/types";
import { useParams } from "next/navigation";
import {
IoCheckmarkCircleOutline,
IoCopyOutline,
IoDownloadOutline,
IoEyeOffOutline,
IoEyeOutline,
IoHourglassOutline,
IoReloadOutline,
IoSaveOutline,
} from "react-icons/io5";
import { GiStarFormation } from "react-icons/gi";
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
import Link from "next/link";
import { FaShieldAlt } from "react-icons/fa";
import { RiVipCrownFill } from "react-icons/ri";
import { env } from "@/env.mjs";
import { downloadCSV, isAdmin, isVIP } from "@/utils/helpers";
import type { PresenceItem } from "@/utils/types";
import { useUser } from "@clerk/nextjs";
import { trpc } from "@/app/_trpc/client";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
const Room = () => { export default async function Room() {
const { isSignedIn } = useUser(); const user = await currentUser();
const shapedUser = {
id: user?.id,
firstName: user?.firstName,
lastName: user?.lastName,
imageUrl: user?.imageUrl,
publicMetadata: user?.publicMetadata,
};
return ( return (
<div className="flex flex-col items-center justify-center text-center gap-2"> <div className="flex flex-col items-center justify-center text-center gap-2">
{!isSignedIn ? ( {user ? <VoteUI user={shapedUser} /> : <Loading />}
<div className="flex items-center justify-center">
<span className="loading loading-dots loading-lg"></span>
</div>
) : (
<RoomBody />
)}
</div> </div>
); );
}; }
export default Room;
const RoomBody = ({}) => {
const { isSignedIn, user } = useUser();
const params = useParams();
const roomId = params?.id as string;
const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(false);
const { data: roomFromDb, refetch: refetchRoomFromDb } =
trpc.room.get.useQuery({ id: roomId });
const { data: votesFromDb, refetch: refetchVotesFromDb } =
trpc.vote.getAllByRoomId.useQuery({ roomId });
const setVoteInDb = trpc.vote.set.useMutation({});
const setRoomInDb = trpc.room.set.useMutation({});
configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
clientId: user?.id,
recover: (_, cb) => {
cb(true);
},
});
const [channel] = useChannel(
{
channelName: `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
},
({ name }) => {
if (name === EventTypes.ROOM_UPDATE) {
void refetchVotesFromDb();
void refetchRoomFromDb();
} else if (name === EventTypes.VOTE_UPDATE) {
void refetchVotesFromDb();
}
}
);
const [presenceData] = usePresence<PresenceItem>(
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
{
name: user?.fullName || "",
image: user?.imageUrl || "",
client_id: user?.id || "",
isAdmin: isAdmin(user?.publicMetadata),
isVIP: isVIP(user?.publicMetadata),
}
);
// 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]);
// Init story name
useEffect(() => {
if (isSignedIn && roomFromDb) {
setStoryNameText(roomFromDb.storyName || "");
setRoomScale(roomFromDb.scale || "ERROR");
}
}, [roomFromDb, roomId, isSignedIn, user]);
// Helper functions
const getVoteForCurrentUser = () => {
if (roomFromDb && isSignedIn) {
return votesFromDb && votesFromDb.find((vote) => vote.userId === user.id);
} 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 = () => {
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,
storyName: item.storyName,
scale: item.scale,
votes: item.votes,
};
})
.concat({
id: "LATEST",
created_at: new Date(),
userId: roomFromDb.userId,
roomId: roomFromDb.id,
roomName: roomFromDb.roomName,
storyName: storyNameText,
scale: roomScale,
votes: votesFromDb.map((vote) => {
return {
value: vote.value,
};
}),
});
downloadCSV(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 <p>{matchedVote.value}</p>;
} 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">
<span className="loading loading-dots loading-lg"></span>{" "}
</div>
);
// Room has been loaded
} else if (roomFromDb) {
return (
<span className="text-center">
<div className="text-2xl">{roomFromDb.roomName}</div>
<div className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md mx-auto">
<div>ID:</div>
<div>{roomFromDb.id}</div>
<button>
{copied ? (
<IoCheckmarkCircleOutline className="mx-1 text-green-400 animate-bounce" />
) : (
<IoCopyOutline
className="mx-1 hover:text-primary"
onClick={copyRoomURLHandler}
></IoCopyOutline>
)}
</button>
</div>
{roomFromDb && (
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
<div className="card-body">
<h2 className="card-title mx-auto">
Story: {roomFromDb.storyName}
</h2>
<ul className="p-0 mx-auto flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
{presenceData &&
presenceData
.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) =>
presenceItem.clientId === value.clientId
)
)
.map((presenceItem) => {
return (
<li
key={presenceItem.clientId}
className="flex flex-row items-center justify-center gap-2"
>
<div className="w-10 rounded-full avatar mx-auto">
<Image
src={presenceItem.data.image}
alt={`${presenceItem.data.name}'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 mx-auto">
{presenceItem.data.name}{" "}
{presenceItem.data.isAdmin && (
<div
className="tooltip tooltip-primary"
data-tip="Admin"
>
<FaShieldAlt className="inline-block text-primary" />
</div>
)}{" "}
{presenceItem.data.isVIP && (
<div
className="tooltip tooltip-secondary"
data-tip="VIP"
>
<GiStarFormation className="inline-block text-secondary" />
</div>
)}{" "}
{presenceItem.clientId === roomFromDb.userId && (
<div
className="tooltip tooltip-warning"
data-tip="Room Owner"
>
<RiVipCrownFill className="inline-block text-yellow-500" />
</div>
)}
{" : "}
</p>
{roomFromDb &&
votesFromDb &&
voteString(
roomFromDb.visible,
votesFromDb,
presenceItem.data
)}
</li>
);
})}
</ul>
<div className="join md:btn-group-horizontal mx-auto">
{roomFromDb.scale.split(",").map((scaleItem, index) => {
return (
<button
key={index}
className={`join-item ${
getVoteForCurrentUser()?.value === scaleItem
? "btn btn-active btn-primary"
: "btn"
}`}
onClick={() => setVote(scaleItem)}
>
{scaleItem}
</button>
);
})}
</div>
</div>
</div>
)}
{isSignedIn &&
!!roomFromDb &&
(roomFromDb.userId === user.id || isAdmin(user?.publicMetadata)) && (
<>
<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">
{"Vote Scale (Comma Separated):"}{" "}
</label>
<input
type="text"
placeholder="Scale (Comma Separated)"
className="input input-bordered m-auto"
value={roomScale}
onChange={(event) => {
setRoomScale(event.target.value);
}}
/>
<label className="label mx-auto">{"Story Name:"} </label>
<input
type="text"
placeholder="Story Name"
className="input input-bordered m-auto"
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={() => saveRoom(!roomFromDb.visible, false)}
className="btn btn-primary inline-flex"
>
{roomFromDb.visible ? (
<>
<IoEyeOffOutline className="text-xl mr-1" />
Hide
</>
) : (
<>
<IoEyeOutline className="text-xl mr-1" />
Show
</>
)}
</button>
</div>
<div>
<button
onClick={() =>
saveRoom(
false,
true,
roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true
)
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
{roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
<>
<IoReloadOutline className="text-xl mr-1" /> Reset
</>
) : (
<>
<IoSaveOutline className="text-xl mr-1" /> Save
</>
)}
</button>
</div>
{votesFromDb &&
(roomFromDb.logs.length > 0 ||
votesFromDb.length > 0) && (
<div>
<button
onClick={() => downloadLogs()}
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<IoDownloadOutline className="text-xl" />
</>
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</span>
);
// Room does not exist
} else {
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."
href="/"
className="btn btn-secondary normal-case text-xl m-2"
>
Back to Home
</Link>
</span>
);
}
};