Files
twotalesanimation 7150c3f3d2
Deploy / deploy (push) Successful in 39s
Add /drops/: login-protected, view-only shot previews
- nginx basic auth on /drops/ (page and clips); login file and clips live
  on the server only (docker/auth, docker/drops), never in git or the image
- Fails closed: missing login file returns 500; no-store, noindex
- Page reads manifest.json from the server, shows "taken down" when empty
- Player hides download/cast/PiP, blocks right-click, confidential overlay
- DEPLOY.md step 8: how to share a drop and take it down

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:10:02 +02:00

92 lines
3.1 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
// Served by nginx from the server's docker/drops/ folder, behind the same login as the page.
const base = "/drops/files/";
type Drop = {
title: string;
note?: string;
files: { name: string; src: string }[];
};
export default function DropViewer() {
const [drop, setDrop] = useState<Drop | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
fetch(`${base}manifest.json`, { cache: "no-store" })
.then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
.then(setDrop)
.catch(() => setFailed(true));
}, []);
if (failed) {
return (
<section className="mx-auto max-w-6xl px-4 pt-16 md:px-8 md:pt-24">
<p className="font-mono text-xs tracking-[0.3em] text-accent uppercase">Nothing here</p>
<p className="mt-5 max-w-2xl text-xl leading-relaxed font-light text-muted">
These previews have been taken down. Ask your Two Tales contact if you still need them.
</p>
</section>
);
}
if (!drop) {
return <p className="mx-auto max-w-6xl px-4 pt-16 font-mono text-xs text-muted uppercase md:px-8">Loading…</p>;
}
return (
<>
<section className="mx-auto max-w-6xl px-4 pt-16 md:px-8 md:pt-24">
<p className="font-mono text-xs tracking-[0.3em] text-accent uppercase">Confidential · view only</p>
<h1 className="mt-5 font-display text-[clamp(2rem,7vw,4.5rem)] leading-[0.9] font-thin uppercase">
{drop.title}
</h1>
{drop.note && <p className="mt-6 max-w-3xl text-lg leading-relaxed font-light text-muted">{drop.note}</p>}
</section>
<ol className="mx-auto mt-12 max-w-6xl space-y-12 px-4 md:mt-16 md:px-8">
{drop.files.map((f, i) => (
<li key={f.src}>
<div className="mb-3 flex items-center gap-4 font-mono text-xs tracking-[0.2em] uppercase">
<span className="text-accent">{String(i + 1).padStart(2, "0")}</span>
<span className="h-px w-10 bg-line" />
<span className="break-all text-fg">{f.name}</span>
</div>
<Player src={`${base}${f.src}`} label={f.name} />
</li>
))}
</ol>
</>
);
}
// Plays inline with no download, cast or picture-in-picture options and no right-click menu.
// This only stops casual saving: anyone who can watch can still screen-record.
function Player({ src, label }: { src: string; label: string }) {
return (
<div className="relative overflow-hidden border border-line bg-black" onContextMenu={(e) => e.preventDefault()}>
<video
src={src}
controls
loop
playsInline
preload="metadata"
controlsList="nodownload noremoteplayback noplaybackrate"
disablePictureInPicture
disableRemotePlayback
className="block aspect-video w-full"
aria-label={label}
/>
<span
className="pointer-events-none absolute top-3 right-3 font-mono text-[10px] tracking-[0.25em] text-white/40 uppercase select-none md:text-xs"
aria-hidden="true"
>
Two Tales VFX · Confidential
</span>
</div>
);
}