feat: implement basic snackbar

This commit is contained in:
MAZE 2023-10-30 21:55:08 +03:30
parent 7810d21225
commit 8090599f2b
6 changed files with 107 additions and 8 deletions

View File

@ -8,6 +8,7 @@ import { Container } from '@/components/container';
import { StoreConsumer } from '@/components/store-consumer';
import { Buttons } from '@/components/buttons';
import { Categories } from '@/components/categories';
import { SnackbarProvider } from '@/contexts/snackbar';
import { sounds } from '@/data/sounds';
@ -48,11 +49,13 @@ export function App() {
}, [favoriteSounds, categories]);
return (
<StoreConsumer>
<Container>
<Buttons />
<Categories categories={allCategories} />
</Container>
</StoreConsumer>
<SnackbarProvider>
<StoreConsumer>
<Container>
<Buttons />
<Categories categories={allCategories} />
</Container>
</StoreConsumer>
</SnackbarProvider>
);
}

View File

@ -3,6 +3,7 @@ import { BiPause, BiPlay } from 'react-icons/bi/index';
import { motion } from 'framer-motion';
import { useSoundStore } from '@/store';
import { useSnackbar } from '@/contexts/snackbar';
import { cn } from '@/helpers/styles';
import styles from './play.module.css';
@ -13,8 +14,10 @@ export function PlayButton() {
const toggle = useSoundStore(state => state.togglePlay);
const noSelected = useSoundStore(state => state.noSelected());
const showSnackbar = useSnackbar();
const handleClick = () => {
if (noSelected) return pause();
if (noSelected) return showSnackbar('Please first select a sound to play.');
toggle();
};
@ -26,7 +29,6 @@ export function PlayButton() {
return (
<motion.button
className={cn(styles.playButton, noSelected && styles.disabled)}
disabled={noSelected}
layout
onClick={handleClick}
>

View File

@ -0,0 +1 @@
export { Snackbar } from './snackbar';

View File

@ -0,0 +1,18 @@
.wrapper {
position: fixed;
z-index: 100;
bottom: 24px;
left: 0;
width: 100%;
& .snackbar {
width: max-content;
max-width: 90%;
padding: 12px 16px;
border: 1px solid var(--color-neutral-300);
border-radius: 4px;
margin: 0 auto;
background-color: var(--color-neutral-200);
font-size: var(--font-sm);
}
}

View File

@ -0,0 +1,27 @@
import { motion } from 'framer-motion';
import { mix, fade, slideY } from '@/lib/motion';
import styles from './snackbar.module.css';
interface SnackbarProps {
message: string;
}
export function Snackbar({ message }: SnackbarProps) {
const variants = mix(fade(), slideY(20, 0));
return (
<div className={styles.wrapper}>
<motion.div
animate="show"
className={styles.snackbar}
exit="hidden"
initial="hidden"
variants={variants}
>
{message}
</motion.div>
</div>
);
}

48
src/contexts/snackbar.tsx Normal file
View File

@ -0,0 +1,48 @@
import {
createContext,
useState,
useCallback,
useRef,
useContext,
} from 'react';
import { AnimatePresence } from 'framer-motion';
import { Snackbar } from '@/components/snackbar';
export const SnackbarContext = createContext<
(message: string, duration?: number) => void
>(() => {});
export const useSnackbar = () => useContext(SnackbarContext);
interface SnackbarProviderProps {
children: React.ReactNode;
}
export function SnackbarProvider({ children }: SnackbarProviderProps) {
const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false);
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const show = useCallback((message: string, duration = 5000) => {
setMessage(message);
setIsVisible(true);
if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
setMessage('');
setIsVisible(false);
}, duration);
}, []);
return (
<SnackbarContext.Provider value={show}>
{children}
<AnimatePresence>
{isVisible && <Snackbar message={message} />}
</AnimatePresence>
</SnackbarContext.Provider>
);
}