ported schema builder
23
actions/Schemas/create.ts
Normal file
@ -0,0 +1,23 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/prisma/prismaClient";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const createSchema = async (form: FormData) => {
|
||||
const name = form.get("name")?.toString();
|
||||
const gsId = form.get("gsId")?.toString();
|
||||
|
||||
if (!name || !gsId) return;
|
||||
|
||||
const { id } = await prisma.schema.create({
|
||||
data: {
|
||||
name,
|
||||
schema: "{}",
|
||||
types: "{}",
|
||||
version: 0,
|
||||
gameSystemId: gsId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
redirect(`/game-systems/${gsId}/schema/${id}`);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { Sticky } from "@/lib/sticky";
|
||||
import { prisma } from "@/prisma/prismaClient";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function GameSystem(
|
||||
{ params: { id } }: { params: { id: string } },
|
||||
@ -35,15 +35,25 @@ export default async function GameSystem(
|
||||
<h1>{gameSystem?.name}</h1>
|
||||
</section>
|
||||
<section>
|
||||
<>
|
||||
<div>
|
||||
<Link
|
||||
className="btn-primary mb-6 block w-min whitespace-nowrap"
|
||||
href={`/game-systems/${id}/schema/create`}
|
||||
>
|
||||
Create New Schema
|
||||
</Link>
|
||||
</div>
|
||||
<ul>
|
||||
{gameSystem?.schemas.map((schema) => (
|
||||
<li key={schema.id}>{schema.name}</li>
|
||||
))}
|
||||
{!gameSystem?.schemas.length && (
|
||||
<li>No schemas for {gameSystem?.name}</li>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
</section>
|
||||
<Sticky sidedness={-1}>
|
||||
<h1>HELLO!</h1>
|
||||
</Sticky>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
21
app/game-systems/[id]/schema/[schemaId]/page.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Heading } from "@/components/heading";
|
||||
import { SchemaBuilder } from "@/components/schema";
|
||||
import { prisma } from "@/prisma/prismaClient";
|
||||
|
||||
export default async function CreateSchemaForGameSystem(
|
||||
{ params }: { params: { id: string } },
|
||||
) {
|
||||
const gs = await prisma.gameSystem.findFirst({
|
||||
where: { id: params.id },
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={gs?.name || ""} strapline="Schemas" />
|
||||
<section>
|
||||
<SchemaBuilder></SchemaBuilder>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
@ -12,6 +12,9 @@
|
||||
input {
|
||||
@apply py-2 px-4 rounded-full dark:bg-mixed-200 bg-mixed-600 placeholder:text-dark-500;
|
||||
}
|
||||
textarea {
|
||||
@apply dark:bg-mixed-200 bg-primary-600 rounded-md p-1;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
@ -49,6 +52,9 @@
|
||||
.btn-secondary {
|
||||
@apply dark:text-primary-500 text-primary-100 py-4 px-6 font-bold text-lg;
|
||||
}
|
||||
.btn-small {
|
||||
@apply px-2 py-1;
|
||||
}
|
||||
.p {
|
||||
@apply pb-1;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import { DevToolboxContextProvider } from "@/components/devtools/context";
|
||||
import { RecoilRootClient } from "@/components/recoilRoot";
|
||||
|
||||
const roboto = Roboto({ subsets: ["latin"], weight: "400" });
|
||||
|
||||
@ -72,6 +73,7 @@ export default function RootLayout({
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<RecoilRootClient>
|
||||
<DevToolboxContextProvider
|
||||
isDev={process.env.NODE_ENV !== "production"}
|
||||
>
|
||||
@ -79,6 +81,7 @@ export default function RootLayout({
|
||||
{children}
|
||||
</main>
|
||||
</DevToolboxContextProvider>
|
||||
</RecoilRootClient>
|
||||
<div id="root-portal"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
6
assets/icons/Anvil Icon.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect class="anvil-base" x="4.5" y="9.5" width="10" height="2" rx="0.5" stroke="inherit" fill="none" />
|
||||
<path class="anvil-body"
|
||||
d="M6 1H2C3 2 5.5 2.5 7 3C8.5 3.5 8.54315 4.2918 7 6L6 7V8H13V7C13 7 11.5 6 11 5C10.5 4 11 2.5 15 2V0.5H6V1Z"
|
||||
stroke="inherit" fill="none" />
|
||||
</svg>
|
After Width: | Height: | Size: 385 B |
1
assets/icons/Help Icon copy.min.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" stroke="#fff" fill="#fff" height="28" width="28"><circle stroke-width="3" fill="none" stroke="inherit" r="12.5" cy="14" cx="14"/><path fill="inherit" d="M12.004 17.816v-.867c0-.531.074-1 .223-1.406.148-.414.386-.805.714-1.172.328-.375.762-.758 1.301-1.148.485-.344.871-.653 1.16-.926.297-.274.512-.543.645-.809.14-.273.21-.582.21-.925 0-.508-.187-.895-.562-1.16-.375-.266-.898-.4-1.57-.4s-1.34.106-2.004.317c-.656.211-1.324.489-2.004.832L8.84 7.586c.781-.438 1.629-.79 2.543-1.055.914-.273 1.914-.41 3-.41 1.672 0 2.965.402 3.879 1.207.922.797 1.382 1.813 1.382 3.047 0 .656-.105 1.227-.316 1.71a4.165 4.165 0 01-.937 1.337c-.414.406-.934.836-1.559 1.289-.469.344-.828.633-1.078.867-.25.235-.422.469-.516.703a2.356 2.356 0 00-.129.832v.703zm-.375 4.008c0-.734.2-1.25.598-1.547.406-.297.894-.445 1.464-.445.555 0 1.032.148 1.43.445.406.297.61.813.61 1.547 0 .703-.204 1.211-.61 1.524-.398.312-.875.468-1.43.468-.57 0-1.058-.156-1.464-.468-.399-.313-.598-.82-.598-1.524z"/></svg>
|
After Width: | Height: | Size: 1017 B |
70
assets/icons/Help Icon copy.svg
Normal file
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="Help Icon.svg"
|
||||
id="svg6"
|
||||
version="1.1"
|
||||
viewBox="0 0 28 28"
|
||||
stroke="#ffffff"
|
||||
fill="#ffffff"
|
||||
height="28"
|
||||
width="28">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="1432"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:cy="14"
|
||||
inkscape:cx="14"
|
||||
inkscape:zoom="27.222222"
|
||||
fit-margin-bottom="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-top="0"
|
||||
showgrid="false"
|
||||
id="namedview8"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<circle
|
||||
id="circle2"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke="inherit"
|
||||
r="12.5"
|
||||
cy="14"
|
||||
cx="14" />
|
||||
<!-- <path d="M0.5,13.5a12.5,12.5 0 1,0 25,0a12.5,12.5 0 1,0 -25,0" fill="none" stroke="inherit" /> -->
|
||||
<path
|
||||
id="path4"
|
||||
fill="inherit"
|
||||
d="m 12.0039,17.8164 v -0.8672 c 0,-0.5312 0.0742,-1 0.2227,-1.4062 0.1484,-0.4141 0.3867,-0.8047 0.7148,-1.1719 0.3281,-0.375 0.7617,-0.7578 1.3008,-1.1484 0.4844,-0.3438 0.8711,-0.6524 1.1601,-0.9258 0.2969,-0.2735 0.5118,-0.543 0.6446,-0.8086 0.1406,-0.2735 0.2109,-0.5821 0.2109,-0.9258 0,-0.50781 -0.1875,-0.89453 -0.5625,-1.16016 -0.375,-0.26562 -0.8984,-0.39843 -1.5703,-0.39843 -0.6719,0 -1.3398,0.10547 -2.0039,0.3164 -0.6563,0.21094 -1.3242,0.48828 -2.00391,0.83203 L 8.83984,7.58594 c 0.78125,-0.4375 1.62891,-0.78906 2.54296,-1.05469 0.9141,-0.27344 1.9141,-0.41016 3,-0.41016 1.6719,0 2.9649,0.40235 3.8789,1.20703 0.9219,0.79688 1.3828,1.8125 1.3828,3.04688 0,0.6562 -0.1054,1.2266 -0.3164,1.7109 -0.2031,0.4766 -0.5156,0.9219 -0.9375,1.336 -0.414,0.4062 -0.9336,0.8359 -1.5586,1.289 -0.4687,0.3438 -0.8281,0.6329 -1.0781,0.8672 -0.25,0.2344 -0.4219,0.4688 -0.5156,0.7031 -0.086,0.2266 -0.1289,0.504 -0.1289,0.8321 v 0.7031 z m -0.375,4.0078 c 0,-0.7344 0.1992,-1.25 0.5977,-1.5469 0.4062,-0.2968 0.8945,-0.4453 1.4648,-0.4453 0.5547,0 1.0313,0.1485 1.4297,0.4453 0.4062,0.2969 0.6094,0.8125 0.6094,1.5469 0,0.7031 -0.2032,1.211 -0.6094,1.5235 -0.3984,0.3125 -0.875,0.4687 -1.4297,0.4687 -0.5703,0 -1.0586,-0.1562 -1.4648,-0.4687 -0.3985,-0.3125 -0.5977,-0.8204 -0.5977,-1.5235 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
70
assets/icons/Help Icon.svg
Normal file
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="Help Icon.svg"
|
||||
id="svg6"
|
||||
version="1.1"
|
||||
viewBox="0 0 28 28"
|
||||
stroke="#ffffff"
|
||||
fill="#ffffff"
|
||||
height="28"
|
||||
width="28">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="1432"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:cy="14"
|
||||
inkscape:cx="14"
|
||||
inkscape:zoom="27.222222"
|
||||
fit-margin-bottom="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-top="0"
|
||||
showgrid="false"
|
||||
id="namedview8"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<circle
|
||||
id="circle2"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke="inherit"
|
||||
r="12.5"
|
||||
cy="14"
|
||||
cx="14" />
|
||||
<!-- <path d="M0.5,13.5a12.5,12.5 0 1,0 25,0a12.5,12.5 0 1,0 -25,0" fill="none" stroke="inherit" /> -->
|
||||
<path
|
||||
id="path4"
|
||||
fill="inherit"
|
||||
d="m 12.0039,17.8164 v -0.8672 c 0,-0.5312 0.0742,-1 0.2227,-1.4062 0.1484,-0.4141 0.3867,-0.8047 0.7148,-1.1719 0.3281,-0.375 0.7617,-0.7578 1.3008,-1.1484 0.4844,-0.3438 0.8711,-0.6524 1.1601,-0.9258 0.2969,-0.2735 0.5118,-0.543 0.6446,-0.8086 0.1406,-0.2735 0.2109,-0.5821 0.2109,-0.9258 0,-0.50781 -0.1875,-0.89453 -0.5625,-1.16016 -0.375,-0.26562 -0.8984,-0.39843 -1.5703,-0.39843 -0.6719,0 -1.3398,0.10547 -2.0039,0.3164 -0.6563,0.21094 -1.3242,0.48828 -2.00391,0.83203 L 8.83984,7.58594 c 0.78125,-0.4375 1.62891,-0.78906 2.54296,-1.05469 0.9141,-0.27344 1.9141,-0.41016 3,-0.41016 1.6719,0 2.9649,0.40235 3.8789,1.20703 0.9219,0.79688 1.3828,1.8125 1.3828,3.04688 0,0.6562 -0.1054,1.2266 -0.3164,1.7109 -0.2031,0.4766 -0.5156,0.9219 -0.9375,1.336 -0.414,0.4062 -0.9336,0.8359 -1.5586,1.289 -0.4687,0.3438 -0.8281,0.6329 -1.0781,0.8672 -0.25,0.2344 -0.4219,0.4688 -0.5156,0.7031 -0.086,0.2266 -0.1289,0.504 -0.1289,0.8321 v 0.7031 z m -0.375,4.0078 c 0,-0.7344 0.1992,-1.25 0.5977,-1.5469 0.4062,-0.2968 0.8945,-0.4453 1.4648,-0.4453 0.5547,0 1.0313,0.1485 1.4297,0.4453 0.4062,0.2969 0.6094,0.8125 0.6094,1.5469 0,0.7031 -0.2032,1.211 -0.6094,1.5235 -0.3984,0.3125 -0.875,0.4687 -1.4297,0.4687 -0.5703,0 -1.0586,-0.1562 -1.4648,-0.4687 -0.3985,-0.3125 -0.5977,-0.8204 -0.5977,-1.5235 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
8
assets/icons/Trash Icon Open.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="35" height="35" viewBox="0 0 23 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12H21V32C21 33.1046 20.1046 34 19 34H5C3.89543 34 3 33.1046 3 32V12Z" stroke="inherit" fill="none" stroke-width="2"/>
|
||||
<path d="M7 16L7 29" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M12 16V29" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M17 16V29" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M8.59244 1.36064L12.5317 0.666048C12.8036 0.618097 13.063 0.799681 13.1109 1.07163L13.3714 2.54884L8.44734 3.41708L8.18687 1.93987C8.13891 1.66792 8.3205 1.40859 8.59244 1.36064Z" stroke="inherit" fill="none"/>
|
||||
<path d="M2.05644 4.54394L19.783 1.41827C20.5988 1.27442 21.3768 1.81917 21.5207 2.63501L21.9548 5.09703L1.27382 8.74365L0.8397 6.28163C0.695845 5.46578 1.2406 4.6878 2.05644 4.54394Z" stroke="inherit" fill="none"/>
|
||||
</svg>
|
After Width: | Height: | Size: 943 B |
12
assets/icons/Trash Icon.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg class="trash-can" width="35" height="30" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- body -->
|
||||
<path d="M2 7H20V27C20 28.1046 19.1046 29 18 29H4C2.89543 29 2 28.1046 2 27V7Z" stroke="inherit" fill="none" stroke-width="2"/>
|
||||
<!-- body lines -->
|
||||
<path d="M6 11L6 24" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M11 11V24" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M16 11V24" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- handle -->
|
||||
<path class="trash-lid" d="M9 0.5H13C13.2761 0.5 13.5 0.723858 13.5 1V2.5H8.5V1C8.5 0.723858 8.72386 0.5 9 0.5Z" stroke="inherit" fill="none"/>
|
||||
<!-- cap -->
|
||||
<path class="trash-lid" d="M2 2.5H20C20.8284 2.5 21.5 3.17157 21.5 4V6.5H0.5V4C0.5 3.17157 1.17157 2.5 2 2.5Z" stroke="inherit" fill="none"/>
|
||||
</svg>
|
After Width: | Height: | Size: 872 B |
1
assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
6
components/AnimatedPageContainer/index.css
Normal file
@ -0,0 +1,6 @@
|
||||
.page {
|
||||
transition:
|
||||
transform 500ms,
|
||||
opacity 300ms,
|
||||
z-index 0ms 500ms
|
||||
}
|
46
components/AnimatedPageContainer/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { FC, PropsWithChildren, ReactNode, useState } from "react";
|
||||
import "./index.css";
|
||||
|
||||
interface IProps {
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
const AnimatedPageContainer: FC<PropsWithChildren<IProps>> = (
|
||||
{ children, currentPage },
|
||||
) => {
|
||||
const [uuid] = useState(crypto.randomUUID());
|
||||
|
||||
const renderChild = (child: ReactNode, index: number) => {
|
||||
const isActive = index === currentPage;
|
||||
let position = "active";
|
||||
switch ((index - currentPage) / Math.abs(index - currentPage)) {
|
||||
case 1:
|
||||
position = "right";
|
||||
break;
|
||||
case -1:
|
||||
position = "left";
|
||||
break;
|
||||
default:
|
||||
position = "active";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`page container ${uuid}: ${index}`}
|
||||
data-active={isActive}
|
||||
data-position={position}
|
||||
className="data-[active=true]:opacity-100 data-[active=true]:static opacity-0 top-0 left-0 absolute page data-[position=left]:-translate-x-96 data-[position=right]:translate-x-96 translate-x-0"
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden p-2">
|
||||
{React.Children.map(children, renderChild)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedPageContainer;
|
23
components/Icon/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { FC } from "react";
|
||||
import Help from "../../assets/icons/Help Icon.svg";
|
||||
import Trash from "../../assets/icons/Trash Icon.svg";
|
||||
import Trash_hover from "../../assets/icons/Trash Icon Open.svg";
|
||||
import Anvil from "../../assets/icons/Anvil Icon.svg";
|
||||
|
||||
const library = {
|
||||
Help,
|
||||
Trash,
|
||||
Trash_hover,
|
||||
Anvil,
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
className: string;
|
||||
icon: keyof typeof library;
|
||||
}
|
||||
|
||||
export const Icon: FC<IProps> = ({ className, icon }) => {
|
||||
const ICON = library[icon];
|
||||
|
||||
return <ICON className={className} />;
|
||||
};
|
15
components/Poppables/help.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
import { Poppable } from "@/lib/poppables/components/poppable";
|
||||
import { Icon } from "@/components/Icon";
|
||||
|
||||
export const HelpPopper: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<Poppable
|
||||
content={children}
|
||||
preferredAlign="centered"
|
||||
preferredEdge="bottom"
|
||||
>
|
||||
<Icon icon="help" className="svg-white w-4 h-4" />
|
||||
</Poppable>
|
||||
);
|
||||
};
|
14
components/Poppables/truncation.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
import { Poppable } from "@/lib/poppables/components/poppable";
|
||||
|
||||
export const Truncate: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<Poppable
|
||||
content={children}
|
||||
preferredAlign="centered"
|
||||
preferredEdge="top"
|
||||
>
|
||||
<p className="truncate max-w-full underline">{children}</p>
|
||||
</Poppable>
|
||||
);
|
||||
};
|
15
components/heading.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { FC } from "react";
|
||||
|
||||
interface Props {
|
||||
strapline?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const Heading: FC<Props> = ({ strapline, title }) => {
|
||||
return (
|
||||
<section className="heading">
|
||||
{!!strapline && <h2 className="strapline">{strapline}</h2>}
|
||||
<h1>{title}</h1>
|
||||
</section>
|
||||
);
|
||||
};
|
5
components/recoilRoot.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { RecoilRoot } from "recoil";
|
||||
|
||||
export const RecoilRootClient = RecoilRoot;
|
135
components/schema/field-editor.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { useObjectStateWrapper } from "../../hooks/useObjectState";
|
||||
import { ValueField } from "./value-field";
|
||||
import { HelpPopper } from "../Poppables/help";
|
||||
import { Icon } from "../Icon";
|
||||
import { RESERVED_FIELDS } from "../../constants/ReservedFields";
|
||||
import {
|
||||
fieldTypeOptions,
|
||||
FieldTypes,
|
||||
fieldTypesWithValues,
|
||||
} from "./fieldtypes";
|
||||
|
||||
interface IProps {
|
||||
update: (arg: FieldType) => void;
|
||||
field: FieldType;
|
||||
fieldName: string;
|
||||
deleteField: (arg: string) => void;
|
||||
}
|
||||
|
||||
export const FieldEditor: FC<IProps> = (
|
||||
{ update, field, fieldName, deleteField },
|
||||
) => {
|
||||
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(
|
||||
field,
|
||||
(e) => update(typeof e === "function" ? e(field) : e),
|
||||
);
|
||||
|
||||
const shouldShowValueField = useCallback(
|
||||
() => fieldTypesWithValues.includes(field.type) || field.isConstant,
|
||||
[field.isConstant, field.type],
|
||||
);
|
||||
|
||||
const [reserved, setReserved] = useState<FieldType | undefined>(
|
||||
RESERVED_FIELDS[fieldName],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setReserved(RESERVED_FIELDS[fieldName]);
|
||||
}, [fieldName]);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(field.value);
|
||||
// }, [field])
|
||||
|
||||
return (
|
||||
<li className="odd:bg-black/50">
|
||||
<div className="flex gap-2 items-center">
|
||||
<p>{fieldName}</p>
|
||||
{reserved && (
|
||||
<HelpPopper>
|
||||
<p className="text-xs">
|
||||
This is a reserved field name, these exist for internal purposes,
|
||||
but are still useful when creating a type, and as such have
|
||||
specific settings that you cannot override. If you need control
|
||||
over the field properties, please use a different field name
|
||||
</p>
|
||||
</HelpPopper>
|
||||
)}
|
||||
</div>
|
||||
{!reserved && (
|
||||
<div className=" flex gap-x-4 items-center p-2 w-full">
|
||||
<label className="w-min">
|
||||
Field Type:
|
||||
<select
|
||||
className="capitalize"
|
||||
{...bindProperty("type")}
|
||||
disabled={!!reserved}
|
||||
>
|
||||
{fieldTypeOptions.map((o) => (
|
||||
<option
|
||||
key={"fieldtypes" + o}
|
||||
className="capitalize"
|
||||
value={FieldTypes[o]}
|
||||
>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{shouldShowValueField() && (
|
||||
<ValueField type={field.type} bind={bindProperty("value")} />
|
||||
)}
|
||||
<span className="flex items-center gap-2">
|
||||
<label>
|
||||
<input type="checkbox" {...bindPropertyCheck("isConstant")} />
|
||||
{" "}
|
||||
Is constant
|
||||
</label>
|
||||
<HelpPopper>
|
||||
<p className="text-sm">
|
||||
Constant values can't be overwritten in publications. When
|
||||
a dice field is set to a constant value, it instead rolls a dice
|
||||
of that value whenever this field is displayed (unless
|
||||
exported). This could be useful for a randomly generated
|
||||
scenario or for cards being drawn as the dice value will
|
||||
automatically be determined by the dice roll.
|
||||
</p>
|
||||
</HelpPopper>
|
||||
</span>
|
||||
<label className="w-min">
|
||||
Minimum:
|
||||
<input
|
||||
className="w-12"
|
||||
type="number"
|
||||
{...bindProperty("minimum")}
|
||||
/>
|
||||
</label>
|
||||
<label className="w-min">
|
||||
Limit:
|
||||
<input className="w-12" type="number" {...bindProperty("limit")} />
|
||||
</label>
|
||||
<HelpPopper>
|
||||
<p className="text-sm">
|
||||
Minimum and Limit apply to the number of entries allowed for this
|
||||
field, not the maximum and minimum value. Set the minimum to 0 to
|
||||
make a field optional. Set the limit to 0 to allow for unlimited
|
||||
entries.
|
||||
</p>
|
||||
</HelpPopper>
|
||||
<button
|
||||
className="no-default self-end ml-auto"
|
||||
onClick={() => deleteField(fieldName)}
|
||||
>
|
||||
<Icon
|
||||
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
|
||||
icon="Trash"
|
||||
>
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
31
components/schema/field-type-input.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { SchemaEditAtom } from "../../recoil/atoms/schema";
|
||||
import { TEMPLATE_TYPES } from "../../constants/TemplateTypes";
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
|
||||
interface IProps {
|
||||
bind: InputBinder;
|
||||
}
|
||||
|
||||
export const FieldTypeInput: FC<PropsWithChildren<IProps>> = ({ bind }) => {
|
||||
const schema = useRecoilValue(SchemaEditAtom);
|
||||
|
||||
return (
|
||||
<label className="w-min">
|
||||
Type:
|
||||
<input type="text" {...bind} list="type-editor-type-list" />
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map((k) => (
|
||||
<option key={"templatetypes" + k} className="capitalize" value={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map((k) => (
|
||||
<option key={"schematypes" + k} className="capitalize" value={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
);
|
||||
};
|
26
components/schema/fieldtypes.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export const fieldTypeOptions: (keyof typeof FieldTypes)[] = [
|
||||
"number",
|
||||
"text",
|
||||
"long text",
|
||||
"checkbox",
|
||||
"type",
|
||||
"dice",
|
||||
"select",
|
||||
"any",
|
||||
];
|
||||
export enum FieldTypes {
|
||||
number = "number",
|
||||
text = "text",
|
||||
"long text" = "long text",
|
||||
checkbox = "checkbox",
|
||||
type = "@type",
|
||||
dice = "dice",
|
||||
any = "@select",
|
||||
select = "select",
|
||||
}
|
||||
export const fieldTypesWithValues = [
|
||||
FieldTypes.dice,
|
||||
FieldTypes.type,
|
||||
FieldTypes.select,
|
||||
FieldTypes.any,
|
||||
];
|
198
components/schema/index.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import AnimatedPageContainer from "@/components/AnimatedPageContainer";
|
||||
import { TypeEditor } from "./type-editor";
|
||||
import { useObjectStateWrapper } from "@/hooks/useObjectState";
|
||||
import { useInput } from "../../hooks/useInput";
|
||||
import { useRecoilState, useResetRecoilState } from "recoil";
|
||||
import { SchemaEditAtom } from "@/recoil/atoms/schema";
|
||||
import { SchemaViewer } from "./schema-viewer";
|
||||
import { TemplateEditor } from "./template-editor";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FieldTypes } from "./fieldtypes";
|
||||
|
||||
export const SchemaBuilder: FC = () => {
|
||||
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
||||
const resetSchema = useResetRecoilState(SchemaEditAtom);
|
||||
const { update: updateSchema, bindProperty: bindSchemaProperty } =
|
||||
useObjectStateWrapper<Schema>(schema, setSchema);
|
||||
|
||||
const { schemaId } = useParams<{ schemaId: string }>();
|
||||
|
||||
const { value: typeName, bind: bindTypeName, reset: resetTypeName } =
|
||||
useInput("");
|
||||
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
|
||||
const [lastSaved, setLastSaved] = useState(schema);
|
||||
|
||||
const [selectedType, setSelectedType] = useState("");
|
||||
|
||||
const saveType = useCallback((name: string, type: TypeType) => {
|
||||
updateSchema((e) => ({
|
||||
types: {
|
||||
...e.types,
|
||||
[name]: type,
|
||||
},
|
||||
}));
|
||||
resetTypeName();
|
||||
setPageNumber(0);
|
||||
setSelectedType("");
|
||||
}, [resetTypeName, updateSchema]);
|
||||
|
||||
const saveSchema = useCallback(async () => {
|
||||
setLastSaved(schema);
|
||||
// const sid = await GameSystemsService.saveSchema(schema);
|
||||
// if (schemaId === 'new') navigate('/schema/'+sid)
|
||||
}, [schema]);
|
||||
|
||||
const selectTypeForEdit = useCallback((typeKey: string) => {
|
||||
setSelectedType(typeKey);
|
||||
setPageNumber(1);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
value: schemaFieldName,
|
||||
bind: bindTemplateName,
|
||||
reset: resetTemplateName,
|
||||
} = useInput("", { disallowSpaces: true });
|
||||
const addSchemaField = useCallback(() => {
|
||||
updateSchema((s) => ({
|
||||
schema: {
|
||||
...s.schema,
|
||||
[schemaFieldName]: {
|
||||
display: "",
|
||||
type: FieldTypes.any,
|
||||
},
|
||||
},
|
||||
}));
|
||||
resetTemplateName();
|
||||
}, [resetTemplateName, schemaFieldName, updateSchema]);
|
||||
|
||||
const updateSchemaField = useCallback((key: string, template: Template) => {
|
||||
updateSchema((s) => ({
|
||||
schema: {
|
||||
...s.schema,
|
||||
[key]: template,
|
||||
},
|
||||
}));
|
||||
}, [updateSchema]);
|
||||
|
||||
const deleteType = useCallback((key: string) => {
|
||||
updateSchema((s) => {
|
||||
const types = { ...s.types };
|
||||
delete types[key];
|
||||
return { types };
|
||||
});
|
||||
}, [updateSchema]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 p-8">
|
||||
<div className="panel w-2/3 h-full flex flex-col gap-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
{...bindSchemaProperty("name")}
|
||||
placeholder="Schema Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="subheader mb-2">Add Schema Field</p>
|
||||
<div className="mb-2">
|
||||
<input type="text" {...bindTemplateName} />
|
||||
<button onClick={addSchemaField} disabled={!schemaFieldName}>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<ul className="rounded-lg overflow-hidden">
|
||||
{Object.entries(schema.schema).map((
|
||||
[schemaFieldKey, schemaField],
|
||||
) => (
|
||||
<TemplateEditor
|
||||
key={schemaFieldKey}
|
||||
templateKey={schemaFieldKey}
|
||||
template={schemaField}
|
||||
update={updateSchemaField}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<AnimatedPageContainer currentPage={pageNumber}>
|
||||
<div>
|
||||
<p className="subheader mb-2">Add a type</p>
|
||||
<input type="text" {...bindTypeName} />
|
||||
<button
|
||||
className="interactive"
|
||||
disabled={!typeName}
|
||||
onClick={() => setPageNumber(1)}
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
<TypeEditor
|
||||
name={selectedType || typeName}
|
||||
saveType={saveType}
|
||||
type={selectedType
|
||||
? schema.types[selectedType as keyof typeof schema.types]
|
||||
: undefined}
|
||||
/>
|
||||
</AnimatedPageContainer>
|
||||
<ul className="mt-3 w-96">
|
||||
{Object.keys(schema.types).map((t) => (
|
||||
<li
|
||||
key={"type" + t}
|
||||
className="odd:bg-black/50 flex justify-between p-2"
|
||||
>
|
||||
{t}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
title="Edit"
|
||||
className="no-default"
|
||||
onClick={() => selectTypeForEdit(t)}
|
||||
>
|
||||
<Icon
|
||||
icon="Anvil"
|
||||
className="anvil svg-olive-drab hover:svg-olive-drab-100 w-6 h-6"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
title="Delete"
|
||||
className="no-default"
|
||||
onClick={() => deleteType(t)}
|
||||
>
|
||||
<Icon
|
||||
icon="Trash"
|
||||
className="trash-can svg-red-700 hover:svg-red-500 w-6 h-6"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel basis-1/3">
|
||||
<div className="flex gap-2 mb-2">
|
||||
<button
|
||||
onClick={saveSchema}
|
||||
disabled={lastSaved === schema}
|
||||
>
|
||||
Save Schema
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-800"
|
||||
onClick={() => setSchema(lastSaved)}
|
||||
disabled={lastSaved === schema}
|
||||
>
|
||||
Discard Changes
|
||||
</button>
|
||||
</div>
|
||||
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
94
components/schema/schema-viewer.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { Truncate } from "@/components/Poppables/truncation";
|
||||
import { Accordion, AccordionContent } from "../../lib/accordion";
|
||||
import { FieldTypes, fieldTypesWithValues } from "./fieldtypes";
|
||||
|
||||
interface IProps {
|
||||
schema: Schema;
|
||||
onTypeClick?: (arg: string, arg1: TypeType) => void;
|
||||
}
|
||||
|
||||
export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
|
||||
const createValueLable = useCallback((field: FieldType) => {
|
||||
if (field.isConstant) {
|
||||
if (field.type === FieldTypes.dice) return "Auto-rolled";
|
||||
return "Constant value:";
|
||||
}
|
||||
switch (field.type) {
|
||||
case FieldTypes.type:
|
||||
return "Type:";
|
||||
case FieldTypes.dice:
|
||||
return "Dice:";
|
||||
case FieldTypes.select:
|
||||
return "Options:";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div className="whitespace-pre-wrap">{JSON.stringify(schema, null, 2)}</div> */}
|
||||
<div>
|
||||
<p className="font-bold text-lg">{schema.name}</p>
|
||||
<hr />
|
||||
<p className="font-bold italic">Templates</p>
|
||||
<ul>
|
||||
{Object.entries(schema.schema).map(([templateKey, template]) => (
|
||||
<li key={templateKey}>
|
||||
<p className="font-bold">{templateKey}</p>
|
||||
<p className="font-thin text-xs">{template.type}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<hr />
|
||||
<p className="font-bold italic">Types</p>
|
||||
<ul className="rounded-lg overflow-hidden grid">
|
||||
{Object.entries(schema.types).map(([typeKey, type]) => (
|
||||
<li
|
||||
key={"type viewer" + typeKey}
|
||||
// onClick={() => onTypeClick && onTypeClick(typeKey, type)}
|
||||
data-clickable={!!onTypeClick}
|
||||
className="odd:bg-black/50 p-2 group overflow-hidden"
|
||||
>
|
||||
<Accordion
|
||||
title={
|
||||
<p className="group-data-[expanded]/controlled:mb-2 transition-all font-bold">
|
||||
{typeKey}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<AccordionContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(type).map(([fieldKey, field]) => (
|
||||
<div
|
||||
key={"field viewer" + fieldKey}
|
||||
className="rounded-lg border border-olive-drab p-2"
|
||||
>
|
||||
<p className="font-bold">{fieldKey}</p>
|
||||
<p className="font-thin capitalize text-xs">
|
||||
{field.type}
|
||||
</p>
|
||||
<p className="font-thin capitalize text-xs">
|
||||
Maximum entries:{" "}
|
||||
{field.limit === 0 ? "unlimited " : field.limit}
|
||||
</p>
|
||||
{(field.isConstant ||
|
||||
fieldTypesWithValues.includes(field.type)) && (
|
||||
<p className="font-thin capitalize text-xs">
|
||||
{createValueLable(field)}{" "}
|
||||
<Truncate>{field.value}</Truncate>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</Accordion>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
75
components/schema/template-editor.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { useObjectStateWrapper } from "@/hooks/useObjectState";
|
||||
import { TEMPLATE_TYPES } from "@/constants/TemplateTypes";
|
||||
import { SchemaEditAtom } from "@/recoil/atoms/schema";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { Icon } from "@/components/Icon";
|
||||
|
||||
interface IProps {
|
||||
templateKey: string;
|
||||
update: (arg0: string, arg1: Template) => void;
|
||||
template: Template;
|
||||
}
|
||||
|
||||
export const TemplateEditor: FC<IProps> = (
|
||||
{ templateKey, update, template },
|
||||
) => {
|
||||
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
||||
const updateTemplate = useCallback(
|
||||
(t: Template | ((arg: Template) => Template)) => {
|
||||
update(templateKey, typeof t === "function" ? t(template) : t);
|
||||
},
|
||||
[templateKey, update, template],
|
||||
);
|
||||
|
||||
const { bindProperty } = useObjectStateWrapper(
|
||||
template,
|
||||
updateTemplate,
|
||||
);
|
||||
|
||||
const deleteTemplate = useCallback(() => {
|
||||
setSchema((s: Schema) => {
|
||||
const templates = { ...s.schema };
|
||||
delete templates[templateKey];
|
||||
return {
|
||||
...s,
|
||||
schema: templates,
|
||||
};
|
||||
});
|
||||
}, [setSchema, templateKey]);
|
||||
return (
|
||||
<li className="odd:bg-black/50 p-2">
|
||||
<p className="font-bold">{templateKey}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<label className="w-min">
|
||||
Type:
|
||||
<input
|
||||
type="text"
|
||||
{...bindProperty("type", { disallowSpaces: true })}
|
||||
list="type-editor-type-list"
|
||||
/>
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map((k) => (
|
||||
<option key={"templatetype" + k} value={k}>{k}</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map((k) => (
|
||||
<option key={"schematype" + k} value={k}>{k}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
<textarea {...bindProperty("display")} cols={30} rows={10}></textarea>
|
||||
</div>
|
||||
<button
|
||||
className="no-default"
|
||||
onClick={deleteTemplate}
|
||||
>
|
||||
<Icon
|
||||
icon="Trash"
|
||||
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
104
components/schema/type-editor.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import {
|
||||
FC,
|
||||
FormEvent,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useObjectState } from "../../hooks/useObjectState";
|
||||
import { useInput } from "../../hooks/useInput";
|
||||
import { FieldEditor } from "./field-editor";
|
||||
import { FieldTypes } from "./fieldtypes";
|
||||
|
||||
interface IProps {
|
||||
name: string;
|
||||
saveType: (arg0: string, arg1: TypeType) => void;
|
||||
type?: TypeType;
|
||||
}
|
||||
|
||||
const constantProperties = ["metadata"];
|
||||
|
||||
export const TypeEditor: FC<PropsWithChildren<IProps>> = (
|
||||
{ saveType, name, type: passedType },
|
||||
) => {
|
||||
const {
|
||||
update: updateType,
|
||||
reset: resetType,
|
||||
state: type,
|
||||
setState: setType,
|
||||
} = useObjectState<TypeType>({});
|
||||
|
||||
const {
|
||||
value: propertyName,
|
||||
bind: bindPropertyName,
|
||||
reset: resetPropertyName,
|
||||
} = useInput("", { disallowSpaces: true });
|
||||
|
||||
const save = () => {
|
||||
saveType(name, type);
|
||||
resetType();
|
||||
};
|
||||
|
||||
const addField = useCallback((e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
updateType({
|
||||
[propertyName]: {
|
||||
type: FieldTypes.number,
|
||||
value: "",
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
},
|
||||
});
|
||||
resetPropertyName();
|
||||
}, [propertyName, updateType, resetPropertyName]);
|
||||
|
||||
const updateField = useCallback(
|
||||
(k: keyof typeof type) => (field: FieldType) => {
|
||||
updateType({ [k]: field });
|
||||
},
|
||||
[updateType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
passedType && setType(passedType);
|
||||
}, [passedType, setType]);
|
||||
|
||||
const deleteField = useCallback((name: string) => {
|
||||
setType((t) => {
|
||||
const fields = { ...t };
|
||||
delete fields[name];
|
||||
return fields;
|
||||
});
|
||||
}, [setType]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="subheader">
|
||||
{passedType ? "Editing" : "Creating"} type "{name}"
|
||||
</p>
|
||||
<form onSubmit={addField}>
|
||||
<input type="text" {...bindPropertyName} />
|
||||
<button disabled={!propertyName}>Add Field</button>
|
||||
</form>
|
||||
<ul className="rounded-lg overflow-hidden">
|
||||
{Object.entries(type).reverse().filter(([k]) =>
|
||||
!constantProperties.includes(k)
|
||||
).map(([key, value]) => (
|
||||
<FieldEditor
|
||||
key={"field-editor" + key}
|
||||
field={value}
|
||||
update={updateField(key)}
|
||||
fieldName={key}
|
||||
deleteField={deleteField}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<button onClick={save} disabled={!Object.keys(type).length}>
|
||||
Save Type
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
129
components/schema/value-field.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { ChangeEvent, FC, useRef } from "react";
|
||||
import { FieldTypeInput } from "./field-type-input";
|
||||
import { useInput } from "../../hooks/useInput";
|
||||
import { HelpPopper } from "../Poppables/help";
|
||||
import { FieldTypes } from "./fieldtypes";
|
||||
|
||||
interface IValueProps {
|
||||
type: FieldTypes;
|
||||
bind: InputBinder;
|
||||
}
|
||||
|
||||
const DICE_SIDES = [3, 4, 6, 8, 10, 12, 20, 100];
|
||||
|
||||
export const ValueField: FC<IValueProps> = ({ type, bind }) => {
|
||||
const { value: diceCount, bind: bindDiceCount } = useInput(1);
|
||||
const { value: diceSides, bind: bindDiceSides } = useInput("");
|
||||
|
||||
const diceInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
switch (type) {
|
||||
case FieldTypes.dice: {
|
||||
const onChange = (
|
||||
handler: (
|
||||
arg: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => void,
|
||||
) =>
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
handler(e);
|
||||
setTimeout(() => {
|
||||
if (!diceInputRef.current) return;
|
||||
e.target = diceInputRef.current;
|
||||
bind.onChange(e);
|
||||
}, 0);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<label className="w-min">
|
||||
Count:
|
||||
<input
|
||||
className="w-12"
|
||||
type="number"
|
||||
{...bindDiceCount}
|
||||
onChange={onChange(bindDiceCount.onChange)}
|
||||
/>
|
||||
</label>
|
||||
<label className="w-min">
|
||||
Sides:
|
||||
<select
|
||||
{...bindDiceSides}
|
||||
onChange={onChange(bindDiceSides.onChange)}
|
||||
>
|
||||
<option value=""></option>
|
||||
{DICE_SIDES.map((d) => (
|
||||
<option key={"dice sides" + d} value={"d" + d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<input
|
||||
ref={diceInputRef}
|
||||
className="hidden"
|
||||
type="text"
|
||||
name={bind.name}
|
||||
value={diceCount + diceSides}
|
||||
readOnly
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case FieldTypes.type:
|
||||
return <FieldTypeInput bind={bind} />;
|
||||
case FieldTypes.number:
|
||||
return (
|
||||
<label className="w-min">
|
||||
Value:<input className="w-16" type="number" {...bind} />
|
||||
</label>
|
||||
);
|
||||
case FieldTypes.text:
|
||||
return (
|
||||
<label className="w-min">
|
||||
Value:<input type="text" {...bind} />
|
||||
</label>
|
||||
);
|
||||
case FieldTypes.select:
|
||||
return (
|
||||
<>
|
||||
<label className="w-min">
|
||||
<div className="flex gap-2 items-center">
|
||||
Values:
|
||||
<HelpPopper>
|
||||
<p className="text-xs">
|
||||
A comma separated list (no spaces, spaces are reserved for
|
||||
values) of options that can be chosen while creating
|
||||
publications. Ex: earthquake,wind storm,fire tornado,rainbow.
|
||||
Alternatively, you can specify a display value and an actual
|
||||
value separated with a colon. This is useful for when you want
|
||||
to create a reference in a publication with a dropdown field.
|
||||
Ex: Rapid Fire:^core.weaponAbilities[name=rapid
|
||||
fire],Heavy:^core.weaponAbilities[name=heavy]
|
||||
</p>
|
||||
</HelpPopper>
|
||||
</div>
|
||||
<input type="text" {...bind} />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
case FieldTypes.any:
|
||||
return (
|
||||
<>
|
||||
<label className="w-min">
|
||||
<div className="flex gap-2 items-center">
|
||||
Type options:
|
||||
<HelpPopper>
|
||||
<p className="text-xs">
|
||||
A comma separated list (no spaces, spaces are reserved for
|
||||
values) of options that are names of types that can be
|
||||
selected when creating a publication, Ex: dice,number,text. Do
|
||||
not leave this blank, allowing for any type to be selected
|
||||
makes querying gross.
|
||||
</p>
|
||||
</HelpPopper>
|
||||
</div>
|
||||
<input type="text" {...bind} />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
25
constants/ReservedFields.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { FieldTypes } from "@/components/schema/fieldtypes";
|
||||
|
||||
export const RESERVED_FIELDS: Record<string, FieldType> = {
|
||||
maximum: {
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
type: FieldTypes.number,
|
||||
value: "",
|
||||
},
|
||||
minimum: {
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
type: FieldTypes.number,
|
||||
value: "",
|
||||
},
|
||||
relative: {
|
||||
isConstant: true,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
type: FieldTypes.text,
|
||||
value: "$",
|
||||
},
|
||||
};
|
95
constants/TemplateTypes.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { FieldTypes } from "@/components/schema/fieldtypes";
|
||||
|
||||
export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
||||
section: {
|
||||
name: {
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
type: FieldTypes.text,
|
||||
value: "",
|
||||
},
|
||||
body: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes["long text"],
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
steps: {
|
||||
steps: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes.type,
|
||||
value: "section",
|
||||
},
|
||||
},
|
||||
image: {
|
||||
name: {
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
type: FieldTypes.text,
|
||||
value: "",
|
||||
},
|
||||
link: {
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
type: FieldTypes.text,
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
list: {
|
||||
items: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes["long text"],
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
table_column: {
|
||||
name: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes.any,
|
||||
value: "",
|
||||
},
|
||||
value: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes.any,
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
table_row: {
|
||||
columns: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes.type,
|
||||
value: "tableColumn",
|
||||
},
|
||||
},
|
||||
table: {
|
||||
rows: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
minimum: 1,
|
||||
type: FieldTypes.type,
|
||||
value: "tableRow",
|
||||
},
|
||||
header: {
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 0,
|
||||
type: FieldTypes.type,
|
||||
value: "tableRow",
|
||||
},
|
||||
},
|
||||
};
|
7
hooks/types.d.ts
vendored
@ -1,7 +0,0 @@
|
||||
export type InputBinder = {
|
||||
name: string;
|
||||
value: string | number;
|
||||
onChange: (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
||||
) => void;
|
||||
};
|
@ -17,7 +17,7 @@ export const Portal: FC<PropsWithChildren<IProps>> = (
|
||||
document.body.appendChild(container);
|
||||
setContainer(container);
|
||||
return () => {
|
||||
document.body.removeChild(container);
|
||||
document.body.contains(container) && document.body.removeChild(container);
|
||||
};
|
||||
}, [className, el]);
|
||||
|
||||
|
@ -1,6 +1,15 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/i,
|
||||
issuer: /\.[jt]sx?$/,
|
||||
use: ["@svgr/webpack"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
2776
package-lock.json
generated
@ -15,9 +15,11 @@
|
||||
"next": "14.1.0",
|
||||
"prisma": "^5.11.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
"react-dom": "^18",
|
||||
"recoil": "^0.7.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `types` to the `Schema` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `Schema` ADD COLUMN `types` JSON NOT NULL;
|
@ -29,6 +29,7 @@ model Schema {
|
||||
|
||||
name String
|
||||
schema Json
|
||||
types Json
|
||||
version Int
|
||||
}
|
||||
|
||||
|
6
recoil/atoms/schema.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
export const SchemaEditAtom = atom<Schema>({
|
||||
key: "schema-edit",
|
||||
default: { name: "", types: {}, schema: {}, id: "" },
|
||||
});
|
37
types.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
// MD Parser
|
||||
type IdentifiedToken<M> = {
|
||||
metadata: M;
|
||||
children?: Token[];
|
||||
@ -54,3 +55,39 @@ type IdentifierRegistration = <N = Record<string, string>>(
|
||||
openTagRx?: RegExp,
|
||||
closeTagRx?: RegExp,
|
||||
) => void;
|
||||
|
||||
// Schema
|
||||
type MetadataType = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
type FieldType = {
|
||||
type: FieldTypes;
|
||||
value: string;
|
||||
isConstant: boolean;
|
||||
limit: number;
|
||||
minimum: number;
|
||||
};
|
||||
|
||||
type TypeType = Record<string, FieldType>;
|
||||
|
||||
type Template = {
|
||||
type: string;
|
||||
display: string;
|
||||
};
|
||||
|
||||
type Schema = {
|
||||
id: string;
|
||||
name: string;
|
||||
schema: Record<string, Template>;
|
||||
types: Record<string, TypeType>;
|
||||
};
|
||||
|
||||
// Input Binder
|
||||
type InputBinder = {
|
||||
name: string;
|
||||
value: string | number;
|
||||
onChange: (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
||||
) => void;
|
||||
};
|
||||
|