ported schema builder

This commit is contained in:
Emmaline Autumn 2024-03-19 14:49:50 -06:00
parent 56f0442d33
commit 3a5fe1911a
38 changed files with 4112 additions and 37 deletions

23
actions/Schemas/create.ts Normal file
View 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}`);
};

View File

@ -1,5 +1,5 @@
import { Sticky } from "@/lib/sticky";
import { prisma } from "@/prisma/prismaClient"; import { prisma } from "@/prisma/prismaClient";
import Link from "next/link";
export default async function GameSystem( export default async function GameSystem(
{ params: { id } }: { params: { id: string } }, { params: { id } }: { params: { id: string } },
@ -35,15 +35,25 @@ export default async function GameSystem(
<h1>{gameSystem?.name}</h1> <h1>{gameSystem?.name}</h1>
</section> </section>
<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> <ul>
{gameSystem?.schemas.map((schema) => ( {gameSystem?.schemas.map((schema) => (
<li key={schema.id}>{schema.name}</li> <li key={schema.id}>{schema.name}</li>
))} ))}
{!gameSystem?.schemas.length && (
<li>No schemas for {gameSystem?.name}</li>
)}
</ul> </ul>
</>
</section> </section>
<Sticky sidedness={-1}>
<h1>HELLO!</h1>
</Sticky>
</> </>
); );
} }

View 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>
</>
);
}

View File

@ -12,6 +12,9 @@
input { input {
@apply py-2 px-4 rounded-full dark:bg-mixed-200 bg-mixed-600 placeholder:text-dark-500; @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, h1,
h2, h2,
h3, h3,
@ -49,6 +52,9 @@
.btn-secondary { .btn-secondary {
@apply dark:text-primary-500 text-primary-100 py-4 px-6 font-bold text-lg; @apply dark:text-primary-500 text-primary-100 py-4 px-6 font-bold text-lg;
} }
.btn-small {
@apply px-2 py-1;
}
.p { .p {
@apply pb-1; @apply pb-1;
} }

View File

@ -10,6 +10,7 @@ import {
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import Link from "next/link"; import Link from "next/link";
import { DevToolboxContextProvider } from "@/components/devtools/context"; import { DevToolboxContextProvider } from "@/components/devtools/context";
import { RecoilRootClient } from "@/components/recoilRoot";
const roboto = Roboto({ subsets: ["latin"], weight: "400" }); const roboto = Roboto({ subsets: ["latin"], weight: "400" });
@ -72,6 +73,7 @@ export default function RootLayout({
))} ))}
</ul> </ul>
</nav> </nav>
<RecoilRootClient>
<DevToolboxContextProvider <DevToolboxContextProvider
isDev={process.env.NODE_ENV !== "production"} isDev={process.env.NODE_ENV !== "production"}
> >
@ -79,6 +81,7 @@ export default function RootLayout({
{children} {children}
</main> </main>
</DevToolboxContextProvider> </DevToolboxContextProvider>
</RecoilRootClient>
<div id="root-portal"></div> <div id="root-portal"></div>
</body> </body>
</html> </html>

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View File

@ -0,0 +1,6 @@
.page {
transition:
transform 500ms,
opacity 300ms,
z-index 0ms 500ms
}

View 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
View 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} />;
};

View 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>
);
};

View 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
View 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>
);
};

View File

@ -0,0 +1,5 @@
"use client";
import { RecoilRoot } from "recoil";
export const RecoilRootClient = RecoilRoot;

View 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:&nbsp;
<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&apos;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>
);
};

View 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>
);
};

View 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
View 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>
);
};

View 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>
</>
);
};

View 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>
);
};

View 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 &quot;{name}&quot;
</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>
);
};

View 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:&nbsp;
<input
className="w-12"
type="number"
{...bindDiceCount}
onChange={onChange(bindDiceCount.onChange)}
/>
</label>
<label className="w-min">
Sides:&nbsp;
<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 <></>;
}
};

View 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: "$",
},
};

View 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
View File

@ -1,7 +0,0 @@
export type InputBinder = {
name: string;
value: string | number;
onChange: (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
) => void;
};

View File

@ -17,7 +17,7 @@ export const Portal: FC<PropsWithChildren<IProps>> = (
document.body.appendChild(container); document.body.appendChild(container);
setContainer(container); setContainer(container);
return () => { return () => {
document.body.removeChild(container); document.body.contains(container) && document.body.removeChild(container);
}; };
}, [className, el]); }, [className, el]);

View File

@ -1,6 +1,15 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
webpack(config) {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ["@svgr/webpack"],
});
return config;
},
}; };
export default nextConfig; export default nextConfig;

2776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,11 @@
"next": "14.1.0", "next": "14.1.0",
"prisma": "^5.11.0", "prisma": "^5.11.0",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18",
"recoil": "^0.7.7"
}, },
"devDependencies": { "devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",

View File

@ -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;

View File

@ -29,6 +29,7 @@ model Schema {
name String name String
schema Json schema Json
types Json
version Int version Int
} }

6
recoil/atoms/schema.ts Normal file
View 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
View File

@ -1,3 +1,4 @@
// MD Parser
type IdentifiedToken<M> = { type IdentifiedToken<M> = {
metadata: M; metadata: M;
children?: Token[]; children?: Token[];
@ -54,3 +55,39 @@ type IdentifierRegistration = <N = Record<string, string>>(
openTagRx?: RegExp, openTagRx?: RegExp,
closeTagRx?: RegExp, closeTagRx?: RegExp,
) => void; ) => 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;
};