feat(levels): add hover interaction and smooth React Spring transitions
Add hover/touch support and smooth animations to the levels page slider for an enhanced interactive experience. Changes: - Add hover/touch move handlers to slider for instant level preview - Integrate React Spring for smooth scale transitions between levels - Animate abacus container with smooth scale transformations - Support both mouse and touch events for mobile compatibility - Update UI text to mention hover, drag, and touch interactions The slider now responds immediately to mouse/touch position, making it easy to explore different levels by simply hovering over the slider. React Spring provides smooth transitions when switching between levels with different abacus sizes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useSpring, animated } from '@react-spring/web'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../../styled-system/css'
|
||||
@@ -121,12 +122,36 @@ const allLevels = [
|
||||
|
||||
export default function LevelsPage() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const sliderRef = useRef<HTMLInputElement>(null)
|
||||
const currentLevel = allLevels[currentIndex]
|
||||
|
||||
// Calculate scale factor based on number of columns to fit the page
|
||||
// Smaller scale for more columns (Dan levels with 30 columns)
|
||||
const scaleFactor = Math.min(2.5, 20 / currentLevel.digits)
|
||||
|
||||
// Animate scale factor with React Spring for smooth transitions
|
||||
const animatedProps = useSpring({
|
||||
scaleFactor,
|
||||
config: { tension: 280, friction: 60 },
|
||||
})
|
||||
|
||||
// Handle mouse/touch move over slider for hover interaction
|
||||
const handleSliderHover = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (!sliderRef.current) return
|
||||
|
||||
const rect = sliderRef.current.getBoundingClientRect()
|
||||
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
|
||||
const x = clientX - rect.left
|
||||
const percentage = Math.max(0, Math.min(1, x / rect.width))
|
||||
const newIndex = Math.round(percentage * (allLevels.length - 1))
|
||||
|
||||
if (newIndex !== currentIndex) {
|
||||
setCurrentIndex(newIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate an interesting non-zero number to display on the abacus
|
||||
// Use a suffix pattern so rightmost digits stay constant as columns increase
|
||||
// This prevents beads from shifting: ones always 9, tens always 8, etc.
|
||||
@@ -263,7 +288,7 @@ export default function LevelsPage() {
|
||||
</div>
|
||||
|
||||
{/* Abacus Display */}
|
||||
<div
|
||||
<animated.div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -275,6 +300,9 @@ export default function LevelsPage() {
|
||||
borderColor: 'gray.700',
|
||||
overflowX: 'auto',
|
||||
})}
|
||||
style={{
|
||||
transform: animatedProps.scaleFactor.to((s) => `scale(${s / scaleFactor})`),
|
||||
}}
|
||||
>
|
||||
<AbacusReact
|
||||
value={displayValue}
|
||||
@@ -283,7 +311,7 @@ export default function LevelsPage() {
|
||||
showNumbers={true}
|
||||
customStyles={darkStyles}
|
||||
/>
|
||||
</div>
|
||||
</animated.div>
|
||||
|
||||
{/* Digit Count */}
|
||||
<div className={css({ textAlign: 'center', color: 'gray.400', fontSize: 'sm' })}>
|
||||
@@ -308,26 +336,33 @@ export default function LevelsPage() {
|
||||
Explore All Levels
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400' })}>
|
||||
Drag the slider to see each rank
|
||||
Hover, drag, or touch the slider to see each rank
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Range Slider */}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={allLevels.length - 1}
|
||||
value={currentIndex}
|
||||
onChange={(e) => setCurrentIndex(Number(e.target.value))}
|
||||
className={css({
|
||||
w: '100%',
|
||||
h: '2',
|
||||
bg: 'gray.700',
|
||||
rounded: 'full',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
{/* Range Slider with hover support */}
|
||||
<div
|
||||
onMouseMove={handleSliderHover}
|
||||
onTouchMove={handleSliderHover}
|
||||
className={css({ position: 'relative' })}
|
||||
>
|
||||
<input
|
||||
ref={sliderRef}
|
||||
type="range"
|
||||
min="0"
|
||||
max={allLevels.length - 1}
|
||||
value={currentIndex}
|
||||
onChange={(e) => setCurrentIndex(Number(e.target.value))}
|
||||
className={css({
|
||||
w: '100%',
|
||||
h: '2',
|
||||
bg: 'gray.700',
|
||||
rounded: 'full',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Level Markers */}
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user