feat: add basic animations with Framer Motion

This commit is contained in:
MAZE
2023-10-12 19:40:02 +03:30
parent f2efe3c490
commit fa7b90eeec
6 changed files with 104 additions and 56 deletions

View File

@@ -12,6 +12,7 @@ import {
useRole,
useInteractions,
} from '@floating-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useSoundStore } from '@/store';
import { usePlay } from '@/contexts/play';
@@ -66,9 +67,10 @@ export function Buttons() {
return (
<div className={styles.buttons}>
<button
<motion.button
className={cn(styles.playButton, noSelected && styles.disabled)}
disabled={noSelected}
layout
onClick={handleClick}
>
{isPlaying ? (
@@ -86,27 +88,37 @@ export function Buttons() {
Play
</>
)}
</button>
</motion.button>
<button
disabled={noSelected && !hasHistory}
ref={refs.setReference}
{...getReferenceProps}
aria-label={
hasHistory ? 'Restore Unselected Sounds' : 'Unselect All Sounds'
}
className={cn(
styles.smallButton,
hasHistory ? styles.restore : styles.delete,
noSelected && !hasHistory && styles.disabled,
<AnimatePresence>
{(!noSelected || hasHistory) && (
<motion.div
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
initial={{ opacity: 0, x: 20 }}
>
<button
disabled={noSelected && !hasHistory}
ref={refs.setReference}
{...getReferenceProps}
aria-label={
hasHistory ? 'Restore Unselected Sounds' : 'Unselect All Sounds'
}
className={cn(
styles.smallButton,
hasHistory ? styles.restore : styles.delete,
noSelected && !hasHistory && styles.disabled,
)}
onClick={() => {
if (hasHistory) restoreHistory();
else if (!noSelected) unselectAll(true);
}}
>
{hasHistory ? <BiUndo /> : <BiTrash />}
</button>
</motion.div>
)}
onClick={() => {
if (hasHistory) restoreHistory();
else if (!noSelected) unselectAll(true);
}}
>
{hasHistory ? <BiUndo /> : <BiTrash />}
</button>
</AnimatePresence>
{isTooltipOpen && (
<div

View File

@@ -1,6 +0,0 @@
.help {
margin-top: 20px;
color: var(--color-foreground-subtle);
font-size: var(--font-sm);
text-align: center;
}

View File

@@ -1,9 +1,9 @@
import { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { BiSolidHeart } from 'react-icons/bi/index';
import { AnimatePresence } from 'framer-motion';
import { useFavoriteStore } from '@/store/favorite';
import { useSoundStore } from '@/store/sound';
import { Container } from '@/components/container';
import { StoreConsumer } from '../store-consumer';
@@ -13,13 +13,10 @@ import { PlayProvider } from '@/contexts/play';
import { sounds } from '@/data/sounds';
import styles from './categories.module.css';
export function Categories() {
const categories = useMemo(() => sounds.categories, []);
const favorites = useFavoriteStore(useShallow(state => state.favorites));
const noSelected = useSoundStore(state => state.noSelected());
const favoriteSounds = useMemo(() => {
const favoriteSounds = categories
@@ -40,30 +37,29 @@ export function Categories() {
<PlayProvider>
<Container>
<Buttons />
{noSelected && <p className={styles.help}>Select a sound to play!</p>}
<div>
{!!favoriteSounds.length && (
<Category
functional={false}
icon={<BiSolidHeart />}
id="favorites"
title="Favorites"
sounds={
favoriteSounds as Array<{
src: string;
label: string;
id: string;
icon: React.ReactNode;
}>
}
/>
)}
<AnimatePresence initial={false}>
{!!favoriteSounds.length && (
<Category
functional={false}
icon={<BiSolidHeart />}
id="favorites"
title="Favorites"
sounds={
favoriteSounds as Array<{
src: string;
label: string;
id: string;
icon: React.ReactNode;
}>
}
/>
)}
{categories.map(category => (
<Category {...category} key={category.id} />
))}
{categories.map(category => (
<Category {...category} key={category.id} />
))}
</AnimatePresence>
</div>
</Container>
</PlayProvider>

View File

@@ -1,3 +1,5 @@
import { motion } from 'framer-motion';
import { Sounds } from '@/components/sounds';
import styles from './category.module.css';
@@ -23,7 +25,12 @@ export function Category({
title,
}: CategoryProps) {
return (
<div className={styles.category}>
<motion.div
animate={{ opacity: 1 }}
className={styles.category}
initial={{ opacity: 0 }}
layout
>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>{icon}</div>
@@ -32,6 +39,6 @@ export function Category({
<h2 className={styles.title}>{title}</h2>
<Sounds functional={functional} id={id} sounds={sounds} />
</div>
</motion.div>
);
}