Skip to content

Commit

Permalink
feat: implement favorite sounds functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
remvze committed Oct 10, 2023
1 parent e7c786f commit cb34b59
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 81 deletions.
29 changes: 28 additions & 1 deletion src/components/categories/categories.tsx
Original file line number Diff line number Diff line change
@@ -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 { StoreConsumer } from '../store-consumer';
import { Category } from '@/components/category';
Expand All @@ -7,7 +13,18 @@ import { PlayProvider } from '@/contexts/play';
import { sounds } from '@/data/sounds';

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 (
<StoreConsumer>
Expand All @@ -16,6 +33,16 @@ export function Categories() {
<PlayButton />

<div>
{!!favoriteSounds.length && (
<Category
functional={false}
icon={<BiSolidHeart />}
id="favorites"
sounds={favoriteSounds}
title="Favorites"
/>
)}

{categories.map(category => (
<Category {...category} key={category.id} />
))}
Expand Down
11 changes: 9 additions & 2 deletions src/components/category/category.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface CategoryProps {
icon: React.ReactNode;
title: string;
id: string;
functional: boolean;
sounds: Array<{
label: string;
src: string;
Expand All @@ -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 (
<div className={styles.category}>
<div className={styles.iconContainer}>
Expand All @@ -24,7 +31,7 @@ export function Category({ icon, id, sounds, title }: CategoryProps) {

<h2 className={styles.title}>{title}</h2>

<Sounds id={id} sounds={sounds} />
<Sounds functional={functional} id={id} sounds={sounds} />
</div>
);
}
95 changes: 22 additions & 73 deletions src/components/sound/sound.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@
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 {
position: relative;
z-index: 2;
Expand Down Expand Up @@ -85,77 +107,4 @@
font-weight: 600;
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;
}
}
}
20 changes: 18 additions & 2 deletions src/components/sound/sound.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useCallback, useEffect } from 'react';
import { BiHeart, BiSolidHeart } from 'react-icons/bi/index';

import { Range } from './range';

import { useSound } from '@/hooks/use-sound';
import { useSoundStore } from '@/store';
import { useFavoriteStore } from '@/store/favorite';
import { usePlay } from '@/contexts/play';
import { cn } from '@/helpers/styles';

Expand All @@ -15,11 +17,13 @@ interface SoundProps {
icon: React.ReactNode;
hidden: boolean;
id: string;
functional: boolean;
selectHidden: (key: string) => void;
unselectHidden: (key: string) => void;
}

export function Sound({
functional,
hidden,
icon,
id,
Expand All @@ -36,15 +40,18 @@ export function Sound({
const volume = useSoundStore(state => state.sounds[id].volume);
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 });

useEffect(() => {
if (isSelected && isPlaying) {
if (isSelected && isPlaying && functional) {
sound?.play();
} else {
sound?.pause();
}
}, [isSelected, sound, isPlaying]);
}, [isSelected, sound, isPlaying, functional]);

useEffect(() => {
if (hidden && isSelected) selectHidden(label);
Expand Down Expand Up @@ -77,6 +84,15 @@ export function Sound({
onClick={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>
<h3 id={label}>{label}</h3>
<Range id={id} label={label} />
Expand Down
8 changes: 6 additions & 2 deletions src/components/sounds/sounds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styles from './sounds.module.css';

interface SoundsProps {
id: string;
functional: boolean;
sounds: Array<{
label: string;
src: string;
Expand All @@ -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 [hiddenSelections, setHiddenSelections] = useState<{
Expand Down Expand Up @@ -50,14 +51,17 @@ export function Sounds({ id, sounds }: SoundsProps) {
<Sound
key={sound.label}
{...sound}
functional={functional}
hidden={!showAll && index > 5}
selectHidden={selectHidden}
unselectHidden={unselectHidden}
/>
))}

{sounds.length < 2 && new Array(2 - sounds.length).fill(<div />)}
</div>

{sounds.length > 4 && (
{sounds.length > 6 && (
<button
className={cn(
styles.button,
Expand Down
2 changes: 2 additions & 0 deletions src/components/store-consumer/store-consumer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from 'react';

import { useSoundStore } from '@/store';
import { useFavoriteStore } from '@/store/favorite';

interface StoreConsumerProps {
children: React.ReactNode;
Expand All @@ -9,6 +10,7 @@ interface StoreConsumerProps {
export function StoreConsumer({ children }: StoreConsumerProps) {
useEffect(() => {
useSoundStore.persist.rehydrate();
useFavoriteStore.persist.rehydrate();
});

return <>{children}</>;
Expand Down
24 changes: 24 additions & 0 deletions src/store/favorite/favorite.actions.ts
Original file line number Diff line number Diff line change
@@ -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] });
}
},
};
};
14 changes: 14 additions & 0 deletions src/store/favorite/favorite.state.ts
Original file line number Diff line number Diff line change
@@ -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: [] });
20 changes: 20 additions & 0 deletions src/store/favorite/index.ts
Original file line number Diff line number Diff line change
@@ -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,
},
),
);
2 changes: 1 addition & 1 deletion src/store/sound/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const useSoundStore = create<SoundState & SoundActions>()(
...createActions(...a),
}),
{
name: 'moodist-sound',
name: 'moodist-sounds',
skipHydration: true,
storage: createJSONStorage(() => localStorage),
version: 0,
Expand Down

0 comments on commit cb34b59

Please sign in to comment.