Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions examples/turbosync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/
# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# cloudflare
/.wrangler
/dist/
37 changes: 37 additions & 0 deletions examples/turbosync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# TurboSync

Synchronized video playback rooms. Watch videos with friends online with real-time playback sync using Cloudflare Durable Objects.

## What It Does

TurboSync lets you create rooms where everyone watches videos together in perfect sync. Play, pause, seek, or change the volume — it happens instantly for everyone in the room. Create public rooms or protect them with passwords for private viewing sessions.

## Why It's Awesome

- **Zero setup** — Just open the site, create a room, and share the link
- **Real-time sync** — WebSocket-powered communication keeps everyone on the same timestamp
- **Cloudflare-powered** — Built on Durable Objects with SQLite for rock-solid state management
- **Next.js made portable** — Originally a Next.js app, now running on Cloudflare Workers thanks to vinext
- **Beautiful UI** — Dark/light mode, responsive design, modern interface

## Tech Stack

- Next.js (App Router) → deployed via [vinext](https://github.com/anomalyco/vinext)
- Cloudflare Workers + Durable Objects
- Tailwind CSS + shadcn/ui

## Live Demo

**https://turbosync.devshell.blog**

Or: `turbosync.alisanan9090.workers.dev`

## Local Development

```bash
cd turbosync
pnpm install
pnpm dev
```

Open http://localhost:5173
Binary file added examples/turbosync/app/favicon.ico
Binary file not shown.
126 changes: 126 additions & 0 deletions examples/turbosync/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";

@custom-variant dark (&:is(.dark *));

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}

:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
36 changes: 36 additions & 0 deletions examples/turbosync/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "TurboSync",
description: "Synchronized video playback rooms",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Toaster richColors position="bottom-right" />
</body>
</html>
);
}
143 changes: 143 additions & 0 deletions examples/turbosync/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"use client";

import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Video,
Zap,
Lock,
ChevronRight,
Activity,
MonitorPlay,
} from "lucide-react";

export default function LandingPage() {
return (
<div className="min-h-screen bg-black text-white font-sans selection:bg-white/20">
{/* Navigation */}
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-white/10 bg-black/50 backdrop-blur-md">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="flex items-center gap-2 font-bold text-lg tracking-tight">
<Video size={20} className="text-white" />
TurboSync
</div>
<div className="flex items-center gap-4">
<Link
href="/room"
className="text-sm text-[#A1A1AA] hover:text-white transition-colors font-medium"
>
Join Room
</Link>
<Link href="/room">
<span className="inline-flex h-9 items-center justify-center rounded-full bg-white px-4 py-2 text-sm font-medium text-black transition-colors hover:bg-gray-200">
Start Syncing
</span>
</Link>
</div>
</div>
</nav>

{/* Hero Section */}
<main className="pt-32 pb-16 px-6 relative overflow-hidden">
{/* Glow effect */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[400px] bg-white/5 rounded-full blur-[120px] pointer-events-none" />

<div className="max-w-4xl mx-auto text-center relative z-10">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs text-[#A1A1AA] font-medium mb-8">
<Activity size={14} className="text-green-400" />
<span className="opacity-80">Real-time local playback</span>
</div>

<h1 className="text-5xl md:text-7xl font-bold tracking-tighter mb-6 bg-clip-text text-transparent bg-gradient-to-br from-white via-white to-gray-500">
Watch together.
<br />
Without the buffer.
</h1>

<p className="text-lg md:text-xl text-[#A1A1AA] mb-10 max-w-2xl mx-auto font-medium tracking-tight">
TurboSync uses WebSockets to perfectly synchronize local video files
across multiple devices. No streaming servers. No latency. Crystal
clear native quality.
</p>

<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link href="/room">
<Button
size="lg"
className="h-12 px-8 rounded-full bg-white text-black hover:bg-gray-200 text-base font-semibold"
>
Create a Room
<ChevronRight size={16} className="ml-1 opacity-50" />
</Button>
</Link>
<Link href="/room">
<Button
size="lg"
variant="outline"
className="h-12 px-8 rounded-full border-white/10 bg-white/5 text-white hover:bg-white/10 text-base font-semibold"
>
Join existing
</Button>
</Link>
</div>
</div>

{/* Feature Grid */}
<div className="max-w-6xl mx-auto mt-32 grid md:grid-cols-3 gap-6">
<FeatureCard
icon={<Zap size={24} className="text-white" />}
title="Zero Latency"
description="Videos remain on your local disk. We only sync the play/pause actions and timestamps via ultra-fast WebSockets."
/>
<FeatureCard
icon={<MonitorPlay size={24} className="text-white" />}
title="Native Quality"
description="Say goodbye to blocky screen sharing. Watch your 4K HDR files in pristine native quality with 0 drop in bitrate."
/>
<FeatureCard
icon={<Lock size={24} className="text-white" />}
title="Secure & Private"
description="Your media files never touch our servers. Room metadata is ephemeral, fast and securely authenticated."
/>
</div>
</main>

{/* Footer */}
<footer className="border-t border-white/10 mt-20">
<div className="max-w-7xl mx-auto px-6 py-8 flex flex-col md:flex-row items-center justify-between text-sm text-[#A1A1AA] font-medium">
<div className="flex items-center gap-2">
<Video size={16} />
TurboSync
</div>
<p className="mt-4 md:mt-0">
© {new Date().getFullYear()} TurboSync. Built for synchronous play.
</p>
</div>
</footer>
</div>
);
}

function FeatureCard({
icon,
title,
description,
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="bg-white/5 border border-white/10 rounded-2xl p-8 hover:bg-white/[0.07] transition-colors">
<div className="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mb-6 border border-white/5">
{icon}
</div>
<h3 className="text-xl font-bold mb-3 text-white tracking-tight">
{title}
</h3>
<p className="text-[#A1A1AA] leading-relaxed font-medium">
{description}
</p>
</div>
);
}
Loading