Toast notification styling
This commit is contained in:
@ -6,6 +6,22 @@ export const theme = {
|
|||||||
syne: ['Syne', 'sans-serif'],
|
syne: ['Syne', 'sans-serif'],
|
||||||
inter: ['Inter', 'sans-serif'],
|
inter: ['Inter', 'sans-serif'],
|
||||||
},
|
},
|
||||||
|
animation: {
|
||||||
|
enter: 'enter 200ms ease-out',
|
||||||
|
leave: 'leave 200ms ease-in forwards',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
enter: {
|
||||||
|
'0%': { transform: 'scale(0.75) translateY(-110%)', opacity: 0 },
|
||||||
|
'25%': { opacity: 0 },
|
||||||
|
'100%': { transform: 'scale(1) translateX(0)', opacity: 1 },
|
||||||
|
},
|
||||||
|
leave: {
|
||||||
|
'0%': { transform: 'scale(1) translateX(0)', opacity: 1 },
|
||||||
|
'75%': { opacity: 0 },
|
||||||
|
'100%': { transform: 'scale(0.75) translateY(-110%)', opacity: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
// Pass props to your component by passing an `args` object to your story
|
||||||
|
//
|
||||||
|
// ```tsx
|
||||||
|
// export const Primary: Story = {
|
||||||
|
// args: {
|
||||||
|
// propName: propValue
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// See https://storybook.js.org/docs/react/writing-stories/args.
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ToastNotification from './ToastNotification'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ToastNotification> = {
|
||||||
|
component: ToastNotification,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof ToastNotification>
|
||||||
|
|
||||||
|
export const Primary: Story = {}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { render } from '@redwoodjs/testing/web'
|
||||||
|
|
||||||
|
import ToastNotification from './ToastNotification'
|
||||||
|
|
||||||
|
// Improve this test with help from the Redwood Testing Doc:
|
||||||
|
// https://redwoodjs.com/docs/testing#testing-components
|
||||||
|
|
||||||
|
describe('Toast', () => {
|
||||||
|
it('renders successfully', () => {
|
||||||
|
expect(() => {
|
||||||
|
render(<ToastNotification />)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
60
web/src/components/ToastNotification/ToastNotification.tsx
Normal file
60
web/src/components/ToastNotification/ToastNotification.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { mdiCloseCircle, mdiCheckCircle, mdiClose } from '@mdi/js'
|
||||||
|
import { Icon } from '@mdi/react'
|
||||||
|
|
||||||
|
import { Toast, toast, ToastType } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
t: Toast
|
||||||
|
type: ToastType
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToastNotification = ({ t, type, message }: Props) => {
|
||||||
|
let iconElement: React.JSX.Element
|
||||||
|
|
||||||
|
if (type === 'blank' || type === 'custom') iconElement = <></>
|
||||||
|
else if (type === 'loading')
|
||||||
|
iconElement = <span className="loading loading-spinner loading-md" />
|
||||||
|
else {
|
||||||
|
let icon: string
|
||||||
|
let color: string
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
icon = mdiCheckCircle
|
||||||
|
color = 'text-success'
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
icon = mdiCloseCircle
|
||||||
|
color = 'text-error'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
iconElement = <Icon path={icon} className={`w-8 ${color}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
t.visible ? 'animate-enter' : 'animate-leave'
|
||||||
|
} pointer-events-auto flex w-full max-w-56 items-center space-x-2 rounded-xl bg-base-200 p-2 shadow-xl ${
|
||||||
|
type === 'blank' ? 'first:space-x-0' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{iconElement}
|
||||||
|
<p className="w-full font-inter">{message}</p>
|
||||||
|
{type !== 'loading' ? (
|
||||||
|
<button
|
||||||
|
onClick={() => toast.dismiss(t.id)}
|
||||||
|
className="btn btn-ghost btn-xs"
|
||||||
|
>
|
||||||
|
<Icon path={mdiClose} className="w-4" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToastNotification
|
@ -2,8 +2,10 @@ import { mdiMenu } from '@mdi/js'
|
|||||||
import Icon from '@mdi/react'
|
import Icon from '@mdi/react'
|
||||||
|
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import { Toaster } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification/ToastNotification'
|
||||||
|
|
||||||
interface NavbarRoute {
|
interface NavbarRoute {
|
||||||
name: string
|
name: string
|
||||||
@ -32,6 +34,22 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Toaster
|
||||||
|
position="top-right"
|
||||||
|
containerClassName="-mr-2 mt-16"
|
||||||
|
containerStyle={{
|
||||||
|
zIndex: 50, // "z-50" does not work
|
||||||
|
}}
|
||||||
|
gutter={8}
|
||||||
|
>
|
||||||
|
{(t) => (
|
||||||
|
<ToastNotification
|
||||||
|
t={t}
|
||||||
|
type={t.type}
|
||||||
|
message={t.message.toString()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Toaster>
|
||||||
<div className="sticky top-0 z-50 p-2">
|
<div className="sticky top-0 z-50 p-2">
|
||||||
<div className="navbar rounded-xl bg-base-300 shadow-xl">
|
<div className="navbar rounded-xl bg-base-300 shadow-xl">
|
||||||
<div className="navbar-start space-x-2 lg:first:space-x-0">
|
<div className="navbar-start space-x-2 lg:first:space-x-0">
|
||||||
|
@ -1,19 +1,53 @@
|
|||||||
import { Link, routes } from '@redwoodjs/router'
|
|
||||||
import { Metadata } from '@redwoodjs/web'
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata title="Home" description="Home page" />
|
<Metadata title="Home" description="Home page" />
|
||||||
|
|
||||||
<h1>HomePage</h1>
|
<button className="btn" onClick={() => toast.loading('Loading...')}>
|
||||||
<p>
|
Infinite loading
|
||||||
Find me in <code>./web/src/pages/HomePage/HomePage.tsx</code>
|
</button>
|
||||||
</p>
|
<button
|
||||||
<p>
|
className="btn"
|
||||||
My default route is named <code>home</code>, link to me with `
|
onClick={() =>
|
||||||
<Link to={routes.home()}>Home</Link>`
|
toast.promise(
|
||||||
</p>
|
new Promise<number>((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const x = Math.random()
|
||||||
|
x >= 0.5 ? resolve(x) : reject(x)
|
||||||
|
}, 750)
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: 'Loading...',
|
||||||
|
success: (res) => `Success: ${res.toFixed(4)}`,
|
||||||
|
error: (err: number) => `Error: ${err.toFixed(4)}`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Loading -{'>'} Random result
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={() => toast('Blank')}>
|
||||||
|
Blank
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={() => toast.error('Error')}>
|
||||||
|
Error
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={() => toast.success('Success')}>
|
||||||
|
Success
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={() =>
|
||||||
|
toast(
|
||||||
|
"I'm baby sriracha poutine hammock pour-over direct trade, bruh coloring book ascot gatekeep put a bird on it YOLO biodiesel. Lyft wayfarers cloud bread, la croix lo-fi pork belly synth williamsburg before they sold out."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
A pretty big notification
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user