From 2429b9d3193d0882d2bc4caaf4971c41d4ccc92f Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sat, 5 Sep 2026 18:39:03 +0700 Subject: [PATCH] feat : update --- app/admin/[character]/[page]/page.tsx | 2 + components/admin/editor-presence.tsx | 2 +- components/admin/extra-sheet-editor.tsx | 52 ++++++++++++++++--- components/admin/glossary-manager.tsx | 9 +++- components/admin/guide-visibility-control.tsx | 2 +- components/public/structured-guide.tsx | 8 ++- components/public/visitor-presence.tsx | 4 +- lib/guides/extra-sheet.test.ts | 30 +++++++++-- lib/guides/extra-sheet.ts | 41 +++++++++++++-- 9 files changed, 129 insertions(+), 21 deletions(-) diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx index 5b58451..49bdeac 100644 --- a/app/admin/[character]/[page]/page.tsx +++ b/app/admin/[character]/[page]/page.tsx @@ -24,6 +24,8 @@ const sections = [ "extra", ] as const; +export const instant = false; + export default async function StructuredEditorPage({ params, }: { diff --git a/components/admin/editor-presence.tsx b/components/admin/editor-presence.tsx index 4e47898..dad118b 100644 --- a/components/admin/editor-presence.tsx +++ b/components/admin/editor-presence.tsx @@ -12,7 +12,7 @@ export function EditorPresence({ guideId }: { guideId: string }) {
- Editing {editors.length || (presence ? 0 : "—")} + Editing {editors.length || (presence ? 0 : "")} {editors.map((editor) => ( {editor.name} diff --git a/components/admin/extra-sheet-editor.tsx b/components/admin/extra-sheet-editor.tsx index b5d9d4a..8e77684 100644 --- a/components/admin/extra-sheet-editor.tsx +++ b/components/admin/extra-sheet-editor.tsx @@ -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 ( -
+
); @@ -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(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({ + + onChange(matrixToExtraSheet( value, next as ExtraSheetMatrix, @@ -313,13 +349,17 @@ export function ExtraSheetEditor({ แก้ไขเซลล์ - แถว {active.row + 1} · {activeColumn?.label ?? "คอลัมน์"} + {selectedCount > 1 + ? `นำข้อความเดียวกันไปใช้กับ ${selectedCount} เซลล์ที่เลือก` + : `แถว ${active.row + 1} · ${activeColumn?.label ?? "คอลัมน์"}`} diff --git a/components/admin/glossary-manager.tsx b/components/admin/glossary-manager.tsx index 8f1f4e9..f24f9fd 100644 --- a/components/admin/glossary-manager.tsx +++ b/components/admin/glossary-manager.tsx @@ -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 = { @@ -101,7 +103,10 @@ export function GlossaryManager({ {(catalogItem) => ( - + {catalogItem.name} diff --git a/components/admin/guide-visibility-control.tsx b/components/admin/guide-visibility-control.tsx index 85dbe96..4cca262 100644 --- a/components/admin/guide-visibility-control.tsx +++ b/components/admin/guide-visibility-control.tsx @@ -89,7 +89,7 @@ export function GuideVisibilityControl({ onClick={() => updateVisibility(true)} > {pending ? : } - Publish + Make Publish )}
diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 6b757c5..a166afb 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -1285,7 +1285,13 @@ function ExtraSectionContent({ {row.sheet.rows.map((sheetRow) => ( {row.sheet!.columns.map((column) => ( - + ))} diff --git a/components/public/visitor-presence.tsx b/components/public/visitor-presence.tsx index 689588e..66feba5 100644 --- a/components/public/visitor-presence.tsx +++ b/components/public/visitor-presence.tsx @@ -11,11 +11,11 @@ export function VisitorPresence({ guideId }: { guideId?: string }) {
- ทั้งหมด {counts?.global ?? "—"} + ทั้งหมด {counts?.global ?? ""} {guideId ? ( - Guide นี้ {counts?.guide ?? "—"} + Guide นี้ {counts?.guide ?? ""} ) : null}
diff --git a/lib/guides/extra-sheet.test.ts b/lib/guides/extra-sheet.test.ts index 48c97fa..dcabf7a 100644 --- a/lib/guides/extra-sheet.test.ts +++ b/lib/guides/extra-sheet.test.ts @@ -13,6 +13,7 @@ import { moveExtraSheetRow, normalizeExtraSheetRichText, updateExtraSheetCell, + updateExtraSheetCells, } from "@/lib/guides/extra-sheet"; function idFactory() { @@ -26,8 +27,8 @@ describe("Extra sheet", () => { expect(extraSheetSchema.safeParse(sheet).success).toBe(true); expect(sheet.columns.map((column) => column.label)).toEqual(["คอลัมน์ 1", "คอลัมน์ 2"]); expect(extraSheetToMatrix(sheet)).toEqual([ - [{ value: "" }, { value: "" }], - [{ value: "" }, { value: "" }], + [{ value: "", backgroundColor: undefined }, { value: "", backgroundColor: undefined }], + [{ value: "", backgroundColor: undefined }, { value: "", backgroundColor: undefined }], ]); }); @@ -42,8 +43,8 @@ describe("Extra sheet", () => { const moved = moveExtraSheetRow(moveExtraSheetColumn(sheet, 0, 1), 0, 1); expect(extraSheetToMatrix(moved)).toEqual([ - [{ value: "" }, { value: "C" }], - [{ value: "B" }, { value: "A" }], + [{ value: "", backgroundColor: undefined }, { value: "C", backgroundColor: undefined }], + [{ value: "B", backgroundColor: undefined }, { value: "A", backgroundColor: undefined }], ]); }); @@ -60,7 +61,10 @@ describe("Extra sheet", () => { const reduced = deleteExtraSheetColumn(expanded, 2); expect(reduced.rows.every((row) => !(columnId in row.cells))).toBe(true); - expect(deleteExtraSheetRow(reduced, 2).rows).toHaveLength(2); + const secondRowId = reduced.rows[1].id; + const withoutFirstRow = deleteExtraSheetRow(reduced, 0); + expect(withoutFirstRow.rows).toHaveLength(2); + expect(withoutFirstRow.rows[0].id).toBe(secondRowId); }); it("expands rows and columns when a pasted matrix is larger", () => { @@ -102,4 +106,20 @@ describe("Extra sheet", () => { "

safe

", ); }); + + it("updates rich text and backgrounds across a cell selection", () => { + const sheet = createExtraSheet(idFactory()); + const updated = updateExtraSheetCells(sheet, [ + { row: 0, column: 0 }, + { row: 1, column: 1 }, + ], { value: "

Shared

", backgroundColor: "#16A34A" }); + + expect(extraSheetToMatrix(updated)).toEqual([ + [{ value: "

Shared

", backgroundColor: "#16A34A" }, { value: "", backgroundColor: undefined }], + [{ value: "", backgroundColor: undefined }, { value: "

Shared

", backgroundColor: "#16A34A" }], + ]); + expect(extraSheetSchema.safeParse(updated).success).toBe(true); + expect(updateExtraSheetCells(updated, [{ row: 0, column: 0 }], { backgroundColor: null }) + .rows[0].backgrounds).toEqual({}); + }); }); diff --git a/lib/guides/extra-sheet.ts b/lib/guides/extra-sheet.ts index 2f06559..c456d7f 100644 --- a/lib/guides/extra-sheet.ts +++ b/lib/guides/extra-sheet.ts @@ -12,6 +12,7 @@ export const extraSheetSchema = z.strictObject({ rows: z.array(z.strictObject({ id: extraSheetId, cells: z.record(z.string(), z.string().max(10_000)), + backgrounds: z.record(z.string(), z.string().regex(/^#[0-9A-Fa-f]{6}$/)).default({}), })).min(1), }).superRefine((sheet, context) => { const columnIds = new Set(sheet.columns.map((column) => column.id)); @@ -27,7 +28,7 @@ export const extraSheetSchema = z.strictObject({ context.addIssue({ code: "custom", path: ["rows"], message: "รหัสแถวต้องไม่ซ้ำกัน" }); } sheet.rows.forEach((row, rowIndex) => { - Object.keys(row.cells).forEach((columnId) => { + [...Object.keys(row.cells), ...Object.keys(row.backgrounds)].forEach((columnId) => { if (!columnIds.has(columnId)) { context.addIssue({ code: "custom", @@ -41,8 +42,9 @@ export const extraSheetSchema = z.strictObject({ export type ExtraSheet = z.output; -export type ExtraSheetCell = { value: string }; +export type ExtraSheetCell = { value: string; backgroundColor?: string }; export type ExtraSheetMatrix = Array>; +type SheetPoint = { row: number; column: number }; export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): string { const labels = new Set(columns.map((column) => column.label.toLocaleLowerCase())); @@ -61,13 +63,17 @@ export function createExtraSheet(createId: (prefix: string) => string): ExtraShe rows: [1, 2].map(() => ({ id: createId("extra-sheet-row"), cells: Object.fromEntries(columns.map((column) => [column.id, ""])), + backgrounds: {}, })), }; } export function extraSheetToMatrix(sheet: ExtraSheet): ExtraSheetMatrix { return sheet.rows.map((row) => - sheet.columns.map((column) => ({ value: row.cells[column.id] ?? "" })), + sheet.columns.map((column) => ({ + value: row.cells[column.id] ?? "", + backgroundColor: row.backgrounds[column.id], + })), ); } @@ -94,6 +100,11 @@ export function matrixToExtraSheet( column.id, String(matrix[rowIndex]?.[columnIndex]?.value ?? ""), ])), + backgrounds: Object.fromEntries(columns.flatMap((column, columnIndex) => { + const backgroundColor = matrix[rowIndex]?.[columnIndex]?.backgroundColor + ?? sheet.rows[rowIndex]?.backgrounds[column.id]; + return backgroundColor ? [[column.id, backgroundColor]] : []; + })), })); return { columns, rows }; } @@ -115,6 +126,7 @@ export function addExtraSheetRow(sheet: ExtraSheet, id: string): ExtraSheet { rows: [...sheet.rows, { id, cells: Object.fromEntries(sheet.columns.map((column) => [column.id, ""])), + backgrounds: {}, }], }; } @@ -152,6 +164,7 @@ export function deleteExtraSheetColumn(sheet: ExtraSheet, index: number): ExtraS rows: sheet.rows.map((row) => ({ ...row, cells: Object.fromEntries(Object.entries(row.cells).filter(([key]) => key !== columnId)), + backgrounds: Object.fromEntries(Object.entries(row.backgrounds).filter(([key]) => key !== columnId)), })), }; } @@ -178,6 +191,28 @@ export function updateExtraSheetCell( }; } +export function updateExtraSheetCells( + sheet: ExtraSheet, + points: Iterable, + update: { value?: string; backgroundColor?: string | null }, +): ExtraSheet { + const selected = new Set(Array.from(points, ({ row, column }) => `${row}:${column}`)); + return { + ...sheet, + rows: sheet.rows.map((row, rowIndex) => { + const cells = { ...row.cells }; + const backgrounds = { ...row.backgrounds }; + sheet.columns.forEach((column, columnIndex) => { + if (!selected.has(`${rowIndex}:${columnIndex}`)) return; + if (update.value !== undefined) cells[column.id] = update.value; + if (update.backgroundColor === null) delete backgrounds[column.id]; + else if (update.backgroundColor !== undefined) backgrounds[column.id] = update.backgroundColor; + }); + return { ...row, cells, backgrounds }; + }), + }; +} + export function normalizeExtraSheetRichText(sheet: ExtraSheet): ExtraSheet { return { ...sheet,