Toaster component created

This commit is contained in:
2025-08-09 16:39:58 -04:00
parent 4e3c984125
commit 46aa1e4dda
8 changed files with 181 additions and 78 deletions

View File

@@ -1,69 +1 @@
# React + TypeScript + Vite Under Construction
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -1,5 +1,2 @@
Inputs: Inputs:
slider, multi-value slider slider, multi-value slider
Toaster

View File

@@ -0,0 +1,32 @@
import type { ToasterProps } from "$/types/Toaster";
import { Transition } from "@headlessui/react";
import clsx from "clsx";
export default function Toaster({
toast,
className
}: ToasterProps){
return (
<Transition
show={toast.length > 1 || (toast.length === 1 && toast[0].hideTime > new Date())}
enter="transform transition duration-500"
enterFrom="-translate-y-[25vh]"
enterTo="translate-y-0 ease-out"
leave="transform transition duration-500"
leaveFrom="translate-y-0 ease-in"
leaveTo="-translate-y-[25vh]"
>
<div
className={clsx(
"fixed top-16 left-1/2 -translate-x-1/2 z-100",
"flex flex-col items-center justify-center",
"shadow-lg shadow-black/40",
className
)}
>
{ toast.map((toast) => (<div key={toast.id}>{toast.message}</div>)) }
</div>
</Transition>
);
}

View File

@@ -0,0 +1,95 @@
import { DangerMessageBlock, SuccessMessageBlock, WarningMessageBlock } from "$/component/message";
import Toaster from "$/component/toaster/Toaster";
import type { Toast, ToastProviderProps, ToastProviderState } from "$/types/Toaster";
import moment from "moment";
import { createContext, useCallback, useContext, useMemo, useState } from "react";
const toastInitialState: ToastProviderState = {
toast: [],
hideToast: () => {},
addToast: () => "",
addSuccess: () => "",
addWarning: () => "",
addDanger: () => ""
};
export const ToasterProviderContext = createContext<ToastProviderState>(toastInitialState);
export default function ToasterProvider({
className,
children
}: ToastProviderProps){
const [ toast, setToast ] = useState<Toast[]>([]);
const hideToast = useCallback((id: string) => {
setToast((prev) => {
if(prev.length === 1 && prev[0].id === id){
const current = prev[0].hideTime > moment(new Date()).subtract(600, "ms").toDate() ? [...prev] : [];
if(current.length > 0){
setTimeout(() => hideToast(id), 600);
}
return current;
}
else{
return prev.filter((toast) => toast.id !== id);
}
});
}, [ toast ]);
const addToast = useCallback((message: React.ReactNode, duration?: number) => {
if(!duration){
duration = 5000;
}
const id = crypto.randomUUID();
setToast((prev) => [ ...prev, { id, message, duration, hideTime: moment(new Date()).add(duration, "ms").toDate() } ]);
setTimeout(() => hideToast(id), duration);
return id;
}, [ toast ]);
const addSuccess = useCallback((message: React.ReactNode, duration?: number) => {
return addToast(<SuccessMessageBlock>{message}</SuccessMessageBlock>, duration);
}, [ addToast ]);
const addWarning = useCallback((message: React.ReactNode, duration?: number) => {
return addToast(<WarningMessageBlock>{message}</WarningMessageBlock>, duration);
}, [ addToast ]);
const addDanger = useCallback((message: React.ReactNode, duration?: number) => {
return addToast(<DangerMessageBlock>{message}</DangerMessageBlock>, duration);
}, [ addToast ]);
const value: ToastProviderState = useMemo(() => ({
toast,
hideToast,
addToast,
addSuccess,
addWarning,
addDanger
}), [ toast, hideToast, addToast, addSuccess, addWarning, addDanger ]);
return (
<ToasterProviderContext.Provider value={value}>
<Toaster
toast={toast}
className={className}
/>
{children}
</ToasterProviderContext.Provider>
);
}
export function useToaster(){
const context = useContext(ToasterProviderContext);
if(!context){
throw new Error("useToaster must be used within a ToasterProvider");
}
return context;
}

24
lib/types/Toaster.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
export interface Toast {
id: string;
message: React.ReactNode;
hideTime: Date;
}
export interface ToastProviderState {
toast: Toast[];
hideToast: (id: string) => void;
addToast: (message: ReactNode, duration?: number) => string;
addSuccess: (message: ReactNode, duration?: number) => string;
addWarning: (message: ReactNode, duration?: number) => string;
addDanger: (message: ReactNode, duration?: number) => string;
}
export interface ToastProviderProps {
className?: string;
children: React.ReactNode;
}
export interface ToasterProps {
toast: Toast[];
className?: string;
}

View File

@@ -1,4 +1,5 @@
import ThemeProvider from "$/providers/theme/ThemeProvider"; import ThemeProvider from "$/providers/theme/ThemeProvider";
import ToasterProvider from "$/providers/toaster/ToasterProvider";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
@@ -17,7 +18,9 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<ThemeProvider defaultTheme="dark"> <ThemeProvider defaultTheme="dark">
<RouterProvider router={router}/> <ToasterProvider className="bg-zinc-700 text-white px-4 py-2 min-w-32 max-w-128 rounded-lg gap-y-4">
<RouterProvider router={router}/>
</ToasterProvider>
</ThemeProvider> </ThemeProvider>
</StrictMode> </StrictMode>
); );

View File

@@ -1,5 +1,7 @@
import { DangerButton, PrimaryButton, SuccessButton, WarningButton } from "$/component/button"; import { DangerButton, PrimaryButton, SuccessButton, WarningButton } from "$/component/button";
import { PrimaryMessageBlock } from "$/component/message";
import { Modal } from "$/component/modal"; import { Modal } from "$/component/modal";
import { useToaster } from "$/providers/toaster/ToasterProvider";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
@@ -12,6 +14,9 @@ export const Route = createFileRoute("/modal/")({
function ModalPage(){ function ModalPage(){
const [ displayCenteredModal, setDisplayCenteredModal ] = useState(false); const [ displayCenteredModal, setDisplayCenteredModal ] = useState(false);
const [ displayTopModal, setDisplayTopModal ] = useState(false); const [ displayTopModal, setDisplayTopModal ] = useState(false);
const [ toasterNumber, setToasterNumber ] = useState(1);
const { addToast, addSuccess, addWarning, addDanger } = useToaster();
return ( return (
@@ -36,22 +41,22 @@ function ModalPage(){
<div <div
className="flex flex-row items-center justify-center gap-x-4"> className="flex flex-row items-center justify-center gap-x-4">
<PrimaryButton <PrimaryButton
onClick={() => {}} onClick={() => { addToast(<PrimaryMessageBlock>Toaster {toasterNumber}</PrimaryMessageBlock>); setToasterNumber(toasterNumber + 1); }}
> >
Timed Modal Timed Modal
</PrimaryButton> </PrimaryButton>
<SuccessButton <SuccessButton
onClick={() => {}} onClick={() => { addSuccess(`Success Toaster ${toasterNumber}`); setToasterNumber(toasterNumber + 1); }}
> >
Timed Modal Timed Modal
</SuccessButton> </SuccessButton>
<WarningButton <WarningButton
onClick={() => {}} onClick={() => { addWarning(`Warning Toaster ${toasterNumber}`); setToasterNumber(toasterNumber + 1); }}
> >
Timed Modal Timed Modal
</WarningButton> </WarningButton>
<DangerButton <DangerButton
onClick={() => {}} onClick={() => { addDanger(`Danger Toaster ${toasterNumber}`); setToasterNumber(toasterNumber + 1); }}
> >
Timed Modal Timed Modal
</DangerButton> </DangerButton>

View File

@@ -14,6 +14,8 @@ import WarningCheckbox from "$/component/input/checkbox/WarningCheckbox";
import DateInput from "$/component/input/date/DateInput"; import DateInput from "$/component/input/date/DateInput";
import DateTimeInput from "$/component/input/date/DateTimeInput"; import DateTimeInput from "$/component/input/date/DateTimeInput";
import TimeInput from "$/component/input/date/TimeInput"; import TimeInput from "$/component/input/date/TimeInput";
import MultiNumberSlider from "$/component/input/number/MultiNumberSlider";
import NumberSlider from "$/component/input/number/NumberSlider";
import DangerRadioButton from "$/component/input/radio/DangerRadioButton"; import DangerRadioButton from "$/component/input/radio/DangerRadioButton";
import DarkRadioButton from "$/component/input/radio/DarkRadioButton"; import DarkRadioButton from "$/component/input/radio/DarkRadioButton";
import InfoRadioButton from "$/component/input/radio/InfoRadioButton"; import InfoRadioButton from "$/component/input/radio/InfoRadioButton";
@@ -43,7 +45,7 @@ import { useState } from "react";
import { BsCheck, BsX } from "react-icons/bs"; import { BsCheck, BsX } from "react-icons/bs";
export function SwitchContent(): React.ReactNode{ export function SwitchContent(){
const sizes: MattrixwvSwitchSize[] = [ "xs", "sm", "md", "lg", "xl" ]; const sizes: MattrixwvSwitchSize[] = [ "xs", "sm", "md", "lg", "xl" ];
@@ -761,3 +763,16 @@ export function DateContent(){
</div> </div>
); );
} }
export function SliderContent(){
return (
<div
className="flex flex-col items-center justify-center my-8 gap-y-8"
>
<NumberSlider
/>
<MultiNumberSlider
/>
</div>
);
}