mirror of
https://github.com/remvze/moodist.git
synced 2025-12-08 22:25:05 -05:00
feat: implement basic Zustand store
This commit is contained in:
parent
e2cd75a332
commit
22bb65de0d
38
package-lock.json
generated
38
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
1
src/components/store-consumer/index.ts
Normal file
1
src/components/store-consumer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { StoreConsumer } from './store-consumer';
|
||||
15
src/components/store-consumer/store-consumer.tsx
Normal file
15
src/components/store-consumer/store-consumer.tsx
Normal 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}</>;
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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
1
src/store/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { useSoundStore } from './sound';
|
||||
20
src/store/sound/index.ts
Normal file
20
src/store/sound/index.ts
Normal 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,
|
||||
},
|
||||
),
|
||||
);
|
||||
45
src/store/sound/sound.actions.ts
Normal file
45
src/store/sound/sound.actions.ts
Normal 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 },
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
37
src/store/sound/sound.state.ts
Normal file
37
src/store/sound/sound.state.ts
Normal 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;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user