Skip to content

Commit e1b60f8

Browse files
SupShadowclaude
andcommitted
feat: add theme system, extended shortcuts, achievements, secrets, and onboarding
- Theme System: 5 color themes (SIGNAL, CYBER, NEON, MATRIX, MINIMAL) - New ThemeContext with CSS custom properties - ThemePicker component for UI - T key to cycle themes - Keyboard Shortcuts: Extended from 8 to 14 shortcuts - J/K for 10s seek backward/forward - 0-9 for percentage jumps - T for theme cycling - Achievements: Added 10 new achievements - listen_30min, complete_10, session_3/10/25 - repeat_5/20 (secret), weekend_warrior (secret) - theme_changer, all_themes (secret) - Secrets: Added 6 new secrets - double_tap, keyboard_ninja, visualizer_zen - speed_demon, long_press, night_session - Onboarding Tour: 7-step interactive tutorial for first-time users - Lazy Loading: Heavy components now dynamically imported - FullscreenVisualizer, ShareCard, Leaderboard - DevDocs, ArtistTools, OnboardingTour 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f0bc87a commit e1b60f8

11 files changed

Lines changed: 940 additions & 29 deletions

components/DevBar.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
"use client";
22

33
import { useState } from "react";
4-
import { DevDocs, DevDocsButton } from "./DevDocs";
5-
import { ArtistTools, ArtistToolsButton } from "./ArtistTools";
4+
import dynamic from "next/dynamic";
5+
import { DevDocsButton } from "./DevDocs";
6+
import { ArtistToolsButton } from "./ArtistTools";
67
import { useExperience } from "@/contexts/ExperienceContext";
78
import { Code } from "lucide-react";
89

10+
// Lazy load heavy modal components (23KB+)
11+
const DevDocs = dynamic(
12+
() => import("./DevDocs").then(mod => ({ default: mod.DevDocs })),
13+
{ ssr: false }
14+
);
15+
16+
const ArtistTools = dynamic(
17+
() => import("./ArtistTools").then(mod => ({ default: mod.ArtistTools })),
18+
{ ssr: false }
19+
);
20+
921
export function DevBar() {
1022
const [showDocs, setShowDocs] = useState(false);
1123
const [showArtistTools, setShowArtistTools] = useState(false);

components/ExperiencePanel.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
Download,
2020
Share2,
2121
X,
22+
Palette,
2223
} from "lucide-react";
24+
import { ThemePicker } from "@/components/ThemePicker";
2325

2426
export function ExperiencePanel() {
2527
const [isOpen, setIsOpen] = useState(false);
@@ -213,6 +215,11 @@ function StatsTab() {
213215
</span>
214216
</div>
215217
</div>
218+
219+
{/* Theme Settings */}
220+
<div className="p-4 border border-white/10">
221+
<ThemePicker />
222+
</div>
216223
</div>
217224
);
218225
}

components/FluxPlayer.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
import { useEffect, useRef, useState, useCallback } from "react";
44
import { Play, Pause, SkipBack, SkipForward, List, Volume2, Volume1, VolumeX, Maximize2, Keyboard } from "lucide-react";
55
import { motion, useMotionValue, useTransform, animate } from "framer-motion";
6+
import dynamic from "next/dynamic";
67
import { useAudio } from "@/contexts/AudioContext";
8+
import { useTheme } from "@/contexts/ThemeContext";
79
import { useKeyboardShortcuts } from "@/hooks";
810
import { getArtworkPath } from "@/lib/tracks";
911
import TrackList from "./TrackList";
10-
import FullscreenVisualizer from "./FullscreenVisualizer";
1112
import KeyboardHints from "./KeyboardHints";
1213
import TypewriterText from "./TypewriterText";
1314
import TransmitButton from "./TransmitButton";
1415
import { cn, assetPath } from "@/lib/utils";
1516
import Image from "next/image";
1617

18+
// Lazy load heavy visualizer component (13KB+)
19+
const FullscreenVisualizer = dynamic(
20+
() => import("./FullscreenVisualizer"),
21+
{ ssr: false }
22+
);
23+
1724
function formatTime(seconds: number): string {
1825
if (!isFinite(seconds) || seconds < 0) return "0:00";
1926
const mins = Math.floor(seconds / 60);
@@ -79,6 +86,8 @@ export default function FluxPlayer() {
7986
seekToPercent,
8087
} = useAudio();
8188

89+
const { cycleTheme } = useTheme();
90+
8291
const [isPlaylistOpen, setIsPlaylistOpen] = useState(false);
8392
const [isVisualizerOpen, setIsVisualizerOpen] = useState(false);
8493
const [isHelpOpen, setIsHelpOpen] = useState(false);
@@ -103,6 +112,22 @@ export default function FluxPlayer() {
103112
setIsHelpOpen(false);
104113
setIsPlaylistOpen(false);
105114
},
115+
// New shortcuts
116+
onSeekBackward: () => {
117+
// Seek 10 seconds back
118+
const newTime = Math.max(0, currentTime - 10);
119+
seekToPercent(duration > 0 ? newTime / duration : 0);
120+
},
121+
onSeekForward: () => {
122+
// Seek 10 seconds forward
123+
const newTime = Math.min(duration, currentTime + 10);
124+
seekToPercent(duration > 0 ? newTime / duration : 0);
125+
},
126+
onSeekPercent: (percent) => {
127+
// Jump to percentage (0-90%)
128+
seekToPercent(percent / 100);
129+
},
130+
onToggleTheme: cycleTheme,
106131
});
107132

108133
useEffect(() => {

components/OnboardingTour.tsx

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
"use client";
2+
3+
import { useState, useEffect, useCallback } from "react";
4+
import { motion, AnimatePresence } from "framer-motion";
5+
import { X, ChevronRight, ChevronLeft, Play, Keyboard, Maximize2, Palette, List } from "lucide-react";
6+
7+
interface TourStep {
8+
id: string;
9+
title: string;
10+
description: string;
11+
icon: typeof Play;
12+
position: "bottom" | "top" | "center";
13+
highlight?: string; // CSS selector or description
14+
}
15+
16+
const TOUR_STEPS: TourStep[] = [
17+
{
18+
id: "welcome",
19+
title: "WELCOME_TO_FLUX",
20+
description: "An immersive audio experience. Let me show you around.",
21+
icon: Play,
22+
position: "center",
23+
},
24+
{
25+
id: "playback",
26+
title: "PLAYBACK_CONTROLS",
27+
description: "Use the play button or press SPACE to start the music. Arrow keys skip tracks.",
28+
icon: Play,
29+
position: "bottom",
30+
highlight: "Play/Pause button",
31+
},
32+
{
33+
id: "tracklist",
34+
title: "TRACK_LIST",
35+
description: "Click the list icon or press L to browse all available tracks.",
36+
icon: List,
37+
position: "bottom",
38+
highlight: "Track list button",
39+
},
40+
{
41+
id: "visualizer",
42+
title: "FULLSCREEN_VISUALIZER",
43+
description: "Press F for an immersive fullscreen audio visualization experience.",
44+
icon: Maximize2,
45+
position: "bottom",
46+
highlight: "Fullscreen button",
47+
},
48+
{
49+
id: "themes",
50+
title: "COLOR_THEMES",
51+
description: "Press T to cycle through different color themes. Find your vibe.",
52+
icon: Palette,
53+
position: "center",
54+
},
55+
{
56+
id: "keyboard",
57+
title: "KEYBOARD_SHORTCUTS",
58+
description: "Press ? anytime to see all available keyboard shortcuts.",
59+
icon: Keyboard,
60+
position: "center",
61+
},
62+
{
63+
id: "secrets",
64+
title: "HIDDEN_SECRETS",
65+
description: "Explore to find hidden achievements and secrets. Some unlock special features...",
66+
icon: Play,
67+
position: "center",
68+
},
69+
];
70+
71+
const STORAGE_KEY = "flux-onboarding-complete";
72+
73+
export default function OnboardingTour() {
74+
const [isVisible, setIsVisible] = useState(false);
75+
const [currentStep, setCurrentStep] = useState(0);
76+
const [mounted, setMounted] = useState(false);
77+
78+
useEffect(() => {
79+
setMounted(true);
80+
// Check if user has completed onboarding
81+
const completed = localStorage.getItem(STORAGE_KEY);
82+
if (!completed) {
83+
// Delay start to let the app load
84+
const timer = setTimeout(() => setIsVisible(true), 2000);
85+
return () => clearTimeout(timer);
86+
}
87+
}, []);
88+
89+
const completeTour = useCallback(() => {
90+
localStorage.setItem(STORAGE_KEY, "true");
91+
setIsVisible(false);
92+
}, []);
93+
94+
const nextStep = useCallback(() => {
95+
if (currentStep < TOUR_STEPS.length - 1) {
96+
setCurrentStep(prev => prev + 1);
97+
} else {
98+
completeTour();
99+
}
100+
}, [currentStep, completeTour]);
101+
102+
const prevStep = useCallback(() => {
103+
if (currentStep > 0) {
104+
setCurrentStep(prev => prev - 1);
105+
}
106+
}, [currentStep]);
107+
108+
const skipTour = useCallback(() => {
109+
completeTour();
110+
}, [completeTour]);
111+
112+
if (!mounted || !isVisible) return null;
113+
114+
const step = TOUR_STEPS[currentStep];
115+
const Icon = step.icon;
116+
const isLastStep = currentStep === TOUR_STEPS.length - 1;
117+
const isFirstStep = currentStep === 0;
118+
119+
return (
120+
<AnimatePresence>
121+
{isVisible && (
122+
<>
123+
{/* Backdrop */}
124+
<motion.div
125+
initial={{ opacity: 0 }}
126+
animate={{ opacity: 1 }}
127+
exit={{ opacity: 0 }}
128+
className="fixed inset-0 bg-void/90 backdrop-blur-sm z-[200]"
129+
onClick={skipTour}
130+
/>
131+
132+
{/* Tour card */}
133+
<motion.div
134+
key={step.id}
135+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
136+
animate={{ opacity: 1, scale: 1, y: 0 }}
137+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
138+
transition={{ type: "spring", damping: 25, stiffness: 300 }}
139+
className={`fixed z-[201] w-full max-w-md px-4 ${
140+
step.position === "bottom"
141+
? "bottom-32 left-1/2 -translate-x-1/2"
142+
: step.position === "top"
143+
? "top-20 left-1/2 -translate-x-1/2"
144+
: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
145+
}`}
146+
>
147+
<div className="relative border border-signal/50 bg-void-deep/95 backdrop-blur-xl shadow-[0_0_60px_rgba(255,69,0,0.3)]">
148+
{/* Progress indicator */}
149+
<div className="absolute top-0 left-0 right-0 h-1 bg-stark/10">
150+
<motion.div
151+
className="h-full bg-signal"
152+
initial={{ width: 0 }}
153+
animate={{ width: `${((currentStep + 1) / TOUR_STEPS.length) * 100}%` }}
154+
transition={{ duration: 0.3 }}
155+
/>
156+
</div>
157+
158+
{/* Skip button */}
159+
<button
160+
onClick={skipTour}
161+
className="absolute top-3 right-3 p-1 text-stark/50 hover:text-signal transition-colors"
162+
aria-label="Skip tour"
163+
>
164+
<X className="w-5 h-5" />
165+
</button>
166+
167+
{/* Content */}
168+
<div className="p-6 pt-8">
169+
{/* Icon */}
170+
<div className="flex justify-center mb-4">
171+
<motion.div
172+
className="w-16 h-16 border-2 border-signal/50 flex items-center justify-center"
173+
animate={{
174+
boxShadow: ["0 0 0px rgba(255,69,0,0)", "0 0 20px rgba(255,69,0,0.5)", "0 0 0px rgba(255,69,0,0)"]
175+
}}
176+
transition={{ duration: 2, repeat: Infinity }}
177+
>
178+
<Icon className="w-8 h-8 text-signal" />
179+
</motion.div>
180+
</div>
181+
182+
{/* Title */}
183+
<h2 className="font-mono text-lg text-signal text-center tracking-widest mb-3">
184+
{step.title}
185+
</h2>
186+
187+
{/* Description */}
188+
<p className="font-mono text-sm text-stark/80 text-center leading-relaxed mb-6">
189+
{step.description}
190+
</p>
191+
192+
{/* Step indicator */}
193+
<div className="flex justify-center gap-1 mb-6">
194+
{TOUR_STEPS.map((_, index) => (
195+
<div
196+
key={index}
197+
className={`w-2 h-2 transition-colors ${
198+
index === currentStep
199+
? "bg-signal"
200+
: index < currentStep
201+
? "bg-signal/50"
202+
: "bg-stark/20"
203+
}`}
204+
/>
205+
))}
206+
</div>
207+
208+
{/* Navigation buttons */}
209+
<div className="flex items-center justify-between gap-4">
210+
<button
211+
onClick={prevStep}
212+
disabled={isFirstStep}
213+
className={`flex items-center gap-2 px-4 py-2 font-mono text-xs tracking-wider transition-colors ${
214+
isFirstStep
215+
? "text-stark/30 cursor-not-allowed"
216+
: "text-stark/70 hover:text-signal"
217+
}`}
218+
>
219+
<ChevronLeft className="w-4 h-4" />
220+
BACK
221+
</button>
222+
223+
<button
224+
onClick={skipTour}
225+
className="font-mono text-xs text-stark/50 hover:text-stark/70 transition-colors"
226+
>
227+
SKIP_TOUR
228+
</button>
229+
230+
<button
231+
onClick={nextStep}
232+
className="flex items-center gap-2 px-4 py-2 font-mono text-xs tracking-wider bg-signal/10 border border-signal/50 text-signal hover:bg-signal/20 transition-colors"
233+
>
234+
{isLastStep ? "START" : "NEXT"}
235+
<ChevronRight className="w-4 h-4" />
236+
</button>
237+
</div>
238+
</div>
239+
240+
{/* Corner decorations */}
241+
<div className="absolute top-0 left-0 w-4 h-4 border-l-2 border-t-2 border-signal" />
242+
<div className="absolute top-0 right-0 w-4 h-4 border-r-2 border-t-2 border-signal" />
243+
<div className="absolute bottom-0 left-0 w-4 h-4 border-l-2 border-b-2 border-signal" />
244+
<div className="absolute bottom-0 right-0 w-4 h-4 border-r-2 border-b-2 border-signal" />
245+
</div>
246+
</motion.div>
247+
</>
248+
)}
249+
</AnimatePresence>
250+
);
251+
}
252+
253+
// Hook to manually trigger the tour
254+
export function useOnboardingTour() {
255+
const resetTour = useCallback(() => {
256+
localStorage.removeItem(STORAGE_KEY);
257+
window.location.reload();
258+
}, []);
259+
260+
return { resetTour };
261+
}

0 commit comments

Comments
 (0)