diff --git a/src/App.tsx b/src/App.tsx index d6084a5..6acdf8f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { StatsBar } from './components/StatsBar' import { FilterBar } from './components/FilterBar' import { StoreCard } from './components/StoreCard' import { StoreTable } from './components/StoreTable' +import { MobileRolodex } from './components/MobileRolodex' import { StoreModal } from './components/StoreModal' import { DeleteConfirm } from './components/DeleteConfirm' import { Pagination } from './components/Pagination' @@ -185,11 +186,9 @@ export default function App() { ) : ( <> - {/* Mobile: always cards */} -
- {stores.map(s => ( - - ))} + {/* Mobile: rolodex */} +
+
{/* Desktop: toggle between table and grid */} diff --git a/src/components/MobileRolodex.tsx b/src/components/MobileRolodex.tsx new file mode 100644 index 0000000..98da624 --- /dev/null +++ b/src/components/MobileRolodex.tsx @@ -0,0 +1,186 @@ +import { useState, useRef } from 'react' +import { Pencil, Trash2, ChevronLeft, ChevronRight } from 'lucide-react' +import type { Store } from '../types' +import { RouteBadge } from './RouteBadge' +import { TagChip } from './TagChip' +import { parseTags, formatDate } from '../utils' + +interface Props { + stores: Store[] + onEdit: (store: Store) => void + onDelete: (store: Store) => void +} + +export function MobileRolodex({ stores, onEdit, onDelete }: Props) { + const [index, setIndex] = useState(0) + const [dragging, setDragging] = useState(false) + const [dragOffset, setDragOffset] = useState(0) + const touchStartX = useRef(0) + const touchStartY = useRef(0) + const isHorizontalDrag = useRef(null) + + const total = stores.length + + function goTo(i: number) { + setIndex(Math.max(0, Math.min(total - 1, i))) + setDragOffset(0) + } + + function onTouchStart(e: React.TouchEvent) { + touchStartX.current = e.touches[0].clientX + touchStartY.current = e.touches[0].clientY + isHorizontalDrag.current = null + setDragging(true) + } + + function onTouchMove(e: React.TouchEvent) { + const dx = e.touches[0].clientX - touchStartX.current + const dy = e.touches[0].clientY - touchStartY.current + + // Determine drag axis on first significant move + if (isHorizontalDrag.current === null && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) { + isHorizontalDrag.current = Math.abs(dx) > Math.abs(dy) + } + + if (isHorizontalDrag.current) { + e.preventDefault() + // Dampen drag at edges + const atStart = index === 0 && dx > 0 + const atEnd = index === total - 1 && dx < 0 + setDragOffset(atStart || atEnd ? dx * 0.2 : dx) + } + } + + function onTouchEnd() { + setDragging(false) + if (isHorizontalDrag.current && Math.abs(dragOffset) > 60) { + dragOffset < 0 ? goTo(index + 1) : goTo(index - 1) + } else { + setDragOffset(0) + } + isHorizontalDrag.current = null + } + + if (total === 0) return null + + // Positions relative to current index + function getCardStyle(offset: number): React.CSSProperties { + const x = offset * 88 + dragOffset * (offset === 0 ? 1 : 0.15) + const scale = offset === 0 ? 1 : 0.88 + const opacity = Math.abs(offset) > 1 ? 0 : offset === 0 ? 1 : 0.5 + const zIndex = offset === 0 ? 10 : Math.abs(offset) === 1 ? 5 : 0 + const rotateY = offset === 0 ? (dragOffset * -0.04) : (offset > 0 ? -6 : 6) + + return { + transform: `translateX(${x}%) scale(${scale}) rotateY(${rotateY}deg)`, + opacity, + zIndex, + transition: dragging ? 'none' : 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)', + } + } + + return ( +
+ {/* Card stack */} +
+ {/* Render prev, current, next (and one more each side for depth) */} + {[-2, -1, 0, 1, 2].map(offset => { + const cardIndex = index + offset + if (cardIndex < 0 || cardIndex >= total) return null + const store = stores[cardIndex] + const storeTagList = parseTags(store.tags) + return ( +
+ {/* Card header */} +
+
+

{store.store}

+

{store.category}

+
+ +
+ + {/* Tags */} + {storeTagList.length > 0 && ( +
+ {storeTagList.map(t => )} +
+ )} + +
+ {formatDate(store.created_at)} + {/* Actions only on current card */} + {offset === 0 && ( +
+ + +
+ )} +
+
+ ) + })} +
+ + {/* Navigation */} +
+ + + {/* Dot indicators — show up to 9, scroll window for larger sets */} +
+ {total <= 9 ? ( + Array.from({ length: total }, (_, i) => ( +
+ + +
+
+ ) +}