feat: pre-warn user of any enka error

This commit is contained in:
2026-04-10 12:37:08 +07:00 Unverified
parent 0e9da93667
commit a9410ba2e0
5 changed files with 291 additions and 153 deletions
+212 -116
View File
@@ -12,7 +12,7 @@ import {
Wrench,
} from "lucide-react";
import Image from "next/image";
import { useEffect, useState } from "react";
import { use, useEffect, useState } from "react";
import { toast } from "sonner";
import GuideImage1 from "#/guide/1.png";
import GuideImage2 from "#/guide/2.png";
@@ -26,7 +26,6 @@ import {
AlertDialogContent,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -34,7 +33,7 @@ import { Label } from "@/components/ui/label";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Spinner } from "@/components/ui/spinner";
import { getCharacters } from "@/lib/api";
import { shared } from "@/lib/comms";
import { IccContext, shared } from "@/lib/comms";
import { uidRegex } from "@/lib/const";
import type { characters } from "@/lib/db/schema";
import { cn } from "@/lib/utils";
@@ -60,9 +59,9 @@ export function CharacterChooser({
const [isError, setIsError] = useState<boolean | string>(false);
const [isLoading, setIsLoading] = useState(false);
const [manual, setManual] = useState(false);
const [isRefreshing, setRefreshing] = useState(false);
const [guideDialogOpen, setGuideDialogOpen] = useState(false);
const [cacheTtl, setCacheTtl] = useState(new Date());
const [, setWarningUid] = shared.state("warning.uid");
const [, setWarningSrc] = shared.state("warning.src");
const [, setWarning] = shared.state("warning");
useEffect(() => {
if (defaultUid) setUid(defaultUid);
@@ -84,7 +83,7 @@ export function CharacterChooser({
return setIsError("ไม่พบผู้เล่นที่มี UID นี้");
if (!res.ok) return setIsError(true);
if (!data.playerInfo) return setIsError(true);
setCacheTtl(new Date(Date.now() + (data.ttl + 5) * 1000));
if (!data.playerInfo.showAvatarInfoList)
return setIsError("UID นี้ไม่มีตัวละครที่จัดแสดงเป็นสาธารณะ");
const charIds = data.playerInfo.showAvatarInfoList.map((c) =>
@@ -160,111 +159,19 @@ export function CharacterChooser({
? isError
: "เกิดข้อผิดพลาดในการดึงข้อมูล"}
</div>
{typeof isError === "string" && isError.startsWith("UID") && (
<AlertDialog
onOpenChange={setGuideDialogOpen}
open={guideDialogOpen}
>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm">
<Wrench />
วิธีแก้ไข
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>วิธีแก้ไขตัวละครไม่ขึ้น</AlertDialogTitle>
<div className="gap-1 flex flex-col">
<center>
ในเกม เปิด<b>เมนูเกม</b>แล้วไปที่ <b>แก้ไขข้อมูลส่วนตัว</b>
</center>
<Image
src={GuideImage1}
width={500}
height={200}
alt="Guide Image 1"
className="border-2 border-foreground rounded-md"
/>
</div>
<div className="gap-1 flex flex-col">
<center>
ใส่ตัวละครที่ต้องการ แล้วเปิด "<b>แสดงรายละเอียดตัวละคร</b>"
</center>
<Image
src={GuideImage2}
width={500}
height={200}
alt="Guide Image 1"
className="border-2 border-foreground rounded-md"
/>
<span className="ml-[60%] md:ml-[65%]">
อย่าลืมเปิด <ArrowBigUp className="inline" />
</span>
</div>
<center>ออกจากเกมเพื่อให้ข้อมูลอัพเดท แล้วกดรีโหลด</center>
<AlertDialogFooter suppressHydrationWarning>
<AlertDialogCancel>ปิดหน้าต่าง</AlertDialogCancel>
<Countdown
time={cacheTtl}
render={(s) => (
<Button
disabled={!!s || isRefreshing}
onClick={() => {
setRefreshing(true);
async function refreshChars() {
const res = await fetch(`/api/enka/${uid}`);
const data:
| (EnkaNetworkUser & { message: undefined })
| { message: string; playerInfo: undefined } =
await res
.json()
.catch(() => ({ message: "not json" }));
if (
data.message === "This player does not exist."
)
return toast.error("ไม่พบผู้เล่นที่มี UID นี้");
if (!res.ok)
return toast.error("เกิดข้อผิดพลาดในการดึงข้อมูล");
if (!data.playerInfo)
return toast.error("เกิดข้อผิดพลาดในการดึงข้อมูล");
setCacheTtl(
new Date(Date.now() + data.ttl * 1000),
);
if (!data.playerInfo.showAvatarInfoList)
return toast.error(
"ยังไม่พบตัวละคร ลองใหม่อีกครั้ง",
);
const charIds =
data.playerInfo.showAvatarInfoList.map((c) =>
c.avatarId.toString(),
);
const chars = await getCharacters(charIds);
setChars(
charIds
.map(
(e) => chars.find((c) => c.amber === e)!,
)
.filter(Boolean),
);
setSelected(undefined);
setGuideDialogOpen(false);
}
refreshChars()
.catch((e) => {
console.error(e);
toast.error("ข้อผิดพลาดภายในระบบ");
})
.finally(() => setRefreshing(false));
}}
>
{isRefreshing ? <Spinner /> : <UserSearch />}
{s ? `รีโหลดใหม่ (${s})` : `รีโหลดใหม่`}
</Button>
)}
/>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setWarningUid(uid);
setWarningSrc("input");
setWarning("guide");
}}
>
<Wrench />
วิธีแก้ไข
</Button>
</Placeholder>
) : isLoading ? (
<Placeholder>
@@ -353,9 +260,198 @@ export function ClearCookie() {
}
export function WarningDialog() {
const [_manual, _setManual] = shared.state("manual");
const [warning, setWarning] = shared.state("warning");
const [uid] = shared.state("warning.uid");
shared.signal("beforeSubmit", () => {
_manual;
});
const [open, setOpen] = useState(false);
const [isRefreshing, setRefreshing] = useState(false);
const [cacheTtl, setCacheTtl] = useState(new Date());
const icc = use(IccContext);
// 👀 open when warning === "guide"
useEffect(() => {
setOpen(warning === "guide");
}, [warning]);
async function refreshChars() {
if (!uid) return;
setRefreshing(true);
try {
const res = await fetch(`/api/enka/${uid}`);
const data:
| (EnkaNetworkUser & { message: undefined })
| { message: string; playerInfo: undefined } = await res
.json()
.catch(() => ({ message: "not json" }));
if (data.message === "This player does not exist.")
return toast.error("ไม่พบผู้เล่นที่มี UID นี้");
if (!res.ok || !data.playerInfo)
return toast.error("เกิดข้อผิดพลาดในการดึงข้อมูล");
setCacheTtl(new Date(Date.now() + data.ttl * 1000));
if (!data.playerInfo.showAvatarInfoList)
return toast.error("ยังไม่พบตัวละคร ลองใหม่อีกครั้ง");
const charIds = data.playerInfo.showAvatarInfoList.map((c) =>
c.avatarId.toString(),
);
await getCharacters(charIds); // warm cache / ensure available
// ✅ success → close dialog + notify
setWarning(undefined);
icc.emit("warningSolved");
setOpen(false);
} catch (e) {
console.error(e);
toast.error("ข้อผิดพลาดภายในระบบ");
} finally {
setRefreshing(false);
}
}
if (warning === "nf")
return (
<AlertDialog onOpenChange={() => setWarning(undefined)} open>
<AlertDialogContent>
<AlertDialogTitle>เดี๋ยวนะ</AlertDialogTitle>
UID นี้ไม่มีอยู่จริง แน่เหรอว่าจะส่ง?
<AlertDialogFooter>
<AlertDialogCancel>ยกเลิก</AlertDialogCancel>
<Button
variant="destructive"
onClick={() => {
setWarning(undefined);
icc.emit("warningSolved");
}}
>
ยังไงก็จะส่ง
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
if (warning === "showcase")
return (
<AlertDialog onOpenChange={() => setWarning(undefined)} open>
<AlertDialogContent>
<AlertDialogTitle>เดี๋ยวนะ</AlertDialogTitle>
ตัวละครที่เลือกไม่ได้อยู่ในตั้งโชว์
<br />
ถ้าส่งก่อนใส่มันจะไม่ขึ้นนะ ไปแก้ก่อนมั้ย
<AlertDialogFooter>
<AlertDialogCancel>ยกเลิก</AlertDialogCancel>
<Button
variant="destructive"
onClick={() => {
setWarning(undefined);
icc.emit("warningSolved");
}}
>
ยังไงก็จะส่ง
</Button>
<Button onClick={() => setWarning("guide")}>
<Wrench />
วิธีแก้
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
if (warning === "private")
return (
<AlertDialog onOpenChange={() => setWarning(undefined)} open>
<AlertDialogContent>
<AlertDialogTitle>เดี๋ยวนะ</AlertDialogTitle>
ใน UID นี้ไม่มีตัวละครอะไรตั้งโชว์อยู่เลย
<br />
ถ้าส่งก่อนใส่มันจะไม่ขึ้นนะ ไปแก้ก่อนมั้ย
<AlertDialogFooter>
<AlertDialogCancel>ยกเลิก</AlertDialogCancel>
<Button
variant="destructive"
onClick={() => {
setWarning(undefined);
icc.emit("warningSolved");
}}
>
ยังไงก็จะส่ง
</Button>
<Button onClick={() => setWarning("guide")}>
<Wrench />
วิธีแก้
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
if (!warning) return null;
return (
<AlertDialog
open={open}
onOpenChange={(o) => {
setOpen(o);
if (!o) setWarning(undefined);
}}
>
<AlertDialogContent>
<AlertDialogTitle>วิธีแก้ไขตัวละครไม่ขึ้น</AlertDialogTitle>
<div className="flex flex-col gap-2">
<center>
ในเกม เปิด<b>เมนูเกม</b>แล้วไปที่ <b>แก้ไขข้อมูลส่วนตัว</b>
</center>
<Image
src={GuideImage1}
width={500}
height={200}
alt="Guide Image 1"
className="border-2 border-foreground rounded-md"
/>
<center>
ใส่ตัวละครที่ต้องการ แล้วเปิด "<b>แสดงรายละเอียดตัวละคร</b>"
</center>
<Image
src={GuideImage2}
width={500}
height={200}
alt="Guide Image 2"
className="border-2 border-foreground rounded-md"
/>
<span className="ml-[60%] md:ml-[65%]">
อย่าลืมเปิด <ArrowBigUp className="inline" />
</span>
<center>ออกจากเกมเพื่อให้ข้อมูลอัพเดท แล้วกดรีโหลด</center>
</div>
<AlertDialogFooter>
<AlertDialogCancel>ปิดหน้าต่าง</AlertDialogCancel>
<Countdown
time={cacheTtl}
render={(s) => (
<Button disabled={!!s || isRefreshing} onClick={refreshChars}>
{isRefreshing ? <Spinner /> : <UserSearch />}
{s ? `รีโหลดใหม่ (${s})` : `รีโหลดใหม่`}
</Button>
)}
/>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
+31 -3
View File
@@ -1,15 +1,43 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { submitArtifact } from "@/lib/api";
import { checkEnkaStatus, submitArtifact } from "@/lib/api";
import { shared } from "@/lib/comms";
export function ArtifactFormWrapper({
edit,
enka,
...props
}: React.ComponentProps<"form"> & { edit?: { sub: string; token: string } }) {
}: React.ComponentProps<"form"> & {
edit?: { sub: string; token: string };
enka: boolean;
}) {
const router = useRouter();
async function submit(data: FormData) {
const [, setDialog] = shared.state("warning");
const [, setWarningUid] = shared.state("warning.uid");
const [warningSrc, setWarningSrc] = shared.state("warning.src");
const [dataHold, setDataHold] = useState<FormData>();
// Disable warning for 60 seconds
shared.signal(
"warningSolved",
() => dataHold && warningSrc === "submit" && submit(dataHold, false),
);
async function submit(data: FormData, doCheck = true) {
if (enka && doCheck) {
const check = await checkEnkaStatus(
(data.get("uid") ?? "").toString(),
(data.get("character") ?? "").toString(),
);
console.log(data.get("uid"), data.get("character"), check);
setWarningUid(data.get("character")?.toString());
setWarningSrc("submit");
setDataHold(data);
if (check) return setDialog(check);
}
submitArtifact(data, edit)
.then((q) => {
if (typeof q === "string") return toast.error(q);
+8 -34
View File
@@ -31,7 +31,12 @@ import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
import { getArtifactConfig } from "@/lib/api";
import { db } from "@/lib/db";
import { characters, submissions } from "@/lib/db/schema";
import { CharacterChooser, ClearCookie, Disclaimer } from "./client";
import {
CharacterChooser,
ClearCookie,
Disclaimer,
WarningDialog,
} from "./client";
import { ArtifactFormWrapper } from "./form";
import { LiveButton } from "./live";
import { RulesDialog } from "./rules";
@@ -151,6 +156,7 @@ export default async function ArtifactFormPage({
<ArtifactFormWrapper
id="mainform"
edit={q ? { token: q.editToken, sub: q.id } : undefined}
enka={config.enka}
>
<div className="flex flex-col gap-3">
<div className="grid gap-2">
@@ -267,40 +273,8 @@ export default async function ArtifactFormPage({
</div>
</CardFooter>
</Card>
{/*
<div className="hidden md:block bg-card p-5 rounded border border-border relative">
<span className="absolute font-semibold text-7xl -translate-y-3/4 w-110 text-center text-shadow-lg/80">
Guide
</span>
<video
src="https://cdn.gunshiz.top/buzz/artifact/guide.mp4"
controls
className="aspect-video w-110"
>
<track kind="captions" />
</video>
</div>
<Dialog>
<DialogTrigger asChild>
<div className="md:hidden absolute w-full bottom-0 p-1 font-semibold div flex justify-center items-center gap-2 text-xl bg-input/30 border">
<SquarePlay />
วิธีลงทะเบียน
</div>
</DialogTrigger>
<DialogContent className="p-1">
<DialogTitle className="hidden">Guide</DialogTitle>
<video
src="https://cdn.gunshiz.top/buzz/artifact/guide.mp4"
controls
autoPlay
className="aspect-video w-110"
>
<track kind="captions" />
</video>
</DialogContent>
</Dialog>
*/}
{sid && !q && <ClearCookie />}
<WarningDialog />
</div>
);
}
+18
View File
@@ -147,6 +147,24 @@ export async function getCardStatus(submissionId: string) {
return res;
}
export async function checkEnkaStatus(uid: string, char: string) {
const [ch] = await db
.select({
amber: characters.amber,
})
.from(characters)
.where(eq(characters.name, char));
const res = await fetch(
`http://mts.dgnr.us:8809/v1/card/genshin/${uid}/${ch ? ch.amber.split("-")[0] : "10000005"}?debug=dump`,
);
const text = await res.text(),
{ status } = res;
if (text === "Character not found in showcase") return "showcase";
if (text === "The showcase for this UID is private") return "private";
if (status === 404) return "nf";
return false;
}
export async function toggleCheck(submissionId: string) {
if (!(await adminCheck())) throw "Unauthorized";
await db
+22
View File
@@ -1,5 +1,11 @@
"use client";
declare global {
interface Window {
icc: IccMutator;
}
}
import {
createContext,
use,
@@ -17,12 +23,17 @@ type SharedStates = Partial<{
"rubgram.services": string[];
"tl.deleteMode": boolean;
inspect: Record<string, string>;
// nf(Not Found) and showcase(Not In Showcase) is error dialog that opens guide
warning: "showcase" | "nf" | "guide" | "private";
"warning.uid": string;
"warning.src": "submit" | "input";
_raw: Record<string, unknown>;
}>;
type SharedSignals = {
beforeSubmit?: undefined;
sync?: undefined;
warningSolved?: undefined;
} & SharedStates;
type IccMutator = {
@@ -105,6 +116,9 @@ export default function IccProvider({
active: true,
});
if (typeof window !== "undefined" && location.hostname === "localhost")
window.icc = contextValue.current;
return <IccContext value={contextValue.current}>{children}</IccContext>;
}
@@ -185,4 +199,12 @@ export const shared = {
});
}, [v]);
},
emit<K extends keyof SharedSignals>(
key: K,
...data: SharedSignals[K] extends undefined ? [] : [SharedSignals[K]]
) {
const comms = use(IccContext);
comms.emit(key, ...data);
},
};