feat : update
CI / Verify (push) Successful in 1m45s
CI / Build immutable images and deploy (push) Successful in 2m26s

This commit is contained in:
2026-10-07 19:01:45 +07:00 Unverified
parent 26660aaa49
commit 6d0feb11ca
25 changed files with 87 additions and 32 deletions
@@ -33,7 +33,7 @@ import {
EmptyTitle,
} from "@/components/ui/empty";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { isValidPercent } from "@/lib/guides/percent-input";
+2 -1
View File
@@ -14,7 +14,8 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
import { ClearableInputGroupInput as InputGroupInput } from "@/components/admin/ui/clearable-input";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { characterRarityGradientClass } from "@/lib/catalog/rarity";
+1 -1
View File
@@ -82,7 +82,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import {
InputGroup,
InputGroupAddon,
+1 -1
View File
@@ -14,7 +14,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
+1 -1
View File
@@ -35,7 +35,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -27,7 +27,7 @@ import {
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import {
Combobox,
ComboboxContent,
+1 -1
View File
@@ -119,7 +119,7 @@ import {
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Select,
@@ -81,7 +81,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import {
Popover,
PopoverContent,
+1 -1
View File
@@ -47,8 +47,8 @@ import {
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { ClearableInputGroupInput as InputGroupInput } from "@/components/admin/ui/clearable-input";
import { Spinner } from "@/components/ui/spinner";
import { useAdminPresence } from "@/hooks/use-presence";
import type { CatalogSyncJob } from "@/lib/catalog/sync";
+1 -1
View File
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import { Textarea } from "@/components/ui/textarea";
import { supportsWeaponType } from "@/lib/catalog/lunaris";
+55
View File
@@ -0,0 +1,55 @@
"use client";
import { useRef, useState, type ComponentProps } from "react";
import { XIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
export function ClearableInput({ className, ...props }: ComponentProps<typeof Input>) {
const editableText = ["text", "search", "email", "url", "tel"].includes(props.type ?? "text");
if (!editableText) return <Input className={className} {...props} />;
return <InputGroup className={className}><ClearableInputGroupInput {...props} /></InputGroup>;
}
export function ClearableInputGroupInput({ ref, onChange, onValueChange, ...props }: ComponentProps<typeof Input>) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [uncontrolledValue, setUncontrolledValue] = useState(props.defaultValue ?? "");
const value = props.value ?? uncontrolledValue;
return (
<>
<InputGroupInput
{...props}
ref={(node) => {
inputRef.current = node;
if (typeof ref === "function") return ref(node);
if (ref) ref.current = node;
}}
onChange={(event) => {
setUncontrolledValue(event.target.value);
onValueChange?.(event.target.value);
onChange?.(event);
}}
/>
{String(value).length > 0 && !props.disabled && !props.readOnly ? (
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label={`ล้าง${props["aria-label"] ?? props.placeholder ?? "ข้อความ"}`}
onClick={() => {
const input = inputRef.current;
if (!input) return;
// Use the native setter so React receives the same change as typing.
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(input, "");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.focus();
}}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
) : null}
</>
);
}
+1 -1
View File
@@ -36,7 +36,7 @@ import {
EmptyTitle,
} from "@/components/ui/empty";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import {
Select,
SelectContent,
+1 -1
View File
@@ -49,7 +49,7 @@ export function MobileSlipForm() {
{ready && <><p>ยอดชำระ ฿{amount}</p><form onSubmit={submit} className="flex flex-col gap-4">
<Field><FieldLabel htmlFor="mobile-slip">เลือกรูปสลิปจากแอปธนาคาร</FieldLabel>
<Input id="mobile-slip" name="file" type="file" accept="image/png,image/jpeg,image/webp" required disabled={busy} /></Field>
<Button type="submit" className="min-h-11" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิป"}</Button>
<Button type="submit" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิป"}</Button>
</form></>}
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
</div>;
@@ -70,7 +70,7 @@ export function MobileSlipHandoff({ checkoutId }: { checkoutId: string }) {
}, [checkoutId, link, router]);
return <div className="flex w-full flex-col items-start gap-3">
<Button type="button" variant="link" className="min-h-11 px-0 text-foreground underline" disabled={busy}
<Button type="button" variant="link" className="px-0 text-foreground underline" disabled={busy}
onClick={() => { if (link && !expired && secondsLeft > 0) setOpen(true); else void createLink(); }}>
{busy ? "กำลังสร้าง QR..." : "ใช้มือถืออีกเครื่องอัปโหลด"}
</Button>
+2 -2
View File
@@ -53,7 +53,7 @@ export function CommissionPaymentForm({ checkoutId, amountBaht, qr, expiresAt, s
{/* Generated locally from the fixed checkout amount and configured recipient. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={qr} alt={`PromptPay QR สำหรับชำระเงิน ${amountBaht} บาท`} width={300} height={300} />
<Button nativeButton={false} className="min-h-11" render={<a href={qr} download="promptpay-qr.png" />}>บันทึก QR สำหรับชำระเงิน</Button>
<Button nativeButton={false} render={<a href={qr} download="promptpay-qr.png" />}>บันทึก QR สำหรับชำระเงิน</Button>
<strong>฿{amountBaht}</strong>
<p className="text-center text-sm text-muted-foreground">ใช้มือถือเครื่องนี้ชำระเงิน: บันทึก QR แล้วเลือกรูปในแอปธนาคาร</p>
<p className="text-sm text-muted-foreground">โอนเงินแล้วอัปโหลดรูปสลิปเพื่อยืนยัน</p>
@@ -63,6 +63,6 @@ export function CommissionPaymentForm({ checkoutId, amountBaht, qr, expiresAt, s
<Field><FieldLabel htmlFor="slip">อัปโหลดสลิปที่ชำระเงินแล้ว</FieldLabel>
<Input id="slip" name="file" type="file" accept="image/png,image/jpeg,image/webp" required />
<MobileSlipHandoff checkoutId={checkoutId} /></Field>
<Button type="submit" className="min-h-11" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิปและเปิดแชตคำขอ"}</Button>
<Button type="submit" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิปและเปิดแชตคำขอ"}</Button>
</FieldGroup>{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}</form></>;
}
+1 -1
View File
@@ -278,7 +278,7 @@ export function CommissionRequestForm({ catalog, userId }: { catalog: Catalog; u
<p className="text-xs text-muted-foreground sm:hidden">{request.teams.length} ทีม - {request.weapons.reduce((sum, set) => sum + set.weaponKeys.filter(Boolean).length, 0)} อาวุธ - {request.constellations.reduce((sum, set) => sum + set.levels.length, 0)} ระดับกลุ่มดาว</p>
{request.teams.length === 0 && request.weapons.length + request.constellations.length > 0 && <p className="text-sm text-muted-foreground">รวมค่าบริการพื้นฐาน ฿100</p>}
</div>
<Button className="min-h-11" disabled={!hydrated || busy || commissionPrice(request) === 0} onClick={() => void submit()}>{busy ? "กำลังสร้างคำขอ…" : "ยืนยันและชำระเงิน"}</Button>
<Button disabled={!hydrated || busy || commissionPrice(request) === 0} onClick={() => void submit()}>{busy ? "กำลังสร้างคำขอ…" : "ยืนยันและชำระเงิน"}</Button>
</CardFooter>
</Card>
</FieldSet>;
+1 -1
View File
@@ -148,7 +148,7 @@ export function ArtifactImageExport({
return (
<Dialog>
<DialogTrigger render={<Button type="button" variant="outline" size="sm" className="max-sm:min-h-11" />}>
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
<DownloadIcon data-icon="inline-start" />
บันทึกภาพ
</DialogTrigger>
+2 -2
View File
@@ -229,7 +229,7 @@ export function GuideImageExport({
type="button"
variant="outline"
size="sm"
className={mobileIconOnly ? "max-sm:size-11 max-sm:px-0 max-sm:has-data-[icon=inline-start]:pl-0" : "max-sm:min-h-11"}
className={mobileIconOnly ? "max-sm:size-11 max-sm:px-0 max-sm:has-data-[icon=inline-start]:pl-0" : undefined}
disabled={capturing || disabled}
onClick={() => {
if (modeSelection === "direct") setCaptureMode("with-notes");
@@ -299,7 +299,7 @@ export function GuideImageExport({
);
const exportAction = (
<Button type="button" className="max-sm:min-h-11" disabled={capturing || disabled} onClick={requestCapture}>
<Button type="button" disabled={capturing || disabled} onClick={requestCapture}>
{capturing ? <Spinner data-icon="inline-start" /> : <DownloadIcon data-icon="inline-start" />}
{capturing ? "กำลังบันทึก…" : buttonLabel}
</Button>
+1 -1
View File
@@ -36,7 +36,7 @@ export function PublicNavigation() {
</nav>
<div className="lg:hidden">
<Drawer>
<DrawerTrigger render={<Button type="button" variant="outline" size="sm" className="min-h-11" aria-label="เปิดเมนูหลัก" />}>
<DrawerTrigger render={<Button type="button" variant="outline" size="sm" aria-label="เปิดเมนูหลัก" />}>
<MenuIcon data-icon="inline-start" aria-hidden="true" />เมนู
</DrawerTrigger>
<DrawerContent>
+4 -4
View File
@@ -170,7 +170,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<InputGroup className="min-h-11 sm:min-h-9 min-w-0 flex-1 sm:w-48 sm:flex-none">
<InputGroup className="min-h-11 sm:min-h-9 min-w-0 flex-1 sm:w-96 sm:flex-none">
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
@@ -192,15 +192,15 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
</div>
<div className="sm:hidden">
<Drawer>
<DrawerTrigger render={<Button variant="outline" className="min-h-11 w-full" />}>
<DrawerTrigger render={<Button variant="outline" className="w-full" />}>
<SlidersHorizontalIcon data-icon="inline-start" />ตัวกรอง{activeFilters.length ? ` (${activeFilters.length})` : ""}
</DrawerTrigger>
<DrawerContent>
<DrawerHeader><DrawerTitle>ตัวกรองไกด์</DrawerTitle><DrawerDescription>เลือกธาตุ อาวุธ หรือจำนวนดาว</DrawerDescription></DrawerHeader>
<div className="flex max-h-[60svh] flex-col gap-5 overflow-y-auto px-4 pb-4">{filterControls(true)}</div>
<div className="flex gap-2 border-t p-4">
<Button variant="outline" className="min-h-11" disabled={!activeFilters.length} onClick={() => clearParams()}>ล้างตัวกรอง</Button>
<DrawerClose render={<Button className="min-h-11 flex-1" />}>ดู {filtered.length} ไกด์</DrawerClose>
<Button variant="outline" disabled={!activeFilters.length} onClick={() => clearParams()}>ล้างตัวกรอง</Button>
<DrawerClose render={<Button className="flex-1" />}>ดู {filtered.length} ไกด์</DrawerClose>
</div>
</DrawerContent>
</Drawer>
-1
View File
@@ -779,7 +779,6 @@ function ArtifactSectionContent({
const TierGroup = exporting ? Card : Fragment;
const tierCards = guide.artifactGroups.map((condition, conditionIndex) => (
<div key={condition.id} data-tier-condition={conditionIndex} className="artifact-export-tierlist flex flex-col gap-5">
{!exporting && <ComparisonContext />}
{exporting && guide.artifactGroups.length > 1 ? <p className="text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
<TierList>
{condition.tiers.map((group, groupIndex) => <TierGroup key={group.id}>
+1 -1
View File
@@ -549,7 +549,7 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
<CardContent className="py-8 text-center text-sm text-muted-foreground">
<div className="flex flex-col items-center gap-3">
<p>{query.trim() ? "ไม่พบทีมที่ตรงกับคำค้นหาในเงื่อนไขนี้" : "ยังไม่มีทีมในเงื่อนไขนี้"}</p>
{query && <Button className="min-h-11" variant="outline" onClick={() => setQuery("")}>ล้างการค้นหา</Button>}
{query && <Button variant="outline" onClick={() => setQuery("")}>ล้างการค้นหา</Button>}
</div>
</CardContent>
</Card>
+1 -1
View File
@@ -177,7 +177,7 @@ export function WeaponImageExport({
return (
<Dialog>
<DialogTrigger render={<Button type="button" variant="outline" size="sm" className="max-sm:min-h-11" />}>
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
<DownloadIcon data-icon="inline-start" />
บันทึกภาพ
</DialogTrigger>