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
11 changes: 11 additions & 0 deletions src/app/(core)/v/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Static placeholder in the same layout as the player page (no animation)
export default function Loading() {
return (
<div className="flex min-h-0 flex-1 flex-col gap-8 px-4 pb-4 sm:flex-row sm:px-8 sm:pb-8">
<div className="aspect-video w-full bg-zinc-950 sm:w-auto sm:flex-1" />
<div className="flex flex-col gap-2 text-sm text-zinc-500 sm:w-[26rem]">
Loading markers
</div>
</div>
);
}
84 changes: 66 additions & 18 deletions src/app/(core)/v/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,82 @@
import { auth } from "@clerk/nextjs/server";
import {
getTwitchTokenFromClerk,
getVodWithMarkers,
} from "~/utils/twitch-server";
import type { Metadata } from "next";
import { ButtonLink } from "~/app/_components/common/button";
import { SignInButton } from "~/app/_components/signin";
import { EXPORT_BUFFER_SECONDS, MAX_BUFFER_SECONDS } from "~/utils/markers";
import { getVodWithMarkers } from "~/utils/twitch-server";
import { VodPlayer } from "./player";
import Script from "next/script";

export const dynamic = "force-dynamic";
export const metadata: Metadata = { title: "VOD markers | MarkerThing" };

// I do the revalidate 0 here because "force-dynamic" doesn't actually work
// See: https://github.com/vercel/next.js/issues/47273
export const revalidate = 60;
const Notice = (props: { title: string; children: React.ReactNode }) => (
<div className="m-auto flex max-w-md flex-col items-center gap-4 p-8 text-center">
<h1 className="text-xl font-semibold">{props.title}</h1>
{props.children}
</div>
);

export default async function VodPage({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const { slug } = await params;
const self = await auth();
if (!self || !self.userId) return <div>You have to be signed in</div>;
const [{ slug }, query, self] = await Promise.all([
params,
searchParams,
auth(),
]);

const token = await getTwitchTokenFromClerk(self.userId);
if (!self.userId) {
return (
<Notice title="Sign in to see markers">
<SignInButton />
</Notice>
);
}

const vodDetails = await getVodWithMarkers(slug, token);
const result = await getVodWithMarkers(slug, self.userId);

if (result.status === "not-found") {
return (
<Notice title="VOD not found">
<p>This VOD does not exist, or Twitch deleted it.</p>
<ButtonLink href="/">Go home</ButtonLink>
</Notice>
);
}

if (result.status === "creator-not-connected") {
return (
<Notice title="Markers not available">
<p>
{result.rejected
? `${result.creator} needs to sign in to MarkerThing again.`
: `${result.creator} has not signed in to MarkerThing yet.`}
</p>
<ButtonLink href="/">Go home</ButtonLink>
</Notice>
);
}

// A repeated param (?offset=1&offset=2) arrives as an array. Use the first.
const param = (key: string) => [query[key]].flat()[0];
const buffer = Number(param("buffer"));

return (
<>
<Script src="https://player.twitch.tv/js/embed/v1.js" async />
<VodPlayer id={slug} vod={vodDetails} />
</>
<VodPlayer
vod={result.vod}
initial={{
offset: param("offset") ?? "",
buffer:
Number.isInteger(buffer) &&
buffer >= 0 &&
buffer <= MAX_BUFFER_SECONDS
? buffer
: EXPORT_BUFFER_SECONDS,
numbered: param("numbered") === "1",
}}
/>
);
}
Loading
Loading