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