Skip to content

Commit

Permalink
wip: toasts api completed
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Dec 4, 2023
1 parent bf1c55a commit 954f48b
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 7 deletions.
8 changes: 4 additions & 4 deletions docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ Processo:
- [x][4h] update notices via socket.io
- [x][2h] tooltips on everything
- [ ][1h] zap hosting advertisement
- [ ][1d] toasts API
- [ ] generic toasts
- [ ] markdown toasts
- [ ] error toasts with discord link
- [x][1d] toasts API
- [x] generic toasts
- [x] markdown toasts
- [x] error toasts with discord link
- [x][2h] prompts API
- [ ][2h] server controls
- [ ][1h] server scheduled restarts (legacy style)
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions panel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.12.0",
"react-markdown": "^9.0.1",
"socket.io-client": "^4.7.2",
Expand Down
169 changes: 169 additions & 0 deletions panel/src/components/TxToaster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import MarkdownProse from "@/components/MarkdownProse";
import { cn, handleExternalLinkClick } from "@/lib/utils";
import { cva } from "class-variance-authority";
import { AlertCircleIcon, AlertOctagonIcon, CheckCircleIcon, ChevronRightCircle, InfoIcon, Loader2Icon, XIcon } from "lucide-react";
import toast, { Toast, Toaster } from "react-hot-toast";
import { useEffect, useState } from "react";

/**
* Types
*/
type TxToastType = 'default' | 'loading' | 'info' | 'success' | 'warning' | 'error';

type TxToastData = string | {
title?: string;
message: string;
md?: true
}

type TxToastOptions = {
id?: string;
duration?: number;
}

type CustomToastProps = {
t: Toast,
type: TxToastType,
data: TxToastData,
}


/**
* Components
*/
const toastBarVariants = cva(
`max-w-xl w-full sm:w-auto sm:min-w-[28rem] relative overflow-hidden z-40
p-3 pr-10 flex items-center justify-between space-x-4
rounded-xl border shadow-lg transition-all pointer-events-none
text-black/75 dark:text-white/90`,
{
variants: {
type: {
default: "dark:border-primary/25 bg-white dark:bg-secondary dark:text-secondary-foreground",
loading: "dark:border-primary/25 bg-white dark:bg-secondary dark:text-secondary-foreground",
info: "border-info/70 bg-info-hint",
success: "border-success/70 bg-success-hint",
warning: "border-warning/70 bg-warning-hint",
error: "border-destructive/70 bg-destructive-hint",
},
},
defaultVariants: {
type: "default",
},
}
);

const toastIconMap = {
default: <ChevronRightCircle className="stroke-muted-foreground animate-toastbar-icon" />,
loading: <Loader2Icon className="animate-spin" />,
info: <InfoIcon className="stroke-info animate-toastbar-icon" />,
success: <CheckCircleIcon className="stroke-success animate-toastbar-icon" />,
warning: <AlertCircleIcon className="stroke-warning animate-toastbar-icon" />,
error: <AlertOctagonIcon className="stroke-destructive animate-toastbar-icon" />,
} as const;

export const CustomToast = ({ t, type, data }: CustomToastProps) => {
const [elapsedTime, setElapsedTime] = useState(0);

useEffect(() => {
let timer: NodeJS.Timeout | null = null;
const cleanup = () => { timer && clearInterval(timer) };

if (type === "loading" && t.visible) {
timer = setInterval(() => {
setElapsedTime((prevElapsedTime) => prevElapsedTime + 1);
}, 1000);
} else if (timer) {
cleanup();
}

return cleanup;
}, [type, t.visible]);

return (
<div
className={cn(
toastBarVariants({ type }),
t.visible ? "animate-toastbar-enter" : "animate-toastbar-leave"
)}
>
<div className="flex-shrink-0 flex flex-col gap-2 items-center">
{type === "loading" && elapsedTime > 5 ? (
<div className="min-w-[2.65rem] text-center bg-muted/75 rounded-full">
<span className="text-xs text-secondary-foreground">{elapsedTime}s</span>
</div>
) : toastIconMap[type]}
</div>
<p className="flex-grow">
{typeof data === "string" ? (
<span className="block whitespace-pre-line">{data}</span>
) : data.md ? (
<>
<MarkdownProse md={`**${data.title}**`} isSmall isTitle />
<MarkdownProse md={data.message} isSmall />
</>
) : (
<>
<span className="font-semibold mb-1">{data.title}</span>
<span className="block whitespace-pre-line">{data.message}</span>
</>
)}
{type === 'error' && (
<small className="block text-xs tracking-wide text-muted-foreground">
For support, visit&nbsp;
<a
href="http://discord.gg/txAdmin"
target="_blank"
onClick={handleExternalLinkClick}
className="text-destructive-foregroundx font-semibold no-underline hover:underline m-0"
>
discord.gg/txAdmin
</a>.
</small>
)}
</p>

<button onClick={() => toast.dismiss(t.id)} className="absolute right-4 top-4 opacity-70">
<XIcon className="h-6 sm:w-6 md:h-5 md:w-5" />
<span className="sr-only">Close</span>
</button>
</div>
);
};


//Element to be added to MainShell
export default function TxToaster() {
return <Toaster
reverseOrder={true}
containerClassName="top-[calc(4.5rem+1px)]"
containerStyle={{
top: 'calc(4.5rem+1px)',
zIndex: 40,
}}
/>
}


/**
* Utilities
*/
//Returns a toast with the given type
const callToast = (type: TxToastType, data: TxToastData, options: TxToastOptions = {}) => {
options.duration ??= type === 'loading' ? Infinity : 5_000;
return toast.custom((t: Toast) => {
return <CustomToast t={t} type={type} data={data} />;
}, options);
}

//Exported functions
export const txToast = {
default: (data: TxToastData, options?: TxToastOptions) => callToast('default', data, options),
loading: (data: TxToastData, options?: TxToastOptions) => callToast('loading', data, options),
info: (data: TxToastData, options?: TxToastOptions) => callToast('info', data, options),
success: (data: TxToastData, options?: TxToastOptions) => callToast('success', data, options),
warning: (data: TxToastData, options?: TxToastOptions) => callToast('warning', data, options),
error: (data: TxToastData, options?: TxToastOptions) => callToast('error', data, options),
dismiss: toast.dismiss,
remove: toast.remove,
}
10 changes: 10 additions & 0 deletions panel/src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,19 @@
--ring: 224 10.2% 9.6%;

/* Semantic colors used for button variants and maybe more */
/* hint generated with the 50 variant of the color - https://uicolors.app/create */
--destructive: 0 84.5% 59.6%;
--destructive-foreground: 0 0% 100%;
--destructive-hint: 0 85.7% 97.3%;
--warning: 47.1 77.5% 56.5%;
--warning-foreground: 48.9 77.1% 6.9%;
--warning-hint: 53.3 75% 95.3%;
--success: 136.6 77.6% 56.3%;
--success-foreground: 137.5 80% 5.9%;
--success-hint: 136 88.2% 96.7%;
--info: 191.1 77.6% 56.3%;
--info-foreground: 195 66.7% 1.2%;
--info-hint: 187.1 89.5% 96.3%;

/* Other stuff */
--font-sans: "Inter Variable";
Expand Down Expand Up @@ -151,14 +156,19 @@
--ring: 224 6.7% 97.1%;

/* Semantic colors used for button variants and maybe more */
/* hint generated with the 950 variant of the color - https://uicolors.app/create */
--destructive: 0 91.3% 68.4%;
--destructive-foreground: 0 0% 100%;
--destructive-hint: 0 74.7% 15.5%;
--warning: 47.2 75.9% 62.5%;
--warning-foreground: 48.9 77.1% 6.9%;
--warning-hint: 21.9 72.2% 14.1%;
--success: 136.7 73.1% 60.6%;
--success-foreground: 136.2 72.2% 7.1%;
--success-hint: 139.5 84.3% 10%;
--info: 191.1 69.2% 61.8%;
--info-foreground: 197.1 63.6% 2.2%;
--info-hint: 200.5 57.9% 14.9%;

/* Other stuff */
--font-sans: "Inter Variable";
Expand Down
2 changes: 2 additions & 0 deletions panel/src/layout/MainShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getSocket } from '@/lib/utils';
import { useProcessPlayerlistEvents } from '@/hooks/playerlist';
import ConfirmDialog from '@/components/ConfirmDialog';
import PromptDialog from '@/components/PromptDialog';
import TxToaster from '../components/TxToaster';


export default function MainShell() {
Expand Down Expand Up @@ -94,6 +95,7 @@ export default function MainShell() {
<WarningBar />
<ConfirmDialog />
<PromptDialog />
<TxToaster />
{/* <BreakpointDebugger /> */}
</>;
}
2 changes: 2 additions & 0 deletions panel/src/pages/testing/TestingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import TmpTerminal from "./TmpTerminal";
import TmpWarningBarState from "./TmpWarningBarState";
import { useSetPageTitle } from "@/hooks/pages";
import TmpSocket from "./TmpSocket";
import TmpToasts from "./TmpToasts";


export default function TestingPage() {
const setPageTitle = useSetPageTitle();
setPageTitle();

return <div className="flex flex-col gap-4 w-full m-4">
{/* <TmpToasts /> */}
{/* <TmpSocket /> */}
{/* <TmpWarningBarState /> */}
{/* <TmpAuthState /> */}
Expand Down
73 changes: 73 additions & 0 deletions panel/src/pages/testing/TmpToasts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Button } from "@/components/ui/button";
import { txToast } from "@/components/TxToaster";


export default function TmpToasts() {
const openDefault = () => {
txToast.default('default');
txToast.info('info');
txToast.success('success');
txToast.warning('warning');
txToast.error('error');
}

const openSpecial = () => {
txToast.loading('long simulated loading', { duration: 15_000 });
txToast.default('longer duration', { duration: 10_000 });
txToast.default('Simple text\nLine Break');
txToast.default({
title: 'Object message',
message: 'Simple message **without** markdown\nbut auto line break.',
});
txToast.error({
title: 'Error: The bot requires the \`GUILD_MEMBERS\` intent:',
message: `- Go to the [Discord Dev Portal](https://discord.com/developers/applications)
- Navigate to \`Bot > Privileged Gateway Intents\`.
- Enable the \`GUILD_MEMBERS\` intent.
- Save on the dev portal.
- Go to the \`txAdmin > Settings > Discord Bot\` and press save.`,
md: true,
});
}

const openUpdatable = () => {
const toastId = txToast.loading('loading...');
setTimeout(() => {
// txToast.dismiss(toastId);
txToast.success('Bla bla bla!', { id: toastId });
}, 2500);
}

return <>
<div className="mx-auto mt-auto flex gap-4 group">
<Button
size={'lg'}
variant="outline"
onClick={() => { txToast.dismiss() }}
>
Wipe
</Button>
<Button
size={'lg'}
variant="default"
onClick={openDefault}
>
Default
</Button>
<Button
size={'lg'}
variant="default"
onClick={openSpecial}
>
Special
</Button>
<Button
size={'lg'}
variant="default"
onClick={openUpdatable}
>
Updatable
</Button>
</div>
</>;
}

0 comments on commit 954f48b

Please sign in to comment.