Merge pull request #46 from atridadl/dev

2.2.0
 Moved to App router vs Pages router
Better caching
This commit is contained in:
Atridad Lahiji 2023-08-28 12:46:28 -06:00 committed by GitHub
commit 779f74380a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 427 additions and 932 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "sprintpadawan", "name": "sprintpadawan",
"version": "2.1.0", "version": "2.2.0",
"description": "Plan. Sprint. Repeat.", "description": "Plan. Sprint. Repeat.",
"private": true, "private": true,
"scripts": { "scripts": {
@ -14,7 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@ably-labs/react-hooks": "^2.1.1", "@ably-labs/react-hooks": "^2.1.1",
"@clerk/nextjs": "^4.23.2", "@clerk/nextjs": "^4.23.3",
"@neondatabase/serverless": "^0.6.0", "@neondatabase/serverless": "^0.6.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.7",
@ -23,7 +23,7 @@
"@trpc/next": "10.38.0", "@trpc/next": "10.38.0",
"@trpc/react-query": "10.38.0", "@trpc/react-query": "10.38.0",
"@trpc/server": "10.38.0", "@trpc/server": "10.38.0",
"@unkey/api": "^0.6.16", "@unkey/api": "^0.6.19",
"@upstash/ratelimit": "^0.4.3", "@upstash/ratelimit": "^0.4.3",
"@upstash/redis": "^1.22.0", "@upstash/redis": "^1.22.0",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
@ -37,7 +37,6 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-email": "^1.9.4", "react-email": "^1.9.4",
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"resend": "^1.0.0",
"sharp": "^0.32.5", "sharp": "^0.32.5",
"superjson": "1.13.1", "superjson": "1.13.1",
"zod": "^3.22.2" "zod": "^3.22.2"
@ -45,19 +44,19 @@
"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.6", "@types/node": "^20.5.7",
"@types/react": "^18.2.21", "@types/react": "^18.2.21",
"@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1", "@typescript-eslint/parser": "^6.4.1",
"daisyui": "^3.6.1", "bufferutil": "^4.0.7",
"daisyui": "^3.6.3",
"drizzle-kit": "^0.19.13", "drizzle-kit": "^0.19.13",
"eslint": "^8.47.0", "eslint": "^8.48.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": "5.0.2",
"ct3aMetadata": { "ws": "^8.13.0"
"initVersion": "7.5.9"
} }
} }

603
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#1f2937</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,27 +0,0 @@
{
"name": "Sprint Padawan",
"short_name": "Sprint Padawan",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#1F2937",
"background_color": "#1F2937",
"start_url": "/",
"display": "standalone",
"orientation": "portrait"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -1,24 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2280 4865 c-527 -60 -877 -209 -1275 -541 -434 -362 -714 -862 -800
-1429 -22 -143 -30 -389 -16 -530 13 -145 66 -382 121 -548 244 -729 860
-1302 1605 -1492 224 -58 323 -70 580 -70 247 0 347 11 555 62 401 97 797 323
1086 619 163 167 204 218 342 427 206 311 299 602 333 1040 41 529 -75 1030
-341 1482 -275 464 -721 789 -1266 921 -264 64 -660 89 -924 59z m638 -1051
c12 -14 22 -36 22 -49 0 -24 -98 -297 -265 -735 -47 -124 -85 -231 -85 -237 0
-10 74 -13 339 -13 l338 0 27 -26 c30 -31 34 -69 9 -104 -10 -14 -191 -241
-403 -505 -212 -264 -454 -566 -538 -671 -84 -106 -159 -195 -168 -198 -57
-22 -124 17 -124 71 0 15 77 235 171 488 94 253 173 468 176 477 5 15 -22 17
-337 20 l-342 3 -24 28 c-13 15 -24 41 -24 56 0 22 113 170 531 692 292 365
543 677 558 693 49 52 97 56 139 10z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,19 +0,0 @@
{
"name": "Sprint Padawan",
"short_name": "Sprint Padawan",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#1f2937",
"background_color": "#1f2937",
"display": "standalone"
}

View file

@ -1,5 +1,7 @@
"use client";
import { GiTechnoHeart } from "react-icons/gi"; import { GiTechnoHeart } from "react-icons/gi";
import packagejson from "../../package.json"; import packagejson from "../../../package.json";
const Footer = () => { const Footer = () => {
return ( return (

View file

@ -1,7 +1,9 @@
"use client";
import { UserButton, useUser } from "@clerk/nextjs"; import { UserButton, useUser } from "@clerk/nextjs";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter, usePathname } from "next/navigation";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
interface NavbarProps { interface NavbarProps {
@ -11,9 +13,10 @@ interface NavbarProps {
const Navbar = ({ title }: NavbarProps) => { const Navbar = ({ title }: NavbarProps) => {
const { isLoaded, isSignedIn } = useUser(); const { isLoaded, isSignedIn } = useUser();
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const navigationMenu = () => { const navigationMenu = () => {
if (router.pathname !== "/dashboard" && isSignedIn) { if (pathname !== "/dashboard" && isSignedIn) {
return ( return (
<Link className="btn btn-secondary btn-outline mx-2" href="/dashboard"> <Link className="btn btn-secondary btn-outline mx-2" href="/dashboard">
Dashboard Dashboard

View file

@ -1,10 +1,14 @@
"use client";
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 { 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 { api } from "~/utils/api";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { trpc } from "../_trpc/client";
export const dynamic = "force-dynamic";
const RoomList = () => { const RoomList = () => {
const { isSignedIn, user } = useUser(); const { isSignedIn, user } = useUser();
@ -25,11 +29,11 @@ const RoomList = () => {
const [roomName, setRoomName] = useState<string>(""); const [roomName, setRoomName] = useState<string>("");
const { data: roomsFromDb, refetch: refetchRoomsFromDb } = const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
api.room.getAll.useQuery(undefined, { trpc.room.getAll.useQuery(undefined, {
enabled: isSignedIn, enabled: isSignedIn,
}); });
const createRoom = api.room.create.useMutation({}); const createRoom = trpc.room.create.useMutation({});
const createRoomHandler = () => { const createRoomHandler = () => {
createRoom.mutate({ name: roomName }); createRoom.mutate({ name: roomName });
@ -39,7 +43,7 @@ const RoomList = () => {
false; false;
}; };
const deleteRoom = api.room.delete.useMutation({}); const deleteRoom = trpc.room.delete.useMutation({});
const deleteRoomHandler = (roomId: string) => { const deleteRoomHandler = (roomId: string) => {
if (isSignedIn) { if (isSignedIn) {

View file

@ -0,0 +1,27 @@
"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>
);
}

5
src/app/_trpc/client.ts Normal file
View file

@ -0,0 +1,5 @@
import { createTRPCReact } from "@trpc/react-query";
import { type AppRouter } from "~/server/trpc";
export const trpc = createTRPCReact<AppRouter>({});

View file

@ -1,13 +1,13 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const config = { export const runtime = "edge";
runtime: "edge", export const preferredRegion = ["pdx1"];
regions: ["pdx1"],
};
export default function handler() { function handler() {
return NextResponse.json( return NextResponse.json(
{ message: "Private Pong!" }, { message: "Private Pong!" },
{ status: 200, statusText: "SUCCESS" } { status: 200, statusText: "SUCCESS" }
); );
} }
export { handler as GET };

View file

@ -1,13 +1,13 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const config = { export const runtime = "edge";
runtime: "edge", export const preferredRegion = ["pdx1"];
regions: ["pdx1"],
};
export default function handler() { function handler() {
return NextResponse.json( return NextResponse.json(
{ message: "Public Pong!" }, { message: "Public Pong!" },
{ status: 200, statusText: "SUCCESS" } { status: 200, statusText: "SUCCESS" }
); );
} }
export { handler as GET };

View file

@ -0,0 +1,20 @@
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"];
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: createTRPCContext,
batching: {
enabled: false,
},
});
export { handler as GET, handler as POST };

View file

@ -9,12 +9,10 @@ import {
WebhookEvents, WebhookEvents,
} from "~/utils/types"; } from "~/utils/types";
export const config = { export const runtime = "edge";
runtime: "edge", export const preferredRegion = ["pdx1"];
regions: ["pdx1"],
};
export default async function handler(req: NextRequest) { async function handler(req: NextRequest) {
try { try {
const eventBody = (await req.json()) as WebhookEventBody; const eventBody = (await req.json()) as WebhookEventBody;
const { data, type } = WebhookEventBodySchema.parse(eventBody); const { data, type } = WebhookEventBodySchema.parse(eventBody);
@ -22,11 +20,7 @@ export default async function handler(req: NextRequest) {
switch (type) { switch (type) {
case WebhookEvents.USER_CREATED: case WebhookEvents.USER_CREATED:
success = await onUserCreatedHandler( success = await onUserCreatedHandler(data.id);
data.id,
data.email_addresses?.map((email) => email.email_address) || [],
`${data.first_name} ${data.last_name}`
);
if (success) { if (success) {
return NextResponse.json( return NextResponse.json(
{ result: "USER CREATED" }, { result: "USER CREATED" },
@ -61,3 +55,5 @@ export default async function handler(req: NextRequest) {
); );
} }
} }
export { handler as POST };

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -1,7 +1,9 @@
"use client";
import type { NextPage } from "next"; import type { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import RoomList from "~/components/RoomList"; import RoomList from "~/app/_components/RoomList";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -10,6 +12,8 @@ import { GiStarFormation } from "react-icons/gi";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { isAdmin, isVIP } from "~/utils/helpers"; import { isAdmin, isVIP } from "~/utils/helpers";
export const dynamic = "force-dynamic";
const Home: NextPage = () => { const Home: NextPage = () => {
return ( return (
<> <>

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

30
src/app/layout.tsx Normal file
View file

@ -0,0 +1,30 @@
import { ClerkProvider } from "@clerk/nextjs";
import Footer from "~/app/_components/Footer";
import Navbar from "~/app/_components/Navbar";
import "~/styles/globals.css";
import Provider from "./_trpc/Provider";
export const metadata = {
title: "Sprint Padawan",
description: "Plan. Sprint. Repeat.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en" className="h-[100%] w-[100%] fixed overflow-y-auto">
<body className="block h-[100%]">
<Navbar title="Sprint Padawan" />
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
<Provider>{children}</Provider>
</div>
<Footer />
</body>
</html>
</ClerkProvider>
);
}

View file

@ -1,10 +1,12 @@
"use client";
import { type NextPage } from "next"; import { type NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { EventTypes } from "~/utils/types"; import { EventTypes } from "~/utils/types";
import { useRouter } from "next/router"; import { useParams } from "next/navigation";
import { import {
IoCheckmarkCircleOutline, IoCheckmarkCircleOutline,
IoCopyOutline, IoCopyOutline,
@ -16,7 +18,6 @@ import {
IoSaveOutline, IoSaveOutline,
} from "react-icons/io5"; } from "react-icons/io5";
import { GiStarFormation } from "react-icons/gi"; import { GiStarFormation } from "react-icons/gi";
import { api } from "~/utils/api";
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks"; import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
import Link from "next/link"; import Link from "next/link";
import { FaShieldAlt } from "react-icons/fa"; import { FaShieldAlt } from "react-icons/fa";
@ -25,6 +26,9 @@ import { env } from "~/env.mjs";
import { downloadCSV, isAdmin, isVIP } from "~/utils/helpers"; import { downloadCSV, isAdmin, isVIP } from "~/utils/helpers";
import type { PresenceItem } from "~/utils/types"; import type { PresenceItem } from "~/utils/types";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { trpc } from "~/app/_trpc/client";
export const dynamic = "force-dynamic";
const Room: NextPage = () => { const Room: NextPage = () => {
const { isSignedIn } = useUser(); const { isSignedIn } = useUser();
@ -52,21 +56,21 @@ export default Room;
const RoomBody = ({}) => { const RoomBody = ({}) => {
const { isSignedIn, user } = useUser(); const { isSignedIn, user } = useUser();
const { query } = useRouter(); const params = useParams();
const roomId = query.id as string; const roomId = params?.id as string;
const [storyNameText, setStoryNameText] = useState<string>(""); const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>(""); const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(false); const [copied, setCopied] = useState<boolean>(false);
const { data: roomFromDb, refetch: refetchRoomFromDb } = const { data: roomFromDb, refetch: refetchRoomFromDb } =
api.room.get.useQuery({ id: roomId }); trpc.room.get.useQuery({ id: roomId });
const { data: votesFromDb, refetch: refetchVotesFromDb } = const { data: votesFromDb, refetch: refetchVotesFromDb } =
api.vote.getAllByRoomId.useQuery({ roomId }); trpc.vote.getAllByRoomId.useQuery({ roomId });
const setVoteInDb = api.vote.set.useMutation({}); const setVoteInDb = trpc.vote.set.useMutation({});
const setRoomInDb = api.room.set.useMutation({}); const setRoomInDb = trpc.room.set.useMutation({});
configureAbly({ configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY, key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,

View file

@ -1,5 +1,9 @@
"use client";
import { SignIn } from "@clerk/nextjs"; import { SignIn } from "@clerk/nextjs";
export const dynamic = "force-static";
const SignInPage = () => ( const SignInPage = () => (
<div style={styles}> <div style={styles}>
<SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" /> <SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" />

View file

@ -1,5 +1,9 @@
"use client";
import { SignUp } from "@clerk/nextjs"; import { SignUp } from "@clerk/nextjs";
export const dynamic = "force-static";
const SignUpPage = () => ( const SignUpPage = () => (
<div style={styles}> <div style={styles}>
<SignUp path="/sign-up" routing="path" signInUrl="/sign-in" /> <SignUp path="/sign-up" routing="path" signInUrl="/sign-in" />

View file

@ -1,51 +0,0 @@
import {
Body,
Container,
Head,
Heading,
Hr,
Html,
Img,
Preview,
Section,
Text,
} from "@react-email/components";
import * as React from "react";
interface WelcomeTemplateProps {
name: string;
}
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
export const Welcome = ({ name }: WelcomeTemplateProps) => (
<Html>
<Head />
<Preview>🎉 Welcome to Sprint Padawan! 🎉</Preview>
<Body>
<Container>
<Section>
<Img
src={`${baseUrl}/logo.webp`}
width="40"
height="37"
alt={`Sprint Padawan Logo`}
/>
</Section>
<Heading>
🎉 Welcome to Sprint Padawan, <strong>{name}</strong>! 🎉
</Heading>
<Text>Hello {name},</Text>
<Text>Thank you for signing up for Sprint Padawan!</Text>
<Text>
If at any point you encounter issues, please let me know at
support@sprintpadawan.dev.
</Text>
<Hr />
<Text> Atridad Lahiji</Text>
</Container>
</Body>
</Html>
);

View file

@ -1,24 +0,0 @@
import { type AppType } from "next/app";
import { ClerkProvider } from "@clerk/nextjs";
import { api } from "~/utils/api";
import Footer from "~/components/Footer";
import Navbar from "~/components/Navbar";
import "~/styles/globals.css";
const MyApp: AppType = ({ Component, pageProps }) => {
return (
<ClerkProvider {...pageProps}>
<div className="block h-[100%]">
<Navbar title="Sprint Padawan" />
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
<Component {...pageProps} />
</div>
<Footer />
</div>
</ClerkProvider>
);
};
export default api.withTRPC(MyApp);

View file

@ -1,62 +0,0 @@
import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html>
<Head>
<meta name="application-name" content="Sprint Padawan" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Sprint Padawan" />
<meta name="description" content="Plan. Sprint. Repeat." />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-config" content="/icons/browserconfig.xml" />
<meta name="msapplication-TileColor" content="#1F2937" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="theme-color" content="#1F2937" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/manifest.json" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:url" content="https://sprintpadawan.dev" />
<meta name="twitter:title" content="Sprint Padawan" />
<meta name="twitter:description" content="Plan. Sprint. Repeat." />
<meta
name="twitter:image"
content="https://sprintpadawan.dev/android-chrome-192x192.png"
/>
<meta name="twitter:creator" content="@atridadl" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Sprint Padawan" />
<meta property="og:description" content="Plan. Sprint. Repeat." />
<meta property="og:site_name" content="Sprint Padawan" />
<meta property="og:url" content="https://sprintpadawan.dev" />
<meta
property="og:image"
content="https://sprintpadawan.dev/icons/apple-touch-icon.png"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View file

@ -1,19 +0,0 @@
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import type { NextRequest } from "next/server";
export const config = {
runtime: "edge",
regions: ["pdx1"],
unstable_allowDynamic: ["/node_modules/ably/**"],
};
export default async function handler(req: NextRequest) {
return fetchRequestHandler({
endpoint: "/api/trpc",
router: appRouter,
req,
createContext: createTRPCContext,
});
}

View file

@ -1,19 +0,0 @@
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import type { NextRequest } from "next/server";
export const config = {
runtime: "edge",
regions: ["pdx1"],
unstable_allowDynamic: ["/node_modules/ably/**"],
};
export default async function handler(req: NextRequest) {
return fetchRequestHandler({
endpoint: "/api/trpc",
router: appRouter,
req,
createContext: createTRPCContext,
});
}

View file

@ -1,16 +0,0 @@
import { roomRouter } from "~/server/api/routers/room";
import { createTRPCRouter } from "~/server/api/trpc";
import { voteRouter } from "./routers/vote";
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
room: roomRouter,
vote: voteRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;

View file

@ -1,104 +0,0 @@
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1)
* 2. You want to create a new middleware or type of procedure (see Part 3)
*
* tl;dr - this is where all the tRPC server stuff is created and plugged in.
* The pieces you will need to use are documented accordingly near the end
*/
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API
*
* These allow you to access things like the database, the session, etc, when
* processing a request
*
*/
import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import { getAuth } from "@clerk/nextjs/server";
import type {
SignedInAuthObject,
SignedOutAuthObject,
} from "@clerk/nextjs/api";
import { db } from "../db";
interface AuthContext {
auth: SignedInAuthObject | SignedOutAuthObject;
}
/**
* This helper generates the "internals" for a tRPC context. If you need to use
* it, you can export it from here
*
* Examples of things you may need it for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
*/
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>;
/**
* 2. INITIALIZATION
*
* This is where the trpc api is initialized, connecting the context and
* transformer
*/
import { type inferAsyncReturnType, initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import type { NextRequest } from "next/server";
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,
},
});
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these
* a lot in the /src/server/api/routers folder
*/
/**
* This is how you create new routers and subrouters in your tRPC API
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
/**
* Public (unauthed) procedure
*
* This is the base piece you use to build new queries and mutations on your
* tRPC API. It does not guarantee that a user querying is authorized, but you
* can still access user session data if they are logged in
*/
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);

10
src/server/trpc/index.ts Normal file
View file

@ -0,0 +1,10 @@
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;

View file

@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { publishToChannel } from "~/server/ably"; import { publishToChannel } from "~/server/ably";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { createTRPCRouter, protectedProcedure } from "~/server/trpc/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis"; import { fetchCache, invalidateCache, setCache } from "~/server/redis";
import { logs, rooms, votes } from "~/server/schema"; import { logs, rooms, votes } from "~/server/schema";

View file

@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { publishToChannel } from "~/server/ably"; import { publishToChannel } from "~/server/ably";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { createTRPCRouter, protectedProcedure } from "~/server/trpc/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis"; import { fetchCache, invalidateCache, setCache } from "~/server/redis";
import { EventTypes } from "~/utils/types"; import { EventTypes } from "~/utils/types";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

50
src/server/trpc/trpc.ts Normal file
View file

@ -0,0 +1,50 @@
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);

View file

@ -2,10 +2,6 @@ import { eq } from "drizzle-orm";
import { db } from "./db"; import { db } from "./db";
import { rooms } from "./schema"; import { rooms } from "./schema";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { Welcome } from "~/components/templates/Welcome";
import { Resend } from "resend";
const resend = new Resend(env.RESEND_API_KEY);
export const onUserDeletedHandler = async (userId: string) => { export const onUserDeletedHandler = async (userId: string) => {
try { try {
@ -17,11 +13,7 @@ export const onUserDeletedHandler = async (userId: string) => {
} }
}; };
export const onUserCreatedHandler = async ( export const onUserCreatedHandler = async (userId: string) => {
userId: string,
userEmails: string[],
userName?: string
) => {
const userUpdateResponse = await fetch( const userUpdateResponse = await fetch(
`https://api.clerk.com/v1/users/${userId}/metadata`, `https://api.clerk.com/v1/users/${userId}/metadata`,
{ {
@ -41,16 +33,5 @@ export const onUserCreatedHandler = async (
} }
); );
if (userUpdateResponse.ok) {
userEmails.forEach((userEmail) => {
void resend.sendEmail({
from: "no-reply@sprintpadawan.dev",
to: userEmail,
subject: "🎉 Welcome to Sprint Padawan! 🎉",
react: Welcome({ name: userName ? userEmail : userEmail }),
});
});
}
return userUpdateResponse.ok; return userUpdateResponse.ok;
}; };

View file

@ -1,12 +1,3 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html,
container,
body #__next {
height: 100%;
width: 100%;
overflow-y: auto;
position: fixed;
}

View file

@ -1,68 +0,0 @@
/**
* This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
* contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
*
* We also create a few inference helpers for input and output types.
*/
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
import superjson from "superjson";
import { type AppRouter } from "~/server/api/root";
const getBaseUrl = () => {
if (typeof window !== "undefined") return ""; // browser should use relative url
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
};
/** A set of type-safe react-query hooks for your tRPC API. */
export const api = createTRPCNext<AppRouter>({
config() {
return {
/**
* Transformer used for data de-serialization from the server.
*
* @see https://trpc.io/docs/data-transformers
*/
transformer: superjson,
/**
* Links used to determine request flow from client to server.
*
* @see https://trpc.io/docs/links
*/
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
/**
* Whether tRPC should await queries when server rendering pages.
*
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
*/
ssr: false,
});
/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

View file

@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2017", "target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -18,8 +22,15 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["./src/*"] "~/*": [
"./src/*"
]
},
"plugins": [
{
"name": "next"
} }
]
}, },
"include": [ "include": [
".eslintrc.cjs", ".eslintrc.cjs",
@ -27,7 +38,10 @@
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"**/*.cjs", "**/*.cjs",
"**/*.mjs" "**/*.mjs",
".next/types/**/*.ts"
], ],
"exclude": ["node_modules"] "exclude": [
"node_modules"
]
} }