feat : update
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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></>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user