Compare commits
1
Commits
+13
-11
@@ -9,7 +9,7 @@ import {
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { Suspense, ViewTransition } from "react";
|
||||
import banner from "#/logos/artifact.webp";
|
||||
import { Blocker } from "@/components/blocker";
|
||||
import { ComboBox } from "@/components/combobox";
|
||||
@@ -138,16 +138,18 @@ export default async function ArtifactFormPage({
|
||||
<CardTitle>
|
||||
<div className="w-[276.5px]">
|
||||
<Link href="/">
|
||||
<Image
|
||||
style={{
|
||||
transform: "translateY(-70%)",
|
||||
}}
|
||||
className="absolute z-50"
|
||||
height={137.5}
|
||||
width={276.5}
|
||||
src={banner}
|
||||
alt="เสือกไอดีชาวบ้าน"
|
||||
/>
|
||||
<ViewTransition name="branch-/artifact">
|
||||
<Image
|
||||
style={{
|
||||
transform: "translateY(-70%)",
|
||||
}}
|
||||
className="absolute z-50"
|
||||
height={137.5}
|
||||
width={276.5}
|
||||
src={banner}
|
||||
alt="เสือกไอดีชาวบ้าน"
|
||||
/>
|
||||
</ViewTransition>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
||||
@@ -297,7 +297,7 @@ export function DonateAdminPage({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
emptyDescription="No donation came in yet."
|
||||
data={data}
|
||||
data={data.slice(1)}
|
||||
className="w-full overflow-y-auto bg-black/25 backdrop-blur-sm"
|
||||
style={{
|
||||
maxHeight,
|
||||
@@ -311,30 +311,10 @@ export function DonateAdminPage({
|
||||
// component cuz simplicity sake
|
||||
export function DonateWatcher() {
|
||||
const router = useRouter();
|
||||
const [newDonates, setNewDonates] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (newDonates <= 0) return ((document.title = "โดเนททั้งหมด"), undefined);
|
||||
var blink = true;
|
||||
const interval = setInterval(() => {
|
||||
blink = !blink;
|
||||
document.title = blink ? "โดเนททั้งหมด" : `${newDonates} โดเนทใหม่`;
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [newDonates]);
|
||||
|
||||
useEffect(() => {
|
||||
function listener() {
|
||||
setNewDonates(0);
|
||||
}
|
||||
window.addEventListener("focus", listener);
|
||||
return () => window.removeEventListener("focus", listener);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!router) return;
|
||||
return sse.donate.subMany({
|
||||
ping: () => (router.refresh(), setNewDonates((x) => x + 1)),
|
||||
ping: router.refresh.bind(router),
|
||||
update: router.refresh.bind(router),
|
||||
}).clean;
|
||||
}, [router]);
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "../rubgram/admin/@modal/manual/client";
|
||||
import { DownloadButton } from "../rubgram/client";
|
||||
import Link from "next/link";
|
||||
import { ViewTransition } from "react";
|
||||
|
||||
const { TMN_DEST_PHONE_NUM, SASTIFY_API_PRIVKEY } = process.env as Record<
|
||||
string,
|
||||
@@ -273,11 +274,13 @@ export default async function () {
|
||||
{/* oxlint-disable-next-line tailwindcss/enforce-canonical */}
|
||||
<div className="relative aspect-[304.5/30] w-full">
|
||||
<Link href="/">
|
||||
<Image
|
||||
src={DonateLogo}
|
||||
alt="Donate Logo"
|
||||
className="absolute left-1/2 w-3/4 -translate-x-1/2 -translate-y-2/3"
|
||||
/>
|
||||
<ViewTransition name="branch-/donate">
|
||||
<Image
|
||||
src={DonateLogo}
|
||||
alt="Donate Logo"
|
||||
className="absolute left-1/2 w-3/4 -translate-x-1/2 -translate-y-2/3"
|
||||
/>
|
||||
</ViewTransition>
|
||||
</Link>
|
||||
</div>
|
||||
<FormProvider id="tip" inDialog={false} onSubmit={submit}>
|
||||
|
||||
+10
-8
@@ -1,6 +1,6 @@
|
||||
import type { StaticImport } from "next/dist/shared/lib/get-img-props";
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { ViewTransition, type ReactNode } from "react";
|
||||
import Banner from "#/banner_t.webp";
|
||||
import ArtifactLogo from "#/logos/artifact.webp";
|
||||
import DonateLogo from "#/logos/donate.webp";
|
||||
@@ -159,13 +159,15 @@ function HomeLink({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={logo}
|
||||
alt={name}
|
||||
className="h-24 w-48 object-contain transition-transform duration-100 hover:scale-110 sm:h-40 sm:w-50"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<ViewTransition name={`branch-${href}`}>
|
||||
<Image
|
||||
src={logo}
|
||||
alt={name}
|
||||
className="h-24 w-48 object-contain transition-transform duration-100 hover:scale-110 sm:h-40 sm:w-50"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</ViewTransition>
|
||||
<span className="visible block text-lg md:hidden md:text-xs">
|
||||
{name}
|
||||
</span>
|
||||
|
||||
@@ -369,7 +369,7 @@ export async function getDiscordSession() {
|
||||
|
||||
export async function loginDiscord() {
|
||||
redirect(
|
||||
`https://discord.com/oauth2/authorize?client_id=${encodeURIComponent(DISCORD_CLIENT_ID)}&response_type=code&redirect_uri=${encodeURIComponent(`${BASE_URL}/rubgram/callback`)}&scope=identify+guilds.join+guilds`,
|
||||
`https://discord.com/oauth2/authorize?client_id=${encodeURIComponent(DISCORD_CLIENT_ID)}&response_type=code&redirect_uri=${encodeURIComponent(new URL("/rubgram/callback", BASE_URL).href)}&scope=identify+guilds.join+guilds`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+13
-11
@@ -10,7 +10,7 @@ import {
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { Suspense, ViewTransition } from "react";
|
||||
import PromptpayImage from "#/assets/promptpay.jpg";
|
||||
import banner from "#/logos/rubgram.webp";
|
||||
import { Blocker } from "@/components/blocker";
|
||||
@@ -181,16 +181,18 @@ export default async function EndgamePage({
|
||||
<CardTitle>
|
||||
<div className="w-[276.5px]">
|
||||
<Link href="/">
|
||||
<Image
|
||||
style={{
|
||||
transform: "translateY(-70%)",
|
||||
}}
|
||||
className="absolute z-50"
|
||||
height={137.5}
|
||||
width={276.5}
|
||||
src={banner}
|
||||
alt="รับกรรมแทนทางบ้าน"
|
||||
/>
|
||||
<ViewTransition name="branch-/rubgram">
|
||||
<Image
|
||||
style={{
|
||||
transform: "translateY(-70%)",
|
||||
}}
|
||||
className="absolute z-50"
|
||||
height={137.5}
|
||||
width={276.5}
|
||||
src={banner}
|
||||
alt="รับกรรมแทนทางบ้าน"
|
||||
/>
|
||||
</ViewTransition>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
||||
+11
-8
@@ -9,6 +9,7 @@ import { SimpleTooltip } from "@/components/tooltip";
|
||||
import { ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { db } from "@/lib/db";
|
||||
import { tierlistTypes, tierlistVersions } from "@/lib/db/schema";
|
||||
import { ViewTransition } from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "จัดเทียร์ลิสต์",
|
||||
@@ -47,14 +48,16 @@ export default async function TierlistSelectionPage() {
|
||||
<div className="mx-2 flex min-h-full max-w-full flex-col justify-center gap-2">
|
||||
<center className="mb-2">
|
||||
<Link href="/">
|
||||
<Image
|
||||
src={TierlistLogo}
|
||||
alt="Tierlist"
|
||||
className="w-1/2 sm:w-96"
|
||||
width={200}
|
||||
height={100}
|
||||
fetchPriority="high"
|
||||
/>
|
||||
<ViewTransition name="branch-/tl">
|
||||
<Image
|
||||
src={TierlistLogo}
|
||||
alt="Tierlist"
|
||||
className="w-1/2 sm:w-96"
|
||||
width={200}
|
||||
height={100}
|
||||
fetchPriority="high"
|
||||
/>
|
||||
</ViewTransition>
|
||||
</Link>
|
||||
</center>
|
||||
{vers.map((t) => (
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { redis } from "@/lib/db/redis";
|
||||
import { env } from "bun";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
async function probe(name: string, fn: () => Promise<unknown>) {
|
||||
const started = performance.now();
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
|
||||
return {
|
||||
name,
|
||||
ok: true,
|
||||
latencyMs: performance.now() - started,
|
||||
result,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name,
|
||||
ok: false,
|
||||
latencyMs: performance.now() - started,
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
cause: error.cause,
|
||||
}
|
||||
: error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (!await adminCheck()) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const probes = await Promise.all([
|
||||
probe("redis", async () => {
|
||||
const started = performance.now();
|
||||
const result = await redis?.ping();
|
||||
|
||||
return {
|
||||
result,
|
||||
latencyMs: performance.now() - started,
|
||||
};
|
||||
}),
|
||||
|
||||
probe("postgres", async () => {
|
||||
const started = performance.now();
|
||||
const result = await db.execute(sql`SELECT 1`);
|
||||
|
||||
return {
|
||||
result,
|
||||
latencyMs: performance.now() - started,
|
||||
};
|
||||
}),
|
||||
]);
|
||||
|
||||
return Response.json({
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
runtime: {
|
||||
bun: Bun.version,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
pid: process.pid,
|
||||
uptime: process.uptime(),
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
|
||||
memory: process.memoryUsage(),
|
||||
|
||||
eventLoop: {
|
||||
now: performance.now(),
|
||||
},
|
||||
|
||||
request: {
|
||||
url: request.url,
|
||||
headers: Object.fromEntries(request.headers),
|
||||
},
|
||||
|
||||
env,
|
||||
|
||||
probes,
|
||||
});
|
||||
}
|
||||
+13
-55
@@ -1,9 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { redis as redisShared } from "@/lib/db/redis";
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { env } from "bun";
|
||||
const { DISCORD_BOT_TOKEN, GEMINI_TTS_API_KEY } = env as Record<string, string>;
|
||||
|
||||
const redis = redisShared!;
|
||||
|
||||
@@ -23,22 +20,16 @@ export async function GET() {
|
||||
red
|
||||
? (await redis.exists(`health:${k}`))
|
||||
? (await redis.get(`health:${k}`)) === "ok"
|
||||
: await v().then(async (r: any) => {
|
||||
const v = r instanceof Response ? r.ok : !!r;
|
||||
: await v().then(async (v) => {
|
||||
if (v) await redis.setex(`health:${k}`, 900, "ok");
|
||||
return v;
|
||||
})
|
||||
: await v().then((r: any) =>
|
||||
r instanceof Response ? r.ok : !!r,
|
||||
),
|
||||
: await v(),
|
||||
] as const,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Response.json(
|
||||
{ red, ...res },
|
||||
{ status: res.database && red ? 200 : 201 },
|
||||
);
|
||||
return Response.json({ red, ...res });
|
||||
}
|
||||
|
||||
const checks = {
|
||||
@@ -49,52 +40,19 @@ const checks = {
|
||||
.catch(() => false);
|
||||
},
|
||||
async enka() {
|
||||
return fetch("https://enka.network/api/uid/888888888/?info", {
|
||||
return await fetch("https://enka.network/api/uid/888888888/?info", {
|
||||
headers: { "User-Agent": "Buzz, https://buzz.sudloh.com/api/health" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
},
|
||||
async amber() {
|
||||
return fetch("https://gi.yatta.moe/api/v2/en/avatar", {
|
||||
headers: { "User-Agent": "Buzz, https://buzz.sudloh.com/api/health" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
},
|
||||
async yt() {
|
||||
return fetch("https://buzz.sudloh.com/api/live", {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
},
|
||||
async card() {
|
||||
return fetch("https://api.astrxl.dev/v1/card/genshin/stats", {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
},
|
||||
async slip() {
|
||||
return fetch("https://api.slipok.com/", {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
},
|
||||
async tts() {
|
||||
const client = new GoogleGenAI({
|
||||
apiKey: GEMINI_TTS_API_KEY.split(",").pop(),
|
||||
});
|
||||
return client.models
|
||||
.list()
|
||||
.then(() => true)
|
||||
})
|
||||
.then((r) => r.ok)
|
||||
.catch(() => false);
|
||||
},
|
||||
async tmn() {
|
||||
return fetch("https://api.sastify.xyz/health", {
|
||||
async amber() {
|
||||
return await fetch("https://gi.yatta.moe/api/v2/en/avatar", {
|
||||
headers: { "User-Agent": "Buzz, https://buzz.sudloh.com/api/health" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
})
|
||||
.then((r) => r.ok)
|
||||
.catch(() => false);
|
||||
},
|
||||
async discord() {
|
||||
return fetch("https://discord.com/api/v10/users/@me", {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
headers: {
|
||||
Authorization: `Bot ${DISCORD_BOT_TOKEN}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
} satisfies Record<string, () => Promise<Response | boolean>>;
|
||||
} as const;
|
||||
|
||||
@@ -66,10 +66,8 @@ export async function GET() {
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`YouTube API error: ${response.statusText}`);
|
||||
return NextResponse.json("none");
|
||||
}
|
||||
if (!response.ok)
|
||||
throw new Error(`YouTube API error: ${response.statusText}`);
|
||||
|
||||
const data: APISearchResponse = await response.json();
|
||||
|
||||
@@ -89,7 +87,3 @@ export async function GET() {
|
||||
|
||||
return NextResponse.json<YoutubeLiveInfo>(res);
|
||||
}
|
||||
|
||||
export const POST = GET;
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"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": "^1.27.0",
|
||||
"lucide-react": "^0.562.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@1.27.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw=="],
|
||||
"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=="],
|
||||
|
||||
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
|
||||
@@ -72,18 +72,3 @@ 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": "^1.27.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.40.0",
|
||||
"next": "16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
|
||||
Reference in New Issue
Block a user