mirror of
https://github.com/HugeFrog24/shakethefrog.git
synced 2026-04-30 23:02:17 +00:00
Bugfix
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useDarkMode() {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has a dark mode preference in localStorage
|
||||
const isDark = localStorage.getItem('darkMode') === 'true';
|
||||
// Check system preference if no localStorage value
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
setDarkMode(isDark ?? systemPrefersDark);
|
||||
|
||||
// Add listener for system theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
if (localStorage.getItem('darkMode') === null) {
|
||||
setDarkMode(e.matches);
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(!darkMode);
|
||||
localStorage.setItem('darkMode', (!darkMode).toString());
|
||||
};
|
||||
|
||||
return { darkMode, toggleDarkMode };
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIfMobile = () => {
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i;
|
||||
setIsMobile(mobileRegex.test(userAgent));
|
||||
};
|
||||
|
||||
checkIfMobile();
|
||||
window.addEventListener('resize', checkIfMobile);
|
||||
|
||||
return () => window.removeEventListener('resize', checkIfMobile);
|
||||
}, []);
|
||||
|
||||
return isMobile;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useLocale } from 'next-intl';
|
||||
import { getLocalizedSkinName } from '../config/skin-names';
|
||||
import { type Locale } from '../../i18n/request';
|
||||
|
||||
// Define grammatical cases
|
||||
type GrammaticalCase = 'nominative' | 'accusative' | 'dative' | 'genitive' | 'instrumental' | 'prepositional';
|
||||
|
||||
/**
|
||||
* Hook to get localized skin names
|
||||
*/
|
||||
export function useLocalizedSkinName() {
|
||||
const locale = useLocale();
|
||||
|
||||
/**
|
||||
* Get a localized skin name with the appropriate grammatical case
|
||||
* @param skinId The skin ID
|
||||
* @param grammaticalCase The grammatical case to use (for languages that need it)
|
||||
* @returns The localized skin name
|
||||
*/
|
||||
const getLocalizedName = (skinId: string, grammaticalCase: GrammaticalCase = 'nominative'): string => {
|
||||
return getLocalizedSkinName(skinId, locale as Locale, grammaticalCase);
|
||||
};
|
||||
|
||||
return getLocalizedName;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useFeature } from '../providers/FeatureProvider';
|
||||
|
||||
interface PricesData {
|
||||
prices: Record<string, string>;
|
||||
}
|
||||
|
||||
export function usePrices() {
|
||||
const paymentsEnabled = useFeature('paymentsEnabled');
|
||||
const [prices, setPrices] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!paymentsEnabled) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchPrices = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/api/prices');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prices');
|
||||
}
|
||||
|
||||
const data: PricesData = await response.json();
|
||||
setPrices(data.prices);
|
||||
} catch (err) {
|
||||
console.error('Error fetching prices:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch prices');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPrices();
|
||||
}, [paymentsEnabled]);
|
||||
|
||||
const getPrice = (skinId: string): string | null => {
|
||||
if (!paymentsEnabled || loading) {
|
||||
return null;
|
||||
}
|
||||
return prices[skinId] ?? null;
|
||||
};
|
||||
|
||||
return {
|
||||
prices,
|
||||
loading,
|
||||
error,
|
||||
enabled: paymentsEnabled,
|
||||
getPrice
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
const AUDIO_URL = '/audio/starley_call_on_me.ogg';
|
||||
const FADE_IN_SEC = 0.1;
|
||||
const FADE_OUT_SEC = 0.8;
|
||||
const QUIET_TIMEOUT_MS = 300;
|
||||
const PLAY_GAIN = 0.7;
|
||||
// exponentialRampToValueAtTime can't reach 0; use a tiny positive target
|
||||
const NEAR_ZERO = 0.0001;
|
||||
|
||||
export function useShakeAudio() {
|
||||
const ctxRef = useRef<AudioContext | null>(null);
|
||||
const bufferRef = useRef<AudioBuffer | null>(null);
|
||||
const sourceRef = useRef<AudioBufferSourceNode | null>(null);
|
||||
const gainRef = useRef<GainNode | null>(null);
|
||||
const quietTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const fadeOutAndStop = useCallback(() => {
|
||||
const ctx = ctxRef.current;
|
||||
const source = sourceRef.current;
|
||||
const gain = gainRef.current;
|
||||
if (!ctx || !source || !gain) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
gain.gain.cancelScheduledValues(now);
|
||||
gain.gain.setValueAtTime(gain.gain.value, now);
|
||||
gain.gain.linearRampToValueAtTime(NEAR_ZERO, now + FADE_OUT_SEC);
|
||||
|
||||
try {
|
||||
source.stop(now + FADE_OUT_SEC + 0.05);
|
||||
} catch {
|
||||
// source may already be stopped
|
||||
}
|
||||
|
||||
sourceRef.current = null;
|
||||
gainRef.current = null;
|
||||
}, []);
|
||||
|
||||
const bump = useCallback(async () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
if (!ctxRef.current) {
|
||||
try {
|
||||
ctxRef.current = new AudioContext();
|
||||
} catch (err) {
|
||||
console.error('AudioContext creation failed:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = ctxRef.current;
|
||||
if (ctx.state === 'suspended') {
|
||||
try {
|
||||
await ctx.resume();
|
||||
} catch (err) {
|
||||
console.error('AudioContext resume failed:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bufferRef.current) {
|
||||
try {
|
||||
const res = await fetch(AUDIO_URL);
|
||||
const arr = await res.arrayBuffer();
|
||||
bufferRef.current = await ctx.decodeAudioData(arr);
|
||||
} catch (err) {
|
||||
console.error('Audio decode failed:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (quietTimerRef.current) clearTimeout(quietTimerRef.current);
|
||||
quietTimerRef.current = setTimeout(fadeOutAndStop, QUIET_TIMEOUT_MS);
|
||||
|
||||
if (sourceRef.current) return;
|
||||
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = bufferRef.current;
|
||||
source.loop = true;
|
||||
|
||||
const gain = ctx.createGain();
|
||||
const now = ctx.currentTime;
|
||||
gain.gain.setValueAtTime(NEAR_ZERO, now);
|
||||
gain.gain.linearRampToValueAtTime(PLAY_GAIN, now + FADE_IN_SEC);
|
||||
|
||||
source.connect(gain).connect(ctx.destination);
|
||||
source.start(now);
|
||||
|
||||
sourceRef.current = source;
|
||||
gainRef.current = gain;
|
||||
}, [fadeOutAndStop]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (quietTimerRef.current) clearTimeout(quietTimerRef.current);
|
||||
const source = sourceRef.current;
|
||||
if (source) {
|
||||
try { source.stop(); } catch { /* already stopped */ }
|
||||
source.disconnect();
|
||||
}
|
||||
const ctx = ctxRef.current;
|
||||
if (ctx) ctx.close().catch(() => { /* ignore */ });
|
||||
};
|
||||
}, []);
|
||||
|
||||
return bump;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { appConfig } from '../config/app';
|
||||
import { SkinId } from '../types';
|
||||
|
||||
export function useSkin() {
|
||||
const searchParams = useSearchParams();
|
||||
const skinParam = searchParams.get('skin');
|
||||
|
||||
// Validate that the skin exists in our config
|
||||
const isValidSkin = skinParam && Object.keys(appConfig.skins).includes(skinParam);
|
||||
|
||||
// Return the skin from URL if valid, otherwise return default skin
|
||||
const currentSkin = (isValidSkin ? skinParam : appConfig.defaultSkin) as SkinId;
|
||||
|
||||
return currentSkin;
|
||||
}
|
||||
Reference in New Issue
Block a user