feat(don): add method toggle
Build and Push Image / build-backend (push) Successful in 1m1s
Build and Push Image / push (push) Has been cancelled
Build and Push Image / build-frontend (push) Has been cancelled

This commit is contained in:
2026-07-19 02:58:44 +07:00 Unverified
parent 9b338dc850
commit 6b64059501
11 changed files with 149 additions and 49 deletions
@@ -18,6 +18,7 @@ import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import { guides } from "@/lib/db/schema";
import { forceRefresh } from "../../../settings/api";
export default async function GuideEditPage({
params,
@@ -66,6 +67,7 @@ export default async function GuideEditPage({
await actionLog(`Updated guide ${data.name}`, data);
revalidatePath("/admin/guide");
forceRefresh("/guide");
return { toast: "Guide updated successfully.", close: true };
}
@@ -18,6 +18,7 @@ import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import { guides } from "@/lib/db/schema";
import { forceRefresh } from "../../../settings/api";
export default async function GuideCreatePage() {
if (!(await adminCheck())) redirect("/login");
@@ -64,6 +65,7 @@ export default async function GuideCreatePage() {
await actionLog(`Added guide ${data.name}`, data);
revalidatePath("/admin/guide");
forceRefresh("/guide");
return { toast: "Guide added successfully.", close: true };
}
+3
View File
@@ -6,6 +6,7 @@ import { redirect } from "next/navigation";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import { guides } from "@/lib/db/schema";
import { forceRefresh } from "../settings/api";
export async function hideGuide(id: string) {
if (!(await adminCheck())) redirect("/login");
@@ -15,5 +16,7 @@ export async function hideGuide(id: string) {
hidden: not(guides.hidden),
})
.where(eq(guides.id, id));
forceRefresh("/guide");
revalidatePath("/admin/guide");
}
+22 -7
View File
@@ -10,16 +10,16 @@ import { settings } from "@/lib/db/schema";
import { adminCheck } from "@/lib/auth";
import { sse } from "@/lib/db/sse-endpoints";
export async function getEnka() {
export async function getSettongs() {
if (!(await adminCheck())) throw "Unauthorized";
const [{ enka } = { enka: true }] = await db.select().from(settings).limit(1);
return enka;
const [
settangs = { enka: true, donatePromptpay: true, donateTruemoney: true },
] = await db.select().from(settings).limit(1);
return settangs;
}
export async function toggleEnka(state: boolean) {
if (!(await adminCheck())) throw "Unauthorized";
const last = await getEnka();
if (last === state) return;
await db
.insert(settings)
.values({ enka: state })
@@ -39,7 +39,22 @@ export async function syncAmber() {
return res.stdout;
}
export async function forceRefresh() {
export async function forceRefresh(prefix: string | null = null) {
if (!(await adminCheck())) throw "Unauthorized";
sse.active.pub("refresh", null);
sse.active.pub("refresh", prefix);
await actionLog("Pulled a force refresh");
}
export async function toggleDonatePaymentMethod(
method: "donatePromptpay" | "donateTruemoney",
state: boolean,
) {
if (!(await adminCheck())) throw "Unauthorized";
await db
.insert(settings)
.values({ [method]: state })
.onConflictDoUpdate({ target: settings.id, set: { [method]: state } });
await actionLog("Changed a settings", { [method]: state });
}
+9 -3
View File
@@ -8,11 +8,11 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { SettingsAccountSection } from "./account";
import { getEnka } from "./api";
import { getSettongs } from "./api";
import { SettingsServicesSection } from "./services";
export default async function () {
const enka = await getEnka();
const { enka, donatePromptpay, donateTruemoney } = await getSettongs();
return (
<div className="grid h-full w-full gap-8 p-2 pl-0">
@@ -22,7 +22,11 @@ export default async function () {
<TabsTrigger value="account">บัญชี</TabsTrigger>
</TabsList>
<TabsContent value="external">
<SettingsServicesSection enka={enka} />
<SettingsServicesSection
enka={enka}
donPp={donatePromptpay}
donTmn={donateTruemoney}
/>
</TabsContent>
<TabsContent value="account">
<SettingsAccountSection />
@@ -49,3 +53,5 @@ export function Section({
</Card>
);
}
export const dynamic = "force-dynamic";
+62 -9
View File
@@ -1,28 +1,36 @@
"use client";
import { Bomb, FolderSync } from "lucide-react";
import { useEffect, useState } from "react";
import { useState } from "react";
import { CdnChooser } from "@/components/chooser";
import { Switch } from "@/components/ui/switch";
import { shared } from "@/lib/comms";
import { forceRefresh, syncAmber, toggleEnka } from "./api";
import {
forceRefresh,
syncAmber,
toggleDonatePaymentMethod,
toggleEnka,
} from "./api";
import { Section } from "./page";
import { ActionButton } from "@/components/action-button";
import { Input } from "@/components/ui/input";
export function SettingsServicesSection({
enka: enkaInitial,
donPp: initialDonPp,
donTmn: initialDonTmn,
}: {
enka: boolean;
donPp: boolean;
donTmn: boolean;
}) {
const [syncResult, setSyncResult] = useState("");
const [enka, setEnka] = useState<boolean>();
const [enka, setEnka] = useState<boolean>(enkaInitial);
const [donPp, setDonPp] = useState<boolean>(initialDonPp);
const [donTmn, setDonTmn] = useState<boolean>(initialDonTmn);
const [pathToReload, setPathToReload] = useState("");
const [debug] = shared.state("debug");
useEffect(() => {
if (typeof enka !== "boolean") return setEnka(enkaInitial);
toggleEnka(enka);
}, [enka, enkaInitial]);
async function sync() {
return syncAmber().then(setSyncResult);
}
@@ -31,7 +39,13 @@ export function SettingsServicesSection({
<Section title="บริการนอก">
<div className="flex h-full flex-col gap-2">
<div className="flex items-center gap-2">
<Switch checked={enka} onCheckedChange={setEnka} />
<Switch
checked={enka}
onCheckedChange={(v) => (
setEnka(v),
toggleEnka(v).then(forceRefresh.bind(null, "/artifact"))
)}
/>
เปิดใช้การดึงตัวละครใน Artifact
</div>
<div className="flex items-center gap-2">
@@ -44,6 +58,45 @@ export function SettingsServicesSection({
FORCE RELOAD
</ActionButton>
</div>
<div className="flex items-center gap-2">
<Input
placeholder="/path/prefix/to/reload"
className="w-60"
onChange={(v) => setPathToReload(v.target.value)}
/>
<ActionButton
action={forceRefresh.bind(null, pathToReload)}
variant="destructive"
>
<Bomb />
Reload
</ActionButton>
</div>
<span>Donate Payment Method</span>
<div className="flex items-center gap-2">
<Switch
checked={donPp}
onCheckedChange={(v) => (
setDonPp(v),
toggleDonatePaymentMethod("donatePromptpay", v).then(
forceRefresh.bind(null, "/donate"),
)
)}
/>
Promptpay
</div>
<div className="flex items-center gap-2">
<Switch
checked={donTmn}
onCheckedChange={(v) => (
setDonTmn(v),
toggleDonatePaymentMethod("donateTruemoney", v).then(
forceRefresh.bind(null, "/donate"),
)
)}
/>
Truemoney
</div>
{debug && (
<div className="grid w-fit gap-1 rounded-sm border border-dashed p-1">
<span className="text-sm text-muted-foreground">
+42 -26
View File
@@ -1,5 +1,5 @@
import { sql } from "drizzle-orm";
import { QrCodeIcon, SendIcon } from "lucide-react";
import { CircleX, QrCodeIcon, SendIcon } from "lucide-react";
import type { Metadata } from "next";
import z from "zod";
import { th } from "zod/v4/locales";
@@ -345,34 +345,50 @@ export default async function () {
]}
>
<FormChoice value="tmn">
<FormInput name="link" label="ลิ้งค์อั่งเปา">
<Input placeholder="https://gift.truemoney.com/campaign/?v=..." />
</FormInput>
{artifactConfig.donateTruemoney ? (
<FormInput name="link" label="ลิ้งค์อั่งเปา">
<Input placeholder="https://gift.truemoney.com/campaign/?v=..." />
</FormInput>
) : (
<div className="flex gap-2">
<CircleX className="text-red-500" />{" "}
ขณะนี้การโอนเงินด้วยทรูมันนี่ใช้ไม่ได้ชั่วคราว
</div>
)}
</FormChoice>
<FormChoice value="pp">
<div className="flex w-full gap-2 pb-2">
<Image
src={PromptpayImage}
alt="Promptpay QR Code"
className="max-w-32 shrink-0 rounded"
/>
<div className="relative flex shrink-0 flex-col">
<span className="text-sm font-bold">บัญชีรับโดเนท</span>
<span className="text-sm text-muted-foreground">
ผู้รับ: นาย พัชรพล พลพันธุ์
</span>
<span className="text-sm text-muted-foreground">
บัญชี: xxx-x-x8666-x
</span>
<span className="text-sm text-muted-foreground">
เลขที่อ้างอิง: 004999056945438
</span>
<DownloadButton />
{artifactConfig.donatePromptpay ? (
<>
<div className="flex w-full gap-2 pb-2">
<Image
src={PromptpayImage}
alt="Promptpay QR Code"
className="max-w-32 shrink-0 rounded"
/>
<div className="relative flex shrink-0 flex-col">
<span className="text-sm font-bold">บัญชีรับโดเนท</span>
<span className="text-sm text-muted-foreground">
ผู้รับ: นาย พัชรพล พลพันธุ์
</span>
<span className="text-sm text-muted-foreground">
บัญชี: xxx-x-x8666-x
</span>
<span className="text-sm text-muted-foreground">
เลขที่อ้างอิง: 004999056945438
</span>
<DownloadButton />
</div>
</div>
<FormInput name="slip">
<SlipUpload />
</FormInput>
</>
) : (
<div className="flex gap-2">
<CircleX className="text-red-500" />{" "}
ขณะนี้การโอนเงินด้วยพร้อมเพย์ใช้ไม่ได้ชั่วคราว
</div>
</div>
<FormInput name="slip">
<SlipUpload />
</FormInput>
)}
</FormChoice>
</FormTab>
<div className="flex justify-end">
+2 -1
View File
@@ -74,7 +74,8 @@ export function VersionCheck({ headless = false }: { headless?: boolean }) {
}
if (newVersion !== version) ver.current = newVersion;
},
refresh() {
refresh(prefix) {
if (prefix && !window.location.pathname.startsWith(prefix)) return;
window.location.hash = "#update";
queueMicrotask(() => window.location.reload());
},
+3 -1
View File
@@ -74,7 +74,9 @@ export const versions = pgTable("versions", {
export const settings = pgTable("settings", {
id: boolean().primaryKey().default(true),
enka: boolean().notNull().default(false), // Turn this off when Enka is on maintenance
enka: boolean().notNull().default(true), // Turn this off when Enka is on maintenance
donatePromptpay: boolean().notNull().default(true),
donateTruemoney: boolean().notNull().default(true),
donateGoal: numeric({ mode: "number" }),
donateGoalStarting: timestamp(),
});
+1 -1
View File
@@ -48,7 +48,7 @@ export const sse = sseEndpointMap({
active: {
version: z.string(),
live: z.custom<YoutubeLiveInfo>(),
refresh: z.null(),
refresh: z.string().nullable(),
},
// Admin Live Log
log: {
+1 -1
View File
@@ -50,7 +50,7 @@ export default {
},
deploymentId: process.env.NEXT_DEPLOYMENT_ID,
// reactCompiler: true,
allowedDevOrigins: ["astral:3000", "dev3000.astrxl.dev"],
allowedDevOrigins: ["astral:3000", "dev3000.astrxl.dev", "nyx"],
// basePath: "/beta"
output: "standalone",
} satisfies NextConfig;