diff --git a/README.md b/README.md index 8b1bacc..7f13206 100644 --- a/README.md +++ b/README.md @@ -1,69 +1 @@ -# React + TypeScript + Vite - -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... - }, - }, -]) -``` +Under Construction diff --git a/TODO.txt b/TODO.txt index eb09e5f..c5c2084 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,2 @@ Inputs: slider, multi-value slider - - -Toaster diff --git a/lib/component/toaster/Toaster.tsx b/lib/component/toaster/Toaster.tsx new file mode 100644 index 0000000..f821db4 --- /dev/null +++ b/lib/component/toaster/Toaster.tsx @@ -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 ( + 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]" + > +
+ { toast.map((toast) => (
{toast.message}
)) } +
+
+ ); +} diff --git a/lib/providers/toaster/ToasterProvider.tsx b/lib/providers/toaster/ToasterProvider.tsx new file mode 100644 index 0000000..f47bccc --- /dev/null +++ b/lib/providers/toaster/ToasterProvider.tsx @@ -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(toastInitialState); + + +export default function ToasterProvider({ + className, + children +}: ToastProviderProps){ + const [ toast, setToast ] = useState([]); + + + 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({message}, duration); + }, [ addToast ]); + + const addWarning = useCallback((message: React.ReactNode, duration?: number) => { + return addToast({message}, duration); + }, [ addToast ]); + + const addDanger = useCallback((message: React.ReactNode, duration?: number) => { + return addToast({message}, duration); + }, [ addToast ]); + + const value: ToastProviderState = useMemo(() => ({ + toast, + hideToast, + addToast, + addSuccess, + addWarning, + addDanger + }), [ toast, hideToast, addToast, addSuccess, addWarning, addDanger ]); + + return ( + + + {children} + + ); +} + + +export function useToaster(){ + const context = useContext(ToasterProviderContext); + + if(!context){ + throw new Error("useToaster must be used within a ToasterProvider"); + } + + return context; +} diff --git a/lib/types/Toaster.d.ts b/lib/types/Toaster.d.ts new file mode 100644 index 0000000..8215da2 --- /dev/null +++ b/lib/types/Toaster.d.ts @@ -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; +} diff --git a/src/main.tsx b/src/main.tsx index ad81c80..cd9592b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,5 @@ import ThemeProvider from "$/providers/theme/ThemeProvider"; +import ToasterProvider from "$/providers/toaster/ToasterProvider"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; @@ -17,7 +18,9 @@ declare module "@tanstack/react-router" { createRoot(document.getElementById('root')!).render( - + + + ); diff --git a/src/routes/modal/index.tsx b/src/routes/modal/index.tsx index 8ca8702..8225cde 100644 --- a/src/routes/modal/index.tsx +++ b/src/routes/modal/index.tsx @@ -1,5 +1,7 @@ import { DangerButton, PrimaryButton, SuccessButton, WarningButton } from "$/component/button"; +import { PrimaryMessageBlock } from "$/component/message"; import { Modal } from "$/component/modal"; +import { useToaster } from "$/providers/toaster/ToasterProvider"; import { createFileRoute } from "@tanstack/react-router"; import { useState } from "react"; @@ -12,6 +14,9 @@ export const Route = createFileRoute("/modal/")({ function ModalPage(){ const [ displayCenteredModal, setDisplayCenteredModal ] = useState(false); const [ displayTopModal, setDisplayTopModal ] = useState(false); + const [ toasterNumber, setToasterNumber ] = useState(1); + + const { addToast, addSuccess, addWarning, addDanger } = useToaster(); return ( @@ -36,22 +41,22 @@ function ModalPage(){
{}} + onClick={() => { addToast(Toaster {toasterNumber}); setToasterNumber(toasterNumber + 1); }} > Timed Modal {}} + onClick={() => { addSuccess(`Success Toaster ${toasterNumber}`); setToasterNumber(toasterNumber + 1); }} > Timed Modal {}} + onClick={() => { addWarning(`Warning Toaster ${toasterNumber}`); setToasterNumber(toasterNumber + 1); }} > Timed Modal {}} + onClick={() => { addDanger(`Danger Toaster ${toasterNumber}`); setToasterNumber(toasterNumber + 1); }} > Timed Modal diff --git a/src/util/InputUtils.tsx b/src/util/InputUtils.tsx index c5f27e2..b2d19ff 100644 --- a/src/util/InputUtils.tsx +++ b/src/util/InputUtils.tsx @@ -14,6 +14,8 @@ import WarningCheckbox from "$/component/input/checkbox/WarningCheckbox"; import DateInput from "$/component/input/date/DateInput"; import DateTimeInput from "$/component/input/date/DateTimeInput"; 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 DarkRadioButton from "$/component/input/radio/DarkRadioButton"; import InfoRadioButton from "$/component/input/radio/InfoRadioButton"; @@ -43,7 +45,7 @@ import { useState } from "react"; import { BsCheck, BsX } from "react-icons/bs"; -export function SwitchContent(): React.ReactNode{ +export function SwitchContent(){ const sizes: MattrixwvSwitchSize[] = [ "xs", "sm", "md", "lg", "xl" ]; @@ -761,3 +763,16 @@ export function DateContent(){
); } + +export function SliderContent(){ + return ( +
+ + +
+ ); +}