Initial WatchLink scaffold
Some checks failed
Build / build (push) Failing after 1m29s
Release Dry Run / release-dry-run (push) Successful in 1m24s
Template Compliance / compliance (push) Failing after 5s

This commit is contained in:
MrSphay
2026-05-15 03:11:41 +02:00
commit d3e84feedd
51 changed files with 2215 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import Link from "next/link";
import { Gauge, MonitorPlay, Shield, UserRoundPlus, UsersRound } from "lucide-react";
const nav = [
{ href: "/dashboard", label: "Dashboard", icon: Gauge },
{ href: "/rooms/@admin", label: "Rooms", icon: MonitorPlay },
{ href: "/friends", label: "Friends", icon: UsersRound },
{ href: "/admin", label: "Admin", icon: Shield },
{ href: "/setup", label: "Setup", icon: UserRoundPlus }
];
export function AppShell({ children, active = "Dashboard" }: { children: React.ReactNode; active?: string }) {
return (
<div className="app-shell">
<aside className="sidebar">
<Link className="brand" href="/dashboard">
<span className="brand-mark">WL</span>
<span>WatchLink</span>
</Link>
<nav className="nav-list" aria-label="Primary">
{nav.map((item) => {
const Icon = item.icon;
return (
<Link key={item.href} href={item.href} className={`nav-item ${active === item.label ? "active" : ""}`}>
<Icon size={17} />
{item.label}
</Link>
);
})}
</nav>
</aside>
<main className="main">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,131 @@
"use client";
import { useMemo, useState } from "react";
import { Pause, Play, Radio, SkipForward } from "lucide-react";
import { io } from "socket.io-client";
import { normalizeMediaUrl } from "@/lib/media";
import { activity, participants, queue } from "@/lib/sample-data";
import { StatusBadge } from "./status-badge";
const socket = io({
path: "/api/socket",
autoConnect: false
});
export function RoomConsole({ roomSlug = "@admin" }: { roomSlug?: string }) {
const [connected, setConnected] = useState(false);
const [source, setSource] = useState("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
const media = useMemo(() => normalizeMediaUrl(source), [source]);
function connect() {
if (!socket.connected) {
socket.connect();
socket.emit("room:join", { roomSlug, user: "Admin" });
}
setConnected(true);
}
function emit(event: "media:set" | "playback:play" | "playback:pause" | "playback:seek") {
connect();
socket.emit(event, {
provider: media.provider,
originalUrl: media.originalUrl,
playbackUrl: media.playbackUrl,
position: event === "playback:seek" ? 82 : undefined
});
}
return (
<div className="room-layout">
<section className="panel">
<div className="panel-header">
<div>
<h2>{roomSlug}</h2>
<span className="eyebrow">Persistent room</span>
</div>
<StatusBadge tone={connected ? "good" : "warn"}>{connected ? "Online" : "Local preview"}</StatusBadge>
</div>
<div className="video-frame">
<div className="video-state">
<StatusBadge tone="good">{media.provider}</StatusBadge>
<h2>Shared playback state</h2>
<p>
Participants in this room can set the source, play, pause, and seek. Late joiners receive the latest room
state from the realtime server.
</p>
<p className="eyebrow">{media.playbackUrl}</p>
</div>
</div>
<div className="controls">
<button className="button primary" onClick={() => emit("playback:play")}>
<Play size={16} /> Play
</button>
<button className="button" onClick={() => emit("playback:pause")}>
<Pause size={16} /> Pause
</button>
<input
className="input"
aria-label="Source URL"
value={source}
onChange={(event) => setSource(event.target.value)}
placeholder="Source URL"
/>
<button className="button" onClick={() => emit("media:set")}>
<Radio size={16} /> Source URL
</button>
<button className="button" onClick={() => emit("playback:seek")}>
<SkipForward size={16} /> Seek
</button>
</div>
</section>
<aside className="stack">
<Panel title="Queue">
{queue.map((item) => (
<div className="row" key={item.title}>
<div className="row-title">
<strong>{item.title}</strong>
<span>
{item.provider} by {item.by}
</span>
</div>
<StatusBadge>{item.duration}</StatusBadge>
</div>
))}
</Panel>
<Panel title="Participants">
{participants.map((item) => (
<div className="row" key={item.name}>
<div className="row-title">
<strong>{item.name}</strong>
<span>{item.role}</span>
</div>
<StatusBadge tone={item.status === "Buffering" ? "warn" : "good"}>{item.status}</StatusBadge>
</div>
))}
</Panel>
<Panel title="Activity">
{activity.map((item) => (
<div className="row" key={item}>
<div className="row-title">
<strong>{item}</strong>
<span>just now</span>
</div>
</div>
))}
</Panel>
</aside>
</div>
);
}
function Panel({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="panel">
<div className="panel-header">
<h3>{title}</h3>
</div>
<div className="panel-body">{children}</div>
</section>
);
}

View File

@@ -0,0 +1,5 @@
import { clsx } from "clsx";
export function StatusBadge({ children, tone = "neutral" }: { children: React.ReactNode; tone?: string }) {
return <span className={clsx("badge", tone)}>{children}</span>;
}