toast messages

This commit is contained in:
Emmaline Autumn 2024-08-15 04:11:57 -06:00
parent 9838324b35
commit b9b744e97f
10 changed files with 231 additions and 68 deletions

View File

@ -1,16 +1,21 @@
"use client"; "use client";
import { useToast } from "@/components/toast";
import { TTCMD } from "@/components/ttcmd"; import { TTCMD } from "@/components/ttcmd";
import { useEffect } from "react";
import { FC, use } from "react"; 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 ( const { createToast } = useToast();
<TTCMD useEffect(() => {
body={text} createToast({
parserId="home" fading: false,
title="home" msg: "TEST TOAST",
/> type: "error",
); });
}, [createToast]);
return <TTCMD body={text} parserId="home" title="home" />;
}; };

View File

@ -93,6 +93,12 @@
} }
} }
@layer utilities {
.fade-toast {
animation: fadeOut 300ms forwards;
}
}
@keyframes identifier { @keyframes identifier {
} }
@ -102,3 +108,12 @@
list-style: square; list-style: square;
} }
} }
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@ -11,6 +11,8 @@ import {
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";
const roboto = Roboto({ subsets: ["latin"], weight: "400" }); const roboto = Roboto({ subsets: ["latin"], weight: "400" });
@ -74,13 +76,14 @@ export default function RootLayout({
</ul> </ul>
</nav> </nav>
<RecoilRootClient> <RecoilRootClient>
<DevToolboxContextProvider <JotaiProvider>
isDev={process.env.NODE_ENV !== "production"} <DevToolboxContextProvider
> isDev={process.env.NODE_ENV !== "production"}
<main className="p-8 w-full overflow-visible"> >
{children} <main className="p-8 w-full overflow-visible">{children}</main>
</main> <Toaster />
</DevToolboxContextProvider> </DevToolboxContextProvider>
</JotaiProvider>
</RecoilRootClient> </RecoilRootClient>
<div id="root-portal"></div> <div id="root-portal"></div>
</body> </body>

View File

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

View File

@ -0,0 +1,7 @@
"use client";
import { Provider } from "jotai";
export function JotaiProvider(props: React.PropsWithChildren) {
return <Provider>{props.children}</Provider>;
}

View File

@ -12,6 +12,7 @@ import { TemplateEditor } from "./template-editor";
import { Icon } from "@/components/Icon"; import { Icon } from "@/components/Icon";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { FieldTypes } from "./fieldtypes"; import { FieldTypes } from "./fieldtypes";
import { prisma } from "@/prisma/prismaClient";
export const SchemaBuilder: FC = () => { export const SchemaBuilder: FC = () => {
const [schema, setSchema] = useRecoilState(SchemaEditAtom); const [schema, setSchema] = useRecoilState(SchemaEditAtom);
@ -19,10 +20,16 @@ export const SchemaBuilder: FC = () => {
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, gameSystemId } = useParams<{
schemaId: string;
gameSystemId?: string;
}>();
const { value: typeName, bind: bindTypeName, reset: resetTypeName } = const {
useInput(""); value: typeName,
bind: bindTypeName,
reset: resetTypeName,
} = useInput("");
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
@ -30,23 +37,36 @@ export const SchemaBuilder: FC = () => {
const [selectedType, setSelectedType] = useState(""); const [selectedType, setSelectedType] = useState("");
const saveType = useCallback((name: string, type: TypeType) => { const saveType = useCallback(
updateSchema((e) => ({ (name: string, type: TypeType) => {
types: { updateSchema((e) => ({
...e.types, types: {
[name]: type, ...e.types,
}, [name]: type,
})); },
resetTypeName(); }));
setPageNumber(0); resetTypeName();
setSelectedType(""); setPageNumber(0);
}, [resetTypeName, updateSchema]); setSelectedType("");
},
[resetTypeName, updateSchema]
);
const saveSchema = useCallback(async () => { const saveSchema = useCallback(async () => {
setLastSaved(schema); // "use server";
// const sid = await GameSystemsService.saveSchema(schema); // setLastSaved(schema);
// if (schemaId === 'new') navigate('/schema/'+sid) // await prisma.schema.upsert({
}, [schema]); // where: { id: schema.id },
// update: { ...schema },
// create: {
// name: schema.name,
// schema: schema.schema,
// types: schema.types,
// version: 0,
// gameSystemId,
// },
// });
}, [schema, gameSystemId]);
const selectTypeForEdit = useCallback((typeKey: string) => { const selectTypeForEdit = useCallback((typeKey: string) => {
setSelectedType(typeKey); setSelectedType(typeKey);
@ -71,22 +91,28 @@ export const SchemaBuilder: FC = () => {
resetSchemaFieldName(); resetSchemaFieldName();
}, [resetSchemaFieldName, schemaFieldName, updateSchema]); }, [resetSchemaFieldName, schemaFieldName, updateSchema]);
const updateSchemaField = useCallback((key: string, template: Template) => { const updateSchemaField = useCallback(
updateSchema((s) => ({ (key: string, template: Template) => {
schema: { updateSchema((s) => ({
...s.schema, schema: {
[key]: template, ...s.schema,
}, [key]: template,
})); },
}, [updateSchema]); }));
},
[updateSchema]
);
const deleteType = useCallback((key: string) => { const deleteType = useCallback(
updateSchema((s) => { (key: string) => {
const types = { ...s.types }; updateSchema((s) => {
delete types[key]; const types = { ...s.types };
return { types }; delete types[key];
}); return { types };
}, [updateSchema]); });
},
[updateSchema]
);
return ( return (
<div className="flex gap-4 p-8"> <div className="flex gap-4 p-8">
@ -107,16 +133,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.schema).map(
[schemaFieldKey, schemaField], ([schemaFieldKey, schemaField]) => (
) => ( <TemplateEditor
<TemplateEditor key={schemaFieldKey}
key={schemaFieldKey} templateKey={schemaFieldKey}
templateKey={schemaFieldKey} template={schemaField}
template={schemaField} update={updateSchemaField}
update={updateSchemaField} />
/> )
))} )}
</ul> </ul>
</div> </div>
<hr /> <hr />
@ -136,9 +162,11 @@ export const SchemaBuilder: FC = () => {
<TypeEditor <TypeEditor
name={selectedType || typeName} name={selectedType || typeName}
saveType={saveType} saveType={saveType}
type={selectedType type={
? schema.types[selectedType as keyof typeof schema.types] selectedType
: undefined} ? schema.types[selectedType as keyof typeof schema.types]
: undefined
}
/> />
</AnimatedPageContainer> </AnimatedPageContainer>
<ul className="mt-3 w-96"> <ul className="mt-3 w-96">

111
components/toast/index.tsx Normal file
View 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>
);
}

View File

@ -30,7 +30,6 @@ export function OnDemandResolver({
const stackIdxs = Array.from(new Set(template.match(/\$\d/g))); const stackIdxs = Array.from(new Set(template.match(/\$\d/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(); if (typeof thing === "function") thing = thing();
content = content.replaceAll(idx, thing as string); content = content.replaceAll(idx, thing as string);

View File

@ -239,7 +239,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 +253,6 @@ export const buildOnlyDefaultElements = () => {
}, },
(token) => { (token) => {
const { children } = token; const { children } = token;
debugger;
return ( return (
<> <>
<ol <ol

View File

@ -27,7 +27,6 @@ export class TTCQueryResolver {
const last = this.stack.at(-1); const last = this.stack.at(-1);
if (typeof last === "function" && !onDemand) return last(); if (typeof last === "function" && !onDemand) return last();
if (onDemand) debugger;
return last; return last;
} }