From 22bb65de0d4ea9f485e4923b9c8715233df3114e Mon Sep 17 00:00:00 2001 From: MAZE Date: Tue, 10 Oct 2023 17:29:12 +0330 Subject: [PATCH] feat: implement basic Zustand store --- package-lock.json | 38 +++++++++++++++- package.json | 3 +- src/components/categories/categories.tsx | 23 +++++----- src/components/category/category.tsx | 7 ++- src/components/sound/sound.tsx | 44 +++++++++++------- src/components/sounds/sounds.tsx | 7 ++- src/components/store-consumer/index.ts | 1 + .../store-consumer/store-consumer.tsx | 15 +++++++ src/data/sounds.tsx | 18 +++++++- src/pages/index.astro | 4 +- src/store/index.ts | 1 + src/store/sound/index.ts | 20 +++++++++ src/store/sound/sound.actions.ts | 45 +++++++++++++++++++ src/store/sound/sound.state.ts | 37 +++++++++++++++ 14 files changed, 228 insertions(+), 35 deletions(-) create mode 100644 src/components/store-consumer/index.ts create mode 100644 src/components/store-consumer/store-consumer.tsx create mode 100644 src/store/index.ts create mode 100644 src/store/sound/index.ts create mode 100644 src/store/sound/sound.actions.ts create mode 100644 src/store/sound/sound.state.ts diff --git a/package-lock.json b/package-lock.json index ecade10..4a8560a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "4.11.0", - "react-wrap-balancer": "1.1.0" + "react-wrap-balancer": "1.1.0", + "zustand": "4.4.3" }, "devDependencies": { "@commitlint/cli": "17.7.2", @@ -15135,6 +15136,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -16044,6 +16053,33 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.3.tgz", + "integrity": "sha512-oRy+X3ZazZvLfmv6viIaQmtLOMeij1noakIsK/Y47PWYhT8glfXzQ4j0YcP5i0P0qI1A4rIB//SGROGyZhx91A==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 54eca21..b1b9a8f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "4.11.0", - "react-wrap-balancer": "1.1.0" + "react-wrap-balancer": "1.1.0", + "zustand": "4.4.3" }, "devDependencies": { "@commitlint/cli": "17.7.2", diff --git a/src/components/categories/categories.tsx b/src/components/categories/categories.tsx index 7b3a50d..eec5577 100644 --- a/src/components/categories/categories.tsx +++ b/src/components/categories/categories.tsx @@ -1,4 +1,5 @@ import { Container } from '@/components/container'; +import { StoreConsumer } from '../store-consumer'; import { Category } from '@/components/category'; import { PlayButton } from '@/components/play-button'; import { PlayProvider } from '@/contexts/play'; @@ -9,16 +10,18 @@ export function Categories() { const { categories } = sounds; return ( - - - + + + + -
- {categories.map(category => ( - - ))} -
-
-
+
+ {categories.map(category => ( + + ))} +
+
+
+ ); } diff --git a/src/components/category/category.tsx b/src/components/category/category.tsx index 09407e1..63c2ea2 100644 --- a/src/components/category/category.tsx +++ b/src/components/category/category.tsx @@ -6,7 +6,12 @@ interface CategoryProps { icon: React.ReactNode; title: string; id: string; - sounds: Array<{ label: string; src: string; icon: React.ReactNode }>; + sounds: Array<{ + label: string; + src: string; + icon: React.ReactNode; + id: string; + }>; } export function Category({ icon, id, sounds, title }: CategoryProps) { diff --git a/src/components/sound/sound.tsx b/src/components/sound/sound.tsx index 992b68a..c4445c7 100644 --- a/src/components/sound/sound.tsx +++ b/src/components/sound/sound.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; -import { useLocalStorage } from '@/hooks/use-local-storage'; import { useSound } from '@/hooks/use-sound'; +import { useSoundStore } from '@/store'; import { usePlay } from '@/contexts/play'; import { cn } from '@/helpers/styles'; @@ -12,6 +12,7 @@ interface SoundProps { src: string; icon: React.ReactNode; hidden: boolean; + id: string; selectHidden: (key: string) => void; unselectHidden: (key: string) => void; } @@ -19,17 +20,24 @@ interface SoundProps { export function Sound({ hidden, icon, + id, label, selectHidden, src, unselectHidden, }: SoundProps) { const { isPlaying, play } = usePlay(); - const [isSelected, setIsSelected] = useLocalStorage( - `${label}-is-selected`, - false, - ); - const [volume, setVolume] = useLocalStorage(`${label}-volume`, 0.5); + // const [isSelected, setIsSelected] = useLocalStorage( + // `${label}-is-selected`, + // false, + // ); + // const [volume, setVolume] = useLocalStorage(`${label}-volume`, 0.5); + + const select = useSoundStore(state => state.select); + const unselect = useSoundStore(state => state.unselect); + const setVolume = useSoundStore(state => state.setVolume); + const volume = useSoundStore(state => state.sounds[id].volume); + const isSelected = useSoundStore(state => state.sounds[id].isSelected); const sound = useSound(src, { loop: true, volume }); @@ -46,21 +54,21 @@ export function Sound({ else if (hidden && !isSelected) unselectHidden(label); }, [label, isSelected, hidden, selectHidden, unselectHidden]); - const select = useCallback(() => { - setIsSelected(true); + const _select = useCallback(() => { + select(id); play(); - }, [setIsSelected, play]); + }, [select, play, id]); - const unselect = useCallback(() => { - setIsSelected(false); - setVolume(0.5); - }, [setIsSelected, setVolume]); + const _unselect = useCallback(() => { + unselect(id); + setVolume(id, 0.5); + }, [unselect, setVolume, id]); const toggle = useCallback(() => { - if (isSelected) return unselect(); + if (isSelected) return _unselect(); - select(); - }, [isSelected, unselect, select]); + _select(); + }, [isSelected, _unselect, _select]); return (
isSelected && setVolume(Number(e.target.value) / 100)} onClick={e => e.stopPropagation()} + onChange={e => + isSelected && setVolume(id, Number(e.target.value) / 100) + } />
); diff --git a/src/components/sounds/sounds.tsx b/src/components/sounds/sounds.tsx index 97cf3c9..ee5f126 100644 --- a/src/components/sounds/sounds.tsx +++ b/src/components/sounds/sounds.tsx @@ -8,7 +8,12 @@ import styles from './sounds.module.css'; interface SoundsProps { id: string; - sounds: Array<{ label: string; src: string; icon: React.ReactNode }>; + sounds: Array<{ + label: string; + src: string; + icon: React.ReactNode; + id: string; + }>; } export function Sounds({ id, sounds }: SoundsProps) { diff --git a/src/components/store-consumer/index.ts b/src/components/store-consumer/index.ts new file mode 100644 index 0000000..84b9b4b --- /dev/null +++ b/src/components/store-consumer/index.ts @@ -0,0 +1 @@ +export { StoreConsumer } from './store-consumer'; diff --git a/src/components/store-consumer/store-consumer.tsx b/src/components/store-consumer/store-consumer.tsx new file mode 100644 index 0000000..eb90381 --- /dev/null +++ b/src/components/store-consumer/store-consumer.tsx @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +import { useSoundStore } from '@/store'; + +interface StoreConsumerProps { + children: React.ReactNode; +} + +export function StoreConsumer({ children }: StoreConsumerProps) { + useEffect(() => { + useSoundStore.persist.rehydrate(); + }); + + return <>{children}; +} diff --git a/src/data/sounds.tsx b/src/data/sounds.tsx index 556e189..0556411 100644 --- a/src/data/sounds.tsx +++ b/src/data/sounds.tsx @@ -13,7 +13,12 @@ export const sounds: { id: string; title: string; icon: React.ReactNode; - sounds: Array<{ label: string; src: string; icon: React.ReactNode }>; + sounds: Array<{ + label: string; + src: string; + icon: React.ReactNode; + id: string; + }>; }>; } = { categories: [ @@ -23,41 +28,49 @@ export const sounds: { sounds: [ { icon: , + id: 'rain', label: 'Rain', src: '/sounds/rain.mp3', }, { icon: , + id: 'birds', label: 'Birds', src: '/sounds/birds.mp3', }, { icon: , + id: 'river', label: 'River', src: '/sounds/river.mp3', }, { icon: , + id: 'thunder', label: 'Thunder', src: '/sounds/thunder.mp3', }, { icon: , + id: 'crickets', label: 'Crickets', src: '/sounds/crickets.mp3', }, { icon: , + id: 'waves', label: 'Waves', src: '/sounds/waves.mp3', }, { icon: , + id: 'seagulls', label: 'Seagulls', src: '/sounds/seagulls.mp3', }, { icon: , + id: 'campfire', label: 'Campfire', src: '/sounds/campfire.mp3', }, @@ -70,16 +83,19 @@ export const sounds: { sounds: [ { icon: , + id: 'airport', label: 'Airport', src: '/sounds/airport.mp3', }, { icon: , + id: 'cafe', label: 'Cafe', src: '/sounds/cafe.mp3', }, { icon: , + id: 'rain-on-window', label: 'Rain on Window', src: '/sounds/rain-on-window.mp3', }, diff --git a/src/pages/index.astro b/src/pages/index.astro index a099b17..154067e 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,14 +3,12 @@ import Layout from '@/layouts/layout.astro'; import { Hero } from '@/components/hero'; import { Categories } from '@/components/categories'; - -import { sounds } from '@/data/sounds'; ---
- +
diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..faff52a --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1 @@ +export { useSoundStore } from './sound'; diff --git a/src/store/sound/index.ts b/src/store/sound/index.ts new file mode 100644 index 0000000..04f3883 --- /dev/null +++ b/src/store/sound/index.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; + +import { type SoundState, createState } from './sound.state'; +import { type SoundActions, createActions } from './sound.actions'; + +export const useSoundStore = create()( + persist( + (...a) => ({ + ...createState(...a), + ...createActions(...a), + }), + { + name: 'moodist-sound', + skipHydration: true, + storage: createJSONStorage(() => localStorage), + version: 0, + }, + ), +); diff --git a/src/store/sound/sound.actions.ts b/src/store/sound/sound.actions.ts new file mode 100644 index 0000000..60de6fe --- /dev/null +++ b/src/store/sound/sound.actions.ts @@ -0,0 +1,45 @@ +import type { StateCreator } from 'zustand'; + +import type { SoundState } from './sound.state'; + +export interface SoundActions { + select: (id: string) => void; + unselect: (id: string) => void; + setVolume: (id: string, volume: number) => void; +} + +export const createActions: StateCreator< + SoundActions & SoundState, + [], + [], + SoundActions +> = (set, get) => { + return { + select(id) { + set({ + sounds: { + ...get().sounds, + [id]: { ...get().sounds[id], isSelected: true }, + }, + }); + }, + + setVolume(id, volume) { + set({ + sounds: { + ...get().sounds, + [id]: { ...get().sounds[id], volume }, + }, + }); + }, + + unselect(id) { + set({ + sounds: { + ...get().sounds, + [id]: { ...get().sounds[id], isSelected: false }, + }, + }); + }, + }; +}; diff --git a/src/store/sound/sound.state.ts b/src/store/sound/sound.state.ts new file mode 100644 index 0000000..3b2187a --- /dev/null +++ b/src/store/sound/sound.state.ts @@ -0,0 +1,37 @@ +import type { StateCreator } from 'zustand'; + +import type { SoundActions } from './sound.actions'; + +import { sounds } from '@/data/sounds'; + +export interface SoundState { + sounds: { + [id: string]: { + isSelected: boolean; + volume: number; + }; + }; +} + +export const createState: StateCreator< + SoundState & SoundActions, + [], + [], + SoundState +> = () => { + const state: SoundState = { sounds: {} }; + const { categories } = sounds; + + categories.forEach(category => { + const { sounds } = category; + + sounds.forEach(sound => { + state.sounds[sound.id] = { + isSelected: false, + volume: 0.5, + }; + }); + }); + + return state; +};