feat: implement favorite sounds functionality

This commit is contained in:
MAZE 2023-10-10 23:42:39 +03:30
parent e7c786f259
commit cb34b59d86
10 changed files with 144 additions and 81 deletions

View file

@ -1,3 +1,9 @@
import { useEffect, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { BiSolidHeart } from 'react-icons/bi/index';
import { useFavoriteStore } from '@/store/favorite';
import { Container } from '@/components/container'; import { Container } from '@/components/container';
import { StoreConsumer } from '../store-consumer'; import { StoreConsumer } from '../store-consumer';
import { Category } from '@/components/category'; import { Category } from '@/components/category';
@ -7,7 +13,18 @@ import { PlayProvider } from '@/contexts/play';
import { sounds } from '@/data/sounds'; import { sounds } from '@/data/sounds';
export function Categories() { export function Categories() {
const { categories } = sounds; const categories = useMemo(() => sounds.categories, []);
const favorites = useFavoriteStore(useShallow(state => state.favorites));
const favoriteSounds = useMemo(() => {
return categories
.map(category => category.sounds)
.flat()
.filter(sound => favorites.includes(sound.id));
}, [favorites, categories]);
useEffect(() => console.log({ favoriteSounds }), [favoriteSounds]);
return ( return (
<StoreConsumer> <StoreConsumer>
@ -16,6 +33,16 @@ export function Categories() {
<PlayButton /> <PlayButton />
<div> <div>
{!!favoriteSounds.length && (
<Category
functional={false}
icon={<BiSolidHeart />}
id="favorites"
sounds={favoriteSounds}
title="Favorites"
/>
)}
{categories.map(category => ( {categories.map(category => (
<Category {...category} key={category.id} /> <Category {...category} key={category.id} />
))} ))}

View file

@ -6,6 +6,7 @@ interface CategoryProps {
icon: React.ReactNode; icon: React.ReactNode;
title: string; title: string;
id: string; id: string;
functional: boolean;
sounds: Array<{ sounds: Array<{
label: string; label: string;
src: string; src: string;
@ -14,7 +15,13 @@ interface CategoryProps {
}>; }>;
} }
export function Category({ icon, id, sounds, title }: CategoryProps) { export function Category({
functional = true,
icon,
id,
sounds,
title,
}: CategoryProps) {
return ( return (
<div className={styles.category}> <div className={styles.category}>
<div className={styles.iconContainer}> <div className={styles.iconContainer}>
@ -24,7 +31,7 @@ export function Category({ icon, id, sounds, title }: CategoryProps) {
<h2 className={styles.title}>{title}</h2> <h2 className={styles.title}>{title}</h2>
<Sounds id={id} sounds={sounds} /> <Sounds functional={functional} id={id} sounds={sounds} />
</div> </div>
); );
} }

View file

@ -29,6 +29,28 @@
content: ''; content: '';
} }
& .favoriteButton {
position: absolute;
top: 10px;
right: 10px;
display: flex;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
border: 1px solid var(--color-neutral-200);
border-radius: 50%;
background-color: black;
background-color: var(--color-neutral-100);
color: var(--color-foreground);
cursor: pointer;
outline: none;
&.isFavorite {
color: #f43f5e;
}
}
& .icon { & .icon {
position: relative; position: relative;
z-index: 2; z-index: 2;
@ -85,77 +107,4 @@
font-weight: 600; font-weight: 600;
line-height: 1.6; line-height: 1.6;
} }
& input {
width: 100%;
max-width: 120px;
margin-top: 10px;
/********** Range Input Styles **********/
/* Range Reset */
appearance: none;
background: transparent;
cursor: pointer;
/* Removes default focus */
&:focus {
outline: none;
}
&:disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}
/***** Chrome, Safari, Opera and Edge Chromium styles *****/
&::-webkit-slider-runnable-track {
height: 0.5rem;
border-radius: 0.5rem;
background-color: #27272a;
}
&::-webkit-slider-thumb {
width: 14px;
height: 14px;
border: 1px solid #52525b;
border-radius: 50%;
margin-top: -3px;
appearance: none;
background-color: #3f3f46;
}
&:not(:disabled):focus::-webkit-slider-thumb {
border: 1px solid #053a5f;
outline: 3px solid #053a5f;
outline-offset: 0.125rem;
}
/******** Firefox styles ********/
&::-moz-range-track {
height: 0.5rem;
border-radius: 0.5rem;
background-color: #27272a;
}
&::-moz-range-thumb {
width: 14px;
height: 14px;
border: none;
border: 1px solid #52525b;
border-radius: 0;
border-radius: 50%;
margin-top: -3px;
background-color: #3f3f46;
}
&:not(:disabled):focus::-moz-range-thumb {
border: 1px solid #053a5f;
outline: 3px solid #053a5f;
outline-offset: 0.125rem;
}
}
} }

View file

@ -1,9 +1,11 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { BiHeart, BiSolidHeart } from 'react-icons/bi/index';
import { Range } from './range'; import { Range } from './range';
import { useSound } from '@/hooks/use-sound'; import { useSound } from '@/hooks/use-sound';
import { useSoundStore } from '@/store'; import { useSoundStore } from '@/store';
import { useFavoriteStore } from '@/store/favorite';
import { usePlay } from '@/contexts/play'; import { usePlay } from '@/contexts/play';
import { cn } from '@/helpers/styles'; import { cn } from '@/helpers/styles';
@ -15,11 +17,13 @@ interface SoundProps {
icon: React.ReactNode; icon: React.ReactNode;
hidden: boolean; hidden: boolean;
id: string; id: string;
functional: boolean;
selectHidden: (key: string) => void; selectHidden: (key: string) => void;
unselectHidden: (key: string) => void; unselectHidden: (key: string) => void;
} }
export function Sound({ export function Sound({
functional,
hidden, hidden,
icon, icon,
id, id,
@ -36,15 +40,18 @@ export function Sound({
const volume = useSoundStore(state => state.sounds[id].volume); const volume = useSoundStore(state => state.sounds[id].volume);
const isSelected = useSoundStore(state => state.sounds[id].isSelected); const isSelected = useSoundStore(state => state.sounds[id].isSelected);
const isFavorite = useFavoriteStore(state => state.favorites.includes(id));
const toggleFavorite = useFavoriteStore(state => state.toggleFavorite);
const sound = useSound(src, { loop: true, volume }); const sound = useSound(src, { loop: true, volume });
useEffect(() => { useEffect(() => {
if (isSelected && isPlaying) { if (isSelected && isPlaying && functional) {
sound?.play(); sound?.play();
} else { } else {
sound?.pause(); sound?.pause();
} }
}, [isSelected, sound, isPlaying]); }, [isSelected, sound, isPlaying, functional]);
useEffect(() => { useEffect(() => {
if (hidden && isSelected) selectHidden(label); if (hidden && isSelected) selectHidden(label);
@ -77,6 +84,15 @@ export function Sound({
onClick={toggle} onClick={toggle}
onKeyDown={toggle} onKeyDown={toggle}
> >
<button
className={cn(styles.favoriteButton, isFavorite && styles.isFavorite)}
onClick={e => {
e.stopPropagation();
toggleFavorite(id);
}}
>
{isFavorite ? <BiSolidHeart /> : <BiHeart />}
</button>
<div className={styles.icon}>{icon}</div> <div className={styles.icon}>{icon}</div>
<h3 id={label}>{label}</h3> <h3 id={label}>{label}</h3>
<Range id={id} label={label} /> <Range id={id} label={label} />

View file

@ -8,6 +8,7 @@ import styles from './sounds.module.css';
interface SoundsProps { interface SoundsProps {
id: string; id: string;
functional: boolean;
sounds: Array<{ sounds: Array<{
label: string; label: string;
src: string; src: string;
@ -16,7 +17,7 @@ interface SoundsProps {
}>; }>;
} }
export function Sounds({ id, sounds }: SoundsProps) { export function Sounds({ functional, id, sounds }: SoundsProps) {
const [showAll, setShowAll] = useLocalStorage(`${id}-show-more`, false); const [showAll, setShowAll] = useLocalStorage(`${id}-show-more`, false);
const [hiddenSelections, setHiddenSelections] = useState<{ const [hiddenSelections, setHiddenSelections] = useState<{
@ -50,14 +51,17 @@ export function Sounds({ id, sounds }: SoundsProps) {
<Sound <Sound
key={sound.label} key={sound.label}
{...sound} {...sound}
functional={functional}
hidden={!showAll && index > 5} hidden={!showAll && index > 5}
selectHidden={selectHidden} selectHidden={selectHidden}
unselectHidden={unselectHidden} unselectHidden={unselectHidden}
/> />
))} ))}
{sounds.length < 2 && new Array(2 - sounds.length).fill(<div />)}
</div> </div>
{sounds.length > 4 && ( {sounds.length > 6 && (
<button <button
className={cn( className={cn(
styles.button, styles.button,

View file

@ -1,6 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSoundStore } from '@/store'; import { useSoundStore } from '@/store';
import { useFavoriteStore } from '@/store/favorite';
interface StoreConsumerProps { interface StoreConsumerProps {
children: React.ReactNode; children: React.ReactNode;
@ -9,6 +10,7 @@ interface StoreConsumerProps {
export function StoreConsumer({ children }: StoreConsumerProps) { export function StoreConsumer({ children }: StoreConsumerProps) {
useEffect(() => { useEffect(() => {
useSoundStore.persist.rehydrate(); useSoundStore.persist.rehydrate();
useFavoriteStore.persist.rehydrate();
}); });
return <>{children}</>; return <>{children}</>;

View file

@ -0,0 +1,24 @@
import type { StateCreator } from 'zustand';
import type { FavoriteState } from './favorite.state';
export interface FavoriteActions {
toggleFavorite: (id: string) => void;
}
export const createActions: StateCreator<
FavoriteActions & FavoriteState,
[],
[],
FavoriteActions
> = (set, get) => {
return {
toggleFavorite(id) {
if (get().favorites.includes(id)) {
set({ favorites: get().favorites.filter(_id => _id !== id) });
} else {
set({ favorites: [...get().favorites, id] });
}
},
};
};

View file

@ -0,0 +1,14 @@
import type { StateCreator } from 'zustand';
import type { FavoriteActions } from './favorite.actions';
export interface FavoriteState {
favorites: Array<string>;
}
export const createState: StateCreator<
FavoriteState & FavoriteActions,
[],
[],
FavoriteState
> = () => ({ favorites: [] });

View file

@ -0,0 +1,20 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { type FavoriteState, createState } from './favorite.state';
import { type FavoriteActions, createActions } from './favorite.actions';
export const useFavoriteStore = create<FavoriteState & FavoriteActions>()(
persist(
(...a) => ({
...createState(...a),
...createActions(...a),
}),
{
name: 'moodist-favorites',
skipHydration: true,
storage: createJSONStorage(() => localStorage),
version: 0,
},
),
);

View file

@ -11,7 +11,7 @@ export const useSoundStore = create<SoundState & SoundActions>()(
...createActions(...a), ...createActions(...a),
}), }),
{ {
name: 'moodist-sound', name: 'moodist-sounds',
skipHydration: true, skipHydration: true,
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
version: 0, version: 0,