Skip to content

Commit

Permalink
refactor: rename interfaces, separate toast file, move remove functio…
Browse files Browse the repository at this point in the history
…nality to store
  • Loading branch information
Rue-pro committed Mar 15, 2024
1 parent 761d308 commit 5d6716a
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 137 deletions.
4 changes: 2 additions & 2 deletions src/shared/ui/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { ComponentChild } from 'preact'

import styles from './styles.module.scss'

export type IButtonColors = 'accent' | 'success' | 'error' | 'info'
export type TButtonColor = 'accent' | 'success' | 'error' | 'info'

export interface Props extends JSXInternal.HTMLAttributes<HTMLButtonElement> {
startIcon?: ComponentChild
endIcon?: ComponentChild
variant?: 'primary' | 'secondary'
color?: IButtonColors
color?: TButtonColor
}

export const Button = ({
Expand Down
63 changes: 63 additions & 0 deletions src/shared/ui/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import cn from 'classnames'
import { ComponentChildren } from 'preact'

import { browser } from '@shared/browser'

import { Button, TButtonColor } from '../Button'
import { ClearIcon } from '../icons/ClearIcon'
import styles from './styles.module.scss'

export type TToastType = 'error' | 'info' | 'success'
export interface IToast {
title: string
message?: ComponentChildren
details?: ComponentChildren
id: string
type: TToastType
}

interface Props {
toast: IToast
removeToast: () => void
openDetails: (details: ComponentChildren) => void
}

const mapToastTypeToButtonColor: Record<TToastType, TButtonColor> = {
error: 'error',
info: 'info',
success: 'success',
}

export const Toast = ({ toast, removeToast, openDetails }: Props) => {
const { title, message, details, type } = toast

const toastClass = cn(styles.toast, styles[`toast--variant-${type}`])

return (
<li className={toastClass} role="status">
<div className={styles.toast__content}>
<p className={cn(styles.toast__title, 'h2')}>{title}</p>

{typeof message === 'string' ? <p>{message}</p> : message}

{!!details && (
<Button
variant="secondary"
color={mapToastTypeToButtonColor[type]}
onClick={() => openDetails(details)}
>
Open details
</Button>
)}
</div>

<Button
variant="secondary"
color={mapToastTypeToButtonColor[type]}
endIcon={<ClearIcon />}
onClick={removeToast}
aria-label={browser.i18n.getMessage('closeToast')}
/>
</li>
)
}
47 changes: 47 additions & 0 deletions src/shared/ui/Toast/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ComponentChildren, createContext } from 'preact'
import { useContext, useState } from 'preact/hooks'

import { Modal } from '../Modal'
import { Toast } from './Toast'
import styles from './styles.module.scss'
import { IAddToastProps, useToastsStore } from './toastStore'

const ToastDispatchContext = createContext<{
addToast: (toast: IAddToastProps) => void
}>({
addToast: () => {
throw Error('Not implemented')
},
})

export const ToastProvider = ({
children,
}: {
children: ComponentChildren
}) => {
const { toasts, addToast, removeToast } = useToastsStore()
const [details, setDetails] = useState<ComponentChildren | null>(null)

return (
<ToastDispatchContext.Provider value={{ addToast }}>
<ul className={styles.toasts}>
{toasts.map((toast) => (
<Toast
key={toast.id}
toast={toast}
removeToast={() => removeToast(toast.id)}
openDetails={setDetails}
/>
))}
</ul>

{children}

<Modal open={!!details} onClose={() => setDetails(null)}>
{details}
</Modal>
</ToastDispatchContext.Provider>
)
}

export const useToast = () => useContext(ToastDispatchContext)
136 changes: 1 addition & 135 deletions src/shared/ui/Toast/index.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1 @@
import cn from 'classnames'
import { ComponentChildren, createContext } from 'preact'
import {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'preact/hooks'

import { browser } from '@shared/browser'

import { Button, IButtonColors } from '../Button'
import { Modal } from '../Modal'
import { ClearIcon } from '../icons/ClearIcon'
import styles from './styles.module.scss'

interface IToast {
title: string
message?: ComponentChildren
details?: ComponentChildren
id: string
type: 'error' | 'info' | 'success'
}

type IAddToastProps = Omit<IToast, 'id'>

const ToastDispatchContext = createContext<{
addToast: (toast: IAddToastProps) => void
}>({
addToast: () => {
throw Error('Not implemented')
},
})

export const ToastProvider = ({
children,
}: {
children: ComponentChildren
}) => {
const [details, setDetails] = useState<ComponentChildren | null>(null)
const [toasts, setToasts] = useState<IToast[]>([])

const addToast = (toast: IAddToastProps) => {
setToasts((prevToasts) => [
{
id: new Date().toISOString(),
...toast,
},
...prevToasts,
])
}

const removeToast = useCallback((id: string) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id))
}, [])

return (
<ToastDispatchContext.Provider value={{ addToast }}>
<ul className={styles.toasts}>
{toasts.map((toast) => (
<Toast
key={toast.id}
toast={toast}
removeToast={() => removeToast(toast.id)}
openDetails={setDetails}
/>
))}
</ul>

{children}

<Modal open={!!details} onClose={() => setDetails(null)}>
{details}
</Modal>
</ToastDispatchContext.Provider>
)
}

export const useToast = () => useContext(ToastDispatchContext)

interface ToastProps {
toast: IToast
removeToast: () => void
openDetails: (details: ComponentChildren) => void
}

const mapToastTypeToButtonColor: Record<IToast['type'], IButtonColors> = {
error: 'error',
info: 'info',
success: 'success',
}

export const Toast = ({ toast, removeToast, openDetails }: ToastProps) => {
const timerRef = useRef<NodeJS.Timeout>()
useEffect(() => {
timerRef.current = setTimeout(() => {
removeToast()
}, 3000)

return () => clearTimeout(timerRef.current)
}, [])

const { title, message, details, type } = toast

const toastClass = cn(styles.toast, styles[`toast--variant-${type}`])

return (
<li className={toastClass} role="status">
<div className={styles.toast__content}>
<p className={cn(styles.toast__title, 'h2')}>{title}</p>

{typeof message === 'string' ? <p>{message}</p> : message}

{!!details && (
<Button
variant="secondary"
color={mapToastTypeToButtonColor[type]}
onClick={() => openDetails(details)}
>
Open details
</Button>
)}
</div>

<Button
variant="secondary"
color={mapToastTypeToButtonColor[type]}
endIcon={<ClearIcon />}
onClick={removeToast}
aria-label={browser.i18n.getMessage('closeToast')}
/>
</li>
)
}
export { ToastProvider, useToast } from './ToastContext'
1 change: 1 addition & 0 deletions src/shared/ui/Toast/toastStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const useToastsStore = () => {

const removeToast = useCallback((id: string) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id))
clearTimeout(timersRef.current[id])
delete timersRef.current[id]
}, [])

Expand Down

0 comments on commit 5d6716a

Please sign in to comment.