Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/components/gamification/AchievementsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { motion } from "framer-motion";
import { Lock, CheckCircle } from "lucide-react";
import { useGamificationStore } from "@/app/stores/useGamificationStore";
import { useUIStore } from "@/app/stores/useUIStore";
import { Card, CardHeader, CardTitle, CardContent } from "../ui/Card";

interface AchievementsPanelProps {
Expand All @@ -11,7 +12,8 @@ interface AchievementsPanelProps {

export function AchievementsPanel({ className }: AchievementsPanelProps) {
const achievements = useGamificationStore((state) => state.achievements);
const animationsEnabled = useGamificationStore((state) => state.animationsEnabled);
const reducedMotion = useUIStore((state) => state.reducedMotion);
const animationsEnabled = !reducedMotion;

const unlockedCount = achievements.filter((a) => a.unlockedAt).length;
const totalCount = achievements.length;
Expand Down
70 changes: 21 additions & 49 deletions src/app/components/gamification/GamificationSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { Volume2, VolumeX, Sparkles, SparklesIcon } from "lucide-react";
import { Volume2, VolumeX, Sparkles, Accessibility } from "lucide-react";
import { useGamificationStore } from "@/app/stores/useGamificationStore";
import { useUIStore } from "@/app/stores/useUIStore";
import { useSoundEffect } from "@/app/utils/soundManager";
import { Card, CardHeader, CardTitle, CardContent } from "../ui/Card";
import { useEffect } from "react";
Expand All @@ -11,45 +12,34 @@ interface GamificationSettingsProps {
}

export function GamificationSettings({ className }: GamificationSettingsProps) {
const soundEnabled = useGamificationStore((state) => state.soundEnabled);
const animationsEnabled = useGamificationStore((state) => state.animationsEnabled);
const soundVolume = useGamificationStore((state) => state.soundVolume);
const toggleSound = useGamificationStore((state) => state.toggleSound);
const toggleAnimations = useGamificationStore((state) => state.toggleAnimations);
const setSoundVolume = useGamificationStore((state) => state.setSoundVolume);
const soundEnabled = useUIStore((state) => state.soundEnabled);
const reducedMotion = useUIStore((state) => state.reducedMotion);
const setSoundEnabled = useUIStore((state) => state.setSoundEnabled);
const setReducedMotion = useUIStore((state) => state.setReducedMotion);

const sound = useSoundEffect();

// Sync sound manager with store
useEffect(() => {
sound.setEnabled(soundEnabled);
sound.setVolume(soundVolume);
}, [soundEnabled, soundVolume, sound]);
}, [soundEnabled, sound]);

const handleToggleSound = () => {
toggleSound();
if (!soundEnabled) {
const nextState = !soundEnabled;
setSoundEnabled(nextState);
if (nextState) {
// Play a test sound when enabling
setTimeout(() => sound.play("success"), 100);
}
};

const handleToggleAnimations = () => {
toggleAnimations();
if (soundEnabled) {
const handleToggleReducedMotion = () => {
setReducedMotion(!reducedMotion);
if (soundEnabled && reducedMotion) {
sound.play("click");
}
};

const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
setSoundVolume(newVolume);
// Play a test sound at the new volume
if (soundEnabled) {
sound.play("xpGain");
}
};

return (
<Card className={className}>
<CardHeader>
Expand Down Expand Up @@ -88,48 +78,30 @@ export function GamificationSettings({ className }: GamificationSettingsProps) {
</button>
</div>

{/* Volume slider */}
{soundEnabled && (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900 dark:text-zinc-100">
Volume: {Math.round(soundVolume * 100)}%
</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={soundVolume}
onChange={handleVolumeChange}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-zinc-700 accent-blue-600"
/>
</div>
)}

{/* Animations toggle */}
{/* Reduced Motion toggle */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Sparkles
<Accessibility
className={`h-5 w-5 ${
animationsEnabled ? "text-purple-600 dark:text-purple-400" : "text-gray-400"
reducedMotion ? "text-purple-600 dark:text-purple-400" : "text-gray-400"
}`}
/>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-zinc-100">Animations</p>
<p className="text-sm font-medium text-gray-900 dark:text-zinc-100">Reduced Motion</p>
<p className="text-xs text-gray-600 dark:text-zinc-400">
Enable micro-animations and effects
Disable or tone down non-essential animations
</p>
</div>
</div>
<button
onClick={handleToggleAnimations}
onClick={handleToggleReducedMotion}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
animationsEnabled ? "bg-purple-600" : "bg-gray-300 dark:bg-zinc-700"
reducedMotion ? "bg-purple-600" : "bg-gray-300 dark:bg-zinc-700"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
animationsEnabled ? "translate-x-6" : "translate-x-1"
reducedMotion ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
Expand Down
4 changes: 3 additions & 1 deletion src/app/components/gamification/KingdomProgressWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { motion } from "framer-motion";
import { Crown, TrendingUp } from "lucide-react";
import { useGamificationStore, getNextLevelInfo } from "@/app/stores/useGamificationStore";
import { useUIStore } from "@/app/stores/useUIStore";
import { Card } from "../ui/Card";

interface KingdomProgressWidgetProps {
Expand All @@ -14,7 +15,8 @@ export function KingdomProgressWidget({ className, compact = false }: KingdomPro
const level = useGamificationStore((state) => state.level);
const xp = useGamificationStore((state) => state.xp);
const kingdomTitle = useGamificationStore((state) => state.kingdomTitle);
const animationsEnabled = useGamificationStore((state) => state.animationsEnabled);
const reducedMotion = useUIStore((state) => state.reducedMotion);
const animationsEnabled = !reducedMotion;

const { nextLevel, xpToNext, progress } = getNextLevelInfo(xp);
const isMaxLevel = nextLevel === level;
Expand Down
6 changes: 4 additions & 2 deletions src/app/components/gamification/LevelUpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X, Crown, Sparkles, Gift } from "lucide-react";
import { useGamificationStore } from "@/app/stores/useGamificationStore";
import { useUIStore } from "@/app/stores/useUIStore";
import { useSoundEffect } from "@/app/utils/soundManager";
import { Button } from "../ui/Button";
import { useModalFocusTrap } from "../../hooks/useModalFocusTrap";
Expand All @@ -12,8 +13,9 @@ export function LevelUpModal() {
const showModal = useGamificationStore((state) => state.showLevelUpModal);
const pendingLevelUp = useGamificationStore((state) => state.pendingLevelUp);
const dismissLevelUp = useGamificationStore((state) => state.dismissLevelUp);
const soundEnabled = useGamificationStore((state) => state.soundEnabled);
const animationsEnabled = useGamificationStore((state) => state.animationsEnabled);
const soundEnabled = useUIStore((state) => state.soundEnabled);
const reducedMotion = useUIStore((state) => state.reducedMotion);
const animationsEnabled = !reducedMotion;
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);

Expand Down
12 changes: 7 additions & 5 deletions src/app/components/gamification/XPGainAnimation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { motion, AnimatePresence } from "framer-motion";
import { Sparkles } from "lucide-react";
import { useEffect, useState } from "react";
import { useUIStore } from "@/app/stores/useUIStore";

interface XPGainAnimationProps {
amount: number;
Expand All @@ -18,6 +19,7 @@ export function XPGainAnimation({
position = "top",
}: XPGainAnimationProps) {
const [isVisible, setIsVisible] = useState(show);
const reducedMotion = useUIStore((state) => state.reducedMotion);

useEffect(() => {
if (show) {
Expand All @@ -40,16 +42,16 @@ export function XPGainAnimation({
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, scale: 0.5, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: -20 }}
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.5, y: 20 }}
animate={reducedMotion ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.8, y: -20 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className={`fixed left-1/2 ${positionClasses[position]} z-50 -translate-x-1/2`}
>
<div className="flex items-center gap-2 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 px-6 py-3 shadow-lg">
<motion.div
animate={{ rotate: [0, 360] }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
animate={reducedMotion ? {} : { rotate: [0, 360] }}
transition={reducedMotion ? {} : { duration: 1, repeat: Infinity, ease: "linear" }}
>
<Sparkles size={20} className="text-yellow-300" />
</motion.div>
Expand Down
5 changes: 4 additions & 1 deletion src/app/components/global_ui/GlobalXPGain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import { useEffect, useState } from "react";
import { useGamificationStore } from "../../stores/useGamificationStore";
import { useUIStore } from "../../stores/useUIStore";
import { XPGainAnimation } from "../gamification/XPGainAnimation";
import { useSoundEffect } from "../../utils/soundManager";

export function GlobalXPGain() {
const recentXPGain = useGamificationStore((state) => state.recentXPGain);
const clearRecentXPGain = useGamificationStore((state) => state.clearRecentXPGain);
const soundEnabled = useGamificationStore((state) => state.soundEnabled);
const soundEnabled = useUIStore((state) => state.soundEnabled);
const sound = useSoundEffect();

const [queue, setQueue] = useState<{ amount: number; reason: string; id: number }[]>([]);
Expand All @@ -20,6 +21,7 @@ export function GlobalXPGain() {

useEffect(() => {
if (recentXPGain) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setQueue((prev) => {
// Prevent duplicate enqueue
if (
Expand All @@ -37,6 +39,7 @@ export function GlobalXPGain() {
useEffect(() => {
if (!activeGain && queue.length > 0) {
const next = queue[0];
// eslint-disable-next-line react-hooks/set-state-in-effect
setActiveGain(next);
setQueue((prev) => prev.slice(1));
if (soundEnabled) {
Expand Down
5 changes: 2 additions & 3 deletions src/app/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export {
selectToasts,
selectIsGlobalLoading,
selectGlobalLoadingMessage,
selectSoundEnabled,
selectReducedMotion,
} from "./useUIStore";
export type { Toast, ToastVariant, ModalId, UIStore } from "./useUIStore";

Expand All @@ -43,9 +45,6 @@ export {
selectXP,
selectKingdomTitle,
selectAchievements,
selectSoundEnabled,
selectAnimationsEnabled,
selectSoundVolume,
selectShowLevelUpModal,
selectPendingLevelUp,
getNextLevelInfo,
Expand Down
36 changes: 0 additions & 36 deletions src/app/stores/useGamificationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ interface GamificationState {
kingdomTitle: KingdomLevel;
achievements: Achievement[];

// Settings
soundEnabled: boolean;
animationsEnabled: boolean;
soundVolume: number; // 0-1

// UI state
showLevelUpModal: boolean;
pendingLevelUp: LevelUpReward | null;
Expand All @@ -64,11 +59,6 @@ interface GamificationActions {
unlockAchievement: (achievementId: string) => void;
updateAchievementProgress: (achievementId: string, progress: number) => void;

// Settings
toggleSound: () => void;
toggleAnimations: () => void;
setSoundVolume: (volume: number) => void;

// Reset
resetGamification: () => void;
}
Expand Down Expand Up @@ -191,9 +181,6 @@ const initialState: GamificationState = {
xp: 0,
kingdomTitle: "Peasant",
achievements: INITIAL_ACHIEVEMENTS,
soundEnabled: true,
animationsEnabled: true,
soundVolume: 0.5,
showLevelUpModal: false,
pendingLevelUp: null,
recentXPGain: null,
Expand Down Expand Up @@ -297,26 +284,6 @@ export const useGamificationStore = create<GamificationStore>()(
);
},

toggleSound: () => {
set(
(state) => ({ soundEnabled: !state.soundEnabled }),
false,
"gamification/toggleSound",
);
},

toggleAnimations: () => {
set(
(state) => ({ animationsEnabled: !state.animationsEnabled }),
false,
"gamification/toggleAnimations",
);
},

setSoundVolume: (volume) => {
set({ soundVolume: Math.max(0, Math.min(1, volume)) }, false, "gamification/setVolume");
},

resetGamification: () => {
set({ ...initialState }, false, "gamification/reset");
},
Expand All @@ -335,9 +302,6 @@ export const selectLevel = (state: GamificationStore) => state.level;
export const selectXP = (state: GamificationStore) => state.xp;
export const selectKingdomTitle = (state: GamificationStore) => state.kingdomTitle;
export const selectAchievements = (state: GamificationStore) => state.achievements;
export const selectSoundEnabled = (state: GamificationStore) => state.soundEnabled;
export const selectAnimationsEnabled = (state: GamificationStore) => state.animationsEnabled;
export const selectSoundVolume = (state: GamificationStore) => state.soundVolume;
export const selectShowLevelUpModal = (state: GamificationStore) => state.showLevelUpModal;
export const selectPendingLevelUp = (state: GamificationStore) => state.pendingLevelUp;

Expand Down
Loading