Compare commits
26 Commits
3656fc42ac
...
dev
Author | SHA1 | Date | |
---|---|---|---|
c8f20fbda8 | |||
a2fde9cc79 | |||
84cbea8ce1 | |||
b529445851 | |||
f87a759048 | |||
5b16cc60f7 | |||
fd5e5bcc8b | |||
6760d06b18 | |||
b2e7223c16 | |||
1799c8da79 | |||
9c9edd9e90 | |||
3417fdd3d7 | |||
f6fc85bf7e | |||
e5f3cb0c34 | |||
df3171b646 | |||
d17ff63662 | |||
e42a938b13 | |||
545656cf22 | |||
5f2243b49a | |||
729aba68ce | |||
da044ac9d5 | |||
b9b744e97f | |||
9838324b35 | |||
9e2184352f | |||
0f100bba3d | |||
2b2b88f970 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -43,3 +43,5 @@ temp.json
|
|||||||
temp.md
|
temp.md
|
||||||
|
|
||||||
.dragonshoard/
|
.dragonshoard/
|
||||||
|
|
||||||
|
certificates
|
@@ -4,6 +4,9 @@ WORKDIR /ttc
|
|||||||
|
|
||||||
ADD . .
|
ADD . .
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV AUTH_TRUST_HOST true
|
||||||
|
|
||||||
RUN npm i
|
RUN npm i
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
@@ -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;
|
|
||||||
};
|
|
@@ -1,7 +0,0 @@
|
|||||||
"use server";
|
|
||||||
import { prisma } from "@/prisma/prismaClient";
|
|
||||||
|
|
||||||
// DEV TOOL ONLY
|
|
||||||
export async function deleteAllGameSystems() {
|
|
||||||
await prisma.gameSystem.deleteMany();
|
|
||||||
}
|
|
@@ -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}`);
|
|
||||||
};
|
|
@@ -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
130
actions/Schemas/index.ts
Normal 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}`);
|
||||||
|
};
|
14
actions/auth/index.ts
Normal file
14
actions/auth/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use server";
|
||||||
|
import { auth, signIn, signOut } from "@/auth";
|
||||||
|
|
||||||
|
export const signInWithDiscord = async () => {
|
||||||
|
await signIn("discord");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const signInWithCreds = async (formData: FormData) => {
|
||||||
|
await signIn("credentials", formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const signOutOfApp = () => signOut();
|
||||||
|
|
||||||
|
export const getSession = async () => await auth();
|
3
app/api/auth/[...nextauth]/route.ts
Normal file
3
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { handlers } from "@/auth";
|
||||||
|
|
||||||
|
export const { GET, POST } = handlers;
|
@@ -5,12 +5,5 @@ import { FC, use } from "react";
|
|||||||
|
|
||||||
export const HomeClient: FC<{ body: Promise<string> }> = ({ body }) => {
|
export const HomeClient: FC<{ body: Promise<string> }> = ({ body }) => {
|
||||||
const text = use(body);
|
const text = use(body);
|
||||||
|
return <TTCMD body={text} parserId="home" title="home" />;
|
||||||
return (
|
|
||||||
<TTCMD
|
|
||||||
body={text}
|
|
||||||
parserId="home"
|
|
||||||
title="home"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@@ -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">
|
||||||
|
@@ -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";
|
||||||
|
@@ -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"
|
||||||
|
@@ -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",
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
@apply py-2 px-4 rounded-full dark:bg-mixed-200 bg-mixed-600 placeholder:text-dark-500;
|
@apply py-2 px-4 rounded-lg dark:bg-mixed-200 bg-mixed-600 placeholder:text-dark-500;
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
@apply dark:bg-mixed-200 bg-primary-600 rounded-md p-1;
|
@apply dark:bg-mixed-200 bg-primary-600 rounded-md p-1;
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply rounded-full;
|
@apply rounded-lg;
|
||||||
}
|
}
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply dark:bg-primary-500 bg-primary-100 py-4 px-6 dark:text-mixed-100 text-white font-bold text-lg btn;
|
@apply dark:bg-primary-500 bg-primary-100 py-4 px-6 dark:text-mixed-100 text-white font-bold text-lg btn;
|
||||||
@@ -93,6 +93,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.fade-toast {
|
||||||
|
animation: fadeOut 300ms forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separated-list > li:not(:last-child) {
|
||||||
|
@apply border-b border-mixed-600 w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-menu {
|
||||||
|
animation: fadeIn 100ms forwards;
|
||||||
|
}
|
||||||
|
.fade-menu[data-closing="true"] {
|
||||||
|
animation: fadeOut 100ms forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes identifier {
|
@keyframes identifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,3 +119,20 @@
|
|||||||
list-style: square;
|
list-style: square;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
102
app/layout.tsx
102
app/layout.tsx
@@ -1,16 +1,17 @@
|
|||||||
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";
|
||||||
|
import { JotaiProvider } from "@/components/jotaiProvider";
|
||||||
|
import { Toaster } from "@/components/toast";
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
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" });
|
||||||
|
|
||||||
@@ -19,71 +20,42 @@ 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">
|
||||||
<body className={roboto.className + " flex min-h-[100vh]"}>
|
<SessionProvider>
|
||||||
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
|
<body className={roboto.className + " flex min-h-[100vh]"}>
|
||||||
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
|
||||||
<Link href="/">Tabletop Commander</Link>
|
<div className="flex flex-col h-full">
|
||||||
</h1>
|
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
||||||
<ul className="my-6 flex flex-col gap-6">
|
<Link href="/">Tabletop Commander</Link>
|
||||||
{navItems.map((n) => (
|
</h1>
|
||||||
<li key={"nav-item" + n.text}>
|
<Nav game={currentGame ?? undefined} />
|
||||||
<Link
|
<div className="mt-auto">
|
||||||
href={n.to}
|
<User />
|
||||||
className="flex items-center gap-2 group hover:text-purple-300 transition-colors"
|
</div>
|
||||||
>
|
</div>
|
||||||
<n.icon className="w-6 h-6 group-hover:fill-purple-300 transition-colors" />
|
</nav>
|
||||||
{n.text}
|
<RecoilRootClient>
|
||||||
</Link>
|
<JotaiProvider>
|
||||||
</li>
|
<DevToolboxContextProvider
|
||||||
))}
|
isDev={process.env.NODE_ENV !== "production"}
|
||||||
</ul>
|
>
|
||||||
</nav>
|
<main className="p-8 w-full overflow-visible">{children}</main>
|
||||||
<RecoilRootClient>
|
<Toaster />
|
||||||
<DevToolboxContextProvider
|
</DevToolboxContextProvider>
|
||||||
isDev={process.env.NODE_ENV !== "production"}
|
</JotaiProvider>
|
||||||
>
|
</RecoilRootClient>
|
||||||
<main className="p-8 w-full overflow-visible">
|
<div id="root-portal"></div>
|
||||||
{children}
|
</body>
|
||||||
</main>
|
</SessionProvider>
|
||||||
</DevToolboxContextProvider>
|
<SSE />
|
||||||
</RecoilRootClient>
|
|
||||||
<div id="root-portal"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -11,8 +11,7 @@ export default function Home() {
|
|||||||
<h2 className="strapline">Tabletop Commander</h2>
|
<h2 className="strapline">Tabletop Commander</h2>
|
||||||
<h1>How does it work?</h1>
|
<h1>How does it work?</h1>
|
||||||
</section>
|
</section>
|
||||||
{
|
{/* <section className="w-full my-6">
|
||||||
/* <section className="w-full my-6">
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<p>
|
<p>
|
||||||
Tabletop Commander (TC) is a rules-and-tools app for tabletop games
|
Tabletop Commander (TC) is a rules-and-tools app for tabletop games
|
||||||
@@ -131,8 +130,7 @@ export default function Home() {
|
|||||||
will be done. If this makes it to production, tell Emma she forgot to
|
will be done. If this makes it to production, tell Emma she forgot to
|
||||||
turn the home page into magic
|
turn the home page into magic
|
||||||
</cite>
|
</cite>
|
||||||
</section> */
|
</section> */}
|
||||||
}
|
|
||||||
<Suspense fallback={<MDSkeletonLoader />}>
|
<Suspense fallback={<MDSkeletonLoader />}>
|
||||||
<HomeClient body={body} />
|
<HomeClient body={body} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
19
app/sign-in/page.tsx
Normal file
19
app/sign-in/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { auth } from "@/auth";
|
||||||
|
import SignIn from "@/components/signIn";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
async function SignInUp() {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (session?.user) redirect("/");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid place-items-center h-full">
|
||||||
|
<div>
|
||||||
|
<SignIn />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignInUp;
|
5
app/testing/page.tsx
Normal file
5
app/testing/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { MDEditor } from "@/components/mdeditor";
|
||||||
|
|
||||||
|
export default function Testing() {
|
||||||
|
return <MDEditor />;
|
||||||
|
}
|
@@ -1,6 +1,5 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect class="anvil-base" x="4.5" y="9.5" width="10" height="2" rx="0.5" stroke="inherit" fill="none" />
|
<rect class="prefix__anvil-base" x="4.5" y="9.5" width="10" height="2" rx=".5" stroke="#000" />
|
||||||
<path class="anvil-body"
|
<path class="prefix__anvil-body" d="M6 1H2c1 1 3.5 1.5 5 2s1.543 1.292 0 3L6 7v1h7V7s-1.5-1-2-2 0-2.5 4-3V.5H6V1z"
|
||||||
d="M6 1H2C3 2 5.5 2.5 7 3C8.5 3.5 8.54315 4.2918 7 6L6 7V8H13V7C13 7 11.5 6 11 5C10.5 4 11 2.5 15 2V0.5H6V1Z"
|
stroke="#000" />
|
||||||
stroke="inherit" fill="none" />
|
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 342 B |
5
assets/icons/Discord.svg
Normal file
5
assets/icons/Discord.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-discord"
|
||||||
|
viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
80
auth/index.ts
Normal file
80
auth/index.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import Discord from "next-auth/providers/discord";
|
||||||
|
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { SecretClient } from "@/lib/secret/init";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
|
||||||
|
const sClient = SecretClient();
|
||||||
|
|
||||||
|
const clientId = await sClient.fetchSecret("discord_client_id");
|
||||||
|
const clientSecret = await sClient.fetchSecret("discord_client_secret");
|
||||||
|
|
||||||
|
return {
|
||||||
|
providers: [
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
email: true,
|
||||||
|
emailVerified: true,
|
||||||
|
username: true,
|
||||||
|
passwordHash: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: credentials.email as string,
|
||||||
|
passwordHash: pwHash,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
email: true,
|
||||||
|
emailVerified: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.passwordHash = null;
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
adapter: PrismaAdapter(prisma),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
async function saltAndHashPassword(password: string) {
|
||||||
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
return hash;
|
||||||
|
}
|
@@ -1,23 +1,184 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import Help from "../../assets/icons/Help Icon.svg";
|
|
||||||
import Trash from "../../assets/icons/Trash Icon.svg";
|
const Help = () => (
|
||||||
import Trash_hover from "../../assets/icons/Trash Icon Open.svg";
|
<svg
|
||||||
import Anvil from "../../assets/icons/Anvil Icon.svg";
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="#fff"
|
||||||
|
fill="#fff"
|
||||||
|
height="28"
|
||||||
|
width="28"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
stroke-width="3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
r="12.5"
|
||||||
|
cy="14"
|
||||||
|
cx="14"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="inherit"
|
||||||
|
d="M12.004 17.816v-.867c0-.531.074-1 .223-1.406.148-.414.386-.805.714-1.172.328-.375.762-.758 1.301-1.148.485-.344.871-.653 1.16-.926.297-.274.512-.543.645-.809.14-.273.21-.582.21-.925 0-.508-.187-.895-.562-1.16-.375-.266-.898-.4-1.57-.4s-1.34.106-2.004.317c-.656.211-1.324.489-2.004.832L8.84 7.586c.781-.438 1.629-.79 2.543-1.055.914-.273 1.914-.41 3-.41 1.672 0 2.965.402 3.879 1.207.922.797 1.382 1.813 1.382 3.047 0 .656-.105 1.227-.316 1.71a4.165 4.165 0 01-.937 1.337c-.414.406-.934.836-1.559 1.289-.469.344-.828.633-1.078.867-.25.235-.422.469-.516.703a2.356 2.356 0 00-.129.832v.703zm-.375 4.008c0-.734.2-1.25.598-1.547.406-.297.894-.445 1.464-.445.555 0 1.032.148 1.43.445.406.297.61.813.61 1.547 0 .703-.204 1.211-.61 1.524-.398.312-.875.468-1.43.468-.57 0-1.058-.156-1.464-.468-.399-.313-.598-.82-.598-1.524z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Trash = () => (
|
||||||
|
<svg
|
||||||
|
className="trash-can"
|
||||||
|
width="35"
|
||||||
|
height="30"
|
||||||
|
viewBox="0 0 22 30"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 7H20V27C20 28.1046 19.1046 29 18 29H4C2.89543 29 2 28.1046 2 27V7Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6 11L6 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11 11V24"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 11V24"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="trash-lid"
|
||||||
|
d="M9 0.5H13C13.2761 0.5 13.5 0.723858 13.5 1V2.5H8.5V1C8.5 0.723858 8.72386 0.5 9 0.5Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="trash-lid"
|
||||||
|
d="M2 2.5H20C20.8284 2.5 21.5 3.17157 21.5 4V6.5H0.5V4C0.5 3.17157 1.17157 2.5 2 2.5Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Trash_hover = () => (
|
||||||
|
<svg
|
||||||
|
width="35"
|
||||||
|
height="35"
|
||||||
|
viewBox="0 0 23 35"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 12H21V32C21 33.1046 20.1046 34 19 34H5C3.89543 34 3 33.1046 3 32V12Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7 16L7 29"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 16V29"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M17 16V29"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8.59244 1.36064L12.5317 0.666048C12.8036 0.618097 13.063 0.799681 13.1109 1.07163L13.3714 2.54884L8.44734 3.41708L8.18687 1.93987C8.13891 1.66792 8.3205 1.40859 8.59244 1.36064Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.05644 4.54394L19.783 1.41827C20.5988 1.27442 21.3768 1.81917 21.5207 2.63501L21.9548 5.09703L1.27382 8.74365L0.8397 6.28163C0.695845 5.46578 1.2406 4.6878 2.05644 4.54394Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Anvil = () => (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
className="prefix__anvil-base"
|
||||||
|
x="4.5"
|
||||||
|
y="9.5"
|
||||||
|
width="10"
|
||||||
|
height="2"
|
||||||
|
rx=".5"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="prefix__anvil-body"
|
||||||
|
d="M6 1H2c1 1 3.5 1.5 5 2s1.543 1.292 0 3L6 7v1h7V7s-1.5-1-2-2 0-2.5 4-3V.5H6V1z"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Discord = () => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
className="bi bi-discord"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const library = {
|
const library = {
|
||||||
Help,
|
Help,
|
||||||
Trash,
|
Trash,
|
||||||
Trash_hover,
|
Trash_hover,
|
||||||
Anvil,
|
Anvil,
|
||||||
|
Discord,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
className: string;
|
className?: string;
|
||||||
icon: keyof typeof library;
|
icon: keyof typeof library;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Icon: FC<IProps> = ({ className, icon }) => {
|
export const Icon: FC<IProps> = ({ className, icon }) => {
|
||||||
const ICON = library[icon];
|
const ICON = library[icon];
|
||||||
|
|
||||||
return <ICON className={className} />;
|
return (
|
||||||
|
<span className={className}>
|
||||||
|
<ICON />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
7
components/jotaiProvider.tsx
Normal file
7
components/jotaiProvider.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Provider } from "jotai";
|
||||||
|
|
||||||
|
export function JotaiProvider(props: React.PropsWithChildren) {
|
||||||
|
return <Provider>{props.children}</Provider>;
|
||||||
|
}
|
21
components/mdeditor/TextEditor.tsx
Normal file
21
components/mdeditor/TextEditor.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
19
components/mdeditor/index.tsx
Normal file
19
components/mdeditor/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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">
|
||||||
|
@@ -1,52 +1,79 @@
|
|||||||
"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 { 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 } = useParams<{ schemaId: string }>();
|
const { schemaId, id: gameSystemId } = useParams<{
|
||||||
|
schemaId: string;
|
||||||
|
id: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
const { value: typeName, bind: bindTypeName, reset: resetTypeName } =
|
useEffect(() => {
|
||||||
useInput("");
|
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 {
|
||||||
|
value: typeName,
|
||||||
|
bind: bindTypeName,
|
||||||
|
reset: resetTypeName,
|
||||||
|
} = useInput("");
|
||||||
|
|
||||||
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("");
|
||||||
|
|
||||||
const saveType = useCallback((name: string, type: TypeType) => {
|
const saveType = useCallback(
|
||||||
updateSchema((e) => ({
|
(name: string, type: TypeType) => {
|
||||||
types: {
|
updateSchema((e) => ({
|
||||||
...e.types,
|
types: {
|
||||||
[name]: type,
|
...e.types,
|
||||||
},
|
[name]: type,
|
||||||
}));
|
},
|
||||||
resetTypeName();
|
}));
|
||||||
setPageNumber(0);
|
resetTypeName();
|
||||||
setSelectedType("");
|
setPageNumber(0);
|
||||||
}, [resetTypeName, updateSchema]);
|
setSelectedType("");
|
||||||
|
},
|
||||||
|
[resetTypeName, updateSchema],
|
||||||
|
);
|
||||||
|
|
||||||
const saveSchema = useCallback(async () => {
|
const saveSchema = useCallback(async () => {
|
||||||
setLastSaved(schema);
|
createToast({ msg: "Saving Schema", fading: true });
|
||||||
// const sid = await GameSystemsService.saveSchema(schema);
|
await saveSchemaDb(schema, schema.version);
|
||||||
// if (schemaId === 'new') navigate('/schema/'+sid)
|
}, [createToast, schema]);
|
||||||
}, [schema]);
|
|
||||||
|
|
||||||
const selectTypeForEdit = useCallback((typeKey: string) => {
|
const selectTypeForEdit = useCallback((typeKey: string) => {
|
||||||
setSelectedType(typeKey);
|
setSelectedType(typeKey);
|
||||||
@@ -60,33 +87,36 @@ 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((key: string, template: Template) => {
|
const updateSchemaField = useCallback(
|
||||||
updateSchema((s) => ({
|
(key: string, fieldType: FieldTypes) => {
|
||||||
schema: {
|
updateSchema((s) => ({
|
||||||
...s.schema,
|
fields: {
|
||||||
[key]: template,
|
...s.fields,
|
||||||
},
|
[key]: fieldType,
|
||||||
}));
|
},
|
||||||
}, [updateSchema]);
|
}));
|
||||||
|
},
|
||||||
|
[updateSchema],
|
||||||
|
);
|
||||||
|
|
||||||
const deleteType = useCallback((key: string) => {
|
const deleteType = useCallback(
|
||||||
updateSchema((s) => {
|
(key: string) => {
|
||||||
const types = { ...s.types };
|
updateSchema((s) => {
|
||||||
delete types[key];
|
const types = { ...s.types };
|
||||||
return { types };
|
delete types[key];
|
||||||
});
|
return { types };
|
||||||
}, [updateSchema]);
|
});
|
||||||
|
},
|
||||||
|
[updateSchema],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 p-8">
|
<div className="flex gap-4 p-8">
|
||||||
@@ -107,16 +137,16 @@ 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}
|
fieldType={schemaField as FieldTypes}
|
||||||
template={schemaField}
|
update={updateSchemaField}
|
||||||
update={updateSchemaField}
|
/>
|
||||||
/>
|
),
|
||||||
))}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -136,9 +166,11 @@ export const SchemaBuilder: FC = () => {
|
|||||||
<TypeEditor
|
<TypeEditor
|
||||||
name={selectedType || typeName}
|
name={selectedType || typeName}
|
||||||
saveType={saveType}
|
saveType={saveType}
|
||||||
type={selectedType
|
type={
|
||||||
? schema.types[selectedType as keyof typeof schema.types]
|
selectedType
|
||||||
: undefined}
|
? schema.types[selectedType as keyof typeof schema.types]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AnimatedPageContainer>
|
</AnimatedPageContainer>
|
||||||
<ul className="mt-3 w-96">
|
<ul className="mt-3 w-96">
|
||||||
@@ -154,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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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}>
|
||||||
|
44
components/signIn.tsx
Normal file
44
components/signIn.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { signInWithCreds, signInWithDiscord } from "@/actions/auth";
|
||||||
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
|
export default function SignIn() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<form action={signInWithCreds} className="flex flex-col gap-2">
|
||||||
|
<input
|
||||||
|
className="w-full"
|
||||||
|
placeholder="email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="w-full"
|
||||||
|
placeholder="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
role="button"
|
||||||
|
type="submit"
|
||||||
|
className="w-full p-2 rounded-lg bg-primary-500"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div>
|
||||||
|
<div className="dark:text-dark-500 text-primary-600 ">or</div>
|
||||||
|
<div className="dark:border-dark-500 border-primary-600 flex-grow border-b"></div>
|
||||||
|
</div>
|
||||||
|
<form action={signInWithDiscord}>
|
||||||
|
<button
|
||||||
|
className="w-full p-2 bg-[#816ab1] rounded-lg flex items-center justify-center"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Icon icon="Discord" className="mr-4 inline-block" />
|
||||||
|
Sign in with Discord
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
111
components/toast/index.tsx
Normal file
111
components/toast/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Portal } from "@/lib/portal/components";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type toastMessage = {
|
||||||
|
msg: ReactNode;
|
||||||
|
type?: "error" | "default";
|
||||||
|
fading: boolean;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IDToastMessage = toastMessage & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toastAtom = atom<IDToastMessage[]>([]);
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const [_, setToasts] = useAtom(toastAtom);
|
||||||
|
|
||||||
|
const createToast = useCallback(
|
||||||
|
(t: toastMessage) => {
|
||||||
|
const idd = { ...t, id: crypto.randomUUID() };
|
||||||
|
setToasts((toasts) => {
|
||||||
|
return [...toasts, idd];
|
||||||
|
});
|
||||||
|
|
||||||
|
return idd;
|
||||||
|
},
|
||||||
|
[setToasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearToast = useCallback(
|
||||||
|
(t: toastMessage) => setToasts((toasts) => toasts.filter((to) => to != t)),
|
||||||
|
[setToasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
createToast,
|
||||||
|
clearToast,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const [toasts, setToasts] = useAtom(toastAtom);
|
||||||
|
|
||||||
|
const clearToast = useCallback(
|
||||||
|
(t: toastMessage) => {
|
||||||
|
setToasts((toasts) => {
|
||||||
|
return toasts.filter((to) => to !== t);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setToasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!toasts.length) return <></>;
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 max-w-[95vw] flex flex-col gap-4">
|
||||||
|
{toasts.map((t) => (
|
||||||
|
<Toast key={"toast " + t.id} toast={t} clearToast={clearToast} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toast(props: {
|
||||||
|
toast: toastMessage;
|
||||||
|
clearToast: (t: toastMessage) => void;
|
||||||
|
}) {
|
||||||
|
const { toast, clearToast } = props;
|
||||||
|
const [fading, setFading] = useState(false);
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
setFading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
clearToast(toast);
|
||||||
|
}, 300);
|
||||||
|
}, [clearToast, toast]);
|
||||||
|
|
||||||
|
const fadeOut = useCallback(() => {
|
||||||
|
setTimeout(clear, toast.duration ?? 3000);
|
||||||
|
}, [clear, toast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!toast.fading) return;
|
||||||
|
fadeOut();
|
||||||
|
}, [fadeOut, toast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-fading={fading}
|
||||||
|
data-type={toast.type}
|
||||||
|
className="relative p-6 px-16 toast data-[fading=true]:fade-toast rounded-md bg-mixed-300 data-[type=error]:bg-red-900 border-2 border-mixed-400 data-[type=error]:border-red-700"
|
||||||
|
>
|
||||||
|
{toast.msg}
|
||||||
|
{!toast.fading && (
|
||||||
|
<button
|
||||||
|
className="top-2 right-2 text-xs absolute"
|
||||||
|
onClick={() => clear()}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
33
components/user/index.tsx
Normal file
33
components/user/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { UserCircleIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { UserMenu } from "./menu";
|
||||||
|
|
||||||
|
export const User: FC = async () => {
|
||||||
|
const session = await auth();
|
||||||
|
return (
|
||||||
|
<UserMenu signedIn={!!session?.user}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{session?.user?.image ? (
|
||||||
|
<img
|
||||||
|
src={session.user.image}
|
||||||
|
alt="user avatar"
|
||||||
|
className="rounded-full w-12"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="w-12 h-12 inline-block">
|
||||||
|
<UserCircleIcon className="w-full h-full" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{session?.user?.name ? (
|
||||||
|
<span>Hey there, {session.user.name}!</span>
|
||||||
|
) : (
|
||||||
|
<a className="block flex-grow h-full" href="/sign-in">
|
||||||
|
Sign In
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</UserMenu>
|
||||||
|
);
|
||||||
|
};
|
50
components/user/menu.tsx
Normal file
50
components/user/menu.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { signOutOfApp } from "@/actions/auth";
|
||||||
|
import { FC, PropsWithChildren, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export const UserMenu: FC<PropsWithChildren<{ signedIn: boolean }>> = ({
|
||||||
|
children,
|
||||||
|
signedIn,
|
||||||
|
}) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [closing, setClosing] = useState(true);
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setClosing((c) => !c);
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
setVisible((v) => !v);
|
||||||
|
},
|
||||||
|
visible ? 100 : 0
|
||||||
|
);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={signedIn ? toggle : undefined}
|
||||||
|
className="relative bg-mixed-200 p-2 rounded-lg cursor-pointer w-[220px]"
|
||||||
|
>
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
data-closing={closing}
|
||||||
|
className="absolute bottom-full left-0 right-0 fade-menu"
|
||||||
|
>
|
||||||
|
<ul className="separated-list w-full">
|
||||||
|
<li>
|
||||||
|
<a className="block p-2" href="/profile">
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="p-2" onClick={() => signOutOfApp()}>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -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"
|
||||||
|
74
lib/dice.ts
74
lib/dice.ts
@@ -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;
|
||||||
|
@@ -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}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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;
|
||||||
|
77
lib/tcmd/Resolver.tsx
Normal file
77
lib/tcmd/Resolver.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { PublicationAtom } from "@/recoil/atoms/publication";
|
||||||
|
import { useState, useEffect, useCallback, useRef, ReactNode } from "react";
|
||||||
|
import { TTCQueryResolver } from "../ttcQuery/TTCResolvers";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
export function Resolver({ resolver }: { resolver: string }) {
|
||||||
|
const [parser] = useAtom(PublicationAtom);
|
||||||
|
const [res] = useState(new TTCQueryResolver(parser));
|
||||||
|
const [content, setContent] = useState<ReactNode>("");
|
||||||
|
useEffect(() => {
|
||||||
|
let resolved = res.resolve(resolver);
|
||||||
|
|
||||||
|
setContent(
|
||||||
|
typeof resolved?.display === "function" ? (
|
||||||
|
<resolved.display />
|
||||||
|
) : (
|
||||||
|
resolved?.display
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [resolver, res]);
|
||||||
|
return <span>{content}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTemplate = "$x";
|
||||||
|
export function OnDemandResolver({
|
||||||
|
resolver,
|
||||||
|
template,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
resolver: string;
|
||||||
|
template: string;
|
||||||
|
title?: string;
|
||||||
|
}) {
|
||||||
|
const [parser] = useAtom(PublicationAtom);
|
||||||
|
const res = useRef(new TTCQueryResolver(parser));
|
||||||
|
const [content, setContent] = useState<ReactNode>("");
|
||||||
|
const generateContent = useCallback(() => {
|
||||||
|
setContent(() => {
|
||||||
|
let content = template || defaultTemplate;
|
||||||
|
|
||||||
|
const stackIdxs = Array.from(new Set(content.match(/\$(?:\d+|x)/g)));
|
||||||
|
for (const idx of stackIdxs) {
|
||||||
|
let thing = res.current.getFromStack(idx);
|
||||||
|
if (Array.isArray(thing)) thing = thing.at(0);
|
||||||
|
console.log(thing);
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const resolve = useCallback(() => {
|
||||||
|
res.current.resolve(resolver, true);
|
||||||
|
generateContent();
|
||||||
|
}, [res, resolver, generateContent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-2 rounded-md p-1 bg-black/20 flex flex-col">
|
||||||
|
<button
|
||||||
|
className="text-primary-600"
|
||||||
|
onMouseDown={() => setContent("")}
|
||||||
|
onClick={resolve}
|
||||||
|
>
|
||||||
|
{title ?? "Resolve"}
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
{!!content && <span>{content}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -3,12 +3,15 @@ import Link from "next/link";
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { Poppable } from "../poppables/components/poppable";
|
import { Poppable } from "../poppables/components/poppable";
|
||||||
import { Accordion, AccordionContent } from "../accordion";
|
import { Accordion, AccordionContent } from "../accordion";
|
||||||
|
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>>();
|
||||||
|
|
||||||
@@ -16,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,
|
||||||
@@ -24,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,
|
||||||
@@ -32,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,
|
||||||
@@ -53,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,
|
||||||
@@ -123,8 +126,8 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
/(?<!\/)(?:\[\])+/g,
|
/(?<![\/\?])(?:\[\])+/g,
|
||||||
/\/\[\]/g
|
/\/\[\]/g,
|
||||||
);
|
);
|
||||||
|
|
||||||
// card
|
// card
|
||||||
@@ -169,7 +172,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
/\[\[/g,
|
/\[\[/g,
|
||||||
/\]\]/g
|
/\]\]/g,
|
||||||
);
|
);
|
||||||
|
|
||||||
// fenced code block
|
// fenced code block
|
||||||
@@ -191,7 +194,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// list
|
// list
|
||||||
@@ -230,7 +233,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ordered-list
|
// ordered-list
|
||||||
@@ -238,7 +241,6 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
"ordered-list",
|
"ordered-list",
|
||||||
/(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g,
|
/(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g,
|
||||||
(s, rx) => {
|
(s, rx) => {
|
||||||
// debugger;
|
|
||||||
return {
|
return {
|
||||||
content:
|
content:
|
||||||
s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse ordered list",
|
s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse ordered list",
|
||||||
@@ -253,7 +255,6 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
const { children } = token;
|
const { children } = token;
|
||||||
debugger;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ol
|
<ol
|
||||||
@@ -272,7 +273,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</ol>
|
</ol>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ordered list-item
|
// ordered list-item
|
||||||
@@ -301,7 +302,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
))}
|
))}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// heading
|
// heading
|
||||||
@@ -337,7 +338,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// image
|
// image
|
||||||
@@ -374,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
|
||||||
@@ -418,7 +419,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// inline-code
|
// inline-code
|
||||||
@@ -441,7 +442,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// bold
|
// bold
|
||||||
@@ -459,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
|
||||||
@@ -478,7 +479,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
return <span className="italic">{token.content}</span>;
|
return <span className="italic">{token.content}</span>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// popover
|
// popover
|
||||||
@@ -514,9 +515,10 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Poppable>
|
</Poppable>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// accordion
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"accordion",
|
"accordion",
|
||||||
/\[accordion(\s.*?)?]\n+((?:.|\n)*?)\n+\[\/accordion\]/g,
|
/\[accordion(\s.*?)?]\n+((?:.|\n)*?)\n+\[\/accordion\]/g,
|
||||||
@@ -543,11 +545,13 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
@@ -567,9 +571,10 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// horizontal rule
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"hr",
|
"hr",
|
||||||
/^-{3,}$/gm,
|
/^-{3,}$/gm,
|
||||||
@@ -584,9 +589,10 @@ 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
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"comment",
|
"comment",
|
||||||
/<!--[\s\S]+?-->/g,
|
/<!--[\s\S]+?-->/g,
|
||||||
@@ -601,9 +607,10 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// frontmatter
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"frontmatter",
|
"frontmatter",
|
||||||
/^---([\s\S]*?)---/g,
|
/^---([\s\S]*?)---/g,
|
||||||
@@ -619,9 +626,10 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
return <>{token.raw}</>;
|
return <>{token.raw}</>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// table
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"table",
|
"table",
|
||||||
/(?<=\n|^)\| [\s\S]*? \|(?=(\n|$)(?!\|))/g,
|
/(?<=\n|^)\| [\s\S]*? \|(?=(\n|$)(?!\|))/g,
|
||||||
@@ -649,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[][] = [],
|
||||||
@@ -673,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 {
|
||||||
@@ -764,7 +772,76 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
)}
|
)}
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// resolver
|
||||||
|
registerIdentifier(
|
||||||
|
"resolver",
|
||||||
|
/\?\?<<(.*?)>>/g,
|
||||||
|
(s) => {
|
||||||
|
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
|
||||||
|
if (inp == undefined)
|
||||||
|
return {
|
||||||
|
content: "Error parsing resolver: " + s,
|
||||||
|
metadata: {},
|
||||||
|
raw: "ERROR",
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
content: inp,
|
||||||
|
metadata: {},
|
||||||
|
raw: s,
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(t) => {
|
||||||
|
if (t.content.startsWith("Error"))
|
||||||
|
return <span className="red-500">{t.content}</span>;
|
||||||
|
return <Resolver resolver={t.content} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// on-demand resolver
|
||||||
|
registerIdentifier(
|
||||||
|
"on-demand resolver",
|
||||||
|
/\?\?\[.*?\](\(.*?\))<<(.*?)>>/g,
|
||||||
|
(s) => {
|
||||||
|
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
|
||||||
|
const title = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0];
|
||||||
|
const template = s.match(/(?<=\]\()(.*?)(?=\))/)![0];
|
||||||
|
if (inp == undefined)
|
||||||
|
return {
|
||||||
|
content: "Error parsing resolver: " + s,
|
||||||
|
metadata: {
|
||||||
|
title: "",
|
||||||
|
template: "",
|
||||||
|
},
|
||||||
|
raw: "ERROR",
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
content: inp,
|
||||||
|
metadata: {
|
||||||
|
title,
|
||||||
|
template,
|
||||||
|
},
|
||||||
|
raw: s,
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(t) => {
|
||||||
|
if (t.content.startsWith("Error"))
|
||||||
|
return <span className="red-500">{t.content}</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnDemandResolver
|
||||||
|
resolver={t.content}
|
||||||
|
template={t.metadata.template}
|
||||||
|
title={t.metadata.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return TokenIdentifiers;
|
return TokenIdentifiers;
|
||||||
@@ -773,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;
|
||||||
@@ -829,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;
|
||||||
|
|
||||||
@@ -837,10 +914,11 @@ function search(
|
|||||||
s,
|
s,
|
||||||
// s.substring(0, end - start),
|
// s.substring(0, end - start),
|
||||||
openRx,
|
openRx,
|
||||||
closeRx
|
closeRx,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newEnd === null) throw Error("There was an issue finding a closing tag");
|
if (newEnd === null)
|
||||||
|
throw Error("There was an issue finding a closing tag for ");
|
||||||
|
|
||||||
end = newEnd + start;
|
end = newEnd + start;
|
||||||
|
|
||||||
@@ -856,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;
|
||||||
|
@@ -5,14 +5,16 @@ import { buildOnlyDefaultElements, TokenRenderers } from "./TokenIdentifiers";
|
|||||||
|
|
||||||
export const createElements = (body: string): Token[] => {
|
export const createElements = (body: string): Token[] => {
|
||||||
const tokens = tokenize(body);
|
const tokens = tokenize(body);
|
||||||
console.log(tokens);
|
|
||||||
return buildAbstractSyntaxTree(tokens).map((t) => t.token);
|
return buildAbstractSyntaxTree(tokens).map((t) => t.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);
|
||||||
@@ -177,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));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,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 = {};
|
||||||
|
|
||||||
|
48
lib/ttcQuery/DiceChart.tsx
Normal file
48
lib/ttcQuery/DiceChart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@@ -147,13 +147,3 @@ export class TTCQueryParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example usage:
|
|
||||||
// const
|
|
||||||
|
|
||||||
// const parser = new TTCQueryParser(data);
|
|
||||||
|
|
||||||
// // Example queries
|
|
||||||
// console.log(parser.search("$weapon_abilities[name=Rapid Fire].body")); // Example output
|
|
||||||
// // console.log(parser.search("^weapon_abilities[name=Rapid Fire]", data)); // Example output
|
|
||||||
// console.log(parser.search("$weapon_abilities[name=Rapid Fire].body", data)); // Example output
|
|
||||||
|
@@ -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,19 +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?.display === "function" && !onDemand) {
|
||||||
|
last.display = last.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
return last;
|
||||||
|
} catch (e) {
|
||||||
|
return { value: e?.toString(), display: e?.toString() };
|
||||||
}
|
}
|
||||||
const last = this.stack.at(-1);
|
|
||||||
if (typeof last === "function" && !onDemand) return last();
|
|
||||||
return last;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) this.runQuery(resolver);
|
if (this.isQuery(resolver)) return this.runQuery(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 (
|
||||||
@@ -40,78 +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);
|
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)
|
query.startsWith("_") && this.context
|
||||||
: this.parser.search(query.replace(/^[?_].?/, ""));
|
? 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.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;
|
||||||
console.log(num1, num2);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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
3
lib/utils/sum.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const sum = (...args: number[]) => {
|
||||||
|
return args.reduce((a, b) => a + b, 0);
|
||||||
|
};
|
@@ -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
|
||||||
@@ -34,13 +34,17 @@ ttcMD is a flavor of markdown that has been specifically designed to use with [t
|
|||||||
|
|
||||||
One thing to note, however, is that ttcMD is *not* standard markdown. It does not generate valid markup for just about any element, and as such it will not be released in any scope wider than inside TTC itself. One could more accurately call it a layout language than actual markdown.
|
One thing to note, however, is that ttcMD is *not* standard markdown. It does not generate valid markup for just about any element, and as such it will not be released in any scope wider than inside TTC itself. One could more accurately call it a layout language than actual markdown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Enhanced Standard Elements
|
## Enhanced Standard Elements
|
||||||
|
|
||||||
This section will cover all of the enhancements that are added for basic markdown elements
|
This section will cover all of the enhancements that are added for basic markdown elements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
|
|
||||||
You can use the typical link syntax `[link name](/link/location)`, but there are a few presets that allow you to style them to look a bit nicer.
|
You can use the typical link syntax `[link name](/link/location)` but there are a few presets that allow you to style them to look a bit nicer.
|
||||||
|
|
||||||
**Primary Button:**
|
**Primary Button:**
|
||||||
|
|
||||||
@@ -56,6 +60,8 @@ You can use the typical link syntax `[link name](/link/location)`, but there are
|
|||||||
|
|
||||||
[~~cta link name](#links)
|
[~~cta link name](#links)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
|
|
||||||
Generally tables will only be as wide as their content needs. To make a table take up the full width, you can use this syntax:
|
Generally tables will only be as wide as their content needs. To make a table take up the full width, you can use this syntax:
|
||||||
@@ -118,10 +124,14 @@ Full width with a centered body column
|
|||||||
|
|
||||||
/[]
|
/[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Custom Elements
|
## Custom Elements
|
||||||
|
|
||||||
This section will cover the specific elements custom built for Tabletop Commander.
|
This section will cover the specific elements custom built for Tabletop Commander.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Pop-outs
|
### Pop-outs
|
||||||
|
|
||||||
Pop-outs, or popovers, are the little cards that "pop out" when you hover over an item.
|
Pop-outs, or popovers, are the little cards that "pop out" when you hover over an item.
|
||||||
@@ -134,10 +144,14 @@ This syntax `^[This is my favorite image]<<*Goofy!* .
|
Cards are just neat boxes. They can hold any markdown within them, but not other cards (it looks bad).
|
||||||
@@ -248,6 +264,8 @@ Additionally, you can specify a number after the opening brackets (`[[2 ... ]]`)
|
|||||||
|
|
||||||
/[]
|
/[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Block
|
### Block
|
||||||
|
|
||||||
[][][]
|
[][][]
|
||||||
@@ -273,6 +291,7 @@ Additionally, you can specify a number after the opening brackets (`[[!2 ... ]]`
|
|||||||
|
|
||||||
/[]
|
/[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Grid
|
### Grid
|
||||||
|
|
||||||
@@ -319,12 +338,16 @@ This card will end up in the third column...
|
|||||||
|
|
||||||
/[]
|
/[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Query Elements
|
## Query Elements
|
||||||
|
|
||||||
The following elements are used in combination with ttcQuery. These are definitely more advanced. If you understand generally what "dot notation" is in programming, then it will be a lot easier, but don't let that deter you. Once you understand what it is, you can come back to this and be able to create really cool layouts!
|
The following elements are used in combination with ttcQuery. These are definitely more advanced. If you understand generally what "dot notation" is in programming, then it will be a lot easier, but don't let that deter you. Once you understand what it is, you can come back to this and be able to create really cool layouts!
|
||||||
|
|
||||||
Query elements (aside for the on-demand resolver) are calculated before parsing the markdown. Will that matter to you? Probably not, but could be necessary as you think about how you are writing your query elements.
|
Query elements (aside for the on-demand resolver) are calculated before parsing the markdown. Will that matter to you? Probably not, but could be necessary as you think about how you are writing your query elements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Resolver
|
### Resolver
|
||||||
|
|
||||||
The resolver is the basic element that allows you to get data, but it has a lot of functionality to it. It has not been fully implemented yet
|
The resolver is the basic element that allows you to get data, but it has a lot of functionality to it. It has not been fully implemented yet
|
||||||
@@ -333,21 +356,33 @@ Syntax: `??<<List -> QueryValue>>`
|
|||||||
|
|
||||||
If you've read the ttcQuery docs, you'll know that the `List` type means that you can input any number of `QueryValues` in this element. When the resolver runs, it runs each `QueryValue` from left to right. The last `QueryValue` to run is what gets rendered, but you have access to previous values through their index by using the `$#` variables.
|
If you've read the ttcQuery docs, you'll know that the `List` type means that you can input any number of `QueryValues` in this element. When the resolver runs, it runs each `QueryValue` from left to right. The last `QueryValue` to run is what gets rendered, but you have access to previous values through their index by using the `$#` variables.
|
||||||
|
|
||||||
As `QueryValues` are capable of arithmetic, you can quite easily do simple math inline, though it's unfortunately not quite as simple as just writing an equation. Each `QueryValue` can only do one arithmetic operation. This is to prevent performance issues with parsing arbitrary calculations. So, if you wanted to get the average of the values of 1, 2, 3 and 4, the query would look like this: `??<<1+2,3+4,$0+$1,$2/4>>` which will result in the value 2.5. Arithmetic will fail if a value provided is not a number and will render a message in the markdown.
|
As `QueryValues` are capable of arithmetic, you can quite easily do simple math inline, though it's unfortunately not quite as simple as just writing an equation. Each `QueryValue` can only do one arithmetic operation. This is to prevent performance issues with parsing arbitrary calculations. So, if you wanted to get the average of the values of 1, 2, 3 and 4, the query would look like this: `??<<1+2,3+4,$0+$1,$2/4>>` which will result in the value ??<<1+2,3+4,$0+$1,$2/4>>. Arithmetic will fail if a value provided is not a number and will render a message in the markdown.
|
||||||
|
|
||||||
If the resolver results in a list of items, it will list all of them together, separated by commas.
|
If the resolver results in a list of items, it will list all of them together, separated by commas.
|
||||||
|
|
||||||
Let's say you want to get the the result of rolling a dice field. You would simply write `$$<<_.path.to.dice,$0.roll>>`. This will roll the dice when the markdown is render, which means it only happens once. If you want to reroll the dice, you either need to reload the markdown by closing the viewer and reopening it, or you need to use an On-demand Resolver.
|
Let's say you want to get the the result of rolling a dice field. You would simply write `??<<_.path.to.dice,$0.roll>>` and get the result of the dice roll like this: ??<<_.path.to.dice,$0.roll>>. This will roll the dice when the markdown is rendered, which means it only happens once. If you want to reroll the dice, you either need to reload the markdown by closing the viewer and reopening it, or you need to use an On-demand Resolver.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### On-demand Resolver
|
### On-demand Resolver
|
||||||
|
|
||||||
This works very similarly to the normal resolver, but when it renders it will have a button that you can press. When you press it, it recalculates its value. This is very useful for dice and decks of cards. It has not been fully implemented yet
|
This works very similarly to the normal resolver, but when it renders it will have a button that you can press. When you press it, it recalculates its value. This is very useful for dice and decks of cards. It has not been fully implemented yet
|
||||||
|
|
||||||
Here's the syntax: `??[Template]<<List::QueryValue>>`. Template is a basic string that has access to all values of the resolver using the `$#` variables. If template is left blank, the value that the resolver finally reaches will be rendered. In lieu of a template, you can also have it render the display of a field if it has one.
|
Here's the syntax: `??[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]<<_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>>`
|
||||||
|
|
||||||
For drawing a card and having it show the card's display: `??[]<<_path.to.deck,$0.draw,$1.display>>`
|
??[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>>`
|
||||||
|
]]
|
||||||
|
/[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Query Block Template
|
### Query Block Template
|
||||||
|
|
||||||
@@ -366,6 +401,8 @@ Syntax:
|
|||||||
|
|
||||||
While in a Query Block Template, all resolvers will use the queried value as the root `_` variable. If the original query returns multiple values, it will render the markdown for each result. This is what makes it so useful. When you want to render your 40k army list, you can do so with very little markdown.
|
While in a Query Block Template, all resolvers will use the queried value as the root `_` variable. If the original query returns multiple values, it will render the markdown for each result. This is what makes it so useful. When you want to render your 40k army list, you can do so with very little markdown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Query Block
|
### Query Block
|
||||||
|
|
||||||
Similar to the Query Block Template, the query block collects the values of the query and passes it to the markdown inside of it. However, instead of rendering the whole block, it instead renders the markdown a single time. It has not been fully implemented yet
|
Similar to the Query Block Template, the query block collects the values of the query and passes it to the markdown inside of it. However, instead of rendering the whole block, it instead renders the markdown a single time. It has not been fully implemented yet
|
||||||
|
@@ -12,4 +12,9 @@
|
|||||||
3. my
|
3. my
|
||||||
4. name
|
4. name
|
||||||
5. is
|
5. is
|
||||||
6. welcome
|
6. welcome
|
||||||
|
|
||||||
|
- [-\_hello1234!@#$%^\&\*()-](#-_hello1234-)
|
||||||
|
|
||||||
|
|
||||||
|
# -_hello1234!@#$%^&*()-
|
||||||
|
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
1
notmiddleware.ts
Normal file
1
notmiddleware.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { auth as middleware } from "@/auth";
|
1578
package-lock.json
generated
1578
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -6,17 +6,27 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"postinstall": "bun ./postinstall/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@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.11.0",
|
"@prisma/client": "^5.18.0",
|
||||||
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@uiw/codemirror-theme-duotone": "^4.23.0",
|
||||||
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"isomorphic-dompurify": "^2.4.0",
|
"isomorphic-dompurify": "^2.4.0",
|
||||||
"next": "14.1.0",
|
"jotai": "^2.9.3",
|
||||||
"prisma": "^5.11.0",
|
"next": "^14.2.5",
|
||||||
|
"next-auth": "^5.0.0-beta.20",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"recoil": "^0.7.7"
|
"recoil": "^0.7.7",
|
||||||
|
"url-loader": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
@@ -27,6 +37,7 @@
|
|||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.1.0",
|
"eslint-config-next": "14.1.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
"prisma": "^5.18.0",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^7.2.0"
|
"typescript-eslint": "^7.2.0"
|
||||||
|
22
postinstall/buildEnv.ts
Normal file
22
postinstall/buildEnv.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const { SecretClient } = require("../lib/secret/init");
|
||||||
|
const { writeFile } = require("fs/promises");
|
||||||
|
|
||||||
|
const requiredKeys = [
|
||||||
|
"discord_client_id",
|
||||||
|
"discord_client_secret",
|
||||||
|
"ttc:database_url",
|
||||||
|
];
|
||||||
|
|
||||||
|
const secretClient = SecretClient();
|
||||||
|
|
||||||
|
async function buildEnv() {
|
||||||
|
secretClient.fetchToken();
|
||||||
|
let secrets = "";
|
||||||
|
for (const key of requiredKeys) {
|
||||||
|
const value = await secretClient.fetchSecret(key);
|
||||||
|
secrets += `${key.replace("ttc:", "").toUpperCase()}=${value}\n`;
|
||||||
|
}
|
||||||
|
await writeFile(".env", secrets, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEnv();
|
1
postinstall/index.ts
Normal file
1
postinstall/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
require("./buildEnv.ts");
|
@@ -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;
|
|
@@ -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`);
|
|
@@ -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;
|
|
177
prisma/migrations/20240820145917_init/migration.sql
Normal file
177
prisma/migrations/20240820145917_init/migration.sql
Normal 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;
|
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Schema" ALTER COLUMN "originalId" DROP NOT NULL;
|
123
prisma/migrations/20240904083737_new_defaults/migration.sql
Normal file
123
prisma/migrations/20240904083737_new_defaults/migration.sql
Normal 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;
|
@@ -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"
|
@@ -1,67 +1,175 @@
|
|||||||
// 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 {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
gameSystem GameSystem @relation(fields: [gameSystemId], references: [id])
|
gameSystem GameSystem? @relation(fields: [gameSystemId], references: [id])
|
||||||
gameSystemId String
|
gameSystemId String?
|
||||||
publications Publication[]
|
publications Publication[]
|
||||||
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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model PublicationRevision {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
Tag Tag[]
|
||||||
|
previousId String?
|
||||||
|
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
|
||||||
|
|
||||||
|
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())
|
||||||
publication Publication @relation(fields: [publicationId], references: [id])
|
name String
|
||||||
publicationId String
|
Publication Publication @relation(fields: [publicationId], references: [id])
|
||||||
|
publicationId String
|
||||||
|
PublicationRevision PublicationRevision @relation(fields: [publicationRevisionId], references: [id])
|
||||||
|
publicationRevisionId String
|
||||||
|
|
||||||
|
// TagsOnPublications TagsOnPublications[]
|
||||||
}
|
}
|
||||||
|
|
||||||
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?
|
||||||
|
|
||||||
username String
|
name String?
|
||||||
email String @unique
|
username String? @unique
|
||||||
|
email String? @unique
|
||||||
|
emailVerified DateTime?
|
||||||
|
passwordHash String?
|
||||||
|
image String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
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 {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
providerAccountId String
|
||||||
|
refresh_token String? @db.Text
|
||||||
|
access_token String? @db.Text
|
||||||
|
expires_at Int?
|
||||||
|
token_type String?
|
||||||
|
scope String?
|
||||||
|
id_token String? @db.Text
|
||||||
|
session_state String?
|
||||||
|
refresh_token_expires_in Int?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([provider, providerAccountId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionToken String @unique
|
||||||
|
userId String
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@unique([identifier, token])
|
||||||
}
|
}
|
||||||
|
14
recoil/atoms/publication.ts
Normal file
14
recoil/atoms/publication.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { TTCQueryParser } from "@/lib/ttcQuery/TTCQueryParser";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
// import { atom } from "recoil";
|
||||||
|
|
||||||
|
// export const PublicationAtom = atom({
|
||||||
|
// key: "publication",
|
||||||
|
// default: new TTCQueryParser({
|
||||||
|
// path: { to: { dice: "2d6" } },
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
|
||||||
|
export const PublicationAtom = atom(
|
||||||
|
new TTCQueryParser({ path: { to: { dice: "2d6" } } }),
|
||||||
|
);
|
@@ -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,
|
||||||
});
|
});
|
||||||
|
@@ -51,6 +51,6 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("@tailwindcss/container-queries")],
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
@@ -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
18
types.d.ts
vendored
@@ -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
54
util/isEmailVerified.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user