This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eruda": "^3.4.3",
|
"eruda": "^3.4.3",
|
||||||
|
"howler": "^2.2.4",
|
||||||
"i18next": "^25.8.17",
|
"i18next": "^25.8.17",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"motion": "^12.35.1",
|
"motion": "^12.35.1",
|
||||||
@@ -34,10 +35,12 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-i18next": "^16.5.6",
|
"react-i18next": "^16.5.6",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"xstate": "^5.28.0"
|
"xstate": "^5.28.0",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/router-plugin": "^1.166.3",
|
"@tanstack/router-plugin": "^1.166.3",
|
||||||
|
"@types/howler": "^2.2.12",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -47,6 +47,9 @@ importers:
|
|||||||
eruda:
|
eruda:
|
||||||
specifier: ^3.4.3
|
specifier: ^3.4.3
|
||||||
version: 3.4.3
|
version: 3.4.3
|
||||||
|
howler:
|
||||||
|
specifier: ^2.2.4
|
||||||
|
version: 2.2.4
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^25.8.17
|
specifier: ^25.8.17
|
||||||
version: 25.8.17(typescript@5.9.3)
|
version: 25.8.17(typescript@5.9.3)
|
||||||
@@ -71,10 +74,16 @@ importers:
|
|||||||
xstate:
|
xstate:
|
||||||
specifier: ^5.28.0
|
specifier: ^5.28.0
|
||||||
version: 5.28.0
|
version: 5.28.0
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.12
|
||||||
|
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tanstack/router-plugin':
|
'@tanstack/router-plugin':
|
||||||
specifier: ^1.166.3
|
specifier: ^1.166.3
|
||||||
version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
|
'@types/howler':
|
||||||
|
specifier: ^2.2.12
|
||||||
|
version: 2.2.12
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.1
|
specifier: ^24.10.1
|
||||||
version: 24.12.0
|
version: 24.12.0
|
||||||
@@ -1226,6 +1235,9 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/howler@2.2.12':
|
||||||
|
resolution: {integrity: sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==}
|
||||||
|
|
||||||
'@types/node@24.12.0':
|
'@types/node@24.12.0':
|
||||||
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
|
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
|
||||||
|
|
||||||
@@ -1567,6 +1579,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
howler@2.2.4:
|
||||||
|
resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==}
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
|
|
||||||
@@ -2268,6 +2283,24 @@ packages:
|
|||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
zustand@5.0.12:
|
||||||
|
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ark/schema@0.56.0':
|
'@ark/schema@0.56.0':
|
||||||
@@ -3145,6 +3178,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/howler@2.2.12': {}
|
||||||
|
|
||||||
'@types/node@24.12.0':
|
'@types/node@24.12.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
@@ -3481,6 +3516,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
howler@2.2.4: {}
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
void-elements: 3.1.0
|
void-elements: 3.1.0
|
||||||
@@ -4096,3 +4133,9 @@ snapshots:
|
|||||||
yaml@2.8.2: {}
|
yaml@2.8.2: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
|
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
react: 19.2.4
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { type ReactNode, type RefObject, useMemo } from "react";
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
||||||
import tg, { STORAGE_KEYS } from "@/tg";
|
|
||||||
import type { SoundKey } from "./sounds";
|
|
||||||
import { SOUNDS, preloadSounds } from "./sounds";
|
|
||||||
|
|
||||||
type AudioCtxValue = {
|
|
||||||
isEnabled: boolean;
|
|
||||||
setIsEnabled: (enabled: boolean) => void;
|
|
||||||
loopingRef: RefObject<Map<SoundKey, HTMLAudioElement>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AudioCtx = createContext<AudioCtxValue | null>(null);
|
|
||||||
|
|
||||||
export function useAudioSettings(): {
|
|
||||||
isEnabled: boolean;
|
|
||||||
setIsEnabled: (enabled: boolean) => void;
|
|
||||||
} {
|
|
||||||
const ctx = useContext(AudioCtx);
|
|
||||||
if (!ctx) throw new Error("useAudioSettings must be used within AudioProvider");
|
|
||||||
return { isEnabled: ctx.isEnabled, setIsEnabled: ctx.setIsEnabled };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePlaySound(): (
|
|
||||||
key: SoundKey,
|
|
||||||
{ mode, force }?: { mode?: "once" | "loop"; force: boolean },
|
|
||||||
) => void {
|
|
||||||
const ctx = useContext(AudioCtx);
|
|
||||||
if (!ctx) throw new Error("usePlaySound must be used within AudioProvider");
|
|
||||||
|
|
||||||
const { isEnabled, loopingRef } = ctx;
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(key: SoundKey, { mode, force }: { mode?: "once" | "loop"; force?: boolean } = {}) => {
|
|
||||||
if (!isEnabled && !force) return;
|
|
||||||
|
|
||||||
if (mode === "loop" && loopingRef.current.has(key)) return;
|
|
||||||
|
|
||||||
const audio = new Audio(SOUNDS[key]);
|
|
||||||
|
|
||||||
if (mode === "loop") {
|
|
||||||
audio.loop = true;
|
|
||||||
loopingRef.current.set(key, audio);
|
|
||||||
} else {
|
|
||||||
audio.addEventListener(
|
|
||||||
"ended",
|
|
||||||
() => {
|
|
||||||
audio.src = "";
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.play().catch(() => {
|
|
||||||
if (mode === "loop") loopingRef.current.delete(key);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[isEnabled, loopingRef],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AudioProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [isEnabled, setIsEnabled] = useState(true);
|
|
||||||
const hydratedRef = useRef(false);
|
|
||||||
const loopingRef = useRef<Map<SoundKey, HTMLAudioElement>>(null);
|
|
||||||
if (!loopingRef.current) {
|
|
||||||
loopingRef.current = new Map();
|
|
||||||
preloadSounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
tg.storage.getItem(STORAGE_KEYS.soundEnabled).then((value) => {
|
|
||||||
if (value !== "") {
|
|
||||||
setIsEnabled(value !== "false");
|
|
||||||
}
|
|
||||||
hydratedRef.current = true;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hydratedRef.current) return;
|
|
||||||
tg.storage.setItem(STORAGE_KEYS.soundEnabled, String(isEnabled));
|
|
||||||
}, [isEnabled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isEnabled) {
|
|
||||||
for (const audio of loopingRef.current!.values()) {
|
|
||||||
audio.pause();
|
|
||||||
audio.src = "";
|
|
||||||
}
|
|
||||||
loopingRef.current!.clear();
|
|
||||||
}
|
|
||||||
}, [isEnabled]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AudioCtx
|
|
||||||
value={useMemo(
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
isEnabled,
|
|
||||||
setIsEnabled,
|
|
||||||
loopingRef,
|
|
||||||
}) as AudioCtxValue,
|
|
||||||
[isEnabled],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AudioCtx>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export { AudioProvider, useAudioSettings, usePlaySound } from "./AudioContext";
|
export { useAudioStore } from "./store";
|
||||||
export { SOUNDS } from "./sounds";
|
export { SOUNDS } from "./sounds";
|
||||||
export type { SoundKey } from "./sounds";
|
export type { SoundKey } from "./sounds";
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { prefetch } from "@/helpers/dom";
|
import { Howl } from "howler";
|
||||||
|
|
||||||
import click from "./assets/click.mp3";
|
import click from "./assets/click.mp3";
|
||||||
|
|
||||||
export const SOUNDS = {
|
const SOUND_SOURCES = {
|
||||||
click,
|
click,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type SoundKey = keyof typeof SOUNDS;
|
export type SoundKey = keyof typeof SOUND_SOURCES;
|
||||||
|
|
||||||
|
export const SOUNDS: Record<SoundKey, Howl> = {} as Record<SoundKey, Howl>;
|
||||||
|
|
||||||
export function preloadSounds() {
|
export function preloadSounds() {
|
||||||
for (const src of Object.values(SOUNDS)) {
|
for (const [key, src] of Object.entries(SOUND_SOURCES)) {
|
||||||
prefetch(src);
|
SOUNDS[key as SoundKey] = new Howl({ src: [src], preload: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/audio/store.ts
Normal file
76
src/audio/store.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import tg, { STORAGE_KEYS } from "@/tg";
|
||||||
|
import type { SoundKey } from "./sounds";
|
||||||
|
import { SOUNDS, preloadSounds } from "./sounds";
|
||||||
|
|
||||||
|
type AudioState = {
|
||||||
|
isEnabled: boolean;
|
||||||
|
_hydrated: boolean;
|
||||||
|
setIsEnabled: (enabled: boolean) => void;
|
||||||
|
hydrate: () => Promise<void>;
|
||||||
|
play: (key: SoundKey, opts?: { mode?: "once" | "loop"; force?: boolean }) => void;
|
||||||
|
stopLoop: (key: SoundKey) => void;
|
||||||
|
stopAllLoops: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const looping = new Map<SoundKey, number>();
|
||||||
|
|
||||||
|
export const useAudioStore = create<AudioState>((set, get) => {
|
||||||
|
preloadSounds();
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEnabled: true,
|
||||||
|
_hydrated: false,
|
||||||
|
|
||||||
|
setIsEnabled(enabled: boolean) {
|
||||||
|
set({ isEnabled: enabled });
|
||||||
|
if (get()._hydrated) {
|
||||||
|
tg.storage.setItem(STORAGE_KEYS.soundEnabled, String(enabled));
|
||||||
|
}
|
||||||
|
if (!enabled) {
|
||||||
|
get().stopAllLoops();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async hydrate() {
|
||||||
|
const value = await tg.storage.getItem(STORAGE_KEYS.soundEnabled);
|
||||||
|
if (value !== "") {
|
||||||
|
set({ isEnabled: value !== "false" });
|
||||||
|
}
|
||||||
|
set({ _hydrated: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
play(key: SoundKey, opts?: { mode?: "once" | "loop"; force?: boolean }) {
|
||||||
|
const { isEnabled } = get();
|
||||||
|
if (!isEnabled && !opts?.force) return;
|
||||||
|
|
||||||
|
const howl = SOUNDS[key];
|
||||||
|
if (!howl) return;
|
||||||
|
|
||||||
|
if (opts?.mode === "loop") {
|
||||||
|
if (looping.has(key)) return;
|
||||||
|
howl.loop(true);
|
||||||
|
const id = howl.play();
|
||||||
|
looping.set(key, id);
|
||||||
|
} else {
|
||||||
|
howl.loop(false);
|
||||||
|
howl.play();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopLoop(key: SoundKey) {
|
||||||
|
const id = looping.get(key);
|
||||||
|
if (id != null) {
|
||||||
|
SOUNDS[key]?.stop(id);
|
||||||
|
looping.delete(key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopAllLoops() {
|
||||||
|
for (const [key, id] of looping) {
|
||||||
|
SOUNDS[key]?.stop(id);
|
||||||
|
}
|
||||||
|
looping.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { motion, type HTMLMotionProps } from "motion/react";
|
import { motion, type HTMLMotionProps } from "motion/react";
|
||||||
import clsx, { type ClassValue } from "clsx";
|
import clsx, { type ClassValue } from "clsx";
|
||||||
|
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
import tg from "@/tg";
|
import tg from "@/tg";
|
||||||
|
|
||||||
import classes from "./Button.module.css";
|
import classes from "./Button.module.css";
|
||||||
@@ -19,7 +19,7 @@ const VARIANTS_MAP = {
|
|||||||
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
|
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
|
||||||
|
|
||||||
export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
|
export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import LightSurface from "@components/surface/LightSurface";
|
|||||||
import BackIcon from "./icons/BackIcon";
|
import BackIcon from "./icons/BackIcon";
|
||||||
import StartIcon from "./icons/StartIcon";
|
import StartIcon from "./icons/StartIcon";
|
||||||
import classes from "./Pagination.module.css";
|
import classes from "./Pagination.module.css";
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -21,7 +21,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function Pagination({ value, total, onChange, variant = "default" }: Props) {
|
export default function Pagination({ value, total, onChange, variant = "default" }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
|
|
||||||
const isAtStart = value <= 1 || total <= 1;
|
const isAtStart = value <= 1 || total <= 1;
|
||||||
const isAtEnd = value >= total || total <= 1;
|
const isAtEnd = value >= total || total <= 1;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import DarkSurface from "@components/surface/DarkSurface";
|
|||||||
import { motion, type HTMLMotionProps } from "motion/react";
|
import { motion, type HTMLMotionProps } from "motion/react";
|
||||||
|
|
||||||
import classes from "./TabSelector.module.css";
|
import classes from "./TabSelector.module.css";
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
import tg from "@/tg";
|
import tg from "@/tg";
|
||||||
|
|
||||||
type Tab = {
|
type Tab = {
|
||||||
@@ -20,7 +20,7 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) {
|
export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) {
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
|
|
||||||
const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1;
|
const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { type ReactNode, useRef, useState, type ChangeEvent, useId } from "react
|
|||||||
import KeyboardIcon from "@components/icons/KeyboardIcon";
|
import KeyboardIcon from "@components/icons/KeyboardIcon";
|
||||||
|
|
||||||
import classes from "./NumberInput.module.css";
|
import classes from "./NumberInput.module.css";
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
import tg from "@/tg";
|
import tg from "@/tg";
|
||||||
|
|
||||||
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
|
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
|
||||||
@@ -31,7 +31,7 @@ export default function NumberInput({
|
|||||||
onChange,
|
onChange,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
|
|
||||||
const stableId = useId();
|
const stableId = useId();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|||||||
11
src/main.tsx
11
src/main.tsx
@@ -14,7 +14,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import tg from "@/tg";
|
import tg from "@/tg";
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import { AudioProvider } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
import { LiftProvider } from "@components/lift";
|
import { LiftProvider } from "@components/lift";
|
||||||
|
|
||||||
import "./styles/index.css";
|
import "./styles/index.css";
|
||||||
@@ -30,15 +30,14 @@ declare module "@tanstack/react-router" {
|
|||||||
|
|
||||||
tg.init();
|
tg.init();
|
||||||
i18n.init();
|
i18n.init();
|
||||||
|
useAudioStore.getState().hydrate();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
<QueryClientProvider client={new QueryClient()}>
|
||||||
<AudioProvider>
|
<LiftProvider>
|
||||||
<LiftProvider>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
</LiftProvider>
|
||||||
</LiftProvider>
|
|
||||||
</AudioProvider>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ const TransactionsHistoryModal = lazy(() => import("./components/TransactionsHis
|
|||||||
const LanguageModal = lazy(() => import("./components/LanguageModal"));
|
const LanguageModal = lazy(() => import("./components/LanguageModal"));
|
||||||
|
|
||||||
import classes from "./Settings.module.css";
|
import classes from "./Settings.module.css";
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
|
|
||||||
const [state, send] = useMachine(settingsMachine);
|
const [state, send] = useMachine(settingsMachine);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Modal from "@components/modals/Modal";
|
|||||||
import ContentSurface from "@components/surface/ContentSurface";
|
import ContentSurface from "@components/surface/ContentSurface";
|
||||||
import DarkSurface from "@components/surface/DarkSurface";
|
import DarkSurface from "@components/surface/DarkSurface";
|
||||||
import { useLanguages } from "@/i18n/useLanguages";
|
import { useLanguages } from "@/i18n/useLanguages";
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
import tg from "@/tg";
|
import tg from "@/tg";
|
||||||
|
|
||||||
import classes from "./LanguageModal.module.css";
|
import classes from "./LanguageModal.module.css";
|
||||||
@@ -21,7 +21,7 @@ type Props = {
|
|||||||
export default function LanguageModal({ open, onClose, onBack }: Props) {
|
export default function LanguageModal({ open, onClose, onBack }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { languages, current, setLanguage } = useLanguages();
|
const { languages, current, setLanguage } = useLanguages();
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
|
|
||||||
const handleSelect = (key: string) => {
|
const handleSelect = (key: string) => {
|
||||||
play("click");
|
play("click");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Modal from "@components/modals/Modal";
|
|||||||
import ContentSurface from "@components/surface/ContentSurface";
|
import ContentSurface from "@components/surface/ContentSurface";
|
||||||
import LightSurface from "@components/surface/LightSurface";
|
import LightSurface from "@components/surface/LightSurface";
|
||||||
import SwitchInput from "@components/form/SwitchInput";
|
import SwitchInput from "@components/form/SwitchInput";
|
||||||
import { useAudioSettings, usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
import tg from "@/tg";
|
import tg from "@/tg";
|
||||||
|
|
||||||
import classes from "./SettingsModal.module.css";
|
import classes from "./SettingsModal.module.css";
|
||||||
@@ -23,8 +23,7 @@ type Props = {
|
|||||||
export default function SettingsModal({ open, onClose, onNavigate, liftIds }: Props) {
|
export default function SettingsModal({ open, onClose, onNavigate, liftIds }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { current } = useLanguages();
|
const { current } = useLanguages();
|
||||||
const { isEnabled, setIsEnabled } = useAudioSettings();
|
const { isEnabled, setIsEnabled, play } = useAudioStore();
|
||||||
const play = usePlaySound();
|
|
||||||
|
|
||||||
const handleNavigate = (modal: SettingsModalId) => {
|
const handleNavigate = (modal: SettingsModalId) => {
|
||||||
play("click");
|
play("click");
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import rouletteIcon from "./assets/roulette.svg";
|
|||||||
import tasksIcon from "./assets/tasks.svg";
|
import tasksIcon from "./assets/tasks.svg";
|
||||||
import earningsIcon from "./assets/earnings.svg";
|
import earningsIcon from "./assets/earnings.svg";
|
||||||
import tg, { useTelegramViewportValue } from "@/tg";
|
import tg, { useTelegramViewportValue } from "@/tg";
|
||||||
import { usePlaySound } from "@/audio";
|
import { useAudioStore } from "@/audio";
|
||||||
|
|
||||||
const ANIMATION_DURATION = 0.2;
|
const ANIMATION_DURATION = 0.2;
|
||||||
const SPRING_ANIMATION = {
|
const SPRING_ANIMATION = {
|
||||||
@@ -193,7 +193,7 @@ function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Navigation() {
|
export default function Navigation() {
|
||||||
const play = usePlaySound();
|
const play = useAudioStore((s) => s.play);
|
||||||
const matchRoute = useMatchRoute();
|
const matchRoute = useMatchRoute();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [menuOpen, setMenuOpen] = useState<number>(0);
|
const [menuOpen, setMenuOpen] = useState<number>(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user