diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx
index 132c7b6..0ba5b6d 100644
--- a/src/components/app/app.tsx
+++ b/src/components/app/app.tsx
@@ -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 (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/buttons/play/play.tsx b/src/components/buttons/play/play.tsx
index 10d4ef2..b6a85c8 100644
--- a/src/components/buttons/play/play.tsx
+++ b/src/components/buttons/play/play.tsx
@@ -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 (
diff --git a/src/components/snackbar/index.ts b/src/components/snackbar/index.ts
new file mode 100644
index 0000000..0d26e66
--- /dev/null
+++ b/src/components/snackbar/index.ts
@@ -0,0 +1 @@
+export { Snackbar } from './snackbar';
diff --git a/src/components/snackbar/snackbar.module.css b/src/components/snackbar/snackbar.module.css
new file mode 100644
index 0000000..5df37ab
--- /dev/null
+++ b/src/components/snackbar/snackbar.module.css
@@ -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);
+ }
+}
diff --git a/src/components/snackbar/snackbar.tsx b/src/components/snackbar/snackbar.tsx
new file mode 100644
index 0000000..819623e
--- /dev/null
+++ b/src/components/snackbar/snackbar.tsx
@@ -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 (
+
+
+ {message}
+
+
+ );
+}
diff --git a/src/contexts/snackbar.tsx b/src/contexts/snackbar.tsx
new file mode 100644
index 0000000..886c8c9
--- /dev/null
+++ b/src/contexts/snackbar.tsx
@@ -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 | 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 (
+
+ {children}
+
+
+ {isVisible && }
+
+
+ );
+}