2.3.0
This commit is contained in:
parent
c8e578f350
commit
8ee6573e70
27 changed files with 825 additions and 2006 deletions
|
@ -11,6 +11,7 @@ UPSTASH_RATELIMIT_SECONDS=""
|
||||||
#Auth
|
#Auth
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
|
||||||
CLERK_SECRET_KEY=""
|
CLERK_SECRET_KEY=""
|
||||||
|
CLERK_WEBHOOK_SIGNING_SECRET=""
|
||||||
|
|
||||||
# Ably
|
# Ably
|
||||||
ABLY_PRIVATE_KEY=""
|
ABLY_PRIVATE_KEY=""
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// @ts-check
|
import "./src/env.mjs";
|
||||||
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs"));
|
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
|
@ -16,6 +15,11 @@ const config = {
|
||||||
"img.clerk.com",
|
"img.clerk.com",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
serverActions: true,
|
||||||
|
serverMinification: true,
|
||||||
|
swcMinify: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
28
package.json
28
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sprintpadawan",
|
"name": "sprintpadawan",
|
||||||
"version": "2.2.2",
|
"version": "2.3.0",
|
||||||
"description": "Plan. Sprint. Repeat.",
|
"description": "Plan. Sprint. Repeat.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -14,50 +14,46 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ably-labs/react-hooks": "^2.1.1",
|
"@ably-labs/react-hooks": "^2.1.1",
|
||||||
"@clerk/nextjs": "^4.23.4",
|
"@clerk/nextjs": "^4.23.5",
|
||||||
"@clerk/themes": "^1.7.5",
|
"@clerk/themes": "^1.7.5",
|
||||||
"@neondatabase/serverless": "^0.6.0",
|
"@neondatabase/serverless": "^0.6.0",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@t3-oss/env-nextjs": "^0.6.1",
|
"@t3-oss/env-nextjs": "^0.6.1",
|
||||||
"@tanstack/react-query": "^4.33.0",
|
|
||||||
"@trpc/client": "10.38.1",
|
|
||||||
"@trpc/next": "10.38.1",
|
|
||||||
"@trpc/react-query": "10.38.1",
|
|
||||||
"@trpc/server": "10.38.1",
|
|
||||||
"@unkey/api": "^0.6.21",
|
"@unkey/api": "^0.6.21",
|
||||||
"@upstash/ratelimit": "^0.4.4",
|
"@upstash/ratelimit": "^0.4.4",
|
||||||
"@upstash/redis": "^1.22.0",
|
"@upstash/redis": "^1.22.0",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"csv42": "^4.0.0",
|
"csv42": "^5.0.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"drizzle-orm": "^0.28.5",
|
"drizzle-orm": "^0.28.6",
|
||||||
"next": "^13.4.19",
|
"next": "^13.4.19",
|
||||||
"nextjs-cors": "^2.1.2",
|
"nextjs-cors": "^2.1.2",
|
||||||
"postcss": "^8.4.29",
|
"postcss": "^8.4.29",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-email": "^1.9.4",
|
"react-icons": "^4.11.0",
|
||||||
"react-icons": "^4.10.1",
|
|
||||||
"sharp": "^0.32.5",
|
"sharp": "^0.32.5",
|
||||||
"superjson": "1.13.1",
|
"superjson": "1.13.1",
|
||||||
|
"svix": "^1.11.0",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.44.2",
|
"@types/eslint": "^8.44.2",
|
||||||
"@types/json2csv": "^5.0.3",
|
"@types/json2csv": "^5.0.3",
|
||||||
"@types/node": "^20.5.9",
|
"@types/node": "^20.6.0",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"bufferutil": "^4.0.7",
|
"bufferutil": "^4.0.7",
|
||||||
"daisyui": "^3.6.5",
|
"daisyui": "^3.7.3",
|
||||||
"drizzle-kit": "^0.19.13",
|
"drizzle-kit": "^0.19.13",
|
||||||
"eslint": "^8.48.0",
|
"encoding": "^0.1.13",
|
||||||
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-next": "^13.4.19",
|
"eslint-config-next": "^13.4.19",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"utf-8-validate": "6.0.3",
|
"utf-8-validate": "5.0.2",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1743
pnpm-lock.yaml
generated
1743
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
|
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import { trpc } from "../_trpc/client";
|
|
||||||
import LoadingIndicator from "./LoadingIndicator";
|
import LoadingIndicator from "./LoadingIndicator";
|
||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
|
import { createRoom, deleteRoom, getRooms } from "@/server/actions/room";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
@ -26,30 +26,50 @@ const RoomList = () => {
|
||||||
|
|
||||||
useChannel(
|
useChannel(
|
||||||
`${env.NEXT_PUBLIC_APP_ENV}-${user?.id}`,
|
`${env.NEXT_PUBLIC_APP_ENV}-${user?.id}`,
|
||||||
() => void refetchRoomsFromDb()
|
() => void getRoomsHandler()
|
||||||
);
|
);
|
||||||
|
|
||||||
const [roomName, setRoomName] = useState<string>("");
|
const [roomName, setRoomName] = useState<string>("");
|
||||||
|
const [roomsFromDb, setRoomsFromDb] = useState<
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
roomName: string;
|
||||||
|
}[]
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
created_at: Date | null;
|
||||||
|
userId: string;
|
||||||
|
roomName: string | null;
|
||||||
|
storyName: string | null;
|
||||||
|
visible: boolean;
|
||||||
|
scale: string;
|
||||||
|
}[]
|
||||||
|
| undefined
|
||||||
|
| null
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
|
const createRoomHandler = async () => {
|
||||||
trpc.room.getAll.useQuery(undefined);
|
await createRoom(roomName);
|
||||||
|
|
||||||
const createRoom = trpc.room.create.useMutation({});
|
|
||||||
|
|
||||||
const createRoomHandler = () => {
|
|
||||||
createRoom.mutate({ name: roomName });
|
|
||||||
setRoomName("");
|
setRoomName("");
|
||||||
(document.querySelector("#roomNameInput") as HTMLInputElement).value = "";
|
(document.querySelector("#roomNameInput") as HTMLInputElement).value = "";
|
||||||
(document.querySelector("#new-room-modal") as HTMLInputElement).checked =
|
(document.querySelector("#new-room-modal") as HTMLInputElement).checked =
|
||||||
false;
|
false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRoom = trpc.room.delete.useMutation({});
|
const getRoomsHandler = async () => {
|
||||||
|
const dbRooms = await getRooms();
|
||||||
const deleteRoomHandler = (roomId: string) => {
|
setRoomsFromDb(dbRooms);
|
||||||
deleteRoom.mutate({ id: roomId });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteRoomHandler = async (roomId: string) => {
|
||||||
|
await deleteRoom(roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void getRoomsHandler();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-8">
|
<div className="flex flex-col items-center justify-center gap-8">
|
||||||
{/* Modal for Adding Rooms */}
|
{/* Modal for Adding Rooms */}
|
||||||
|
@ -85,7 +105,7 @@ const RoomList = () => {
|
||||||
<label
|
<label
|
||||||
htmlFor="new-room-modal"
|
htmlFor="new-room-modal"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => createRoomHandler()}
|
onClick={() => void createRoomHandler()}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</label>
|
</label>
|
||||||
|
@ -121,7 +141,7 @@ const RoomList = () => {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="m-2"
|
className="m-2"
|
||||||
onClick={() => deleteRoomHandler(room.id)}
|
onClick={() => void deleteRoomHandler(room.id)}
|
||||||
>
|
>
|
||||||
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -23,9 +23,10 @@ import { RiVipCrownFill } from "react-icons/ri";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import { isAdmin, isVIP, jsonToCsv } from "@/utils/helpers";
|
import { isAdmin, isVIP, jsonToCsv } from "@/utils/helpers";
|
||||||
import type { PresenceItem } from "@/utils/types";
|
import type { PresenceItem } from "@/utils/types";
|
||||||
import { trpc } from "@/app/_trpc/client";
|
|
||||||
import LoadingIndicator from "@/app/_components/LoadingIndicator";
|
import LoadingIndicator from "@/app/_components/LoadingIndicator";
|
||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
|
import { getRoom, setRoom } from "@/server/actions/room";
|
||||||
|
import { getVotes, setVote } from "@/server/actions/vote";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
@ -40,14 +41,51 @@ const VoteUI = () => {
|
||||||
const [roomScale, setRoomScale] = useState<string>("");
|
const [roomScale, setRoomScale] = useState<string>("");
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
const { data: roomFromDb, refetch: refetchRoomFromDb } =
|
const [roomFromDb, setRoomFromDb] = useState<
|
||||||
trpc.room.get.useQuery({ id: roomId });
|
| {
|
||||||
|
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
|
||||||
|
>();
|
||||||
|
|
||||||
const { data: votesFromDb, refetch: refetchVotesFromDb } =
|
const [votesFromDb, setVotesFromDb] = useState<
|
||||||
trpc.vote.getAllByRoomId.useQuery({ roomId });
|
| {
|
||||||
|
id: string;
|
||||||
|
created_at: Date | null;
|
||||||
|
userId: string;
|
||||||
|
roomId: string;
|
||||||
|
value: string;
|
||||||
|
}[]
|
||||||
|
| undefined
|
||||||
|
| null
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const setVoteInDb = trpc.vote.set.useMutation({});
|
const getRoomHandler = async () => {
|
||||||
const setRoomInDb = trpc.room.set.useMutation({});
|
const dbRoom = await getRoom(roomId);
|
||||||
|
setRoomFromDb(dbRoom);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVotesHandler = async () => {
|
||||||
|
const dbVotes = await getVotes(roomId);
|
||||||
|
setVotesFromDb(dbVotes);
|
||||||
|
};
|
||||||
|
|
||||||
configureAbly({
|
configureAbly({
|
||||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||||
|
@ -63,10 +101,10 @@ const VoteUI = () => {
|
||||||
},
|
},
|
||||||
({ name }) => {
|
({ name }) => {
|
||||||
if (name === EventTypes.ROOM_UPDATE) {
|
if (name === EventTypes.ROOM_UPDATE) {
|
||||||
void refetchVotesFromDb();
|
void getVotesHandler();
|
||||||
void refetchRoomFromDb();
|
void getRoomHandler();
|
||||||
} else if (name === EventTypes.VOTE_UPDATE) {
|
} else if (name === EventTypes.VOTE_UPDATE) {
|
||||||
void refetchVotesFromDb();
|
void getVotesHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -98,6 +136,8 @@ const VoteUI = () => {
|
||||||
if (roomFromDb) {
|
if (roomFromDb) {
|
||||||
setStoryNameText(roomFromDb.storyName || "");
|
setStoryNameText(roomFromDb.storyName || "");
|
||||||
setRoomScale(roomFromDb.scale || "ERROR");
|
setRoomScale(roomFromDb.scale || "ERROR");
|
||||||
|
} else {
|
||||||
|
void getRoomHandler();
|
||||||
}
|
}
|
||||||
}, [roomFromDb, roomId, user]);
|
}, [roomFromDb, roomId, user]);
|
||||||
|
|
||||||
|
@ -112,25 +152,26 @@ const VoteUI = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setVote = (value: string) => {
|
const setVoteHandler = async (value: string) => {
|
||||||
if (roomFromDb) {
|
if (roomFromDb) {
|
||||||
setVoteInDb.mutate({
|
await setVote(value, roomFromDb.id);
|
||||||
roomId: roomFromDb.id,
|
|
||||||
value: value,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveRoom = (visible: boolean, reset = false, log = false) => {
|
const setRoomHandler = async (
|
||||||
|
visible: boolean,
|
||||||
|
reset = false,
|
||||||
|
log = false
|
||||||
|
) => {
|
||||||
if (roomFromDb) {
|
if (roomFromDb) {
|
||||||
setRoomInDb.mutate({
|
await setRoom(
|
||||||
name: storyNameText,
|
storyNameText,
|
||||||
roomId: roomFromDb.id,
|
visible,
|
||||||
scale: roomScale,
|
roomScale,
|
||||||
visible: visible,
|
roomFromDb.id,
|
||||||
reset: reset,
|
reset,
|
||||||
log: log,
|
log
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -308,7 +349,7 @@ const VoteUI = () => {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className="join md:btn-group-horizontal mx-auto">
|
<div className="join md:btn-group-horizontal mx-auto">
|
||||||
{roomFromDb.scale.split(",").map((scaleItem, index) => {
|
{roomFromDb.scale?.split(",").map((scaleItem, index) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -317,7 +358,7 @@ const VoteUI = () => {
|
||||||
? "btn btn-active btn-primary"
|
? "btn btn-active btn-primary"
|
||||||
: "btn"
|
: "btn"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setVote(scaleItem)}
|
onClick={() => void setVoteHandler(scaleItem)}
|
||||||
>
|
>
|
||||||
{scaleItem}
|
{scaleItem}
|
||||||
</button>
|
</button>
|
||||||
|
@ -364,7 +405,9 @@ const VoteUI = () => {
|
||||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => saveRoom(!roomFromDb.visible, false)}
|
onClick={() =>
|
||||||
|
void setRoomHandler(!roomFromDb.visible, false)
|
||||||
|
}
|
||||||
className="btn btn-primary inline-flex"
|
className="btn btn-primary inline-flex"
|
||||||
>
|
>
|
||||||
{roomFromDb.visible ? (
|
{roomFromDb.visible ? (
|
||||||
|
@ -384,7 +427,7 @@ const VoteUI = () => {
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
saveRoom(
|
void setRoomHandler(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
roomFromDb.storyName === storyNameText ||
|
roomFromDb.storyName === storyNameText ||
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { httpLink } from "@trpc/client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import superjson from "superjson";
|
|
||||||
|
|
||||||
import { trpc } from "./client";
|
|
||||||
|
|
||||||
export default function Provider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [queryClient] = useState(() => new QueryClient({}));
|
|
||||||
const [trpcClient] = useState(() =>
|
|
||||||
trpc.createClient({
|
|
||||||
links: [
|
|
||||||
httpLink({
|
|
||||||
url: "/api/trpc",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
transformer: superjson,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
||||||
</trpc.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { createTRPCReact } from "@trpc/react-query";
|
|
||||||
|
|
||||||
import { type AppRouter } from "@/server/trpc";
|
|
||||||
|
|
||||||
export const trpc = createTRPCReact<AppRouter>({});
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
||||||
|
|
||||||
import { appRouter } from "@/server/trpc";
|
|
||||||
import { createTRPCContext } from "@/server/trpc/trpc";
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = ["pdx1"];
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
export const revalidate = 0;
|
|
||||||
export const fetchCache = "force-no-store";
|
|
||||||
|
|
||||||
const handler = (req: Request) =>
|
|
||||||
fetchRequestHandler({
|
|
||||||
endpoint: "/api/trpc",
|
|
||||||
req,
|
|
||||||
router: appRouter,
|
|
||||||
createContext: createTRPCContext,
|
|
||||||
batching: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
|
|
@ -2,57 +2,85 @@ import { type NextRequest, NextResponse } from "next/server";
|
||||||
import {
|
import {
|
||||||
onUserCreatedHandler,
|
onUserCreatedHandler,
|
||||||
onUserDeletedHandler,
|
onUserDeletedHandler,
|
||||||
} from "@/server/webhookHelpers";
|
} from "@/utils/webhookHelpers";
|
||||||
import {
|
|
||||||
type WebhookEventBody,
|
import { headers } from "next/headers";
|
||||||
WebhookEventBodySchema,
|
import type { WebhookEvent } from "@clerk/nextjs/server";
|
||||||
WebhookEvents,
|
import { Webhook } from "svix";
|
||||||
} from "@/utils/types";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = "edge";
|
||||||
export const preferredRegion = ["pdx1"];
|
export const preferredRegion = ["pdx1"];
|
||||||
|
|
||||||
async function handler(req: NextRequest) {
|
async function handler(req: NextRequest) {
|
||||||
|
// Get the headers
|
||||||
|
const headerPayload = headers();
|
||||||
|
const svix_id = headerPayload.get("svix-id");
|
||||||
|
const svix_timestamp = headerPayload.get("svix-timestamp");
|
||||||
|
const svix_signature = headerPayload.get("svix-signature");
|
||||||
|
|
||||||
|
// If there are no headers, error out
|
||||||
|
if (!svix_id || !svix_timestamp || !svix_signature) {
|
||||||
|
return new Response("Error occured -- no svix headers", {
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the body
|
||||||
|
const body = JSON.stringify(await req.json());
|
||||||
|
|
||||||
|
// Create a new SVIX instance with your secret.
|
||||||
|
const wh = new Webhook(env.CLERK_WEBHOOK_SIGNING_SECRET);
|
||||||
|
|
||||||
|
let evt: WebhookEvent;
|
||||||
|
|
||||||
|
// Verify the payload with the headers
|
||||||
try {
|
try {
|
||||||
const eventBody = (await req.json()) as WebhookEventBody;
|
evt = wh.verify(body, {
|
||||||
const { data, type } = WebhookEventBodySchema.parse(eventBody);
|
"svix-id": svix_id,
|
||||||
let success = false;
|
"svix-timestamp": svix_timestamp,
|
||||||
|
"svix-signature": svix_signature,
|
||||||
|
}) as WebhookEvent;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error verifying webhook:", err);
|
||||||
|
return new Response("Error occured", {
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
// Get the ID and type
|
||||||
case WebhookEvents.USER_CREATED:
|
const { id } = evt.data;
|
||||||
success = await onUserCreatedHandler(data.id);
|
const eventType = evt.type;
|
||||||
if (success) {
|
let success = false;
|
||||||
return NextResponse.json(
|
|
||||||
{ result: "USER CREATED" },
|
|
||||||
{ status: 200, statusText: "USER CREATED" }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ result: "USER WITH THIS ID NOT FOUND" },
|
|
||||||
{ status: 404, statusText: "USER WITH THIS ID NOT FOUND" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case WebhookEvents.USER_DELETED:
|
|
||||||
success = await onUserDeletedHandler(data.id);
|
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case "user.created":
|
||||||
|
success = await onUserCreatedHandler(id);
|
||||||
|
if (success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ result: "USER DELETED" },
|
{ result: "USER CREATED" },
|
||||||
{ status: 200, statusText: "USER DELETED" }
|
{ status: 200, statusText: "USER CREATED" }
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
default:
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ result: "INVALID WEBHOOK EVENT TYPE" },
|
{ result: "USER WITH THIS ID NOT FOUND" },
|
||||||
{ status: 400, statusText: "INVALID WEBHOOK EVENT TYPE" }
|
{ status: 404, statusText: "USER WITH THIS ID NOT FOUND" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
case "user.deleted":
|
||||||
return NextResponse.json(
|
success = await onUserDeletedHandler(id);
|
||||||
{ result: "INVALID WEBHOOK EVENT BODY" },
|
|
||||||
{ status: 400, statusText: "INVALID WEBHOOK EVENT BODY" }
|
return NextResponse.json(
|
||||||
);
|
{ result: "USER DELETED" },
|
||||||
|
{ status: 200, statusText: "USER DELETED" }
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json(
|
||||||
|
{ result: "INVALID WEBHOOK EVENT TYPE" },
|
||||||
|
{ status: 400, statusText: "INVALID WEBHOOK EVENT TYPE" }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 67 KiB |
|
@ -4,11 +4,11 @@ import { GiStarFormation } from "react-icons/gi";
|
||||||
import { isAdmin, isVIP } from "@/utils/helpers";
|
import { isAdmin, isVIP } from "@/utils/helpers";
|
||||||
import { currentUser } from "@clerk/nextjs";
|
import { currentUser } from "@clerk/nextjs";
|
||||||
|
|
||||||
export const runtime = "edge";
|
// export const runtime = "edge";
|
||||||
export const preferredRegion = ["pdx1"];
|
// export const preferredRegion = ["pdx1"];
|
||||||
export const dynamic = "force-dynamic";
|
// export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
// export const revalidate = 0;
|
||||||
export const fetchCache = "force-no-store";
|
// export const fetchCache = "force-no-store";
|
||||||
|
|
||||||
export default async function Dashboard() {
|
export default async function Dashboard() {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -2,7 +2,6 @@ import { ClerkLoaded, ClerkProvider } from "@clerk/nextjs";
|
||||||
import Footer from "@/app/_components/Footer";
|
import Footer from "@/app/_components/Footer";
|
||||||
import Header from "@/app/_components/Header";
|
import Header from "@/app/_components/Header";
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
import Provider from "./_trpc/Provider";
|
|
||||||
import { dark } from "@clerk/themes";
|
import { dark } from "@clerk/themes";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
|
@ -16,13 +15,17 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ClerkProvider appearance={{ baseTheme: dark }}>
|
<ClerkProvider
|
||||||
|
appearance={{
|
||||||
|
baseTheme: dark,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<html lang="en" className="h-[100%] w-[100%] fixed overflow-y-auto">
|
<html lang="en" className="h-[100%] w-[100%] fixed overflow-y-auto">
|
||||||
<body className="h-[100%] w-[100%] fixed overflow-y-auto">
|
<body className="h-[100%] w-[100%] fixed overflow-y-auto">
|
||||||
<ClerkLoaded>
|
<ClerkLoaded>
|
||||||
<Header title={metadata.title} />
|
<Header title={metadata.title} />
|
||||||
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
||||||
<Provider>{children}</Provider>
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</ClerkLoaded>
|
</ClerkLoaded>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import VoteUI from "@/app/_components/VoteUI";
|
import VoteUI from "@/app/_components/VoteUI";
|
||||||
|
|
||||||
export const runtime = "edge";
|
// export const runtime = "edge";
|
||||||
export const preferredRegion = ["pdx1"];
|
// export const preferredRegion = ["pdx1"];
|
||||||
export const dynamic = "force-dynamic";
|
// export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
// export const revalidate = 0;
|
||||||
export const fetchCache = "force-no-store";
|
// export const fetchCache = "force-no-store";
|
||||||
|
|
||||||
export default function Room() {
|
export default function Room() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const env = createEnv({
|
||||||
APP_ENV: z.string(),
|
APP_ENV: z.string(),
|
||||||
UNKEY_ROOT_KEY: z.string(),
|
UNKEY_ROOT_KEY: z.string(),
|
||||||
CLERK_SECRET_KEY: z.string(),
|
CLERK_SECRET_KEY: z.string(),
|
||||||
|
CLERK_WEBHOOK_SIGNING_SECRET: z.string(),
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_ABLY_PUBLIC_KEY: z.string(),
|
NEXT_PUBLIC_ABLY_PUBLIC_KEY: z.string(),
|
||||||
|
|
|
@ -15,7 +15,7 @@ const rateLimit = new Ratelimit({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default authMiddleware({
|
export default authMiddleware({
|
||||||
publicRoutes: ["/", "/api/public/(.*)"],
|
publicRoutes: ["/", "/api/public/(.*)", "/api/webhooks"],
|
||||||
afterAuth: async (auth, req) => {
|
afterAuth: async (auth, req) => {
|
||||||
if (!auth.userId && auth.isPublicRoute) {
|
if (!auth.userId && auth.isPublicRoute) {
|
||||||
const { success } = await rateLimit.limit(req.ip || "");
|
const { success } = await rateLimit.limit(req.ip || "");
|
||||||
|
@ -28,10 +28,7 @@ export default authMiddleware({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (req.nextUrl.pathname.includes("/api/private")) {
|
||||||
req.nextUrl.pathname.includes("/api/webhooks") ||
|
|
||||||
req.nextUrl.pathname.includes("/api/private")
|
|
||||||
) {
|
|
||||||
const { success } = await rateLimit.limit(req.ip || "");
|
const { success } = await rateLimit.limit(req.ip || "");
|
||||||
|
|
||||||
const isValid = await validateRequest(req);
|
const isValid = await validateRequest(req);
|
||||||
|
@ -67,5 +64,5 @@ export default authMiddleware({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
|
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api)(.*)"],
|
||||||
};
|
};
|
||||||
|
|
230
src/server/actions/room.ts
Normal file
230
src/server/actions/room.ts
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { logs, rooms, votes } from "../schema";
|
||||||
|
import { auth } from "@clerk/nextjs";
|
||||||
|
import { fetchCache, invalidateCache, setCache } from "../redis";
|
||||||
|
import { publishToChannel } from "../ably";
|
||||||
|
import { EventTypes } from "@/utils/types";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new room.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the room.
|
||||||
|
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating the success of room creation.
|
||||||
|
*/
|
||||||
|
export const createRoom = async (name: string) => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await db
|
||||||
|
.insert(rooms)
|
||||||
|
.values({
|
||||||
|
id: `room_${createId()}`,
|
||||||
|
userId,
|
||||||
|
roomName: name,
|
||||||
|
storyName: "First Story!",
|
||||||
|
scale: "0.5,1,2,3,5,8",
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const success = room.length > 0;
|
||||||
|
if (room) {
|
||||||
|
await invalidateCache(`kv_roomlist_${userId}`);
|
||||||
|
|
||||||
|
await publishToChannel(
|
||||||
|
`${userId}`,
|
||||||
|
EventTypes.ROOM_LIST_UPDATE,
|
||||||
|
JSON.stringify(room)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a room with the specified ID.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the room to delete.
|
||||||
|
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating the success of room deletion.
|
||||||
|
*/
|
||||||
|
export const deleteRoom = async (id: string) => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedRoom = await db
|
||||||
|
.delete(rooms)
|
||||||
|
.where(eq(rooms.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const success = deletedRoom.length > 0;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await publishToChannel(
|
||||||
|
`${userId}`,
|
||||||
|
EventTypes.ROOM_LIST_UPDATE,
|
||||||
|
JSON.stringify(deletedRoom)
|
||||||
|
);
|
||||||
|
|
||||||
|
await publishToChannel(
|
||||||
|
`${id}`,
|
||||||
|
EventTypes.ROOM_UPDATE,
|
||||||
|
JSON.stringify(deletedRoom)
|
||||||
|
);
|
||||||
|
|
||||||
|
await invalidateCache(`kv_roomlist_${userId}`);
|
||||||
|
|
||||||
|
await publishToChannel(
|
||||||
|
`${userId}`,
|
||||||
|
EventTypes.ROOM_LIST_UPDATE,
|
||||||
|
JSON.stringify(deletedRoom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a room with the specified ID.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the room to retrieve.
|
||||||
|
* @returns {Promise<object|null>} - A promise that resolves to the retrieved room object or null if not found.
|
||||||
|
*/
|
||||||
|
export const getRoom = async (id: string) => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomFromDb = await db.query.rooms.findFirst({
|
||||||
|
where: eq(rooms.id, id),
|
||||||
|
with: {
|
||||||
|
logs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return roomFromDb || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of rooms.
|
||||||
|
*
|
||||||
|
* @returns {Promise<object[]|null>} - A promise that resolves to an array of room objects or null if not found.
|
||||||
|
*/
|
||||||
|
export const getRooms = async () => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedResult = await fetchCache<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
roomName: string;
|
||||||
|
}[]
|
||||||
|
>(`kv_roomlist_${userId}`);
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
return cachedResult;
|
||||||
|
} else {
|
||||||
|
const roomList = await db.query.rooms.findMany({
|
||||||
|
where: eq(rooms.userId, userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
await setCache(`kv_roomlist_${userId}`, roomList);
|
||||||
|
|
||||||
|
return roomList;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the properties of a room.
|
||||||
|
*
|
||||||
|
* @param {string} name - The new name of the room.
|
||||||
|
* @param {boolean} visible - Indicates if the room is visible.
|
||||||
|
* @param {string} scale - The scale values for the room.
|
||||||
|
* @param {string} roomId - The ID of the room to update.
|
||||||
|
* @param {boolean} reset - Indicates whether to reset room data.
|
||||||
|
* @param {boolean} log - Indicates whether to log changes.
|
||||||
|
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating the success of the room update.
|
||||||
|
*/
|
||||||
|
export const setRoom = async (
|
||||||
|
name: string,
|
||||||
|
visible: boolean,
|
||||||
|
scale: string,
|
||||||
|
roomId: string,
|
||||||
|
reset: boolean,
|
||||||
|
log: boolean
|
||||||
|
) => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
if (log) {
|
||||||
|
const oldRoom = await db.query.rooms.findFirst({
|
||||||
|
where: eq(rooms.id, roomId),
|
||||||
|
with: {
|
||||||
|
votes: true,
|
||||||
|
logs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
oldRoom &&
|
||||||
|
(await db.insert(logs).values({
|
||||||
|
id: `log_${createId()}`,
|
||||||
|
userId: userId,
|
||||||
|
roomId: roomId,
|
||||||
|
scale: oldRoom.scale,
|
||||||
|
votes: oldRoom.votes.map((vote) => {
|
||||||
|
return {
|
||||||
|
name: vote.userId,
|
||||||
|
value: vote.value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
roomName: oldRoom.roomName,
|
||||||
|
storyName: oldRoom.storyName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(votes).where(eq(votes.roomId, roomId));
|
||||||
|
|
||||||
|
await invalidateCache(`kv_votes_${roomId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoom = await db
|
||||||
|
.update(rooms)
|
||||||
|
.set({
|
||||||
|
storyName: name,
|
||||||
|
visible: visible,
|
||||||
|
scale: [...new Set(scale.split(","))]
|
||||||
|
.filter((item) => item !== "")
|
||||||
|
.toString(),
|
||||||
|
})
|
||||||
|
.where(eq(rooms.id, roomId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const success = newRoom.length > 0;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await publishToChannel(
|
||||||
|
`${roomId}`,
|
||||||
|
EventTypes.ROOM_UPDATE,
|
||||||
|
JSON.stringify(newRoom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
94
src/server/actions/vote.ts
Normal file
94
src/server/actions/vote.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { votes } from "../schema";
|
||||||
|
import { auth } from "@clerk/nextjs";
|
||||||
|
import { fetchCache, invalidateCache, setCache } from "../redis";
|
||||||
|
import { publishToChannel } from "../ably";
|
||||||
|
import { EventTypes } from "@/utils/types";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves votes for a specific room.
|
||||||
|
*
|
||||||
|
* @param {string} roomId - The ID of the room for which votes are retrieved.
|
||||||
|
* @returns {Promise<object[]|null>} - A promise that resolves to an array of vote objects or null if not found.
|
||||||
|
*/
|
||||||
|
export const getVotes = async (roomId: string) => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedResult = await fetchCache<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
created_at: Date;
|
||||||
|
userId: string;
|
||||||
|
roomId: string;
|
||||||
|
}[]
|
||||||
|
>(`kv_votes_${roomId}`);
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
return cachedResult;
|
||||||
|
} else {
|
||||||
|
const votesByRoomId = await db.query.votes.findMany({
|
||||||
|
where: eq(votes.roomId, roomId),
|
||||||
|
});
|
||||||
|
|
||||||
|
await setCache(`kv_votes_${roomId}`, votesByRoomId);
|
||||||
|
|
||||||
|
return votesByRoomId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a vote value for a room.
|
||||||
|
*
|
||||||
|
* @param {string} value - The value of the vote.
|
||||||
|
* @param {string} roomId - The ID of the room for which the vote is being set.
|
||||||
|
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating the success of the vote setting.
|
||||||
|
*/
|
||||||
|
export const setVote = async (value: string, roomId: string) => {
|
||||||
|
const { userId } = auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertResult = await db
|
||||||
|
.insert(votes)
|
||||||
|
.values({
|
||||||
|
id: `vote_${createId()}`,
|
||||||
|
value: value,
|
||||||
|
userId: userId,
|
||||||
|
roomId: roomId,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [votes.userId, votes.roomId],
|
||||||
|
set: {
|
||||||
|
value: value,
|
||||||
|
userId: userId,
|
||||||
|
roomId: roomId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = upsertResult.rowCount > 0;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await invalidateCache(`kv_votes_${roomId}`);
|
||||||
|
|
||||||
|
await publishToChannel(`${roomId}`, EventTypes.VOTE_UPDATE, value);
|
||||||
|
|
||||||
|
await publishToChannel(
|
||||||
|
`stats`,
|
||||||
|
EventTypes.STATS_UPDATE,
|
||||||
|
JSON.stringify(success)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
|
@ -1,10 +0,0 @@
|
||||||
import { createTRPCRouter } from "./trpc";
|
|
||||||
import { roomRouter } from "./routers/room";
|
|
||||||
import { voteRouter } from "./routers/vote";
|
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
|
||||||
room: roomRouter,
|
|
||||||
vote: voteRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
|
|
@ -1,188 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { publishToChannel } from "@/server/ably";
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/trpc/trpc";
|
|
||||||
|
|
||||||
import { fetchCache, invalidateCache, setCache } from "@/server/redis";
|
|
||||||
import { logs, rooms, votes } from "@/server/schema";
|
|
||||||
import { EventTypes } from "@/utils/types";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const roomRouter = createTRPCRouter({
|
|
||||||
// Create
|
|
||||||
create: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const room = await ctx.db
|
|
||||||
.insert(rooms)
|
|
||||||
.values({
|
|
||||||
id: `room_${createId()}`,
|
|
||||||
userId: ctx.auth.userId,
|
|
||||||
roomName: input.name,
|
|
||||||
storyName: "First Story!",
|
|
||||||
scale: "0.5,1,2,3,5,8",
|
|
||||||
visible: false,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const success = room.length > 0;
|
|
||||||
if (room) {
|
|
||||||
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`${ctx.auth.userId}`,
|
|
||||||
EventTypes.ROOM_LIST_UPDATE,
|
|
||||||
JSON.stringify(room)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Get One
|
|
||||||
get: protectedProcedure
|
|
||||||
.input(z.object({ id: z.string() }))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const roomFromDb = await ctx.db.query.rooms.findFirst({
|
|
||||||
where: eq(rooms.id, input.id),
|
|
||||||
with: {
|
|
||||||
logs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return roomFromDb || null;
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Get All
|
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
const cachedResult = await fetchCache<
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
createdAt: Date;
|
|
||||||
roomName: string;
|
|
||||||
}[]
|
|
||||||
>(`kv_roomlist_${ctx.auth.userId}`);
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return cachedResult;
|
|
||||||
} else {
|
|
||||||
const roomList = await ctx.db.query.rooms.findMany({
|
|
||||||
where: eq(rooms.userId, ctx.auth.userId),
|
|
||||||
});
|
|
||||||
|
|
||||||
await setCache(`kv_roomlist_${ctx.auth.userId}`, roomList);
|
|
||||||
|
|
||||||
return roomList;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Update One
|
|
||||||
set: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
visible: z.boolean(),
|
|
||||||
scale: z.string(),
|
|
||||||
roomId: z.string(),
|
|
||||||
reset: z.boolean(),
|
|
||||||
log: z.boolean(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
if (input.reset) {
|
|
||||||
if (input.log) {
|
|
||||||
const oldRoom = await ctx.db.query.rooms.findFirst({
|
|
||||||
where: eq(rooms.id, input.roomId),
|
|
||||||
with: {
|
|
||||||
votes: true,
|
|
||||||
logs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
oldRoom &&
|
|
||||||
(await ctx.db.insert(logs).values({
|
|
||||||
id: `log_${createId()}`,
|
|
||||||
userId: ctx.auth.userId,
|
|
||||||
roomId: input.roomId,
|
|
||||||
scale: oldRoom.scale,
|
|
||||||
votes: oldRoom.votes.map((vote) => {
|
|
||||||
return {
|
|
||||||
name: vote.userId,
|
|
||||||
value: vote.value,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
roomName: oldRoom.roomName,
|
|
||||||
storyName: oldRoom.storyName,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.db.delete(votes).where(eq(votes.roomId, input.roomId));
|
|
||||||
|
|
||||||
await invalidateCache(`kv_votes_${input.roomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRoom = await ctx.db
|
|
||||||
.update(rooms)
|
|
||||||
.set({
|
|
||||||
storyName: input.name,
|
|
||||||
visible: input.visible,
|
|
||||||
scale: [...new Set(input.scale.split(","))]
|
|
||||||
.filter((item) => item !== "")
|
|
||||||
.toString(),
|
|
||||||
})
|
|
||||||
.where(eq(rooms.id, input.roomId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const success = newRoom.length > 0;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await publishToChannel(
|
|
||||||
`${input.roomId}`,
|
|
||||||
EventTypes.ROOM_UPDATE,
|
|
||||||
JSON.stringify(newRoom)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Delete One
|
|
||||||
delete: protectedProcedure
|
|
||||||
.input(z.object({ id: z.string() }))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const deletedRoom = await ctx.db
|
|
||||||
.delete(rooms)
|
|
||||||
.where(eq(rooms.id, input.id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const success = deletedRoom.length > 0;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await invalidateCache(`kv_roomcount`);
|
|
||||||
await invalidateCache(`kv_votecount`);
|
|
||||||
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`${ctx.auth.userId}`,
|
|
||||||
EventTypes.ROOM_LIST_UPDATE,
|
|
||||||
JSON.stringify(deletedRoom)
|
|
||||||
);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`${input.id}`,
|
|
||||||
EventTypes.ROOM_UPDATE,
|
|
||||||
JSON.stringify(deletedRoom)
|
|
||||||
);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`stats`,
|
|
||||||
EventTypes.STATS_UPDATE,
|
|
||||||
JSON.stringify(deletedRoom)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { publishToChannel } from "@/server/ably";
|
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/trpc/trpc";
|
|
||||||
import { fetchCache, invalidateCache, setCache } from "@/server/redis";
|
|
||||||
import { EventTypes } from "@/utils/types";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { votes } from "@/server/schema";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
|
||||||
|
|
||||||
export const voteRouter = createTRPCRouter({
|
|
||||||
getAllByRoomId: protectedProcedure
|
|
||||||
.input(z.object({ roomId: z.string() }))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const cachedResult = await fetchCache<
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
value: string;
|
|
||||||
created_at: Date;
|
|
||||||
userId: string;
|
|
||||||
roomId: string;
|
|
||||||
}[]
|
|
||||||
>(`kv_votes_${input.roomId}`);
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return cachedResult;
|
|
||||||
} else {
|
|
||||||
const votesByRoomId = await ctx.db.query.votes.findMany({
|
|
||||||
where: eq(votes.roomId, input.roomId),
|
|
||||||
});
|
|
||||||
|
|
||||||
await setCache(`kv_votes_${input.roomId}`, votesByRoomId);
|
|
||||||
|
|
||||||
return votesByRoomId;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
set: protectedProcedure
|
|
||||||
.input(z.object({ value: z.string(), roomId: z.string() }))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const upsertResult = await ctx.db
|
|
||||||
.insert(votes)
|
|
||||||
.values({
|
|
||||||
id: `vote_${createId()}`,
|
|
||||||
value: input.value,
|
|
||||||
userId: ctx.auth.userId,
|
|
||||||
roomId: input.roomId,
|
|
||||||
})
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: [votes.userId, votes.roomId],
|
|
||||||
set: {
|
|
||||||
value: input.value,
|
|
||||||
userId: ctx.auth.userId,
|
|
||||||
roomId: input.roomId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const success = upsertResult.rowCount > 0;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await invalidateCache(`kv_votecount`);
|
|
||||||
await invalidateCache(`kv_votes_${input.roomId}`);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`${input.roomId}`,
|
|
||||||
EventTypes.VOTE_UPDATE,
|
|
||||||
input.value
|
|
||||||
);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`stats`,
|
|
||||||
EventTypes.STATS_UPDATE,
|
|
||||||
JSON.stringify(success)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,50 +0,0 @@
|
||||||
import type {
|
|
||||||
SignedInAuthObject,
|
|
||||||
SignedOutAuthObject,
|
|
||||||
} from "@clerk/nextjs/api";
|
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { TRPCError, type inferAsyncReturnType, initTRPC } from "@trpc/server";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
|
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import superjson from "superjson";
|
|
||||||
|
|
||||||
interface AuthContext {
|
|
||||||
auth: SignedInAuthObject | SignedOutAuthObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createInnerTRPCContext = ({ auth }: AuthContext) => {
|
|
||||||
return {
|
|
||||||
auth,
|
|
||||||
db,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createTRPCContext = ({ req }: FetchCreateContextFnOptions) => {
|
|
||||||
return createInnerTRPCContext({ auth: getAuth(req as NextRequest) });
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Context = inferAsyncReturnType<typeof createTRPCContext>;
|
|
||||||
|
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
|
||||||
transformer: superjson,
|
|
||||||
errorFormatter({ shape }) {
|
|
||||||
return shape;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// check if the user is signed in, otherwise through a UNAUTHORIZED CODE
|
|
||||||
const isAuthed = t.middleware(({ next, ctx }) => {
|
|
||||||
if (!ctx.auth.userId) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return next({
|
|
||||||
ctx: {
|
|
||||||
auth: ctx.auth,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createTRPCRouter = t.router;
|
|
||||||
export const publicProcedure = t.procedure;
|
|
||||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
type BetterEnum<T> = T[keyof T];
|
type BetterEnum<T> = T[keyof T];
|
||||||
|
|
||||||
export const EventTypes = {
|
export const EventTypes = {
|
||||||
|
@ -10,37 +8,6 @@ export const EventTypes = {
|
||||||
} as const;
|
} as const;
|
||||||
export type EventType = BetterEnum<typeof EventTypes>;
|
export type EventType = BetterEnum<typeof EventTypes>;
|
||||||
|
|
||||||
export const WebhookEvents = {
|
|
||||||
USER_CREATED: "user.created",
|
|
||||||
USER_DELETED: "user.deleted",
|
|
||||||
} as const;
|
|
||||||
export type WebhookEvent = BetterEnum<typeof WebhookEvents>;
|
|
||||||
|
|
||||||
export const WebhookEventBodySchema = z.object({
|
|
||||||
data: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
email_addresses: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
email_address: z.string().email(),
|
|
||||||
id: z.string().optional(),
|
|
||||||
verification: z
|
|
||||||
.object({
|
|
||||||
status: z.string().optional(),
|
|
||||||
strategy: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
first_name: z.string().nullable().optional(),
|
|
||||||
last_name: z.string().nullable().optional(),
|
|
||||||
}),
|
|
||||||
type: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type WebhookEventBody = z.infer<typeof WebhookEventBodySchema>;
|
|
||||||
|
|
||||||
export interface PresenceItem {
|
export interface PresenceItem {
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "./db";
|
import { db } from "../server/db";
|
||||||
import { rooms } from "./schema";
|
import { rooms } from "../server/schema";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const onUserDeletedHandler = async (userId: string) => {
|
export const onUserDeletedHandler = async (userId: string | undefined) => {
|
||||||
|
if (!userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.delete(rooms).where(eq(rooms.userId, userId));
|
await db.delete(rooms).where(eq(rooms.userId, userId));
|
||||||
|
|
||||||
|
@ -13,7 +17,11 @@ export const onUserDeletedHandler = async (userId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onUserCreatedHandler = async (userId: string) => {
|
export const onUserCreatedHandler = async (userId: string | undefined) => {
|
||||||
|
if (!userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const userUpdateResponse = await fetch(
|
const userUpdateResponse = await fetch(
|
||||||
`https://api.clerk.com/v1/users/${userId}/metadata`,
|
`https://api.clerk.com/v1/users/${userId}/metadata`,
|
||||||
{
|
{
|
Loading…
Add table
Reference in a new issue