9 changed files with 71 additions and 213 deletions
+13 -11
View File
@@ -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>
+2 -22
View File
@@ -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]);
+8 -5
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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) => (
-92
View File
@@ -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
View File
@@ -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;