Compare commits
17 Commits
main
...
729aba68ce
Author | SHA1 | Date | |
---|---|---|---|
729aba68ce | |||
da044ac9d5 | |||
b9b744e97f | |||
9838324b35 | |||
9e2184352f | |||
0f100bba3d | |||
2b2b88f970 | |||
3656fc42ac | |||
7839dbbc1c | |||
e6d8583220 | |||
f8fa3ec924 | |||
269a844a68 | |||
7fc76d2781 | |||
1664d3bc7f | |||
e6880af3ee | |||
71bf62b622 | |||
3a5fe1911a |
@@ -4,6 +4,9 @@ WORKDIR /ttc
|
||||
|
||||
ADD . .
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV AUTH_TRUST_HOST true
|
||||
|
||||
RUN npm i
|
||||
RUN npm run build
|
||||
|
||||
|
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}`);
|
||||
};
|
20
actions/Schemas/find.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/prisma/prismaClient";
|
||||
|
||||
export const findSchema = async (id: string) => {
|
||||
const schema = await prisma.schema.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
gameSystem: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return schema;
|
||||
};
|
12
actions/auth/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
"use server";
|
||||
import { signIn, signOut } from "@/auth";
|
||||
|
||||
export const signInWithDiscord = async () => {
|
||||
await signIn("discord");
|
||||
};
|
||||
|
||||
export const signInWithCreds = async (formData: FormData) => {
|
||||
await signIn("credentials", formData);
|
||||
};
|
||||
|
||||
export const signOutOfApp = () => signOut();
|
3
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
@@ -5,12 +5,5 @@ import { FC, use } from "react";
|
||||
|
||||
export const HomeClient: FC<{ body: Promise<string> }> = ({ body }) => {
|
||||
const text = use(body);
|
||||
|
||||
return (
|
||||
<TTCMD
|
||||
body={text}
|
||||
parserId="home"
|
||||
title="home"
|
||||
/>
|
||||
);
|
||||
return <TTCMD body={text} parserId="home" title="home" />;
|
||||
};
|
||||
|
@@ -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>
|
||||
<ul>
|
||||
{gameSystem?.schemas.map((schema) => (
|
||||
<li key={schema.id}>{schema.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -9,8 +9,12 @@
|
||||
body {
|
||||
@apply dark:bg-mixed-100 bg-primary-600;
|
||||
}
|
||||
input {
|
||||
@apply py-2 px-4 rounded-full dark:bg-mixed-200 bg-mixed-600 placeholder:text-dark-500;
|
||||
input,
|
||||
select {
|
||||
@apply py-2 px-4 rounded-lg 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,
|
||||
@@ -43,12 +47,18 @@
|
||||
/* @apply bg-mixed-200 rounded-3xl p-6 shadow-2xl */;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
.btn-primary {
|
||||
@apply dark:bg-primary-500 bg-primary-100 py-4 px-6 dark:text-mixed-100 text-white rounded-full font-bold text-lg;
|
||||
@apply dark:bg-primary-500 bg-primary-100 py-4 px-6 dark:text-mixed-100 text-white font-bold text-lg btn;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
@@ -83,6 +93,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.fade-toast {
|
||||
animation: fadeOut 300ms forwards;
|
||||
}
|
||||
|
||||
.separated-list > li:not(:last-child) {
|
||||
@apply border-b border-mixed-600 w-full;
|
||||
}
|
||||
|
||||
.fade-menu {
|
||||
animation: fadeIn 100ms forwards;
|
||||
}
|
||||
.fade-menu[data-closing="true"] {
|
||||
animation: fadeOut 100ms forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes identifier {
|
||||
}
|
||||
|
||||
@@ -92,3 +119,20 @@
|
||||
list-style: square;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,11 @@ import {
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import { DevToolboxContextProvider } from "@/components/devtools/context";
|
||||
import { RecoilRootClient } from "@/components/recoilRoot";
|
||||
import { JotaiProvider } from "@/components/jotaiProvider";
|
||||
import { Toaster } from "@/components/toast";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { User } from "@/components/user/index";
|
||||
|
||||
const roboto = Roboto({ subsets: ["latin"], weight: "400" });
|
||||
|
||||
@@ -53,34 +58,44 @@ export default function RootLayout({
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={roboto.className + " flex min-h-[100vh]"}>
|
||||
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
|
||||
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
||||
<Link href="/">Tabletop Commander</Link>
|
||||
</h1>
|
||||
<ul className="my-6 flex flex-col gap-6">
|
||||
{navItems.map((n) => (
|
||||
<li key={"nav-item" + n.text}>
|
||||
<Link
|
||||
href={n.to}
|
||||
className="flex items-center gap-2 group hover:text-purple-300 transition-colors"
|
||||
>
|
||||
<n.icon className="w-6 h-6 group-hover:fill-purple-300 transition-colors" />
|
||||
{n.text}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<DevToolboxContextProvider
|
||||
isDev={process.env.NODE_ENV !== "production"}
|
||||
>
|
||||
<main className="p-8 w-full overflow-visible">
|
||||
{children}
|
||||
</main>
|
||||
</DevToolboxContextProvider>
|
||||
<div id="root-portal"></div>
|
||||
</body>
|
||||
<SessionProvider>
|
||||
<body className={roboto.className + " flex min-h-[100vh]"}>
|
||||
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
|
||||
<div className="flex flex-col h-full">
|
||||
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
||||
<Link href="/">Tabletop Commander</Link>
|
||||
</h1>
|
||||
<ul className="my-6 flex flex-col gap-6">
|
||||
{navItems.map((n) => (
|
||||
<li key={"nav-item" + n.text}>
|
||||
<Link
|
||||
href={n.to}
|
||||
className="flex items-center gap-2 group hover:text-purple-300 transition-colors"
|
||||
>
|
||||
<n.icon className="w-6 h-6 group-hover:fill-purple-300 transition-colors" />
|
||||
{n.text}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-auto">
|
||||
<User />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<RecoilRootClient>
|
||||
<JotaiProvider>
|
||||
<DevToolboxContextProvider
|
||||
isDev={process.env.NODE_ENV !== "production"}
|
||||
>
|
||||
<main className="p-8 w-full overflow-visible">{children}</main>
|
||||
<Toaster />
|
||||
</DevToolboxContextProvider>
|
||||
</JotaiProvider>
|
||||
</RecoilRootClient>
|
||||
<div id="root-portal"></div>
|
||||
</body>
|
||||
</SessionProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@ export default function Home() {
|
||||
<h2 className="strapline">Tabletop Commander</h2>
|
||||
<h1>How does it work?</h1>
|
||||
</section>
|
||||
{
|
||||
/* <section className="w-full my-6">
|
||||
{/* <section className="w-full my-6">
|
||||
<div className="card">
|
||||
<p>
|
||||
Tabletop Commander (TC) is a rules-and-tools app for tabletop games
|
||||
@@ -131,8 +130,7 @@ export default function Home() {
|
||||
will be done. If this makes it to production, tell Emma she forgot to
|
||||
turn the home page into magic
|
||||
</cite>
|
||||
</section> */
|
||||
}
|
||||
</section> */}
|
||||
<Suspense fallback={<MDSkeletonLoader />}>
|
||||
<HomeClient body={body} />
|
||||
</Suspense>
|
||||
|
19
app/sign-in/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { auth } from "@/auth";
|
||||
import SignIn from "@/components/signIn";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
async function SignInUp() {
|
||||
const session = await auth();
|
||||
|
||||
if (session?.user) redirect("/");
|
||||
|
||||
return (
|
||||
<div className="grid place-items-center h-full">
|
||||
<div>
|
||||
<SignIn />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignInUp;
|
5
assets/icons/Anvil Icon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect class="prefix__anvil-base" x="4.5" y="9.5" width="10" height="2" rx=".5" stroke="#000" />
|
||||
<path class="prefix__anvil-body" d="M6 1H2c1 1 3.5 1.5 5 2s1.543 1.292 0 3L6 7v1h7V7s-1.5-1-2-2 0-2.5 4-3V.5H6V1z"
|
||||
stroke="#000" />
|
||||
</svg>
|
After Width: | Height: | Size: 342 B |
5
assets/icons/Discord.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-discord"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
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 |
27
assets/icons/Help Icon.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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">
|
||||
<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: 2.9 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 |
70
auth/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import Discord from "next-auth/providers/discord";
|
||||
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
providers: [
|
||||
Discord({
|
||||
clientId: process.env.DISCORD_CLIENT_ID,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||
}),
|
||||
Credentials({
|
||||
credentials: {
|
||||
email: {},
|
||||
password: {},
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
let user = null;
|
||||
|
||||
const pwHash = await saltAndHashPassword(
|
||||
credentials.password as string
|
||||
);
|
||||
user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: credentials.email as string,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
username: true,
|
||||
passwordHash: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
email: credentials.email as string,
|
||||
passwordHash: pwHash,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
user.passwordHash = null;
|
||||
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
],
|
||||
adapter: PrismaAdapter(prisma),
|
||||
});
|
||||
async function saltAndHashPassword(password: string) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
return hash;
|
||||
}
|
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;
|
184
components/Icon/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { FC } from "react";
|
||||
|
||||
const Help = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#fff"
|
||||
fill="#fff"
|
||||
height="28"
|
||||
width="28"
|
||||
>
|
||||
<circle
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
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>
|
||||
);
|
||||
|
||||
const Trash = () => (
|
||||
<svg
|
||||
className="trash-can"
|
||||
width="35"
|
||||
height="30"
|
||||
viewBox="0 0 22 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 7H20V27C20 28.1046 19.1046 29 18 29H4C2.89543 29 2 28.1046 2 27V7Z"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M6 11L6 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M11 11V24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M16 11V24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
className="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="currentColor"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
className="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="currentColor"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Trash_hover = () => (
|
||||
<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="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M7 16L7 29"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 16V29"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M17 16V29"
|
||||
stroke="currentColor"
|
||||
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="currentColor"
|
||||
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="currentColor"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Anvil = () => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
className="prefix__anvil-base"
|
||||
x="4.5"
|
||||
y="9.5"
|
||||
width="10"
|
||||
height="2"
|
||||
rx=".5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
className="prefix__anvil-body"
|
||||
d="M6 1H2c1 1 3.5 1.5 5 2s1.543 1.292 0 3L6 7v1h7V7s-1.5-1-2-2 0-2.5 4-3V.5H6V1z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Discord = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
className="bi bi-discord"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const library = {
|
||||
Help,
|
||||
Trash,
|
||||
Trash_hover,
|
||||
Anvil,
|
||||
Discord,
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
icon: keyof typeof library;
|
||||
}
|
||||
|
||||
export const Icon: FC<IProps> = ({ className, icon }) => {
|
||||
const ICON = library[icon];
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
<ICON />
|
||||
</span>
|
||||
);
|
||||
};
|
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>
|
||||
);
|
||||
};
|
7
components/jotaiProvider.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Provider } from "jotai";
|
||||
|
||||
export function JotaiProvider(props: React.PropsWithChildren) {
|
||||
return <Provider>{props.children}</Provider>;
|
||||
}
|
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 min-w-min"
|
||||
type="number"
|
||||
{...bindProperty("minimum")}
|
||||
/>
|
||||
</label>
|
||||
<label className="w-min">
|
||||
Limit:
|
||||
<input className="w-12 min-w-min" 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,
|
||||
];
|
227
components/schema/index.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
"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";
|
||||
import { prisma } from "@/prisma/prismaClient";
|
||||
|
||||
export const SchemaBuilder: FC = () => {
|
||||
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
||||
const resetSchema = useResetRecoilState(SchemaEditAtom);
|
||||
const { update: updateSchema, bindProperty: bindSchemaProperty } =
|
||||
useObjectStateWrapper<Schema>(schema, setSchema);
|
||||
|
||||
const { schemaId, gameSystemId } = useParams<{
|
||||
schemaId: string;
|
||||
gameSystemId?: 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 () => {
|
||||
// "use server";
|
||||
// setLastSaved(schema);
|
||||
// await prisma.schema.upsert({
|
||||
// where: { id: schema.id },
|
||||
// update: { ...schema },
|
||||
// create: {
|
||||
// name: schema.name,
|
||||
// schema: schema.schema,
|
||||
// types: schema.types,
|
||||
// version: 0,
|
||||
// gameSystemId,
|
||||
// },
|
||||
// });
|
||||
}, [schema, gameSystemId]);
|
||||
|
||||
const selectTypeForEdit = useCallback((typeKey: string) => {
|
||||
setSelectedType(typeKey);
|
||||
setPageNumber(1);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
value: schemaFieldName,
|
||||
bind: bindSchemaFieldName,
|
||||
reset: resetSchemaFieldName,
|
||||
} = useInput("", { disallowSpaces: true });
|
||||
const addSchemaField = useCallback(() => {
|
||||
updateSchema((s) => ({
|
||||
schema: {
|
||||
...s.schema,
|
||||
[schemaFieldName]: {
|
||||
display: "",
|
||||
type: FieldTypes.any,
|
||||
},
|
||||
},
|
||||
}));
|
||||
resetSchemaFieldName();
|
||||
}, [resetSchemaFieldName, 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" {...bindSchemaFieldName} />
|
||||
<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
|
||||
className="btn btn-small bg-green-800"
|
||||
onClick={saveSchema}
|
||||
disabled={lastSaved === schema}
|
||||
>
|
||||
Save Schema
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-800 btn btn-small"
|
||||
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 <></>;
|
||||
}
|
||||
};
|
34
components/signIn.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { signInWithCreds, signInWithDiscord } from "@/actions/auth";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export default function SignIn() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<form action={signInWithCreds} className="flex flex-col gap-2">
|
||||
<input
|
||||
className="w-full"
|
||||
placeholder="email"
|
||||
type="email"
|
||||
name="email"
|
||||
/>
|
||||
<input
|
||||
className="w-full"
|
||||
placeholder="password"
|
||||
type="password"
|
||||
name="password"
|
||||
/>
|
||||
</form>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div>
|
||||
<div className="dark:text-dark-500 text-primary-600 ">or</div>
|
||||
<div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div>
|
||||
</div>
|
||||
<form action={signInWithDiscord}>
|
||||
<button className="w-full p-2 bg-[#816ab1] rounded-lg" type="submit">
|
||||
<Icon icon="Discord" className="mr-4 inline-block" />
|
||||
Sign in with Discord
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
111
components/toast/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { Portal } from "@/lib/portal/components";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type toastMessage = {
|
||||
msg: ReactNode;
|
||||
type?: "error" | "default";
|
||||
fading: boolean;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
type IDToastMessage = toastMessage & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const toastAtom = atom<IDToastMessage[]>([]);
|
||||
|
||||
export function useToast() {
|
||||
const [_, setToasts] = useAtom(toastAtom);
|
||||
|
||||
const createToast = useCallback(
|
||||
(t: toastMessage) => {
|
||||
const idd = { ...t, id: crypto.randomUUID() };
|
||||
setToasts((toasts) => {
|
||||
return [...toasts, idd];
|
||||
});
|
||||
|
||||
return idd;
|
||||
},
|
||||
[setToasts]
|
||||
);
|
||||
|
||||
const clearToast = useCallback(
|
||||
(t: toastMessage) => setToasts((toasts) => toasts.filter((to) => to != t)),
|
||||
[setToasts]
|
||||
);
|
||||
|
||||
return {
|
||||
createToast,
|
||||
clearToast,
|
||||
};
|
||||
}
|
||||
|
||||
export function Toaster() {
|
||||
const [toasts, setToasts] = useAtom(toastAtom);
|
||||
|
||||
const clearToast = useCallback(
|
||||
(t: toastMessage) => {
|
||||
setToasts((toasts) => {
|
||||
return toasts.filter((to) => to !== t);
|
||||
});
|
||||
},
|
||||
[setToasts]
|
||||
);
|
||||
|
||||
if (!toasts.length) return <></>;
|
||||
return (
|
||||
<Portal>
|
||||
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 max-w-[95vw] flex flex-col gap-4">
|
||||
{toasts.map((t) => (
|
||||
<Toast key={"toast " + t.id} toast={t} clearToast={clearToast} />
|
||||
))}
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function Toast(props: {
|
||||
toast: toastMessage;
|
||||
clearToast: (t: toastMessage) => void;
|
||||
}) {
|
||||
const { toast, clearToast } = props;
|
||||
const [fading, setFading] = useState(false);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setFading(true);
|
||||
setTimeout(() => {
|
||||
clearToast(toast);
|
||||
}, 300);
|
||||
}, [clearToast, toast]);
|
||||
|
||||
const fadeOut = useCallback(() => {
|
||||
setTimeout(clear, toast.duration ?? 3000);
|
||||
}, [clear, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toast.fading) return;
|
||||
fadeOut();
|
||||
}, [fadeOut, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-fading={fading}
|
||||
data-type={toast.type}
|
||||
className="relative p-6 px-16 toast data-[fading=true]:fade-toast rounded-md bg-mixed-300 data-[type=error]:bg-red-900 border-2 border-mixed-400 data-[type=error]:border-red-700"
|
||||
>
|
||||
{toast.msg}
|
||||
{!toast.fading && (
|
||||
<button
|
||||
className="top-2 right-2 text-xs absolute"
|
||||
onClick={() => clear()}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -2,7 +2,7 @@ import { createElements } from "@/lib/tcmd";
|
||||
import React, { FC, Suspense, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { MDSkeletonLoader } from "../loader";
|
||||
import { DevTool } from "../devtools/Toolbox";
|
||||
import { DevTool } from "../devtools/DevTool";
|
||||
|
||||
interface Props {
|
||||
body: string;
|
||||
|
33
components/user/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { auth } from "@/auth";
|
||||
import { UserCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { FC } from "react";
|
||||
import { UserMenu } from "./menu";
|
||||
|
||||
export const User: FC = async () => {
|
||||
const session = await auth();
|
||||
return (
|
||||
<UserMenu signedIn={!!session?.user}>
|
||||
<div className="flex gap-2 items-center">
|
||||
{session?.user?.image ? (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt="user avatar"
|
||||
className="rounded-full w-12"
|
||||
/>
|
||||
) : (
|
||||
<span className="w-12 h-12 inline-block">
|
||||
<UserCircleIcon className="w-full h-full" />
|
||||
</span>
|
||||
)}
|
||||
{session?.user?.name ? (
|
||||
<span>Hey there, {session.user.name}!</span>
|
||||
) : (
|
||||
<a className="block flex-grow h-full" href="/sign-in">
|
||||
Sign In
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</UserMenu>
|
||||
);
|
||||
};
|
50
components/user/menu.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { signOutOfApp } from "@/actions/auth";
|
||||
import { FC, PropsWithChildren, useCallback, useState } from "react";
|
||||
|
||||
export const UserMenu: FC<PropsWithChildren<{ signedIn: boolean }>> = ({
|
||||
children,
|
||||
signedIn,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [closing, setClosing] = useState(true);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setClosing((c) => !c);
|
||||
setTimeout(
|
||||
() => {
|
||||
setVisible((v) => !v);
|
||||
},
|
||||
visible ? 100 : 0
|
||||
);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={signedIn ? toggle : undefined}
|
||||
className="relative bg-mixed-200 p-2 rounded-lg cursor-pointer w-[220px]"
|
||||
>
|
||||
{visible && (
|
||||
<div
|
||||
data-closing={closing}
|
||||
className="absolute bottom-full left-0 right-0 fade-menu"
|
||||
>
|
||||
<ul className="separated-list w-full">
|
||||
<li>
|
||||
<a className="block p-2" href="/profile">
|
||||
Profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button className="p-2" onClick={() => signOutOfApp()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
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;
|
||||
};
|
89
lib/dice.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export class Dice {
|
||||
private count!: number;
|
||||
private sides!: number;
|
||||
|
||||
constructor(dice: string) {
|
||||
this.parseDice(dice);
|
||||
}
|
||||
|
||||
private parseDice(dice: string) {
|
||||
const [c, s] = dice.split(/[dD]/);
|
||||
this.count = Number(c);
|
||||
this.sides = Number(s);
|
||||
}
|
||||
|
||||
public roll() {
|
||||
let total = 0;
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
total += this.rollSingle();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private rollSingle() {
|
||||
return Math.ceil(Math.random() * this.sides);
|
||||
}
|
||||
|
||||
public rollAvg() {
|
||||
return this.roll() / this.count;
|
||||
}
|
||||
|
||||
public rollTimes(times: number) {
|
||||
let total = 0;
|
||||
for (let i = 0; i < times; i++) {
|
||||
total += this.roll();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public rollTimesAvg(times: number) {
|
||||
return this.rollTimes(times) / times;
|
||||
}
|
||||
|
||||
public getNormalizedRollDistribution(): Record<number, number> {
|
||||
const distribution: Record<number, number> = this.computeDistribution();
|
||||
|
||||
// Normalize the distribution
|
||||
const totalOutcomes = Math.pow(this.sides, this.count);
|
||||
for (const sum in distribution) {
|
||||
if (distribution.hasOwnProperty(sum)) {
|
||||
distribution[sum] /= totalOutcomes;
|
||||
}
|
||||
}
|
||||
|
||||
return distribution;
|
||||
}
|
||||
|
||||
public getRollDistribution(): Record<number, number> {
|
||||
return this.computeDistribution();
|
||||
}
|
||||
|
||||
private computeDistribution(): Record<number, number> {
|
||||
const distribution: Record<number, number> = {};
|
||||
|
||||
// Helper function to compute the sum distribution for given number of dice
|
||||
const computeSumDistribution = (
|
||||
dice: number,
|
||||
sides: number,
|
||||
currentSum: number,
|
||||
currentDice: number
|
||||
): void => {
|
||||
if (currentDice === dice) {
|
||||
distribution[currentSum] = (distribution[currentSum] || 0) + 1;
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i <= sides; i++) {
|
||||
computeSumDistribution(dice, sides, currentSum + i, currentDice + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Compute distribution
|
||||
computeSumDistribution(this.count, this.sides, 0, 0);
|
||||
|
||||
return distribution;
|
||||
}
|
||||
// STATIC
|
||||
static isDice(d: string) {
|
||||
return /\d+[dD]\d+/.test(d);
|
||||
}
|
||||
}
|
@@ -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]);
|
||||
|
||||
|
54
lib/tcmd/Resolver.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { PublicationAtom } from "@/recoil/atoms/publication";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { TTCQueryResolver } from "../ttcQuery/TTCResolvers";
|
||||
|
||||
export function Resolver({ resolver }: { resolver: string }) {
|
||||
const parser = useRecoilValue(PublicationAtom);
|
||||
const [res] = useState(new TTCQueryResolver(parser));
|
||||
const [content, setContent] = useState("");
|
||||
useEffect(() => {
|
||||
setContent(res.resolve(resolver));
|
||||
}, [resolver, res]);
|
||||
return <span>{content}</span>;
|
||||
}
|
||||
|
||||
export function OnDemandResolver({
|
||||
resolver,
|
||||
template,
|
||||
title,
|
||||
}: {
|
||||
resolver: string;
|
||||
template: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const parser = useRecoilValue(PublicationAtom);
|
||||
const res = useRef(new TTCQueryResolver(parser));
|
||||
const [content, setContent] = useState("");
|
||||
const generateContent = useCallback(() => {
|
||||
let content = template;
|
||||
const stackIdxs = Array.from(new Set(template.match(/\$\d/g)));
|
||||
for (const idx of stackIdxs) {
|
||||
let thing = res.current.getFromStack(idx);
|
||||
if (Array.isArray(thing)) thing = thing.at(0);
|
||||
if (typeof thing === "function") thing = thing();
|
||||
content = content.replaceAll(idx, thing as string);
|
||||
}
|
||||
setContent(content);
|
||||
}, [res, template]);
|
||||
|
||||
const resolve = useCallback(() => {
|
||||
res.current.resolve(resolver, true);
|
||||
generateContent();
|
||||
}, [res, resolver, generateContent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onMouseDown={() => setContent("")} onClick={resolve}>
|
||||
{title ?? "Resolve"}
|
||||
</button>
|
||||
<br />
|
||||
{!!content && <span>{content}</span>}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -3,12 +3,13 @@ import Link from "next/link";
|
||||
import React, { Fragment } from "react";
|
||||
import { Poppable } from "../poppables/components/poppable";
|
||||
import { Accordion, AccordionContent } from "../accordion";
|
||||
import { OnDemandResolver, Resolver } from "./Resolver";
|
||||
|
||||
export const TokenRenderers = new Map<string, TokenRenderer<any>>();
|
||||
|
||||
export function buildIdentifierMap(): [
|
||||
TokenIdentifierMap,
|
||||
IdentifierRegistration,
|
||||
IdentifierRegistration
|
||||
] {
|
||||
const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();
|
||||
|
||||
@@ -16,7 +17,7 @@ export function buildIdentifierMap(): [
|
||||
type: string,
|
||||
match: RegExp,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||
renderFunction: TokenRenderer<M>,
|
||||
renderFunction: TokenRenderer<M>
|
||||
): void;
|
||||
function registerIdentifier<M>(
|
||||
type: string,
|
||||
@@ -24,7 +25,7 @@ export function buildIdentifierMap(): [
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||
renderFunction: TokenRenderer<M>,
|
||||
openTagRx: RegExp,
|
||||
closeTagRx: RegExp,
|
||||
closeTagRx: RegExp
|
||||
): void;
|
||||
function registerIdentifier<M = Record<string, string>>(
|
||||
type: string,
|
||||
@@ -32,7 +33,7 @@ export function buildIdentifierMap(): [
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||
renderFunction: TokenRenderer<M>,
|
||||
openTagRx?: RegExp,
|
||||
closeTagRx?: RegExp,
|
||||
closeTagRx?: RegExp
|
||||
) {
|
||||
TokenIdentifiers.set(type, {
|
||||
rx: match,
|
||||
@@ -45,17 +46,18 @@ export function buildIdentifierMap(): [
|
||||
|
||||
return { ...token, ...identifiedToken } as Token<M>;
|
||||
},
|
||||
search: openTagRx && closeTagRx
|
||||
? (s, start, end) => {
|
||||
return search(
|
||||
s,
|
||||
start,
|
||||
end,
|
||||
new RegExp(openTagRx, "g"),
|
||||
new RegExp(closeTagRx, "g"),
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
search:
|
||||
openTagRx && closeTagRx
|
||||
? (s, start, end) => {
|
||||
return search(
|
||||
s,
|
||||
start,
|
||||
end,
|
||||
new RegExp(openTagRx, "g"),
|
||||
new RegExp(closeTagRx, "g")
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
TokenRenderers.set(type, renderFunction);
|
||||
}
|
||||
@@ -67,7 +69,6 @@ export const buildOnlyDefaultElements = () => {
|
||||
const [TokenIdentifiers, registerIdentifier] = buildIdentifierMap();
|
||||
|
||||
TokenRenderers.set("text", (t: Token<any>) => {
|
||||
debugger;
|
||||
return (
|
||||
<span className="whitespace-pre-wrap">
|
||||
{t.content.replaceAll(/\\n ?/g, "\n")}
|
||||
@@ -105,20 +106,26 @@ export const buildOnlyDefaultElements = () => {
|
||||
const { children, metadata } = token;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
"--grid-cols": metadata.columns,
|
||||
} as React.CSSProperties}
|
||||
style={
|
||||
{
|
||||
"--grid-cols": metadata.columns,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="grid grid-cols-dynamic gap-x-8 mb-6"
|
||||
>
|
||||
{children?.map((c) => {
|
||||
const Comp = c.metadata.span ? Fragment : "div";
|
||||
return <Comp className="p" key={c.uuid}>{c.render(c)}</Comp>;
|
||||
return (
|
||||
<Comp className="p" key={c.uuid}>
|
||||
{c.render(c)}
|
||||
</Comp>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
/(?<!\/)(?:\[\])+/g,
|
||||
/\/\[\]/g,
|
||||
/(?<![\/\?])(?:\[\])+/g,
|
||||
/\/\[\]/g
|
||||
);
|
||||
|
||||
// card
|
||||
@@ -149,18 +156,21 @@ export const buildOnlyDefaultElements = () => {
|
||||
return (
|
||||
<div
|
||||
data-block={!!metadata.isBlock}
|
||||
style={{
|
||||
"--v-span": metadata.span || 1,
|
||||
} as React.CSSProperties}
|
||||
style={
|
||||
{
|
||||
"--v-span": metadata.span || 1,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="data-[block=false]:card data-[block=false]:mb-6 col-span-2"
|
||||
>
|
||||
{children?.map((e) => <Fragment key={e.uuid}>{e.render(e)}
|
||||
</Fragment>)}
|
||||
{children?.map((e) => (
|
||||
<Fragment key={e.uuid}>{e.render(e)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
/\[\[/g,
|
||||
/\]\]/g,
|
||||
/\]\]/g
|
||||
);
|
||||
|
||||
// fenced code block
|
||||
@@ -182,16 +192,16 @@ export const buildOnlyDefaultElements = () => {
|
||||
{token.content}
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// list
|
||||
registerIdentifier(
|
||||
"list",
|
||||
/^\s*-\s([\s\S]*?)\n\n/gm,
|
||||
/(?<=\n\n?|^) *-\s([\s\S]*?)(?=\n\n|$)/g,
|
||||
(s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse list",
|
||||
content: s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse list",
|
||||
raw: s,
|
||||
metadata: {
|
||||
initialDepth:
|
||||
@@ -221,17 +231,58 @@ export const buildOnlyDefaultElements = () => {
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ordered-list
|
||||
registerIdentifier(
|
||||
"ordered-list",
|
||||
/(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g,
|
||||
(s, rx) => {
|
||||
return {
|
||||
content:
|
||||
s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse ordered list",
|
||||
raw: s,
|
||||
metadata: {
|
||||
// initialDepth:
|
||||
// s.replace("\n", "").split(/\d+\./).at(0)?.length.toString() || "1",
|
||||
},
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersChildrenOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children } = token;
|
||||
return (
|
||||
<>
|
||||
<ol
|
||||
// data-depth={(Number(metadata.initialDepth) / 2) % 3}
|
||||
className="ml-6 list-decimal"
|
||||
>
|
||||
{children?.map((c) => {
|
||||
return (
|
||||
<li key={c.uuid}>
|
||||
{c.children?.map((c: Token<any>) => (
|
||||
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// ordered list-item
|
||||
// list-item
|
||||
registerIdentifier(
|
||||
"list-item",
|
||||
/^\s*-\s(.*?)$/gm,
|
||||
/(?<=^|\n) *(?:-|\d+\.)\s(.*?)(?=\n|$)/g,
|
||||
(s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, ""))?.at(1) ||
|
||||
"Unable to parse list-item",
|
||||
content:
|
||||
s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse list-item",
|
||||
raw: s,
|
||||
metadata: {
|
||||
initialDepth:
|
||||
@@ -245,13 +296,11 @@ export const buildOnlyDefaultElements = () => {
|
||||
return (
|
||||
<li data-depth={metadata.initialDepth} className="ml-2">
|
||||
{children?.map((c) => (
|
||||
<Fragment key={c.uuid}>
|
||||
(c.render(c))
|
||||
</Fragment>
|
||||
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// heading
|
||||
@@ -259,8 +308,8 @@ export const buildOnlyDefaultElements = () => {
|
||||
"heading",
|
||||
/^#+\s(.*?)$/gm,
|
||||
(s, rx) => {
|
||||
const content = s.match(new RegExp(rx, ""))?.at(1) ||
|
||||
"Unable to parse heading";
|
||||
const content =
|
||||
s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse heading";
|
||||
return {
|
||||
content: content,
|
||||
raw: s,
|
||||
@@ -287,7 +336,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
{token.content}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// image
|
||||
@@ -319,13 +368,12 @@ export const buildOnlyDefaultElements = () => {
|
||||
USE_PROFILES: { svg: true },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
></div>
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={metadata.src} alt={token.content} />;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// anchor
|
||||
@@ -360,14 +408,16 @@ export const buildOnlyDefaultElements = () => {
|
||||
const { metadata } = token;
|
||||
return (
|
||||
<Link
|
||||
className={metadata.classes ||
|
||||
"dark:text-primary-600 underline dark:no-underline"}
|
||||
className={
|
||||
metadata.classes ||
|
||||
"dark:text-primary-600 underline dark:no-underline"
|
||||
}
|
||||
href={metadata.href}
|
||||
>
|
||||
{token.content}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// inline-code
|
||||
@@ -376,8 +426,8 @@ export const buildOnlyDefaultElements = () => {
|
||||
/(?<=\s|^)`(.*?)`(?=[\s,.!?)]|$)/gi,
|
||||
(s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, "i"))?.at(1) ||
|
||||
"Unable to parse inline-code",
|
||||
content:
|
||||
s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse inline-code",
|
||||
raw: s,
|
||||
metadata: {},
|
||||
uuid: crypto.randomUUID(),
|
||||
@@ -390,7 +440,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
{token.content}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// bold
|
||||
@@ -408,7 +458,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
},
|
||||
(token) => {
|
||||
return <span className="font-bold">{token.content}</span>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// italic
|
||||
@@ -417,8 +467,8 @@ export const buildOnlyDefaultElements = () => {
|
||||
/(?<!\*)\*([^\*]+?)\*(?!\*)/g,
|
||||
(s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, "i"))?.at(1) ||
|
||||
"Unable to parse italic",
|
||||
content:
|
||||
s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse italic",
|
||||
raw: s,
|
||||
metadata: {},
|
||||
uuid: crypto.randomUUID(),
|
||||
@@ -427,7 +477,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
},
|
||||
(token) => {
|
||||
return <span className="italic">{token.content}</span>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// popover
|
||||
@@ -449,9 +499,11 @@ export const buildOnlyDefaultElements = () => {
|
||||
const { children, metadata, uuid } = token;
|
||||
return (
|
||||
<Poppable
|
||||
content={children?.map((c) => (
|
||||
<Fragment key={uuid}>{c.render(c)}</Fragment>
|
||||
)) || token.content}
|
||||
content={
|
||||
children?.map((c) => (
|
||||
<Fragment key={uuid}>{c.render(c)}</Fragment>
|
||||
)) || token.content
|
||||
}
|
||||
preferredAlign="centered"
|
||||
preferredEdge="bottom"
|
||||
className="cursor-pointer mx-2"
|
||||
@@ -461,9 +513,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
</span>
|
||||
</Poppable>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// accordion
|
||||
registerIdentifier(
|
||||
"accordion",
|
||||
/\[accordion(\s.*?)?]\n+((?:.|\n)*?)\n+\[\/accordion\]/g,
|
||||
@@ -490,9 +543,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// paragraph
|
||||
registerIdentifier(
|
||||
"p",
|
||||
/(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
|
||||
@@ -507,8 +561,6 @@ export const buildOnlyDefaultElements = () => {
|
||||
(token) => {
|
||||
const { children } = token;
|
||||
|
||||
debugger;
|
||||
|
||||
return (
|
||||
<div className="p">
|
||||
{children?.map((e) => {
|
||||
@@ -516,9 +568,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// horizontal rule
|
||||
registerIdentifier(
|
||||
"hr",
|
||||
/^-{3,}$/gm,
|
||||
@@ -533,9 +586,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
},
|
||||
() => {
|
||||
return <div className="w-full border-b border-mixed-500 my-3"></div>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// comment
|
||||
registerIdentifier(
|
||||
"comment",
|
||||
/<!--[\s\S]+?-->/g,
|
||||
@@ -550,9 +604,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
},
|
||||
() => {
|
||||
return <></>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// frontmatter
|
||||
registerIdentifier(
|
||||
"frontmatter",
|
||||
/^---([\s\S]*?)---/g,
|
||||
@@ -568,9 +623,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
},
|
||||
(token) => {
|
||||
return <>{token.raw}</>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// table
|
||||
registerIdentifier(
|
||||
"table",
|
||||
/(?<=\n|^)\| [\s\S]*? \|(?=(\n|$)(?!\|))/g,
|
||||
@@ -622,7 +678,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
}
|
||||
|
||||
const maxColumns = Math.max(
|
||||
...[...headerRows, ...bodyRows, ...footerRows].map((r) => r.length),
|
||||
...[...headerRows, ...bodyRows, ...footerRows].map((r) => r.length)
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -680,8 +736,12 @@ export const buildOnlyDefaultElements = () => {
|
||||
<td
|
||||
key={r.join() + i + c}
|
||||
className="data-[center=true]:text-center"
|
||||
data-center={!!(columnPattern?.at(i) &&
|
||||
columnPattern.at(i)?.includes("^"))}
|
||||
data-center={
|
||||
!!(
|
||||
columnPattern?.at(i) &&
|
||||
columnPattern.at(i)?.includes("^")
|
||||
)
|
||||
}
|
||||
>
|
||||
{child?.render(child) || c}
|
||||
</td>
|
||||
@@ -709,7 +769,76 @@ export const buildOnlyDefaultElements = () => {
|
||||
)}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// resolver
|
||||
registerIdentifier(
|
||||
"resolver",
|
||||
/\?\?<<(.*?)>>/g,
|
||||
(s) => {
|
||||
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
|
||||
if (inp == undefined)
|
||||
return {
|
||||
content: "Error parsing resolver: " + s,
|
||||
metadata: {},
|
||||
raw: "ERROR",
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
return {
|
||||
content: inp,
|
||||
metadata: {},
|
||||
raw: s,
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
if (t.content.startsWith("Error"))
|
||||
return <span className="red-500">{t.content}</span>;
|
||||
return <Resolver resolver={t.content} />;
|
||||
}
|
||||
);
|
||||
|
||||
// on-demand resolver
|
||||
registerIdentifier(
|
||||
"on-demand resolver",
|
||||
/\?\?\[.*?\](\(.*?\))?<<(.*?)>>/g,
|
||||
(s) => {
|
||||
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
|
||||
const template = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0];
|
||||
const title = s.match(/(?<=\]\()(.*?)(?=\))/)?.at(0);
|
||||
if (inp == undefined)
|
||||
return {
|
||||
content: "Error parsing resolver: " + s,
|
||||
metadata: {
|
||||
title: "",
|
||||
template: "",
|
||||
},
|
||||
raw: "ERROR",
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
return {
|
||||
content: inp,
|
||||
metadata: {
|
||||
title,
|
||||
template,
|
||||
},
|
||||
raw: s,
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
if (t.content.startsWith("Error"))
|
||||
return <span className="red-500">{t.content}</span>;
|
||||
|
||||
return (
|
||||
<OnDemandResolver
|
||||
resolver={t.content}
|
||||
template={t.metadata.template}
|
||||
title={t.metadata.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return TokenIdentifiers;
|
||||
@@ -718,7 +847,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
function findMatchingClosedParenthesis(
|
||||
str: string,
|
||||
openRegex: RegExp,
|
||||
closedRegex: RegExp,
|
||||
closedRegex: RegExp
|
||||
): number | null {
|
||||
let openings = 0;
|
||||
let closings = 0;
|
||||
@@ -774,7 +903,7 @@ function search(
|
||||
start: number,
|
||||
end: number,
|
||||
openRx: RegExp,
|
||||
closeRx: RegExp,
|
||||
closeRx: RegExp
|
||||
): SearchResult {
|
||||
const oldEnd = end;
|
||||
|
||||
@@ -782,10 +911,11 @@ function search(
|
||||
s,
|
||||
// s.substring(0, end - start),
|
||||
openRx,
|
||||
closeRx,
|
||||
closeRx
|
||||
);
|
||||
|
||||
if (newEnd === null) throw Error("There was an issue finding a closing tag");
|
||||
if (newEnd === null)
|
||||
throw Error("There was an issue finding a closing tag for ");
|
||||
|
||||
end = newEnd + start;
|
||||
|
||||
|
@@ -109,7 +109,7 @@ type ParentChildMap = {
|
||||
};
|
||||
|
||||
const parentChildMap: ParentChildMap = {
|
||||
"list": ["list-item"],
|
||||
list: ["list-item"],
|
||||
// Add more mappings as needed...
|
||||
};
|
||||
|
||||
@@ -128,10 +128,8 @@ function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] {
|
||||
for (const otherBlock of blocks) {
|
||||
if (
|
||||
otherBlock !== block &&
|
||||
(
|
||||
otherBlock.start === block.start ||
|
||||
(otherBlock.end === block.end && otherBlock.start < block.start)
|
||||
)
|
||||
(otherBlock.start === block.start ||
|
||||
(otherBlock.end === block.end && otherBlock.start < block.start))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -155,29 +153,32 @@ const contentToChildren = (token: Token) => {
|
||||
}
|
||||
|
||||
token.children = zipArrays(
|
||||
content.split(splitMarker).map((c): Token => ({
|
||||
content: c.replaceAll("\n", " "),
|
||||
metadata: {},
|
||||
raw: c,
|
||||
type: token.rendersChildrenOnly ? "p" : "text",
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly: token.rendersChildrenOnly ? false : true,
|
||||
render: TokenRenderers.get(token.rendersChildrenOnly ? "p" : "text")!,
|
||||
children: token.rendersChildrenOnly && c.replaceAll("\n", "")
|
||||
? [
|
||||
{
|
||||
content: c.replaceAll("\n", " "),
|
||||
metadata: {},
|
||||
raw: c,
|
||||
type: "text",
|
||||
uuid: crypto.randomUUID(),
|
||||
render: TokenRenderers.get("text")!,
|
||||
rendersContentOnly: true,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
})),
|
||||
token.children || [],
|
||||
content.split(splitMarker).map(
|
||||
(c): Token => ({
|
||||
content: c.replaceAll("\n", " "),
|
||||
metadata: {},
|
||||
raw: c,
|
||||
type: token.rendersChildrenOnly ? "p" : "text",
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly: token.rendersChildrenOnly ? false : true,
|
||||
render: TokenRenderers.get(token.rendersChildrenOnly ? "p" : "text")!,
|
||||
children:
|
||||
token.rendersChildrenOnly && c.replaceAll("\n", "")
|
||||
? [
|
||||
{
|
||||
content: c.replaceAll("\n", " "),
|
||||
metadata: {},
|
||||
raw: c,
|
||||
type: "text",
|
||||
uuid: crypto.randomUUID(),
|
||||
render: TokenRenderers.get("text")!,
|
||||
rendersContentOnly: true,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
})
|
||||
),
|
||||
token.children || []
|
||||
).filter((c) => c.children?.length || (c.rendersContentOnly && c.content));
|
||||
};
|
||||
|
||||
|
149
lib/ttcQuery/TTCQueryParser.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export class TTCQueryParser {
|
||||
private data: QueryableObject;
|
||||
private relativeMap: Map<QueryableObject, QueryableObject>;
|
||||
|
||||
constructor(data: QueryableObject) {
|
||||
this.data = data;
|
||||
this.relativeMap = new Map();
|
||||
this.buildRelativeMap(this.data, null);
|
||||
}
|
||||
|
||||
public search(
|
||||
query: string,
|
||||
currentObject: QueryableObject = this.data
|
||||
): any[] {
|
||||
// Normalize the query string by trimming whitespace
|
||||
query = query.trim();
|
||||
|
||||
// Determine the base structure to search
|
||||
let result: any[] = [];
|
||||
|
||||
// Perform initial parsing based on the query syntax
|
||||
if (query.startsWith("^")) {
|
||||
result = this.queryCurrentObject(query, currentObject);
|
||||
} else if (query.startsWith("$")) {
|
||||
result = this.queryRelativeObject(query, currentObject);
|
||||
} else {
|
||||
result = this.searchInObject(this.data, query);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private queryCurrentObject(
|
||||
query: string,
|
||||
currentObject: QueryableObject
|
||||
): any[] {
|
||||
// Example implementation for querying the current object
|
||||
const subQuery = query.substring(1); // Remove '^'
|
||||
return this.searchInObject(currentObject, subQuery);
|
||||
}
|
||||
|
||||
private queryRelativeObject(
|
||||
query: string,
|
||||
currentObject: QueryableObject
|
||||
): any[] {
|
||||
const relativeObject = this.relativeMap.get(currentObject);
|
||||
|
||||
if (!relativeObject) {
|
||||
throw new Error("Relative object not found.");
|
||||
}
|
||||
const subQuery = query.substring(1); // Remove '$'
|
||||
return this.searchInObject(relativeObject, subQuery);
|
||||
}
|
||||
|
||||
private buildRelativeMap(
|
||||
obj: QueryableObject,
|
||||
relative: QueryableObject | null
|
||||
): void {
|
||||
if (obj.relative) {
|
||||
relative = obj;
|
||||
}
|
||||
this.relativeMap.set(obj, relative || obj);
|
||||
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
this.buildRelativeMap(obj[key], relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private searchInObject(obj: any, subQuery?: string): any[] {
|
||||
// Handle subqueries and search in the provided object
|
||||
if (!subQuery) {
|
||||
return [obj]; // Return the entire object if no subquery is present
|
||||
}
|
||||
|
||||
// Split the subquery based on dot (.) to navigate through the object
|
||||
const keys = subQuery.split(".");
|
||||
let current: any = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === "object") {
|
||||
if (key.includes("[") && key.includes("]")) {
|
||||
const [prop, selector] = key.split("[");
|
||||
const index = selector.slice(0, -1);
|
||||
|
||||
if (Array.isArray(current[prop])) {
|
||||
if (!isNaN(Number(index))) {
|
||||
current = current[prop][Number(index)];
|
||||
} else {
|
||||
const [k, comparator, value] = this.parseSelector(index);
|
||||
current = this.applySelector(current[prop], k, comparator, value);
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
current = Array.isArray(current)
|
||||
? current.map((e: any) => e[key])
|
||||
: current[key];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(current) ? current : [current];
|
||||
}
|
||||
|
||||
private parseSelector(selector: string): [string, string, string] {
|
||||
const match = selector.match(/(.+)(=|==|>|<|>=|<=|!!|!|!=)(.+)/);
|
||||
if (match) {
|
||||
return [match[1], match[2], match[3]];
|
||||
}
|
||||
return ["", "=", selector];
|
||||
}
|
||||
|
||||
private applySelector(
|
||||
array: any[],
|
||||
key: string,
|
||||
comparator: string,
|
||||
value: string
|
||||
): any[] {
|
||||
switch (comparator) {
|
||||
case "=":
|
||||
return array.filter((item) => (key ? item[key] : item).includes(value));
|
||||
case "==":
|
||||
return array.filter((item) => (key ? item[key] : item) === value);
|
||||
case ">":
|
||||
return array.filter((item) => (key ? item[key] : item) > value);
|
||||
case "<":
|
||||
return array.filter((item) => (key ? item[key] : item) < value);
|
||||
case ">=":
|
||||
return array.filter((item) => (key ? item[key] : item) >= value);
|
||||
case "<=":
|
||||
return array.filter((item) => (key ? item[key] : item) <= value);
|
||||
case "!!":
|
||||
return array.filter((item) => (key ? item[key] : item));
|
||||
case "!=":
|
||||
return array.filter(
|
||||
(item) => !(key ? item[key] : item).includes(value)
|
||||
);
|
||||
case "!":
|
||||
return array.filter((item) => !(key ? item[key] : item));
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
117
lib/ttcQuery/TTCResolvers.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Dice } from "../dice";
|
||||
import { TTCQueryParser } from "./TTCQueryParser";
|
||||
|
||||
export class TTCQueryResolver {
|
||||
private parser: TTCQueryParser | null;
|
||||
private context: QueryableObject | null = null;
|
||||
|
||||
private stack: any[] = [];
|
||||
|
||||
constructor(parser?: TTCQueryParser) {
|
||||
this.parser = parser || null;
|
||||
}
|
||||
|
||||
public setParser(parser: TTCQueryParser) {
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
public setContext(obj: QueryableObject) {
|
||||
this.context = obj;
|
||||
}
|
||||
|
||||
public resolve(resolver: string, onDemand?: boolean) {
|
||||
const resList = resolver.split(",");
|
||||
for (const res of resList) {
|
||||
this.stack.push(this.parseResolver(res));
|
||||
}
|
||||
const last = this.stack.at(-1);
|
||||
if (typeof last === "function" && !onDemand) return last();
|
||||
|
||||
return last;
|
||||
}
|
||||
|
||||
private parseResolver(resolver: string) {
|
||||
if (this.isArithmetic(resolver)) return this.solveArithmetic(resolver);
|
||||
if (this.isQuery(resolver)) return this.runQuery(resolver);
|
||||
return resolver;
|
||||
}
|
||||
private isQuery(resolver: string) {
|
||||
return (
|
||||
resolver.startsWith("?") ||
|
||||
resolver.startsWith("_") ||
|
||||
/^\$\d\./.test(resolver)
|
||||
);
|
||||
}
|
||||
private runQuery(query: string) {
|
||||
if (!this.parser) throw "Parser not defined in query resolver";
|
||||
if (query.startsWith("$")) {
|
||||
const [_, idx, q] = query.match(/^(\$\d+)\.(.*)/) || [];
|
||||
if (!_) throw "Detected stack query but did not match the regex";
|
||||
const stackItem = this.getFromStack(idx);
|
||||
if (typeof stackItem === "string" && Dice.isDice(stackItem)) {
|
||||
return this.handleDice(stackItem, q);
|
||||
}
|
||||
|
||||
return this.parser.search(q, stackItem as QueryableObject);
|
||||
}
|
||||
|
||||
// if (query.startsWith("?") || query.startsWith("_"))
|
||||
return query.startsWith("_") && this.context
|
||||
? this.parser.search(query.replace("_", "^"), this.context).at(0)
|
||||
: this.parser.search(query.replace(/^[?_].?/, "")).at(0);
|
||||
}
|
||||
|
||||
private handleDice(dice: string, query: string) {
|
||||
const d = new Dice(dice);
|
||||
const [method, n] = query.split(":");
|
||||
let num = Number(n);
|
||||
if (n && n.startsWith("$")) num = this.getFromStack(n);
|
||||
switch (method) {
|
||||
case "roll":
|
||||
return () => d.roll();
|
||||
case "rollAvg":
|
||||
return () => d.rollAvg();
|
||||
case "rollTimes":
|
||||
return () => d.rollTimes(num);
|
||||
case "rollTimesAvg":
|
||||
return () => d.rollTimesAvg(num);
|
||||
default:
|
||||
return "No valid method provided for dice";
|
||||
}
|
||||
}
|
||||
|
||||
private isArithmetic(resolver: string) {
|
||||
return resolver.split(/\+|\/|-|\*|\^/).filter((e) => !!e).length > 1;
|
||||
}
|
||||
private solveArithmetic(resolver: string) {
|
||||
const [n1, op, n2] = resolver
|
||||
.match(/(\$?\d+)([+\-*\/^])(\$?\d+)/)
|
||||
?.slice(1) || ["", "+", ""];
|
||||
let num1 = Number(n1),
|
||||
num2 = Number(n2);
|
||||
|
||||
if (n1.startsWith("$")) num1 = this.getFromStack<number>(n1);
|
||||
if (n2.startsWith("$")) num2 = this.getFromStack<number>(n2);
|
||||
|
||||
switch (op) {
|
||||
case "+":
|
||||
return num1 + num2;
|
||||
case "-":
|
||||
return num1 - num2;
|
||||
case "*":
|
||||
return num1 * num2;
|
||||
case "/":
|
||||
return num1 / num2;
|
||||
case "^":
|
||||
return num1 ^ num2;
|
||||
default:
|
||||
throw "Arithmetic detected but no proper operator assigned";
|
||||
}
|
||||
}
|
||||
|
||||
public getFromStack<T>(stackIndex: string): T {
|
||||
const i = Number(stackIndex.replace("$", ""));
|
||||
const val = this.stack[i] as T;
|
||||
return val;
|
||||
}
|
||||
}
|
@@ -7,7 +7,7 @@ updated: March 14th, 2024
|
||||
|
||||
# Table of Contents
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What even is ttcMD?](#what-even-is-ttcmd)
|
||||
- [What even is ttcMD?](#what-even-is-ttcmd)
|
||||
- [Enhanced Standard Elements](#enhanced-standard-elements)
|
||||
- [Links](#links)
|
||||
- [Tables](#tables)
|
||||
@@ -26,19 +26,25 @@ updated: March 14th, 2024
|
||||
|
||||
---
|
||||
|
||||
# What even is ttcMD?
|
||||
This help article contains a lot of examples of how to use the syntax of ttcMD and what they look like when put into practice. It's very information dense and also a bit chaotic with all of the examples, so please feel free to use the table of contents to find the specific section you are looking for.
|
||||
|
||||
## What even is ttcMD?
|
||||
|
||||
ttcMD is a flavor of markdown that has been specifically designed to use with [ttcQuery](/help/ttcQuery.md). It has all of the basic syntax of [markdown](https://www.markdownguide.org/cheat-sheet/), but also includes Tables, basic Fenced Code Blocks and a slew of custom elements and styling annotations.
|
||||
|
||||
One thing to note, however, is that ttcMD is *not* standard markdown. It does not generate valid markup for just about any element, and as such it will not be released in any scope wider than inside TTC itself. One could more accurately call it a layout language than actual markdown.
|
||||
|
||||
---
|
||||
|
||||
## Enhanced Standard Elements
|
||||
|
||||
This section will cover all of the enhancements that are added for basic markdown elements
|
||||
|
||||
---
|
||||
|
||||
### Links
|
||||
|
||||
You can use the typical link syntax `[link name](/link/location)`, but there are a few presets that allow you to style them to look a bit nicer.
|
||||
You can use the typical link syntax `[link name](/link/location)` but there are a few presets that allow you to style them to look a bit nicer.
|
||||
|
||||
**Primary Button:**
|
||||
|
||||
@@ -54,6 +60,8 @@ You can use the typical link syntax `[link name](/link/location)`, but there are
|
||||
|
||||
[~~cta link name](#links)
|
||||
|
||||
---
|
||||
|
||||
### Tables
|
||||
|
||||
Generally tables will only be as wide as their content needs. To make a table take up the full width, you can use this syntax:
|
||||
@@ -116,10 +124,14 @@ Full width with a centered body column
|
||||
|
||||
/[]
|
||||
|
||||
---
|
||||
|
||||
## Custom Elements
|
||||
|
||||
This section will cover the specific elements custom built for Tabletop Commander.
|
||||
|
||||
---
|
||||
|
||||
### Pop-outs
|
||||
|
||||
Pop-outs, or popovers, are the little cards that "pop out" when you hover over an item.
|
||||
@@ -132,10 +144,14 @@ This syntax `^[This is my favorite image]<<*Goofy!* .
|
||||
@@ -246,6 +264,8 @@ Additionally, you can specify a number after the opening brackets (`[[2 ... ]]`)
|
||||
|
||||
/[]
|
||||
|
||||
---
|
||||
|
||||
### Block
|
||||
|
||||
[][][]
|
||||
@@ -271,6 +291,7 @@ Additionally, you can specify a number after the opening brackets (`[[!2 ... ]]`
|
||||
|
||||
/[]
|
||||
|
||||
---
|
||||
|
||||
### Grid
|
||||
|
||||
@@ -317,12 +338,16 @@ This card will end up in the third column...
|
||||
|
||||
/[]
|
||||
|
||||
---
|
||||
|
||||
## Query Elements
|
||||
|
||||
The following elements are used in combination with ttcQuery. These are definitely more advanced. If you understand generally what "dot notation" is in programming, then it will be a lot easier, but don't let that deter you. Once you understand what it is, you can come back to this and be able to create really cool layouts!
|
||||
|
||||
Query elements (aside for the on-demand resolver) are calculated before parsing the markdown. Will that matter to you? Probably not, but could be necessary as you think about how you are writing your query elements.
|
||||
|
||||
---
|
||||
|
||||
### Resolver
|
||||
|
||||
The resolver is the basic element that allows you to get data, but it has a lot of functionality to it. It has not been fully implemented yet
|
||||
@@ -331,21 +356,33 @@ Syntax: `??<<List -> QueryValue>>`
|
||||
|
||||
If you've read the ttcQuery docs, you'll know that the `List` type means that you can input any number of `QueryValues` in this element. When the resolver runs, it runs each `QueryValue` from left to right. The last `QueryValue` to run is what gets rendered, but you have access to previous values through their index by using the `$#` variables.
|
||||
|
||||
As `QueryValues` are capable of arithmetic, you can quite easily do simple math inline, though it's unfortunately not quite as simple as just writing an equation. Each `QueryValue` can only do one arithmetic operation. This is to prevent performance issues with parsing arbitrary calculations. So, if you wanted to get the average of the values of 1, 2, 3 and 4, the query would look like this: `??<<1+2,3+4,$0+$1,$2/4>>` which will result in the value 2.5. Arithmetic will fail if a value provided is not a number and will render a message in the markdown.
|
||||
As `QueryValues` are capable of arithmetic, you can quite easily do simple math inline, though it's unfortunately not quite as simple as just writing an equation. Each `QueryValue` can only do one arithmetic operation. This is to prevent performance issues with parsing arbitrary calculations. So, if you wanted to get the average of the values of 1, 2, 3 and 4, the query would look like this: `??<<1+2,3+4,$0+$1,$2/4>>` which will result in the value ??<<1+2,3+4,$0+$1,$2/4>>. Arithmetic will fail if a value provided is not a number and will render a message in the markdown.
|
||||
|
||||
If the resolver results in a list of items, it will list all of them together, separated by commas.
|
||||
|
||||
Let's say you want to get the the result of rolling a dice field. You would simply write `$$<<_.path.to.dice,$0.roll>>`. This will roll the dice when the markdown is render, which means it only happens once. If you want to reroll the dice, you either need to reload the markdown by closing the viewer and reopening it, or you need to use an On-demand Resolver.
|
||||
Let's say you want to get the the result of rolling a dice field. You would simply write `??<<_.path.to.dice,$0.roll>>` and get the result of the dice roll like this: ??<<_.path.to.dice,$0.roll>>. This will roll the dice when the markdown is rendered, which means it only happens once. If you want to reroll the dice, you either need to reload the markdown by closing the viewer and reopening it, or you need to use an On-demand Resolver.
|
||||
|
||||
---
|
||||
|
||||
### On-demand Resolver
|
||||
|
||||
This works very similarly to the normal resolver, but when it renders it will have a button that you can press. When you press it, it recalculates its value. This is very useful for dice and decks of cards. It has not been fully implemented yet
|
||||
|
||||
Here's the syntax: `??[Template]<<List::QueryValue>>`. Template is a basic string that has access to all values of the resolver using the `$#` variables. If template is left blank, the value that the resolver finally reaches will be rendered. In lieu of a template, you can also have it render the display of a field if it has one.
|
||||
Here's the syntax: `??[Template](Text?)<<List::QueryValue>>`. Template is a basic string that has access to all values of the resolver using the `$#` variables. If template is left blank, the value that the resolver finally reaches will be rendered. In lieu of a template, you can also have it render the display of a field if it has one. Text is the text that will be rendered in the button. If not provided, the button will simply same "Resolve."
|
||||
|
||||
To use the dice as an example again, here's how you would do that: `??[Rolling $0, you got: $1]<<_path.to.dice,$0.roll>>`
|
||||
[][]
|
||||
[[
|
||||
To use the dice as an example again, here's how you would do that: `??[Rolling $0, you got: $1](Roll 2d6)<<_.path.to.dice,$0.roll>>`
|
||||
|
||||
For drawing a card and having it show the card's display: `??[]<<_path.to.deck,$0.draw,$1.display>>`
|
||||
??[Rolling $0, you got: $1](Roll 2d6)<<_.path.to.dice,$0.roll>>
|
||||
]]
|
||||
|
||||
[[
|
||||
For drawing a card and having it show the card's display: `??[]<<_.path.to.deck,$0.draw,$1.display>>`
|
||||
]]
|
||||
/[]
|
||||
|
||||
---
|
||||
|
||||
### Query Block Template
|
||||
|
||||
@@ -364,9 +401,11 @@ Syntax:
|
||||
|
||||
While in a Query Block Template, all resolvers will use the queried value as the root `_` variable. If the original query returns multiple values, it will render the markdown for each result. This is what makes it so useful. When you want to render your 40k army list, you can do so with very little markdown.
|
||||
|
||||
---
|
||||
|
||||
### Query Block
|
||||
|
||||
Similar to the Query Block Template, the query block passes the values of the query and passes it to the markdown inside of it. However, instead of rendering the whole block, it instead renders the markdown a single time. It has not been fully implemented yet
|
||||
Similar to the Query Block Template, the query block collects the values of the query and passes it to the markdown inside of it. However, instead of rendering the whole block, it instead renders the markdown a single time. It has not been fully implemented yet
|
||||
|
||||
Unlike the Query Block Template, it does not change the `_` variable to the queried value. For resolvers to work with the queried data, you need to access it with the `$$` variable. `$$` will be iterated over and will act similar to the Query Block Template, but on a per-line basis, meaning that if a line contains the `$$` variable, it will render that line of markdown for each result of the query. This is primarily useful for something like a List or a Table.
|
||||
|
||||
|
82
md/help articles/The basics of schemas and objects.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: THe basics of schemas and objects
|
||||
author: Emmaline Autumn
|
||||
date: June 11th, 2024
|
||||
updated: June 11th, 2024
|
||||
---
|
||||
|
||||
# Table of Contents
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What is a schema?](#what-is-a-schema)
|
||||
- [Schema Templates](#schema-templates)
|
||||
- [Schema and Type Fields](#schema-and-type-fields)
|
||||
- [Schema Types](#schema-types)
|
||||
- [Default Types](#default-types)
|
||||
- [Special Types](#special-types)
|
||||
- [How to make a schema](#how-to-make-a-schema)
|
||||
|
||||
---
|
||||
|
||||
# What is a schema?
|
||||
|
||||
A schema is a representation of the structure of a collection of data. In TTC, a schema is how we define the structure of game objects. Schemas have three parts: a template, fields, and types.
|
||||
|
||||
Schemas are effectively describing a relationship of parent objects and child objects as a collection of key-value pairs.
|
||||
|
||||
## Schema Templates
|
||||
|
||||
The template is how the object will be represented when rendered in markdown. It works in a similar way to Query Block Templates, with access to the `_` root variable.
|
||||
|
||||
By default, there is a simple template that renders them out as a key-value pair. The default template looks like this:
|
||||
|
||||
[][][]
|
||||
|
||||
```
|
||||
??<<_.!!>>[[
|
||||
|
||||
# ??<<_.key>> $${{_.value}}
|
||||
|
||||
]]
|
||||
```
|
||||
|
||||
/[]
|
||||
|
||||
## Schema and Type Fields
|
||||
|
||||
Schema fields are the description of the parent-child structure. Fields are named and can be left blank or filled in at any point of a publication's lifecycle.
|
||||
|
||||
If a field is rendered, it checks if it has a value in the publication or not. If it does, it will render the type's template. If it does not have a value, it will render an appropriate input for the type of the field. The exception is "required" fields. These require that the base (first) publication using that schema is required to have a value.
|
||||
|
||||
Fields by default are limited to a maximum of 1 entry, but you can increase the limit or set it to unlimited.
|
||||
|
||||
## Schema Types
|
||||
|
||||
Schema types are almost like mini-schemas that go inside of a schema. A schema holds all objects related to a discipline inside the rules (e.g. a Codex in Warhammer 40k, or an expansion), but a type is used to describe the fields inside of that schema (e.g. a datasheet for a unit in Warhammer 40k).
|
||||
|
||||
Types can be made up of other types as well. For example, the built in table type is made up of table rows which are made up of table columns.These compound types allow you to make better, more complex schemas that can do more.
|
||||
|
||||
Types also have a template. By default, it uses the same template as the schema to just render all of its contents, but you can customize it to your needs using ttcMD.
|
||||
|
||||
### Default Types
|
||||
|
||||
There are 5 default schema field types: section, steps, image, list, and table. These types have basic templates that you can't change that help build more complex types or even just build quick and dirty layouts.
|
||||
|
||||
There are also type field types, such as text, number, long text, and checkbox.
|
||||
|
||||
Each type has different limitations that you can enforce in the publications. Number has a minimum and a maximum, text has a maximum length.
|
||||
|
||||
### Special Types
|
||||
|
||||
Special types have specific programmed behaviors. These include decks of cards, dice, select and any. I am trying to figure out a good way to make it possible for you to make your own behaviors, but that is not top priority at the moment.
|
||||
|
||||
# How to make a schema
|
||||
|
||||
1. Inside of a game system, you will click "create new schema." This will take you to the schema editor. The first thing to do is to give your schema a name. This should be unique in the context of the game system.
|
||||
2. You will then either create your custom types or add fields to the schema below. Let's start by creating our own type by giving it an name and clicking "configure." We will move into an area of the editor that allows us to add fields. Let's give this type two fields, name them count and description.
|
||||
3. For count, let's select a number for the field type and set the minimum to 0. Since we only want 1 count, let's set the Limit field to 1 and the Minimum field to 1.
|
||||
4. For the description, let's make it constant. You'll notice that there is now an additional field that you can use to input a constant value, let's describe this type as "A sample schema type that holds a count."
|
||||
5. Now that we've finished with this type, click "save type." It will be added to the list of Types in the schema viewer on the side where you can review.
|
||||
6. Go up to the schema fields and let's add a field by typing the name for the field and clicking add. Let's call ours "Population"
|
||||
7. In the list below, our new field has been added. We can now change the type of that field by typing it into the Type field. It will populate suggestions as you type.
|
||||
8. Now you can set the limits of the field.
|
||||
9. Lastly, and most importantly, click "Save Schema" at the top right.
|
@@ -1,4 +1,15 @@
|
||||
| test | Table | header |
|
||||
| ---- | ----- | ------ |
|
||||
| test | table | row |
|
||||
| shorter | *row* |
|
||||
|
||||
- hello
|
||||
- everybody
|
||||
- yes you
|
||||
- my
|
||||
- name
|
||||
- is
|
||||
- welcome
|
||||
|
||||
1. hello
|
||||
2. everybody
|
||||
3. my
|
||||
4. name
|
||||
5. is
|
||||
6. welcome
|
1
middleware.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { auth as middleware } from "@/auth";
|
@@ -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;
|
||||
|
3874
package-lock.json
generated
19
package.json
@@ -6,18 +6,26 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"postinstall": "bun ./postinstall/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.4.2",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@prisma/client": "^5.11.0",
|
||||
"@prisma/client": "^5.18.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"isomorphic-dompurify": "^2.4.0",
|
||||
"next": "14.1.0",
|
||||
"prisma": "^5.11.0",
|
||||
"jotai": "^2.9.3",
|
||||
"next": "^14.2.5",
|
||||
"next-auth": "^5.0.0-beta.20",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
"react-dom": "^18",
|
||||
"recoil": "^0.7.7",
|
||||
"url-loader": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
@@ -25,6 +33,7 @@
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"prisma": "^5.18.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^7.2.0"
|
||||
|
22
postinstall/buildEnv.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const { SecretClient } = require("../lib/secret/init");
|
||||
const { writeFile } = require("fs/promises");
|
||||
|
||||
const requiredKeys = [
|
||||
"discord_client_id",
|
||||
"discord_client_secret",
|
||||
"ttc:database_url",
|
||||
];
|
||||
|
||||
const secretClient = SecretClient();
|
||||
|
||||
async function buildEnv() {
|
||||
secretClient.fetchToken();
|
||||
let secrets = "";
|
||||
for (const key of requiredKeys) {
|
||||
const value = await secretClient.fetchSecret(key);
|
||||
secrets += `${key.replace("ttc:", "").toUpperCase()}=${value}\n`;
|
||||
}
|
||||
await writeFile(".env", secrets, "utf-8");
|
||||
}
|
||||
|
||||
buildEnv();
|
1
postinstall/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
require("./buildEnv.ts");
|
@@ -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;
|
83
prisma/migrations/20240815100640_retagging/migration.sql
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `authorId` to the `GameSystem` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `authorId` to the `Publication` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `authorId` to the `Schema` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `originalId` to the `Schema` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `name` to the `Tag` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE `Schema` DROP FOREIGN KEY `Schema_gameSystemId_fkey`;
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE `Tag` DROP FOREIGN KEY `Tag_publicationId_fkey`;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `GameSystem` ADD COLUMN `authorId` VARCHAR(191) NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Publication` ADD COLUMN `authorId` VARCHAR(191) NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Schema` ADD COLUMN `authorId` VARCHAR(191) NOT NULL,
|
||||
ADD COLUMN `originalId` VARCHAR(191) NOT NULL,
|
||||
MODIFY `gameSystemId` VARCHAR(191) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Tag` ADD COLUMN `name` VARCHAR(191) NOT NULL,
|
||||
MODIFY `publicationId` VARCHAR(191) NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `TagsOnPublications` (
|
||||
`publicationId` VARCHAR(191) NOT NULL,
|
||||
`tagId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`publicationId`, `tagId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `TagsOnTags` (
|
||||
`parentTagId` VARCHAR(191) NOT NULL,
|
||||
`childTagId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`parentTagId`, `childTagId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `User` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`username` VARCHAR(191) NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `User_email_key`(`email`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `GameSystem` ADD CONSTRAINT `GameSystem_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Schema` ADD CONSTRAINT `Schema_gameSystemId_fkey` FOREIGN KEY (`gameSystemId`) REFERENCES `GameSystem`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Schema` ADD CONSTRAINT `Schema_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Publication` ADD CONSTRAINT `Publication_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `TagsOnPublications` ADD CONSTRAINT `TagsOnPublications_publicationId_fkey` FOREIGN KEY (`publicationId`) REFERENCES `Publication`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `TagsOnPublications` ADD CONSTRAINT `TagsOnPublications_tagId_fkey` FOREIGN KEY (`tagId`) REFERENCES `Tag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Tag` ADD CONSTRAINT `Tag_publicationId_fkey` FOREIGN KEY (`publicationId`) REFERENCES `Publication`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `TagsOnTags` ADD CONSTRAINT `TagsOnTags_parentTagId_fkey` FOREIGN KEY (`parentTagId`) REFERENCES `Tag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `TagsOnTags` ADD CONSTRAINT `TagsOnTags_childTagId_fkey` FOREIGN KEY (`childTagId`) REFERENCES `Tag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
71
prisma/migrations/20240818144724_auth/migration.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `User` ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
ADD COLUMN `emailVerified` DATETIME(3) NULL,
|
||||
ADD COLUMN `image` VARCHAR(191) NULL,
|
||||
ADD COLUMN `name` VARCHAR(191) NULL,
|
||||
ADD COLUMN `updatedAt` DATETIME(3) NOT NULL,
|
||||
MODIFY `username` VARCHAR(191) NULL,
|
||||
MODIFY `email` VARCHAR(191) NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Account` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`userId` VARCHAR(191) NOT NULL,
|
||||
`type` VARCHAR(191) NOT NULL,
|
||||
`provider` VARCHAR(191) NOT NULL,
|
||||
`providerAccountId` VARCHAR(191) NOT NULL,
|
||||
`refresh_token` TEXT NULL,
|
||||
`access_token` TEXT NULL,
|
||||
`expires_at` INTEGER NULL,
|
||||
`token_type` VARCHAR(191) NULL,
|
||||
`scope` VARCHAR(191) NULL,
|
||||
`id_token` TEXT NULL,
|
||||
`session_state` VARCHAR(191) NULL,
|
||||
`refresh_token_expires_in` INTEGER NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `Account_userId_key`(`userId`),
|
||||
INDEX `Account_userId_idx`(`userId`),
|
||||
UNIQUE INDEX `Account_provider_providerAccountId_key`(`provider`, `providerAccountId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Session` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`sessionToken` VARCHAR(191) NOT NULL,
|
||||
`userId` VARCHAR(191) NOT NULL,
|
||||
`expires` DATETIME(3) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `Session_sessionToken_key`(`sessionToken`),
|
||||
INDEX `Session_userId_idx`(`userId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `VerificationToken` (
|
||||
`identifier` VARCHAR(191) NOT NULL,
|
||||
`token` VARCHAR(191) NOT NULL,
|
||||
`expires` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `VerificationToken_identifier_token_key`(`identifier`, `token`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `User_username_key` ON `User`(`username`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Account` ADD CONSTRAINT `Account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
2
prisma/migrations/20240818160027_pw/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `User` ADD COLUMN `passwordHash` VARCHAR(191) NULL;
|
@@ -14,8 +14,10 @@ datasource db {
|
||||
}
|
||||
|
||||
model GameSystem {
|
||||
id String @id @default(cuid())
|
||||
schemas Schema[]
|
||||
id String @id @default(cuid())
|
||||
schemas Schema[]
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId String
|
||||
|
||||
name String @unique
|
||||
created DateTime @default(now())
|
||||
@@ -23,13 +25,17 @@ model GameSystem {
|
||||
|
||||
model Schema {
|
||||
id String @id @default(cuid())
|
||||
gameSystem GameSystem @relation(fields: [gameSystemId], references: [id])
|
||||
gameSystemId String
|
||||
gameSystem GameSystem? @relation(fields: [gameSystemId], references: [id])
|
||||
gameSystemId String?
|
||||
publications Publication[]
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId String
|
||||
|
||||
name String
|
||||
schema Json
|
||||
version Int
|
||||
originalId String
|
||||
name String
|
||||
schema Json
|
||||
types Json
|
||||
version Int
|
||||
}
|
||||
|
||||
model Publication {
|
||||
@@ -37,13 +43,101 @@ model Publication {
|
||||
schema Schema @relation(fields: [schemaId], references: [id])
|
||||
schemaId String
|
||||
tags Tag[]
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId String
|
||||
|
||||
name String
|
||||
data Json
|
||||
name String
|
||||
data Json
|
||||
TagsOnPublications TagsOnPublications[]
|
||||
}
|
||||
|
||||
model TagsOnPublications {
|
||||
publication Publication @relation(fields: [publicationId], references: [id])
|
||||
publicationId String
|
||||
tagId String
|
||||
tag Tag @relation(fields: [tagId], references: [id])
|
||||
|
||||
@@id([publicationId, tagId])
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
publication Publication @relation(fields: [publicationId], references: [id])
|
||||
publicationId String
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
Publication Publication? @relation(fields: [publicationId], references: [id])
|
||||
publicationId String?
|
||||
TagsOnPublications TagsOnPublications[]
|
||||
childTagsOnTags TagsOnTags[] @relation("childTag")
|
||||
parentTagsOnTags TagsOnTags[] @relation("parentTag")
|
||||
}
|
||||
|
||||
model TagsOnTags {
|
||||
parentTagId String
|
||||
parentTag Tag @relation(fields: [parentTagId], references: [id], "parentTag")
|
||||
childTagId String
|
||||
childTag Tag @relation(fields: [childTagId], references: [id], "childTag")
|
||||
|
||||
@@id([parentTagId, childTagId])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
schemas Schema[]
|
||||
gameSystems GameSystem[]
|
||||
publications Publication[]
|
||||
|
||||
name String?
|
||||
username String? @unique
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
passwordHash String?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
refresh_token_expires_in Int?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
9
recoil/atoms/publication.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TTCQueryParser } from "@/lib/ttcQuery/TTCQueryParser";
|
||||
import { atom } from "recoil";
|
||||
|
||||
export const PublicationAtom = atom({
|
||||
key: "publication",
|
||||
default: new TTCQueryParser({
|
||||
path: { to: { dice: "2d6" } },
|
||||
}),
|
||||
});
|
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: "" },
|
||||
});
|
44
types.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
// MD Parser
|
||||
type IdentifiedToken<M> = {
|
||||
metadata: M;
|
||||
children?: Token[];
|
||||
@@ -30,7 +31,7 @@ type FrontMatter = Record<string, string>;
|
||||
type SearchFunction = (
|
||||
s: string,
|
||||
start: number,
|
||||
end: number,
|
||||
end: number
|
||||
) => {
|
||||
start: number;
|
||||
end: number;
|
||||
@@ -52,5 +53,44 @@ type IdentifierRegistration = <N = Record<string, string>>(
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<N>,
|
||||
renderFunction: TokenRenderer<N>,
|
||||
openTagRx?: RegExp,
|
||||
closeTagRx?: 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;
|
||||
};
|
||||
|
||||
// Query
|
||||
type QueryableObject = Record<string, any>;
|
||||
|