feat: implement basic Zustand store

This commit is contained in:
MAZE 2023-10-10 17:29:12 +03:30
parent e2cd75a332
commit 22bb65de0d
14 changed files with 228 additions and 35 deletions

38
package-lock.json generated
View file

@ -15,7 +15,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "4.11.0", "react-icons": "4.11.0",
"react-wrap-balancer": "1.1.0" "react-wrap-balancer": "1.1.0",
"zustand": "4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.7.2", "@commitlint/cli": "17.7.2",
@ -15135,6 +15136,14 @@
"punycode": "^2.1.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -16044,6 +16053,33 @@
"url": "https://github.com/sponsors/colinhacks" "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": { "node_modules/zwitch": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View file

@ -28,7 +28,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "4.11.0", "react-icons": "4.11.0",
"react-wrap-balancer": "1.1.0" "react-wrap-balancer": "1.1.0",
"zustand": "4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.7.2", "@commitlint/cli": "17.7.2",

View file

@ -1,4 +1,5 @@
import { Container } from '@/components/container'; import { Container } from '@/components/container';
import { StoreConsumer } from '../store-consumer';
import { Category } from '@/components/category'; import { Category } from '@/components/category';
import { PlayButton } from '@/components/play-button'; import { PlayButton } from '@/components/play-button';
import { PlayProvider } from '@/contexts/play'; import { PlayProvider } from '@/contexts/play';
@ -9,6 +10,7 @@ export function Categories() {
const { categories } = sounds; const { categories } = sounds;
return ( return (
<StoreConsumer>
<PlayProvider> <PlayProvider>
<Container> <Container>
<PlayButton /> <PlayButton />
@ -20,5 +22,6 @@ export function Categories() {
</div> </div>
</Container> </Container>
</PlayProvider> </PlayProvider>
</StoreConsumer>
); );
} }

View file

@ -6,7 +6,12 @@ interface CategoryProps {
icon: React.ReactNode; icon: React.ReactNode;
title: string; title: string;
id: 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) { export function Category({ icon, id, sounds, title }: CategoryProps) {

View file

@ -1,7 +1,7 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useLocalStorage } from '@/hooks/use-local-storage';
import { useSound } from '@/hooks/use-sound'; import { useSound } from '@/hooks/use-sound';
import { useSoundStore } from '@/store';
import { usePlay } from '@/contexts/play'; import { usePlay } from '@/contexts/play';
import { cn } from '@/helpers/styles'; import { cn } from '@/helpers/styles';
@ -12,6 +12,7 @@ interface SoundProps {
src: string; src: string;
icon: React.ReactNode; icon: React.ReactNode;
hidden: boolean; hidden: boolean;
id: string;
selectHidden: (key: string) => void; selectHidden: (key: string) => void;
unselectHidden: (key: string) => void; unselectHidden: (key: string) => void;
} }
@ -19,17 +20,24 @@ interface SoundProps {
export function Sound({ export function Sound({
hidden, hidden,
icon, icon,
id,
label, label,
selectHidden, selectHidden,
src, src,
unselectHidden, unselectHidden,
}: SoundProps) { }: SoundProps) {
const { isPlaying, play } = usePlay(); const { isPlaying, play } = usePlay();
const [isSelected, setIsSelected] = useLocalStorage( // const [isSelected, setIsSelected] = useLocalStorage(
`${label}-is-selected`, // `${label}-is-selected`,
false, // false,
); // );
const [volume, setVolume] = useLocalStorage(`${label}-volume`, 0.5); // 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 }); const sound = useSound(src, { loop: true, volume });
@ -46,21 +54,21 @@ export function Sound({
else if (hidden && !isSelected) unselectHidden(label); else if (hidden && !isSelected) unselectHidden(label);
}, [label, isSelected, hidden, selectHidden, unselectHidden]); }, [label, isSelected, hidden, selectHidden, unselectHidden]);
const select = useCallback(() => { const _select = useCallback(() => {
setIsSelected(true); select(id);
play(); play();
}, [setIsSelected, play]); }, [select, play, id]);
const unselect = useCallback(() => { const _unselect = useCallback(() => {
setIsSelected(false); unselect(id);
setVolume(0.5); setVolume(id, 0.5);
}, [setIsSelected, setVolume]); }, [unselect, setVolume, id]);
const toggle = useCallback(() => { const toggle = useCallback(() => {
if (isSelected) return unselect(); if (isSelected) return _unselect();
select(); _select();
}, [isSelected, unselect, select]); }, [isSelected, _unselect, _select]);
return ( return (
<div <div
@ -83,8 +91,10 @@ export function Sound({
min={0} min={0}
type="range" type="range"
value={volume * 100} value={volume * 100}
onChange={e => isSelected && setVolume(Number(e.target.value) / 100)}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
onChange={e =>
isSelected && setVolume(id, Number(e.target.value) / 100)
}
/> />
</div> </div>
); );

View file

@ -8,7 +8,12 @@ import styles from './sounds.module.css';
interface SoundsProps { interface SoundsProps {
id: 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 Sounds({ id, sounds }: SoundsProps) { export function Sounds({ id, sounds }: SoundsProps) {

View file

@ -0,0 +1 @@
export { StoreConsumer } from './store-consumer';

View file

@ -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}</>;
}

View file

@ -13,7 +13,12 @@ export const sounds: {
id: string; id: string;
title: string; title: string;
icon: React.ReactNode; icon: React.ReactNode;
sounds: Array<{ label: string; src: string; icon: React.ReactNode }>; sounds: Array<{
label: string;
src: string;
icon: React.ReactNode;
id: string;
}>;
}>; }>;
} = { } = {
categories: [ categories: [
@ -23,41 +28,49 @@ export const sounds: {
sounds: [ sounds: [
{ {
icon: <FaCloudShowersHeavy />, icon: <FaCloudShowersHeavy />,
id: 'rain',
label: 'Rain', label: 'Rain',
src: '/sounds/rain.mp3', src: '/sounds/rain.mp3',
}, },
{ {
icon: <PiBirdFill />, icon: <PiBirdFill />,
id: 'birds',
label: 'Birds', label: 'Birds',
src: '/sounds/birds.mp3', src: '/sounds/birds.mp3',
}, },
{ {
icon: <BiWater />, icon: <BiWater />,
id: 'river',
label: 'River', label: 'River',
src: '/sounds/river.mp3', src: '/sounds/river.mp3',
}, },
{ {
icon: <MdOutlineThunderstorm />, icon: <MdOutlineThunderstorm />,
id: 'thunder',
label: 'Thunder', label: 'Thunder',
src: '/sounds/thunder.mp3', src: '/sounds/thunder.mp3',
}, },
{ {
icon: <GiCricket />, icon: <GiCricket />,
id: 'crickets',
label: 'Crickets', label: 'Crickets',
src: '/sounds/crickets.mp3', src: '/sounds/crickets.mp3',
}, },
{ {
icon: <FaWater />, icon: <FaWater />,
id: 'waves',
label: 'Waves', label: 'Waves',
src: '/sounds/waves.mp3', src: '/sounds/waves.mp3',
}, },
{ {
icon: <GiSeagull />, icon: <GiSeagull />,
id: 'seagulls',
label: 'Seagulls', label: 'Seagulls',
src: '/sounds/seagulls.mp3', src: '/sounds/seagulls.mp3',
}, },
{ {
icon: <BsFire />, icon: <BsFire />,
id: 'campfire',
label: 'Campfire', label: 'Campfire',
src: '/sounds/campfire.mp3', src: '/sounds/campfire.mp3',
}, },
@ -70,16 +83,19 @@ export const sounds: {
sounds: [ sounds: [
{ {
icon: <BsAirplaneFill />, icon: <BsAirplaneFill />,
id: 'airport',
label: 'Airport', label: 'Airport',
src: '/sounds/airport.mp3', src: '/sounds/airport.mp3',
}, },
{ {
icon: <BiSolidCoffeeAlt />, icon: <BiSolidCoffeeAlt />,
id: 'cafe',
label: 'Cafe', label: 'Cafe',
src: '/sounds/cafe.mp3', src: '/sounds/cafe.mp3',
}, },
{ {
icon: <GiWindow />, icon: <GiWindow />,
id: 'rain-on-window',
label: 'Rain on Window', label: 'Rain on Window',
src: '/sounds/rain-on-window.mp3', src: '/sounds/rain-on-window.mp3',
}, },

View file

@ -3,14 +3,12 @@ import Layout from '@/layouts/layout.astro';
import { Hero } from '@/components/hero'; import { Hero } from '@/components/hero';
import { Categories } from '@/components/categories'; import { Categories } from '@/components/categories';
import { sounds } from '@/data/sounds';
--- ---
<Layout title="Welcome to Astro."> <Layout title="Welcome to Astro.">
<main> <main>
<Hero /> <Hero />
<Categories categories={sounds.categories} client:load /> <Categories client:load />
</main> </main>
</Layout> </Layout>

1
src/store/index.ts Normal file
View file

@ -0,0 +1 @@
export { useSoundStore } from './sound';

20
src/store/sound/index.ts Normal file
View file

@ -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<SoundState & SoundActions>()(
persist(
(...a) => ({
...createState(...a),
...createActions(...a),
}),
{
name: 'moodist-sound',
skipHydration: true,
storage: createJSONStorage(() => localStorage),
version: 0,
},
),
);

View file

@ -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 },
},
});
},
};
};

View file

@ -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;
};