-
Notifications
You must be signed in to change notification settings - Fork 531
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
303 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
<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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</>; | ||
} |
Oops, something went wrong.