Compare commits

...

19 Commits

Author SHA1 Message Date
c8f20fbda8 Fixes broken accordion ref
Fixes broken poppable ref
adds Schema page
Fixes schema creation not including game system id
2024-09-09 08:37:53 -06:00
a2fde9cc79 game system nav context
sse endpoint
2024-09-08 06:43:39 -06:00
84cbea8ce1 full switch to jotai, finishes schema version query fixes 2024-09-05 05:22:29 -06:00
b529445851 changes schema editor to jotai 2024-09-04 04:28:57 -06:00
f87a759048 new models 2024-09-04 03:08:35 -06:00
5b16cc60f7 enables schema saving in db, initial refactor of db schema for revisions 2024-09-02 03:16:10 -06:00
fd5e5bcc8b ttcmd: attempts to fix infinite loop on p tags 2024-08-22 11:55:57 -06:00
6760d06b18 update last edited 2024-08-21 17:35:44 -06:00
b2e7223c16 Adds capability of on-demand resolver to render more complex stack items 2024-08-21 16:08:10 -06:00
1799c8da79 Adds a default template to resolver, adds "last" variable 2024-08-21 15:43:59 -06:00
9c9edd9e90 resolver rework 2024-08-21 15:19:41 -06:00
3417fdd3d7 Fixes id creator making inaccurate ids 2024-08-21 14:34:35 -06:00
f6fc85bf7e Changed order of resolver fields 2024-08-21 14:23:06 -06:00
e5f3cb0c34 Let's you get a number from the stack to use in dice 2024-08-20 19:49:43 -06:00
df3171b646 More dice stuff 2024-08-20 17:50:10 -06:00
d17ff63662 Markdown editor 2024-08-20 14:56:51 -06:00
e42a938b13 move to postgres, adds user checks to content creation 2024-08-20 09:55:49 -06:00
545656cf22 inlines secret fetching for auth to remove postinstall step 2024-08-20 08:25:02 -06:00
5f2243b49a fixes create to now check for author before creating game system 2024-08-20 07:51:20 -06:00
58 changed files with 1702 additions and 929 deletions

2
.gitignore vendored
View File

@@ -43,3 +43,5 @@ temp.json
temp.md temp.md
.dragonshoard/ .dragonshoard/
certificates

View File

@@ -1,15 +0,0 @@
"use server";
import { prisma } from "@/prisma/prismaClient";
export const createGameSystem = async (name: string) => {
const { id } = await prisma.gameSystem.create({
data: {
name,
},
select: {
id: true,
},
});
return id;
};

View File

@@ -1,7 +0,0 @@
"use server";
import { prisma } from "@/prisma/prismaClient";
// DEV TOOL ONLY
export async function deleteAllGameSystems() {
await prisma.gameSystem.deleteMany();
}

View File

@@ -1,23 +0,0 @@
"use server";
import { prisma } from "@/prisma/prismaClient";
import { redirect } from "next/navigation";
export const createSchema = async (form: FormData) => {
const name = form.get("name")?.toString();
const gsId = form.get("gsId")?.toString();
if (!name || !gsId) return;
const { id } = await prisma.schema.create({
data: {
name,
schema: "{}",
types: "{}",
version: 0,
gameSystemId: gsId,
},
select: { id: true },
});
redirect(`/game-systems/${gsId}/schema/${id}`);
};

View File

@@ -1,20 +0,0 @@
"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;
};

130
actions/Schemas/index.ts Normal file
View File

@@ -0,0 +1,130 @@
"use server";
import { auth } from "@/auth";
import { isEmailVerified } from "@/util/isEmailVerified";
import { redirect } from "next/navigation";
import { prisma } from "@/prisma/prismaClient";
import { Schema } from "@/types";
export const saveSchemaDb = async (s: Schema, version: number) => {
const sesh = await auth();
if (!sesh?.user?.id) return;
const { id, SchemaRevision } = await prisma.schema.upsert({
// data: {
// ...s,
// },
create: {
name: s.name,
SchemaRevision: {
create: {
fields: s.fields,
types: s.types,
},
},
authorId: sesh.user.id,
gameSystemId: s.gameSystemId,
id: undefined,
},
update: {
name: s.name,
},
where: {
id: s.id,
},
include: {
// id: true,
SchemaRevision: {
where: {
version: s.version,
},
select: {
version: true,
isFinal: true,
},
},
},
});
// const schema2 = await prisma.schema.findUnique({where:{id}})
if (
!SchemaRevision.at(0) ||
SchemaRevision[0].version < version ||
SchemaRevision[0].isFinal
) {
await prisma.schemaRevision.create({
data: {
schemaId: id,
types: s.types,
fields: s.fields,
version,
},
});
}
redirect(`/game-systems/${s.gameSystemId}/schema/${id}`);
};
export const findSchema = async (
id: string,
version: number,
): Promise<Schema | null> => {
const schema = await prisma.schema.findFirst({
where: {
id,
},
// include: {
// gameSystem: {
// select: {
// id: true,
// name: true,
// },
// },
// },
include: {
SchemaRevision: {
where: {
version,
},
select: {
version: true,
fields: true,
types: true,
},
},
},
// select: {
// id: true,
// name: true,
// },
});
if (!schema?.SchemaRevision[0]) return null;
return {
fields: schema.SchemaRevision[0].fields,
types: schema.SchemaRevision[0].types,
id: schema.id,
gameSystemId: schema.gameSystemId,
name: schema.name,
} as Schema;
};
export const createSchema = async (form: FormData) => {
const name = form.get("name")?.toString();
const gsId = form.get("gsId")?.toString();
const session = await auth();
if (!name || !gsId || !session?.user?.id || !isEmailVerified(session.user.id))
return;
const { id } = await prisma.schema.create({
data: {
name,
gameSystemId: gsId,
authorId: session.user.id,
},
select: { id: true },
});
redirect(`/game-systems/${gsId}/schema/${id}`);
};

View File

@@ -1,5 +1,5 @@
"use server"; "use server";
import { signIn, signOut } from "@/auth"; import { auth, signIn, signOut } from "@/auth";
export const signInWithDiscord = async () => { export const signInWithDiscord = async () => {
await signIn("discord"); await signIn("discord");
@@ -10,3 +10,5 @@ export const signInWithCreds = async (formData: FormData) => {
}; };
export const signOutOfApp = () => signOut(); export const signOutOfApp = () => signOut();
export const getSession = async () => await auth();

View File

@@ -1,18 +1,21 @@
// import { setCurrentGameSystem } from "@/actions/GameSystems/client";
import { setCurrentGameSystem } from "@/actions/GameSystems/client";
import { auth } from "@/auth";
import { prisma } from "@/prisma/prismaClient"; import { prisma } from "@/prisma/prismaClient";
import Link from "next/link"; import Link from "next/link";
export default async function GameSystem( export default async function GameSystem({
{ params: { id } }: { params: { id: string } }, params: { id },
) { }: {
params: { id: string };
}) {
if (!id) throw "HOW DID YOU GET HERE?"; if (!id) throw "HOW DID YOU GET HERE?";
const gameSystem = await prisma.gameSystem.findFirst({ const gameSystem = await prisma.gameSystem.findFirst({
where: { where: {
id, id,
}, },
select: { include: {
id: true,
name: true,
schemas: { schemas: {
select: { select: {
name: true, name: true,
@@ -26,8 +29,16 @@ export default async function GameSystem(
}, },
}, },
}, },
// select: {
// id: true,
// name: true,
// },
}); });
const session = await auth();
session?.user?.id && (await setCurrentGameSystem(session.user.id, id));
return ( return (
<> <>
<section className="heading"> <section className="heading">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { deleteAllGameSystems } from "@/actions/GameSystems/deleteAll"; import { deleteAllGameSystems } from "@/actions/GameSystems/devactions";
import { DevTool } from "@/components/devtools/DevTool"; import { DevTool } from "@/components/devtools/DevTool";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { FC, PropsWithChildren } from "react"; import { FC, PropsWithChildren } from "react";

View File

@@ -1,20 +1,22 @@
import { prisma } from "@/prisma/prismaClient"; "use client";
import { createGameSystem } from "@/actions/GameSystems";
import { useToast } from "@/components/toast";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default function CreateGameSystem() { export default function CreateGameSystem() {
const { createToast } = useToast();
async function create(form: FormData) { async function create(form: FormData) {
"use server";
const name = form.get("name")?.toString(); const name = form.get("name")?.toString();
if (!name) return; if (!name)
const { id } = await prisma.gameSystem.create({ return createToast({ msg: "Please provide a name", fading: true });
data: { createToast({ msg: "Creating Game System", fading: true });
name, const id = await createGameSystem(name);
}, if (!id)
select: { return createToast({
id: true, msg: "Issue creating game system. Is your email verified?",
}, fading: true,
}); type: "error",
});
redirect(`/game-systems/${id}`); redirect(`/game-systems/${id}`);
} }
@@ -22,7 +24,6 @@ export default function CreateGameSystem() {
<form action={create}> <form action={create}>
<input <input
type="text" type="text"
// {...bind}
name="name" name="name"
placeholder="Create a new game system..." placeholder="Create a new game system..."
className="w-min" className="w-min"

View File

@@ -2,8 +2,12 @@ import { prisma } from "@/prisma/prismaClient";
import CreateGameSystem from "./create"; import CreateGameSystem from "./create";
import { GameSystemsClient } from "./client"; import { GameSystemsClient } from "./client";
import Link from "next/link"; import Link from "next/link";
import { setCurrentGameSystem } from "@/actions/GameSystems/client";
import { auth } from "@/auth";
export default async function GameSystems() { export default async function GameSystems() {
const session = await auth();
session?.user?.id && (await setCurrentGameSystem(session.user.id));
const existingGameSystems = await prisma.gameSystem.findMany({ const existingGameSystems = await prisma.gameSystem.findMany({
orderBy: { orderBy: {
created: "asc", created: "asc",

View File

@@ -1,13 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Roboto } from "next/font/google"; import { Roboto } from "next/font/google";
import "./globals.css"; import "./globals.css";
import {
BookOpenIcon,
CircleStackIcon,
Cog8ToothIcon,
PuzzlePieceIcon,
QuestionMarkCircleIcon,
} from "@heroicons/react/24/solid";
import Link from "next/link"; import Link from "next/link";
import { DevToolboxContextProvider } from "@/components/devtools/context"; import { DevToolboxContextProvider } from "@/components/devtools/context";
import { RecoilRootClient } from "@/components/recoilRoot"; import { RecoilRootClient } from "@/components/recoilRoot";
@@ -15,6 +8,10 @@ import { JotaiProvider } from "@/components/jotaiProvider";
import { Toaster } from "@/components/toast"; import { Toaster } from "@/components/toast";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import { User } from "@/components/user/index"; import { User } from "@/components/user/index";
import { getCurrentGameSystem } from "@/actions/GameSystems";
import { Nav } from "@/components/nav";
import { SSE } from "@/components/sse";
import { auth } from "@/auth";
const roboto = Roboto({ subsets: ["latin"], weight: "400" }); const roboto = Roboto({ subsets: ["latin"], weight: "400" });
@@ -23,38 +20,12 @@ export const metadata: Metadata = {
description: "Rules and tools for tabletop games!", description: "Rules and tools for tabletop games!",
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const navItems = [ const currentGame = await getCurrentGameSystem();
{
to: "/game-systems",
icon: PuzzlePieceIcon,
text: "Game Systems",
},
{
to: "/schemas",
icon: CircleStackIcon,
text: "Schemas",
},
{
to: "/publications",
icon: BookOpenIcon,
text: "Publications",
},
{
to: "/settings",
icon: Cog8ToothIcon,
text: "Settings",
},
{
to: "/help",
icon: QuestionMarkCircleIcon,
text: "How do?",
},
];
return ( return (
<html lang="en"> <html lang="en">
@@ -65,19 +36,7 @@ export default function RootLayout({
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600"> <h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
<Link href="/">Tabletop Commander</Link> <Link href="/">Tabletop Commander</Link>
</h1> </h1>
<ul className="my-6 flex flex-col gap-6"> <Nav game={currentGame ?? undefined} />
{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"> <div className="mt-auto">
<User /> <User />
</div> </div>
@@ -96,6 +55,7 @@ export default function RootLayout({
<div id="root-portal"></div> <div id="root-portal"></div>
</body> </body>
</SessionProvider> </SessionProvider>
<SSE />
</html> </html>
); );
} }

5
app/testing/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { MDEditor } from "@/components/mdeditor";
export default function Testing() {
return <MDEditor />;
}

View File

@@ -5,45 +5,38 @@ import Credentials from "next-auth/providers/credentials";
import Discord from "next-auth/providers/discord"; import Discord from "next-auth/providers/discord";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { SecretClient } from "@/lib/secret/init";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export const { handlers, signIn, signOut, auth } = NextAuth({ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
providers: [ const sClient = SecretClient();
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( const clientId = await sClient.fetchSecret("discord_client_id");
credentials.password as string const clientSecret = await sClient.fetchSecret("discord_client_secret");
);
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) { return {
user = await prisma.user.create({ providers: [
data: { Discord({
clientId,
clientSecret,
// redirectProxyUrl:
// "https://bottomsurgery.local:3000/api/auth/callback/discord",
}),
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, email: credentials.email as string,
passwordHash: pwHash,
}, },
select: { select: {
name: true, name: true,
@@ -51,18 +44,35 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
email: true, email: true,
emailVerified: true, emailVerified: true,
username: 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; return user;
} },
}),
user.passwordHash = null; ],
adapter: PrismaAdapter(prisma),
return user; };
},
}),
],
adapter: PrismaAdapter(prisma),
}); });
async function saltAndHashPassword(password: string) { async function saltAndHashPassword(password: string) {
const hash = await bcrypt.hash(password, 10); const hash = await bcrypt.hash(password, 10);

View File

@@ -1,6 +1,6 @@
import { FC, PropsWithChildren } from "react"; import { FC, PropsWithChildren } from "react";
import { Poppable } from "@/lib/poppables/components/poppable"; import { Poppable } from "@/lib/poppables/components/poppable";
import { Icon } from "@/components/Icon"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
export const HelpPopper: FC<PropsWithChildren> = ({ children }) => { export const HelpPopper: FC<PropsWithChildren> = ({ children }) => {
return ( return (
@@ -9,7 +9,7 @@ export const HelpPopper: FC<PropsWithChildren> = ({ children }) => {
preferredAlign="centered" preferredAlign="centered"
preferredEdge="bottom" preferredEdge="bottom"
> >
<Icon icon="Help" className="svg-white w-4 h-4" /> <QuestionMarkCircleIcon className="w-4 h-4 fill-white" />
</Poppable> </Poppable>
); );
}; };

View File

@@ -0,0 +1,21 @@
import CodeMirror from "@uiw/react-codemirror";
import { markdown } from "@codemirror/lang-markdown";
import { duotoneDark } from "@uiw/codemirror-theme-duotone";
interface TextEditorProps {
value: string;
onChange: (value: string) => void;
}
export const TextEditor: React.FC<TextEditorProps> = ({ value, onChange }) => {
return (
<CodeMirror
value={value}
extensions={[markdown({ extensions: [] })]}
onChange={(value, _viewUpdate) => {
onChange(value);
}}
theme={duotoneDark}
/>
);
};

View File

@@ -0,0 +1,19 @@
"use client";
import { useDeferredValue, useState } from "react";
import { TextEditor } from "./TextEditor";
import { TTCMD } from "../ttcmd";
export const MDEditor: React.FC = () => {
const [text, setText] = useState("??<<2d6,$0.distribution>>");
const body = useDeferredValue(text);
return (
<div className="flex gap-8">
<div className="w-1/2">
<TextEditor value={text} onChange={setText} />
</div>
<div className="w-1/2">
<TTCMD body={body} parserId="preview" title="preview" />
</div>
</div>
);
};

View File

@@ -2,13 +2,14 @@ import { FC, useCallback, useEffect, useState } from "react";
import { useObjectStateWrapper } from "../../hooks/useObjectState"; import { useObjectStateWrapper } from "../../hooks/useObjectState";
import { ValueField } from "./value-field"; import { ValueField } from "./value-field";
import { HelpPopper } from "../Poppables/help"; import { HelpPopper } from "../Poppables/help";
import { Icon } from "../Icon";
import { RESERVED_FIELDS } from "../../constants/ReservedFields"; import { RESERVED_FIELDS } from "../../constants/ReservedFields";
import { import {
fieldTypeOptions, fieldTypeOptions,
FieldTypes, FieldTypes,
fieldTypesWithValues, fieldTypesWithValues,
} from "./fieldtypes"; } from "./fieldtypes";
import { TrashIcon } from "@heroicons/react/24/solid";
import { FieldType } from "@/types";
interface IProps { interface IProps {
update: (arg: FieldType) => void; update: (arg: FieldType) => void;
@@ -17,9 +18,12 @@ interface IProps {
deleteField: (arg: string) => void; deleteField: (arg: string) => void;
} }
export const FieldEditor: FC<IProps> = ( export const FieldEditor: FC<IProps> = ({
{ update, field, fieldName, deleteField }, update,
) => { field,
fieldName,
deleteField,
}) => {
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper( const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(
field, field,
(e) => update(typeof e === "function" ? e(field) : e), (e) => update(typeof e === "function" ? e(field) : e),
@@ -83,9 +87,8 @@ export const FieldEditor: FC<IProps> = (
)} )}
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<label> <label>
<input type="checkbox" {...bindPropertyCheck("isConstant")} /> <input type="checkbox" {...bindPropertyCheck("isConstant")} /> Is
{" "} constant
Is constant
</label> </label>
<HelpPopper> <HelpPopper>
<p className="text-sm"> <p className="text-sm">
@@ -108,7 +111,11 @@ export const FieldEditor: FC<IProps> = (
</label> </label>
<label className="w-min"> <label className="w-min">
Limit: Limit:
<input className="w-12 min-w-min" type="number" {...bindProperty("limit")} /> <input
className="w-12 min-w-min"
type="number"
{...bindProperty("limit")}
/>
</label> </label>
<HelpPopper> <HelpPopper>
<p className="text-sm"> <p className="text-sm">
@@ -122,11 +129,7 @@ export const FieldEditor: FC<IProps> = (
className="no-default self-end ml-auto" className="no-default self-end ml-auto"
onClick={() => deleteField(fieldName)} onClick={() => deleteField(fieldName)}
> >
<Icon <TrashIcon className="w-6 h-6 fill-white" />
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
icon="Trash"
>
</Icon>
</button> </button>
</div> </div>
)} )}

View File

@@ -1,14 +1,15 @@
import { useRecoilValue } from "recoil";
import { SchemaEditAtom } from "../../recoil/atoms/schema"; import { SchemaEditAtom } from "../../recoil/atoms/schema";
import { TEMPLATE_TYPES } from "../../constants/TemplateTypes"; import { TEMPLATE_TYPES } from "../../constants/TemplateTypes";
import { FC, PropsWithChildren } from "react"; import { FC, PropsWithChildren } from "react";
import { useAtom } from "jotai";
import { InputBinder } from "@/types";
interface IProps { interface IProps {
bind: InputBinder; bind: InputBinder;
} }
export const FieldTypeInput: FC<PropsWithChildren<IProps>> = ({ bind }) => { export const FieldTypeInput: FC<PropsWithChildren<IProps>> = ({ bind }) => {
const schema = useRecoilValue(SchemaEditAtom); const [schema] = useAtom(SchemaEditAtom);
return ( return (
<label className="w-min"> <label className="w-min">

View File

@@ -1,30 +1,48 @@
"use client"; "use client";
import { FC, useCallback, useState } from "react"; import { FC, useCallback, useEffect, useState } from "react";
import AnimatedPageContainer from "@/components/AnimatedPageContainer"; import AnimatedPageContainer from "@/components/AnimatedPageContainer";
import { TypeEditor } from "./type-editor"; import { TypeEditor } from "./type-editor";
import { useObjectStateWrapper } from "@/hooks/useObjectState"; import { useObjectStateWrapper } from "@/hooks/useObjectState";
import { useInput } from "../../hooks/useInput"; import { useInput } from "../../hooks/useInput";
import { useRecoilState, useResetRecoilState } from "recoil";
import { SchemaEditAtom } from "@/recoil/atoms/schema"; import { SchemaEditAtom } from "@/recoil/atoms/schema";
import { SchemaViewer } from "./schema-viewer"; import { SchemaViewer } from "./schema-viewer";
import { TemplateEditor } from "./template-editor"; import { TemplateEditor } from "./template-editor";
import { Icon } from "@/components/Icon"; import { Icon } from "@/components/Icon";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { FieldTypes } from "./fieldtypes"; import { FieldTypes } from "./fieldtypes";
import { prisma } from "@/prisma/prismaClient"; import { findSchema, saveSchemaDb } from "@/actions/Schemas/index";
import { useToast } from "../toast";
import { useAtom } from "jotai";
import { Schema, TypeType } from "@/types";
import { TrashIcon } from "@heroicons/react/24/solid";
import { PencilSquareIcon } from "@heroicons/react/24/solid";
export const SchemaBuilder: FC = () => { export const SchemaBuilder: FC = () => {
const [schema, setSchema] = useRecoilState(SchemaEditAtom); const [schema, setSchema] = useAtom<Schema>(SchemaEditAtom);
const resetSchema = useResetRecoilState(SchemaEditAtom); // const resetSchema = useResetRecoilState(SchemaEditAtom);
const { createToast } = useToast();
const { update: updateSchema, bindProperty: bindSchemaProperty } = const { update: updateSchema, bindProperty: bindSchemaProperty } =
useObjectStateWrapper<Schema>(schema, setSchema); useObjectStateWrapper<Schema>(schema, setSchema);
const { schemaId, gameSystemId } = useParams<{ const { schemaId, id: gameSystemId } = useParams<{
schemaId: string; schemaId: string;
gameSystemId?: string; id: string;
}>(); }>();
useEffect(() => {
if (schemaId !== "create" && schemaId !== schema.id)
findSchema(schemaId, 0).then((sc) => {
if (!sc) return;
setSchema(sc);
});
}, [schema.id, schemaId, setSchema]);
useEffect(() => {
if (gameSystemId && !schema.gameSystemId)
setSchema((sc) => ({ ...sc, gameSystemId }));
}, [gameSystemId, schema.gameSystemId, setSchema]);
const { const {
value: typeName, value: typeName,
bind: bindTypeName, bind: bindTypeName,
@@ -33,7 +51,7 @@ export const SchemaBuilder: FC = () => {
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [lastSaved, setLastSaved] = useState(schema); const [lastSaved, _setLastSaved] = useState(schema);
const [selectedType, setSelectedType] = useState(""); const [selectedType, setSelectedType] = useState("");
@@ -49,24 +67,13 @@ export const SchemaBuilder: FC = () => {
setPageNumber(0); setPageNumber(0);
setSelectedType(""); setSelectedType("");
}, },
[resetTypeName, updateSchema] [resetTypeName, updateSchema],
); );
const saveSchema = useCallback(async () => { const saveSchema = useCallback(async () => {
// "use server"; createToast({ msg: "Saving Schema", fading: true });
// setLastSaved(schema); await saveSchemaDb(schema, schema.version);
// await prisma.schema.upsert({ }, [createToast, schema]);
// 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) => { const selectTypeForEdit = useCallback((typeKey: string) => {
setSelectedType(typeKey); setSelectedType(typeKey);
@@ -80,27 +87,24 @@ export const SchemaBuilder: FC = () => {
} = useInput("", { disallowSpaces: true }); } = useInput("", { disallowSpaces: true });
const addSchemaField = useCallback(() => { const addSchemaField = useCallback(() => {
updateSchema((s) => ({ updateSchema((s) => ({
schema: { fields: {
...s.schema, ...s.fields,
[schemaFieldName]: { [schemaFieldName]: FieldTypes.any,
display: "",
type: FieldTypes.any,
},
}, },
})); }));
resetSchemaFieldName(); resetSchemaFieldName();
}, [resetSchemaFieldName, schemaFieldName, updateSchema]); }, [resetSchemaFieldName, schemaFieldName, updateSchema]);
const updateSchemaField = useCallback( const updateSchemaField = useCallback(
(key: string, template: Template) => { (key: string, fieldType: FieldTypes) => {
updateSchema((s) => ({ updateSchema((s) => ({
schema: { fields: {
...s.schema, ...s.fields,
[key]: template, [key]: fieldType,
}, },
})); }));
}, },
[updateSchema] [updateSchema],
); );
const deleteType = useCallback( const deleteType = useCallback(
@@ -111,7 +115,7 @@ export const SchemaBuilder: FC = () => {
return { types }; return { types };
}); });
}, },
[updateSchema] [updateSchema],
); );
return ( return (
@@ -133,15 +137,15 @@ export const SchemaBuilder: FC = () => {
</button> </button>
</div> </div>
<ul className="rounded-lg overflow-hidden"> <ul className="rounded-lg overflow-hidden">
{Object.entries(schema.schema).map( {Object.entries(schema.fields).map(
([schemaFieldKey, schemaField]) => ( ([schemaFieldKey, schemaField]) => (
<TemplateEditor <TemplateEditor
key={schemaFieldKey} key={schemaFieldKey}
templateKey={schemaFieldKey} templateKey={schemaFieldKey}
template={schemaField} fieldType={schemaField as FieldTypes}
update={updateSchemaField} update={updateSchemaField}
/> />
) ),
)} )}
</ul> </ul>
</div> </div>
@@ -182,20 +186,14 @@ export const SchemaBuilder: FC = () => {
className="no-default" className="no-default"
onClick={() => selectTypeForEdit(t)} onClick={() => selectTypeForEdit(t)}
> >
<Icon <PencilSquareIcon className="w-6 h-6 fill-white" />
icon="Anvil"
className="anvil svg-olive-drab hover:svg-olive-drab-100 w-6 h-6"
/>
</button> </button>
<button <button
title="Delete" title="Delete"
className="no-default" className="no-default"
onClick={() => deleteType(t)} onClick={() => deleteType(t)}
> >
<Icon <TrashIcon className="w-6 h-6 fill-white" />
icon="Trash"
className="trash-can svg-red-700 hover:svg-red-500 w-6 h-6"
/>
</button> </button>
</div> </div>
</li> </li>

View File

@@ -34,10 +34,10 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
<hr /> <hr />
<p className="font-bold italic">Templates</p> <p className="font-bold italic">Templates</p>
<ul> <ul>
{Object.entries(schema.schema).map(([templateKey, template]) => ( {Object.entries(schema.fields).map(([templateKey, template]) => (
<li key={templateKey}> <li key={templateKey}>
<p className="font-bold">{templateKey}</p> <p className="font-bold">{templateKey}</p>
<p className="font-thin text-xs">{template.type}</p> <p className="text-mixed-600 ml-2">Type: {template}</p>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -1,39 +1,38 @@
import { FC, useCallback } from "react"; import { FC, useCallback, useEffect } from "react";
import { useObjectStateWrapper } from "@/hooks/useObjectState";
import { TEMPLATE_TYPES } from "@/constants/TemplateTypes"; import { TEMPLATE_TYPES } from "@/constants/TemplateTypes";
import { SchemaEditAtom } from "@/recoil/atoms/schema"; import { SchemaEditAtom } from "@/recoil/atoms/schema";
import { useRecoilState } from "recoil";
import { Icon } from "@/components/Icon"; import { Icon } from "@/components/Icon";
import { FieldTypes } from "./fieldtypes";
import { useAtom } from "jotai";
import { useInput } from "@/hooks/useInput";
import { Schema } from "@/types";
import { TrashIcon } from "@heroicons/react/24/solid";
interface IProps { interface IProps {
templateKey: string; templateKey: string;
update: (arg0: string, arg1: Template) => void; update: (arg0: string, arg1: FieldTypes) => void;
template: Template; fieldType: FieldTypes;
} }
export const TemplateEditor: FC<IProps> = ( export const TemplateEditor: FC<IProps> = ({
{ templateKey, update, template }, templateKey,
) => { update,
const [schema, setSchema] = useRecoilState(SchemaEditAtom); fieldType,
const updateTemplate = useCallback( }) => {
(t: Template | ((arg: Template) => Template)) => { const [schema, setSchema] = useAtom(SchemaEditAtom);
update(templateKey, typeof t === "function" ? t(template) : t); const { bind: bindFieldType, value } = useInput(fieldType);
},
[templateKey, update, template],
);
const { bindProperty } = useObjectStateWrapper( useEffect(() => {
template, update(templateKey, value);
updateTemplate, }, []);
);
const deleteTemplate = useCallback(() => { const deleteField = useCallback(() => {
setSchema((s: Schema) => { setSchema((s: Schema) => {
const templates = { ...s.schema }; const fields = { ...s.fields };
delete templates[templateKey]; delete fields[templateKey];
return { return {
...s, ...s,
schema: templates, schema: fields,
}; };
}); });
}, [setSchema, templateKey]); }, [setSchema, templateKey]);
@@ -46,28 +45,25 @@ export const TemplateEditor: FC<IProps> = (
Type: Type:
<input <input
type="text" type="text"
{...bindProperty("type", { disallowSpaces: true })} {...bindFieldType}
list="type-editor-type-list" list="type-editor-type-list"
/> />
<datalist id="type-editor-type-list"> <datalist id="type-editor-type-list">
{Object.keys(TEMPLATE_TYPES).map((k) => ( {Object.keys(TEMPLATE_TYPES).map((k) => (
<option key={"templatetype" + k} value={k}>{k}</option> <option key={"templatetype" + k} value={k}>
{k}
</option>
))} ))}
{Object.keys(schema.types).map((k) => ( {Object.keys(schema.types).map((k) => (
<option key={"schematype" + k} value={k}>{k}</option> <option key={"schematype" + k} value={k}>
{k}
</option>
))} ))}
</datalist> </datalist>
</label> </label>
<textarea {...bindProperty("display")} cols={30} rows={10}></textarea>
</div> </div>
<button <button className="no-default" onClick={deleteField}>
className="no-default" <TrashIcon className="w-6 h-6 fill-white" />
onClick={deleteTemplate}
>
<Icon
icon="Trash"
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
/>
</button> </button>
</div> </div>
</li> </li>

View File

@@ -9,6 +9,7 @@ import { useObjectState } from "../../hooks/useObjectState";
import { useInput } from "../../hooks/useInput"; import { useInput } from "../../hooks/useInput";
import { FieldEditor } from "./field-editor"; import { FieldEditor } from "./field-editor";
import { FieldTypes } from "./fieldtypes"; import { FieldTypes } from "./fieldtypes";
import { FieldType, TypeType } from "@/types";
interface IProps { interface IProps {
name: string; name: string;
@@ -18,9 +19,11 @@ interface IProps {
const constantProperties = ["metadata"]; const constantProperties = ["metadata"];
export const TypeEditor: FC<PropsWithChildren<IProps>> = ( export const TypeEditor: FC<PropsWithChildren<IProps>> = ({
{ saveType, name, type: passedType }, saveType,
) => { name,
type: passedType,
}) => {
const { const {
update: updateType, update: updateType,
reset: resetType, reset: resetType,
@@ -39,19 +42,22 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
resetType(); resetType();
}; };
const addField = useCallback((e: FormEvent) => { const addField = useCallback(
e.preventDefault(); (e: FormEvent) => {
updateType({ e.preventDefault();
[propertyName]: { updateType({
type: FieldTypes.number, [propertyName]: {
value: "", type: FieldTypes.number,
isConstant: false, value: "",
limit: 1, isConstant: false,
minimum: 1, limit: 1,
}, minimum: 1,
}); },
resetPropertyName(); });
}, [propertyName, updateType, resetPropertyName]); resetPropertyName();
},
[propertyName, updateType, resetPropertyName],
);
const updateField = useCallback( const updateField = useCallback(
(k: keyof typeof type) => (field: FieldType) => { (k: keyof typeof type) => (field: FieldType) => {
@@ -64,13 +70,16 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
passedType && setType(passedType); passedType && setType(passedType);
}, [passedType, setType]); }, [passedType, setType]);
const deleteField = useCallback((name: string) => { const deleteField = useCallback(
setType((t) => { (name: string) => {
const fields = { ...t }; setType((t) => {
delete fields[name]; const fields = { ...t };
return fields; delete fields[name];
}); return fields;
}, [setType]); });
},
[setType],
);
return ( return (
<div> <div>
@@ -82,17 +91,18 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
<button disabled={!propertyName}>Add Field</button> <button disabled={!propertyName}>Add Field</button>
</form> </form>
<ul className="rounded-lg overflow-hidden"> <ul className="rounded-lg overflow-hidden">
{Object.entries(type).reverse().filter(([k]) => {Object.entries(type)
!constantProperties.includes(k) .reverse()
).map(([key, value]) => ( .filter(([k]) => !constantProperties.includes(k))
<FieldEditor .map(([key, value]) => (
key={"field-editor" + key} <FieldEditor
field={value} key={"field-editor" + key}
update={updateField(key)} field={value}
fieldName={key} update={updateField(key)}
deleteField={deleteField} fieldName={key}
/> deleteField={deleteField}
))} />
))}
</ul> </ul>
<div> <div>
<button onClick={save} disabled={!Object.keys(type).length}> <button onClick={save} disabled={!Object.keys(type).length}>

View File

@@ -17,6 +17,13 @@ export default function SignIn() {
type="password" type="password"
name="password" name="password"
/> />
<button
role="button"
type="submit"
className="w-full p-2 rounded-lg bg-primary-500"
>
Sign In
</button>
</form> </form>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div> <div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div>
@@ -24,7 +31,10 @@ export default function SignIn() {
<div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div> <div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div>
</div> </div>
<form action={signInWithDiscord}> <form action={signInWithDiscord}>
<button className="w-full p-2 bg-[#816ab1] rounded-lg" type="submit"> <button
className="w-full p-2 bg-[#816ab1] rounded-lg flex items-center justify-center"
type="submit"
>
<Icon icon="Discord" className="mr-4 inline-block" /> <Icon icon="Discord" className="mr-4 inline-block" />
Sign in with Discord Sign in with Discord
</button> </button>

View File

@@ -6,17 +6,21 @@ interface IProps {
title?: ReactNode; title?: ReactNode;
} }
export const Accordion: FC<PropsWithChildren<IProps>> = ( export const Accordion: FC<PropsWithChildren<IProps>> = ({
{ children, expandOnHover, expanded, title }, children,
) => { expandOnHover,
expanded,
title,
}) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<div <div
data-expanded={open || expanded} data-expanded={open || expanded}
data-expandonhover={expandOnHover} data-expandonhover={expandOnHover}
className={(expandOnHover ? "group/hover" : "group/controlled") + className={
" group"} (expandOnHover ? "group/hover" : "group/controlled") + " group"
}
onClick={() => !title && !expandOnHover && setOpen(!open)} onClick={() => !title && !expandOnHover && setOpen(!open)}
> >
{!!title && ( {!!title && (
@@ -24,9 +28,7 @@ export const Accordion: FC<PropsWithChildren<IProps>> = (
className="flex justify-between cursor-pointer" className="flex justify-between cursor-pointer"
onClick={() => !expandOnHover && setOpen(!open)} onClick={() => !expandOnHover && setOpen(!open)}
> >
<div className="accordion-title"> <div className="accordion-title">{title}</div>
{title}
</div>
<div <div
className={` className={`
group-hover/hover:-rotate-180 group-hover/hover:-rotate-180
@@ -41,10 +43,8 @@ export const Accordion: FC<PropsWithChildren<IProps>> = (
scale-y-50 scale-y-50
`} `}
> >
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"> <span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"></span>
</span> <span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"></span>
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center">
</span>
</div> </div>
</div> </div>
)} )}
@@ -64,15 +64,15 @@ export const AccordionContent: FC<PropsWithChildren> = ({ children }) => {
} }
}, []); }, []);
const Child = () => (
<div className="absolute bottom-0 w-full" ref={updateRef}>
{children}
</div>
);
return ( return (
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
{<Child />} <div
key={"accordion-content"}
className="absolute bottom-0 w-full"
ref={updateRef}
>
{children}
</div>
<span <span
style={{ ["--v-height" as never]: height + "px" }} style={{ ["--v-height" as never]: height + "px" }}
className="w-0 block h-0 group-hover/hover:h-variable group-data-[expanded]/controlled:h-variable transition-all duration-700" className="w-0 block h-0 group-hover/hover:h-variable group-data-[expanded]/controlled:h-variable transition-all duration-700"

View File

@@ -1,9 +1,17 @@
import { sum } from "./utils/sum";
export class Dice { export class Dice {
private count!: number; private count!: number;
private sides!: number; private sides!: number;
private diceString: string;
toString() {
return this.diceString;
}
constructor(dice: string) { constructor(dice: string) {
this.parseDice(dice); this.parseDice(dice);
this.diceString = dice;
} }
private parseDice(dice: string) { private parseDice(dice: string) {
@@ -13,11 +21,23 @@ export class Dice {
} }
public roll() { public roll() {
let total = 0; let results = [];
for (let i = 0; i < this.count; i++) { for (let i = 0; i < this.count; i++) {
total += this.rollSingle(); results.push(this.rollSingle());
} }
return total; return {
total: sum(...results),
max: Math.max(...results),
min: Math.min(...results),
results,
};
}
public rollMax() {
return this.roll().max;
}
public rollMin() {
return this.roll().min;
} }
private rollSingle() { private rollSingle() {
@@ -25,13 +45,13 @@ export class Dice {
} }
public rollAvg() { public rollAvg() {
return this.roll() / this.count; return this.roll().total / this.count;
} }
public rollTimes(times: number) { public rollTimes(times: number) {
let total = 0; let total = 0;
for (let i = 0; i < times; i++) { for (let i = 0; i < times; i++) {
total += this.roll(); total += this.roll().total;
} }
return total; return total;
} }
@@ -58,32 +78,36 @@ export class Dice {
return this.computeDistribution(); return this.computeDistribution();
} }
private computeDistribution(): Record<number, number> { public computeDistribution(): Record<number, number> {
const maxSum = this.count * this.sides;
const dp: number[][] = Array.from({ length: this.count + 1 }, () =>
Array(maxSum + 1).fill(0)
);
dp[0][0] = 1;
for (let dice = 1; dice <= this.count; dice++) {
for (let sum = 0; sum <= maxSum; sum++) {
for (let face = 1; face <= this.sides; face++) {
if (sum >= face) {
dp[dice][sum] += dp[dice - 1][sum - face];
}
}
}
}
const distribution: Record<number, number> = {}; const distribution: Record<number, number> = {};
for (let sum = this.count; sum <= maxSum; sum++) {
// Helper function to compute the sum distribution for given number of dice distribution[sum] = dp[this.count][sum];
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; return distribution;
} }
// STATIC // STATIC
static isDice(d: string) { static isDice(d: string) {
return /\d+[dD]\d+/.test(d); return /\d+[dD]\d+/.test(d);
} }
} }
// globalThis.Dice = Dice;

View File

@@ -1,6 +1,13 @@
"use client"; "use client";
import { FC, PropsWithChildren, ReactNode, useCallback, useState } from "react"; import {
FC,
PropsWithChildren,
ReactNode,
useCallback,
useRef,
useState,
} from "react";
import { PoppableContent } from "./poppable-content"; import { PoppableContent } from "./poppable-content";
import { useDebounce } from "../../../hooks/useDebounce"; import { useDebounce } from "../../../hooks/useDebounce";
@@ -12,38 +19,45 @@ interface IProps {
spacing?: number; spacing?: number;
} }
export const Poppable: FC<PropsWithChildren<IProps>> = ( export const Poppable: FC<PropsWithChildren<IProps>> = ({
{ className, content, children, preferredEdge, preferredAlign, spacing }, className,
) => { content,
children,
preferredEdge,
preferredAlign,
spacing,
}) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const closing = useDebounce(!isHovered, 1000); const closing = useDebounce(!isHovered, 1000);
const closed = useDebounce(closing, 300); const closed = useDebounce(closing, 300);
const [ref, setRef] = useState<HTMLElement>(); // const [ref, setRef] = useState<HTMLElement>();
const updateRef = useCallback((node: HTMLElement) => { // const updateRef = useCallback((node: HTMLElement) => {
if (!node) return; // if (!node) return;
setRef(node); // setRef(node);
}, []); // }, []);
const ref = useRef(null);
return ( return (
<> <>
<span <span
ref={updateRef} ref={ref}
className={className} className={className}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
{children} {children}
</span> </span>
{!!ref && ( {!!ref.current && (
<PoppableContent <PoppableContent
preferredAlign={preferredAlign} preferredAlign={preferredAlign}
preferredEdge={preferredEdge} preferredEdge={preferredEdge}
spacing={spacing} spacing={spacing}
isClosing={closing} isClosing={closing}
isClosed={closed} isClosed={closed}
relativeElement={ref} relativeElement={ref.current}
setHover={setIsHovered} setHover={setIsHovered}
> >
{content} {content}

View File

@@ -1,5 +1,7 @@
// import { mkdirSync, readFileSync, writeFileSync } from "fs"; // import { mkdirSync, readFileSync, writeFileSync } from "fs";
// import { writeFile } from "fs/promises"; import { writeFile } from "fs/promises";
import { mkdirSync, readFileSync } from "fs";
export class DHSecretClient { export class DHSecretClient {
private token!: Promise<string>; //Set by init private token!: Promise<string>; //Set by init
@@ -18,9 +20,9 @@ export class DHSecretClient {
private cacheDir: string, private cacheDir: string,
) { ) {
this.cacheLocation = this.cacheDir.trim().replace(/\/^/, "") + "/.dh_cache"; this.cacheLocation = this.cacheDir.trim().replace(/\/^/, "") + "/.dh_cache";
// mkdirSync(this.cacheDir, { recursive: true }); mkdirSync(this.cacheDir, { recursive: true });
// writeFileSync(this.cacheLocation, "{}", { encoding: "utf-8", flag: "wx" });
// this.readDiskCache(); this.readDiskCache();
this.token = this.fetchToken(); this.token = this.fetchToken();
} }
@@ -43,19 +45,23 @@ export class DHSecretClient {
return token; return token;
} }
// private readDiskCache() { private readDiskCache() {
// const cache = readFileSync(this.cacheLocation, "utf-8"); try {
const cache = readFileSync(this.cacheLocation, "utf-8");
// this.cache = JSON.parse(cache || "{}"); this.cache = JSON.parse(cache || "{}");
// } } catch {
// private async writeDiskCache() { this.cache = {};
// await writeFile(this.cacheLocation, JSON.stringify(this.cache), "utf-8"); this.writeDiskCache().then(this.readDiskCache);
// } }
}
private async writeDiskCache() {
await writeFile(this.cacheLocation, JSON.stringify(this.cache), "utf-8");
}
private writeCache(key: string, value: string, expires?: number) { private writeCache(key: string, value: string, expires?: number) {
this.cache[key] = { value, expires }; this.cache[key] = { value, expires };
// this.writeDiskCache(); this.writeDiskCache();
} }
private readCache(key: string) { private readCache(key: string) {
@@ -73,7 +79,10 @@ export class DHSecretClient {
} }
async fetchSecret(secret_name: string, environment?: string) { async fetchSecret(secret_name: string, environment?: string) {
const uri = this.dhBaseUri + "/api/keys/" + secret_name + const uri =
this.dhBaseUri +
"/api/keys/" +
secret_name +
(environment ? "?env=" + environment : ""); (environment ? "?env=" + environment : "");
const cached = this.readCache(secret_name); const cached = this.readCache(secret_name);

View File

@@ -5,8 +5,8 @@ if (!globalThis.Secrets) {
"https://dragonshoard.cyborggrizzly.com", "https://dragonshoard.cyborggrizzly.com",
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? "./.dragonshoard" ? "./.dragonshoard"
: "/.dragonshoard", : "/.dragonshoard"
); );
} }
export const SecretClient = () => globalThis.Secrets; export const SecretClient = (): DHSecretClient => globalThis.Secrets;

View File

@@ -1,18 +1,27 @@
import { PublicationAtom } from "@/recoil/atoms/publication"; import { PublicationAtom } from "@/recoil/atoms/publication";
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef, ReactNode } from "react";
import { useRecoilValue } from "recoil";
import { TTCQueryResolver } from "../ttcQuery/TTCResolvers"; import { TTCQueryResolver } from "../ttcQuery/TTCResolvers";
import { useAtom } from "jotai";
export function Resolver({ resolver }: { resolver: string }) { export function Resolver({ resolver }: { resolver: string }) {
const parser = useRecoilValue(PublicationAtom); const [parser] = useAtom(PublicationAtom);
const [res] = useState(new TTCQueryResolver(parser)); const [res] = useState(new TTCQueryResolver(parser));
const [content, setContent] = useState(""); const [content, setContent] = useState<ReactNode>("");
useEffect(() => { useEffect(() => {
setContent(res.resolve(resolver)); let resolved = res.resolve(resolver);
setContent(
typeof resolved?.display === "function" ? (
<resolved.display />
) : (
resolved?.display
),
);
}, [resolver, res]); }, [resolver, res]);
return <span>{content}</span>; return <span>{content}</span>;
} }
const defaultTemplate = "$x";
export function OnDemandResolver({ export function OnDemandResolver({
resolver, resolver,
template, template,
@@ -22,19 +31,29 @@ export function OnDemandResolver({
template: string; template: string;
title?: string; title?: string;
}) { }) {
const parser = useRecoilValue(PublicationAtom); const [parser] = useAtom(PublicationAtom);
const res = useRef(new TTCQueryResolver(parser)); const res = useRef(new TTCQueryResolver(parser));
const [content, setContent] = useState(""); const [content, setContent] = useState<ReactNode>("");
const generateContent = useCallback(() => { const generateContent = useCallback(() => {
let content = template; setContent(() => {
const stackIdxs = Array.from(new Set(template.match(/\$\d/g))); let content = template || defaultTemplate;
for (const idx of stackIdxs) {
let thing = res.current.getFromStack(idx); const stackIdxs = Array.from(new Set(content.match(/\$(?:\d+|x)/g)));
if (Array.isArray(thing)) thing = thing.at(0); for (const idx of stackIdxs) {
if (typeof thing === "function") thing = thing(); let thing = res.current.getFromStack(idx);
content = content.replaceAll(idx, thing as string); if (Array.isArray(thing)) thing = thing.at(0);
} console.log(thing);
setContent(content); if (typeof thing.display === "function") {
const disp = thing.display();
if (typeof disp === "string" || typeof disp === "number")
content = content.replaceAll(idx, disp.toString());
else return disp as ReactNode;
}
// else if (idx === defaultTemplate && )
else content = content.replaceAll(idx, thing.display as string);
return content;
}
});
}, [res, template]); }, [res, template]);
const resolve = useCallback(() => { const resolve = useCallback(() => {
@@ -43,12 +62,16 @@ export function OnDemandResolver({
}, [res, resolver, generateContent]); }, [res, resolver, generateContent]);
return ( return (
<> <div className="my-2 rounded-md p-1 bg-black/20 flex flex-col">
<button onMouseDown={() => setContent("")} onClick={resolve}> <button
className="text-primary-600"
onMouseDown={() => setContent("")}
onClick={resolve}
>
{title ?? "Resolve"} {title ?? "Resolve"}
</button> </button>
<br /> <br />
{!!content && <span>{content}</span>} {!!content && <span>{content}</span>}
</> </div>
); );
} }

View File

@@ -5,11 +5,13 @@ import { Poppable } from "../poppables/components/poppable";
import { Accordion, AccordionContent } from "../accordion"; import { Accordion, AccordionContent } from "../accordion";
import { OnDemandResolver, Resolver } from "./Resolver"; import { OnDemandResolver, Resolver } from "./Resolver";
// import "crypto";
export const TokenRenderers = new Map<string, TokenRenderer<any>>(); export const TokenRenderers = new Map<string, TokenRenderer<any>>();
export function buildIdentifierMap(): [ export function buildIdentifierMap(): [
TokenIdentifierMap, TokenIdentifierMap,
IdentifierRegistration IdentifierRegistration,
] { ] {
const TokenIdentifiers = new Map<string, TokenIdentifier<any>>(); const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();
@@ -17,7 +19,7 @@ export function buildIdentifierMap(): [
type: string, type: string,
match: RegExp, match: RegExp,
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>, parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
renderFunction: TokenRenderer<M> renderFunction: TokenRenderer<M>,
): void; ): void;
function registerIdentifier<M>( function registerIdentifier<M>(
type: string, type: string,
@@ -25,7 +27,7 @@ export function buildIdentifierMap(): [
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>, parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
renderFunction: TokenRenderer<M>, renderFunction: TokenRenderer<M>,
openTagRx: RegExp, openTagRx: RegExp,
closeTagRx: RegExp closeTagRx: RegExp,
): void; ): void;
function registerIdentifier<M = Record<string, string>>( function registerIdentifier<M = Record<string, string>>(
type: string, type: string,
@@ -33,7 +35,7 @@ export function buildIdentifierMap(): [
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>, parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
renderFunction: TokenRenderer<M>, renderFunction: TokenRenderer<M>,
openTagRx?: RegExp, openTagRx?: RegExp,
closeTagRx?: RegExp closeTagRx?: RegExp,
) { ) {
TokenIdentifiers.set(type, { TokenIdentifiers.set(type, {
rx: match, rx: match,
@@ -54,7 +56,7 @@ export function buildIdentifierMap(): [
start, start,
end, end,
new RegExp(openTagRx, "g"), new RegExp(openTagRx, "g"),
new RegExp(closeTagRx, "g") new RegExp(closeTagRx, "g"),
); );
} }
: undefined, : undefined,
@@ -125,7 +127,7 @@ export const buildOnlyDefaultElements = () => {
); );
}, },
/(?<![\/\?])(?:\[\])+/g, /(?<![\/\?])(?:\[\])+/g,
/\/\[\]/g /\/\[\]/g,
); );
// card // card
@@ -170,7 +172,7 @@ export const buildOnlyDefaultElements = () => {
); );
}, },
/\[\[/g, /\[\[/g,
/\]\]/g /\]\]/g,
); );
// fenced code block // fenced code block
@@ -192,7 +194,7 @@ export const buildOnlyDefaultElements = () => {
{token.content} {token.content}
</pre> </pre>
); );
} },
); );
// list // list
@@ -231,7 +233,7 @@ export const buildOnlyDefaultElements = () => {
</ul> </ul>
</> </>
); );
} },
); );
// ordered-list // ordered-list
@@ -271,7 +273,7 @@ export const buildOnlyDefaultElements = () => {
</ol> </ol>
</> </>
); );
} },
); );
// ordered list-item // ordered list-item
@@ -300,7 +302,7 @@ export const buildOnlyDefaultElements = () => {
))} ))}
</li> </li>
); );
} },
); );
// heading // heading
@@ -336,7 +338,7 @@ export const buildOnlyDefaultElements = () => {
{token.content} {token.content}
</div> </div>
); );
} },
); );
// image // image
@@ -373,7 +375,7 @@ export const buildOnlyDefaultElements = () => {
} }
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
return <img src={metadata.src} alt={token.content} />; return <img src={metadata.src} alt={token.content} />;
} },
); );
// anchor // anchor
@@ -417,7 +419,7 @@ export const buildOnlyDefaultElements = () => {
{token.content} {token.content}
</Link> </Link>
); );
} },
); );
// inline-code // inline-code
@@ -440,7 +442,7 @@ export const buildOnlyDefaultElements = () => {
{token.content} {token.content}
</span> </span>
); );
} },
); );
// bold // bold
@@ -458,7 +460,7 @@ export const buildOnlyDefaultElements = () => {
}, },
(token) => { (token) => {
return <span className="font-bold">{token.content}</span>; return <span className="font-bold">{token.content}</span>;
} },
); );
// italic // italic
@@ -477,7 +479,7 @@ export const buildOnlyDefaultElements = () => {
}, },
(token) => { (token) => {
return <span className="italic">{token.content}</span>; return <span className="italic">{token.content}</span>;
} },
); );
// popover // popover
@@ -513,7 +515,7 @@ export const buildOnlyDefaultElements = () => {
</span> </span>
</Poppable> </Poppable>
); );
} },
); );
// accordion // accordion
@@ -543,12 +545,13 @@ export const buildOnlyDefaultElements = () => {
</Accordion> </Accordion>
</div> </div>
); );
} },
); );
// paragraph // paragraph
registerIdentifier( registerIdentifier(
"p", "p",
// /(?<=\n\n|^)([\s\S]*?)(?=\n\n|$)/g,
/(?<=\n\n)([\s\S]*?)(?=\n\n)/g, /(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
(s) => { (s) => {
return { return {
@@ -568,7 +571,7 @@ export const buildOnlyDefaultElements = () => {
})} })}
</div> </div>
); );
} },
); );
// horizontal rule // horizontal rule
@@ -586,7 +589,7 @@ export const buildOnlyDefaultElements = () => {
}, },
() => { () => {
return <div className="w-full border-b border-mixed-500 my-3"></div>; return <div className="w-full border-b border-mixed-500 my-3"></div>;
} },
); );
// comment // comment
@@ -604,7 +607,7 @@ export const buildOnlyDefaultElements = () => {
}, },
() => { () => {
return <></>; return <></>;
} },
); );
// frontmatter // frontmatter
@@ -623,7 +626,7 @@ export const buildOnlyDefaultElements = () => {
}, },
(token) => { (token) => {
return <>{token.raw}</>; return <>{token.raw}</>;
} },
); );
// table // table
@@ -654,8 +657,8 @@ export const buildOnlyDefaultElements = () => {
r r
.split("|") .split("|")
.map((c) => c.trim()) .map((c) => c.trim())
.filter((c) => !!c) .filter((c) => !!c),
) ),
); );
let headerRows: string[][] = [], let headerRows: string[][] = [],
@@ -678,7 +681,7 @@ export const buildOnlyDefaultElements = () => {
} }
const maxColumns = Math.max( const maxColumns = Math.max(
...[...headerRows, ...bodyRows, ...footerRows].map((r) => r.length) ...[...headerRows, ...bodyRows, ...footerRows].map((r) => r.length),
); );
return { return {
@@ -769,7 +772,7 @@ export const buildOnlyDefaultElements = () => {
)} )}
</table> </table>
); );
} },
); );
// resolver // resolver
@@ -796,17 +799,17 @@ export const buildOnlyDefaultElements = () => {
if (t.content.startsWith("Error")) if (t.content.startsWith("Error"))
return <span className="red-500">{t.content}</span>; return <span className="red-500">{t.content}</span>;
return <Resolver resolver={t.content} />; return <Resolver resolver={t.content} />;
} },
); );
// on-demand resolver // on-demand resolver
registerIdentifier( registerIdentifier(
"on-demand resolver", "on-demand resolver",
/\?\?\[.*?\](\(.*?\))?<<(.*?)>>/g, /\?\?\[.*?\](\(.*?\))<<(.*?)>>/g,
(s) => { (s) => {
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0]; const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
const template = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0]; const title = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0];
const title = s.match(/(?<=\]\()(.*?)(?=\))/)?.at(0); const template = s.match(/(?<=\]\()(.*?)(?=\))/)![0];
if (inp == undefined) if (inp == undefined)
return { return {
content: "Error parsing resolver: " + s, content: "Error parsing resolver: " + s,
@@ -838,7 +841,7 @@ export const buildOnlyDefaultElements = () => {
title={t.metadata.title} title={t.metadata.title}
/> />
); );
} },
); );
return TokenIdentifiers; return TokenIdentifiers;
@@ -847,7 +850,7 @@ export const buildOnlyDefaultElements = () => {
function findMatchingClosedParenthesis( function findMatchingClosedParenthesis(
str: string, str: string,
openRegex: RegExp, openRegex: RegExp,
closedRegex: RegExp closedRegex: RegExp,
): number | null { ): number | null {
let openings = 0; let openings = 0;
let closings = 0; let closings = 0;
@@ -903,7 +906,7 @@ function search(
start: number, start: number,
end: number, end: number,
openRx: RegExp, openRx: RegExp,
closeRx: RegExp closeRx: RegExp,
): SearchResult { ): SearchResult {
const oldEnd = end; const oldEnd = end;
@@ -911,7 +914,7 @@ function search(
s, s,
// s.substring(0, end - start), // s.substring(0, end - start),
openRx, openRx,
closeRx closeRx,
); );
if (newEnd === null) if (newEnd === null)
@@ -931,7 +934,7 @@ function search(
function generateId(t: string, usedIds: string[]) { function generateId(t: string, usedIds: string[]) {
let id = t let id = t
.toLowerCase() .toLowerCase()
.replace(/[^a-z\s]/gi, "") .replace(/[^a-z\s-\d]/gi, "")
.trim() .trim()
.replaceAll(" ", "-"); .replaceAll(" ", "-");
let idNum = 1; let idNum = 1;

View File

@@ -11,7 +11,10 @@ export const createElements = (body: string): Token[] => {
const tokenize = (body: string) => { const tokenize = (body: string) => {
const tokenizedBody: TokenMarker[] = []; const tokenizedBody: TokenMarker[] = [];
body = body.replaceAll(/[ \t]+\n/g, "\n").replaceAll(/\n{3,}/g, "\n\n"); body = body
.trim()
.replaceAll(/[ \t]+\n/g, "\n")
.replaceAll(/\n{3,}/g, "\n\n");
const addToken = (thing: TokenMarker) => { const addToken = (thing: TokenMarker) => {
tokenizedBody.push(thing); tokenizedBody.push(thing);
@@ -176,9 +179,9 @@ const contentToChildren = (token: Token) => {
}, },
] ]
: undefined, : undefined,
}) }),
), ),
token.children || [] token.children || [],
).filter((c) => c.children?.length || (c.rendersContentOnly && c.content)); ).filter((c) => c.children?.length || (c.rendersContentOnly && c.content));
}; };
@@ -269,7 +272,10 @@ export function extractFrontMatter(body: string): [FrontMatter, string] {
const rx = /^---([\s\S]*?)---/; const rx = /^---([\s\S]*?)---/;
const [_, frontmatterString] = body.match(rx) || ["", ""]; const [_, frontmatterString] = body.match(rx) || ["", ""];
body = body.replace(rx, ""); console.log(body.replace(rx, ""));
body = body.replace(rx, "").trim();
console.log(body);
const frontMatter: FrontMatter = {}; const frontMatter: FrontMatter = {};

View File

@@ -0,0 +1,48 @@
import { FC } from "react";
export const DiceChart: FC<{ dice: Record<string, number> }> = ({ dice }) => {
const data = Object.entries(dice).sort((a, b) => Number(a[0]) - Number(b[0]));
const max = Math.max(...data.map((d) => d[1]));
const _min = Math.min(...data.map((d) => d[1]));
const uniqueValues = Array.from(new Set(data.map((d) => d[1])));
return (
<div className="p-8 bg-black/30 rounded-md max-w-[35rem]">
<div className="relative flex px-2 gap-1 justify-around items-end h-48 border-b border-l">
<div className="absolute inset-0 flex flex-col-reverse">
{uniqueValues.map((_, i) => (
<div
key={"dicechartline" + i}
className="relative w-full border-t border-mixed-300/50 flex-grow"
>
{/* <div className="absolute right-full text-xs flex items-center -translate-y-1/2">
<span>
{Math.round(max / uniqueValues.length) * (i + 1)} {}
</span>
<div className="border-b w-1 ml-1"></div>
</div> */}
</div>
))}
</div>
{data.map((d) => (
<div
key={"dice" + d[0]}
title={d[0] + ": " + d[1]}
className="flex-grow px-[1px] border hover:border-mixed-300/50 border-transparent h-full flex transition-colors rounded-md @container"
>
<div
className="bg-purple-800 relative rounded-t-sm w-full mt-auto"
style={{ height: (d[1] / max) * 100 + "%" }}
>
<span className="absolute top-full left-1/2 -translate-x-1/2 text-xs @[.75rem]:inline hidden">
{d[0]}
</span>
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -1,11 +1,20 @@
/* eslint-disable react/display-name */
import { ReactNode } from "react";
import { Dice } from "../dice"; import { Dice } from "../dice";
import { sum } from "../utils/sum";
import { DiceChart } from "./DiceChart";
import { TTCQueryParser } from "./TTCQueryParser"; import { TTCQueryParser } from "./TTCQueryParser";
interface StackItem {
value: any;
display: ReactNode | (() => ReactNode);
}
export class TTCQueryResolver { export class TTCQueryResolver {
private parser: TTCQueryParser | null; private parser: TTCQueryParser | null;
private context: QueryableObject | null = null; private context: QueryableObject | null = null;
private stack: any[] = []; private stack: StackItem[] = [];
constructor(parser?: TTCQueryParser) { constructor(parser?: TTCQueryParser) {
this.parser = parser || null; this.parser = parser || null;
@@ -19,21 +28,35 @@ export class TTCQueryResolver {
this.context = obj; this.context = obj;
} }
public resolve(resolver: string, onDemand?: boolean) { public resolve(resolver: string, onDemand?: boolean): StackItem | undefined {
const resList = resolver.split(","); try {
for (const res of resList) { const resList = resolver.split(",");
this.stack.push(this.parseResolver(res)); for (const res of resList) {
} this.stack.push(this.parseResolver(res));
const last = this.stack.at(-1); }
if (typeof last === "function" && !onDemand) return last(); const last = this.stack.at(-1);
if (typeof last?.display === "function" && !onDemand) {
last.display = last.display();
}
return last; return last;
} catch (e) {
return { value: e?.toString(), display: e?.toString() };
}
} }
private parseResolver(resolver: string) { private parseResolver(resolver: string): StackItem {
if (this.isArithmetic(resolver)) return this.solveArithmetic(resolver); if (this.isArithmetic(resolver)) return this.solveArithmetic(resolver);
if (this.isQuery(resolver)) return this.runQuery(resolver); if (this.isQuery(resolver)) return this.runQuery(resolver);
return resolver; if (Dice.isDice(resolver)) {
const value = new Dice(resolver);
const dice: StackItem = {
value,
display: () => value.toString(),
};
return dice;
}
return { value: resolver, display: resolver };
} }
private isQuery(resolver: string) { private isQuery(resolver: string) {
return ( return (
@@ -42,76 +65,140 @@ export class TTCQueryResolver {
/^\$\d\./.test(resolver) /^\$\d\./.test(resolver)
); );
} }
private runQuery(query: string) { private runQuery(query: string): StackItem {
if (!this.parser) throw "Parser not defined in query resolver"; if (!this.parser) throw "Parser not defined in query resolver";
if (query.startsWith("$")) { if (query.startsWith("$")) {
const [_, idx, q] = query.match(/^(\$\d+)\.(.*)/) || []; const [_, idx, q] = query.match(/^(\$\d+)\.(.*)/) || [];
if (!_) throw "Detected stack query but did not match the regex"; if (!_) throw "Detected stack query but did not match the regex";
const stackItem = this.getFromStack(idx); const stackItem = this.getFromStack(idx);
if (typeof stackItem === "string" && Dice.isDice(stackItem)) {
return this.handleDice(stackItem, q); if (stackItem.value instanceof Dice) {
return {
value: query,
display: this.handleDice(stackItem.value, q),
};
} }
return this.parser.search(q, stackItem as QueryableObject); const [res] = this.parser.search(q, stackItem.value as QueryableObject);
if (Dice.isDice(res)) {
const value = new Dice(res);
return {
value,
display: () => value.toString(),
};
}
return {
value: res,
display() {
return (
this.value.render ?? this.value ?? "Error resolving query: " + query
);
},
};
} }
// if (query.startsWith("?") || query.startsWith("_")) // if (query.startsWith("?") || query.startsWith("_"))
return query.startsWith("_") && this.context const res =
? this.parser.search(query.replace("_", "^"), this.context).at(0) query.startsWith("_") && this.context
: this.parser.search(query.replace(/^[?_].?/, "")).at(0); ? this.parser.search(query.replace("_", "^"), this.context).at(0)
} : this.parser.search(query.replace(/^[?_].?/, "")).at(0);
if (Dice.isDice(res)) {
private handleDice(dice: string, query: string) { const value = new Dice(res);
const d = new Dice(dice); return {
const [method, n] = query.split(":"); value,
let num = Number(n); display: () => value.toString(),
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";
} }
return {
value: res,
display() {
return (
this.value.render ?? this.value ?? "Error resolving query: " + query
);
},
};
} }
private isArithmetic(resolver: string) { private isArithmetic(resolver: string) {
return resolver.split(/\+|\/|-|\*|\^/).filter((e) => !!e).length > 1; return resolver.split(/\+|\/|-|\*|\^/).filter((e) => !!e).length > 1;
} }
private solveArithmetic(resolver: string) { private solveArithmetic(resolver: string): StackItem {
const [n1, op, n2] = resolver const [n1, op, n2] = resolver
.match(/(\$?\d+)([+\-*\/^])(\$?\d+)/) .match(/(\$?\d+)([+\-*\/^])(\$?\d+)/)
?.slice(1) || ["", "+", ""]; ?.slice(1) || ["", "+", ""];
let num1 = Number(n1), let num1: number = Number(n1),
num2 = Number(n2); num2: number = Number(n2);
if (n1.startsWith("$")) num1 = this.getFromStack<number>(n1); if (n1.startsWith("$")) {
if (n2.startsWith("$")) num2 = this.getFromStack<number>(n2); const result = this.getFromStack(n1).value;
num1 = result instanceof Dice ? result.roll().total : Number(result);
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";
} }
if (n2.startsWith("$")) {
const result = this.getFromStack(n1).value;
num2 = result instanceof Dice ? result.roll().total : Number(result);
}
const thing: StackItem = {
value: () => {
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";
}
},
display() {
return typeof this.value === "function" ? this.value() : this.value;
},
};
return thing;
} }
public getFromStack<T>(stackIndex: string): T { public getFromStack(stackIndex: string): StackItem {
if (stackIndex === "$x") return this.stack.at(-1)!;
const i = Number(stackIndex.replace("$", "")); const i = Number(stackIndex.replace("$", ""));
const val = this.stack[i] as T; const val = this.stack[i];
return val; return val;
} }
private handleDice(dice: Dice, query: string) {
const [method, n] = query.split(":");
let num = Number(n);
// if (n && n.startsWith("$")) num = this.getFromStack(n);
switch (method) {
case "roll":
return () => dice.roll.apply(dice).total;
case "rollAvg":
return () => dice.rollAvg.apply(dice);
case "rollTimes":
return () => dice.rollTimes.apply(dice, [num]);
case "rollTimesAvg":
return () => dice.rollTimesAvg.apply(dice, [num]);
case "rollLowest":
return () => dice.rollMin.apply(dice);
case "rollHighest":
return () => dice.rollMax.apply(dice);
case "rollDropHighest":
return () =>
sum(...dice.roll.apply(dice).results.toSorted().toSpliced(-1, 1));
case "rollDropLowest":
return () =>
sum(...dice.roll.apply(dice).results.toSorted().toSpliced(0, 1));
case "distribution":
return () => DiceChart({ dice: dice.getRollDistribution.apply(dice) });
case "distribution.normalized":
return () =>
DiceChart({ dice: dice.getNormalizedRollDistribution.apply(dice) });
default:
return () => "No valid method provided for dice";
}
}
} }

3
lib/utils/sum.ts Normal file
View File

@@ -0,0 +1,3 @@
export const sum = (...args: number[]) => {
return args.reduce((a, b) => a + b, 0);
};

View File

@@ -2,7 +2,7 @@
title: How to use ttcMD title: How to use ttcMD
author: Emmaline Autumn author: Emmaline Autumn
date: March 14th, 2024 date: March 14th, 2024
updated: March 14th, 2024 updated: August 21st, 2024
--- ---
# Table of Contents # Table of Contents
@@ -368,17 +368,17 @@ Let's say you want to get the the result of rolling a dice field. You would simp
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 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](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." Here's the syntax: `??[Text](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. 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](Roll 2d6)<<_.path.to.dice,$0.roll>>` To use the dice as an example again, here's how you would do that: `??[Roll 2d6](Rolling $0, you got: $1)<<_.path.to.dice,$0.roll>>`
??[Rolling $0, you got: $1](Roll 2d6)<<_.path.to.dice,$0.roll>> ??[Roll 2d6](Rolling $0, you got: $1)<<_.path.to.dice,$0.roll>>
]] ]]
[[ [[
For drawing a card and having it show the card's display: `??[]<<_.path.to.deck,$0.draw,$1.display>>` For drawing a card and having it show the card's display: `??[]()<<_.path.to.deck,$0.draw,$1.display>>`
]] ]]
/[] /[]

View File

@@ -12,4 +12,9 @@
3. my 3. my
4. name 4. name
5. is 5. is
6. welcome 6. welcome
- [-\_hello1234!@#$%^\&\*()-](#-_hello1234-)
# -_hello1234!@#$%^&*()-

View File

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

472
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,13 @@
}, },
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.4.2", "@auth/prisma-adapter": "^2.4.2",
"@codemirror/lang-markdown": "^6.2.5",
"@heroicons/react": "^2.1.1", "@heroicons/react": "^2.1.1",
"@prisma/client": "^5.18.0", "@prisma/client": "^5.18.0",
"@tailwindcss/container-queries": "^0.1.1",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@uiw/codemirror-theme-duotone": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"isomorphic-dompurify": "^2.4.0", "isomorphic-dompurify": "^2.4.0",
"jotai": "^2.9.3", "jotai": "^2.9.3",

View File

@@ -1,46 +0,0 @@
-- CreateTable
CREATE TABLE `GameSystem` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`created` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Schema` (
`id` VARCHAR(191) NOT NULL,
`gameSystemId` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`schema` JSON NOT NULL,
`version` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Publication` (
`id` VARCHAR(191) NOT NULL,
`schemaId` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`data` JSON NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Tag` (
`id` VARCHAR(191) NOT NULL,
`publicationId` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Schema` ADD CONSTRAINT `Schema_gameSystemId_fkey` FOREIGN KEY (`gameSystemId`) REFERENCES `GameSystem`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Publication` ADD CONSTRAINT `Publication_schemaId_fkey` FOREIGN KEY (`schemaId`) REFERENCES `Schema`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Tag` ADD CONSTRAINT `Tag_publicationId_fkey` FOREIGN KEY (`publicationId`) REFERENCES `Publication`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `GameSystem` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX `GameSystem_name_key` ON `GameSystem`(`name`);

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `types` to the `Schema` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `Schema` ADD COLUMN `types` JSON NOT NULL;

View File

@@ -1,83 +0,0 @@
/*
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;

View File

@@ -1,71 +0,0 @@
/*
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;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `User` ADD COLUMN `passwordHash` VARCHAR(191) NULL;

View File

@@ -0,0 +1,177 @@
-- CreateTable
CREATE TABLE "GameSystem" (
"id" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GameSystem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Schema" (
"id" TEXT NOT NULL,
"gameSystemId" TEXT,
"authorId" TEXT NOT NULL,
"originalId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"schema" JSONB NOT NULL,
"types" JSONB NOT NULL,
"version" INTEGER NOT NULL,
CONSTRAINT "Schema_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Publication" (
"id" TEXT NOT NULL,
"schemaId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data" JSONB NOT NULL,
CONSTRAINT "Publication_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TagsOnPublications" (
"publicationId" TEXT NOT NULL,
"tagId" TEXT NOT NULL,
CONSTRAINT "TagsOnPublications_pkey" PRIMARY KEY ("publicationId","tagId")
);
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"publicationId" TEXT,
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TagsOnTags" (
"parentTagId" TEXT NOT NULL,
"childTagId" TEXT NOT NULL,
CONSTRAINT "TagsOnTags_pkey" PRIMARY KEY ("parentTagId","childTagId")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"username" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"passwordHash" TEXT,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"refresh_token_expires_in" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "GameSystem_name_key" ON "GameSystem"("name");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Account_userId_key" ON "Account"("userId");
-- CreateIndex
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- 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_schemaId_fkey" FOREIGN KEY ("schemaId") REFERENCES "Schema"("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;
-- 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;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Schema" ALTER COLUMN "originalId" DROP NOT NULL;

View File

@@ -0,0 +1,123 @@
/*
Warnings:
- You are about to drop the column `authorId` on the `Publication` table. All the data in the column will be lost.
- You are about to drop the column `data` on the `Publication` table. All the data in the column will be lost.
- You are about to drop the column `originalId` on the `Schema` table. All the data in the column will be lost.
- You are about to drop the column `schema` on the `Schema` table. All the data in the column will be lost.
- You are about to drop the column `types` on the `Schema` table. All the data in the column will be lost.
- You are about to drop the column `version` on the `Schema` table. All the data in the column will be lost.
- You are about to drop the `TagsOnPublications` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `TagsOnTags` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `schemaVersion` to the `Publication` table without a default value. This is not possible if the table is not empty.
- Added the required column `publicationRevisionId` to the `Tag` table without a default value. This is not possible if the table is not empty.
- Made the column `publicationId` on table `Tag` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Publication" DROP CONSTRAINT "Publication_authorId_fkey";
-- DropForeignKey
ALTER TABLE "Tag" DROP CONSTRAINT "Tag_publicationId_fkey";
-- DropForeignKey
ALTER TABLE "TagsOnPublications" DROP CONSTRAINT "TagsOnPublications_publicationId_fkey";
-- DropForeignKey
ALTER TABLE "TagsOnPublications" DROP CONSTRAINT "TagsOnPublications_tagId_fkey";
-- DropForeignKey
ALTER TABLE "TagsOnTags" DROP CONSTRAINT "TagsOnTags_childTagId_fkey";
-- DropForeignKey
ALTER TABLE "TagsOnTags" DROP CONSTRAINT "TagsOnTags_parentTagId_fkey";
-- AlterTable
ALTER TABLE "GameSystem" ADD COLUMN "isPublic" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "Publication" DROP COLUMN "authorId",
DROP COLUMN "data",
ADD COLUMN "schemaVersion" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "Schema" DROP COLUMN "originalId",
DROP COLUMN "schema",
DROP COLUMN "types",
DROP COLUMN "version";
-- AlterTable
ALTER TABLE "Tag" ADD COLUMN "publicationRevisionId" TEXT NOT NULL,
ALTER COLUMN "publicationId" SET NOT NULL;
-- DropTable
DROP TABLE "TagsOnPublications";
-- DropTable
DROP TABLE "TagsOnTags";
-- CreateTable
CREATE TABLE "SchemaRevision" (
"id" TEXT NOT NULL,
"schemaId" TEXT NOT NULL,
"fields" JSONB NOT NULL DEFAULT '{}',
"types" JSONB NOT NULL DEFAULT '{}',
"version" INTEGER NOT NULL DEFAULT 0,
"isFinal" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "SchemaRevision_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PublicationRevision" (
"id" TEXT NOT NULL,
"previousId" TEXT,
"publicationId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"version" TEXT NOT NULL,
"isFinal" BOOLEAN NOT NULL DEFAULT false,
"data" JSONB NOT NULL DEFAULT '{}',
CONSTRAINT "PublicationRevision_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GameSystemFollows" (
"gameSystemId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "GameSystemFollows_pkey" PRIMARY KEY ("userId","gameSystemId")
);
-- CreateIndex
CREATE UNIQUE INDEX "SchemaRevision_schemaId_version_key" ON "SchemaRevision"("schemaId", "version");
-- CreateIndex
CREATE UNIQUE INDEX "PublicationRevision_publicationId_version_key" ON "PublicationRevision"("publicationId", "version");
-- AddForeignKey
ALTER TABLE "SchemaRevision" ADD CONSTRAINT "SchemaRevision_schemaId_fkey" FOREIGN KEY ("schemaId") REFERENCES "Schema"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Publication" ADD CONSTRAINT "Publication_schemaVersion_schemaId_fkey" FOREIGN KEY ("schemaVersion", "schemaId") REFERENCES "SchemaRevision"("version", "schemaId") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PublicationRevision" ADD CONSTRAINT "PublicationRevision_publicationId_fkey" FOREIGN KEY ("publicationId") REFERENCES "Publication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PublicationRevision" ADD CONSTRAINT "PublicationRevision_previousId_fkey" FOREIGN KEY ("previousId") REFERENCES "PublicationRevision"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PublicationRevision" ADD CONSTRAINT "PublicationRevision_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_publicationId_fkey" FOREIGN KEY ("publicationId") REFERENCES "Publication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_publicationRevisionId_fkey" FOREIGN KEY ("publicationRevisionId") REFERENCES "PublicationRevision"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GameSystemFollows" ADD CONSTRAINT "GameSystemFollows_gameSystemId_fkey" FOREIGN KEY ("gameSystemId") REFERENCES "GameSystem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GameSystemFollows" ADD CONSTRAINT "GameSystemFollows_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually # Please do not edit this file manually
# It should be added in your version-control system (i.e. Git) # It should be added in your version-control system (i.e. Git)
provider = "mysql" provider = "postgresql"

View File

@@ -1,26 +1,23 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
datasource db { datasource db {
provider = "mysql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model GameSystem { model GameSystem {
id String @id @default(cuid()) id String @id @default(cuid())
schemas Schema[] schemas Schema[]
author User @relation(fields: [authorId], references: [id]) author User @relation(fields: [authorId], references: [id])
authorId String authorId String
followers GameSystemFollows[]
name String @unique name String @unique
created DateTime @default(now()) created DateTime @default(now())
isPublic Boolean @default(false)
thingUsers User[] @relation("currentGame")
} }
model Schema { model Schema {
@@ -31,59 +28,87 @@ model Schema {
author User @relation(fields: [authorId], references: [id]) author User @relation(fields: [authorId], references: [id])
authorId String authorId String
originalId String name String
name String
schema Json SchemaRevision SchemaRevision[]
types Json }
version Int
model SchemaRevision {
id String @id @default(cuid())
schemaId String
parentSchema Schema @relation(fields: [schemaId], references: [id])
Publication Publication[]
fields Json @default("{}")
types Json @default("{}")
version Int @default(0)
isFinal Boolean @default(false)
@@unique([schemaId, version])
} }
model Publication { model Publication {
id String @id @default(cuid()) id String @id @default(cuid())
schema Schema @relation(fields: [schemaId], references: [id]) schema Schema @relation(fields: [schemaId], references: [id])
schemaId String schemaId String
tags Tag[] schemaVersion Int
author User @relation(fields: [authorId], references: [id]) schemaRevision SchemaRevision @relation(fields: [schemaVersion, schemaId], references: [version, schemaId])
authorId String PublicationRevision PublicationRevision[]
tags Tag[]
name String name String
data Json
TagsOnPublications TagsOnPublications[] // TagsOnPublications TagsOnPublications[]
} }
model TagsOnPublications { model PublicationRevision {
publication Publication @relation(fields: [publicationId], references: [id]) id String @id @default(cuid())
publicationId String Tag Tag[]
tagId String previousId String?
tag Tag @relation(fields: [tagId], references: [id]) publicationId String
publication Publication @relation(fields: [publicationId], references: [id])
previousRevision PublicationRevision? @relation(name: "downlineRevisions", fields: [previousId], references: [id])
downlineRevisions PublicationRevision[] @relation("downlineRevisions")
author User @relation(fields: [authorId], references: [id])
authorId String
@@id([publicationId, tagId]) version String
isFinal Boolean @default(false)
data Json @default("{}")
@@unique([publicationId, version])
} }
// 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 { model Tag {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
Publication Publication? @relation(fields: [publicationId], references: [id]) Publication Publication @relation(fields: [publicationId], references: [id])
publicationId String? publicationId String
TagsOnPublications TagsOnPublications[] PublicationRevision PublicationRevision @relation(fields: [publicationRevisionId], references: [id])
childTagsOnTags TagsOnTags[] @relation("childTag") publicationRevisionId String
parentTagsOnTags TagsOnTags[] @relation("parentTag")
}
model TagsOnTags { // TagsOnPublications TagsOnPublications[]
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 { model User {
id String @id @default(cuid()) id String @id @default(cuid())
schemas Schema[] schemas Schema[]
gameSystems GameSystem[] gameSystems GameSystem[]
publications Publication[] accounts Account[]
sessions Session[]
PublicationRevision PublicationRevision[]
followedSystems GameSystemFollows[]
currentGameSystem GameSystem? @relation("currentGame", fields: [currentGameSystemId], references: [id])
currentGameSystemId String?
name String? name String?
username String? @unique username String? @unique
@@ -91,13 +116,20 @@ model User {
emailVerified DateTime? emailVerified DateTime?
passwordHash String? passwordHash String?
image String? image String?
accounts Account[]
sessions Session[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model GameSystemFollows {
gameSystemId String
gameSystem GameSystem @relation(fields: [gameSystemId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
@@id([userId, gameSystemId])
}
model Account { model Account {
id String @id @default(cuid()) id String @id @default(cuid())
userId String @unique userId String @unique

View File

@@ -1,9 +1,14 @@
import { TTCQueryParser } from "@/lib/ttcQuery/TTCQueryParser"; import { TTCQueryParser } from "@/lib/ttcQuery/TTCQueryParser";
import { atom } from "recoil"; import { atom } from "jotai";
// import { atom } from "recoil";
export const PublicationAtom = atom({ // export const PublicationAtom = atom({
key: "publication", // key: "publication",
default: new TTCQueryParser({ // default: new TTCQueryParser({
path: { to: { dice: "2d6" } }, // path: { to: { dice: "2d6" } },
}), // }),
}); // });
export const PublicationAtom = atom(
new TTCQueryParser({ path: { to: { dice: "2d6" } } }),
);

View File

@@ -1,6 +1,17 @@
import { atom } from "recoil"; // import { atom } from "recoil";
import { Schema } from "@/types";
import { atom } from "jotai";
// export const SchemaEditAtom = atom<Schema>({
// key: "schema-edit",
// default: { name: "", types: {}, schema: {}, id: "" },
// });
export const SchemaEditAtom = atom<Schema>({ export const SchemaEditAtom = atom<Schema>({
key: "schema-edit", name: "",
default: { name: "", types: {}, schema: {}, id: "" }, id: "",
types: {},
fields: {},
version: 0,
}); });

View File

@@ -51,6 +51,6 @@ const config: Config = {
}, },
}, },
}, },
plugins: [], plugins: [require("@tailwindcss/container-queries")],
}; };
export default config; export default config;

View File

@@ -1,10 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -22,22 +18,11 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
}, },
"target": "es2022" "target": "es2022"
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"next-env.d.ts", "exclude": ["node_modules"],
"**/*.ts", "files": ["global.d.ts", "types.d.ts", "./components/mdeditor/TextEditor.tsx"]
"**/*.tsx", }
".next/types/**/*.ts",
],
"exclude": [
"node_modules"
],
"files": [
"global.d.ts"
]
}

18
types.d.ts vendored
View File

@@ -1,3 +1,5 @@
import { FieldTypes } from "./components/schema/fieldtypes";
// MD Parser // MD Parser
type IdentifiedToken<M> = { type IdentifiedToken<M> = {
metadata: M; metadata: M;
@@ -31,7 +33,7 @@ type FrontMatter = Record<string, string>;
type SearchFunction = ( type SearchFunction = (
s: string, s: string,
start: number, start: number,
end: number end: number,
) => { ) => {
start: number; start: number;
end: number; end: number;
@@ -53,7 +55,7 @@ type IdentifierRegistration = <N = Record<string, string>>(
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<N>, parseFunction: (s: string, rx: RegExp) => IdentifiedToken<N>,
renderFunction: TokenRenderer<N>, renderFunction: TokenRenderer<N>,
openTagRx?: RegExp, openTagRx?: RegExp,
closeTagRx?: RegExp closeTagRx?: RegExp,
) => void; ) => void;
// Schema // Schema
@@ -76,11 +78,17 @@ type Template = {
display: string; display: string;
}; };
type SchemaFields = Record<string, FieldTypes>;
type SchemaTypes = Record<string, TypeType>;
type Schema = { type Schema = {
id: string; id: string;
name: string; name: string;
schema: Record<string, Template>; fields: SchemaFields;
types: Record<string, TypeType>; types: SchemaTypes;
version: number;
gameSystemId?: string | null;
}; };
// Input Binder // Input Binder
@@ -88,7 +96,7 @@ type InputBinder = {
name: string; name: string;
value: string | number; value: string | number;
onChange: ( onChange: (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
) => void; ) => void;
}; };

54
util/isEmailVerified.ts Normal file
View File

@@ -0,0 +1,54 @@
import { prisma } from "@/prisma/prismaClient";
export async function isEmailVerified(id: string) {
const user = await prisma.user.findUnique({
where: { id },
select: {
emailVerified: true,
email: true,
accounts: { select: { provider: true, access_token: true } },
},
});
if (!user) return false;
if (user?.emailVerified) return true;
const discordAccount = user.accounts.find((a) => a.provider === "discord");
if (user && discordAccount?.access_token) {
const dcUser = await getDiscordUserInfo(discordAccount.access_token);
if (!dcUser.verified) return false;
prisma.user.update({ where: { id }, data: { emailVerified: new Date() } });
return true;
}
}
async function getDiscordUserInfo(accessToken: string): Promise<{
id: string;
username: string;
discriminator: string;
avatar: string;
verified: boolean;
email: string;
flags: number;
banner: string;
accent_color: number;
premium_type: number;
public_flags: number;
avatar_decoration_data: {
sku_id: string;
asset: string;
};
}> {
try {
const response = await fetch("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return await response.json();
} catch (error) {
console.error("Error fetching user info from Discord:", error);
throw error;
}
}