# Project export: skribbl.ai

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: a collaborative whiteboard game enhanced by video chats, messaging, and AI.
- Devpost: https://devpost.com/software/skribbl-ai
- GitHub: https://github.com/nyumat/skribbl.ai
- Demo: https://drawing-is-for-kids-they-say.vercel.app/
- Team: 3 GitHub contributor(s) — Thomas (55 commits), Lewin Bobda (9 commits), Hamza Mostafa (2 commits)

## Devpost submission (written by the team)

### Inspiration

As teammates, we're doing off-season internships, and one thing we agreed on is that it's sooooo tough to both break the ice and find engaging activities during team meetings. And what we got out of that was......27 hours of planning, **96 different ideas from ChatGPT (yeah, even this failed), and one, incredibly fun all-nighter. Inspired by Pictogram, Hangman, and even tools like Figma and Excalidraw, we knew something fun and interactive that could be a go-to for “water-cooler” moments would be awesome to try out during a hackathon. We also saw the need for this in our school clubs, so we decided to build a game that brings people together with a fun and competitive edge!

### What it does

Skribbl.ai is a competitive drawing game where two players compete to replicate an image given through a prompt, all on a shared virtual whiteboard. Elevated with real-time video, voice, and chat, players can communicate and collaborate while racing against time to impress the AI judge, which scores the drawings based on accuracy.

### How we built it

We used several key technologies: 100ms for real-time video and voice features. tribute to Nexus ChromaDB to handle data storage for user interactions. tldraw and tldraw sync for real-time collaborative white-boarding primitives. React and TypeScript to power the frontend. NextAuth for user authentication and session management. Despite starting at 9 pm on the Saturday before the hackathon's end, we managed to pivot from our original idea (voice-powered music production) and complete this project in record time.

### Challenges we ran into

Our original idea of voice-powered music production wasn't compatible with the sponsor Hume's technology, forcing us to pivot. The tight deadline, constant pivoting, and beyond late start added additional pressure, but we powered through to deliver a fully functional app by the end of the hackathon. The Metreon WiFi, especially when building a network-heavy application, lead to many hotspots and remote work.

### Accomplishments we're proud of

We’re incredibly proud of how quickly we pivoted and built a polished app with video, voice, chat, and whiteboard integration in a matter of hours. Finishing the project under such time constraints felt like a huge accomplishment. Yeah, we may not have the crazy large feature set, be we do the one thing we planned to do, really well–at least, we think.

### What we learned

We learned how to adapt quickly when things don’t go as planned, and we gained valuable experience integrating real-time video and collaboration features with technologies like 100ms, ChromaDB, and tldraw. We also experience in perseverance and pushing through idea droughts. Since we're working, adjusting "back" to the hackathon mindset definitely takes time.

### What's next

for Skribbl.ai We’re super stoked to continue improving Skribbl.ai after CalHacks. We surprisingly—especially given our execution, see potential for the app to be used in virtual team-building exercises, school clubs, and social hangouts. Stuff like: Multiplayer modes: Expand to support larger groups and team-based drawing challenges. Advanced AI judging: Improve the AI to evaluate drawings based on creativity, style, and time taken, not just accuracy. Custom game modes: Allow users to create custom challenges, themes, and rules for personalized gameplay. Leaderboard and achievements: Introduce a ranking system, badges, and awards for top players. Mobile app: Develop a mobile-friendly version to make the game accessible across different devices. Interactive spectators: Let spectators participate in the game through voting or live commenting during matches. Real-time drawing hints: Implement features where players can give or receive subtle hints during gameplay without breaking the challenge. Custom avatars and themes: Offer players options to personalize their in-game experience with unique avatars, themes, and board designs. All this stuff seems super exciting to build, and we're glad to have a baseline to expand off of. Well, that's it for skribbl.ai, thanks for reading! Note for GoDaddy: The promo-code we tried to apply MLHCAL24 was not working on the website. We tried the second best thing, in Vercel. ; )

## README (from the GitHub repository)

<img width="1523" alt="image" src="https://github.com/user-attachments/assets/c549c318-c461-4c5a-bbf4-81786e764276">


## Inspiration
As teammates, we're doing off-season internships, and one thing we agreed on is that it's sooooo tough to both break the ice and find engaging activities during team meetings.

And what we got out of that was......27 hours of planning, **96 different ideas from ChatGPT (yeah, even this failed), and one, incredibly fun all-nighter.

Inspired by Pictogram, Hangman, and even tools like Figma and Excalidraw, we knew something fun and interactive that could be a go-to for “water-cooler” moments would be awesome to try out during a hackathon. We also saw the need for this in our school clubs, so we decided to build a game that brings people together with a fun and competitive edge!

## What it does
Skribbl.ai is a competitive drawing game where two players compete to replicate an image given through a prompt, all on a shared virtual whiteboard.

Elevated with real-time video, voice, and chat, players can communicate and collaborate while racing against time to impress the AI judge, which scores the drawings based on accuracy.

## How we built it
We used several key technologies:

- **100ms** for real-time video and voice features.
- **ChromaDB** to handle data storage for user interactions.
- **tldraw** and **tldraw sync** for real-time collaborative white-boarding primitives.
- **React** and **TypeScript** to power the frontend.
- **NextAuth** for user authentication and session management.

Despite starting at 9 pm on the Saturday before the hackathon's end, we managed to pivot from our original idea (voice-powered music production) and complete this project in record time.

## Challenges we ran into
- Our original idea of voice-powered music production wasn't compatible with the sponsor Hume's technology, forcing us to pivot.
- The tight deadline, constant pivoting, and beyond late start added additional pressure, but we powered through to deliver a fully functional app by the end of the hackathon.
- The Metreon WiFi, especially when building a network-heavy application, led to many hotspots and remote work.

## Accomplishments that we're proud of
We’re incredibly proud of how quickly we pivoted and built a polished app with video, voice, chat, and whiteboard integration in a matter of hours. Finishing the project under such time constraints felt like a huge accomplishment. Yeah, we may not have the crazy large feature set, but we do the one thing we planned to do, really well–at least, we think.

## What we learned
We learned how to adapt quickly when things don’t go as planned, and we gained valuable experience integrating real-time video and collaboration features with technologies like 100ms, ChromaDB, and tldraw. 
We also learned perseverance and pushing through idea droughts. Since we're working, adjusting "back" to the hackathon mindset definitely takes time.

## What's next for Skribbl.ai
We’re super stoked to continue improving Skribbl.ai after CalHacks. We surprisingly—especially given our execution, see potential for the app to be used in virtual team-building exercises, school clubs, and social hangouts. Stuff like:

- **Multiplayer modes:** Expand to support larger groups and team-based drawing challenges.
- **Advanced AI judging:** Improve the AI to evaluate drawings based on creativity, style, and time taken, not just accuracy.
- **Custom game modes:** Allow users to create custom challenges, themes, and rules for personalized gameplay.
- **Leaderboard and achievements:** Introduce a ranking system, badges, and awards for top players.
- **Mobile app:** Develop a mobile-friendly version to make the game accessible across different devices.
- **Interactive spectators:** Let spectators participate in the game through voting or live commenting during matches.
- **Real-time drawing hints:** Implement features where players can give or receive subtle hints during gameplay without breaking the challenge.
- **Custom avatars and themes:** Offer players options to personalize their in-game experience with unique avatars, themes, and board designs.

All this stuff seems super exciting to build, and we're glad to have a baseline to expand off of.

Well, that's it for skribbl.ai, thanks for reading!

## Built With
- 100ms
- ChromaDB
- Gemini
- GoDaddy
- OpenAI
- RadixUI
- React
- tldraw
- tRPC
- TypeScript

## Try it out
- [GitHub Repo](https://github.com/drawing-is-for-kids-they-say)
- [Play Skribbl.ai](https://drawing-is-for-kids-they-say.vercel.app)


## Detected evidence (automated analysis)

Indexed codebase: 96 recognized source files, 265 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (106 of 106)

```
.env.example
.eslintrc.cjs
.gitignore
bun.lockb
components.json
next.config.js
package.json
postcss.config.cjs
prettier.config.js
prisma/migrations/20241019234112_calhacks/migration.sql
prisma/migrations/20241020042528_embedding_model/migration.sql
prisma/migrations/migration_lock.toml
prisma/schema.prisma
README.md
src/app/api/auth/[...nextauth]/route.ts
src/app/api/trpc/[trpc]/route.ts
src/app/auth/signup/client-comp.tsx
src/app/auth/signup/confetti.tsx
src/app/auth/signup/page.tsx
src/app/layout.tsx
src/app/not-found.tsx
src/app/page.tsx
src/app/platform/choose/page.tsx
src/app/platform/create/page.tsx
src/app/platform/freeplay/page.tsx
src/app/platform/join/page.tsx
src/app/platform/layout.tsx
src/app/platform/leaderboard/page.tsx
src/app/platform/room/[lobbyCode]/lobby/page.tsx
src/app/platform/room/page.tsx
src/app/test.tsx
src/components/landing/hero.tsx
src/components/logo.tsx
src/components/mode-toggle.tsx
src/components/nav.tsx
src/components/signin-btn.tsx
src/components/theme-provider.tsx
src/components/ui/accordion.tsx
src/components/ui/alert-dialog.tsx
src/components/ui/alert.tsx
src/components/ui/aspect-ratio.tsx
src/components/ui/avatar.tsx
src/components/ui/background-gradient.tsx
src/components/ui/badge.tsx
src/components/ui/breadcrumb.tsx
src/components/ui/button.tsx
src/components/ui/calendar.tsx
src/components/ui/card.tsx
src/components/ui/carousel.tsx
src/components/ui/chart.tsx
src/components/ui/checkbox.tsx
src/components/ui/collapsible.tsx
src/components/ui/command.tsx
src/components/ui/context-menu.tsx
src/components/ui/dialog.tsx
src/components/ui/drawer.tsx
src/components/ui/dropdown-menu.tsx
src/components/ui/form.tsx
src/components/ui/hover-card.tsx
src/components/ui/input-otp.tsx
src/components/ui/input.tsx
src/components/ui/label.tsx
src/components/ui/menubar.tsx
src/components/ui/navigation-menu.tsx
src/components/ui/pagination.tsx
src/components/ui/popover.tsx
src/components/ui/progress.tsx
src/components/ui/radio-group.tsx
src/components/ui/resizable.tsx
src/components/ui/scroll-area.tsx
src/components/ui/select.tsx
src/components/ui/separator.tsx
src/components/ui/sheet.tsx
src/components/ui/sidebar.tsx
src/components/ui/skeleton.tsx
src/components/ui/slider.tsx
src/components/ui/sonner.tsx
src/components/ui/switch.tsx
src/components/ui/table.tsx
src/components/ui/tabs.tsx
src/components/ui/textarea.tsx
src/components/ui/toast.tsx
src/components/ui/toaster.tsx
src/components/ui/toggle-group.tsx
src/components/ui/toggle.tsx
src/components/ui/tooltip.tsx
src/env.js
src/hooks/use-mobile.tsx
src/hooks/use-toast.ts
src/lib/utils.ts
src/server/api/root.ts
src/server/api/routers/embedding.ts
src/server/api/routers/judge.ts
src/server/api/routers/post.ts
src/server/api/trpc.ts
src/server/auth.ts
src/server/chroma.ts
src/server/db.ts
src/styles/globals.css
src/trpc/query-client.ts
src/trpc/react.tsx
src/trpc/server.ts
src/types/content.d.ts
start-database.sh
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @auth/prisma-adapter@^1.6.0, @hookform/resolvers@^3.9.0, @prisma/client@^5.14.0, @radix-ui/react-accordion@^1.2.1, @radix-ui/react-alert-dialog@^1.1.2, @radix-ui/react-aspect-ratio@^1.1.0, @radix-ui/react-avatar@^1.1.1, @radix-ui/react-checkbox@^1.1.2, @radix-ui/react-collapsible@^1.1.1, @radix-ui/react-context-menu@^2.2.2, @radix-ui/react-dialog@^1.1.2, @radix-ui/react-dropdown-menu@^2.1.2, @radix-ui/react-hover-card@^1.1.2, @radix-ui/react-icons@^1.3.0, @radix-ui/react-label@^2.1.0, @radix-ui/react-menubar@^1.1.2, @radix-ui/react-navigation-menu@^1.2.1, @radix-ui/react-popover@^1.1.2, @radix-ui/react-progress@^1.1.0, @radix-ui/react-radio-group@^1.2.1, @radix-ui/react-scroll-area@^1.2.0, @radix-ui/react-select@^2.1.2, @radix-ui/react-separator@^1.1.0, @radix-ui/react-slider@^1.2.1, @radix-ui/react-slot@^1.1.0, @radix-ui/react-switch@^1.1.1, @radix-ui/react-tabs@^1.1.1, @radix-ui/react-toast@^1.2.2, @radix-ui/react-toggle@^1.1.0, @radix-ui/react-toggle-group@^1.1.0, @radix-ui/react-tooltip@^1.1.3, @t3-oss/env-nextjs@^0.10.1, @tanstack/react-query@^5.50.0, @tldraw/sync@^3.3.2, @tldraw/tldraw@^3.3.2, @trpc/client@^11.0.0-rc.446, @trpc/react-query@^11.0.0-rc.446, @trpc/server@^11.0.0-rc.446, @types/eslint@^8.56.10, @types/node@^20.14.10, @types/react@^18.3.3, @types/react-dom@^18.3.0, @typescript-eslint/eslint-plugin@^8.1.0, @typescript-eslint/parser@^8.1.0, @xenova/transformers@^2.17.2, chromadb@^1.9.2, chromadb-default-embed@^2.13.2, class-variance-authority@^0.7.0, clsx@^2.1.1, cmdk@1.0.0, date-fns@^4.1.0, embla-carousel-react@^8.3.0, eslint@^8.57.0, eslint-config-next@^14.2.4, framer-motion@^11.11.9, fs@^0.0.1-security, geist@^1.3.0, input-otp@^1.2.4, lucide-react@^0.453.0, mathjs@^13.2.0, next@^14.2.4, next-auth@^4.24.7, next-themes@^0.3.0, onnxruntime-node@^1.19.2, openai@^4.68.1, postcss@^8.4.39, prettier@^3.3.2, prettier-plugin-tailwindcss@^0.6.5, prisma@^5.14.0, react@^18.3.1, react-confetti-explosion@^2.1.2, react-day-picker@8.10.1, react-dom@^18.3.1, react-hook-form@^7.53.1, react-icons@^5.3.0, react-resizable-panels@^2.1.4, recharts@^2.13.0, server-only@^0.0.1, sonner@^1.5.0, superjson@^2.2.1, tailwind-merge@^2.5.4, tailwindcss@^3.4.3, tailwindcss-animate@^1.0.7, tldraw@^3.3.2, ts-pattern@^5.5.0, typescript@^5.5.3, uuid@^10.0.0, vaul@^1.1.0, zod@^3.23.8

### Recent commits (newest first)

- Update README.md
- fix styling stuff
- Merge pull request #9 from Nyumat/merge-fix
- reset changes
- fix env variables
- what's going on
- pointing to the wrong url
- pesky build fix?
- holy shit this is hard
- random room polish
- Hamza/init chroma (#6)
- support hiding close btn dialog
- stub lobby logic
- game over logic but RT somehow bricked?
- join room workin
- add dark mode support for editor
- fix responsiveness choose flow cards
- add platform layout
- add toaster for toast
- stub lobby page

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "muse",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "next build",
    "db:generate": "prisma migrate dev",
    "db:migrate": "prisma migrate deploy",
    "db:push": "prisma db push",
    "db:studio": "prisma studio",
    "dev": "next dev",
    "postinstall": "prisma generate",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@auth/prisma-adapter": "^1.6.0",
    "@hookform/resolvers": "^3.9.0",
    "@prisma/client": "^5.14.0",
    "@radix-ui/react-accordion": "^1.2.1",
    "@radix-ui/react-alert-dialog": "^1.1.2",
    "@radix-ui/react-aspect-ratio": "^1.1.0",
    "@radix-ui/react-avatar": "^1.1.1",
    "@radix-ui/react-checkbox": "^1.1.2",
    "@radix-ui/react-collapsible": "^1.1.1",
    "@radix-ui/react-context-menu": "^2.2.2",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-dropdown-menu": "^2.1.2",
    "@radix-ui/react-hover-card": "^1.1.2",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-menubar": "^1.1.2",
    "@radix-ui/react-navigation-menu": "^1.2.1",
    "@radix-ui/react-popover": "^1.1.2",
    "@radix-ui/react-progress": "^1.1.0",
    "@radix-ui/react-radio-group": "^1.2.1",
    "@radix-ui/react-scroll-area": "^1.2.0",
    "@radix-ui/react-select": "^2.1.2",
    "@radix-ui/react-separator": "^1.1.0",
    "@radix-ui/react-slider": "^1.2.1",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.1",
    "@radix-ui/react-tabs": "^1.1.1",
    "@radix-ui/react-toast": "^1.2.2",
    "@radix-ui/react-toggle": "^1.1.0",
    "@radix-ui/react-toggle-group": "^1.1.0",
    "@radix-ui/react-tooltip": "^1.1.3",
    "@t3-oss/env-nextjs": "^0.10.1",
    "@tanstack/react-query": "^5.50.0",
    "@tldraw/sync": "^3.3.2",
    "@tldraw/tldraw": "^3.3.2",
    "@trpc/client": "^11.0.0-rc.446",
    "@trpc/react-query": "^11.0.0-rc.446",
    "@trpc/server": "^11.0.0-rc.446",
    "@xenova/transformers": "^2.17.2",
    "chromadb": "^1.9.2",
    "chromadb-default-embed": "^2.13.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "cmdk": "1.0.0",
    "date-fns": "^4.1.0",
    "embla-carousel-react": "^8.3.0",
    "framer-motion": "^11.11.9",
    "fs": "^0.0.1-security",
    "geist": "^1.3.0",
    "input-otp": "^1.2.4",
    "lucide-react": "^0.453.0",
    "mathjs": "^13.2.0",
    "next": "^14.2.4",
    "next-auth": "^4.24.7",
    "next-themes": "^0.3.0",
    "onnxruntime-node": "^1.19.2",
    "openai": "^4.68.1",
    "react": "^18.3.1",
    "react-confetti-explosion": "^2.1.2",
    "react-day-picker": "8.10.1",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.53.1",
    "react-icons": "^5.3.0",
    "react-resizable-panels": "^2.1.4",
    "recharts": "^2.13.0",
    "server-only": "^0.0.1",
    "sonner": "^1.5.0",
    "superjson": "^2.2.1",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7",
    "tldraw": "^3.3.2",
    "ts-pattern": "^5.5.0",
    "uuid": "^10.0.0",
    "vaul": "^1.1.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/eslint": "^8.56.10",
    "@types/node": "^20.14.10",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^8.1.0",
    "@typescript-eslint/parser": "^8.1.0",
    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.4",
    "postcss": "^8.4.39",
    "prettier": "^3.3.2",
    "prettier-plugin-tailwindcss": "^0.6.5",
    "prisma": "^5.14.0",
    "tailwindcss": "^3.4.3",
    "typescript": "^5.5.3"
  },
  "ct3aMetadata": {
    "initVersion": "7.37.0"
  }
}

```

### src/app/page.tsx

```typescript
import { Hero } from "~/components/landing/hero";
import { Navbar } from "~/components/nav";
import { getServerAuthSession } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
// import { AddEmbeddingsButton, ComputeClipEmbeddingsButton } from "./test";
/*
 * T3 Gradient  bg-gradient-to-b from-[#2e026d] to-[#15162c]
 */
export default async function Home() {
  const user = await getServerAuthSession();
  return (
    <HydrateClient>
      <Navbar />
      <main className="flex flex-col items-center justify-center">
        <Hero session={user} />
        {/* {user && <AddEmbeddingsButton />} */}
        {/* {user && <ComputeClipEmbeddingsButton />} */}
      </main>
    </HydrateClient>
  );
}

```

### src/trpc/server.ts

```typescript
import "server-only";

import { createHydrationHelpers } from "@trpc/react-query/rsc";
import { headers } from "next/headers";
import { cache } from "react";

import { createCaller, type AppRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import { createQueryClient } from "./query-client";

/**
 * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
 * handling a tRPC call from a React Server Component.
 */
const createContext = cache(() => {
  const heads = new Headers(headers());
  heads.set("x-trpc-source", "rsc");

  return createTRPCContext({
    headers: heads,
  });
});

const getQueryClient = cache(createQueryClient);
const caller = createCaller(createContext);

export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
  caller,
  getQueryClient
);

```

### src/app/layout.tsx

```typescript
import "~/styles/globals.css";
import { TRPCReactProvider } from "~/trpc/react";

import { GeistSans } from "geist/font/sans";
import { type Metadata } from "next";
import { ThemeProvider } from "~/components/theme-provider";
import { Toaster } from "sonner";

export const metadata: Metadata = {
    metadataBase: new URL("https://drawing-is-for-kids-they-say.vercel.app/"),
    title: "Skribbl.ai - The Gameified Whiteboard for Friends, Teams, and Collaboration",
    description: "skribbl.ai is a gameified whiteboard for friends, teams, and collaboration. Play games, draw, and have fun with your friends!",
    icons: [{ rel: "icon", url: "/favicon.ico" }],
    openGraph: {
        title: "Skribbl.ai - The Gameified Whiteboard for Friends, Teams, and Collaboration",
        description: "skribbl.ai is a gameified whiteboard for friends, teams, and collaboration. Play games, draw, and have fun with your friends!",
        images: [{ url: "/og2.png" }],
    },
};

export default function RootLayout({
    children,
}: Readonly<{ children: React.ReactNode }>) {
    return (
        <html lang="en" className={`${GeistSans.variable}`}>
            <body>
                <ThemeProvider
                    attribute="class"
                    defaultTheme="system"
                    enableSystem
                    disableTransitionOnChange
                >
                    <TRPCReactProvider>{children}</TRPCReactProvider>
                    <Toaster />
                </ThemeProvider>
            </body>
        </html>
    );
}

```

### src/app/platform/layout.tsx

```typescript
"use client";

import { SessionProvider } from 'next-auth/react';

export default function PlatformLayout({ children }) {
    return (
        <div>
            <SessionProvider>
                {children}
            </SessionProvider>
        </div>
    )
}

```

### src/app/platform/leaderboard/page.tsx

```typescript

export default function LeaderboardPage() {
    return (
        <div>Leaderboard Page</div>
    )
}

```

### src/app/auth/signup/page.tsx

```typescript
import { ArrowLeft } from "lucide-react";
import { getProviders } from "next-auth/react";
import Link from "next/link";
import { SkribblLogo } from "~/components/logo";
import { SignInCard } from "./client-comp";

export default async function SignIn() {
    const providers = await getProviders();
    return (
        <div className="space-y-12 py-24">
            <div className="absolute top-4 left-4 m-12">
                <Link href="/">
                    <ArrowLeft
                        className="cursor-pointer text-blue-600"
                        size={20}
                    />
                    <span className="text-sm">Back</span>
                </Link>
            </div>
            <div>
                <div className="flex justify-center mr-2">
                    <SkribblLogo />
                </div>
            </div>
            <SignInCard providers={providers} />
        </div>
    );
}
```

### src/app/platform/freeplay/page.tsx

```typescript
/* eslint-disable react/no-unescaped-entities */
"use client";

import { setUserPreferences, Tldraw } from '@tldraw/tldraw';
import '@tldraw/tldraw/tldraw.css';
import { useSession } from 'next-auth/react';
import { useEffect } from 'react';

export default function WhiteboardPage({
    searchParams,
}: {
    params: { slug: string }
    searchParams: { [key: string]: string | string[] | undefined }
}) {
    const persistKey = searchParams.slug as string
    const { data } = useSession();
    const userId = data?.user?.id;
    useEffect(() => {
        if (userId) {
            setUserPreferences({
                colorScheme: "system",
                id: userId
            });
        }
    }, [userId]);
    return (
        <div className="flex h-screen bg-black text-white">
            <div className="flex-1 bg-gray-900 border border-gray-800">
                <Tldraw persistenceKey={userId} />
            </div>
        </div>
    )
}
```

### src/app/platform/join/page.tsx

```typescript
/* eslint-disable react/no-unescaped-entities */
"use client";

import { motion } from 'framer-motion';
import { ArrowLeft, ArrowRight, Users } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { SkribblLogo } from '~/components/logo';
import { Button } from '~/components/ui/button';
import { Input } from '~/components/ui/input';

export default function JoinRoomPage() {
    const [roomCode, setRoomCode] = useState('')
    const router = useRouter();
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault()
        router.push(`/platform/room?code=${roomCode}`)
    }

    return (
        <div className="min-h-screen bg-black text-white font-sans flex flex-col items-center justify-center p-4">
            <div className="absolute top-4 left-4 m-12">
                <Link href="/platform/choose">
                    <ArrowLeft
                        className="cursor-pointer text-blue-600"
                        size={20}
                    />
                    <span className="text-sm">Back</span>
                </Link>
            </div>
            <SkribblLogo className="mb-8" />
            <motion.div
                initial={{ opacity: 0, y: -20 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ duration: 0.5 }}
                className="w-full max-w-md"
            >
                <div className="text-center mb-8">
                    <Users className="w-16 h-16 mx-auto mb-4 text-blue-500" />
                    <h1 className="text-3xl md:text-4xl font-bold mb-2">Join a Room</h1>
                    <p className="text-gray-400">Enter the room code to join the game</p>
                </div>

                <form onSubmit={handleSubmit} className="space-y-4">
                    <div className="relative">
                        <Input
                            type="text"
                            value={roomCode}
                            onChange={(e) => setRoomCode(e.target.value.toUpperCase())}
                            placeholder="Enter Room Code"
                            className="w-full px-4 py-6 bg-gray-800 rounded-lg text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-600"
                        />
                    </div>
                    <Button
                        type="submit"
                        className="w-full px-4 py-3 bg-blue-500 rounded-lg text-xl font-semibold hover:bg-blue-600 transition-colors flex items-center justify-center"
                        disabled={roomCode.length < 4}
                        onClick={handleSubmit}
                    >
                        Join Room
                        <ArrowRight className="ml-2 w-5 h-5" />
                    </Button>
                </form>

                <motion.div
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    transition={{ delay: 0.5, duration: 0.5 }}
                    className="mt-8 text-center"
                >
                    <Link href="/platform/create" className="text-blue-500 hover:underline">
                        Don't have a code? Create a new room
                    </Link>
                </motion.div>
            </motion.div>
        </div>
    )
}
```

### src/app/platform/choose/page.tsx

```typescript
/* eslint-disable react/no-unescaped-entities */
'use client'
import { motion } from 'framer-motion'
import { PlusCircle, Users } from 'lucide-react'
import { signOut } from 'next-auth/react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { SkribblLogo } from '~/components/logo'
import { BackgroundGradient } from '~/components/ui/background-gradient'
import { Button } from '~/components/ui/button'
import { cn } from '~/lib/utils'

export default function ChooseFlowPage() {
    const router = useRouter()
    return (
        <div className="min-h-screen">
            <header className="flex justify-between items-center p-4 md:p-6">
                <SkribblLogo />
                <nav className="hidden md:flex space-x-6">
                    <Link href="/platform/create" className="hover:text-blue-500 transition-colors">Create Room</Link>
                    <Link href="/platform/join" className="hover:text-blue-500 transition-colors">Join Room</Link>
                    <Link href="/platform/leaderboard" className="hover:text-blue-500 transition-colors">Leaderboard</Link>
                </nav>
                <Button className='bg-blue-600' onClick={() => {
                    signOut({ callbackUrl: '/' })
                }}>Log Out</Button>
            </header>

            <main className="flex flex-col items-center justify-center px-4 py-8 text-center">
                <motion.h1
                    initial={{ opacity: 0, y: -20 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ duration: 0.5 }}
                    className="text-3xl md:text-4xl font-bold mb-6"
                >
                    Choose Your Flow
                </motion.h1>

                <motion.p
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    transition={{ delay: 0.2, duration: 0.5 }}
                    className="text-lg md:text-xl text-gray-400 mb-12 max-w-2xl"
                >
                    Select how you'd like to start your skribbl.ai experience!
                </motion.p>

                <div className="grid grid-cols-1 md:grid-cols-2 gap-20 w-full max-w-lg lg:max-w-3xl">
                    <OptionCard
                        icon={<Users className="w-12 h-12 mb-4" />}
                        title="Join a Room"
                        description="Enter an existing room and collaborate with others."
                        onClick={() => router.push('/platform/join')}
                    />
                    <OptionCard
                        icon={<PlusCircle className="w-12 h-12 mb-4" />}
                        title="Create a Room"
                        description="Start a new room and invite others to join you."
                        onClick={() => router.push(`/platform/create?id=${Math.random().toString(36).substring(7)}`)}
                    />
                </div>
                <div className="flex flex-col items-center justify-center mt-16">
                    <div className="text-gray-400 text-sm mr-4">Not interested?</div>
                    <Button variant='link' onClick={() => router.push('/platform/freeplay')}>Yeah, no thanks. I just want to skribbl on my own.</Button>
                </div>
            </main>
        </div >
    )
}

function OptionCard({ icon, title, description, onClick }) {
    return (
        <>
            <BackgroundGradient className={cn("group relative w-full rounded-3xl")}>
                <motion.button
                    whileTap={{ scale: 0.95 }}
                    className="flex flex-col items-center justify-center p-6 w-full shadow-lg bg-black/70 rounded-3xl text-white transition-colors group-hover:bg-black/80"
                    onClick={onClick}
                >
                    <motion.div
                        initial={{ scale: 0 }}
                        animate={{ scale: 1 }}
                        transition={{ type: 'spring', stiffness: 300, damping: 20 }}
                    >
                        {icon}
                    </motion.div>
                    <h2 className="text-2xl font-semibold mt-4 mb-2">{title}</h2>
                    <p className="text-gray-400">{description}</p>
                </motion.button>
            </BackgroundGradient>
        </>
    )
}
```

[86 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]