diff --git a/package.json b/package.json index 268f8dd..b105b02 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,17 @@ }, "dependencies": { "@radix-ui/react-popover": "^1.1.1", + "@react-spring/web": "^9.7.4", + "@use-gesture/react": "^10.3.1", "autoprefixer": "^10.4.19", "class-variance-authority": "^0.7.0", "next": "14.2.5", "react": "^18", "react-dom": "^18", "tailwind-scrollbar-hide": "^1.1.7", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "ts-pattern": "^5.2.0", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/globals.css b/src/app/globals.css index 99a7b0c..8dd7b79 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -66,4 +66,8 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file + html, + body { + touch-action: none; + } +} diff --git a/src/app/mobile/page.tsx b/src/app/mobile/page.tsx index 1ca0de5..223bf0a 100644 --- a/src/app/mobile/page.tsx +++ b/src/app/mobile/page.tsx @@ -1,5 +1,7 @@ 'use client'; +import { animated, useSpring } from '@react-spring/web'; +import { createUseGesture, dragAction, pinchAction } from '@use-gesture/react'; import Image from 'next/image'; import React from 'react'; @@ -8,9 +10,83 @@ import { cn } from '@/lib/utils'; import { ResortList, Spot } from './data'; +const useGesture = createUseGesture([pinchAction, dragAction]); + const Page = () => { const [selectedTab, setSelectedTab] = React.useState(ResortList[0]); const [selectedSpot, setSelectedSpot] = React.useState(null); + const [style, api] = useSpring(() => ({ scale: 1, x: 0, y: 0 })); + const ref = React.useRef(null); + + useGesture( + { + onPinch: ({ origin: [ox, oy], first, movement: [ms], offset: [s], memo }) => { + if (first) { + const { width, height, x, y } = ref.current!.getBoundingClientRect(); + const tx = ox - (x + width / 2); + const ty = oy - (y + height / 2); + memo = [style.x.get(), style.y.get(), tx, ty]; + } + + const x = memo[0] - (ms - 1) * memo[2]; + const y = memo[1] - (ms - 1) * memo[3]; + api.start({ scale: s, x, y }); + return memo; + }, + onPinchEnd: () => { + if (style.scale.get() < 1) { + api.start({ scale: 1, x: 0, y: 0 }); + } + }, + onDrag: ({ pinching, cancel, offset: [x, y] }) => { + if (pinching) return cancel(); + api.start({ x, y }); + }, + onDragEnd: () => { + const [boundedX, boundedY] = getBoundedPositions( + style.x.get(), + style.y.get(), + style.scale.get() + ); + api.start({ x: boundedX, y: boundedY }); + }, + }, + { + target: ref, + drag: { from: () => [style.x.get(), style.y.get()] }, + pinch: { scaleBounds: { min: 1, max: 6 }, rubberband: true }, + } + ); + + const getBoundedPositions = (x: number, y: number, scale: number): [number, number] => { + const CONTAINER = { width: 376, height: 200 }; + const IMAGE = { width: 376, height: 357 }; + const OFFSET_Y = IMAGE.height - CONTAINER.height; + + const scaledSize = { + width: IMAGE.width * scale, + height: IMAGE.height * scale, + }; + + const bounds = { + x: { + min: (CONTAINER.width - scaledSize.width) / 2, + max: -(CONTAINER.width - scaledSize.width) / 2, + }, + y: { + min: -((scaledSize.height - CONTAINER.height + OFFSET_Y) / 2), + max: Math.max((scaledSize.height - CONTAINER.height - OFFSET_Y) / 2, 0), + }, + }; + + const boundedPosition = { + x: Math.max(bounds.x.min, Math.min(bounds.x.max, x)), + y: Math.max(bounds.y.min, Math.min(bounds.y.max, y)), + }; + + return [boundedPosition.x, boundedPosition.y]; + }; + return (
@@ -32,13 +108,26 @@ const Page = () => {
tab.name === selectedTab.name)!} />
- {`${selectedTab.name}`} + + {`${selectedTab.name}`} +
console.log('hi')} + /> + {selectedSpot && (