Compare commits
24 Commits
0f100bba3d
...
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 |
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,19 +1,21 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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">
|
||||||
|
<SessionProvider>
|
||||||
<body className={roboto.className + " flex min-h-[100vh]"}>
|
<body className={roboto.className + " flex min-h-[100vh]"}>
|
||||||
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
|
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
||||||
<Link href="/">Tabletop Commander</Link>
|
<Link href="/">Tabletop Commander</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<ul className="my-6 flex flex-col gap-6">
|
<Nav game={currentGame ?? undefined} />
|
||||||
{navItems.map((n) => (
|
<div className="mt-auto">
|
||||||
<li key={"nav-item" + n.text}>
|
<User />
|
||||||
<Link
|
</div>
|
||||||
href={n.to}
|
</div>
|
||||||
className="flex items-center gap-2 group hover:text-purple-300 transition-colors"
|
|
||||||
>
|
|
||||||
<n.icon className="w-6 h-6 group-hover:fill-purple-300 transition-colors" />
|
|
||||||
{n.text}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
<RecoilRootClient>
|
<RecoilRootClient>
|
||||||
|
<JotaiProvider>
|
||||||
<DevToolboxContextProvider
|
<DevToolboxContextProvider
|
||||||
isDev={process.env.NODE_ENV !== "production"}
|
isDev={process.env.NODE_ENV !== "production"}
|
||||||
>
|
>
|
||||||
<main className="p-8 w-full overflow-visible">
|
<main className="p-8 w-full overflow-visible">{children}</main>
|
||||||
{children}
|
<Toaster />
|
||||||
</main>
|
|
||||||
</DevToolboxContextProvider>
|
</DevToolboxContextProvider>
|
||||||
|
</JotaiProvider>
|
||||||
</RecoilRootClient>
|
</RecoilRootClient>
|
||||||
<div id="root-portal"></div>
|
<div id="root-portal"></div>
|
||||||
</body>
|
</body>
|
||||||
|
</SessionProvider>
|
||||||
|
<SSE />
|
||||||
</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,36 +1,62 @@
|
|||||||
"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(
|
||||||
|
(name: string, type: TypeType) => {
|
||||||
updateSchema((e) => ({
|
updateSchema((e) => ({
|
||||||
types: {
|
types: {
|
||||||
...e.types,
|
...e.types,
|
||||||
@@ -40,13 +66,14 @@ export const SchemaBuilder: FC = () => {
|
|||||||
resetTypeName();
|
resetTypeName();
|
||||||
setPageNumber(0);
|
setPageNumber(0);
|
||||||
setSelectedType("");
|
setSelectedType("");
|
||||||
}, [resetTypeName, updateSchema]);
|
},
|
||||||
|
[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(
|
||||||
|
(key: string, fieldType: FieldTypes) => {
|
||||||
updateSchema((s) => ({
|
updateSchema((s) => ({
|
||||||
schema: {
|
fields: {
|
||||||
...s.schema,
|
...s.fields,
|
||||||
[key]: template,
|
[key]: fieldType,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}, [updateSchema]);
|
},
|
||||||
|
[updateSchema],
|
||||||
|
);
|
||||||
|
|
||||||
const deleteType = useCallback((key: string) => {
|
const deleteType = useCallback(
|
||||||
|
(key: string) => {
|
||||||
updateSchema((s) => {
|
updateSchema((s) => {
|
||||||
const types = { ...s.types };
|
const types = { ...s.types };
|
||||||
delete types[key];
|
delete types[key];
|
||||||
return { types };
|
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}
|
||||||
template={schemaField}
|
fieldType={schemaField as FieldTypes}
|
||||||
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={
|
||||||
|
selectedType
|
||||||
? schema.types[selectedType as keyof typeof schema.types]
|
? schema.types[selectedType as keyof typeof schema.types]
|
||||||
: undefined}
|
: 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,7 +42,8 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
|
|||||||
resetType();
|
resetType();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addField = useCallback((e: FormEvent) => {
|
const addField = useCallback(
|
||||||
|
(e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
updateType({
|
updateType({
|
||||||
[propertyName]: {
|
[propertyName]: {
|
||||||
@@ -51,7 +55,9 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
resetPropertyName();
|
resetPropertyName();
|
||||||
}, [propertyName, updateType, 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(
|
||||||
|
(name: string) => {
|
||||||
setType((t) => {
|
setType((t) => {
|
||||||
const fields = { ...t };
|
const fields = { ...t };
|
||||||
delete fields[name];
|
delete fields[name];
|
||||||
return fields;
|
return fields;
|
||||||
});
|
});
|
||||||
}, [setType]);
|
},
|
||||||
|
[setType],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -82,9 +91,10 @@ 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))
|
||||||
|
.map(([key, value]) => (
|
||||||
<FieldEditor
|
<FieldEditor
|
||||||
key={"field-editor" + key}
|
key={"field-editor" + key}
|
||||||
field={value}
|
field={value}
|
||||||
|
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"
|
||||||
|
72
lib/dice.ts
72
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;
|
||||||
|
@@ -1,18 +1,27 @@
|
|||||||
import { PublicationAtom } from "@/recoil/atoms/publication";
|
import { PublicationAtom } from "@/recoil/atoms/publication";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef, ReactNode } from "react";
|
||||||
import { useRecoilValue } from "recoil";
|
|
||||||
import { TTCQueryResolver } from "../ttcQuery/TTCResolvers";
|
import { TTCQueryResolver } from "../ttcQuery/TTCResolvers";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
export function Resolver({ resolver }: { resolver: string }) {
|
export function Resolver({ resolver }: { resolver: string }) {
|
||||||
const parser = useRecoilValue(PublicationAtom);
|
const [parser] = useAtom(PublicationAtom);
|
||||||
const [res] = useState(new TTCQueryResolver(parser));
|
const [res] = useState(new TTCQueryResolver(parser));
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState<ReactNode>("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContent(res.resolve(resolver));
|
let resolved = res.resolve(resolver);
|
||||||
|
|
||||||
|
setContent(
|
||||||
|
typeof resolved?.display === "function" ? (
|
||||||
|
<resolved.display />
|
||||||
|
) : (
|
||||||
|
resolved?.display
|
||||||
|
),
|
||||||
|
);
|
||||||
}, [resolver, res]);
|
}, [resolver, res]);
|
||||||
return <span>{content}</span>;
|
return <span>{content}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultTemplate = "$x";
|
||||||
export function OnDemandResolver({
|
export function OnDemandResolver({
|
||||||
resolver,
|
resolver,
|
||||||
template,
|
template,
|
||||||
@@ -22,20 +31,29 @@ export function OnDemandResolver({
|
|||||||
template: string;
|
template: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) {
|
}) {
|
||||||
const parser = useRecoilValue(PublicationAtom);
|
const [parser] = useAtom(PublicationAtom);
|
||||||
const res = useRef(new TTCQueryResolver(parser));
|
const res = useRef(new TTCQueryResolver(parser));
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState<ReactNode>("");
|
||||||
const generateContent = useCallback(() => {
|
const generateContent = useCallback(() => {
|
||||||
let content = template;
|
setContent(() => {
|
||||||
const stackIdxs = Array.from(new Set(template.match(/\$\d/g)));
|
let content = template || defaultTemplate;
|
||||||
|
|
||||||
|
const stackIdxs = Array.from(new Set(content.match(/\$(?:\d+|x)/g)));
|
||||||
for (const idx of stackIdxs) {
|
for (const idx of stackIdxs) {
|
||||||
let thing = res.current.getFromStack(idx);
|
let thing = res.current.getFromStack(idx);
|
||||||
debugger;
|
|
||||||
if (Array.isArray(thing)) thing = thing.at(0);
|
if (Array.isArray(thing)) thing = thing.at(0);
|
||||||
if (typeof thing === "function") thing = thing();
|
console.log(thing);
|
||||||
content = content.replaceAll(idx, thing as string);
|
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;
|
||||||
}
|
}
|
||||||
setContent(content);
|
// else if (idx === defaultTemplate && )
|
||||||
|
else content = content.replaceAll(idx, thing.display as string);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
});
|
||||||
}, [res, template]);
|
}, [res, template]);
|
||||||
|
|
||||||
const resolve = useCallback(() => {
|
const resolve = useCallback(() => {
|
||||||
@@ -44,12 +62,16 @@ export function OnDemandResolver({
|
|||||||
}, [res, resolver, generateContent]);
|
}, [res, resolver, generateContent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="my-2 rounded-md p-1 bg-black/20 flex flex-col">
|
||||||
<button onMouseDown={() => setContent("")} onClick={resolve}>
|
<button
|
||||||
|
className="text-primary-600"
|
||||||
|
onMouseDown={() => setContent("")}
|
||||||
|
onClick={resolve}
|
||||||
|
>
|
||||||
{title ?? "Resolve"}
|
{title ?? "Resolve"}
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<br />
|
||||||
{!!content && <span>{content}</span>}
|
{!!content && <span>{content}</span>}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -5,11 +5,13 @@ import { Poppable } from "../poppables/components/poppable";
|
|||||||
import { Accordion, AccordionContent } from "../accordion";
|
import { Accordion, AccordionContent } from "../accordion";
|
||||||
import { OnDemandResolver, Resolver } from "./Resolver";
|
import { OnDemandResolver, Resolver } from "./Resolver";
|
||||||
|
|
||||||
|
// import "crypto";
|
||||||
|
|
||||||
export const TokenRenderers = new Map<string, TokenRenderer<any>>();
|
export const TokenRenderers = new Map<string, TokenRenderer<any>>();
|
||||||
|
|
||||||
export function buildIdentifierMap(): [
|
export function buildIdentifierMap(): [
|
||||||
TokenIdentifierMap,
|
TokenIdentifierMap,
|
||||||
IdentifierRegistration
|
IdentifierRegistration,
|
||||||
] {
|
] {
|
||||||
const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();
|
const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ export function buildIdentifierMap(): [
|
|||||||
type: string,
|
type: string,
|
||||||
match: RegExp,
|
match: RegExp,
|
||||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||||
renderFunction: TokenRenderer<M>
|
renderFunction: TokenRenderer<M>,
|
||||||
): void;
|
): void;
|
||||||
function registerIdentifier<M>(
|
function registerIdentifier<M>(
|
||||||
type: string,
|
type: string,
|
||||||
@@ -25,7 +27,7 @@ export function buildIdentifierMap(): [
|
|||||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||||
renderFunction: TokenRenderer<M>,
|
renderFunction: TokenRenderer<M>,
|
||||||
openTagRx: RegExp,
|
openTagRx: RegExp,
|
||||||
closeTagRx: RegExp
|
closeTagRx: RegExp,
|
||||||
): void;
|
): void;
|
||||||
function registerIdentifier<M = Record<string, string>>(
|
function registerIdentifier<M = Record<string, string>>(
|
||||||
type: string,
|
type: string,
|
||||||
@@ -33,7 +35,7 @@ export function buildIdentifierMap(): [
|
|||||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||||
renderFunction: TokenRenderer<M>,
|
renderFunction: TokenRenderer<M>,
|
||||||
openTagRx?: RegExp,
|
openTagRx?: RegExp,
|
||||||
closeTagRx?: RegExp
|
closeTagRx?: RegExp,
|
||||||
) {
|
) {
|
||||||
TokenIdentifiers.set(type, {
|
TokenIdentifiers.set(type, {
|
||||||
rx: match,
|
rx: match,
|
||||||
@@ -54,7 +56,7 @@ export function buildIdentifierMap(): [
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
new RegExp(openTagRx, "g"),
|
new RegExp(openTagRx, "g"),
|
||||||
new RegExp(closeTagRx, "g")
|
new RegExp(closeTagRx, "g"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -125,7 +127,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
/(?<![\/\?])(?:\[\])+/g,
|
/(?<![\/\?])(?:\[\])+/g,
|
||||||
/\/\[\]/g
|
/\/\[\]/g,
|
||||||
);
|
);
|
||||||
|
|
||||||
// card
|
// card
|
||||||
@@ -170,7 +172,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
/\[\[/g,
|
/\[\[/g,
|
||||||
/\]\]/g
|
/\]\]/g,
|
||||||
);
|
);
|
||||||
|
|
||||||
// fenced code block
|
// fenced code block
|
||||||
@@ -192,7 +194,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// list
|
// list
|
||||||
@@ -231,7 +233,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ordered-list
|
// ordered-list
|
||||||
@@ -239,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",
|
||||||
@@ -254,7 +255,6 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
const { children } = token;
|
const { children } = token;
|
||||||
debugger;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ol
|
<ol
|
||||||
@@ -273,7 +273,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</ol>
|
</ol>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ordered list-item
|
// ordered list-item
|
||||||
@@ -302,7 +302,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
))}
|
))}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// heading
|
// heading
|
||||||
@@ -338,7 +338,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// image
|
// image
|
||||||
@@ -375,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
|
||||||
@@ -419,7 +419,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// inline-code
|
// inline-code
|
||||||
@@ -442,7 +442,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
{token.content}
|
{token.content}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// bold
|
// bold
|
||||||
@@ -460,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
|
||||||
@@ -479,7 +479,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
return <span className="italic">{token.content}</span>;
|
return <span className="italic">{token.content}</span>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// popover
|
// popover
|
||||||
@@ -515,7 +515,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Poppable>
|
</Poppable>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// accordion
|
// accordion
|
||||||
@@ -545,12 +545,13 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// paragraph
|
// paragraph
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"p",
|
"p",
|
||||||
|
// /(?<=\n\n|^)([\s\S]*?)(?=\n\n|$)/g,
|
||||||
/(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
|
/(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
|
||||||
(s) => {
|
(s) => {
|
||||||
return {
|
return {
|
||||||
@@ -570,7 +571,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// horizontal rule
|
// horizontal rule
|
||||||
@@ -588,7 +589,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
return <div className="w-full border-b border-mixed-500 my-3"></div>;
|
return <div className="w-full border-b border-mixed-500 my-3"></div>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// comment
|
// comment
|
||||||
@@ -606,7 +607,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// frontmatter
|
// frontmatter
|
||||||
@@ -625,7 +626,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
return <>{token.raw}</>;
|
return <>{token.raw}</>;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// table
|
// table
|
||||||
@@ -656,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[][] = [],
|
||||||
@@ -680,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 {
|
||||||
@@ -771,7 +772,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
)}
|
)}
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// resolver
|
// resolver
|
||||||
@@ -798,17 +799,17 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
if (t.content.startsWith("Error"))
|
if (t.content.startsWith("Error"))
|
||||||
return <span className="red-500">{t.content}</span>;
|
return <span className="red-500">{t.content}</span>;
|
||||||
return <Resolver resolver={t.content} />;
|
return <Resolver resolver={t.content} />;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// on-demand resolver
|
// on-demand resolver
|
||||||
registerIdentifier(
|
registerIdentifier(
|
||||||
"on-demand resolver",
|
"on-demand resolver",
|
||||||
/\?\?\[.*?\](\(.*?\))?<<(.*?)>>/g,
|
/\?\?\[.*?\](\(.*?\))<<(.*?)>>/g,
|
||||||
(s) => {
|
(s) => {
|
||||||
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
|
const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0];
|
||||||
const template = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0];
|
const title = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0];
|
||||||
const title = s.match(/(?<=\]\()(.*?)(?=\))/)?.at(0);
|
const template = s.match(/(?<=\]\()(.*?)(?=\))/)![0];
|
||||||
if (inp == undefined)
|
if (inp == undefined)
|
||||||
return {
|
return {
|
||||||
content: "Error parsing resolver: " + s,
|
content: "Error parsing resolver: " + s,
|
||||||
@@ -840,7 +841,7 @@ export const buildOnlyDefaultElements = () => {
|
|||||||
title={t.metadata.title}
|
title={t.metadata.title}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return TokenIdentifiers;
|
return TokenIdentifiers;
|
||||||
@@ -849,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;
|
||||||
@@ -905,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;
|
||||||
|
|
||||||
@@ -913,7 +914,7 @@ function search(
|
|||||||
s,
|
s,
|
||||||
// s.substring(0, end - start),
|
// s.substring(0, end - start),
|
||||||
openRx,
|
openRx,
|
||||||
closeRx
|
closeRx,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newEnd === null)
|
if (newEnd === null)
|
||||||
@@ -933,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;
|
||||||
|
@@ -11,7 +11,10 @@ export const createElements = (body: string): Token[] => {
|
|||||||
const tokenize = (body: string) => {
|
const tokenize = (body: string) => {
|
||||||
const tokenizedBody: TokenMarker[] = [];
|
const tokenizedBody: TokenMarker[] = [];
|
||||||
|
|
||||||
body = body.replaceAll(/[ \t]+\n/g, "\n").replaceAll(/\n{3,}/g, "\n\n");
|
body = body
|
||||||
|
.trim()
|
||||||
|
.replaceAll(/[ \t]+\n/g, "\n")
|
||||||
|
.replaceAll(/\n{3,}/g, "\n\n");
|
||||||
|
|
||||||
const addToken = (thing: TokenMarker) => {
|
const addToken = (thing: TokenMarker) => {
|
||||||
tokenizedBody.push(thing);
|
tokenizedBody.push(thing);
|
||||||
@@ -176,9 +179,9 @@ const contentToChildren = (token: Token) => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
token.children || []
|
token.children || [],
|
||||||
).filter((c) => c.children?.length || (c.rendersContentOnly && c.content));
|
).filter((c) => c.children?.length || (c.rendersContentOnly && c.content));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,7 +272,10 @@ export function extractFrontMatter(body: string): [FrontMatter, string] {
|
|||||||
const rx = /^---([\s\S]*?)---/;
|
const rx = /^---([\s\S]*?)---/;
|
||||||
const [_, frontmatterString] = body.match(rx) || ["", ""];
|
const [_, frontmatterString] = body.match(rx) || ["", ""];
|
||||||
|
|
||||||
body = body.replace(rx, "");
|
console.log(body.replace(rx, ""));
|
||||||
|
|
||||||
|
body = body.replace(rx, "").trim();
|
||||||
|
console.log(body);
|
||||||
|
|
||||||
const frontMatter: FrontMatter = {};
|
const frontMatter: FrontMatter = {};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@@ -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,22 +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 {
|
||||||
|
try {
|
||||||
const resList = resolver.split(",");
|
const resList = resolver.split(",");
|
||||||
for (const res of resList) {
|
for (const res of resList) {
|
||||||
this.stack.push(this.parseResolver(res));
|
this.stack.push(this.parseResolver(res));
|
||||||
}
|
}
|
||||||
const last = this.stack.at(-1);
|
const last = this.stack.at(-1);
|
||||||
if (typeof last === "function" && !onDemand) return last();
|
if (typeof last?.display === "function" && !onDemand) {
|
||||||
|
last.display = last.display();
|
||||||
if (onDemand) debugger;
|
|
||||||
return last;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseResolver(resolver: string) {
|
return last;
|
||||||
|
} catch (e) {
|
||||||
|
return { value: e?.toString(), display: e?.toString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseResolver(resolver: string): StackItem {
|
||||||
if (this.isArithmetic(resolver)) return this.solveArithmetic(resolver);
|
if (this.isArithmetic(resolver)) return this.solveArithmetic(resolver);
|
||||||
if (this.isQuery(resolver)) return this.runQuery(resolver);
|
if (this.isQuery(resolver)) return this.runQuery(resolver);
|
||||||
return resolver;
|
if (Dice.isDice(resolver)) {
|
||||||
|
const value = new Dice(resolver);
|
||||||
|
const dice: StackItem = {
|
||||||
|
value,
|
||||||
|
display: () => value.toString(),
|
||||||
|
};
|
||||||
|
return dice;
|
||||||
|
}
|
||||||
|
return { value: resolver, display: resolver };
|
||||||
}
|
}
|
||||||
private isQuery(resolver: string) {
|
private isQuery(resolver: string) {
|
||||||
return (
|
return (
|
||||||
@@ -43,57 +65,81 @@ export class TTCQueryResolver {
|
|||||||
/^\$\d\./.test(resolver)
|
/^\$\d\./.test(resolver)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
private runQuery(query: string) {
|
private runQuery(query: string): StackItem {
|
||||||
if (!this.parser) throw "Parser not defined in query resolver";
|
if (!this.parser) throw "Parser not defined in query resolver";
|
||||||
if (query.startsWith("$")) {
|
if (query.startsWith("$")) {
|
||||||
const [_, idx, q] = query.match(/^(\$\d+)\.(.*)/) || [];
|
const [_, idx, q] = query.match(/^(\$\d+)\.(.*)/) || [];
|
||||||
if (!_) throw "Detected stack query but did not match the regex";
|
if (!_) throw "Detected stack query but did not match the regex";
|
||||||
const stackItem = this.getFromStack(idx);
|
const stackItem = this.getFromStack(idx);
|
||||||
if (typeof stackItem === "string" && Dice.isDice(stackItem)) {
|
|
||||||
return this.handleDice(stackItem, q);
|
if (stackItem.value instanceof Dice) {
|
||||||
|
return {
|
||||||
|
value: query,
|
||||||
|
display: this.handleDice(stackItem.value, q),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.parser.search(q, stackItem as QueryableObject);
|
const [res] = this.parser.search(q, stackItem.value as QueryableObject);
|
||||||
|
if (Dice.isDice(res)) {
|
||||||
|
const value = new Dice(res);
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
display: () => value.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: res,
|
||||||
|
display() {
|
||||||
|
return (
|
||||||
|
this.value.render ?? this.value ?? "Error resolving query: " + query
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (query.startsWith("?") || query.startsWith("_"))
|
// if (query.startsWith("?") || query.startsWith("_"))
|
||||||
return query.startsWith("_") && this.context
|
const res =
|
||||||
|
query.startsWith("_") && this.context
|
||||||
? this.parser.search(query.replace("_", "^"), this.context).at(0)
|
? this.parser.search(query.replace("_", "^"), this.context).at(0)
|
||||||
: this.parser.search(query.replace(/^[?_].?/, "")).at(0);
|
: this.parser.search(query.replace(/^[?_].?/, "")).at(0);
|
||||||
|
if (Dice.isDice(res)) {
|
||||||
|
const value = new Dice(res);
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
display: () => value.toString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
private handleDice(dice: string, query: string) {
|
value: res,
|
||||||
const d = new Dice(dice);
|
display() {
|
||||||
const [method, n] = query.split(":");
|
return (
|
||||||
let num = Number(n);
|
this.value.render ?? this.value ?? "Error resolving query: " + query
|
||||||
if (n && n.startsWith("$")) num = this.getFromStack(n);
|
);
|
||||||
switch (method) {
|
},
|
||||||
case "roll":
|
};
|
||||||
return () => d.roll();
|
|
||||||
case "rollAvg":
|
|
||||||
return () => d.rollAvg();
|
|
||||||
case "rollTimes":
|
|
||||||
return () => d.rollTimes(num);
|
|
||||||
case "rollTimesAvg":
|
|
||||||
return () => d.rollTimesAvg(num);
|
|
||||||
default:
|
|
||||||
return "No valid method provided for dice";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isArithmetic(resolver: string) {
|
private isArithmetic(resolver: string) {
|
||||||
return resolver.split(/\+|\/|-|\*|\^/).filter((e) => !!e).length > 1;
|
return resolver.split(/\+|\/|-|\*|\^/).filter((e) => !!e).length > 1;
|
||||||
}
|
}
|
||||||
private solveArithmetic(resolver: string) {
|
private solveArithmetic(resolver: string): StackItem {
|
||||||
const [n1, op, n2] = resolver
|
const [n1, op, n2] = resolver
|
||||||
.match(/(\$?\d+)([+\-*\/^])(\$?\d+)/)
|
.match(/(\$?\d+)([+\-*\/^])(\$?\d+)/)
|
||||||
?.slice(1) || ["", "+", ""];
|
?.slice(1) || ["", "+", ""];
|
||||||
let num1 = Number(n1),
|
let num1: number = Number(n1),
|
||||||
num2 = Number(n2);
|
num2: number = Number(n2);
|
||||||
|
|
||||||
if (n1.startsWith("$")) num1 = this.getFromStack<number>(n1);
|
if (n1.startsWith("$")) {
|
||||||
if (n2.startsWith("$")) num2 = this.getFromStack<number>(n2);
|
const result = this.getFromStack(n1).value;
|
||||||
|
num1 = result instanceof Dice ? result.roll().total : Number(result);
|
||||||
|
}
|
||||||
|
if (n2.startsWith("$")) {
|
||||||
|
const result = this.getFromStack(n1).value;
|
||||||
|
num2 = result instanceof Dice ? result.roll().total : Number(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thing: StackItem = {
|
||||||
|
value: () => {
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case "+":
|
case "+":
|
||||||
return num1 + num2;
|
return num1 + num2;
|
||||||
@@ -108,11 +154,51 @@ export class TTCQueryResolver {
|
|||||||
default:
|
default:
|
||||||
throw "Arithmetic detected but no proper operator assigned";
|
throw "Arithmetic detected but no proper operator assigned";
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
display() {
|
||||||
|
return typeof this.value === "function" ? this.value() : this.value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return thing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFromStack<T>(stackIndex: string): T {
|
public getFromStack(stackIndex: string): StackItem {
|
||||||
|
if (stackIndex === "$x") return this.stack.at(-1)!;
|
||||||
const i = Number(stackIndex.replace("$", ""));
|
const i = Number(stackIndex.replace("$", ""));
|
||||||
const val = this.stack[i] as T;
|
const val = this.stack[i];
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
private handleDice(dice: Dice, query: string) {
|
||||||
|
const [method, n] = query.split(":");
|
||||||
|
let num = Number(n);
|
||||||
|
// if (n && n.startsWith("$")) num = this.getFromStack(n);
|
||||||
|
switch (method) {
|
||||||
|
case "roll":
|
||||||
|
return () => dice.roll.apply(dice).total;
|
||||||
|
case "rollAvg":
|
||||||
|
return () => dice.rollAvg.apply(dice);
|
||||||
|
case "rollTimes":
|
||||||
|
return () => dice.rollTimes.apply(dice, [num]);
|
||||||
|
case "rollTimesAvg":
|
||||||
|
return () => dice.rollTimesAvg.apply(dice, [num]);
|
||||||
|
case "rollLowest":
|
||||||
|
return () => dice.rollMin.apply(dice);
|
||||||
|
case "rollHighest":
|
||||||
|
return () => dice.rollMax.apply(dice);
|
||||||
|
case "rollDropHighest":
|
||||||
|
return () =>
|
||||||
|
sum(...dice.roll.apply(dice).results.toSorted().toSpliced(-1, 1));
|
||||||
|
case "rollDropLowest":
|
||||||
|
return () =>
|
||||||
|
sum(...dice.roll.apply(dice).results.toSorted().toSpliced(0, 1));
|
||||||
|
case "distribution":
|
||||||
|
return () => DiceChart({ dice: dice.getRollDistribution.apply(dice) });
|
||||||
|
case "distribution.normalized":
|
||||||
|
return () =>
|
||||||
|
DiceChart({ dice: dice.getNormalizedRollDistribution.apply(dice) });
|
||||||
|
default:
|
||||||
|
return () => "No valid method provided for dice";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
lib/utils/sum.ts
Normal file
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
|
||||||
@@ -368,17 +368,17 @@ Let's say you want to get the the result of rolling a dice field. You would simp
|
|||||||
|
|
||||||
This works very similarly to the normal resolver, but when it renders it will have a button that you can press. When you press it, it recalculates its value. This is very useful for dice and decks of cards. It has not been fully implemented yet
|
This works very similarly to the normal resolver, but when it renders it will have a button that you can press. When you press it, it recalculates its value. This is very useful for dice and decks of cards. It has not been fully implemented yet
|
||||||
|
|
||||||
Here's the syntax: `??[Template](Text?)<<List::QueryValue>>`. Template is a basic string that has access to all values of the resolver using the `$#` variables. If template is left blank, the value that the resolver finally reaches will be rendered. In lieu of a template, you can also have it render the display of a field if it has one. Text is the text that will be rendered in the button. If not provided, the button will simply same "Resolve."
|
Here's the syntax: `??[Text](Template)<<List::QueryValue>>`. Template is a basic string that has access to all values of the resolver using the `$#` variables. If template is left blank, the value that the resolver finally reaches will be rendered. In lieu of a template, you can also have it render the display of a field if it has one. Text is the text that will be rendered in the button. If not provided, the button will simply same "Resolve."
|
||||||
|
|
||||||
[][]
|
[][]
|
||||||
[[
|
[[
|
||||||
To use the dice as an example again, here's how you would do that: `??[Rolling $0, you got: $1](Roll 2d6)<<_.path.to.dice,$0.roll>>`
|
To use the dice as an example again, here's how you would do that: `??[Roll 2d6](Rolling $0, you got: $1)<<_.path.to.dice,$0.roll>>`
|
||||||
|
|
||||||
??[Rolling $0, you got: $1](Roll 2d6)<<_.path.to.dice,$0.roll>>
|
??[Roll 2d6](Rolling $0, you got: $1)<<_.path.to.dice,$0.roll>>
|
||||||
]]
|
]]
|
||||||
|
|
||||||
[[
|
[[
|
||||||
For drawing a card and having it show the card's display: `??[]<<_.path.to.deck,$0.draw,$1.display>>`
|
For drawing a card and having it show the card's display: `??[]()<<_.path.to.deck,$0.draw,$1.display>>`
|
||||||
]]
|
]]
|
||||||
/[]
|
/[]
|
||||||
|
|
||||||
|
@@ -13,3 +13,8 @@
|
|||||||
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,15 +1,9 @@
|
|||||||
// 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,50 +12,164 @@ model GameSystem {
|
|||||||
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
|
|
||||||
types Json
|
SchemaRevision SchemaRevision[]
|
||||||
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
|
||||||
|
schemaVersion Int
|
||||||
|
schemaRevision SchemaRevision @relation(fields: [schemaVersion, schemaId], references: [version, schemaId])
|
||||||
|
PublicationRevision PublicationRevision[]
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
// 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])
|
author User @relation(fields: [authorId], references: [id])
|
||||||
authorId String
|
authorId String
|
||||||
|
|
||||||
name String
|
version String
|
||||||
data Json
|
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
|
||||||
|
Publication Publication @relation(fields: [publicationId], references: [id])
|
||||||
publicationId String
|
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])
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
import { TTCQueryParser } from "@/lib/ttcQuery/TTCQueryParser";
|
import { TTCQueryParser } from "@/lib/ttcQuery/TTCQueryParser";
|
||||||
import { atom } from "recoil";
|
import { atom } from "jotai";
|
||||||
|
// import { atom } from "recoil";
|
||||||
|
|
||||||
export const PublicationAtom = atom({
|
// export const PublicationAtom = atom({
|
||||||
key: "publication",
|
// key: "publication",
|
||||||
default: new TTCQueryParser({
|
// default: new TTCQueryParser({
|
||||||
path: { to: { dice: "2d6" } },
|
// path: { to: { dice: "2d6" } },
|
||||||
}),
|
// }),
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
export const PublicationAtom = atom(
|
||||||
|
new TTCQueryParser({ path: { to: { dice: "2d6" } } }),
|
||||||
|
);
|
||||||
|
@@ -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