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 { 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} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}</>;
|
||||||
|
|
|
||||||
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),
|
...createActions(...a),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'moodist-sound',
|
name: 'moodist-sounds',
|
||||||
skipHydration: true,
|
skipHydration: true,
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 0,
|
version: 0,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue