Skip to content

Commit a35369d

Browse files
google-labs-jules[bot]arii
authored andcommitted
feat: implement mobile navigation gestures and redesign nav bar
This commit enhances the mobile user experience by: 1. Creating a reusable `useSwipeGesture` hook to detect horizontal swipes. 2. Integrating swipe gestures into the Control Panel, allowing users to navigate between the main dashboard, controls, and connection pages with swipes. 3. Redesigning the `BottomNavBar` with a larger height (72px), a modern "glassmorphism" background effect, and an animated indicator for the active tab to improve usability and visual appeal.
1 parent 9590c0f commit a35369d

3 files changed

Lines changed: 134 additions & 53 deletions

File tree

app/client/control/ControlPanel.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,31 @@ import Head from 'next/head'
1212
import { useEffect } from 'react'
1313
import { useWebSocket } from '@/context/WebSocketContext'
1414
import dynamic from 'next/dynamic'
15+
import { useRouter } from 'next/navigation'
16+
import { useSwipeGesture } from '@/hooks/useSwipeGesture'
1517

1618
const SpotifyControls = dynamic(
1719
() => import('./components/SpotifyControls'),
18-
{ loading: () => <Skeleton variant="rectangular" height={280} sx={{ borderRadius: 1, mb: 2 }}/> }
20+
{
21+
loading: () => (
22+
<Skeleton
23+
variant="rectangular"
24+
height={280}
25+
sx={{ borderRadius: 1, mb: 2 }}
26+
/>
27+
),
28+
}
1929
)
2030
import TimerControls from './components/TimerControls'
2131

2232
const ControlPanel = () => {
2333
const { connectionStatus, connect } = useWebSocket()
34+
const router = useRouter()
35+
36+
const swipeHandlers = useSwipeGesture({
37+
onSwipeLeft: () => router.push('/client/connect'),
38+
onSwipeRight: () => router.push('/'),
39+
})
2440

2541
// Reconnect on page visibility
2642
useEffect(() => {
@@ -70,6 +86,7 @@ const ControlPanel = () => {
7086
minHeight: '100vh',
7187
backgroundColor: 'background.default',
7288
}}
89+
{...swipeHandlers}
7390
>
7491
{/* Connection Status */}
7592
<Box sx={{ mb: 2, textAlign: 'center' }}>

components/BottomNavBar.tsx

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,71 +5,90 @@ import FavoriteIcon from '@mui/icons-material/Favorite'
55
import SettingsIcon from '@mui/icons-material/Settings'
66
import BottomNavigation from '@mui/material/BottomNavigation'
77
import BottomNavigationAction from '@mui/material/BottomNavigationAction'
8+
import Box from '@mui/material/Box'
89
import Link from 'next/link'
910
import { usePathname } from 'next/navigation'
10-
import { useState } from 'react'
11+
import { useEffect, useState } from 'react'
12+
13+
const navItems = [
14+
{ href: '/', label: 'Dashboard', icon: <DashboardIcon /> },
15+
{ href: '/client/control', label: 'Controls', icon: <SettingsIcon /> },
16+
{ href: '/client/connect', label: 'Connect', icon: <FavoriteIcon /> },
17+
]
1118

1219
export default function BottomNavBar() {
1320
const pathname = usePathname()
14-
const [value, setValue] = useState(() => {
15-
if (pathname === '/client/control') {
16-
return 1
17-
} else if (pathname === '/client/connect') {
18-
return 2
21+
const [value, setValue] = useState(0)
22+
23+
// Update the active tab when the path changes
24+
useEffect(() => {
25+
const activeIndex = navItems.findIndex((item) => item.href === pathname)
26+
if (activeIndex !== -1) {
27+
setValue(activeIndex)
1928
}
20-
return 0 // Default to Dashboard
21-
})
29+
}, [pathname])
2230

2331
return (
24-
<BottomNavigation
25-
value={value}
26-
onChange={(_event, newValue) => {
27-
setValue(newValue)
28-
}}
29-
showLabels
30-
sx={{
31-
width: '100%',
32-
position: 'fixed',
33-
bottom: 0,
34-
left: 0,
35-
right: 0,
36-
zIndex: 1000,
37-
boxShadow: '0px -2px 4px rgba(0, 0, 0, 0.1)',
38-
}}
39-
>
40-
<BottomNavigationAction
41-
label="Dashboard"
42-
icon={<DashboardIcon />}
43-
component={Link}
44-
href="/"
45-
sx={{
46-
'&:hover, &.Mui-focusVisible': {
47-
backgroundColor: 'action.hover',
48-
},
32+
<Box sx={{ position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 1000 }}>
33+
<BottomNavigation
34+
showLabels
35+
value={value}
36+
onChange={(_event, newValue) => {
37+
// Navigation is handled by Link components, this just updates the visual state
38+
setValue(newValue)
4939
}}
50-
/>
51-
<BottomNavigationAction
52-
label="Phone Controls"
53-
icon={<SettingsIcon />}
54-
component={Link}
55-
href="/client/control"
5640
sx={{
57-
'&:hover, &.Mui-focusVisible': {
58-
backgroundColor: 'action.hover',
59-
},
41+
width: '100%',
42+
height: '72px', // Increased height
43+
// Glassmorphism effect
44+
backgroundColor: 'rgba(20, 20, 20, 0.7)',
45+
backdropFilter: 'blur(10px) saturate(180%)',
46+
WebkitBackdropFilter: 'blur(10px) saturate(180%)', // For Safari
47+
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
48+
boxShadow: '0px -4px 20px rgba(0, 0, 0, 0.25)',
49+
position: 'relative', // Parent for the animated indicator
6050
}}
61-
/>
62-
<BottomNavigationAction
63-
label="Stream HR"
64-
icon={<FavoriteIcon />}
65-
component={Link}
66-
href="/client/connect"
51+
>
52+
{navItems.map((item) => (
53+
<BottomNavigationAction
54+
key={item.href}
55+
label={item.label}
56+
icon={item.icon}
57+
component={Link}
58+
href={item.href}
59+
sx={{
60+
color: 'rgba(255, 255, 255, 0.7)',
61+
transition: 'color 0.3s ease',
62+
'&.Mui-selected': {
63+
color: 'common.white',
64+
},
65+
'& .MuiBottomNavigationAction-label': {
66+
fontSize: '0.8rem',
67+
},
68+
'&:hover, &.Mui-focusVisible': {
69+
backgroundColor: 'transparent',
70+
},
71+
}}
72+
/>
73+
))}
74+
</BottomNavigation>
75+
{/* Animated Indicator */}
76+
<Box
6777
sx={{
68-
'&:hover, &.Mui-focusVisible': {
69-
backgroundColor: 'action.hover',
70-
},
78+
position: 'absolute',
79+
bottom: '8px',
80+
// Centering logic for the indicator
81+
left: `calc(${(100 / navItems.length) * value}% + ${
82+
100 / navItems.length / 2
83+
}% - 20px)`,
84+
width: '40px',
85+
height: '4px',
86+
backgroundColor: 'primary.main',
87+
borderRadius: '2px',
88+
transition: 'left 0.3s cubic-bezier(0.65, 0, 0.35, 1)',
89+
willChange: 'left',
7190
}}
7291
/>
73-
</BottomNavigation>
92+
</Box>
7493
)
7594
}

hooks/useSwipeGesture.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// hooks/useSwipeGesture.ts
2+
import { useState, TouchEvent } from 'react'
3+
4+
interface SwipeHandlers {
5+
onSwipeLeft?: () => void
6+
onSwipeRight?: () => void
7+
}
8+
9+
export const useSwipeGesture = ({
10+
onSwipeLeft,
11+
onSwipeRight,
12+
}: SwipeHandlers) => {
13+
const [touchStart, setTouchStart] = useState<number | null>(null)
14+
const [touchEnd, setTouchEnd] = useState<number | null>(null)
15+
16+
// the required distance between touchStart and touchEnd to be detected as a swipe
17+
const minSwipeDistance = 50
18+
19+
const onTouchStart = (e: TouchEvent) => {
20+
setTouchEnd(null) // otherwise the swipe is fired even with usual touch events
21+
setTouchStart(e.targetTouches[0].clientX)
22+
}
23+
24+
const onTouchMove = (e: TouchEvent) => setTouchEnd(e.targetTouches[0].clientX)
25+
26+
const onTouchEnd = () => {
27+
if (!touchStart || !touchEnd) return
28+
const distance = touchStart - touchEnd
29+
const isLeftSwipe = distance > minSwipeDistance
30+
const isRightSwipe = distance < -minSwipeDistance
31+
32+
if (isLeftSwipe && onSwipeLeft) {
33+
onSwipeLeft()
34+
}
35+
if (isRightSwipe && onSwipeRight) {
36+
onSwipeRight()
37+
}
38+
}
39+
40+
return {
41+
onTouchStart,
42+
onTouchMove,
43+
onTouchEnd,
44+
}
45+
}

0 commit comments

Comments
 (0)