318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
import { formParse, FormSubmitResult } from "@/components/form-submit";
|
|
import {
|
|
FormAction,
|
|
FormChoice,
|
|
FormIf,
|
|
FormInput,
|
|
FormProvider,
|
|
FormRow,
|
|
FormTab,
|
|
FormWrapper,
|
|
} from "@/components/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
CurrencyInput,
|
|
SlipUpload,
|
|
} from "../rubgram/admin/@modal/manual/client";
|
|
import Image from "@/components/image";
|
|
import PromptpayImage from "#/assets/promptpay.jpg";
|
|
import { DownloadButton } from "../rubgram/client";
|
|
import { QrCodeIcon, SendIcon } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import TruemoneyIcon from "#/assets/tmn.webp";
|
|
import z from "zod";
|
|
import { th } from "zod/v4/locales";
|
|
import Cropper from "@/components/cropper";
|
|
import { checkSlip } from "@/lib/payment";
|
|
import { db } from "@/lib/db";
|
|
import { donations, endgameSlips, submissions } from "@/lib/db/schema";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { uidRegex } from "@/lib/const";
|
|
import { getArtifactConfig } from "@/lib/api";
|
|
import { sse } from "@/lib/db/sse-endpoints";
|
|
import { fileToDataUrl } from "@/lib/utils";
|
|
import type { Metadata } from "next";
|
|
const { TMN_DEST_PHONE_NUM, SASTIFY_API_PRIVKEY } = process.env as Record<
|
|
string,
|
|
string
|
|
>;
|
|
|
|
export const metadata: Metadata = {
|
|
title: "โดเนท",
|
|
};
|
|
|
|
type SastifyApiResponse =
|
|
| {
|
|
success: true;
|
|
data: {
|
|
amount: number;
|
|
status: "SUCCESS";
|
|
};
|
|
}
|
|
| {
|
|
success: false;
|
|
code?: string | number;
|
|
message: string;
|
|
};
|
|
|
|
z.config(th());
|
|
|
|
//#region Schema
|
|
const Schema = z
|
|
.object({
|
|
name: z.string().max(50, "ชื่อยาวสุด 50 ตัวอักษร").default("Anonymous"),
|
|
message: z.string().max(500, "ข้อความยาวสุด 200 ตัวอักษร").default(""),
|
|
amount: z.coerce
|
|
.number("จำนวนต้องเป็นตัวเลข")
|
|
.min(10, "โดเนทขั้นต่ำ 1 บาท")
|
|
.max(10000, "โดเนทได้ไม่เกิน 1 หมื่นบาท"),
|
|
image: z.file().optional(),
|
|
})
|
|
.and(
|
|
z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("tmn").optional(),
|
|
link: z.httpUrl(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("pp"),
|
|
slip: z.file(),
|
|
}),
|
|
]),
|
|
)
|
|
.and(
|
|
z.discriminatedUnion("artifact", [
|
|
z.object({
|
|
artifact: z.literal("false").optional(),
|
|
}),
|
|
z.object({
|
|
artifact: z.literal("true"),
|
|
uid: z.string().regex(uidRegex, "รูปแบบ UID ไม่ถูกต้อง"),
|
|
}),
|
|
]),
|
|
);
|
|
|
|
export default async function () {
|
|
//#region Server Data Load
|
|
const artifactConfig = await getArtifactConfig();
|
|
//#region Submit Handler
|
|
async function submit(data: FormData): Promise<FormSubmitResult> {
|
|
"use server";
|
|
const { $, error } = formParse(Schema, data);
|
|
if (error) return { error };
|
|
|
|
return await db.transaction(async (tx): Promise<FormSubmitResult> => {
|
|
if ($.type === "pp") {
|
|
const arrayBuffer = await $.slip.arrayBuffer();
|
|
const buffer = Buffer.from(arrayBuffer);
|
|
const processed = await checkSlip(buffer, $.slip.type, $.amount);
|
|
if (!processed.success)
|
|
return {
|
|
error: {
|
|
where: "slip",
|
|
what: `${processed.code}: ${processed.message}`,
|
|
},
|
|
};
|
|
const [check] = await tx
|
|
.insert(endgameSlips)
|
|
.values({
|
|
slip: buffer,
|
|
amount: $.amount.toString(),
|
|
data: processed,
|
|
ref: processed.data.transRef,
|
|
})
|
|
.returning({ id: endgameSlips.id })
|
|
.catch((e) => {
|
|
console.log(e);
|
|
return [{ id: "conflict" }];
|
|
});
|
|
if (check.id === "conflict")
|
|
return { error: { where: "slip", what: "สลิปนี้ถูกใช้ไปแล้ว" } };
|
|
} else {
|
|
const res: SastifyApiResponse = await fetch(
|
|
"https://api.sastify.xyz/v1/gateway/tmn",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${SASTIFY_API_PRIVKEY}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
phone_number: TMN_DEST_PHONE_NUM,
|
|
voucher_url: $.link,
|
|
}),
|
|
signal: AbortSignal.timeout(30_000),
|
|
},
|
|
)
|
|
.then((r) => r.json())
|
|
.catch((e) => e);
|
|
if (!res.success)
|
|
return { error: { where: "link", what: res.message } };
|
|
}
|
|
|
|
const { name, amount, message, image } = $;
|
|
|
|
const [{ id }] = await tx
|
|
.insert(donations)
|
|
.values({
|
|
name,
|
|
amount,
|
|
message,
|
|
image: image ? Buffer.from(await image.arrayBuffer()) : undefined,
|
|
uid: $.artifact === "true" ? $.uid : null,
|
|
// dont send on screen if less than 10
|
|
sent: $.amount < 10,
|
|
})
|
|
.returning({ id: donations.id });
|
|
if ($.artifact === "true") {
|
|
const res = await tx
|
|
.insert(submissions)
|
|
.values({
|
|
name,
|
|
comment: message,
|
|
uid: $.uid,
|
|
queue: null as unknown as undefined,
|
|
})
|
|
.catch(() => "conflict");
|
|
if (res === "conflict") {
|
|
tx.rollback();
|
|
return { error: { where: "uid", what: "ไม่สามารถสร้างคิวลัดได้" } };
|
|
}
|
|
}
|
|
if ($.amount >= 10)
|
|
sse.donate.pub("ping", {
|
|
id,
|
|
name,
|
|
amount,
|
|
message,
|
|
image: image ? await fileToDataUrl(image) : undefined,
|
|
});
|
|
|
|
return { toast: "ส่งเรียบร้อย", reset: true };
|
|
});
|
|
}
|
|
|
|
//#region TSX
|
|
return (
|
|
<div className="flex items-center justify-center min-h-svh">
|
|
<div className="p-5 bg-card border rounded-md w-full max-w-md">
|
|
<FormProvider id="tip" inDialog={false} onSubmit={submit}>
|
|
<FormRow>
|
|
<FormInput name="image" className="h-full">
|
|
<Cropper />
|
|
</FormInput>
|
|
<div className="grid gap-4 [&>label]:-mb-2 grow">
|
|
<FormInput name="name" label="ชื่อ" subLabel="ไม่จำเป็น">
|
|
<Input placeholder="Anonymous" />
|
|
</FormInput>
|
|
<FormInput
|
|
name="amount"
|
|
label="จำนวนโดเนท"
|
|
subLabel="ขึ้นจอขั้นต่ำ 10 บาท"
|
|
>
|
|
<CurrencyInput placeholder="ขั้นต่ำ 1 บาท" />
|
|
</FormInput>
|
|
</div>
|
|
</FormRow>
|
|
<FormInput name="message" label="ข้อความ" subLabel="max. 500 ตัวอักษร">
|
|
<Textarea placeholder="ข้อความ" />
|
|
</FormInput>
|
|
{!artifactConfig.locked && (
|
|
<>
|
|
<FormInput name="artifact">
|
|
<FormWrapper className="flex gap-2 items-center">
|
|
<Checkbox />
|
|
ลัดคิวเสือกไอดีชาวบ้าน
|
|
</FormWrapper>
|
|
</FormInput>
|
|
<FormIf artifact={true}>
|
|
<FormInput name="uid" label="UID สำหรับเสือกไอดีชาวบ้าน">
|
|
<Input placeholder="814006303" />
|
|
</FormInput>
|
|
</FormIf>
|
|
</>
|
|
)}
|
|
<FormTab
|
|
label="วิธีการโอนเงิน"
|
|
name="type"
|
|
tabs={[
|
|
{
|
|
label: (
|
|
<span className="flex gap-1 items-center">
|
|
<Image
|
|
src={TruemoneyIcon}
|
|
alt="Truemoney"
|
|
className="w-12 h-6 object-cover"
|
|
/>
|
|
TrueMoney
|
|
</span>
|
|
),
|
|
value: "tmn",
|
|
},
|
|
{
|
|
label: (
|
|
<span className="flex gap-1 items-center">
|
|
<QrCodeIcon className="size-6" />
|
|
PromptPay
|
|
</span>
|
|
),
|
|
value: "pp",
|
|
},
|
|
]}
|
|
>
|
|
<FormChoice value="tmn">
|
|
<FormInput name="link" label="ลิ้งค์อั่งเปา">
|
|
<Input placeholder="https://gift.truemoney.com/campaign/?v=..." />
|
|
</FormInput>
|
|
</FormChoice>
|
|
<FormChoice value="pp">
|
|
<div className="flex gap-2 w-full pb-2">
|
|
<Image
|
|
src={PromptpayImage}
|
|
alt="Promptpay QR Code"
|
|
className="rounded max-w-32 shrink-0"
|
|
/>
|
|
<div className="flex flex-col shrink-0 relative">
|
|
<span className="font-bold text-sm">บัญชีรับโดเนท</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>
|
|
</FormChoice>
|
|
</FormTab>
|
|
<div className="flex justify-end">
|
|
<Button asChild>
|
|
<FormAction
|
|
disabled
|
|
type="submit"
|
|
loading={
|
|
<>
|
|
<Spinner />
|
|
Creating...
|
|
</>
|
|
}
|
|
>
|
|
<SendIcon />
|
|
ส่งเลยจัฟลูกพี่
|
|
</FormAction>
|
|
</Button>
|
|
</div>
|
|
</FormProvider>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|