feat: pre-warn user of any enka error
This commit is contained in:
+212
-116
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user