diff --git a/app/routes/api.ably.tsx b/app/routes/api.ably.tsx new file mode 100644 index 0000000..6489f40 --- /dev/null +++ b/app/routes/api.ably.tsx @@ -0,0 +1,55 @@ +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", + }); +} diff --git a/app/routes/api.room.create.tsx b/app/routes/api.room.create.tsx index f21f58e..41b8728 100644 --- a/app/routes/api.room.create.tsx +++ b/app/routes/api.room.create.tsx @@ -1,7 +1,7 @@ import { getAuth } from "@clerk/remix/ssr.server"; import { ActionFunctionArgs, json } from "@remix-run/node"; import { createId } from "@paralleldrive/cuid2"; -import { db } from "~/services/db"; +import { db } from "~/services/db.server"; import { emitter } from "~/services/emitter.server"; import { rooms } from "~/services/schema"; diff --git a/app/routes/api.room.delete.$roomId.tsx b/app/routes/api.room.delete.$roomId.tsx index 98a6c5c..84cefae 100644 --- a/app/routes/api.room.delete.$roomId.tsx +++ b/app/routes/api.room.delete.$roomId.tsx @@ -1,7 +1,7 @@ import { getAuth } from "@clerk/remix/ssr.server"; import { ActionFunctionArgs, json } from "@remix-run/node"; import { eq } from "drizzle-orm"; -import { db } from "~/services/db"; +import { db } from "~/services/db.server"; import { emitter } from "~/services/emitter.server"; import { rooms } from "~/services/schema"; diff --git a/app/routes/api.room.get.$roomId.tsx b/app/routes/api.room.get.$roomId.tsx new file mode 100644 index 0000000..2c2f171 --- /dev/null +++ b/app/routes/api.room.get.$roomId.tsx @@ -0,0 +1,45 @@ +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 { rooms } 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!", + }); + } + + return eventStream(request.signal, function setup(send) { + async function handler() { + const roomList = await db.query.rooms.findMany({ + where: eq(rooms.userId, userId || ""), + }); + send({ event: roomId, data: JSON.stringify(roomList) }); + } + + // Initial fetch + db.query.rooms + .findMany({ + where: eq(rooms.userId, userId || ""), + }) + .then((roomList) => { + send({ event: roomId, data: JSON.stringify(roomList) }); + }); + + emitter.on("roomlist", handler); + + return function clear() { + emitter.off("roomlist", handler); + }; + }); +} diff --git a/app/routes/api.room.get.all.tsx b/app/routes/api.room.get.all.tsx index fc20888..2941158 100644 --- a/app/routes/api.room.get.all.tsx +++ b/app/routes/api.room.get.all.tsx @@ -1,8 +1,8 @@ import { getAuth } from "@clerk/remix/ssr.server"; -import { LoaderFunctionArgs } from "@remix-run/node"; +import { LoaderFunctionArgs, json } from "@remix-run/node"; import { eq } from "drizzle-orm"; import { eventStream } from "remix-utils/sse/server"; -import { db } from "~/services/db"; +import { db } from "~/services/db.server"; import { emitter } from "~/services/emitter.server"; import { rooms } from "~/services/schema"; @@ -10,12 +10,20 @@ import { rooms } from "~/services/schema"; 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!", + }); + } + return eventStream(request.signal, function setup(send) { async function handler() { const roomList = await db.query.rooms.findMany({ where: eq(rooms.userId, userId || ""), }); - send({ event: "roomlist", data: JSON.stringify(roomList) }); + + send({ event: userId!, data: JSON.stringify(roomList) }); } // Initial fetch @@ -24,7 +32,7 @@ export async function loader({ context, params, request }: LoaderFunctionArgs) { where: eq(rooms.userId, userId || ""), }) .then((roomList) => { - send({ event: "roomlist", data: JSON.stringify(roomList) }); + send({ event: userId!, data: JSON.stringify(roomList) }); }); emitter.on("roomlist", handler); diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index 069b37f..310f8fe 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -1,4 +1,3 @@ -import { useUser } from "@clerk/remix"; import { getAuth } from "@clerk/remix/ssr.server"; import { LoaderFunction, redirect } from "@remix-run/node"; import { Link } from "@remix-run/react"; @@ -6,6 +5,7 @@ import { LogInIcon, TrashIcon } from "lucide-react"; import { useState } from "react"; import LoadingIndicator from "~/components/LoadingIndicator"; import { useEventSource } from "remix-utils/sse/react"; +import { useAuth } from "@clerk/remix"; export const loader: LoaderFunction = async (args) => { const { userId } = await getAuth(args); @@ -34,8 +34,9 @@ type RoomsResponse = | null | undefined; -export default function Index() { - let roomsFromDb = useEventSource("/api/room/get/all", { event: "roomlist" }); +export default function Dashboard() { + const { userId } = useAuth(); + let roomsFromDb = useEventSource("/api/room/get/all", { event: userId! }); let roomsFromDbParsed = JSON.parse(roomsFromDb!) as RoomsResponse; diff --git a/app/routes/room.$roomId.tsx b/app/routes/room.$roomId.tsx new file mode 100644 index 0000000..b86887e --- /dev/null +++ b/app/routes/room.$roomId.tsx @@ -0,0 +1,479 @@ +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 {} from "ably/react"; +import * as Ably from "ably"; +import { + CheckCircleIcon, + CopyIcon, + CrownIcon, + DownloadIcon, + EyeIcon, + EyeOffIcon, + HourglassIcon, + RefreshCwIcon, + SaveIcon, + ShieldIcon, + StarIcon, +} from "lucide-react"; +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 { useUser } from "@clerk/remix"; + +export const loader: LoaderFunction = async (args) => { + const { userId } = await getAuth(args); + + if (!userId) { + return redirect("/sign-in"); + } + return {}; +}; + +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 roomFromDbParsed = JSON.parse(roomFromDb!) as RoomResponse; + let votesFromDbParsed = JSON.parse(votesFromDb!) as VoteResponse; + + const [storyNameText, setStoryNameText] = useState(""); + const [roomScale, setRoomScale] = useState(""); + const [copied, setCopied] = useState(false); + + // Handlers + // ================================= + async function getRoomHandler() { + const response = await fetch(`/api/internal/room/${roomId}`, { + cache: "no-cache", + method: "GET", + }); + + return (await response.json()) as RoomResponse; + } + + async function getVotesHandler() { + const dbVotesResponse = await fetch(`/api/internal/room/${roomId}/votes`, { + cache: "no-cache", + method: "GET", + }); + const dbVotes = (await dbVotesResponse.json()) as VoteResponse; + return dbVotes; + } + + async function setVoteHandler(value: string) { + if (roomFromDb) { + await fetch(`/api/internal/room/${roomId}/vote`, { + cache: "no-cache", + method: "PUT", + body: JSON.stringify({ + value, + }), + }); + } + } + + async function setRoomHandler(data: { + visible: boolean; + reset: boolean | undefined; + log: boolean | undefined; + }) { + if (roomFromDb) { + await fetch(`/api/internal/room/${roomId}`, { + cache: "no-cache", + method: "PUT", + body: JSON.stringify({ + name: storyNameText, + visible: data.visible, + scale: roomScale, + reset: data.reset ? data.reset : false, + log: data.log ? data.log : false, + }), + }); + } + } + + // Helpers + // ================================= + const getVoteForCurrentUser = () => { + if (roomFromDb) { + return ( + votesFromDbParsed && + votesFromDbParsed.find((vote) => vote.userId === user?.id) + ); + } else { + return null; + } + }; + + const downloadLogs = () => { + if (roomFromDb && votesFromDb) { + const jsonObject = roomFromDbParsed?.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: roomFromDbParsed.userId, + roomId: roomFromDbParsed.id, + roomName: roomFromDbParsed.roomName, + storyName: storyNameText, + scale: roomScale, + votes: votesFromDbParsed?.map((vote) => { + return { + value: vote.value, + }; + }), + }); + + jsonToCsv(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 votesFromDbParsed, + presenceItem: PresenceItem + ) => { + const matchedVote = votes?.find( + (vote) => vote.userId === presenceItem.client_id + ); + + if (visible) { + if (!!matchedVote) { + return
{matchedVote.value}
; + } else { + return ; + } + } else if (!!matchedVote) { + return ; + } else { + return ; + } + }; + + // 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( + `${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 || ""); + setRoomScale(roomFromDbParsed?.scale || "ERROR"); + } + }, [roomFromDb]); + + // UI + // ================================= + // Room is loading + if (!roomFromDbParsed) { + return ; + // Room has been loaded + } else { + return roomFromDb ? ( +
+
{roomFromDbParsed.roomName}
+
+
ID:
+
{roomFromDbParsed.id}
+ + +
+ + {roomFromDb && ( +
+
+

+ Story: {roomFromDbParsed.storyName} +

+ +
    + {presenceData && + presenceData + .filter( + (value, index, self) => + index === + self.findIndex( + (presenceItem) => + presenceItem.clientId === value.clientId + ) + ) + .map((presenceItem) => { + return ( +
  • +
    + {`${presenceItem.data.name}'s +
    + +

    + {presenceItem.data.name}{" "} + {presenceItem.data.isAdmin && ( + + + + )}{" "} + {presenceItem.data.isVIP && ( + + + + )}{" "} + {presenceItem.clientId === + roomFromDbParsed?.userId && ( + + + + )} + {" : "} +

    + + {roomFromDb && + votesFromDb && + voteString( + roomFromDbParsed?.visible!, + votesFromDbParsed, + presenceItem.data + )} +
  • + ); + })} +
+ +
+ {roomFromDbParsed.scale?.split(",").map((scaleItem, index) => { + return ( + + ); + })} +
+
+
+ )} + + {!!roomFromDbParsed && + (roomFromDbParsed.userId === user?.id || + isAdmin(user?.publicMetadata)) && ( + <> +
+
+

Room Settings

+ + + + { + setRoomScale(event.target.value); + }} + /> + + + + { + setStoryNameText(event.target.value); + }} + /> + +
+
+ +
+ +
+ +
+ + {votesFromDb && + (roomFromDbParsed?.logs.length > 0 || + votesFromDb.length > 0) && ( +
+ +
+ )} +
+
+
+ + )} +
+ ) : ( + +

4️⃣0️⃣4️⃣

+

+ Oops! This room does not appear to exist, or may have been deleted! 😢 +

+ + Back to Home + +
+ ); + } +} diff --git a/app/services/db.ts b/app/services/db.server.ts similarity index 100% rename from app/services/db.ts rename to app/services/db.server.ts diff --git a/app/services/helpers.ts b/app/services/helpers.ts new file mode 100644 index 0000000..937ed6e --- /dev/null +++ b/app/services/helpers.ts @@ -0,0 +1,47 @@ +import { json2csv } from "csv42"; + +export const jsonToCsv = (jsonObject: Array, fileName: string) => { + const csv = json2csv(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", fileName); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + +export function isAdmin(meta: UserPublicMetadata | undefined) { + return (meta?.isAdmin as boolean | undefined) || false; +} + +export function isVIP(meta: UserPublicMetadata | undefined) { + return (meta?.isVIP as boolean | undefined) || false; +} + +export const writeToLogs = ( + level: "warn" | "info" | "error" | "success", + message: string +) => { + switch (level) { + case "info": + console.log(`[ℹ️ INFO]: ${message}`); + break; + case "warn": + console.log(`[⚠️ WARN]: ${message}`); + break; + case "error": + console.log(`[❌ ERROR]: ${message}`); + break; + case "success": + console.log(`[✅ SUCCESS]: ${message}`); + break; + + default: + console.log(`[ℹ️ INFO]: ${message}`); + break; + } +}; diff --git a/app/services/types.ts b/app/services/types.ts new file mode 100644 index 0000000..c9eb7c3 --- /dev/null +++ b/app/services/types.ts @@ -0,0 +1,76 @@ +type BetterEnum = T[keyof T]; + +export const EventTypes = { + ROOM_LIST_UPDATE: "room.list.update", + ROOM_UPDATE: "room.update", + VOTE_UPDATE: "vote.update", +} as const; +export type EventType = BetterEnum; + +export interface PresenceItem { + name: string; + image: string; + client_id: string; + isAdmin: boolean; + isVIP: boolean; +} + +export type RoomsResponse = + | { + id: string; + createdAt: Date; + roomName: string; + }[] + | { + roomName: string | null; + id: string; + created_at: Date | null; + userId: string; + storyName: string | null; + visible: boolean; + scale: string; + }[] + | null + | undefined; + +export type RoomResponse = + | { + id: string; + created_at: Date | null; + userId: string; + roomName: string | null; + storyName: string | null; + visible: boolean; + scale: string | null; + logs: { + id: string; + created_at: Date | null; + userId: string; + roomId: string; + roomName: string | null; + storyName: string | null; + scale: string | null; + votes: unknown; + }[]; + } + | undefined + | null; + +export type VoteResponse = + | { + id: string; + value: string; + created_at: Date | null; + userId: string; + roomId: string; + }[] + | null + | undefined; + +export type AblyTokenResponse = { + token: string; + issued: number; + expires: number; + capability: string; + clientId: string; +}; diff --git a/package.json b/package.json index b54f07d..611f9a5 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@remix-run/node": "^2.3.0", "@remix-run/react": "^2.3.0", "@remix-run/serve": "^2.3.0", + "ably": "1.2.47", + "csv42": "^5.0.0", "drizzle-orm": "^0.29.0", "isbot": "^3.7.1", "lucide-react": "^0.292.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3767f57..b9a25a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ dependencies: '@remix-run/serve': specifier: ^2.3.0 version: 2.3.0(typescript@5.3.2) + ably: + specifier: 1.2.47 + version: 1.2.47(react-dom@18.2.0)(react@18.2.0) + csv42: + specifier: ^5.0.0 + version: 5.0.0 drizzle-orm: specifier: ^0.29.0 version: 0.29.0(@libsql/client@0.4.0-pre.2)(better-sqlite3@9.1.1) @@ -87,6 +93,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@ably/msgpack-js@0.4.0: + resolution: {integrity: sha512-IPt/BoiQwCWubqoNik1aw/6M/DleMdrxJOUpSja6xmMRbT2p1TA8oqKWgfZabqzrq8emRNeSl/+4XABPNnW5pQ==} + dependencies: + bops: 1.0.1 + dev: false + /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1875,6 +1887,18 @@ packages: resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==} dev: true + /@sindresorhus/is@4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: false + + /@szmarczak/http-timer@4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + /@testing-library/dom@8.20.1: resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} engines: {node: '>=12'} @@ -1899,6 +1923,15 @@ packages: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: true + /@types/cacheable-request@6.0.3: + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 20.9.3 + '@types/responselike': 1.0.3 + dev: false + /@types/cookie@0.5.4: resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} @@ -1924,6 +1957,10 @@ packages: '@types/unist': 2.0.10 dev: true + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: false + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -1932,6 +1969,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 20.9.3 + dev: false + /@types/mdast@3.0.15: resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} dependencies: @@ -1986,6 +2029,12 @@ packages: csstype: 3.1.2 dev: true + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 20.9.3 + dev: false + /@types/scheduler@0.16.7: resolution: {integrity: sha512-8g25Nl3AuB1KulTlSUsUhUo/oBgBU6XIXQ+XURpeioEbEJvkO7qI4vDfREv3vJYHHzqXjcAHvoJy4pTtSQNZtA==} dev: true @@ -2201,6 +2250,28 @@ packages: requiresBuild: true optional: true + /ably@1.2.47(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-YaBr4qwRNBfb9Zb6Oolbng7oK0oDadZG0cWCOv+Wh4XOWi91JKWbcy82bzN1RKkFTT/rCFG7nZrXT5pkMroMLg==} + engines: {node: '>=5.10.x'} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + '@ably/msgpack-js': 0.4.0 + got: 11.8.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -2452,6 +2523,11 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-js@1.0.2: + resolution: {integrity: sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==} + engines: {node: '>= 0.4'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2507,6 +2583,13 @@ packages: transitivePeerDependencies: - supports-color + /bops@1.0.1: + resolution: {integrity: sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==} + dependencies: + base64-js: 1.0.2 + to-utf8: 0.0.1 + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -2589,6 +2672,24 @@ packages: unique-filename: 3.0.0 dev: true + /cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: false + + /cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: false + /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: @@ -2716,6 +2817,12 @@ packages: engines: {node: '>=6'} dev: true + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: false + /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -2851,6 +2958,10 @@ packages: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: true + /csv42@5.0.0: + resolution: {integrity: sha512-CUniGKBgHkEkpcPJYC41r8b5PJO1J80W2QmrSeuVDWkiH+bt3wcz16odu62DT/9V6AbfwnCojf1QcPOH2ibfhQ==} + dev: false + /culori@3.3.0: resolution: {integrity: sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2989,6 +3100,11 @@ packages: clone: 1.0.4 dev: true + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} @@ -4265,6 +4381,13 @@ packages: source-map: 0.6.1 dev: true + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: false + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -4384,6 +4507,23 @@ packages: dependencies: get-intrinsic: 1.2.2 + /got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + dev: false + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true @@ -4487,6 +4627,10 @@ packages: lru-cache: 7.18.3 dev: true + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -4497,6 +4641,14 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -4885,7 +5037,6 @@ packages: /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true /json-diff@0.9.0: resolution: {integrity: sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==} @@ -4948,7 +5099,6 @@ packages: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: json-buffer: 3.0.1 - dev: true /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} @@ -5066,6 +5216,11 @@ packages: tslib: 2.4.1 dev: false + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + /lru-cache@10.0.3: resolution: {integrity: sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==} engines: {node: 14 || >=16.14} @@ -5596,6 +5751,11 @@ packages: engines: {node: '>=6'} dev: true + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -5855,6 +6015,11 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + /npm-install-checks@6.3.0: resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -6032,6 +6197,11 @@ packages: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} dev: true + /p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: false + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -6458,6 +6628,11 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -6692,6 +6867,10 @@ packages: engines: {node: '>=0.10.5'} dev: true + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -6724,6 +6903,12 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: false + /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -7317,6 +7502,10 @@ packages: dependencies: is-number: 7.0.0 + /to-utf8@0.0.1: + resolution: {integrity: sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==} + dev: false + /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} diff --git a/remix.config.js b/remix.config.js index 7fac2d3..d5af6f6 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,6 +1,7 @@ /** @type {import('@remix-run/dev').AppConfig} */ export default { ignoredRouteFiles: ["**/.*"], + serverDependenciesToBundle: [/^ably\/react/], // appDirectory: "app", // assetsBuildDirectory: "public/build", // publicPath: "/build/", diff --git a/tsconfig.json b/tsconfig.json index 28cce91..b7cbef1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", "moduleResolution": "Bundler", "resolveJsonModule": true, - "target": "ES2022", + "target": "ESNext", "strict": true, "allowJs": true, "forceConsistentCasingInFileNames": true,