toast messages
This commit is contained in:
parent
9838324b35
commit
b9b744e97f
@ -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" />;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
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>;
|
||||
}
|
@ -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
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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user