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

This commit is contained in:
2026-09-05 18:39:03 +07:00 Unverified
parent 5aa0ae80c1
commit 2429b9d319
9 changed files with 129 additions and 21 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ export function EditorPresence({ guideId }: { guideId: string }) {
<div className="flex flex-wrap items-center gap-1" aria-live="polite">
<Badge variant="secondary">
<UsersIcon data-icon="inline-start" />
Editing {editors.length || (presence ? 0 : "—")}
Editing {editors.length || (presence ? 0 : "")}
</Badge>
{editors.map((editor) => (
<Badge key={editor.userId} variant="outline">{editor.name}</Badge>
+46 -6
View File
@@ -8,10 +8,11 @@ import {
Columns3Icon,
PlusIcon,
PencilIcon,
PaletteIcon,
Rows3Icon,
Trash2Icon,
} from "lucide-react";
import Spreadsheet, { type ColumnIndicatorProps, type DataEditorProps, type Point } from "react-spreadsheet";
import Spreadsheet, { type ColumnIndicatorProps, type DataEditorProps, type Point, type Selection } from "react-spreadsheet";
import { useCallback, useEffect, useMemo, useState } from "react";
import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog";
@@ -48,16 +49,21 @@ import {
moveExtraSheetRow,
nextExtraSheetColumnLabel,
updateExtraSheetCell,
updateExtraSheetCells,
type ExtraSheet,
type ExtraSheetMatrix,
} from "@/lib/guides/extra-sheet";
import { normalizeExtraRichText } from "@/lib/rich-text";
type DeleteTarget = "column" | "row" | null;
const DEFAULT_CELL_BACKGROUND = "#16A34A";
function LiteralDataViewer({ cell }: { cell?: { value?: unknown } }) {
function LiteralDataViewer({ cell }: { cell?: { value?: unknown; backgroundColor?: string } }) {
return (
<div className="Spreadsheet__data-viewer Spreadsheet__data-viewer--preserve-breaks">
<div
className="Spreadsheet__data-viewer Spreadsheet__data-viewer--preserve-breaks size-full"
style={cell?.backgroundColor ? { backgroundColor: cell.backgroundColor } : undefined}
>
<RichTextContent html={normalizeExtraRichText(String(cell?.value ?? ""))} />
</div>
);
@@ -119,6 +125,7 @@ export function ExtraSheetEditor({
const [removeOpen, setRemoveOpen] = useState(false);
const [cellDialogOpen, setCellDialogOpen] = useState(false);
const [cellDraft, setCellDraft] = useState("");
const [selection, setSelection] = useState<Selection | null>(null);
const matrix = useMemo(() => extraSheetToMatrix(value), [value]);
const activeColumn = value.columns[active.column];
const activeRow = value.rows[active.row];
@@ -127,6 +134,11 @@ export function ExtraSheetEditor({
index !== (renaming ? active.column : -1) &&
column.label.toLocaleLowerCase() === normalizedLabel,
);
const selectedPoints = useCallback(() => {
const range = selection?.toRange(matrix);
return range ? Array.from(range) : [active];
}, [active, matrix, selection]);
const selectedCount = selectedPoints().length;
const openAddColumn = () => {
setRenaming(false);
@@ -200,7 +212,30 @@ export function ExtraSheetEditor({
</Button>
<Button type="button" variant="outline" size="sm" onClick={openActiveCellEditor}>
<PencilIcon data-icon="inline-start" />
แก้ไขเซลล์
แก้ไข {selectedCount > 1 ? `${selectedCount} เซลล์` : "เซลล์"}
</Button>
<label className="relative">
<Button type="button" variant="outline" size="sm" render={<span />} nativeButton={false}>
<PaletteIcon data-icon="inline-start" />
สีพื้นหลัง {selectedCount > 1 ? `${selectedCount} เซลล์` : "เซลล์"}
</Button>
<input
type="color"
aria-label="เลือกสีพื้นหลังเซลล์"
className="absolute inset-0 cursor-pointer opacity-0"
value={activeRow?.backgrounds[activeColumn?.id ?? ""] ?? DEFAULT_CELL_BACKGROUND}
onChange={(event) => onChange(updateExtraSheetCells(value, selectedPoints(), {
backgroundColor: event.target.value,
}))}
/>
</label>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onChange(updateExtraSheetCells(value, selectedPoints(), { backgroundColor: null }))}
>
ล้างสีพื้นหลัง
</Button>
<DropdownMenu>
<DropdownMenuTrigger
@@ -300,6 +335,7 @@ export function ExtraSheetEditor({
DataViewer={LiteralDataViewer}
DataEditor={dataEditor}
onActivate={setActive}
onSelect={setSelection}
onChange={(next) => onChange(matrixToExtraSheet(
value,
next as ExtraSheetMatrix,
@@ -313,13 +349,17 @@ export function ExtraSheetEditor({
<DialogHeader>
<DialogTitle>แก้ไขเซลล์</DialogTitle>
<DialogDescription>
แถว {active.row + 1} · {activeColumn?.label ?? "คอลัมน์"}
{selectedCount > 1
? `นำข้อความเดียวกันไปใช้กับ ${selectedCount} เซลล์ที่เลือก`
: `แถว ${active.row + 1} · ${activeColumn?.label ?? "คอลัมน์"}`}
</DialogDescription>
</DialogHeader>
<RichTextEditor value={cellDraft} onChange={setCellDraft} showBubbleMenu={false} />
<DialogFooter showCloseButton>
<Button type="button" onClick={() => {
onChange(updateExtraSheetCell(value, active.row, active.column, cellDraft));
onChange(selectedCount > 1
? updateExtraSheetCells(value, selectedPoints(), { value: cellDraft })
: updateExtraSheetCell(value, active.row, active.column, cellDraft));
setCellDialogOpen(false);
}}>บันทึกเซลล์</Button>
</DialogFooter>
+7 -2
View File
@@ -16,8 +16,10 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import type { GlossaryKind } from "@/lib/glossary";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
type CatalogItem = { key: string; name: string; imageUrl: string };
type CatalogItem = { key: string; name: string; imageUrl: string; rarity?: number | null };
type Alias = { id: string; kind: string; catalogKey: string; alias: string };
const kindLabels: Record<GlossaryKind, string> = {
@@ -101,7 +103,10 @@ export function GlossaryManager({
<ComboboxList>
{(catalogItem) => (
<ComboboxItem key={catalogItem.key} value={catalogItem}>
<span className="relative size-8 shrink-0 overflow-hidden rounded-md bg-muted">
<span className={cn(
"relative size-8 shrink-0 overflow-hidden rounded-md bg-muted",
rarityGradientClass(catalogItem.rarity),
)}>
<Image src={catalogItem.imageUrl} alt="" fill className="object-contain" />
</span>
{catalogItem.name}
@@ -89,7 +89,7 @@ export function GuideVisibilityControl({
onClick={() => updateVisibility(true)}
>
{pending ? <Spinner data-icon="inline-start" /> : <EyeIcon data-icon="inline-start" />}
Publish
Make Publish
</Button>
)}
</div>