Mostly working minus presence
This commit is contained in:
parent
66e20bc6ea
commit
6b93e71595
16 changed files with 512 additions and 150 deletions
|
@ -1,5 +1,5 @@
|
|||
import { rootAuthLoader } from "@clerk/remix/ssr.server";
|
||||
import { ClerkApp, ClerkErrorBoundary } from "@clerk/remix";
|
||||
import { ClerkApp, ClerkErrorBoundary, ClerkLoaded } from "@clerk/remix";
|
||||
import type {
|
||||
LinksFunction,
|
||||
LoaderFunction,
|
||||
|
@ -42,11 +42,14 @@ function App() {
|
|||
<Links />
|
||||
</head>
|
||||
<body className="h-[100%] w-[100%] fixed overflow-y-auto">
|
||||
<ClerkLoaded>
|
||||
<Header title={"Sprint Padawan"} />
|
||||
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Footer />
|
||||
</ClerkLoaded>
|
||||
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { LoaderFunctionArgs, json } from "@remix-run/node";
|
||||
import { AblyTokenResponse } from "~/services/types";
|
||||
|
||||
// Get Room List
|
||||
export async function loader({ context, params, request }: LoaderFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.ABLY_API_KEY) {
|
||||
return new Response(
|
||||
`Missing ABLY_API_KEY environment variable.
|
||||
If you're running locally, please ensure you have a ./.env file with a value for ABLY_API_KEY=your-key.
|
||||
If you're running in Netlify, make sure you've configured env variable ABLY_API_KEY.
|
||||
Please see README.md for more details on configuring your Ably API Key.`,
|
||||
{
|
||||
status: 500,
|
||||
statusText: `Missing ABLY_API_KEY environment variable.
|
||||
If you're running locally, please ensure you have a ./.env file with a value for ABLY_API_KEY=your-key.
|
||||
If you're running in Netlify, make sure you've configured env variable ABLY_API_KEY.
|
||||
Please see README.md for more details on configuring your Ably API Key.`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const keyName = process.env.ABLY_API_KEY!.split(":")[0];
|
||||
|
||||
const tokenResponse = await fetch(
|
||||
`https://rest.ably.io/keys/${keyName}/requestToken`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Basic ${btoa(process.env.ABLY_API_KEY!)}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
keyName,
|
||||
clientId: userId,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
const tokenResponseData = (await tokenResponse.json()) as AblyTokenResponse;
|
||||
|
||||
return json(tokenResponseData, {
|
||||
status: 200,
|
||||
statusText: "SUCCESS",
|
||||
});
|
||||
}
|
|
@ -8,6 +8,13 @@ import { rooms } from "~/services/schema";
|
|||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
const room = await db
|
||||
|
|
|
@ -8,6 +8,13 @@ import { rooms } from "~/services/schema";
|
|||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
const roomId = params.roomId;
|
||||
|
||||
if (!roomId) {
|
||||
|
|
|
@ -19,27 +19,48 @@ export async function loader({ context, params, request }: LoaderFunctionArgs) {
|
|||
});
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
return eventStream(request.signal, function setup(send) {
|
||||
async function handler() {
|
||||
const roomList = await db.query.rooms.findMany({
|
||||
where: eq(rooms.userId, userId || ""),
|
||||
const roomFromDb = await db.query.rooms.findFirst({
|
||||
where: eq(rooms.id, roomId || ""),
|
||||
with: {
|
||||
logs: true,
|
||||
},
|
||||
});
|
||||
send({
|
||||
event: `room-${roomId}`,
|
||||
data: JSON.stringify(roomFromDb),
|
||||
});
|
||||
send({ event: roomId, data: JSON.stringify(roomList) });
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
console.log("HI");
|
||||
db.query.rooms
|
||||
.findMany({
|
||||
where: eq(rooms.userId, userId || ""),
|
||||
.findFirst({
|
||||
where: eq(rooms.id, roomId || ""),
|
||||
with: {
|
||||
logs: true,
|
||||
},
|
||||
})
|
||||
.then((roomList) => {
|
||||
send({ event: roomId, data: JSON.stringify(roomList) });
|
||||
.then((roomFromDb) => {
|
||||
console.log(roomId);
|
||||
return send({
|
||||
event: `room-${roomId}`,
|
||||
data: JSON.stringify(roomFromDb),
|
||||
});
|
||||
});
|
||||
|
||||
emitter.on("roomlist", handler);
|
||||
emitter.on("room", handler);
|
||||
|
||||
return function clear() {
|
||||
emitter.off("roomlist", handler);
|
||||
emitter.off("room", handler);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
64
app/routes/api.room.presence.get.$roomId.tsx
Normal file
64
app/routes/api.room.presence.get.$roomId.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { LoaderFunctionArgs, json } from "@remix-run/node";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { eventStream } from "remix-utils/sse/server";
|
||||
import { db } from "~/services/db.server";
|
||||
import { emitter } from "~/services/emitter.server";
|
||||
import { presence, rooms } from "~/services/schema";
|
||||
|
||||
export async function loader({ context, params, request }: LoaderFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
const roomId = params.roomId;
|
||||
|
||||
if (!roomId) {
|
||||
return json("RoomId Missing!", {
|
||||
status: 400,
|
||||
statusText: "BAD REQUEST!",
|
||||
});
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
return eventStream(request.signal, function setup(send) {
|
||||
async function handler() {
|
||||
const presenceData = await db.query.presence.findMany({
|
||||
where: and(
|
||||
eq(presence.userId, userId || ""),
|
||||
eq(presence.roomId, roomId || "")
|
||||
),
|
||||
});
|
||||
|
||||
send({
|
||||
event: `${userId}-${params.roomId}`,
|
||||
data: JSON.stringify(presenceData),
|
||||
});
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
db.query.presence
|
||||
.findMany({
|
||||
where: and(
|
||||
eq(presence.userId, userId || ""),
|
||||
eq(presence.roomId, roomId || "")
|
||||
),
|
||||
})
|
||||
.then((presenceData) => {
|
||||
return send({
|
||||
event: `${userId}-${params.roomId}`,
|
||||
data: JSON.stringify(presenceData),
|
||||
});
|
||||
});
|
||||
|
||||
emitter.on("presence", handler);
|
||||
|
||||
return function clear() {
|
||||
emitter.off("presence", handler);
|
||||
};
|
||||
});
|
||||
}
|
43
app/routes/api.room.presence.join.$roomId.tsx
Normal file
43
app/routes/api.room.presence.join.$roomId.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { db } from "~/services/db.server";
|
||||
import { emitter } from "~/services/emitter.server";
|
||||
import { rooms } from "~/services/schema";
|
||||
|
||||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
const room = await db
|
||||
.insert(rooms)
|
||||
.values({
|
||||
id: `room_${createId()}`,
|
||||
created_at: Date.now().toString(),
|
||||
userId: userId || "",
|
||||
roomName: data.name,
|
||||
storyName: "First Story!",
|
||||
scale: "0.5,1,2,3,5,8",
|
||||
visible: 0,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const success = room.length > 0;
|
||||
|
||||
if (success) {
|
||||
emitter.emit("roomlist");
|
||||
|
||||
return json(room, {
|
||||
status: 200,
|
||||
statusText: "SUCCESS",
|
||||
});
|
||||
}
|
||||
}
|
42
app/routes/api.room.presence.leave.$roomId.tsx
Normal file
42
app/routes/api.room.presence.leave.$roomId.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "~/services/db.server";
|
||||
import { emitter } from "~/services/emitter.server";
|
||||
import { rooms } from "~/services/schema";
|
||||
|
||||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
const roomId = params.roomId;
|
||||
|
||||
if (!roomId) {
|
||||
return json("RoomId Missing!", {
|
||||
status: 400,
|
||||
statusText: "BAD REQUEST!",
|
||||
});
|
||||
}
|
||||
|
||||
const deletedRoom = await db
|
||||
.delete(rooms)
|
||||
.where(eq(rooms.id, roomId))
|
||||
.returning();
|
||||
|
||||
const success = deletedRoom.length > 0;
|
||||
|
||||
if (success) {
|
||||
emitter.emit("roomlist");
|
||||
|
||||
return json(deletedRoom, {
|
||||
status: 200,
|
||||
statusText: "SUCCESS",
|
||||
});
|
||||
}
|
||||
}
|
87
app/routes/api.room.set.$roomId.tsx
Normal file
87
app/routes/api.room.set.$roomId.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { db } from "~/services/db.server";
|
||||
import { emitter } from "~/services/emitter.server";
|
||||
import { logs, rooms, votes } from "~/services/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const roomId = params.roomId;
|
||||
|
||||
if (data.log) {
|
||||
const oldRoom = await db.query.rooms.findFirst({
|
||||
where: eq(rooms.id, params.roomId || ""),
|
||||
with: {
|
||||
votes: true,
|
||||
logs: true,
|
||||
},
|
||||
});
|
||||
|
||||
oldRoom &&
|
||||
(await db.insert(logs).values({
|
||||
id: `log_${createId()}`,
|
||||
created_at: Date.now().toString(),
|
||||
userId: userId || "",
|
||||
roomId: roomId || "",
|
||||
scale: oldRoom.scale,
|
||||
votes: JSON.stringify(
|
||||
oldRoom.votes.map((vote) => {
|
||||
return {
|
||||
name: vote.userId,
|
||||
value: vote.value,
|
||||
};
|
||||
})
|
||||
),
|
||||
roomName: oldRoom.roomName,
|
||||
storyName: oldRoom.storyName,
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.reset) {
|
||||
await db.delete(votes).where(eq(votes.roomId, params.roomId || ""));
|
||||
}
|
||||
|
||||
const newRoom = data.reset
|
||||
? await db
|
||||
.update(rooms)
|
||||
.set({
|
||||
storyName: data.name,
|
||||
visible: data.visible,
|
||||
scale: [...new Set(data.scale.split(","))]
|
||||
.filter((item) => item !== "")
|
||||
.toString(),
|
||||
})
|
||||
.where(eq(rooms.id, params.roomId || ""))
|
||||
.returning()
|
||||
: await db
|
||||
.update(rooms)
|
||||
.set({
|
||||
visible: data.visible,
|
||||
})
|
||||
.where(eq(rooms.id, params.roomId || ""))
|
||||
.returning();
|
||||
|
||||
const success = newRoom.length > 0;
|
||||
|
||||
if (success) {
|
||||
console.log(success);
|
||||
emitter.emit("room");
|
||||
emitter.emit("votes");
|
||||
|
||||
return json(newRoom, {
|
||||
status: 200,
|
||||
statusText: "SUCCESS",
|
||||
});
|
||||
}
|
||||
}
|
50
app/routes/api.vote.set.$roomId.tsx
Normal file
50
app/routes/api.vote.set.$roomId.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { db } from "~/services/db.server";
|
||||
import { emitter } from "~/services/emitter.server";
|
||||
import { votes } from "~/services/schema";
|
||||
|
||||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const roomId = params.roomId;
|
||||
|
||||
const upsertResult = await db
|
||||
.insert(votes)
|
||||
.values({
|
||||
id: `vote_${createId()}`,
|
||||
created_at: Date.now().toString(),
|
||||
value: data.value,
|
||||
userId: userId || "",
|
||||
roomId: roomId || "",
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [votes.userId, votes.roomId],
|
||||
set: {
|
||||
created_at: Date.now().toString(),
|
||||
value: data.value,
|
||||
userId: userId || "",
|
||||
roomId: roomId,
|
||||
},
|
||||
});
|
||||
|
||||
const success = upsertResult.rowsAffected > 0;
|
||||
|
||||
if (success) {
|
||||
emitter.emit("votes");
|
||||
|
||||
return json(upsertResult, {
|
||||
status: 200,
|
||||
statusText: "SUCCESS",
|
||||
});
|
||||
}
|
||||
}
|
55
app/routes/api.votes.get.$roomId.tsx
Normal file
55
app/routes/api.votes.get.$roomId.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { LoaderFunctionArgs, json } from "@remix-run/node";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eventStream } from "remix-utils/sse/server";
|
||||
import { db } from "~/services/db.server";
|
||||
import { emitter } from "~/services/emitter.server";
|
||||
import { votes } from "~/services/schema";
|
||||
|
||||
// Get Room List
|
||||
export async function loader({ context, params, request }: LoaderFunctionArgs) {
|
||||
const { userId } = await getAuth({ context, params, request });
|
||||
|
||||
const roomId = params.roomId;
|
||||
|
||||
if (!roomId) {
|
||||
return json("RoomId Missing!", {
|
||||
status: 400,
|
||||
statusText: "BAD REQUEST!",
|
||||
});
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return json("Not Signed In!", {
|
||||
status: 403,
|
||||
statusText: "UNAUTHORIZED!",
|
||||
});
|
||||
}
|
||||
|
||||
return eventStream(request.signal, function setup(send) {
|
||||
async function handler() {
|
||||
const votesByRoomId = await db.query.votes.findMany({
|
||||
where: eq(votes.roomId, roomId || ""),
|
||||
});
|
||||
send({ event: `votes-${roomId}`, data: JSON.stringify(votesByRoomId) });
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
db.query.votes
|
||||
.findMany({
|
||||
where: eq(votes.roomId, roomId || ""),
|
||||
})
|
||||
.then((votesByRoomId) => {
|
||||
return send({
|
||||
event: `votes-${roomId}`,
|
||||
data: JSON.stringify(votesByRoomId),
|
||||
});
|
||||
});
|
||||
|
||||
emitter.on("votes", handler);
|
||||
|
||||
return function clear() {
|
||||
emitter.off("votes", handler);
|
||||
};
|
||||
});
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
import { getAuth } from "@clerk/remix/ssr.server";
|
||||
import { LoaderFunction, redirect } from "@remix-run/node";
|
||||
import { Link, useParams } from "@remix-run/react";
|
||||
import { AblyProvider, useChannel, usePresence } from "ably/react";
|
||||
import * as Ably from "ably";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CopyIcon,
|
||||
|
@ -19,13 +17,8 @@ import {
|
|||
import { useEffect, useState } from "react";
|
||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import { useEventSource } from "remix-utils/sse/react";
|
||||
import {
|
||||
EventTypes,
|
||||
PresenceItem,
|
||||
RoomResponse,
|
||||
VoteResponse,
|
||||
} from "~/services/types";
|
||||
import { isAdmin, isVIP, jsonToCsv } from "~/services/helpers";
|
||||
import { PresenceItem, RoomResponse, VoteResponse } from "~/services/types";
|
||||
import { isAdmin, jsonToCsv } from "~/services/helpers";
|
||||
import { useUser } from "@clerk/remix";
|
||||
|
||||
export const loader: LoaderFunction = async (args) => {
|
||||
|
@ -37,21 +30,32 @@ export const loader: LoaderFunction = async (args) => {
|
|||
return {};
|
||||
};
|
||||
|
||||
function RoomContent() {
|
||||
export default function Room() {
|
||||
const { user } = useUser();
|
||||
const params = useParams();
|
||||
const roomId = params.roomId;
|
||||
|
||||
let roomFromDb = useEventSource("/api/room/get", { event: params.roomId });
|
||||
let votesFromDb = useEventSource("/api/votes/get/all", {
|
||||
event: params.roomId,
|
||||
let roomFromDb = useEventSource(`/api/room/get/${roomId}`, {
|
||||
event: `room-${params.roomId}`,
|
||||
});
|
||||
|
||||
let roomFromDbParsed = JSON.parse(roomFromDb!) as RoomResponse;
|
||||
let votesFromDbParsed = JSON.parse(votesFromDb!) as VoteResponse;
|
||||
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 = JSON.parse(roomFromDb!) as RoomResponse | 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
|
||||
|
@ -76,7 +80,7 @@ function RoomContent() {
|
|||
|
||||
async function setVoteHandler(value: string) {
|
||||
if (roomFromDb) {
|
||||
await fetch(`/api/internal/room/${roomId}/vote`, {
|
||||
await fetch(`/api/vote/set/${roomId}`, {
|
||||
cache: "no-cache",
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
|
@ -92,7 +96,7 @@ function RoomContent() {
|
|||
log: boolean | undefined;
|
||||
}) {
|
||||
if (roomFromDb) {
|
||||
await fetch(`/api/internal/room/${roomId}`, {
|
||||
await fetch(`/api/room/set/${roomId}`, {
|
||||
cache: "no-cache",
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
|
@ -174,7 +178,7 @@ function RoomContent() {
|
|||
presenceItem: PresenceItem
|
||||
) => {
|
||||
const matchedVote = votes?.find(
|
||||
(vote) => vote.userId === presenceItem.client_id
|
||||
(vote) => vote.userId === presenceItem.userId
|
||||
);
|
||||
|
||||
if (visible) {
|
||||
|
@ -192,31 +196,6 @@ function RoomContent() {
|
|||
|
||||
// Hooks
|
||||
// =================================
|
||||
useChannel(
|
||||
{
|
||||
channelName: `${process.env.APP_ENV}-${roomId}`,
|
||||
},
|
||||
({ name }: { name: string }) => {
|
||||
if (name === EventTypes.ROOM_UPDATE) {
|
||||
void getRoomHandler();
|
||||
void getVotesHandler();
|
||||
} else if (name === EventTypes.VOTE_UPDATE) {
|
||||
void getVotesHandler();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { presenceData } = usePresence<PresenceItem>(
|
||||
`${process.env.APP_ENV}-${roomId}`,
|
||||
{
|
||||
name: (user?.fullName ?? user?.username) || "",
|
||||
image: user?.imageUrl || "",
|
||||
client_id: user?.id || "unknown",
|
||||
isAdmin: isAdmin(user?.publicMetadata),
|
||||
isVIP: isVIP(user?.publicMetadata),
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (roomFromDb) {
|
||||
setStoryNameText(roomFromDbParsed?.storyName || "");
|
||||
|
@ -259,33 +238,32 @@ function RoomContent() {
|
|||
|
||||
<ul className="p-0 flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
|
||||
{presenceData &&
|
||||
presenceData
|
||||
.filter(
|
||||
presenceDateParsed
|
||||
?.filter(
|
||||
(value, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(presenceItem) =>
|
||||
presenceItem.clientId === value.clientId
|
||||
(presenceItem) => presenceItem.userId === value.userId
|
||||
)
|
||||
)
|
||||
.map((presenceItem) => {
|
||||
return (
|
||||
<li
|
||||
key={presenceItem.clientId}
|
||||
key={presenceItem.userId}
|
||||
className="flex flex-row items-center justify-center gap-2"
|
||||
>
|
||||
<div className="w-10 rounded-full avatar">
|
||||
<img
|
||||
src={presenceItem.data.image}
|
||||
alt={`${presenceItem.data.name}'s Profile Picture`}
|
||||
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.data.name}{" "}
|
||||
{presenceItem.data.isAdmin && (
|
||||
{presenceItem.userFullName}{" "}
|
||||
{presenceItem.isAdmin && (
|
||||
<span
|
||||
className="tooltip tooltip-primary"
|
||||
data-tip="Admin"
|
||||
|
@ -293,7 +271,7 @@ function RoomContent() {
|
|||
<ShieldIcon className="inline-block text-primary" />
|
||||
</span>
|
||||
)}{" "}
|
||||
{presenceItem.data.isVIP && (
|
||||
{presenceItem.isVIP && (
|
||||
<span
|
||||
className="tooltip tooltip-secondary"
|
||||
data-tip="VIP"
|
||||
|
@ -301,7 +279,7 @@ function RoomContent() {
|
|||
<StarIcon className="inline-block text-secondary" />
|
||||
</span>
|
||||
)}{" "}
|
||||
{presenceItem.clientId ===
|
||||
{presenceItem.userId ===
|
||||
roomFromDbParsed?.userId && (
|
||||
<span
|
||||
className="tooltip tooltip-warning"
|
||||
|
@ -318,7 +296,7 @@ function RoomContent() {
|
|||
voteString(
|
||||
roomFromDbParsed?.visible!,
|
||||
votesFromDbParsed,
|
||||
presenceItem.data
|
||||
presenceItem
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
@ -476,15 +454,3 @@ function RoomContent() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function Room() {
|
||||
const client = new Ably.Realtime.Promise({
|
||||
authUrl: "/api/ably",
|
||||
});
|
||||
|
||||
return (
|
||||
<AblyProvider client={client}>
|
||||
<RoomContent />
|
||||
</AblyProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,3 +73,23 @@ export const logsRelations = relations(logs, ({ one }) => ({
|
|||
references: [rooms.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const presence = sqliteTable(
|
||||
"Presence",
|
||||
{
|
||||
id: text("id", { length: 255 }).notNull().primaryKey(),
|
||||
userId: text("userId", { length: 255 }).notNull(),
|
||||
userFullName: text("userFullName", { length: 255 }).notNull(),
|
||||
userImageUrl: text("userImageUrl", { length: 255 }).notNull(),
|
||||
isVIP: integer("isVIP").default(0).notNull(),
|
||||
isAdmin: integer("isAdmin").default(0).notNull(),
|
||||
roomId: text("roomId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => rooms.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
unq: unique().on(table.userId, table.roomId),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,9 +8,12 @@ export const EventTypes = {
|
|||
export type EventType = BetterEnum<typeof EventTypes>;
|
||||
|
||||
export interface PresenceItem {
|
||||
name: string;
|
||||
image: string;
|
||||
client_id: string;
|
||||
id: string;
|
||||
userId: string;
|
||||
userFullName: string;
|
||||
userImageUrl: string;
|
||||
roomId: string;
|
||||
value: string;
|
||||
isAdmin: boolean;
|
||||
isVIP: boolean;
|
||||
}
|
||||
|
@ -66,11 +69,3 @@ export type VoteResponse =
|
|||
}[]
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export type AblyTokenResponse = {
|
||||
token: string;
|
||||
issued: number;
|
||||
expires: number;
|
||||
capability: string;
|
||||
clientId: string;
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"ably": "1.2.47",
|
||||
"csv42": "^5.0.0",
|
||||
"drizzle-orm": "^0.29.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isbot": "^3.7.1",
|
||||
"lucide-react": "^0.292.0",
|
||||
"react": "^18.2.0",
|
||||
|
|
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
|
@ -35,6 +35,9 @@ dependencies:
|
|||
drizzle-orm:
|
||||
specifier: ^0.29.0
|
||||
version: 0.29.0(@libsql/client@0.4.0-pre.2)(better-sqlite3@9.1.1)
|
||||
ioredis:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
isbot:
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
|
@ -1333,6 +1336,10 @@ packages:
|
|||
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
|
||||
dev: true
|
||||
|
||||
/@ioredis/commands@1.2.0:
|
||||
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
||||
dev: false
|
||||
|
||||
/@isaacs/cliui@8.0.2:
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -2828,6 +2835,11 @@ packages:
|
|||
engines: {node: '>=0.8'}
|
||||
dev: true
|
||||
|
||||
/cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
|
@ -3034,7 +3046,6 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/decode-named-character-reference@1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
|
@ -3127,6 +3138,11 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
dev: false
|
||||
|
||||
/depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -4721,6 +4737,23 @@ packages:
|
|||
side-channel: 1.0.4
|
||||
dev: true
|
||||
|
||||
/ioredis@5.3.2:
|
||||
resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.2.0
|
||||
cluster-key-slot: 1.1.2
|
||||
debug: 4.3.4
|
||||
denque: 2.1.0
|
||||
lodash.defaults: 4.2.0
|
||||
lodash.isarguments: 3.1.0
|
||||
redis-errors: 1.2.0
|
||||
redis-parser: 3.0.0
|
||||
standard-as-callback: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
@ -5180,6 +5213,14 @@ packages:
|
|||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
dev: true
|
||||
|
||||
/lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
dev: false
|
||||
|
||||
/lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
dev: false
|
||||
|
||||
/lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
@ -5907,7 +5948,6 @@ packages:
|
|||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
@ -6739,6 +6779,18 @@ packages:
|
|||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
/redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/redis-parser@3.0.0:
|
||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
redis-errors: 1.2.0
|
||||
dev: false
|
||||
|
||||
/reflect.getprototypeof@1.0.4:
|
||||
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -7211,6 +7263,10 @@ packages:
|
|||
get-source: 2.0.12
|
||||
dev: true
|
||||
|
||||
/standard-as-callback@2.1.0:
|
||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||
dev: false
|
||||
|
||||
/statuses@2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
Loading…
Add table
Reference in a new issue