fix : warning from tailwindcss
This commit is contained in:
@@ -7,39 +7,135 @@ import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import type { AutosaveState } from "@/hooks/use-autosave";
|
||||
|
||||
export function AutosaveStatus({ state, issues = [], onSave, conflictVersion, canOverwrite = false, onReloadLatest, onOverwrite }: { state: AutosaveState; issues?: readonly string[]; onSave?: () => void; conflictVersion?: number | null; canOverwrite?: boolean; onReloadLatest?: () => void; onOverwrite?: () => void }) {
|
||||
export function AutosaveStatus({
|
||||
state,
|
||||
issues = [],
|
||||
onSave,
|
||||
conflictVersion,
|
||||
canOverwrite = false,
|
||||
onReloadLatest,
|
||||
onOverwrite,
|
||||
}: {
|
||||
state: AutosaveState;
|
||||
issues?: readonly string[];
|
||||
onSave?: () => void;
|
||||
conflictVersion?: number | null;
|
||||
canOverwrite?: boolean;
|
||||
onReloadLatest?: () => void;
|
||||
onOverwrite?: () => void;
|
||||
}) {
|
||||
const [showIssues, setShowIssues] = useState(false);
|
||||
|
||||
if (state === "saving") return <Badge variant="secondary"><Spinner data-icon="inline-start" />Saving…</Badge>;
|
||||
if (state === "unsaved") return <Badge variant="outline"><ClockIcon data-icon="inline-start" />Waiting to save…</Badge>;
|
||||
if (state === "incomplete") return <div className="flex flex-col items-end gap-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setShowIssues((current) => !current)} aria-expanded={showIssues}>
|
||||
<AlertCircleIcon data-icon="inline-start" />{showIssues ? "ซ่อนรายการ" : "ตรวจสอบข้อมูลที่ยังไม่ครบ"}
|
||||
if (state === "saving")
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<Spinner data-icon="inline-start" />
|
||||
Saving…
|
||||
</Badge>
|
||||
);
|
||||
if (state === "unsaved")
|
||||
return (
|
||||
<Badge variant="outline">
|
||||
<ClockIcon data-icon="inline-start" />
|
||||
Waiting to save…
|
||||
</Badge>
|
||||
);
|
||||
if (state === "incomplete")
|
||||
return (
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowIssues((current) => !current)}
|
||||
aria-expanded={showIssues}
|
||||
>
|
||||
<AlertCircleIcon data-icon="inline-start" />
|
||||
{showIssues ? "ซ่อนรายการ" : "ตรวจสอบข้อมูลที่ยังไม่ครบ"}
|
||||
</Button>
|
||||
{showIssues ? <Alert variant="destructive" className="max-w-md">
|
||||
{showIssues ? (
|
||||
<Alert variant="destructive" className="max-w-md">
|
||||
<AlertCircleIcon aria-hidden="true" />
|
||||
<AlertTitle>กรุณากรอกข้อมูลให้ครบ</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>แก้ไขรายการด้านล่าง แล้วระบบจะบันทึกให้อัตโนมัติ ข้อมูลที่กรอกไว้ยังไม่หาย</p>
|
||||
{issues.length ? <ul className="flex flex-col gap-1">{issues.map((issue, index) => <li key={`${issue}-${index}`}>{issue}</li>)}</ul> : null}
|
||||
<p>
|
||||
แก้ไขรายการด้านล่าง แล้วระบบจะบันทึกให้อัตโนมัติ
|
||||
ข้อมูลที่กรอกไว้ยังไม่หาย
|
||||
</p>
|
||||
{issues.length ? (
|
||||
<ul className="flex flex-col gap-1">
|
||||
{issues.map((issue, index) => (
|
||||
<li key={`${issue}-${index}`}>{issue}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert> : null}
|
||||
</div>;
|
||||
if (state === "conflict") return <Alert id="autosave-conflict" variant="destructive" className="max-w-xl scroll-m-6">
|
||||
</Alert>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
if (state === "conflict")
|
||||
return (
|
||||
<Alert
|
||||
id="autosave-conflict"
|
||||
variant="destructive"
|
||||
className="max-w-xl scroll-m-6"
|
||||
>
|
||||
<AlertCircleIcon aria-hidden="true" />
|
||||
<AlertTitle>มีการบันทึกข้อมูลใหม่จากผู้แก้ไขคนอื่น</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>ระบบหยุด Autosave ไว้เพื่อไม่ให้ Draft นี้เขียนทับข้อมูลล่าสุด{conflictVersion ? ` (version ${conflictVersion})` : ""}</p>
|
||||
<p>
|
||||
ระบบหยุด Autosave ไว้เพื่อไม่ให้ Draft นี้เขียนทับข้อมูลล่าสุด
|
||||
{conflictVersion ? ` (version ${conflictVersion})` : ""}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{onReloadLatest ? <Button type="button" variant="outline" size="sm" onClick={onReloadLatest}>โหลดข้อมูลล่าสุด</Button> : null}
|
||||
{onOverwrite ? <Button type="button" variant="destructive" size="sm" disabled={!canOverwrite} onClick={onOverwrite}>เขียนทับด้วย Draft นี้</Button> : null}
|
||||
{onReloadLatest ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onReloadLatest}
|
||||
>
|
||||
โหลดข้อมูลล่าสุด
|
||||
</Button>
|
||||
) : null}
|
||||
{onOverwrite ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={!canOverwrite}
|
||||
onClick={onOverwrite}
|
||||
>
|
||||
เขียนทับด้วย Draft นี้
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>;
|
||||
if (state === "error") return <div className="flex items-center gap-2"><Badge variant="destructive"><AlertCircleIcon data-icon="inline-start" />Autosave failed</Badge>{onSave ? <Button type="button" variant="outline" size="sm" onClick={onSave}>Save now</Button> : null}</div>;
|
||||
return <Badge variant="secondary"><CheckIcon data-icon="inline-start" />Saved</Badge>;
|
||||
</Alert>
|
||||
);
|
||||
if (state === "error")
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="destructive">
|
||||
<AlertCircleIcon data-icon="inline-start" />
|
||||
Autosave failed
|
||||
</Badge>
|
||||
{onSave ? (
|
||||
<Button type="button" variant="outline" size="sm" onClick={onSave}>
|
||||
Save now
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<CheckIcon data-icon="inline-start" />
|
||||
Saved
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,14 @@ import Spreadsheet, {
|
||||
type RowIndicatorProps,
|
||||
type SpreadsheetRef,
|
||||
} from "react-spreadsheet";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type KeyboardEvent,
|
||||
} from "react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
@@ -90,8 +97,8 @@ const sameAxisCollision: CollisionDetection = (args) => {
|
||||
const axis = String(args.active.id).split(":", 1)[0];
|
||||
return closestCenter({
|
||||
...args,
|
||||
droppableContainers: args.droppableContainers.filter(
|
||||
(container) => String(container.id).startsWith(`${axis}:`),
|
||||
droppableContainers: args.droppableContainers.filter((container) =>
|
||||
String(container.id).startsWith(`${axis}:`),
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -103,13 +110,23 @@ function movedIndex(current: number, from: number, to: number): number {
|
||||
return current;
|
||||
}
|
||||
|
||||
function LiteralDataViewer({ cell }: { cell?: { value?: unknown; backgroundColor?: string } }) {
|
||||
function LiteralDataViewer({
|
||||
cell,
|
||||
}: {
|
||||
cell?: { value?: unknown; backgroundColor?: string };
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="Spreadsheet__data-viewer Spreadsheet__data-viewer--preserve-breaks size-full"
|
||||
style={cell?.backgroundColor ? { backgroundColor: cell.backgroundColor } : undefined}
|
||||
style={
|
||||
cell?.backgroundColor
|
||||
? { backgroundColor: cell.backgroundColor }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<RichTextContent html={normalizeExtraRichText(String(cell?.value ?? ""))} />
|
||||
<RichTextContent
|
||||
html={normalizeExtraRichText(String(cell?.value ?? ""))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -153,14 +170,25 @@ function EditableColumnIndicator({
|
||||
selected,
|
||||
onRename,
|
||||
}: ColumnIndicatorProps & { id: string; onRename: (column: number) => void }) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
strategy: horizontalListSortingStrategy,
|
||||
});
|
||||
return (
|
||||
<th
|
||||
ref={setNodeRef}
|
||||
className={cn("Spreadsheet__header", selected && "Spreadsheet__header--selected", isDragging && "opacity-60")}
|
||||
className={cn(
|
||||
"Spreadsheet__header",
|
||||
selected && "Spreadsheet__header--selected",
|
||||
isDragging && "opacity-60",
|
||||
)}
|
||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||
>
|
||||
<Button
|
||||
@@ -187,15 +215,32 @@ function EditableColumnIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
function SortableRowIndicator({ id, row, label, selected, onSelect }: RowIndicatorProps & { id: string }) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
function SortableRowIndicator({
|
||||
id,
|
||||
row,
|
||||
label,
|
||||
selected,
|
||||
onSelect,
|
||||
}: RowIndicatorProps & { id: string }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
strategy: verticalListSortingStrategy,
|
||||
});
|
||||
return (
|
||||
<th
|
||||
ref={setNodeRef}
|
||||
className={cn("Spreadsheet__header", selected && "Spreadsheet__header--selected", isDragging && "opacity-60")}
|
||||
className={cn(
|
||||
"Spreadsheet__header",
|
||||
selected && "Spreadsheet__header--selected",
|
||||
isDragging && "opacity-60",
|
||||
)}
|
||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||
>
|
||||
<Button
|
||||
@@ -235,7 +280,9 @@ export function ExtraSheetEditor({
|
||||
}) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
const spreadsheetRef = useRef<SpreadsheetRef>(null);
|
||||
const [active, setActive] = useState<Point>({ row: 0, column: 0 });
|
||||
@@ -248,35 +295,50 @@ export function ExtraSheetEditor({
|
||||
const [originalCellDraft, setOriginalCellDraft] = useState("");
|
||||
const [stylePatch, setStylePatch] = useState<RichTextStylePatch>({});
|
||||
const [selectedCount, setSelectedCount] = useState(1);
|
||||
const [history, setHistory] = useState<SheetHistory>({ items: [value], index: 0, value });
|
||||
const [history, setHistory] = useState<SheetHistory>({
|
||||
items: [value],
|
||||
index: 0,
|
||||
value,
|
||||
});
|
||||
const selectedPointsRef = useRef<Point[]>([active]);
|
||||
const matrix = useMemo(() => extraSheetToMatrix(value), [value]);
|
||||
const activeColumn = value.columns[active.column];
|
||||
const activeRow = value.rows[active.row];
|
||||
const canUndo = history.index > 0;
|
||||
const canRedo = history.index < history.items.length - 1;
|
||||
const sortableIds = useMemo(() => [
|
||||
const sortableIds = useMemo(
|
||||
() => [
|
||||
...value.columns.map((column) => columnDragId(column.id)),
|
||||
...value.rows.map((row) => rowDragId(row.id)),
|
||||
], [value.columns, value.rows]);
|
||||
],
|
||||
[value.columns, value.rows],
|
||||
);
|
||||
|
||||
if (history.value !== value) {
|
||||
setHistory(history.items[history.index] === value
|
||||
setHistory(
|
||||
history.items[history.index] === value
|
||||
? { ...history, value }
|
||||
: { items: [value], index: 0, value });
|
||||
: { items: [value], index: 0, value },
|
||||
);
|
||||
}
|
||||
|
||||
const commitSheet = useCallback((next: ExtraSheet) => {
|
||||
const commitSheet = useCallback(
|
||||
(next: ExtraSheet) => {
|
||||
if (JSON.stringify(next) === JSON.stringify(value)) return;
|
||||
setHistory((current) => {
|
||||
const items = [...current.items.slice(0, current.index + 1), next]
|
||||
.slice(-SHEET_HISTORY_LIMIT);
|
||||
const items = [
|
||||
...current.items.slice(0, current.index + 1),
|
||||
next,
|
||||
].slice(-SHEET_HISTORY_LIMIT);
|
||||
return { items, index: items.length - 1, value: next };
|
||||
});
|
||||
onChange(next);
|
||||
}, [onChange, value]);
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
const travel = useCallback((direction: -1 | 1) => {
|
||||
const travel = useCallback(
|
||||
(direction: -1 | 1) => {
|
||||
const index = history.index + direction;
|
||||
const next = history.items[index];
|
||||
if (!next) return;
|
||||
@@ -289,8 +351,12 @@ export function ExtraSheetEditor({
|
||||
setActive(point);
|
||||
selectedPointsRef.current = [point];
|
||||
setSelectedCount(1);
|
||||
window.requestAnimationFrame(() => spreadsheetRef.current?.activate(point));
|
||||
}, [active, history, onChange]);
|
||||
window.requestAnimationFrame(() =>
|
||||
spreadsheetRef.current?.activate(point),
|
||||
);
|
||||
},
|
||||
[active, history, onChange],
|
||||
);
|
||||
|
||||
const activateOnly = useCallback((point: Point) => {
|
||||
setActive(point);
|
||||
@@ -299,45 +365,62 @@ export function ExtraSheetEditor({
|
||||
window.requestAnimationFrame(() => spreadsheetRef.current?.activate(point));
|
||||
}, []);
|
||||
|
||||
const moveRow = useCallback((from: number, to: number) => {
|
||||
const moveRow = useCallback(
|
||||
(from: number, to: number) => {
|
||||
const next = moveExtraSheetRow(value, from, to);
|
||||
if (next === value) return;
|
||||
const point = { ...active, row: movedIndex(active.row, from, to) };
|
||||
commitSheet(next);
|
||||
activateOnly(point);
|
||||
}, [active, activateOnly, commitSheet, value]);
|
||||
},
|
||||
[active, activateOnly, commitSheet, value],
|
||||
);
|
||||
|
||||
const moveColumn = useCallback((from: number, to: number) => {
|
||||
const moveColumn = useCallback(
|
||||
(from: number, to: number) => {
|
||||
const next = moveExtraSheetColumn(value, from, to);
|
||||
if (next === value) return;
|
||||
const point = { ...active, column: movedIndex(active.column, from, to) };
|
||||
commitSheet(next);
|
||||
activateOnly(point);
|
||||
}, [active, activateOnly, commitSheet, value]);
|
||||
},
|
||||
[active, activateOnly, commitSheet, value],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(({ active: dragged, over }: DragEndEvent) => {
|
||||
const handleDragEnd = useCallback(
|
||||
({ active: dragged, over }: DragEndEvent) => {
|
||||
if (!over || dragged.id === over.id) return;
|
||||
const draggedId = String(dragged.id);
|
||||
const overId = String(over.id);
|
||||
if (draggedId.startsWith("row:") && overId.startsWith("row:")) {
|
||||
const from = value.rows.findIndex((row) => rowDragId(row.id) === draggedId);
|
||||
const from = value.rows.findIndex(
|
||||
(row) => rowDragId(row.id) === draggedId,
|
||||
);
|
||||
const to = value.rows.findIndex((row) => rowDragId(row.id) === overId);
|
||||
if (from !== -1 && to !== -1) moveRow(from, to);
|
||||
}
|
||||
if (draggedId.startsWith("column:") && overId.startsWith("column:")) {
|
||||
const from = value.columns.findIndex((column) => columnDragId(column.id) === draggedId);
|
||||
const to = value.columns.findIndex((column) => columnDragId(column.id) === overId);
|
||||
const from = value.columns.findIndex(
|
||||
(column) => columnDragId(column.id) === draggedId,
|
||||
);
|
||||
const to = value.columns.findIndex(
|
||||
(column) => columnDragId(column.id) === overId,
|
||||
);
|
||||
if (from !== -1 && to !== -1) moveColumn(from, to);
|
||||
}
|
||||
}, [moveColumn, moveRow, value.columns, value.rows]);
|
||||
},
|
||||
[moveColumn, moveRow, value.columns, value.rows],
|
||||
);
|
||||
|
||||
const handleSheetKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.closest("input, textarea, [contenteditable='true']")) return;
|
||||
const key = event.key.toLowerCase();
|
||||
const undo = (event.metaKey || event.ctrlKey) && !event.shiftKey && key === "z";
|
||||
const redo = (event.metaKey || event.ctrlKey) && event.shiftKey && key === "z"
|
||||
|| event.ctrlKey && !event.shiftKey && key === "y";
|
||||
const undo =
|
||||
(event.metaKey || event.ctrlKey) && !event.shiftKey && key === "z";
|
||||
const redo =
|
||||
((event.metaKey || event.ctrlKey) && event.shiftKey && key === "z") ||
|
||||
(event.ctrlKey && !event.shiftKey && key === "y");
|
||||
if (undo && canUndo) {
|
||||
event.preventDefault();
|
||||
travel(-1);
|
||||
@@ -347,7 +430,10 @@ export function ExtraSheetEditor({
|
||||
}
|
||||
};
|
||||
const normalizedLabel = columnLabel.trim().toLocaleLowerCase();
|
||||
const labelInvalid = !normalizedLabel || value.columns.some((column, index) =>
|
||||
const labelInvalid =
|
||||
!normalizedLabel ||
|
||||
value.columns.some(
|
||||
(column, index) =>
|
||||
index !== (renaming ? active.column : -1) &&
|
||||
column.label.toLocaleLowerCase() === normalizedLabel,
|
||||
);
|
||||
@@ -356,58 +442,81 @@ export function ExtraSheetEditor({
|
||||
setColumnLabel(nextExtraSheetColumnLabel(value.columns));
|
||||
setLabelDialogOpen(true);
|
||||
};
|
||||
const openRenameColumn = useCallback((columnIndex = active.column) => {
|
||||
const openRenameColumn = useCallback(
|
||||
(columnIndex = active.column) => {
|
||||
const column = value.columns[columnIndex];
|
||||
if (!column) return;
|
||||
setActive((current) => ({ ...current, column: columnIndex }));
|
||||
setRenaming(true);
|
||||
setColumnLabel(column.label);
|
||||
setLabelDialogOpen(true);
|
||||
}, [active.column, value.columns]);
|
||||
},
|
||||
[active.column, value.columns],
|
||||
);
|
||||
const saveColumnLabel = () => {
|
||||
if (labelInvalid) return;
|
||||
if (renaming) {
|
||||
commitSheet({
|
||||
...value,
|
||||
columns: value.columns.map((column, index) =>
|
||||
index === active.column ? { ...column, label: columnLabel.trim() } : column,
|
||||
index === active.column
|
||||
? { ...column, label: columnLabel.trim() }
|
||||
: column,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
const column = value.columns.length;
|
||||
commitSheet(addExtraSheetColumn(
|
||||
commitSheet(
|
||||
addExtraSheetColumn(
|
||||
value,
|
||||
columnLabel.trim(),
|
||||
createId("extra-sheet-column"),
|
||||
));
|
||||
),
|
||||
);
|
||||
activateOnly({ ...active, column });
|
||||
}
|
||||
setLabelDialogOpen(false);
|
||||
};
|
||||
const openCellEditorAt = useCallback((point: Point, content?: string) => {
|
||||
const openCellEditorAt = useCallback(
|
||||
(point: Point, content?: string) => {
|
||||
setActive(point);
|
||||
const column = value.columns[point.column];
|
||||
const row = value.rows[point.row];
|
||||
setCellDraft(normalizeExtraRichText(
|
||||
content ?? (column && row ? row.cells[column.id] ?? "" : ""),
|
||||
));
|
||||
setOriginalCellDraft(normalizeExtraRichText(
|
||||
content ?? (column && row ? row.cells[column.id] ?? "" : ""),
|
||||
));
|
||||
setCellDraft(
|
||||
normalizeExtraRichText(
|
||||
content ?? (column && row ? (row.cells[column.id] ?? "") : ""),
|
||||
),
|
||||
);
|
||||
setOriginalCellDraft(
|
||||
normalizeExtraRichText(
|
||||
content ?? (column && row ? (row.cells[column.id] ?? "") : ""),
|
||||
),
|
||||
);
|
||||
setStylePatch({});
|
||||
setCellDialogOpen(true);
|
||||
}, [value.columns, value.rows]);
|
||||
},
|
||||
[value.columns, value.rows],
|
||||
);
|
||||
const openActiveCellEditor = () => openCellEditorAt(active);
|
||||
const columnIndicator = useCallback((props: ColumnIndicatorProps) => (
|
||||
const columnIndicator = useCallback(
|
||||
(props: ColumnIndicatorProps) => (
|
||||
<EditableColumnIndicator
|
||||
{...props}
|
||||
id={columnDragId(value.columns[props.column].id)}
|
||||
onRename={openRenameColumn}
|
||||
/>
|
||||
), [openRenameColumn, value.columns]);
|
||||
const rowIndicator = useCallback((props: RowIndicatorProps) => (
|
||||
<SortableRowIndicator {...props} id={rowDragId(value.rows[props.row].id)} />
|
||||
), [value.rows]);
|
||||
),
|
||||
[openRenameColumn, value.columns],
|
||||
);
|
||||
const rowIndicator = useCallback(
|
||||
(props: RowIndicatorProps) => (
|
||||
<SortableRowIndicator
|
||||
{...props}
|
||||
id={rowDragId(value.rows[props.row].id)}
|
||||
/>
|
||||
),
|
||||
[value.rows],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -416,7 +525,13 @@ export function ExtraSheetEditor({
|
||||
onKeyDown={handleSheetKeyDown}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<SimpleTooltip text={<>ย้อนกลับ <Kbd>Ctrl/⌘ Z</Kbd></>}>
|
||||
<SimpleTooltip
|
||||
text={
|
||||
<>
|
||||
ย้อนกลับ <Kbd>Ctrl/⌘ Z</Kbd>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -428,7 +543,13 @@ export function ExtraSheetEditor({
|
||||
<Undo2Icon />
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip text={<>ทำซ้ำ <Kbd>Ctrl/⌘ Shift Z</Kbd></>}>
|
||||
<SimpleTooltip
|
||||
text={
|
||||
<>
|
||||
ทำซ้ำ <Kbd>Ctrl/⌘ Shift Z</Kbd>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -453,16 +574,32 @@ export function ExtraSheetEditor({
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
เพิ่มแถว
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={openAddColumn}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={openAddColumn}
|
||||
>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
เพิ่มคอลัมน์
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={openActiveCellEditor}>
|
||||
<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}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
render={<span />}
|
||||
nativeButton={false}
|
||||
>
|
||||
<PaletteIcon data-icon="inline-start" />
|
||||
สีพื้นหลัง {selectedCount > 1 ? `${selectedCount} เซลล์` : "เซลล์"}
|
||||
</Button>
|
||||
@@ -470,13 +607,16 @@ export function ExtraSheetEditor({
|
||||
type="color"
|
||||
aria-label="เลือกสีพื้นหลังเซลล์"
|
||||
className="absolute inset-0 cursor-pointer opacity-0"
|
||||
value={activeRow?.backgrounds?.[activeColumn?.id ?? ""] ?? DEFAULT_CELL_BACKGROUND}
|
||||
value={
|
||||
activeRow?.backgrounds?.[activeColumn?.id ?? ""] ??
|
||||
DEFAULT_CELL_BACKGROUND
|
||||
}
|
||||
onChange={(event) => {
|
||||
commitSheet(updateExtraSheetCells(
|
||||
value,
|
||||
selectedPointsRef.current,
|
||||
{ backgroundColor: event.target.value },
|
||||
));
|
||||
commitSheet(
|
||||
updateExtraSheetCells(value, selectedPointsRef.current, {
|
||||
backgroundColor: event.target.value,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
@@ -484,11 +624,22 @@ export function ExtraSheetEditor({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { backgroundColor: null }))}
|
||||
onClick={() =>
|
||||
commitSheet(
|
||||
updateExtraSheetCells(value, selectedPointsRef.current, {
|
||||
backgroundColor: null,
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
ล้างสีพื้นหลัง
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => setRemoveOpen(true)}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setRemoveOpen(true)}
|
||||
>
|
||||
<Trash2Icon data-icon="inline-start" />
|
||||
ลบตาราง
|
||||
</Button>
|
||||
@@ -497,12 +648,20 @@ export function ExtraSheetEditor({
|
||||
<div
|
||||
className="overflow-x-auto rounded-lg border"
|
||||
onClickCapture={(event) => {
|
||||
if ((event.target as HTMLElement).closest(".Spreadsheet__active-cell--view")) {
|
||||
if (
|
||||
(event.target as HTMLElement).closest(
|
||||
".Spreadsheet__active-cell--view",
|
||||
)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDoubleClickCapture={(event) => {
|
||||
if ((event.target as HTMLElement).closest(".Spreadsheet__cell, .Spreadsheet__active-cell")) {
|
||||
if (
|
||||
(event.target as HTMLElement).closest(
|
||||
".Spreadsheet__cell, .Spreadsheet__active-cell",
|
||||
)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openCellEditorAt(active);
|
||||
@@ -536,11 +695,11 @@ export function ExtraSheetEditor({
|
||||
setSelectedCount(range.size());
|
||||
}
|
||||
}}
|
||||
onChange={(next) => commitSheet(matrixToExtraSheet(
|
||||
value,
|
||||
next as ExtraSheetMatrix,
|
||||
createId,
|
||||
))}
|
||||
onChange={(next) =>
|
||||
commitSheet(
|
||||
matrixToExtraSheet(value, next as ExtraSheetMatrix, createId),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
@@ -561,23 +720,48 @@ export function ExtraSheetEditor({
|
||||
onChange={setCellDraft}
|
||||
showBubbleMenu={false}
|
||||
formatWholeContent={selectedCount > 1}
|
||||
onFormatChange={(patch) => setStylePatch((current) => ({ ...current, ...patch }))}
|
||||
onFormatChange={(patch) =>
|
||||
setStylePatch((current) => ({ ...current, ...patch }))
|
||||
}
|
||||
/>
|
||||
<DialogFooter showCloseButton>
|
||||
<Button type="button" onClick={() => {
|
||||
const contentUnchanged = richTextContentSignature(cellDraft)
|
||||
=== richTextContentSignature(originalCellDraft);
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const contentUnchanged =
|
||||
richTextContentSignature(cellDraft) ===
|
||||
richTextContentSignature(originalCellDraft);
|
||||
if (selectedCount > 1 && contentUnchanged) {
|
||||
if (Object.keys(stylePatch).length > 0) {
|
||||
commitSheet(styleExtraSheetCells(value, selectedPointsRef.current, stylePatch));
|
||||
commitSheet(
|
||||
styleExtraSheetCells(
|
||||
value,
|
||||
selectedPointsRef.current,
|
||||
stylePatch,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
commitSheet(selectedCount > 1
|
||||
? updateExtraSheetCells(value, selectedPointsRef.current, { value: cellDraft })
|
||||
: updateExtraSheetCell(value, active.row, active.column, cellDraft));
|
||||
commitSheet(
|
||||
selectedCount > 1
|
||||
? updateExtraSheetCells(
|
||||
value,
|
||||
selectedPointsRef.current,
|
||||
{ value: cellDraft },
|
||||
)
|
||||
: updateExtraSheetCell(
|
||||
value,
|
||||
active.row,
|
||||
active.column,
|
||||
cellDraft,
|
||||
),
|
||||
);
|
||||
}
|
||||
setCellDialogOpen(false);
|
||||
}}>บันทึกเซลล์</Button>
|
||||
}}
|
||||
>
|
||||
บันทึกเซลล์
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -585,12 +769,18 @@ export function ExtraSheetEditor({
|
||||
<Dialog open={labelDialogOpen} onOpenChange={setLabelDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{renaming ? "แก้ไขชื่อคอลัมน์" : "เพิ่มคอลัมน์"}</DialogTitle>
|
||||
<DialogDescription>ตั้งชื่อที่ใช้อธิบายข้อมูลในคอลัมน์นี้</DialogDescription>
|
||||
<DialogTitle>
|
||||
{renaming ? "แก้ไขชื่อคอลัมน์" : "เพิ่มคอลัมน์"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
ตั้งชื่อที่ใช้อธิบายข้อมูลในคอลัมน์นี้
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={labelInvalid || undefined}>
|
||||
<FieldLabel htmlFor="extra-sheet-column-label">ชื่อคอลัมน์</FieldLabel>
|
||||
<FieldLabel htmlFor="extra-sheet-column-label">
|
||||
ชื่อคอลัมน์
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="extra-sheet-column-label"
|
||||
value={columnLabel}
|
||||
@@ -604,7 +794,11 @@ export function ExtraSheetEditor({
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter showCloseButton>
|
||||
<Button type="button" disabled={labelInvalid} onClick={saveColumnLabel}>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={labelInvalid}
|
||||
onClick={saveColumnLabel}
|
||||
>
|
||||
{renaming ? "บันทึกชื่อ" : "เพิ่มคอลัมน์"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -3890,7 +3890,7 @@ function TeamEditor({
|
||||
<DialogContent className="max-h-[90svh] grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Multiple Apply</DialogTitle>
|
||||
<DialogDescription className="break-words">คัดลอกค่าจาก {teams.find((team) => team.clientId === sourceTeamId)?.title || "Source Team"} ไปยัง {selectedTeamIds.size} Team ที่เลือก: {[...selectedTeamIds].map((id) => teams.find((team) => team.clientId === id)?.title || "Untitled Team").join(", ")}</DialogDescription>
|
||||
<DialogDescription className="wrap-break-word">คัดลอกค่าจาก {teams.find((team) => team.clientId === sourceTeamId)?.title || "Source Team"} ไปยัง {selectedTeamIds.size} Team ที่เลือก: {[...selectedTeamIds].map((id) => teams.find((team) => team.clientId === id)?.title || "Untitled Team").join(", ")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex min-h-0 flex-col gap-5 overflow-y-auto pr-1">
|
||||
{teams.find((team) => team.clientId === sourceTeamId)?.members.map((member, memberIndex) => {
|
||||
|
||||
@@ -167,7 +167,7 @@ export function LoginCard({ turnstileSiteKey }: LoginCardProps) {
|
||||
</Field>
|
||||
{turnstileEnabled ? (
|
||||
<Field>
|
||||
<div ref={turnstileContainerRef} className="min-h-[65px]" />
|
||||
<div ref={turnstileContainerRef} className="min-h-16.25" />
|
||||
<Script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
|
||||
strategy="afterInteractive"
|
||||
|
||||
@@ -279,7 +279,7 @@ function OverviewPoster({
|
||||
<DndContext sensors={posterSensors} onDragEnd={handlePosterDragEnd}>
|
||||
<div
|
||||
ref={posterRef}
|
||||
className="relative isolate aspect-square size-full overflow-hidden bg-black text-white [container-type:inline-size]"
|
||||
className="relative isolate aspect-square size-full overflow-hidden bg-black text-white @container"
|
||||
style={{ fontFamily: '"Genshin Overview", sans-serif' }}
|
||||
>
|
||||
{composition.backgroundUrl ? (
|
||||
@@ -369,7 +369,7 @@ function OverviewPoster({
|
||||
offset={composition.fadeOffset}
|
||||
interactive={interactive && activeLayer === "fade"}
|
||||
className={cn(
|
||||
"absolute inset-x-0 -top-[3cqw] bottom-0",
|
||||
"absolute inset-x-0 top-[-3cqw] bottom-0",
|
||||
activeLayer === "fade"
|
||||
? "touch-none cursor-grab active:cursor-grabbing"
|
||||
: "pointer-events-none",
|
||||
@@ -1497,7 +1497,7 @@ export function OverviewImageGenerator({
|
||||
<div
|
||||
ref={captureRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed top-0 -left-[10000px] order-5 size-[1000px]"
|
||||
className="pointer-events-none fixed top-0 left-[-10000px] order-5 size-250"
|
||||
>
|
||||
<OverviewPoster
|
||||
composition={composition}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ConstellationImage({
|
||||
const footer = group.note;
|
||||
|
||||
return (
|
||||
<div className="relative isolate size-[1500px] overflow-hidden bg-[#070b20] font-heading text-white">
|
||||
<div className="relative isolate size-375 overflow-hidden bg-[#070b20] font-heading text-white">
|
||||
<GuideExportBranding backgroundUrl={background} variant="constellation" />
|
||||
|
||||
<div className="absolute inset-x-20 top-7 z-10 flex items-start justify-center gap-5">
|
||||
@@ -45,14 +45,14 @@ export function ConstellationImage({
|
||||
>
|
||||
{`${guide.character.name}’s Constellations`}
|
||||
</h2>
|
||||
<p className="mt-3 line-clamp-2 max-w-[1100px] whitespace-pre-line text-[42px] leading-tight font-bold text-[#f1badd]">
|
||||
<p className="mt-3 line-clamp-2 max-w-275 whitespace-pre-line text-[42px] leading-tight font-bold text-[#f1badd]">
|
||||
<ExportText th="ดาเมจทั้งทีม" en="Team Damage" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-[160px] bottom-[115px] left-[154px] z-20 w-1 bg-black/90" />
|
||||
<div className="absolute inset-x-14 top-[205px] bottom-[145px] z-20 flex flex-col justify-between">
|
||||
<div className="absolute top-40 bottom-28.75 left-38.5 z-20 w-1 bg-black/90" />
|
||||
<div className="absolute inset-x-14 top-51.25 bottom-36.25 z-20 flex flex-col justify-between">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row.level}
|
||||
@@ -61,7 +61,7 @@ export function ConstellationImage({
|
||||
<div className="text-right text-[58px] leading-none font-bold drop-shadow-[0_5px_4px_rgba(0,0,0,0.9)]">
|
||||
C{row.level}
|
||||
</div>
|
||||
<div className="flex h-[100px] items-center">
|
||||
<div className="flex h-25 items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full shrink-0 rounded-r-xl",
|
||||
@@ -83,7 +83,7 @@ export function ConstellationImage({
|
||||
</div>
|
||||
|
||||
{footer ? (
|
||||
<p data-export-note className="absolute bottom-10 left-[154px] z-30 max-w-[1000px] truncate text-[28px] font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
<p data-export-note className="absolute bottom-10 left-38.5 z-30 max-w-275 truncate text-[28px] font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
{footer}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function DirectorySkeleton() {
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }, (_, index) => (
|
||||
<Card key={index} className="gap-0 py-0">
|
||||
<Skeleton className="aspect-[4/3] rounded-b-none" />
|
||||
<Skeleton className="aspect-4/3 rounded-b-none" />
|
||||
<CardHeader className="pt-4">
|
||||
<Skeleton className="h-5 w-2/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
|
||||
@@ -28,8 +28,8 @@ export function GuideExportBranding({
|
||||
loading="eager"
|
||||
className={
|
||||
constellation
|
||||
? "guide-export-watermark absolute right-4 bottom-0 z-30 h-auto w-[280px] object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
|
||||
: "guide-export-watermark absolute top-4 right-4 z-30 h-auto w-[180px] object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
|
||||
? "guide-export-watermark absolute right-4 bottom-0 z-30 h-auto w-70 object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
|
||||
: "guide-export-watermark absolute top-4 right-4 z-30 h-auto w-45 object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -254,7 +254,7 @@ export function GuideImageExport({
|
||||
? createPortal(
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed top-0 -left-[10000px]"
|
||||
className="pointer-events-none fixed top-0 left-[-10000px]"
|
||||
>
|
||||
<div
|
||||
ref={captureRef}
|
||||
|
||||
@@ -120,7 +120,7 @@ export function MaterialPoster({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("@container relative aspect-[4/5] w-full select-none overflow-hidden bg-black text-white", interactive && characterArtUrl && "cursor-move", className)}
|
||||
className={cn("@container relative aspect-4/5 w-full select-none overflow-hidden bg-black text-white", interactive && characterArtUrl && "cursor-move", className)}
|
||||
style={{ backgroundImage: background }}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
@@ -178,7 +178,7 @@ export function MaterialPoster({
|
||||
className="pointer-events-none object-cover opacity-50"
|
||||
priority={!interactive}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/80" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-black/10 via-transparent to-black/80" />
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-[1.35cqw]"
|
||||
style={{ backgroundImage: accentGradient(...topColors) }}
|
||||
|
||||
@@ -14,14 +14,14 @@ export function SiteFooter() {
|
||||
guide.sudloh.com is not affiliated with HoYoverse. Game content and
|
||||
materials are trademarks and copyrights of HoYoverse. With sincere
|
||||
thanks to{" "}
|
||||
<a
|
||||
<Link
|
||||
href="https://lunaris.moe/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
Lunaris
|
||||
</a>{" "}
|
||||
</Link>{" "}
|
||||
for generously providing the data that helps make these guides
|
||||
possible.
|
||||
</p>
|
||||
|
||||
@@ -1359,7 +1359,7 @@ function ExtraSectionContent({
|
||||
<CardContent
|
||||
className={cn(
|
||||
"pb-5",
|
||||
exporting && "[&_[data-slot=table-container]]:overflow-visible",
|
||||
exporting && "**:data-[slot=table-container]:overflow-visible",
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
|
||||
@@ -23,7 +23,7 @@ export function VisitorPresence({ guideId }: { guideId?: string }) {
|
||||
className=" tabular-nums"
|
||||
title="ผู้เข้าชม Guide นี้ที่กำลังออนไลน์"
|
||||
>
|
||||
Guide นี้ {counts?.guide ?? "-"}
|
||||
ไกด์นี้ {counts?.guide ?? "-"}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export function getDb(): Database {
|
||||
|
||||
if (!databaseUrl) {
|
||||
throw new Error(
|
||||
"DATABASE_URL is required for database-backed routes. Copy .env.example to .env and configure it.",
|
||||
"DATABASE_URL is required for database-backed routes.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user