Skip to content
Merged
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
31 changes: 25 additions & 6 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,25 @@ h1 {
margin: 2rem 0;
}

.sticky-controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
margin: 0;
padding: 0;
background: transparent;
border: none;
box-shadow: none;
}

.sticky-controls .control-btn {
margin: 0;
cursor: pointer;
pointer-events: auto;
}

.control-btn {
padding: 12px 24px;
border: none;
Expand Down Expand Up @@ -637,32 +656,32 @@ h1 {
.exercise-btn {
width: 200px;
}

.settings-menu {
top: 15px;
right: 15px;
}

.settings-toggle {
min-width: 100px;
padding: 6px 12px;
font-size: 0.75rem;
}

.settings-content {
min-width: 260px;
right: -10px;
}

.toggle-buttons {
gap: 0.5rem;
}

.toggle-btn {
min-width: 85px;
font-size: 0.8rem;
}

.settings-content.open {
padding: 1rem;
}
Expand Down
38 changes: 38 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,11 @@ function App() {
const [isRaining, setIsRaining] = useState(false);
const [useShapes, setUseShapes] = useState(false); // Toggle between shapes and circles - default to circles
const [isSettingsOpen, setIsSettingsOpen] = useState(false); // Toggle for settings menu
const [isStartButtonVisible, setIsStartButtonVisible] = useState(true); // Track if start button is visible
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const startButtonRef = useRef<HTMLButtonElement | null>(null);

const currentExercise = breathingExercises[breathingType];
const phaseDuration = currentExercise.phaseDurations[phase];
Expand Down Expand Up @@ -193,6 +195,29 @@ function App() {
};
}, [isActive, timerMinutes, breathingType, remainingTime]);

// Intersection Observer for start button visibility
useEffect(() => {
const startButton = startButtonRef.current;
if (!startButton) return;

const observer = new IntersectionObserver(
([entry]) => {
setIsStartButtonVisible(entry.isIntersecting);
},
{
root: null,
rootMargin: '0px',
threshold: 0.9 // Button is considered visible if 90% is visible

Copilot AI Aug 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 0.9 should be extracted to a named constant (e.g., BUTTON_VISIBILITY_THRESHOLD = 0.9) to improve code readability and make it easier to adjust this threshold value if needed.

Copilot uses AI. Check for mistakes.
}
);

observer.observe(startButton);

return () => {
observer.disconnect();
};
}, []);

const handleStartStop = () => {
setIsActive(!isActive);
if (!isActive) {
Expand Down Expand Up @@ -979,13 +1004,26 @@ function App() {

<div className="controls">
<button
ref={startButtonRef}
className={`control-btn ${isActive ? 'stop' : 'start'}`}
onClick={handleStartStop}
>
{isActive ? 'Pause' : 'Start'}
</button>
</div>

{/* Sticky button that appears when main button is not visible */}
{!isStartButtonVisible && (
<div className="sticky-controls">
<button
className={`control-btn ${isActive ? 'stop' : 'start'}`}
onClick={handleStartStop}
>
{isActive ? 'Pause' : 'Start'}
</button>
</div>
)}
Comment on lines +1016 to +1025

Copilot AI Aug 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sticky button duplicates the exact same logic as the main button (className and text content). Consider extracting this into a reusable component or helper function to reduce code duplication and ensure consistency.

Copilot uses AI. Check for mistakes.

<div className="stats">
<p>Completed Cycles: <span className="cycle-count">{cycleCount}</span>
{currentExercise.maxCycles && (
Expand Down
Loading