mirror of
https://github.com/remvze/moodist.git
synced 2025-12-17 08:54:13 +00:00
feat: implement favorite sounds functionality
This commit is contained in:
parent
e7c786f259
commit
cb34b59d86
10 changed files with 144 additions and 81 deletions
|
|
@ -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';
|
||||
|
|
@ -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>
|
||||
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface CategoryProps {
|
|||
icon: React.ReactNode;
|
||||
title: string;
|
||||
id: string;
|
||||
functional: boolean;
|
||||
sounds: Array<{
|
||||
label: 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 (
|
||||
<div className={styles.category}>
|
||||
<div className={styles.iconContainer}>
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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);
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import styles from './sounds.module.css';
|
|||
|
||||
interface SoundsProps {
|
||||
id: string;
|
||||
functional: boolean;
|
||||
sounds: Array<{
|
||||
label: 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 [hiddenSelections, setHiddenSelections] = useState<{
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { useSoundStore } from '@/store';
|
||||
import { useFavoriteStore } from '@/store/favorite';
|
||||
|
||||
interface StoreConsumerProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -9,6 +10,7 @@ interface StoreConsumerProps {
|
|||
export function StoreConsumer({ children }: StoreConsumerProps) {
|
||||
useEffect(() => {
|
||||
useSoundStore.persist.rehydrate();
|
||||
useFavoriteStore.persist.rehydrate();
|
||||
});
|
||||
|
||||
return <>{children}</>;
|
||||
|
|
|
|||
24
src/store/favorite/favorite.actions.ts
Normal file
24
src/store/favorite/favorite.actions.ts
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
14
src/store/favorite/favorite.state.ts
Normal file
14
src/store/favorite/favorite.state.ts
Normal 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: [] });
|
||||
20
src/store/favorite/index.ts
Normal file
20
src/store/favorite/index.ts
Normal 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,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue