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-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",

View File

@ -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",

View File

@ -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 (
<PlayProvider>
<Container>
<PlayButton />
<StoreConsumer>
<PlayProvider>
<Container>
<PlayButton />
<div>
{categories.map(category => (
<Category {...category} key={category.id} />
))}
</div>
</Container>
</PlayProvider>
<div>
{categories.map(category => (
<Category {...category} key={category.id} />
))}
</div>
</Container>
</PlayProvider>
</StoreConsumer>
);
}

View File

@ -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) {

View File

@ -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 (
<div
@ -83,8 +91,10 @@ export function Sound({
min={0}
type="range"
value={volume * 100}
onChange={e => isSelected && setVolume(Number(e.target.value) / 100)}
onClick={e => e.stopPropagation()}
onChange={e =>
isSelected && setVolume(id, Number(e.target.value) / 100)
}
/>
</div>
);

View File

@ -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) {

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;
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: <FaCloudShowersHeavy />,
id: 'rain',
label: 'Rain',
src: '/sounds/rain.mp3',
},
{
icon: <PiBirdFill />,
id: 'birds',
label: 'Birds',
src: '/sounds/birds.mp3',
},
{
icon: <BiWater />,
id: 'river',
label: 'River',
src: '/sounds/river.mp3',
},
{
icon: <MdOutlineThunderstorm />,
id: 'thunder',
label: 'Thunder',
src: '/sounds/thunder.mp3',
},
{
icon: <GiCricket />,
id: 'crickets',
label: 'Crickets',
src: '/sounds/crickets.mp3',
},
{
icon: <FaWater />,
id: 'waves',
label: 'Waves',
src: '/sounds/waves.mp3',
},
{
icon: <GiSeagull />,
id: 'seagulls',
label: 'Seagulls',
src: '/sounds/seagulls.mp3',
},
{
icon: <BsFire />,
id: 'campfire',
label: 'Campfire',
src: '/sounds/campfire.mp3',
},
@ -70,16 +83,19 @@ export const sounds: {
sounds: [
{
icon: <BsAirplaneFill />,
id: 'airport',
label: 'Airport',
src: '/sounds/airport.mp3',
},
{
icon: <BiSolidCoffeeAlt />,
id: 'cafe',
label: 'Cafe',
src: '/sounds/cafe.mp3',
},
{
icon: <GiWindow />,
id: 'rain-on-window',
label: 'Rain on Window',
src: '/sounds/rain-on-window.mp3',
},

View File

@ -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';
---
<Layout title="Welcome to Astro.">
<main>
<Hero />
<Categories categories={sounds.categories} client:load />
<Categories client:load />
</main>
</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;
};