Initial WatchLink scaffold
This commit is contained in:
35
src/components/app-shell.tsx
Normal file
35
src/components/app-shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
131
src/components/room-console.tsx
Normal file
131
src/components/room-console.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/components/status-badge.tsx
Normal file
5
src/components/status-badge.tsx
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user