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";
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<string> }> = ({ body }) => {
const text = use(body);
return (
<TTCMD
body={text}
parserId="home"
title="home"
/>
);
const { createToast } = useToast();
useEffect(() => {
createToast({
fading: false,
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 {
}
@ -102,3 +108,12 @@
list-style: square;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@ -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({
</ul>
</nav>
<RecoilRootClient>
<JotaiProvider>
<DevToolboxContextProvider
isDev={process.env.NODE_ENV !== "production"}
>
<main className="p-8 w-full overflow-visible">
{children}
</main>
<main className="p-8 w-full overflow-visible">{children}</main>
<Toaster />
</DevToolboxContextProvider>
</JotaiProvider>
</RecoilRootClient>
<div id="root-portal"></div>
</body>

View File

@ -11,8 +11,7 @@ export default function Home() {
<h2 className="strapline">Tabletop Commander</h2>
<h1>How does it work?</h1>
</section>
{
/* <section className="w-full my-6">
{/* <section className="w-full my-6">
<div className="card">
<p>
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
</cite>
</section> */
}
</section> */}
<Suspense fallback={<MDSkeletonLoader />}>
<HomeClient body={body} />
</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 { 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>(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,7 +37,8 @@ export const SchemaBuilder: FC = () => {
const [selectedType, setSelectedType] = useState("");
const saveType = useCallback((name: string, type: TypeType) => {
const saveType = useCallback(
(name: string, type: TypeType) => {
updateSchema((e) => ({
types: {
...e.types,
@ -40,13 +48,25 @@ export const SchemaBuilder: FC = () => {
resetTypeName();
setPageNumber(0);
setSelectedType("");
}, [resetTypeName, updateSchema]);
},
[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) => {
const updateSchemaField = useCallback(
(key: string, template: Template) => {
updateSchema((s) => ({
schema: {
...s.schema,
[key]: template,
},
}));
}, [updateSchema]);
},
[updateSchema]
);
const deleteType = useCallback((key: string) => {
const deleteType = useCallback(
(key: string) => {
updateSchema((s) => {
const types = { ...s.types };
delete types[key];
return { types };
});
}, [updateSchema]);
},
[updateSchema]
);
return (
<div className="flex gap-4 p-8">
@ -107,16 +133,16 @@ export const SchemaBuilder: FC = () => {
</button>
</div>
<ul className="rounded-lg overflow-hidden">
{Object.entries(schema.schema).map((
[schemaFieldKey, schemaField],
) => (
{Object.entries(schema.schema).map(
([schemaFieldKey, schemaField]) => (
<TemplateEditor
key={schemaFieldKey}
templateKey={schemaFieldKey}
template={schemaField}
update={updateSchemaField}
/>
))}
)
)}
</ul>
</div>
<hr />
@ -136,9 +162,11 @@ export const SchemaBuilder: FC = () => {
<TypeEditor
name={selectedType || typeName}
saveType={saveType}
type={selectedType
type={
selectedType
? schema.types[selectedType as keyof typeof schema.types]
: undefined}
: undefined
}
/>
</AnimatedPageContainer>
<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)));
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);

View File

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

View File

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