diff --git a/app/client.tsx b/app/client.tsx index bd52c7c..f6cc54e 100644 --- a/app/client.tsx +++ b/app/client.tsx @@ -1,16 +1,21 @@ "use client"; +import { useToast } from "@/components/toast"; import { TTCMD } from "@/components/ttcmd"; +import { useEffect } from "react"; import { FC, use } from "react"; export const HomeClient: FC<{ body: Promise }> = ({ body }) => { const text = use(body); - return ( - - ); + const { createToast } = useToast(); + useEffect(() => { + createToast({ + fading: false, + msg: "TEST TOAST", + type: "error", + }); + }, [createToast]); + + return ; }; diff --git a/app/globals.css b/app/globals.css index b362e3f..feb1e32 100644 --- a/app/globals.css +++ b/app/globals.css @@ -93,6 +93,12 @@ } } +@layer utilities { + .fade-toast { + animation: fadeOut 300ms forwards; + } +} + @keyframes identifier { } @@ -102,3 +108,12 @@ list-style: square; } } + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index d427cc3..2dbce22 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,6 +11,8 @@ import { import Link from "next/link"; import { DevToolboxContextProvider } from "@/components/devtools/context"; import { RecoilRootClient } from "@/components/recoilRoot"; +import { JotaiProvider } from "@/components/jotaiProvider"; +import { Toaster } from "@/components/toast"; const roboto = Roboto({ subsets: ["latin"], weight: "400" }); @@ -74,13 +76,14 @@ export default function RootLayout({ - -
- {children} -
-
+ + +
{children}
+ +
+
diff --git a/app/page.tsx b/app/page.tsx index 4540704..c3a09ac 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,8 +11,7 @@ export default function Home() {

Tabletop Commander

How does it work?

- { - /*
+ {/*

Tabletop Commander (TC) is a rules-and-tools app for tabletop games @@ -131,8 +130,7 @@ export default function Home() { will be done. If this makes it to production, tell Emma she forgot to turn the home page into magic -

*/ - } +
*/} }> diff --git a/components/jotaiProvider.tsx b/components/jotaiProvider.tsx new file mode 100644 index 0000000..2437da2 --- /dev/null +++ b/components/jotaiProvider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { Provider } from "jotai"; + +export function JotaiProvider(props: React.PropsWithChildren) { + return {props.children}; +} diff --git a/components/schema/index.tsx b/components/schema/index.tsx index beeaaac..c0f89a4 100644 --- a/components/schema/index.tsx +++ b/components/schema/index.tsx @@ -12,6 +12,7 @@ import { TemplateEditor } from "./template-editor"; import { Icon } from "@/components/Icon"; import { useParams } from "next/navigation"; import { FieldTypes } from "./fieldtypes"; +import { prisma } from "@/prisma/prismaClient"; export const SchemaBuilder: FC = () => { const [schema, setSchema] = useRecoilState(SchemaEditAtom); @@ -19,10 +20,16 @@ export const SchemaBuilder: FC = () => { const { update: updateSchema, bindProperty: bindSchemaProperty } = useObjectStateWrapper(schema, setSchema); - const { schemaId } = useParams<{ schemaId: string }>(); + const { schemaId, gameSystemId } = useParams<{ + schemaId: string; + gameSystemId?: string; + }>(); - const { value: typeName, bind: bindTypeName, reset: resetTypeName } = - useInput(""); + const { + value: typeName, + bind: bindTypeName, + reset: resetTypeName, + } = useInput(""); const [pageNumber, setPageNumber] = useState(0); @@ -30,23 +37,36 @@ export const SchemaBuilder: FC = () => { const [selectedType, setSelectedType] = useState(""); - const saveType = useCallback((name: string, type: TypeType) => { - updateSchema((e) => ({ - types: { - ...e.types, - [name]: type, - }, - })); - resetTypeName(); - setPageNumber(0); - setSelectedType(""); - }, [resetTypeName, updateSchema]); + const saveType = useCallback( + (name: string, type: TypeType) => { + updateSchema((e) => ({ + types: { + ...e.types, + [name]: type, + }, + })); + resetTypeName(); + setPageNumber(0); + setSelectedType(""); + }, + [resetTypeName, updateSchema] + ); const saveSchema = useCallback(async () => { - setLastSaved(schema); - // const sid = await GameSystemsService.saveSchema(schema); - // if (schemaId === 'new') navigate('/schema/'+sid) - }, [schema]); + // "use server"; + // setLastSaved(schema); + // await prisma.schema.upsert({ + // where: { id: schema.id }, + // update: { ...schema }, + // create: { + // name: schema.name, + // schema: schema.schema, + // types: schema.types, + // version: 0, + // gameSystemId, + // }, + // }); + }, [schema, gameSystemId]); const selectTypeForEdit = useCallback((typeKey: string) => { setSelectedType(typeKey); @@ -71,22 +91,28 @@ export const SchemaBuilder: FC = () => { resetSchemaFieldName(); }, [resetSchemaFieldName, schemaFieldName, updateSchema]); - const updateSchemaField = useCallback((key: string, template: Template) => { - updateSchema((s) => ({ - schema: { - ...s.schema, - [key]: template, - }, - })); - }, [updateSchema]); + const updateSchemaField = useCallback( + (key: string, template: Template) => { + updateSchema((s) => ({ + schema: { + ...s.schema, + [key]: template, + }, + })); + }, + [updateSchema] + ); - const deleteType = useCallback((key: string) => { - updateSchema((s) => { - const types = { ...s.types }; - delete types[key]; - return { types }; - }); - }, [updateSchema]); + const deleteType = useCallback( + (key: string) => { + updateSchema((s) => { + const types = { ...s.types }; + delete types[key]; + return { types }; + }); + }, + [updateSchema] + ); return (
@@ -107,16 +133,16 @@ export const SchemaBuilder: FC = () => {
    - {Object.entries(schema.schema).map(( - [schemaFieldKey, schemaField], - ) => ( - - ))} + {Object.entries(schema.schema).map( + ([schemaFieldKey, schemaField]) => ( + + ) + )}

@@ -136,9 +162,11 @@ export const SchemaBuilder: FC = () => {
    diff --git a/components/toast/index.tsx b/components/toast/index.tsx new file mode 100644 index 0000000..bedb40d --- /dev/null +++ b/components/toast/index.tsx @@ -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([]); + +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 ( + +
    + {toasts.map((t) => ( + + ))} +
    +
    + ); +} + +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 ( +
    + {toast.msg} + {!toast.fading && ( + + )} +
    + ); +} diff --git a/lib/tcmd/Resolver.tsx b/lib/tcmd/Resolver.tsx index f12b1e5..013aae4 100644 --- a/lib/tcmd/Resolver.tsx +++ b/lib/tcmd/Resolver.tsx @@ -30,7 +30,6 @@ export function OnDemandResolver({ const stackIdxs = Array.from(new Set(template.match(/\$\d/g))); for (const idx of stackIdxs) { let thing = res.current.getFromStack(idx); - debugger; if (Array.isArray(thing)) thing = thing.at(0); if (typeof thing === "function") thing = thing(); content = content.replaceAll(idx, thing as string); diff --git a/lib/tcmd/TokenIdentifiers.tsx b/lib/tcmd/TokenIdentifiers.tsx index 7d2e62b..2d7b3e2 100644 --- a/lib/tcmd/TokenIdentifiers.tsx +++ b/lib/tcmd/TokenIdentifiers.tsx @@ -239,7 +239,6 @@ export const buildOnlyDefaultElements = () => { "ordered-list", /(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g, (s, rx) => { - // debugger; return { content: s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse ordered list", @@ -254,7 +253,6 @@ export const buildOnlyDefaultElements = () => { }, (token) => { const { children } = token; - debugger; return ( <>