feat: chat widget
This commit is contained in:
@@ -66,8 +66,10 @@ export async function GET() {
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(`YouTube API error: ${response.statusText}`);
|
||||
if (!response.ok) {
|
||||
console.error(`YouTube API error: ${response.statusText}`);
|
||||
return NextResponse.json("none");
|
||||
}
|
||||
|
||||
const data: APISearchResponse = await response.json();
|
||||
|
||||
@@ -88,4 +90,6 @@ export async function GET() {
|
||||
return NextResponse.json<YoutubeLiveInfo>(res);
|
||||
}
|
||||
|
||||
export const revalidate = 60;
|
||||
export const POST = GET;
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { Countdown } from "@/app/(ui)/rubgram/client";
|
||||
import type { YoutubeLiveInfo } from "@/app/api/live/route";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { throwNotOk } from "@/lib/utils";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { MessageCircleOff } from "lucide-react";
|
||||
|
||||
export default function ChatWidget() {
|
||||
const [cooldown, setCooldown] = useState<Date>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// post prevents caching
|
||||
const data: YoutubeLiveInfo = await fetch("/api/live", { method: "POST" })
|
||||
.then(throwNotOk)
|
||||
.then((r) => r.json());
|
||||
if (data === "none") return setNotFound(true);
|
||||
const vid = new URL(data.url).searchParams.get("v");
|
||||
router.push(`https://studio.youtube.com/live_chat?is_popout=1&v=${vid}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setNotFound(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!notFound) load();
|
||||
}, [load, notFound]);
|
||||
|
||||
return notFound ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-5 rounded-xl border bg-zinc-800/50 text-4xl">
|
||||
<MessageCircleOff size={64} className="opacity-50" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
load().finally(() => setCooldown(new Date(Date.now() + 60_000)));
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!!cooldown || loading}
|
||||
>
|
||||
{loading ? <Spinner /> : <RefreshCw />}
|
||||
{cooldown ? <Countdown time={cooldown} /> : "รีโหลดใหม่"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "^1.27.0",
|
||||
"motion": "^12.40.0",
|
||||
"next": "16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -1053,7 +1053,7 @@
|
||||
|
||||
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
||||
"lucide-react": ["lucide-react@1.27.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw=="],
|
||||
|
||||
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
|
||||
@@ -72,3 +72,18 @@ export function parseSearchString(
|
||||
export function pausePass<R>(time: number): (res: R) => Promise<R> {
|
||||
return (res) => new Promise((r) => setTimeout(() => r(res), time));
|
||||
}
|
||||
|
||||
// taken from astral/api
|
||||
export class ResponseNotOkError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly response: Response,
|
||||
) {
|
||||
super(`RESPONSE_NOT_OK: Got status ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function throwNotOk(res: Response) {
|
||||
if (!res.ok) throw new ResponseNotOkError(res.status, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "^1.27.0",
|
||||
"motion": "^12.40.0",
|
||||
"next": "16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
|
||||
Reference in New Issue
Block a user