Files
buzz/app/(ui)/donate/page.tsx
T
dmgnr 3d7ee68c5c
Build and Push Image / docker (push) Successful in 1m43s
feat(donate): opengraph
2026-06-08 12:24:13 +07:00

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>
);
}