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

42
package-lock.json generated
View file

@ -13,6 +13,7 @@
"@types/react": "^18.2.25",
"@types/react-dom": "^18.2.10",
"astro": "^3.2.3",
"framer-motion": "10.16.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "4.11.0",
@ -1356,6 +1357,21 @@
"postcss-selector-parser": "^6.0.13"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"node_modules/@esbuild/android-arm": {
"version": "0.19.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.4.tgz",
@ -7181,6 +7197,29 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "10.16.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.4.tgz",
"integrity": "sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA==",
"dependencies": {
"tslib": "^2.4.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@ -14815,8 +14854,7 @@
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",

View file

@ -26,6 +26,7 @@
"@types/react": "^18.2.25",
"@types/react-dom": "^18.2.10",
"astro": "^3.2.3",
"framer-motion": "10.16.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "4.11.0",

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>
);
}