Mostly working minus presence
This commit is contained in:
parent
66e20bc6ea
commit
6b93e71595
16 changed files with 512 additions and 150 deletions
15
app/root.tsx
15
app/root.tsx
|
@ -1,5 +1,5 @@
|
||||||
import { rootAuthLoader } from "@clerk/remix/ssr.server";
|
import { rootAuthLoader } from "@clerk/remix/ssr.server";
|
||||||
import { ClerkApp, ClerkErrorBoundary } from "@clerk/remix";
|
import { ClerkApp, ClerkErrorBoundary, ClerkLoaded } from "@clerk/remix";
|
||||||
import type {
|
import type {
|
||||||
LinksFunction,
|
LinksFunction,
|
||||||
LoaderFunction,
|
LoaderFunction,
|
||||||
|
@ -42,11 +42,14 @@ function App() {
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<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"} />
|
||||||
<Outlet />
|
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
||||||
</div>
|
<Outlet />
|
||||||
<Footer />
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</ClerkLoaded>
|
||||||
|
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
<LiveReload />
|
<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) {
|
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||||
const { userId } = await getAuth({ context, params, request });
|
const { userId } = await getAuth({ context, params, request });
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return json("Not Signed In!", {
|
||||||
|
status: 403,
|
||||||
|
statusText: "UNAUTHORIZED!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
|
|
||||||
const room = await db
|
const room = await db
|
||||||
|
|
|
@ -8,6 +8,13 @@ import { rooms } from "~/services/schema";
|
||||||
export async function action({ request, params, context }: ActionFunctionArgs) {
|
export async function action({ request, params, context }: ActionFunctionArgs) {
|
||||||
const { userId } = await getAuth({ context, params, request });
|
const { userId } = await getAuth({ context, params, request });
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return json("Not Signed In!", {
|
||||||
|
status: 403,
|
||||||
|
statusText: "UNAUTHORIZED!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const roomId = params.roomId;
|
const roomId = params.roomId;
|
||||||
|
|
||||||
if (!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) {
|
return eventStream(request.signal, function setup(send) {
|
||||||
async function handler() {
|
async function handler() {
|
||||||
const roomList = await db.query.rooms.findMany({
|
const roomFromDb = await db.query.rooms.findFirst({
|
||||||
where: eq(rooms.userId, userId || ""),
|
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
|
// Initial fetch
|
||||||
|
console.log("HI");
|
||||||
db.query.rooms
|
db.query.rooms
|
||||||
.findMany({
|
.findFirst({
|
||||||
where: eq(rooms.userId, userId || ""),
|
where: eq(rooms.id, roomId || ""),
|
||||||
|
with: {
|
||||||
|
logs: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then((roomList) => {
|
.then((roomFromDb) => {
|
||||||
send({ event: roomId, data: JSON.stringify(roomList) });
|
console.log(roomId);
|
||||||
|
return send({
|
||||||
|
event: `room-${roomId}`,
|
||||||
|
data: JSON.stringify(roomFromDb),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("roomlist", handler);
|
emitter.on("room", handler);
|
||||||
|
|
||||||
return function clear() {
|
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 { getAuth } from "@clerk/remix/ssr.server";
|
||||||
import { LoaderFunction, redirect } from "@remix-run/node";
|
import { LoaderFunction, redirect } from "@remix-run/node";
|
||||||
import { Link, useParams } from "@remix-run/react";
|
import { Link, useParams } from "@remix-run/react";
|
||||||
import { AblyProvider, useChannel, usePresence } from "ably/react";
|
|
||||||
import * as Ably from "ably";
|
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
@ -19,13 +17,8 @@ import {
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||||
import { useEventSource } from "remix-utils/sse/react";
|
import { useEventSource } from "remix-utils/sse/react";
|
||||||
import {
|
import { PresenceItem, RoomResponse, VoteResponse } from "~/services/types";
|
||||||
EventTypes,
|
import { isAdmin, jsonToCsv } from "~/services/helpers";
|
||||||
PresenceItem,
|
|
||||||
RoomResponse,
|
|
||||||
VoteResponse,
|
|
||||||
} from "~/services/types";
|
|
||||||
import { isAdmin, isVIP, jsonToCsv } from "~/services/helpers";
|
|
||||||
import { useUser } from "@clerk/remix";
|
import { useUser } from "@clerk/remix";
|
||||||
|
|
||||||
export const loader: LoaderFunction = async (args) => {
|
export const loader: LoaderFunction = async (args) => {
|
||||||
|
@ -37,21 +30,32 @@ export const loader: LoaderFunction = async (args) => {
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
function RoomContent() {
|
export default function Room() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const roomId = params.roomId;
|
const roomId = params.roomId;
|
||||||
|
|
||||||
let roomFromDb = useEventSource("/api/room/get", { event: params.roomId });
|
let roomFromDb = useEventSource(`/api/room/get/${roomId}`, {
|
||||||
let votesFromDb = useEventSource("/api/votes/get/all", {
|
event: `room-${params.roomId}`,
|
||||||
event: params.roomId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let roomFromDbParsed = JSON.parse(roomFromDb!) as RoomResponse;
|
let votesFromDb = useEventSource(`/api/votes/get/${roomId}`, {
|
||||||
let votesFromDbParsed = JSON.parse(votesFromDb!) as VoteResponse;
|
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 [storyNameText, setStoryNameText] = useState<string>("");
|
||||||
const [roomScale, setRoomScale] = useState<string>("");
|
const [roomScale, setRoomScale] = useState<string>("");
|
||||||
|
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
|
@ -76,7 +80,7 @@ function RoomContent() {
|
||||||
|
|
||||||
async function setVoteHandler(value: string) {
|
async function setVoteHandler(value: string) {
|
||||||
if (roomFromDb) {
|
if (roomFromDb) {
|
||||||
await fetch(`/api/internal/room/${roomId}/vote`, {
|
await fetch(`/api/vote/set/${roomId}`, {
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -92,7 +96,7 @@ function RoomContent() {
|
||||||
log: boolean | undefined;
|
log: boolean | undefined;
|
||||||
}) {
|
}) {
|
||||||
if (roomFromDb) {
|
if (roomFromDb) {
|
||||||
await fetch(`/api/internal/room/${roomId}`, {
|
await fetch(`/api/room/set/${roomId}`, {
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -174,7 +178,7 @@ function RoomContent() {
|
||||||
presenceItem: PresenceItem
|
presenceItem: PresenceItem
|
||||||
) => {
|
) => {
|
||||||
const matchedVote = votes?.find(
|
const matchedVote = votes?.find(
|
||||||
(vote) => vote.userId === presenceItem.client_id
|
(vote) => vote.userId === presenceItem.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
@ -192,31 +196,6 @@ function RoomContent() {
|
||||||
|
|
||||||
// Hooks
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (roomFromDb) {
|
if (roomFromDb) {
|
||||||
setStoryNameText(roomFromDbParsed?.storyName || "");
|
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">
|
<ul className="p-0 flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
|
||||||
{presenceData &&
|
{presenceData &&
|
||||||
presenceData
|
presenceDateParsed
|
||||||
.filter(
|
?.filter(
|
||||||
(value, index, self) =>
|
(value, index, self) =>
|
||||||
index ===
|
index ===
|
||||||
self.findIndex(
|
self.findIndex(
|
||||||
(presenceItem) =>
|
(presenceItem) => presenceItem.userId === value.userId
|
||||||
presenceItem.clientId === value.clientId
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((presenceItem) => {
|
.map((presenceItem) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={presenceItem.clientId}
|
key={presenceItem.userId}
|
||||||
className="flex flex-row items-center justify-center gap-2"
|
className="flex flex-row items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<div className="w-10 rounded-full avatar">
|
<div className="w-10 rounded-full avatar">
|
||||||
<img
|
<img
|
||||||
src={presenceItem.data.image}
|
src={presenceItem.userImageUrl}
|
||||||
alt={`${presenceItem.data.name}'s Profile Picture`}
|
alt={`${presenceItem.userFullName}'s Profile Picture`}
|
||||||
height={32}
|
height={32}
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
|
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
|
||||||
{presenceItem.data.name}{" "}
|
{presenceItem.userFullName}{" "}
|
||||||
{presenceItem.data.isAdmin && (
|
{presenceItem.isAdmin && (
|
||||||
<span
|
<span
|
||||||
className="tooltip tooltip-primary"
|
className="tooltip tooltip-primary"
|
||||||
data-tip="Admin"
|
data-tip="Admin"
|
||||||
|
@ -293,7 +271,7 @@ function RoomContent() {
|
||||||
<ShieldIcon className="inline-block text-primary" />
|
<ShieldIcon className="inline-block text-primary" />
|
||||||
</span>
|
</span>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{presenceItem.data.isVIP && (
|
{presenceItem.isVIP && (
|
||||||
<span
|
<span
|
||||||
className="tooltip tooltip-secondary"
|
className="tooltip tooltip-secondary"
|
||||||
data-tip="VIP"
|
data-tip="VIP"
|
||||||
|
@ -301,7 +279,7 @@ function RoomContent() {
|
||||||
<StarIcon className="inline-block text-secondary" />
|
<StarIcon className="inline-block text-secondary" />
|
||||||
</span>
|
</span>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{presenceItem.clientId ===
|
{presenceItem.userId ===
|
||||||
roomFromDbParsed?.userId && (
|
roomFromDbParsed?.userId && (
|
||||||
<span
|
<span
|
||||||
className="tooltip tooltip-warning"
|
className="tooltip tooltip-warning"
|
||||||
|
@ -318,7 +296,7 @@ function RoomContent() {
|
||||||
voteString(
|
voteString(
|
||||||
roomFromDbParsed?.visible!,
|
roomFromDbParsed?.visible!,
|
||||||
votesFromDbParsed,
|
votesFromDbParsed,
|
||||||
presenceItem.data
|
presenceItem
|
||||||
)}
|
)}
|
||||||
</li>
|
</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],
|
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 type EventType = BetterEnum<typeof EventTypes>;
|
||||||
|
|
||||||
export interface PresenceItem {
|
export interface PresenceItem {
|
||||||
name: string;
|
id: string;
|
||||||
image: string;
|
userId: string;
|
||||||
client_id: string;
|
userFullName: string;
|
||||||
|
userImageUrl: string;
|
||||||
|
roomId: string;
|
||||||
|
value: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isVIP: boolean;
|
isVIP: boolean;
|
||||||
}
|
}
|
||||||
|
@ -66,11 +69,3 @@ export type VoteResponse =
|
||||||
}[]
|
}[]
|
||||||
| null
|
| null
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export type AblyTokenResponse = {
|
|
||||||
token: string;
|
|
||||||
issued: number;
|
|
||||||
expires: number;
|
|
||||||
capability: string;
|
|
||||||
clientId: string;
|
|
||||||
};
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"ably": "1.2.47",
|
"ably": "1.2.47",
|
||||||
"csv42": "^5.0.0",
|
"csv42": "^5.0.0",
|
||||||
"drizzle-orm": "^0.29.0",
|
"drizzle-orm": "^0.29.0",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
"isbot": "^3.7.1",
|
"isbot": "^3.7.1",
|
||||||
"lucide-react": "^0.292.0",
|
"lucide-react": "^0.292.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
|
@ -35,6 +35,9 @@ dependencies:
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.29.0
|
specifier: ^0.29.0
|
||||||
version: 0.29.0(@libsql/client@0.4.0-pre.2)(better-sqlite3@9.1.1)
|
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:
|
isbot:
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
version: 3.7.1
|
version: 3.7.1
|
||||||
|
@ -1333,6 +1336,10 @@ packages:
|
||||||
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
|
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@ioredis/commands@1.2.0:
|
||||||
|
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@isaacs/cliui@8.0.2:
|
/@isaacs/cliui@8.0.2:
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -2828,6 +2835,11 @@ packages:
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
dev: true
|
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:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3034,7 +3046,6 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/decode-named-character-reference@1.0.2:
|
/decode-named-character-reference@1.0.2:
|
||||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||||
|
@ -3127,6 +3138,11 @@ packages:
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/denque@2.1.0:
|
||||||
|
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/depd@2.0.0:
|
/depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
@ -4721,6 +4737,23 @@ packages:
|
||||||
side-channel: 1.0.4
|
side-channel: 1.0.4
|
||||||
dev: true
|
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:
|
/ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
@ -5180,6 +5213,14 @@ packages:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
dev: true
|
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:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -5907,7 +5948,6 @@ packages:
|
||||||
|
|
||||||
/ms@2.1.2:
|
/ms@2.1.2:
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ms@2.1.3:
|
/ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
@ -6739,6 +6779,18 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
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:
|
/reflect.getprototypeof@1.0.4:
|
||||||
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
|
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -7211,6 +7263,10 @@ packages:
|
||||||
get-source: 2.0.12
|
get-source: 2.0.12
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/standard-as-callback@2.1.0:
|
||||||
|
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/statuses@2.0.1:
|
/statuses@2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
Loading…
Add table
Reference in a new issue