Merge branch 'dev'
This commit is contained in:
commit
9f2957db18
7 changed files with 527 additions and 577 deletions
|
@ -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>
|
||||||
|
|
7
src/app/_components/Loading.tsx
Normal file
7
src/app/_components/Loading.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
return <Loading />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
464
src/app/_components/VoteUI.tsx
Normal file
464
src/app/_components/VoteUI.tsx
Normal 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">4️⃣0️⃣4️⃣</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;
|
|
@ -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 />}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">4️⃣0️⃣4️⃣</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue