diff --git a/Plan.md b/Plan.md index 567a373..9a54203 100644 --- a/Plan.md +++ b/Plan.md @@ -27,8 +27,8 @@ Remove Guide moves content to a recoverable Trash before permanent deletion. - Constellations: C1–C6 description and cumulative performance against C0=100%. - Team Calc: multiple four-character teams, raw damage, calculated member shares, rotation duration, total DPS, and combo text. -- Extra: multiple sections of image-first rows with heading, short body, and an - optional percentage. +- Extra: multiple sections of image-first rows with heading, short body, an + optional percentage, and an optional editable mini spreadsheet. ## Catalog and media diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx index 22dfffd..8d8f841 100644 --- a/app/admin/[character]/[page]/page.tsx +++ b/app/admin/[character]/[page]/page.tsx @@ -8,19 +8,134 @@ import { GuideSectionNav } from "@/components/admin/guide-section-nav"; import { AdminHeader } from "@/components/admin/admin-header"; import { Button } from "@/components/ui/button"; import { getAdminSession } from "@/lib/auth/server"; -import { getActiveCatalog, getAdminGuideContent, getGuideByCharacterKey } from "@/lib/guides/queries"; +import { + getActiveCatalog, + getAdminGuideContent, + getGuideByCharacterKey, +} from "@/lib/guides/queries"; -const sections = ["weapon", "artifact", "constellations", "team", "extra"] as const; +const sections = [ + "weapon", + "artifact", + "constellations", + "team", + "extra", +] as const; -export default async function StructuredEditorPage({ params }: { params: Promise<{ character: string; page: string }> }) { +export default async function StructuredEditorPage({ + params, +}: { + params: Promise<{ character: string; page: string }>; +}) { await connection(); if (!(await getAdminSession())) redirect("/admin/login"); const { character, page } = await params; if (!sections.includes(page as (typeof sections)[number])) notFound(); const guide = await getGuideByCharacterKey(character); if (!guide) notFound(); - const [catalog, content] = await Promise.all([getActiveCatalog(), getAdminGuideContent(guide.slug)]); + const [catalog, content] = await Promise.all([ + getActiveCatalog(), + getAdminGuideContent(guide.slug), + ]); if (!catalog || !content) notFound(); - const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey); - return

{guide.name}

{guide.sections.find((section) => section.slug === page)?.title ?? page}

section.slug === page)?.note ?? null, weaponGroups: content.weaponGroups, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent, note: row.note })) })), artifactProfile: content.artifactProfile, constellationGroups: content.constellationGroups.map((group) => ({ id: group.id, note: group.note, rows: group.rows })), teams: content.teams.map((team) => ({ id: team.id, title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, weaponKey: member.weaponKey, artifactKey: member.artifactKey, secondArtifactKey: member.secondArtifactKey, constellation: member.constellation, refinement: member.refinement, damage: member.damage })) })), extras: content.extras.map((section) => ({ id: section.id, title: section.title, rows: section.rows.map((row) => ({ id: row.id, mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent, backgroundColor: row.backgroundColor, textColor: row.textColor, catalogItems: row.catalogItems.map(({ kind, key }) => ({ kind, key })), highlights: row.highlights, imageUrl: row.imageUrl, imageWidth: row.imageWidth, imageHeight: row.imageHeight })) })) }} />
; + const guideCharacter = catalog.characters.find( + (item) => item.key === guide.characterKey, + ); + return ( +
+ +
+
+ +
+

{guide.name}

+

+ {guide.sections.find((section) => section.slug === page)?.title ?? + page} +

+
+
+ + section.slug === page)?.note ?? + null, + weaponGroups: content.weaponGroups, + artifactGroups: content.artifactGroups.map((group) => ({ + id: group.id, + name: group.name, + rows: group.rows.map((row) => ({ + firstArtifactKey: row.firstArtifactKey, + secondArtifactKey: row.secondArtifactKey, + percent: row.percent, + note: row.note, + })), + })), + artifactProfile: content.artifactProfile, + constellationGroups: content.constellationGroups.map((group) => ({ + id: group.id, + note: group.note, + rows: group.rows, + })), + teams: content.teams.map((team) => ({ + id: team.id, + title: team.title, + rotationSeconds: team.rotationSeconds, + combo: team.combo, + members: team.members.map((member) => ({ + characterKey: member.characterKey, + weaponKey: member.weaponKey, + artifactKey: member.artifactKey, + secondArtifactKey: member.secondArtifactKey, + constellation: member.constellation, + refinement: member.refinement, + damage: member.damage, + })), + })), + extras: content.extras.map((section) => ({ + id: section.id, + title: section.title, + rows: section.rows.map((row) => ({ + id: row.id, + mediaId: row.mediaId, + heading: row.heading, + body: row.body, + percent: row.percent, + backgroundColor: row.backgroundColor, + textColor: row.textColor, + sheet: row.sheet, + catalogItems: row.catalogItems.map(({ kind, key }) => ({ + kind, + key, + })), + highlights: row.highlights, + imageUrl: row.imageUrl, + imageWidth: row.imageWidth, + imageHeight: row.imageHeight, + })), + })), + }} + /> +
+
+ ); } diff --git a/app/globals.css b/app/globals.css index e9fe78f..ffed109 100644 --- a/app/globals.css +++ b/app/globals.css @@ -121,6 +121,22 @@ } } +.extra-sheet-grid { + --background-color: var(--background); + --text-color: var(--foreground); + --readonly-text-color: var(--muted-foreground); + --outline-color: var(--ring); + --outline-background-color: color-mix(in oklch, var(--ring) 20%, transparent); + --border-color: var(--border); + --header-background-color: var(--muted); + --elevation: 0 4px 12px color-mix(in oklch, var(--background) 70%, transparent); +} + +.extra-sheet-grid .Spreadsheet__header--selected { + background: var(--accent); + color: var(--accent-foreground); +} + @media (pointer: coarse) { [data-slot="button"], [data-slot="toggle-group-item"], diff --git a/bun.lock b/bun.lock index 9a530a3..118ba50 100644 --- a/bun.lock +++ b/bun.lock @@ -34,7 +34,9 @@ "react": "19.2.8", "react-dom": "19.2.8", "react-resizable-panels": "^4.12.3", + "react-spreadsheet": "0.10.1", "recharts": "3.8.0", + "scheduler": "^0.27.0", "server-only": "^0.0.1", "shadcn": "^4.19.0", "sharp": "^0.35.4", @@ -740,10 +742,14 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "bahttext": ["bahttext@1.1.0", "", {}, "sha512-3g7DbmCwYhLnnN5hJFB+Mtbst4q2gfXpAdqOMK6tz6FmgtlrhX1n1uhuzkIhEr+WcnFR6eRvebIE5msGuEtA7Q=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + "bessel": ["bessel@1.0.2", "", {}, "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw=="], + "better-auth": ["better-auth@1.7.2", "", { "dependencies": { "@better-auth/core": "1.7.2", "@better-auth/drizzle-adapter": "1.7.2", "@better-auth/kysely-adapter": "1.7.2", "@better-auth/memory-adapter": "1.7.2", "@better-auth/mongo-adapter": "1.7.2", "@better-auth/prisma-adapter": "1.7.2", "@better-auth/telemetry": "1.7.2", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A=="], "better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], @@ -778,8 +784,12 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chevrotain": ["chevrotain@7.1.2", "", { "dependencies": { "regexp-to-ast": "0.5.0" } }, "sha512-9bQsXVQ7UAvzMs7iUBBJ9Yv//exOy7bIR3PByOEk4M64vIE/LsiOiX7VIkMF/vEMlrSStwsaE884Bp9CpjtC5g=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -1014,6 +1024,8 @@ "fast-equals": ["fast-equals@5.4.2", "", {}, "sha512-Ywe6jodPTWOTL9/k0bV7gdfP8twKL5Y8I8CZ933fAY5gBekICZSUQTbyH6ut2NZCNyB05mSUwAuEqdEIaOOlDQ=="], + "fast-formula-parser": ["fast-formula-parser@1.0.19", "", { "dependencies": { "bahttext": "^1.1.0", "bessel": "^1.0.2", "chevrotain": "^7.0.1", "jstat": "^1.9.3" } }, "sha512-8Roq7V1XjuYj3cWZ9tILyqBHGjS7NkejRaHxSWePu0qjIcvimZwe6WfeA9che03dgWrykZqrw691qg3lwiIkMg=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -1250,6 +1262,8 @@ "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "jstat": ["jstat@1.9.6", "", {}, "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -1502,6 +1516,8 @@ "react-resizable-panels": ["react-resizable-panels@4.12.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA=="], + "react-spreadsheet": ["react-spreadsheet@0.10.1", "", { "dependencies": { "array.prototype.flatmap": "^1.2.5", "classnames": "^2.3.1", "fast-formula-parser": "^1.0.19", "use-context-selector": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0", "scheduler": ">=0.19.0" } }, "sha512-w+KqCCA5H81FQ7dEBMOz1cZm9I8PSx9yNs+XwjYMjBfCDsOBBljiau93US8GQTtMbXtsExX8k3qD7hZLTp3UHw=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="], @@ -1516,6 +1532,8 @@ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -1722,6 +1740,8 @@ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + "use-context-selector": ["use-context-selector@2.0.0", "", { "peerDependencies": { "react": ">=18.0.0", "scheduler": ">=0.19.0" } }, "sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g=="], + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], diff --git a/components/admin/extra-sheet-editor.tsx b/components/admin/extra-sheet-editor.tsx new file mode 100644 index 0000000..3c5ea5b --- /dev/null +++ b/components/admin/extra-sheet-editor.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { + ArrowDownIcon, + ArrowLeftIcon, + ArrowRightIcon, + ArrowUpIcon, + Columns3Icon, + PlusIcon, + Rows3Icon, + Trash2Icon, +} from "lucide-react"; +import Spreadsheet, { type Point } from "react-spreadsheet"; +import { useMemo, useState } from "react"; + +import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { + addExtraSheetColumn, + addExtraSheetRow, + deleteExtraSheetColumn, + deleteExtraSheetRow, + extraSheetToMatrix, + matrixToExtraSheet, + moveExtraSheetColumn, + moveExtraSheetRow, + nextExtraSheetColumnLabel, + type ExtraSheet, + type ExtraSheetMatrix, +} from "@/lib/guides/extra-sheet"; + +type DeleteTarget = "column" | "row" | null; + +function LiteralDataViewer({ cell }: { cell?: { value?: unknown } }) { + return ( + + {String(cell?.value ?? "")} + + ); +} + +export function ExtraSheetEditor({ + value, + createId, + onChange, + onRemove, +}: { + value: ExtraSheet; + createId: (prefix: string) => string; + onChange: (value: ExtraSheet) => void; + onRemove: () => void; +}) { + const [active, setActive] = useState({ row: 0, column: 0 }); + const [labelDialogOpen, setLabelDialogOpen] = useState(false); + const [renaming, setRenaming] = useState(false); + const [columnLabel, setColumnLabel] = useState(""); + const [deleteTarget, setDeleteTarget] = useState(null); + const [removeOpen, setRemoveOpen] = useState(false); + const matrix = useMemo(() => extraSheetToMatrix(value), [value]); + const activeColumn = value.columns[active.column]; + const activeRow = value.rows[active.row]; + const normalizedLabel = columnLabel.trim().toLocaleLowerCase(); + const labelInvalid = !normalizedLabel || value.columns.some((column, index) => + index !== (renaming ? active.column : -1) && + column.label.toLocaleLowerCase() === normalizedLabel, + ); + + const openAddColumn = () => { + setRenaming(false); + setColumnLabel(nextExtraSheetColumnLabel(value.columns)); + setLabelDialogOpen(true); + }; + const openRenameColumn = () => { + if (!activeColumn) return; + setRenaming(true); + setColumnLabel(activeColumn.label); + setLabelDialogOpen(true); + }; + const saveColumnLabel = () => { + if (labelInvalid) return; + if (renaming) { + onChange({ + ...value, + columns: value.columns.map((column, index) => + index === active.column ? { ...column, label: columnLabel.trim() } : column, + ), + }); + } else { + const column = value.columns.length; + onChange(addExtraSheetColumn( + value, + columnLabel.trim(), + createId("extra-sheet-column"), + )); + setActive((current) => ({ ...current, column })); + } + setLabelDialogOpen(false); + }; + + return ( +
+
+ + + + } + > + + แถว {active.row + 1} + + + + จัดการแถว {active.row + 1} + { + onChange(moveExtraSheetRow(value, active.row, active.row - 1)); + setActive((current) => ({ ...current, row: current.row - 1 })); + }} + > + ย้ายขึ้น + + { + onChange(moveExtraSheetRow(value, active.row, active.row + 1)); + setActive((current) => ({ ...current, row: current.row + 1 })); + }} + > + ย้ายลง + + + + + setDeleteTarget("row")} + > + ลบแถว + + + + + + } + > + + {activeColumn?.label ?? "คอลัมน์"} + + + + {activeColumn?.label} + แก้ไขชื่อ + { + onChange(moveExtraSheetColumn(value, active.column, active.column - 1)); + setActive((current) => ({ ...current, column: current.column - 1 })); + }} + > + ย้ายไปทางซ้าย + + { + onChange(moveExtraSheetColumn(value, active.column, active.column + 1)); + setActive((current) => ({ ...current, column: current.column + 1 })); + }} + > + ย้ายไปทางขวา + + + + + setDeleteTarget("column")} + > + ลบคอลัมน์ + + + + + +
+ +
+ column.label)} + DataViewer={LiteralDataViewer} + onActivate={setActive} + onChange={(next) => onChange(matrixToExtraSheet( + value, + next as ExtraSheetMatrix, + createId, + ))} + /> +
+ + + + + {renaming ? "แก้ไขชื่อคอลัมน์" : "เพิ่มคอลัมน์"} + ตั้งชื่อที่ใช้อธิบายข้อมูลในคอลัมน์นี้ + + + + ชื่อคอลัมน์ + setColumnLabel(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") saveColumnLabel(); + }} + /> + + + + + + + + + { if (!open) setDeleteTarget(null); }} + title={deleteTarget === "column" ? `ลบ ${activeColumn?.label}?` : `ลบแถว ${active.row + 1}?`} + description="ข้อมูลในรายการนี้จะถูกลบออกจากตาราง" + showSuccessToast={false} + onConfirm={() => { + if (deleteTarget === "column") { + onChange(deleteExtraSheetColumn(value, active.column)); + setActive((current) => ({ ...current, column: Math.max(0, current.column - 1) })); + } else if (deleteTarget === "row") { + onChange(deleteExtraSheetRow(value, active.row)); + setActive((current) => ({ ...current, row: Math.max(0, current.row - 1) })); + } + setDeleteTarget(null); + }} + /> + { + onRemove(); + setRemoveOpen(false); + }} + /> +
+ ); +} diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx index 84209a3..cee1b1a 100644 --- a/components/admin/guide-section-editor.tsx +++ b/components/admin/guide-section-editor.tsx @@ -164,6 +164,7 @@ import { } from "@/lib/guides/percent-input"; import { formatGuideValidationIssues } from "@/lib/guides/validation-messages"; import { normalizeExtraRichText } from "@/lib/rich-text"; +import { createExtraSheet, type ExtraSheet } from "@/lib/guides/extra-sheet"; import { cn } from "@/lib/utils"; type CatalogItem = { @@ -276,6 +277,13 @@ const RichTextEditor = dynamic( loading: () => , }, ); +const ExtraSheetEditor = dynamic( + () => import("@/components/admin/extra-sheet-editor").then((module) => module.ExtraSheetEditor), + { + ssr: false, + loading: () => , + }, +); function createClientId(prefix: string): string { const browserCrypto = globalThis.crypto; @@ -329,6 +337,7 @@ export interface GuideSectionEditorProps { body: string; percent: string | null; backgroundColor: string | null; + sheet: ExtraSheet | null; catalogItems: ExtraCatalogItem[]; highlights: Array<{ text: string; color: string }>; imageUrl: string | null; @@ -3508,6 +3517,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB ? null : Number(row.percent), backgroundColor: row.backgroundColor, + sheet: row.sheet, catalogItems: row.catalogItems, highlights: [], })), @@ -3540,6 +3550,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB id: row.id || createClientId("extra-row-record"), body: normalizeExtraRichText(row.body, row.highlights ?? []), backgroundColor: row.backgroundColor ?? null, + sheet: row.sheet ?? null, catalogItems: row.catalogItems ?? [], highlights: [], clientId: row.clientId || createClientId("extra-row"), @@ -3881,6 +3892,71 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB } /> + + ตารางข้อมูล (optional) + {row.sheet ? ( + + setSections( + sections.map((item, index) => + index === sectionIndex + ? { + ...item, + rows: item.rows.map((candidate, inner) => + inner === rowIndex + ? { ...candidate, sheet } + : candidate, + ), + } + : item, + ), + ) + } + onRemove={() => + setSections( + sections.map((item, index) => + index === sectionIndex + ? { + ...item, + rows: item.rows.map((candidate, inner) => + inner === rowIndex + ? { ...candidate, sheet: null } + : candidate, + ), + } + : item, + ), + ) + } + /> + ) : ( + + )} + Percent (optional) - -
+
) : null} -
); } diff --git a/components/public/guide-export-branding.tsx b/components/public/guide-export-branding.tsx new file mode 100644 index 0000000..08b0ced --- /dev/null +++ b/components/public/guide-export-branding.tsx @@ -0,0 +1,37 @@ +import Image from "next/image"; + +export function GuideExportBranding({ + backgroundUrl, + variant = "standard", +}: { + backgroundUrl: string; + variant?: "standard" | "constellation"; +}) { + const constellation = variant === "constellation"; + + return ( + <> + +
+ + + ); +} diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 096b749..c3d376c 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -15,11 +15,20 @@ import { CardTitle, } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { SiteFooter } from "@/components/public/site-footer"; import { SiteHeader } from "@/components/public/site-header"; import { ScrollableNav } from "@/components/public/scrollable-nav"; import { TeamSearch } from "@/components/public/team-search"; import { GuideImageExport } from "@/components/public/guide-image-export"; +import { GuideExportBranding } from "@/components/public/guide-export-branding"; import { ConstellationImage } from "@/components/public/constellation-image"; import { RichTextContent } from "@/components/rich-text-content"; import { LiveRefresh } from "@/components/live-refresh"; @@ -358,25 +367,9 @@ function WeaponSectionContent({ )} > {exporting ? ( - <> - -
- - + ) : null}
{exporting ? ( - <> - -
- - + ) : null}
{exporting ? ( - <> - -
- - + ) : null}

คู่มือเสริม

@@ -1243,7 +1204,10 @@ function ExtraSectionContent({ {extra.rows.map((row) => ( {row.imageUrl ? ( @@ -1297,6 +1261,40 @@ function ExtraSectionContent({ {row.percent ? {metric(row.percent)} : null} + {row.sheet ? ( + + + + + {row.sheet.columns.map((column) => ( + + {column.label} + + ))} + + + + {row.sheet.rows.map((sheetRow) => ( + + {row.sheet!.columns.map((column) => ( + + {sheetRow.cells[column.id] ?? ""} + + ))} + + ))} + +
+
+ ) : null}
))}
diff --git a/db/schema.ts b/db/schema.ts index faccc84..3c133af 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -17,6 +17,7 @@ import { } from "drizzle-orm/pg-core"; import type { CatalogConstellation, CatalogTalent } from "@/lib/catalog/lunaris"; +import type { ExtraSheet } from "@/lib/guides/extra-sheet"; const authSchema = pgSchema("auth"); const mediaSchema = pgSchema("media"); @@ -665,6 +666,7 @@ export const extraSectionRows = guidesSchema.table( .$type>() .default([]) .notNull(), + sheet: jsonb("sheet").$type(), sortOrder: integer("sort_order").default(0).notNull(), }, (table) => [index("extra_section_row_order_idx").on(table.sectionId, table.sortOrder)], diff --git a/drizzle/0029_rare_multiple_man.sql b/drizzle/0029_rare_multiple_man.sql new file mode 100644 index 0000000..20e4cd1 --- /dev/null +++ b/drizzle/0029_rare_multiple_man.sql @@ -0,0 +1 @@ +ALTER TABLE "guides"."extra_section_row" ADD COLUMN "sheet" jsonb; \ No newline at end of file diff --git a/drizzle/meta/0029_snapshot.json b/drizzle/meta/0029_snapshot.json new file mode 100644 index 0000000..2ffa604 --- /dev/null +++ b/drizzle/meta/0029_snapshot.json @@ -0,0 +1,3456 @@ +{ + "id": "46d8b8cd-5cc2-435e-82ae-e891b24e19d1", + "prevId": "bf1a7c7a-d041-412f-ac75-8751b0630818", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_issuer_unique": { + "name": "account_issuer_unique", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_group": { + "name": "artifact_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_group_order_idx": { + "name": "artifact_group_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_group_guide_id_guide_id_fk": { + "name": "artifact_group_guide_id_guide_id_fk", + "tableFrom": "artifact_group", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_profile": { + "name": "artifact_profile", + "schema": "guides", + "columns": { + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "sands": { + "name": "sands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "goblet": { + "name": "goblet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "circlet": { + "name": "circlet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "substats": { + "name": "substats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "artifact_profile_guide_id_guide_id_fk": { + "name": "artifact_profile_guide_id_guide_id_fk", + "tableFrom": "artifact_profile", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_recommendation": { + "name": "artifact_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "first_artifact_key": { + "name": "first_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_recommendation_order_idx": { + "name": "artifact_recommendation_order_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_recommendation_group_id_artifact_group_id_fk": { + "name": "artifact_recommendation_group_id_artifact_group_id_fk", + "tableFrom": "artifact_recommendation", + "tableTo": "artifact_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.block": { + "name": "block", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "responsive_layout": { + "name": "responsive_layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "block_page_order_idx": { + "name": "block_page_order_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "block_page_id_page_id_fk": { + "name": "block_page_id_page_id_fk", + "tableFrom": "block", + "tableTo": "page", + "schemaTo": "editor", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.artifact": { + "name": "artifact", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_artifact_name_idx": { + "name": "catalog_artifact_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "artifact_key_pk": { + "name": "artifact_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.character": { + "name": "character", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element": { + "name": "element", + "type": "varchar(48)", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element_icon_key": { + "name": "element_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coop_image_key": { + "name": "coop_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_card_image_key": { + "name": "name_card_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constellations": { + "name": "constellations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "catalog_character_name_idx": { + "name": "catalog_character_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "character_key_pk": { + "name": "character_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.weapon": { + "name": "weapon", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_weapon_name_idx": { + "name": "catalog_weapon_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "weapon_key_pk": { + "name": "weapon_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.character": { + "name": "character", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name_th": { + "name": "name_th", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portrait_media_id": { + "name": "portrait_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "element": { + "name": "element", + "type": "varchar(48)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "character_slug_unique": { + "name": "character_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "character_directory_idx": { + "name": "character_directory_idx", + "columns": [ + { + "expression": "visible", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "character_portrait_media_id_media_id_fk": { + "name": "character_portrait_media_id_media_id_fk", + "tableFrom": "character", + "tableTo": "media", + "schemaTo": "media", + "columnsFrom": [ + "portrait_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_condition": { + "name": "constellation_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "constellation_condition_order_idx": { + "name": "constellation_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "constellation_condition_guide_id_guide_id_fk": { + "name": "constellation_condition_guide_id_guide_id_fk", + "tableFrom": "constellation_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_recommendation": { + "name": "constellation_recommendation", + "schema": "guides", + "columns": { + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cumulative_percent": { + "name": "cumulative_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "is_great": { + "name": "is_great", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "constellation_recommendation_condition_id_constellation_condition_id_fk": { + "name": "constellation_recommendation_condition_id_constellation_condition_id_fk", + "tableFrom": "constellation_recommendation", + "tableTo": "constellation_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "constellation_recommendation_condition_id_level_pk": { + "name": "constellation_recommendation_condition_id_level_pk", + "columns": [ + "condition_id", + "level" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.data_source_version": { + "name": "data_source_version", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rows": { + "name": "rows", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evaluation": { + "name": "evaluation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_source_version_unique": { + "name": "data_source_version_unique", + "columns": [ + { + "expression": "data_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_source_version_data_source_id_data_source_id_fk": { + "name": "data_source_version_data_source_id_data_source_id_fk", + "tableFrom": "data_source_version", + "tableTo": "data_source", + "schemaTo": "editor", + "columnsFrom": [ + "data_source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_source_version_created_by_id_user_id_fk": { + "name": "data_source_version_created_by_id_user_id_fk", + "tableFrom": "data_source_version", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.data_source": { + "name": "data_source", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_source_name_idx": { + "name": "data_source_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section_row": { + "name": "extra_section_row", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heading": { + "name": "heading", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "background_color": { + "name": "background_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "text_color": { + "name": "text_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "highlights": { + "name": "highlights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "catalog_items": { + "name": "catalog_items", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sheet": { + "name": "sheet", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "extra_section_row_order_idx": { + "name": "extra_section_row_order_idx", + "columns": [ + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extra_section_row_section_id_extra_section_section_id_fk": { + "name": "extra_section_row_section_id_extra_section_section_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "extra_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "section_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "extra_section_row_media_id_media_id_fk": { + "name": "extra_section_row_media_id_media_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "media", + "schemaTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section": { + "name": "extra_section", + "schema": "guides", + "columns": { + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "extra_section_section_id_guide_section_id_fk": { + "name": "extra_section_section_id_guide_section_id_fk", + "tableFrom": "extra_section", + "tableTo": "guide_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_section": { + "name": "guide_section", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "guide_section_slug_unique": { + "name": "guide_section_slug_unique", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_section_order_idx": { + "name": "guide_section_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_section_guide_id_guide_id_fk": { + "name": "guide_section_guide_id_guide_id_fk", + "tableFrom": "guide_section", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide": { + "name": "guide", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overview": { + "name": "overview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "youtube_url": { + "name": "youtube_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_media_id": { + "name": "cover_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "trashed_at": { + "name": "trashed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "guide_character_key_unique": { + "name": "guide_character_key_unique", + "columns": [ + { + "expression": "character_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_slug_unique": { + "name": "guide_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_directory_idx": { + "name": "guide_directory_idx", + "columns": [ + { + "expression": "trashed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_cover_media_id_media_id_fk": { + "name": "guide_cover_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "schemaTo": "media", + "columnsFrom": [ + "cover_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "media.media": { + "name": "media", + "schema": "media", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_reference_count": { + "name": "current_reference_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revision_reference_count": { + "name": "revision_reference_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_object_key_unique": { + "name": "media_object_key_unique", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_visibility_idx": { + "name": "media_visibility_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_reference_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_created_by_id_user_id_fk": { + "name": "media_created_by_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "media.media_reference": { + "name": "media_reference", + "schema": "media", + "columns": { + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_kind": { + "name": "reference_kind", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_reference_owner_idx": { + "name": "media_reference_owner_idx", + "columns": [ + { + "expression": "owner_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_reference_media_id_media_id_fk": { + "name": "media_reference_media_id_media_id_fk", + "tableFrom": "media_reference", + "tableTo": "media", + "schemaTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "media_reference_media_id_owner_type_owner_id_reference_kind_pk": { + "name": "media_reference_media_id_owner_type_owner_id_reference_kind_pk", + "columns": [ + "media_id", + "owner_type", + "owner_id", + "reference_kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "outbox_event_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "topic": { + "name": "topic", + "type": "varchar(160)", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.page_data_source": { + "name": "page_data_source", + "schema": "editor", + "columns": { + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pinned_version": { + "name": "pinned_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_data_source_dependency_idx": { + "name": "page_data_source_dependency_idx", + "columns": [ + { + "expression": "data_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_data_source_page_id_page_id_fk": { + "name": "page_data_source_page_id_page_id_fk", + "tableFrom": "page_data_source", + "tableTo": "page", + "schemaTo": "editor", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_data_source_data_source_id_data_source_id_fk": { + "name": "page_data_source_data_source_id_data_source_id_fk", + "tableFrom": "page_data_source", + "tableTo": "data_source", + "schemaTo": "editor", + "columnsFrom": [ + "data_source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "page_data_source_page_id_data_source_id_pk": { + "name": "page_data_source_page_id_data_source_id_pk", + "columns": [ + "page_id", + "data_source_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.page": { + "name": "page", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nav_order": { + "name": "nav_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_note": { + "name": "public_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_character_slug_unique": { + "name": "page_character_slug_unique", + "columns": [ + { + "expression": "character_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_navigation_idx": { + "name": "page_navigation_idx", + "columns": [ + { + "expression": "character_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "visible", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "nav_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_character_id_character_id_fk": { + "name": "page_character_id_character_id_fk", + "tableFrom": "page", + "tableTo": "character", + "schemaTo": "editor", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.public_snapshot": { + "name": "public_snapshot", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "page_version": { + "name": "page_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_vector": { + "name": "version_vector", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "vector_hash": { + "name": "vector_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "dto": { + "name": "dto", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_snapshot_version_unique": { + "name": "public_snapshot_version_unique", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "page_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vector_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_snapshot_lookup_idx": { + "name": "public_snapshot_lookup_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "visible", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_snapshot_page_id_page_id_fk": { + "name": "public_snapshot_page_id_page_id_fk", + "tableFrom": "public_snapshot", + "tableTo": "page", + "schemaTo": "editor", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.revision": { + "name": "revision", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "page_version": { + "name": "page_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_revision_id": { + "name": "source_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "checkpoint_hour": { + "name": "checkpoint_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "data_source_versions": { + "name": "data_source_versions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "media_ids": { + "name": "media_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "revision_page_created_idx": { + "name": "revision_page_created_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "revision_checkpoint_hour_unique": { + "name": "revision_checkpoint_hour_unique", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_hour", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"editor\".\"revision\".\"kind\" = 'checkpoint'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revision_page_id_page_id_fk": { + "name": "revision_page_id_page_id_fk", + "tableFrom": "revision", + "tableTo": "page", + "schemaTo": "editor", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revision_author_id_user_id_fk": { + "name": "revision_author_id_user_id_fk", + "tableFrom": "revision", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.slug_alias": { + "name": "slug_alias", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "old_character_slug": { + "name": "old_character_slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "old_page_slug": { + "name": "old_page_slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "target_character_id": { + "name": "target_character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_page_id": { + "name": "target_page_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slug_alias_path_unique": { + "name": "slug_alias_path_unique", + "columns": [ + { + "expression": "old_character_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "old_page_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slug_alias_target_character_id_character_id_fk": { + "name": "slug_alias_target_character_id_character_id_fk", + "tableFrom": "slug_alias", + "tableTo": "character", + "schemaTo": "editor", + "columnsFrom": [ + "target_character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slug_alias_target_page_id_page_id_fk": { + "name": "slug_alias_target_page_id_page_id_fk", + "tableFrom": "slug_alias", + "tableTo": "page", + "schemaTo": "editor", + "columnsFrom": [ + "target_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_composition": { + "name": "team_composition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_seconds": { + "name": "rotation_seconds", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "combo": { + "name": "combo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_composition_order_idx": { + "name": "team_composition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_composition_guide_id_guide_id_fk": { + "name": "team_composition_guide_id_guide_id_fk", + "tableFrom": "team_composition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_member": { + "name": "team_member", + "schema": "guides", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "constellation": { + "name": "constellation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "refinement": { + "name": "refinement", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "damage": { + "name": "damage", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_team_id_team_composition_id_fk": { + "name": "team_member_team_id_team_composition_id_fk", + "tableFrom": "team_member", + "tableTo": "team_composition", + "schemaTo": "guides", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_member_team_id_position_pk": { + "name": "team_member_team_id_position_pk", + "columns": [ + "team_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "editor.template": { + "name": "template", + "schema": "editor", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "structure": { + "name": "structure", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "built_in": { + "name": "built_in", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_slug_unique": { + "name": "template_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'admin'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_condition": { + "name": "weapon_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_condition_order_idx": { + "name": "weapon_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_condition_guide_id_guide_id_fk": { + "name": "weapon_condition_guide_id_guide_id_fk", + "tableFrom": "weapon_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_recommendation": { + "name": "weapon_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refinement": { + "name": "refinement", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "overall_percent": { + "name": "overall_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "personal_percent": { + "name": "personal_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_recommendation_order_idx": { + "name": "weapon_recommendation_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_recommendation_condition_id_weapon_condition_id_fk": { + "name": "weapon_recommendation_condition_id_weapon_condition_id_fk", + "tableFrom": "weapon_recommendation", + "tableTo": "weapon_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index db9dbc2..24e4375 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1788586391986, "tag": "0028_unknown_scarlet_witch", "breakpoints": true + }, + { + "idx": 29, + "version": "7", + "when": 1788594405237, + "tag": "0029_rare_multiple_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/guides/extra-sheet.test.ts b/lib/guides/extra-sheet.test.ts new file mode 100644 index 0000000..1bde083 --- /dev/null +++ b/lib/guides/extra-sheet.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; + +import { + addExtraSheetColumn, + addExtraSheetRow, + createExtraSheet, + deleteExtraSheetColumn, + deleteExtraSheetRow, + extraSheetSchema, + extraSheetToMatrix, + matrixToExtraSheet, + moveExtraSheetColumn, + moveExtraSheetRow, +} from "@/lib/guides/extra-sheet"; + +function idFactory() { + let index = 0; + return (prefix: string) => `${prefix}-${++index}`; +} + +describe("Extra sheet", () => { + it("creates a valid two-by-two sheet", () => { + const sheet = createExtraSheet(idFactory()); + 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: "" }], + ]); + }); + + it("preserves cell identity while rows and columns move", () => { + const createId = idFactory(); + const sheet = createExtraSheet(createId); + const firstColumnId = sheet.columns[0].id; + const secondColumnId = sheet.columns[1].id; + sheet.rows[0].cells[firstColumnId] = "A"; + sheet.rows[0].cells[secondColumnId] = "B"; + sheet.rows[1].cells[firstColumnId] = "C"; + + const moved = moveExtraSheetRow(moveExtraSheetColumn(sheet, 0, 1), 0, 1); + expect(extraSheetToMatrix(moved)).toEqual([ + [{ value: "" }, { value: "C" }], + [{ value: "B" }, { value: "A" }], + ]); + }); + + it("adds and removes rows and columns without orphaned cells", () => { + const createId = idFactory(); + const initial = createExtraSheet(createId); + const columnId = createId("column"); + const expanded = addExtraSheetRow( + addExtraSheetColumn(initial, "Notes", columnId), + createId("row"), + ); + expect(expanded.rows).toHaveLength(3); + expect(expanded.rows.every((row) => row.cells[columnId] === "")).toBe(true); + + const reduced = deleteExtraSheetColumn(expanded, 2); + expect(reduced.rows.every((row) => !(columnId in row.cells))).toBe(true); + expect(deleteExtraSheetRow(reduced, 2).rows).toHaveLength(2); + }); + + it("expands rows and columns when a pasted matrix is larger", () => { + const createId = idFactory(); + const initial = createExtraSheet(createId); + const expanded = matrixToExtraSheet(initial, [ + [{ value: "1" }, { value: "2" }, { value: "3" }], + [{ value: "4" }, { value: "5" }, { value: "6" }], + [{ value: "7" }, { value: "8" }, { value: "=1+1" }], + ], createId); + + expect(expanded.columns).toHaveLength(3); + expect(expanded.rows).toHaveLength(3); + expect(extraSheetToMatrix(expanded)[2][2]?.value).toBe("=1+1"); + expect(extraSheetSchema.safeParse(expanded).success).toBe(true); + }); + + it("rejects duplicate labels and cells for missing columns", () => { + const sheet = createExtraSheet(idFactory()); + sheet.columns[1].label = sheet.columns[0].label.toUpperCase(); + sheet.rows[0].cells.missing = "orphan"; + const result = extraSheetSchema.safeParse(sheet); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.map((issue) => issue.message)).toContain("ชื่อคอลัมน์ต้องไม่ซ้ำกัน"); + expect(result.error.issues.map((issue) => issue.message)).toContain("ไม่พบคอลัมน์ของเซลล์นี้"); + } + }); +}); diff --git a/lib/guides/extra-sheet.ts b/lib/guides/extra-sheet.ts new file mode 100644 index 0000000..f782a27 --- /dev/null +++ b/lib/guides/extra-sheet.ts @@ -0,0 +1,160 @@ +import * as z from "zod"; + +const extraSheetId = z.string().min(1).max(128); + +export const extraSheetSchema = z.strictObject({ + columns: z.array(z.strictObject({ + id: extraSheetId, + label: z.string().trim().min(1).max(120), + })).min(1), + rows: z.array(z.strictObject({ + id: extraSheetId, + cells: z.record(z.string(), z.string().max(10_000)), + })).min(1), +}).superRefine((sheet, context) => { + const columnIds = new Set(sheet.columns.map((column) => column.id)); + if (columnIds.size !== sheet.columns.length) { + context.addIssue({ code: "custom", path: ["columns"], message: "รหัสคอลัมน์ต้องไม่ซ้ำกัน" }); + } + const columnLabels = new Set(sheet.columns.map((column) => column.label.toLocaleLowerCase())); + if (columnLabels.size !== sheet.columns.length) { + context.addIssue({ code: "custom", path: ["columns"], message: "ชื่อคอลัมน์ต้องไม่ซ้ำกัน" }); + } + const rowIds = new Set(sheet.rows.map((row) => row.id)); + if (rowIds.size !== sheet.rows.length) { + context.addIssue({ code: "custom", path: ["rows"], message: "รหัสแถวต้องไม่ซ้ำกัน" }); + } + sheet.rows.forEach((row, rowIndex) => { + Object.keys(row.cells).forEach((columnId) => { + if (!columnIds.has(columnId)) { + context.addIssue({ + code: "custom", + path: ["rows", rowIndex, "cells", columnId], + message: "ไม่พบคอลัมน์ของเซลล์นี้", + }); + } + }); + }); +}); + +export type ExtraSheet = z.output; + +export type ExtraSheetCell = { value: string }; +export type ExtraSheetMatrix = Array>; + +export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): string { + const labels = new Set(columns.map((column) => column.label.toLocaleLowerCase())); + let index = columns.length + 1; + while (labels.has(`คอลัมน์ ${index}`.toLocaleLowerCase())) index += 1; + return `คอลัมน์ ${index}`; +} + +export function createExtraSheet(createId: (prefix: string) => string): ExtraSheet { + const columns = [1, 2].map((index) => ({ + id: createId("extra-sheet-column"), + label: `คอลัมน์ ${index}`, + })); + return { + columns, + rows: [1, 2].map(() => ({ + id: createId("extra-sheet-row"), + cells: Object.fromEntries(columns.map((column) => [column.id, ""])), + })), + }; +} + +export function extraSheetToMatrix(sheet: ExtraSheet): ExtraSheetMatrix { + return sheet.rows.map((row) => + sheet.columns.map((column) => ({ value: row.cells[column.id] ?? "" })), + ); +} + +export function matrixToExtraSheet( + sheet: ExtraSheet, + matrix: ExtraSheetMatrix, + createId: (prefix: string) => string, +): ExtraSheet { + const columnCount = Math.max( + sheet.columns.length, + ...matrix.map((row) => row.length), + ); + const columns = [...sheet.columns]; + while (columns.length < columnCount) { + columns.push({ + id: createId("extra-sheet-column"), + label: nextExtraSheetColumnLabel(columns), + }); + } + const rowCount = Math.max(sheet.rows.length, matrix.length); + const rows = Array.from({ length: rowCount }, (_, rowIndex) => ({ + id: sheet.rows[rowIndex]?.id ?? createId("extra-sheet-row"), + cells: Object.fromEntries(columns.map((column, columnIndex) => [ + column.id, + String(matrix[rowIndex]?.[columnIndex]?.value ?? ""), + ])), + })); + return { columns, rows }; +} + +export function addExtraSheetColumn( + sheet: ExtraSheet, + label: string, + id: string, +): ExtraSheet { + return { + columns: [...sheet.columns, { id, label }], + rows: sheet.rows.map((row) => ({ ...row, cells: { ...row.cells, [id]: "" } })), + }; +} + +export function addExtraSheetRow(sheet: ExtraSheet, id: string): ExtraSheet { + return { + ...sheet, + rows: [...sheet.rows, { + id, + cells: Object.fromEntries(sheet.columns.map((column) => [column.id, ""])), + }], + }; +} + +export function moveExtraSheetColumn( + sheet: ExtraSheet, + from: number, + to: number, +): ExtraSheet { + if (to < 0 || to >= sheet.columns.length || from === to) return sheet; + const columns = [...sheet.columns]; + const [column] = columns.splice(from, 1); + columns.splice(to, 0, column); + return { ...sheet, columns }; +} + +export function moveExtraSheetRow( + sheet: ExtraSheet, + from: number, + to: number, +): ExtraSheet { + if (to < 0 || to >= sheet.rows.length || from === to) return sheet; + const rows = [...sheet.rows]; + const [row] = rows.splice(from, 1); + rows.splice(to, 0, row); + return { ...sheet, rows }; +} + +export function deleteExtraSheetColumn(sheet: ExtraSheet, index: number): ExtraSheet { + if (sheet.columns.length === 1) return sheet; + const columnId = sheet.columns[index]?.id; + if (!columnId) return sheet; + return { + columns: sheet.columns.filter((_, columnIndex) => columnIndex !== index), + rows: sheet.rows.map((row) => ({ + ...row, + cells: Object.fromEntries(Object.entries(row.cells).filter(([key]) => key !== columnId)), + })), + }; +} + +export function deleteExtraSheetRow(sheet: ExtraSheet, index: number): ExtraSheet { + if (sheet.rows.length === 1) return sheet; + return { ...sheet, rows: sheet.rows.filter((_, rowIndex) => rowIndex !== index) }; +} diff --git a/lib/guides/schemas.test.ts b/lib/guides/schemas.test.ts index 71bee25..8dbfb51 100644 --- a/lib/guides/schemas.test.ts +++ b/lib/guides/schemas.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { calculateTeamMetrics } from "@/lib/guides/calculations"; +import { createExtraSheet } from "@/lib/guides/extra-sheet"; import { artifactSectionSchema, constellationSectionSchema, createGuideSchema, extraSectionSchema, teamSectionSchema, updateGuideSchema, weaponSectionSchema } from "@/lib/guides/schemas"; import { recommendedStatElement, @@ -207,6 +208,37 @@ describe("structured guide validation", () => { sections: [{ title: "Extra", rows: [{ ...row, catalogItems: [row.catalogItems[0], row.catalogItems[0]] }] }], }).success).toBe(false); }); + + it("validates an optional Extra card sheet", () => { + let id = 0; + const sheet = createExtraSheet((prefix) => `${prefix}-${++id}`); + sheet.rows[0].cells[sheet.columns[0].id] = "=1+1"; + const row = { + mediaId: null, + heading: "Stats", + body: "", + percent: null, + backgroundColor: null, + highlights: [], + catalogItems: [], + sheet, + }; + const result = extraSectionSchema.safeParse({ + expectedVersion: 1, + sections: [{ title: "Extra", rows: [row] }], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sections[0].rows[0].sheet?.rows[0].cells[sheet.columns[0].id]).toBe("=1+1"); + } + + const invalidSheet = structuredClone(sheet); + invalidSheet.rows[0].cells.missing = "orphan"; + expect(extraSectionSchema.safeParse({ + expectedVersion: 1, + sections: [{ title: "Extra", rows: [{ ...row, sheet: invalidSheet }] }], + }).success).toBe(false); + }); }); describe("Team Calc", () => { diff --git a/lib/guides/schemas.ts b/lib/guides/schemas.ts index 2006e02..c01e76b 100644 --- a/lib/guides/schemas.ts +++ b/lib/guides/schemas.ts @@ -2,6 +2,7 @@ import * as z from "zod"; import { targetStatOptions } from "@/lib/guides/stats"; import { parseYouTubeUrl } from "@/lib/guides/youtube"; +import { extraSheetSchema } from "@/lib/guides/extra-sheet"; import { normalizeExtraRichText } from "@/lib/rich-text"; export const guideSectionKinds = ["weapon", "artifact", "constellations", "team"] as const; @@ -154,6 +155,7 @@ const extraRowSchema = z.strictObject({ (highlights) => new Set(highlights.map((highlight) => highlight.text)).size === highlights.length, "คำหรือวลีที่ไฮไลต์ต้องไม่ซ้ำกัน", ).default([]), + sheet: extraSheetSchema.nullable().default(null), }).transform((row) => ({ ...row, body: normalizeExtraRichText(row.body, row.highlights), diff --git a/package.json b/package.json index dde8536..6e15b11 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,9 @@ "react": "19.2.8", "react-dom": "19.2.8", "react-resizable-panels": "^4.12.3", + "react-spreadsheet": "0.10.1", "recharts": "3.8.0", + "scheduler": "^0.27.0", "server-only": "^0.0.1", "shadcn": "^4.19.0", "sharp": "^0.35.4",