mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 09:24:14 +00:00
feat: implement basic Zustand store
This commit is contained in:
parent
e2cd75a332
commit
22bb65de0d
14 changed files with 228 additions and 35 deletions
38
package-lock.json
generated
38
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,16 +10,18 @@ export function Categories() {
|
||||||
const { categories } = sounds;
|
const { categories } = sounds;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlayProvider>
|
<StoreConsumer>
|
||||||
<Container>
|
<PlayProvider>
|
||||||
<PlayButton />
|
<Container>
|
||||||
|
<PlayButton />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{categories.map(category => (
|
{categories.map(category => (
|
||||||
<Category {...category} key={category.id} />
|
<Category {...category} key={category.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</PlayProvider>
|
</PlayProvider>
|
||||||
|
</StoreConsumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
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;
|
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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
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…
Add table
Reference in a new issue