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