Skip to content

Commit fed17f5

Browse files
committed
feat: Implement swipe navigation and sequence step reordering
Introduces horizontal swipe gestures on the main view to switch between tabs (timer, sequence, stopwatch, settings). Also adds functionality to the SequenceView to reorder steps by moving them up or down.
1 parent 251bb8d commit fed17f5

3 files changed

Lines changed: 118 additions & 56 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 0 additions & 41 deletions
This file was deleted.

App.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11

2-
import React, { useState, useEffect } from 'react';
2+
import React, { useState, useEffect, useRef } from 'react';
33
import Tabs from './components/Tabs';
44
import TimerView from './components/TimerView';
55
import StopwatchView from './components/StopwatchView';
66
import SequenceView from './components/SequenceView';
77
import SettingsView from './components/SettingsView';
88
import { Tab, SoundId } from './types';
99

10+
const TABS: Tab[] = ['timer', 'sequence', 'stopwatch', 'settings'];
11+
1012
const App: React.FC = () => {
1113
const [activeTab, setActiveTab] = useState<Tab>('timer');
1214
const [isLoading, setIsLoading] = useState(true);
@@ -22,6 +24,48 @@ const App: React.FC = () => {
2224
localStorage.setItem('chronos_sound', id);
2325
};
2426

27+
// --- Swipe Logic ---
28+
const touchStartX = useRef<number | null>(null);
29+
const touchStartY = useRef<number | null>(null);
30+
31+
const handleTouchStart = (e: React.TouchEvent) => {
32+
touchStartX.current = e.touches[0].clientX;
33+
touchStartY.current = e.touches[0].clientY;
34+
};
35+
36+
const handleTouchEnd = (e: React.TouchEvent) => {
37+
if (touchStartX.current === null || touchStartY.current === null) return;
38+
39+
const touchEndX = e.changedTouches[0].clientX;
40+
const touchEndY = e.changedTouches[0].clientY;
41+
42+
const deltaX = touchStartX.current - touchEndX;
43+
const deltaY = touchStartY.current - touchEndY;
44+
45+
// Basic swipe thresholds
46+
const minSwipeDistance = 50;
47+
48+
// Ensure horizontal swipe is dominant (X > Y) to avoid switching while scrolling vertically
49+
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > minSwipeDistance) {
50+
const currentIndex = TABS.indexOf(activeTab);
51+
52+
if (deltaX > 0) {
53+
// Swipe Left -> Next Tab
54+
if (currentIndex < TABS.length - 1) {
55+
setActiveTab(TABS[currentIndex + 1]);
56+
}
57+
} else {
58+
// Swipe Right -> Prev Tab
59+
if (currentIndex > 0) {
60+
setActiveTab(TABS[currentIndex - 1]);
61+
}
62+
}
63+
}
64+
65+
touchStartX.current = null;
66+
touchStartY.current = null;
67+
};
68+
2569
useEffect(() => {
2670
if (isLoading) {
2771
const timer = setTimeout(() => {
@@ -45,7 +89,11 @@ const App: React.FC = () => {
4589
}
4690

4791
return (
48-
<div className="h-[100dvh] bg-black text-white flex flex-col font-sans selection:bg-gray-800 overflow-hidden relative animate-in fade-in duration-700">
92+
<div
93+
className="h-[100dvh] bg-black text-white flex flex-col font-sans selection:bg-gray-800 overflow-hidden relative animate-in fade-in duration-700"
94+
onTouchStart={handleTouchStart}
95+
onTouchEnd={handleTouchEnd}
96+
>
4997

5098
{/* Ambient Background Orbs for Glass Effect */}
5199
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-blue-900/20 rounded-full blur-[100px] pointer-events-none"></div>

components/SequenceView.tsx

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,22 @@ const SequenceView: React.FC<SequenceViewProps> = ({ soundId }) => {
219219
setSteps(newSteps);
220220
};
221221

222+
const moveStepUp = (index: number) => {
223+
if (index === 0) return;
224+
const newSteps = [...steps];
225+
// Swap current step with previous step
226+
[newSteps[index - 1], newSteps[index]] = [newSteps[index], newSteps[index - 1]];
227+
setSteps(newSteps);
228+
};
229+
230+
const moveStepDown = (index: number) => {
231+
if (index === steps.length - 1) return;
232+
const newSteps = [...steps];
233+
// Swap current step with next step
234+
[newSteps[index], newSteps[index + 1]] = [newSteps[index + 1], newSteps[index]];
235+
setSteps(newSteps);
236+
};
237+
222238
const updateStep = (id: string, updates: Partial<SequenceStep>) => {
223239
setSteps(steps.map(s => s.id === id ? { ...s, ...updates } : s));
224240
};
@@ -281,9 +297,9 @@ const SequenceView: React.FC<SequenceViewProps> = ({ soundId }) => {
281297
};
282298

283299
// --- Styles ---
284-
// Shared with TimerView for consistency
285-
const controlBoxClass = "bg-gray-900/40 backdrop-blur-xl border border-white/10 p-3 sm:p-4 rounded-[1.5rem] shadow-2xl shadow-black/50";
286-
const glassButton = "relative h-14 w-full rounded-2xl flex items-center justify-center transition-all duration-300 ease-out backdrop-blur-md border border-white/5 overflow-hidden group";
300+
// Shared with TimerView for consistency but modified for SequenceView compactness
301+
const controlBoxClass = "bg-gray-900/40 backdrop-blur-xl border border-white/10 p-2 sm:p-3 rounded-[1.25rem] shadow-2xl shadow-black/50";
302+
const glassButton = "relative h-12 w-full rounded-xl flex items-center justify-center transition-all duration-300 ease-out backdrop-blur-md border border-white/5 overflow-hidden group";
287303
const startBtn = `${glassButton} bg-white/10 hover:bg-white/15 border-white/10 text-green-400 hover:shadow-[0_0_20px_rgba(74,222,128,0.2)] active:scale-[0.98]`;
288304
const resetBtn = `${glassButton} bg-white/5 hover:bg-white/10 active:scale-[0.98] text-white/80 hover:text-white hover:border-white/20`;
289305

@@ -328,15 +344,15 @@ const SequenceView: React.FC<SequenceViewProps> = ({ soundId }) => {
328344
)}
329345
</div>
330346

331-
{/* Controls - Matching TimerView */}
347+
{/* Controls - Matching TimerView but compact */}
332348
<div className="w-full px-4 pb-2 shrink-0">
333349
<div className={controlBoxClass}>
334-
<div className="flex items-center justify-center gap-4">
350+
<div className="flex items-center justify-center gap-3">
335351
<button
336352
onClick={resetSequence}
337-
className={`${resetBtn} w-16`} // Explicit width for uniformity
353+
className={`${resetBtn} w-14`} // Slightly smaller width
338354
>
339-
<RotateCcw size={20} />
355+
<RotateCcw size={18} />
340356
</button>
341357

342358
<button
@@ -347,7 +363,7 @@ const SequenceView: React.FC<SequenceViewProps> = ({ soundId }) => {
347363
: 'bg-red-500/10 border-red-500/20 text-red-400 hover:bg-red-500/20 shadow-[0_0_15px_rgba(239,68,68,0.1)]'
348364
} ${glassButton}`}
349365
>
350-
{isPaused ? <Play size={24} className="ml-1 fill-current" /> : <Pause size={24} className="fill-current" />}
366+
{isPaused ? <Play size={22} className="ml-1 fill-current" /> : <Pause size={22} className="fill-current" />}
351367
</button>
352368
</div>
353369
</div>
@@ -525,25 +541,64 @@ const SequenceView: React.FC<SequenceViewProps> = ({ soundId }) => {
525541
</div>
526542
)}
527543

544+
{/* Step Count Header (Fixed) */}
545+
<div className="px-6 py-2 shrink-0 z-10">
546+
<div className="flex items-center gap-4">
547+
<div className="h-[1px] flex-1 bg-gradient-to-r from-transparent via-white/10 to-white/10"></div>
548+
<span className="text-[10px] font-mono font-medium text-white/40 uppercase tracking-[0.2em] px-3 py-1 rounded-full bg-white/5 border border-white/5 backdrop-blur-sm shadow-sm">
549+
{steps.length} Steps
550+
</span>
551+
<div className="h-[1px] flex-1 bg-gradient-to-l from-transparent via-white/10 to-white/10"></div>
552+
</div>
553+
</div>
554+
528555
{/* Step List */}
529556
<div className="flex-1 min-h-0 overflow-y-auto no-scrollbar px-4 space-y-3 pb-4 relative z-10">
530557
{steps.map((step, index) => (
531558
<div
532559
key={step.id}
533560
className="group relative flex flex-col p-4 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 hover:border-white/10 transition-all duration-300"
534561
>
535-
{/* Top Row: Index, Name, Delete */}
562+
{/* Top Row: Index, Name, Arrows, Actions */}
536563
<div className="flex items-center gap-3 mb-3">
564+
{/* Index Badge */}
537565
<div className="w-6 h-6 shrink-0 rounded-full bg-white/10 flex items-center justify-center text-xs font-mono text-white/50">
538566
{index + 1}
539567
</div>
568+
569+
{/* Title Input */}
540570
<input
541571
type="text"
542572
value={step.label}
543573
onChange={(e) => updateStep(step.id, { label: e.target.value })}
544574
className="bg-transparent border-none text-white font-medium focus:ring-0 p-0 text-base placeholder-white/20 flex-1 min-w-0"
545575
placeholder="Step Name"
546576
/>
577+
578+
{/* Reorder Buttons (Distinct) */}
579+
<div className="flex items-center gap-1 shrink-0">
580+
<button
581+
onClick={() => moveStepUp(index)}
582+
disabled={index === 0}
583+
className={`w-7 h-7 flex items-center justify-center rounded-lg bg-white/5 hover:bg-white/15 text-white/70 disabled:opacity-20 disabled:hover:bg-white/5 transition-all ${index === 0 ? 'cursor-not-allowed' : 'active:scale-95'}`}
584+
title="Move Up"
585+
>
586+
<ChevronUp size={16} />
587+
</button>
588+
<button
589+
onClick={() => moveStepDown(index)}
590+
disabled={index === steps.length - 1}
591+
className={`w-7 h-7 flex items-center justify-center rounded-lg bg-white/5 hover:bg-white/15 text-white/70 disabled:opacity-20 disabled:hover:bg-white/5 transition-all ${index === steps.length - 1 ? 'cursor-not-allowed' : 'active:scale-95'}`}
592+
title="Move Down"
593+
>
594+
<ChevronDown size={16} />
595+
</button>
596+
</div>
597+
598+
{/* Divider */}
599+
<div className="w-[1px] h-4 bg-white/10 mx-1"></div>
600+
601+
{/* Actions */}
547602
<div className="flex items-center gap-1 shrink-0">
548603
<button
549604
onClick={() => duplicateStep(step.id)}
@@ -604,23 +659,23 @@ const SequenceView: React.FC<SequenceViewProps> = ({ soundId }) => {
604659
<div className="h-4"></div>
605660
</div>
606661

607-
{/* Editor Controls - Aligned with Timer View */}
662+
{/* Editor Controls - Compact */}
608663
<div className="w-full px-4 pb-2 shrink-0 z-20">
609664
<div className={controlBoxClass}>
610665
<div className="flex items-center gap-3">
611666
<button
612667
onClick={resetEditor}
613-
className={`${resetBtn} w-16`}
668+
className={`${resetBtn} w-14`}
614669
title="Reset Sequence"
615670
>
616-
<RotateCcw size={20} />
671+
<RotateCcw size={18} />
617672
</button>
618673

619674
<button
620675
onClick={toggleTimer}
621676
className={`${startBtn} flex-1 gap-2 font-bold tracking-wide`}
622677
>
623-
<Play size={20} className="fill-current" />
678+
<Play size={22} className="fill-current" />
624679
<span>START</span>
625680
</button>
626681
</div>

0 commit comments

Comments
 (0)