# Project export: Sventos

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: TreeHacks 2025
- Tagline: An educator's goal is to provide immersive learning experiences, a hard task in the online era. Every student has different needs, and they are entitled to better control of their online learning.
- Devpost: https://devpost.com/software/sventos
- GitHub: https://github.com/aksharye/sventos
- Demo: https://sventos.vercel.app/
- Video: https://www.youtube.com/embed/jWAwiOm4lhs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Akshar Yeccherla (6 commits)

## Devpost submission (written by the team)

### Inspiration

The inspiration of this project was from my own experiences with Zoom. With sometimes having bad eyesight, I wish I had better access to zooming into certain windows and prioritizing different frames that were presented to me during virtual school.

### What it does

Sventos is built on the Zoom Video SDK Next.js template, which allows 1-1 communication using Zoom's APIs. I then extended this to support multiple users, and most importantly, screensharing, making up the key elements of virtual school. Sventos gives users fine control over what they view from meetings/presentations/webinars by being able to move and resize their frames, allowing users quality of life and educators/content-creators the ability to create immersive education/other immersive experiences.

### How we built it

Sventos is built as a fork of the Zoom Video SDK Next.js template, using Next.js, tailwind, and Node.js for the API routes. I further used Windsurf, the agentic AI IDE, to assist me with tricky tasks and overall frontend design.

### Challenges we ran into

Understanding the Zoom API was difficult, but it helped me understand how much control it could give to users if used correctly. Extending the front-end to allow dragging and resizing of frames was a very time-consuming product.

### Accomplishments we're proud of

Constructing the MVP, I thought I wouldn't be able to finish it at some point, but I was able to get a majority of what I wanted to on the board. It also helped me understand the lengthiness of the ideation and building process.

### What we learned

Software is tricky, but it doesn't always have to be complicated to make an impact. While it wasn't complicated in nature, I believe Sventos has a lot of future potential.

### What's next

Supporting multi-screen sharing, I still need to explore more of the Zoom API to see if this is possible, but this may require me to write certain components from scratch. I want to hash this out to meet education use-cases and beyond, as I feel this has good potential for content-creators, especially in the streaming (see, Twitch) industry.

## README (from the GitHub repository)

# Zoom VideoSDK Next.js Quickstart

This repo contains a quickstart for using the Zoom VideoSDK with Next.js. There's a quickstart for both the Next.js App Router and the Pages Router. You can use either the [`app-router`](https://github.com/zoom/videosdk-nextjs-quickstart/tree/app-router) or the [`pages-router`](https://github.com/zoom/videosdk-nextjs-quickstart/tree/pages-router) branch of this repository.

You can learn more about the differences [here](https://nextjs.org/docs#app-router-vs-pages-router).

## Prerequisites

- Node LTS
- Bun (or NPM)
- Zoom Video SDK Account

## Getting Started

1. Clone the repository

```bash
$ git clone https://github.com/zoom/videosdk-nextjs-quickstart.git
```

2. Install dependencies

```bash
$ bun install # or npm install
```

3. Copy `.env.example` to `.env` and fill in your Zoom Video SDK credentials as:

```bash
ZOOM_SDK_KEY="your-key"
ZOOM_SDK_SECRET="your-secret"
```

4. Start the development server

```bash
$ bun dev # or npm run dev
```

You can open [http://localhost:3000](http://localhost:3000) with your browser to view the project.


## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 231 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (27 of 27)

```
.env.example
.eslintrc.json
.gitignore
.prettierrc
bun.lockb
components.json
next.config.mjs
package.json
postcss.config.mjs
public/coi-serviceworker.js
public/draggable.min.js
README.md
src/app/call/[slug]/page.tsx
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/components/DraggableResizable.tsx
src/components/MuteButtons.tsx
src/components/ui/button.tsx
src/components/ui/input.tsx
src/components/Videocall.tsx
src/data/getToken.ts
src/lib/utils.ts
src/utils/layoutUtils.ts
src/utils/videoUtils.ts
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @dnd-kit/core@^6.3.1, @dnd-kit/utilities@^3.2.2, @radix-ui/react-slot@^1.0.2, @types/jsrsasign@^10.5.14, @types/node@^20, @types/react@^18, @types/react-dom@^18, @zoom/videosdk@^1.11.0, class-variance-authority@^0.7.0, clsx@^2.1.1, eslint@^8, eslint-config-next@14.2.3, jsrsasign@^11.1.0, lucide-react@^0.378.0, next@14.2.3, postcss@^8, re-resizable@^6.10.3, react@^18, react-dom@^18, react-draggable@^4.4.6, react-rnd@^10.4.14, server-only@^0.0.1, tailwind-merge@^2.3.0, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5

### Recent commits (newest first)

- fix video player container error
- final changes and build error fixes
- resizability
- UI updates, dragging
- screensharing capabilities
- attempt to add screenshare and other features
- feat: remove useWorkAroundForSafari
- chore: clean up some code for the blog
- fix: css and markup
- chore: prettier
- chore: readme
- feat: move token logic to server-only file
- feat: init videocall
- Initial commit from Create Next App

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

### package.json

```
{
  "name": "videosdk-nextjs-quickstart",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@dnd-kit/core": "^6.3.1",
    "@dnd-kit/utilities": "^3.2.2",
    "@radix-ui/react-slot": "^1.0.2",
    "@types/jsrsasign": "^10.5.14",
    "@zoom/videosdk": "^1.11.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "jsrsasign": "^11.1.0",
    "lucide-react": "^0.378.0",
    "next": "14.2.3",
    "re-resizable": "^6.10.3",
    "react": "^18",
    "react-dom": "^18",
    "react-draggable": "^4.4.6",
    "react-rnd": "^10.4.14",
    "server-only": "^0.0.1",
    "tailwind-merge": "^2.3.0",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.3",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import type { Viewport } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

export const viewport: Viewport = {
  initialScale: 1.0,
  width: "device-width",
};
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Sventos",
  description: "Zoom x Custom Layouts",
  icons: {
    icon: "/favicon.svg",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <head>
        <script src="/draggable.min.js" defer></script>
      </head>
      <body className={inter.className}>{children}</body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
import { useState } from "react";

export default function Home() {
  const [sessionName, setSessionName] = useState("");
  const router = useRouter();
  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24 bg-blue-900 text-white">
      <h1 className="text-4xl font-bold text-center my-4">
        Sventos
      </h1>
      <Input
        type="text"
        className="w-full text-black max-w-xs border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
        placeholder="Session Name"
        value={sessionName}
        onChange={(e) => setSessionName(e.target.value)}
      />
      <Button
        className="w-full max-w-xs mt-8 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-full shadow-lg transition duration-300 ease-in-out transform hover:scale-105"
        disabled={!sessionName}
        onClick={() => router.push(`/call/${sessionName}`)}
      >
        Create Session
      </Button>
    </main>
  );
}

```

### src/app/call/[slug]/page.tsx

```typescript
import { getData } from "@/data/getToken";
import dynamic from "next/dynamic";
import Script from "next/script";

const Videocall = dynamic<{ slug: string; JWT: string }>(
  () => import("../../../components/Videocall"),
  { ssr: false },
);

export default async function Page({ params }: { params: { slug: string } }) {
  const jwt = await getData(params.slug);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Videocall slug={params.slug} JWT={jwt} />
      <Script src="/coi-serviceworker.js" strategy="beforeInteractive" />
    </main>
  );
}

```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss"

const config = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
	],
  prefix: "",
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
      keyframes: {
        "accordion-down": {
          from: { height: "0" },
          to: { height: "var(--radix-accordion-content-height)" },
        },
        "accordion-up": {
          from: { height: "var(--radix-accordion-content-height)" },
          to: { height: "0" },
        },
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
} satisfies Config

export default config
```

### public/coi-serviceworker.js

```javascript
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
//@ts-nocheck
let coepCredentialless = false;
if (typeof window === "undefined") {
  self.addEventListener("install", () => self.skipWaiting());
  self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));

  self.addEventListener("message", (ev) => {
    if (!ev.data) {
      return;
    } else if (ev.data.type === "deregister") {
      self.registration
        .unregister()
        .then(() => {
          return self.clients.matchAll();
        })
        .then((clients) => {
          clients.forEach((client) => client.navigate(client.url));
        });
    } else if (ev.data.type === "coepCredentialless") {
      coepCredentialless = ev.data.value;
    }
  });

  self.addEventListener("fetch", function (event) {
    const r = event.request;
    if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
      return;
    }

    const request =
      coepCredentialless && r.mode === "no-cors"
        ? new Request(r, {
            credentials: "omit",
          })
        : r;
    event.respondWith(
      fetch(request)
        .then((response) => {
          if (response.status === 0) {
            return response;
          }

          const newHeaders = new Headers(response.headers);
          newHeaders.set("Cross-Origin-Embedder-Policy", coepCredentialless ? "credentialless" : "require-corp");
          if (!coepCredentialless) {
            newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
          }
          newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");

          return new Response(response.body, {
            status: response.status,
            statusText: response.statusText,
            headers: newHeaders,
          });
        })
        .catch((e) => console.error(e))
    );
  });
} else {
  (() => {
    const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf");
    window.sessionStorage.removeItem("coiReloadedBySelf");
    const coepDegrading = reloadedBySelf == "coepdegrade";

    // You can customize the behavior of this script through a global `coi` variable.
    const coi = {
      shouldRegister: () => !reloadedBySelf,
      shouldDeregister: () => false,
      coepCredentialless: () => true,
      coepDegrade: () => true,
      doReload: () => window.location.reload(),
      quiet: false,
      ...window.coi,
    };

    const n = navigator;
    const controlling = n.serviceWorker && n.serviceWorker.controller;

    // Record the failure if the page is served by serviceWorker.
    if (controlling && !window.crossOriginIsolated) {
      window.sessionStorage.setItem("coiCoepHasFailed", "true");
    }
    const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed");

    if (controlling) {
      // Reload only on the first failure.
      const reloadToDegrade = coi.coepDegrade() && !(coepDegrading || window.crossOriginIsolated);
      n.serviceWorker.controller.postMessage({
        type: "coepCredentialless",
        value: reloadToDegrade || (coepHasFailed && coi.coepDegrade()) ? false : coi.coepCredentialless(),
      });
      if (reloadToDegrade) {
        !coi.quiet && console.log("Reloading page to degrade COEP.");
        window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade");
        coi.doReload("coepdegrade");
      }

      if (coi.shouldDeregister()) {
        n.serviceWorker.controller.postMessage({ type: "deregister" });
      }
    }

    // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
    // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
    if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;

    if (!window.isSecureContext) {
      !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
      return;
    }

    // In some environments (e.g. Firefox private mode) this won't be available
    if (!n.serviceWorker) {
      !coi.quiet && console.error("COOP/COEP Service Worker not registered, perhaps due to private mode.");
      return;
    }

    n.serviceWorker.register(window.document.currentScript.src).then(
      (registration) => {
        !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);

        registration.addEventListener("updatefound", () => {
          !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
          window.sessionStorage.setItem("coiReloadedBySelf", "updatefound");
          coi.doReload();
        });

        // If the registration is active, but it's not controlling the page
        if (registration.active && !n.serviceWorker.controller) {
          !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
          window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling");
          coi.doReload();
        }
      },
      (err) => {
        !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
      }
    );
  })();
}

```

### src/lib/utils.ts

```typescript
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { type VideoClient } from "@zoom/videosdk";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

```

### src/utils/layoutUtils.ts

```typescript
export const getRandomPosition = (maxX: number, maxY: number) => {
  const x = Math.random() * maxX;
  const y = Math.random() * maxY;
  return { x, y };
};

export const calculateMaxDimensions = (
  containerRect: DOMRect,
  elementWidth: number,
  elementHeight: number
) => {
  const maxX = containerRect.width - elementWidth;
  const maxY = containerRect.height - elementHeight;
  return { maxX, maxY };
};

```

### src/utils/videoUtils.ts

```typescript
import ZoomVideo, { VideoQuality, VideoPlayer } from "@zoom/videosdk";

export const startVideoStream = async (
  mediaStream: any,
  videoElement: HTMLVideoElement,
  userId: number,
  isLocalUser: boolean
) => {
  if (isLocalUser) {
    await mediaStream.startVideo({ videoElement });
  } else {
    await mediaStream.renderVideo(videoElement, userId, 640, 360, 0, 0, VideoQuality.Video_360P);
  }
};

export const stopVideoStream = async (
  mediaStream: any,
  userId: number,
  isLocalUser: boolean
) => {
  if (isLocalUser) {
    await mediaStream.stopVideo();
  } else {
    await mediaStream.stopRenderVideo(userId);
  }
};

```

### src/components/DraggableResizable.tsx

```typescript
"use client";

import { useRef } from "react";
import { Rnd } from "react-rnd";

const DraggableResizable = ({ children }:   { children: any }) => {
  const nodeRef = useRef(null);

  return (
    <Rnd
      default={{
        x: 50,
        y: 50,
        width: 320,
        height: 240,
      }}
      minWidth={200}
      minHeight={150}
      bounds="parent"
      enableResizing={{
        top: true,
        right: true,
        bottom: true,
        left: true,
        topRight: true,
        bottomRight: true,
        bottomLeft: true,
        topLeft: true,
      }}
      nodeRef={nodeRef}
    >
      <div ref={nodeRef} className="relative border border-gray-400 bg-black">
        {children}
      </div>
    </Rnd>
  );
};

export default DraggableResizable;

```

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