From 84019c3881725353ae6fd7cc5cee0765ca7cd20a Mon Sep 17 00:00:00 2001 From: Jeet Debnath Date: Tue, 14 Apr 2026 15:02:33 +0530 Subject: [PATCH] added swipe gestures --- src/App.tsx | 7 ++++ src/hooks/useSwipeNav.ts | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/hooks/useSwipeNav.ts diff --git a/src/App.tsx b/src/App.tsx index 704eed9..134b7f7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,14 @@ import { lazy, Suspense } from 'react' import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { AuthProvider } from './contexts/AuthContext' import { ProtectedRoute } from './components/ProtectedRoute' +import { useSwipeNav } from './hooks/useSwipeNav' import './App.css' +function SwipeNavHandler() { + useSwipeNav() + return null +} + const HomePage = lazy(() => import('./pages/HomePage')) const HistoryPage = lazy(() => import('./pages/HistoryPage')) const SettingsPage = lazy(() => import('./pages/SettingsPage')) @@ -16,6 +22,7 @@ function App() { return ( + } /> diff --git a/src/hooks/useSwipeNav.ts b/src/hooks/useSwipeNav.ts new file mode 100644 index 0000000..792a607 --- /dev/null +++ b/src/hooks/useSwipeNav.ts @@ -0,0 +1,83 @@ +import { useEffect } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' + +const PAGES = ['/write', '/history', '/settings'] +const SWIPE_THRESHOLD = 55 // minimum horizontal px to count as a swipe +const DESKTOP_BREAKPOINT = 860 + +/** Walk up the DOM and return true if any ancestor is horizontally scrollable */ +function isInHScrollable(el: Element | null): boolean { + while (el && el !== document.body) { + const style = window.getComputedStyle(el) + const ox = style.overflowX + if ((ox === 'scroll' || ox === 'auto') && el.scrollWidth > el.clientWidth) { + return true + } + el = el.parentElement + } + return false +} + +/** Swipe left/right to navigate between the three main pages (mobile only) */ +export function useSwipeNav() { + const navigate = useNavigate() + const location = useLocation() + + useEffect(() => { + let startX = 0 + let startY = 0 + let startTarget: Element | null = null + let cancelled = false + + const onTouchStart = (e: TouchEvent) => { + startX = e.touches[0].clientX + startY = e.touches[0].clientY + startTarget = e.target as Element + cancelled = false + } + + const onTouchMove = (e: TouchEvent) => { + // If vertical movement dominates early, cancel the swipe so we never + // accidentally navigate while the user is scrolling. + const dx = Math.abs(e.touches[0].clientX - startX) + const dy = Math.abs(e.touches[0].clientY - startY) + if (!cancelled && dy > dx && dy > 10) cancelled = true + } + + const onTouchEnd = (e: TouchEvent) => { + if (cancelled) return + if (window.innerWidth >= DESKTOP_BREAKPOINT) return + + const dx = e.changedTouches[0].clientX - startX + const dy = e.changedTouches[0].clientY - startY + + // Must be predominantly horizontal + if (Math.abs(dx) <= Math.abs(dy)) return + // Must clear the distance threshold + if (Math.abs(dx) < SWIPE_THRESHOLD) return + // Don't swipe-navigate when inside a horizontal scroll container + if (isInHScrollable(startTarget)) return + // Don't swipe-navigate when a modal/overlay is open + if (document.querySelector('.confirm-modal-overlay, .cropper-overlay, .reminder-modal-overlay')) return + + const idx = PAGES.indexOf(location.pathname) + if (idx === -1) return + + if (dx < 0 && idx < PAGES.length - 1) { + navigate(PAGES[idx + 1]) // swipe left → next page + } else if (dx > 0 && idx > 0) { + navigate(PAGES[idx - 1]) // swipe right → previous page + } + } + + document.addEventListener('touchstart', onTouchStart, { passive: true }) + document.addEventListener('touchmove', onTouchMove, { passive: true }) + document.addEventListener('touchend', onTouchEnd, { passive: true }) + + return () => { + document.removeEventListener('touchstart', onTouchStart) + document.removeEventListener('touchmove', onTouchMove) + document.removeEventListener('touchend', onTouchEnd) + } + }, [navigate, location.pathname]) +}