302 lines
9.1 KiB
TypeScript
302 lines
9.1 KiB
TypeScript
/**
|
|
* rTube demo page — server-rendered HTML body.
|
|
*
|
|
* Video library with sidebar, search, player area, download/copy-link buttons.
|
|
* Client-side tube-demo.ts handles selection, filtering, and playback.
|
|
*/
|
|
|
|
/* ─── Seed Data ─────────────────────────────────────────────── */
|
|
|
|
interface DemoVideo {
|
|
name: string;
|
|
size: number;
|
|
}
|
|
|
|
const DEMO_VIDEOS: DemoVideo[] = [
|
|
{ name: "lac-blanc-test-footage.mp4", size: 245_000_000 },
|
|
{ name: "chamonix-arrival-timelapse.mp4", size: 128_000_000 },
|
|
{ name: "matterhorn-sunset-4k.mp4", size: 512_000_000 },
|
|
{ name: "paragliding-zermatt.webm", size: 89_000_000 },
|
|
{ name: "glacier-paradise-walk.mp4", size: 340_000_000 },
|
|
{ name: "tre-cime-circuit-gopro.mp4", size: 420_000_000 },
|
|
{ name: "dolomites-drone-footage.mkv", size: 780_000_000 },
|
|
{ name: "group-dinner-recap.mp4", size: 67_000_000 },
|
|
];
|
|
|
|
/* ─── Helpers ──────────────────────────────────────────────── */
|
|
|
|
function formatSize(bytes: number): string {
|
|
if (!bytes) return "";
|
|
const units = ["B", "KB", "MB", "GB"];
|
|
let i = 0;
|
|
let b = bytes;
|
|
while (b >= 1024 && i < units.length - 1) {
|
|
b /= 1024;
|
|
i++;
|
|
}
|
|
return `${b.toFixed(1)} ${units[i]}`;
|
|
}
|
|
|
|
function getIcon(filename: string): string {
|
|
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
if (["mp4", "webm", "mov"].includes(ext)) return "\u{1F3AC}";
|
|
if (["mkv", "avi"].includes(ext)) return "\u26A0\uFE0F";
|
|
return "\u{1F4C4}";
|
|
}
|
|
|
|
function isPlayable(filename: string): boolean {
|
|
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
return ["mp4", "webm", "mov", "ogg", "m4v"].includes(ext);
|
|
}
|
|
|
|
/* ─── Render ─────────────────────────────────────────────── */
|
|
|
|
export function renderDemo(): string {
|
|
const videoListHTML = DEMO_VIDEOS.map(
|
|
(v) => `
|
|
<li class="rd-tube-item${!isPlayable(v.name) ? " rd-tube-item--unplayable" : ""}"
|
|
data-video="${v.name}" role="button" tabindex="0">
|
|
<span class="rd-tube-item__icon">${getIcon(v.name)}</span>
|
|
<span class="rd-tube-item__name" title="${v.name}">${v.name}</span>
|
|
<span class="rd-tube-item__size">${formatSize(v.size)}</span>
|
|
</li>`,
|
|
).join("\n");
|
|
|
|
return `
|
|
<div class="rd-root" style="--rd-accent-from:#ef4444; --rd-accent-to:#ec4899">
|
|
|
|
<!-- ── Hero ── -->
|
|
<section class="rd-hero">
|
|
<h1>Video Library</h1>
|
|
<p class="rd-subtitle">Browse, preview, and download videos from the Alpine Explorer expedition</p>
|
|
<div class="rd-meta">
|
|
<span>\u{1F3AC} ${DEMO_VIDEOS.length} videos</span>
|
|
<span style="color:#475569">|</span>
|
|
<span>\u{1F4BE} ${formatSize(DEMO_VIDEOS.reduce((sum, v) => sum + v.size, 0))} total</span>
|
|
<span style="color:#475569">|</span>
|
|
<span>\u{1F3D4} Alpine Explorer 2026</span>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Two-column layout ── -->
|
|
<div class="rd-section">
|
|
<div class="rd-tube-layout">
|
|
|
|
<!-- Sidebar -->
|
|
<aside class="rd-tube-sidebar" id="rd-video-sidebar">
|
|
<input type="text" id="rd-video-search" class="rd-tube-search"
|
|
placeholder="Search videos..." autocomplete="off" />
|
|
<h2 class="rd-tube-sidebar__heading">Library</h2>
|
|
<ul class="rd-tube-list" id="rd-video-list">
|
|
${videoListHTML}
|
|
</ul>
|
|
<p class="rd-tube-empty" id="rd-video-empty" style="display:none">No videos found</p>
|
|
</aside>
|
|
|
|
<!-- Player area -->
|
|
<div class="rd-tube-player-wrap" id="rd-video-player-area">
|
|
<div class="rd-tube-player" id="rd-player-container">
|
|
<p class="rd-tube-placeholder" id="rd-player-placeholder">Select a video to play</p>
|
|
</div>
|
|
|
|
<!-- Info bar (hidden until a video is selected) -->
|
|
<div class="rd-tube-info" id="rd-video-info" style="display:none">
|
|
<p class="rd-tube-info__name" id="rd-video-name"></p>
|
|
<div class="rd-tube-info__actions">
|
|
<a id="rd-download-btn" href="#" download class="rd-btn rd-btn--ghost rd-btn--sm">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
Download
|
|
</a>
|
|
<button id="rd-copy-link-btn" class="rd-btn rd-btn--ghost rd-btn--sm">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
Copy Link
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── CTA ── -->
|
|
<section class="rd-section rd-section--narrow">
|
|
<div class="rd-cta">
|
|
<h2>Host Your Video Library</h2>
|
|
<p>
|
|
rTube gives your community a private video hosting solution with streaming,
|
|
uploads, and live broadcasting — all powered by Cloudflare R2.
|
|
</p>
|
|
<a href="/create-space" style="background:linear-gradient(135deg,#ef4444,#ec4899); box-shadow:0 8px 24px rgba(239,68,68,0.25);">
|
|
Create Your Space
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
|
|
<style>
|
|
/* ── Tube demo layout ── */
|
|
.rd-tube-layout {
|
|
display: grid;
|
|
grid-template-columns: 300px 1fr;
|
|
gap: 1.5rem;
|
|
align-items: start;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.rd-tube-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* ── Sidebar ── */
|
|
.rd-tube-sidebar {
|
|
background: rgba(30, 41, 59, 0.5);
|
|
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
border-radius: 1rem;
|
|
padding: 1rem;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
.rd-tube-search {
|
|
width: 100%;
|
|
padding: 0.625rem 0.875rem;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid #334155;
|
|
border-radius: 0.5rem;
|
|
color: #f1f5f9;
|
|
font-size: 0.875rem;
|
|
margin-bottom: 0.75rem;
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
box-sizing: border-box;
|
|
}
|
|
.rd-tube-search::placeholder { color: #64748b; }
|
|
.rd-tube-search:focus { border-color: #ef4444; }
|
|
.rd-tube-sidebar__heading {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #64748b;
|
|
margin: 0 0 0.5rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── Video list ── */
|
|
.rd-tube-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.125rem;
|
|
}
|
|
.rd-tube-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
padding: 0.5rem 0.625rem;
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
font-size: 0.875rem;
|
|
border-left: 2px solid transparent;
|
|
}
|
|
.rd-tube-item:hover { background: rgba(51, 65, 85, 0.5); }
|
|
.rd-tube-item--active {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
border-left-color: #ef4444;
|
|
}
|
|
.rd-tube-item--unplayable { opacity: 0.6; }
|
|
.rd-tube-item__icon { flex-shrink: 0; font-size: 1rem; }
|
|
.rd-tube-item__name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: #e2e8f0;
|
|
}
|
|
.rd-tube-item__size {
|
|
font-size: 0.7rem;
|
|
color: #475569;
|
|
flex-shrink: 0;
|
|
}
|
|
.rd-tube-empty {
|
|
color: #64748b;
|
|
font-size: 0.875rem;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── Player area ── */
|
|
.rd-tube-player-wrap { min-width: 0; }
|
|
.rd-tube-player {
|
|
background: #000;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
aspect-ratio: 16 / 9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.rd-tube-player video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
.rd-tube-placeholder {
|
|
color: #475569;
|
|
font-size: 1.125rem;
|
|
margin: 0;
|
|
}
|
|
.rd-tube-warning {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: #e2e8f0;
|
|
}
|
|
.rd-tube-warning__icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
|
|
.rd-tube-warning__ext { font-weight: 700; }
|
|
.rd-tube-warning__hint {
|
|
font-size: 0.875rem;
|
|
color: #64748b;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
/* ── Info bar ── */
|
|
.rd-tube-info {
|
|
margin-top: 0.75rem;
|
|
background: rgba(30, 41, 59, 0.5);
|
|
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
border-radius: 0.75rem;
|
|
padding: 0.875rem 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
}
|
|
.rd-tube-info__name {
|
|
font-weight: 500;
|
|
color: #f1f5f9;
|
|
margin: 0;
|
|
font-size: 0.9375rem;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.rd-tube-info__actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Button size variant ── */
|
|
.rd-btn--sm {
|
|
font-size: 0.8125rem;
|
|
padding: 0.375rem 0.75rem;
|
|
gap: 0.375rem;
|
|
}
|
|
</style>`;
|
|
}
|