savibg this weird hacked together state

This commit is contained in:
Atridad Lahiji 2023-11-21 20:59:49 -07:00
parent 4c73ee78ce
commit 719b501386
No known key found for this signature in database
14 changed files with 916 additions and 13 deletions

55
app/routes/api.ably.tsx Normal file
View file

@ -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",
});
}

View file

@ -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";

View file

@ -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";

View file

@ -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);
};
});
}

View file

@ -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);

View file

@ -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;

479
app/routes/room.$roomId.tsx Normal file
View file

@ -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<string>("");
const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(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 <div>{matchedVote.value}</div>;
} else {
return <HourglassIcon className="text-xl text-error" />;
}
} else if (!!matchedVote) {
return <CheckCircleIcon className="text-xl text-success" />;
} else {
return <HourglassIcon className="text-xl animate-spin text-warning" />;
}
};
// 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 || "");
setRoomScale(roomFromDbParsed?.scale || "ERROR");
}
}, [roomFromDb]);
// UI
// =================================
// Room is loading
if (!roomFromDbParsed) {
return <LoadingIndicator />;
// Room has been loaded
} else {
return roomFromDb ? (
<div className="flex flex-col gap-4 text-center justify-center items-center">
<div className="text-2xl">{roomFromDbParsed.roomName}</div>
<div className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
<div>ID:</div>
<div>{roomFromDbParsed.id}</div>
<button>
{copied ? (
<CheckCircleIcon className="mx-1 text-success animate-bounce" />
) : (
<CopyIcon
className="mx-1 hover:text-primary"
onClick={copyRoomURLHandler}
/>
)}
</button>
</div>
{roomFromDb && (
<div className="card card-compact bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title mx-auto">
Story: {roomFromDbParsed.storyName}
</h2>
<ul className="p-0 flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
{presenceData &&
presenceData
.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) =>
presenceItem.clientId === value.clientId
)
)
.map((presenceItem) => {
return (
<li
key={presenceItem.clientId}
className="flex flex-row items-center justify-center gap-2"
>
<div className="w-10 rounded-full avatar">
<img
src={presenceItem.data.image}
alt={`${presenceItem.data.name}'s Profile Picture`}
height={32}
width={32}
/>
</div>
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
{presenceItem.data.name}{" "}
{presenceItem.data.isAdmin && (
<span
className="tooltip tooltip-primary"
data-tip="Admin"
>
<ShieldIcon className="inline-block text-primary" />
</span>
)}{" "}
{presenceItem.data.isVIP && (
<span
className="tooltip tooltip-secondary"
data-tip="VIP"
>
<StarIcon className="inline-block text-secondary" />
</span>
)}{" "}
{presenceItem.clientId ===
roomFromDbParsed?.userId && (
<span
className="tooltip tooltip-warning"
data-tip="Room Owner"
>
<CrownIcon className="inline-block text-yellow-500" />
</span>
)}
{" : "}
</p>
{roomFromDb &&
votesFromDb &&
voteString(
roomFromDbParsed?.visible!,
votesFromDbParsed,
presenceItem.data
)}
</li>
);
})}
</ul>
<div className="join md:btn-group-horizontal mx-auto">
{roomFromDbParsed.scale?.split(",").map((scaleItem, index) => {
return (
<button
key={index}
className={`join-item ${
getVoteForCurrentUser()?.value === scaleItem
? "btn btn-active btn-primary"
: "btn"
}`}
onClick={() => void setVoteHandler(scaleItem)}
>
{scaleItem}
</button>
);
})}
</div>
</div>
</div>
)}
{!!roomFromDbParsed &&
(roomFromDbParsed.userId === user?.id ||
isAdmin(user?.publicMetadata)) && (
<>
<div className="card card-compact bg-base-100 shadow-xl">
<div className="card-body flex flex-col flex-wrap">
<h2 className="card-title">Room Settings</h2>
<label className="label">
{"Vote Scale (Comma Separated):"}{" "}
</label>
<input
type="text"
placeholder="Scale (Comma Separated)"
className="input input-bordered"
value={roomScale}
onChange={(event) => {
setRoomScale(event.target.value);
}}
/>
<label className="label">{"Story Name:"} </label>
<input
type="text"
placeholder="Story Name"
className="input input-bordered"
value={storyNameText}
onChange={(event) => {
setStoryNameText(event.target.value);
}}
/>
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
<div>
<button
onClick={() =>
void setRoomHandler({
visible: !roomFromDbParsed?.visible,
reset: false,
log: false,
})
}
className="btn btn-primary inline-flex"
>
{roomFromDbParsed.visible ? (
<>
<EyeOffIcon className="text-xl mr-1" />
Hide
</>
) : (
<>
<EyeIcon className="text-xl mr-1" />
Show
</>
)}
</button>
</div>
<div>
<button
onClick={() =>
void setRoomHandler({
visible: false,
reset: true,
log:
roomFromDbParsed?.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true,
})
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
{roomFromDbParsed?.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
<>
<RefreshCwIcon className="text-xl mr-1" /> Reset
</>
) : (
<>
<SaveIcon className="text-xl mr-1" /> Save
</>
)}
</button>
</div>
{votesFromDb &&
(roomFromDbParsed?.logs.length > 0 ||
votesFromDb.length > 0) && (
<div>
<button
onClick={() => downloadLogs()}
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<DownloadIcon className="text-xl" />
</>
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
) : (
<span className="text-center">
<h1 className="text-5xl font-bold m-2">404</h1>
<h1 className="text-5xl font-bold m-2">
Oops! This room does not appear to exist, or may have been deleted! 😢
</h1>
<Link
about="Back to home."
to="/"
className="btn btn-secondary normal-case text-xl m-2"
>
Back to Home
</Link>
</span>
);
}
}

47
app/services/helpers.ts Normal file
View file

@ -0,0 +1,47 @@
import { json2csv } from "csv42";
export const jsonToCsv = (jsonObject: Array<object>, 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;
}
};

76
app/services/types.ts Normal file
View file

@ -0,0 +1,76 @@
type BetterEnum<T> = 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<typeof EventTypes>;
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;
};

View file

@ -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",

193
pnpm-lock.yaml generated
View file

@ -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'}

View file

@ -1,6 +1,7 @@
/** @type {import('@remix-run/dev').AppConfig} */
export default {
ignoredRouteFiles: ["**/.*"],
serverDependenciesToBundle: [/^ably\/react/],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// publicPath: "/build/",

View file

@ -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,