Compare commits

..

6 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
35 changed files with 676 additions and 440 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,23 +0,0 @@
"use server";
import { auth } from "@/auth";
import { prisma } from "@/prisma/prismaClient";
import { isEmailVerified } from "@/util/isEmailVerified";
export const createGameSystem = async (name: string) => {
const session = await auth();
if (!session?.user?.id) return null;
if (!isEmailVerified(session.user.id)) return null;
const { id } = await prisma.gameSystem.create({
data: {
name,
authorId: session.user.id,
},
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,29 +0,0 @@
"use server";
import { auth } from "@/auth";
import { prisma } from "@/prisma/prismaClient";
import { isEmailVerified } from "@/util/isEmailVerified";
import { redirect } from "next/navigation";
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,
schema: "{}",
types: "{}",
version: 0,
gameSystemId: gsId,
authorId: session.user.id,
},
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,5 +1,5 @@
"use client"; "use client";
import { createGameSystem } from "@/actions/GameSystems/create"; import { createGameSystem } from "@/actions/GameSystems";
import { useToast } from "@/components/toast"; import { useToast } from "@/components/toast";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";

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

View File

@@ -20,6 +20,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
Discord({ Discord({
clientId, clientId,
clientSecret, clientSecret,
// redirectProxyUrl:
// "https://bottomsurgery.local:3000/api/auth/callback/discord",
}), }),
Credentials({ Credentials({
credentials: { credentials: {
@@ -30,7 +32,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
let user = null; let user = null;
const pwHash = await saltAndHashPassword( const pwHash = await saltAndHashPassword(
credentials.password as string credentials.password as string,
); );
user = await prisma.user.findFirst({ user = await prisma.user.findFirst({
where: { where: {

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

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

@@ -15,9 +15,13 @@ export class DHSecretClient {
* @param dhBaseUri uri for hosted Dragon's Hoard instance * @param dhBaseUri uri for hosted Dragon's Hoard instance
* @param cacheDir path to cache dir * @param cacheDir path to cache dir
*/ */
constructor(private dhBaseUri: string, private cacheDir: string) { constructor(
private dhBaseUri: 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 });
this.readDiskCache(); this.readDiskCache();
this.token = this.fetchToken(); this.token = this.fetchToken();
} }
@@ -42,12 +46,13 @@ export class DHSecretClient {
} }
private readDiskCache() { private readDiskCache() {
const cache = readFileSync(this.cacheLocation, "utf-8"); try {
const cache = readFileSync(this.cacheLocation, "utf-8");
if (!cache) { this.cache = JSON.parse(cache || "{}");
} catch {
this.cache = {}; this.cache = {};
this.writeDiskCache(); this.writeDiskCache().then(this.readDiskCache);
} else this.cache = JSON.parse(cache || "{}"); }
} }
private async writeDiskCache() { private async writeDiskCache() {
await writeFile(this.cacheLocation, JSON.stringify(this.cache), "utf-8"); await writeFile(this.cacheLocation, JSON.stringify(this.cache), "utf-8");

View File

@@ -1,10 +1,10 @@
import { PublicationAtom } from "@/recoil/atoms/publication"; import { PublicationAtom } from "@/recoil/atoms/publication";
import { useState, useEffect, useCallback, useRef, ReactNode } 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<ReactNode>(""); const [content, setContent] = useState<ReactNode>("");
useEffect(() => { useEffect(() => {
@@ -15,7 +15,7 @@ export function Resolver({ resolver }: { resolver: string }) {
<resolved.display /> <resolved.display />
) : ( ) : (
resolved?.display resolved?.display
) ),
); );
}, [resolver, res]); }, [resolver, res]);
return <span>{content}</span>; return <span>{content}</span>;
@@ -31,7 +31,7 @@ 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<ReactNode>(""); const [content, setContent] = useState<ReactNode>("");
const generateContent = useCallback(() => { const generateContent = useCallback(() => {

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,7 +545,7 @@ export const buildOnlyDefaultElements = () => {
</Accordion> </Accordion>
</div> </div>
); );
} },
); );
// paragraph // paragraph
@@ -569,7 +571,7 @@ export const buildOnlyDefaultElements = () => {
})} })}
</div> </div>
); );
} },
); );
// horizontal rule // horizontal rule
@@ -587,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
@@ -605,7 +607,7 @@ export const buildOnlyDefaultElements = () => {
}, },
() => { () => {
return <></>; return <></>;
} },
); );
// frontmatter // frontmatter
@@ -624,7 +626,7 @@ export const buildOnlyDefaultElements = () => {
}, },
(token) => { (token) => {
return <>{token.raw}</>; return <>{token.raw}</>;
} },
); );
// table // table
@@ -655,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[][] = [],
@@ -679,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 {
@@ -770,7 +772,7 @@ export const buildOnlyDefaultElements = () => {
)} )}
</table> </table>
); );
} },
); );
// resolver // resolver
@@ -797,7 +799,7 @@ 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
@@ -839,7 +841,7 @@ export const buildOnlyDefaultElements = () => {
title={t.metadata.title} title={t.metadata.title}
/> />
); );
} },
); );
return TokenIdentifiers; return TokenIdentifiers;
@@ -848,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;
@@ -904,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;
@@ -912,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)

View File

@@ -26,7 +26,6 @@ const tokenize = (body: string) => {
const rx = new RegExp(token.rx); const rx = new RegExp(token.rx);
let match; let match;
while ((match = rx.exec(body)) !== null) { while ((match = rx.exec(body)) !== null) {
if (type === "p") debugger;
const start = match.index; const start = match.index;
const end = rx.lastIndex; const end = rx.lastIndex;
@@ -180,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));
}; };

View File

@@ -80,7 +80,6 @@ export class TTCQueryResolver {
} }
const [res] = this.parser.search(q, stackItem.value as QueryableObject); const [res] = this.parser.search(q, stackItem.value as QueryableObject);
debugger;
if (Dice.isDice(res)) { if (Dice.isDice(res)) {
const value = new Dice(res); const value = new Dice(res);
return { return {

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;
}, },
}; };

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,9 +1,3 @@
// 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"
} }
@@ -14,13 +8,16 @@ datasource db {
} }
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

@@ -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,23 +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",
"./components/mdeditor/TextEditor.tsx"
]
} }

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