diff --git a/llms.txt b/llms.txt index c410ab6..66470a4 100644 --- a/llms.txt +++ b/llms.txt @@ -14,6 +14,7 @@ The application is structured around modular, self-contained components and uses ## Architecture - [Component Structure](src/components/README.md): Details on Atomic Component Design implementation and component organization +- [Bottom Sheet Store](src/stores/bottomSheetStore.ts): Centralized bottom sheet state management - [WebSocket Architecture](src/services/api/websocket/README.md): Comprehensive documentation of the WebSocket implementation for real-time market data and contract pricing - [State Management](src/stores/README.md): Information about Zustand store implementation and state management patterns diff --git a/package-lock.json b/package-lock.json index b099182..74e9140 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "champion-trader", "version": "0.0.0", "dependencies": { + "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-presence": "^1.1.2", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-toggle": "^1.0.3", @@ -1355,6 +1357,168 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz", + "integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.4", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz", + "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", @@ -1477,6 +1641,23 @@ } } }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", @@ -2538,6 +2719,17 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -3471,6 +3663,11 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4398,6 +4595,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -7319,6 +7524,51 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.3.tgz", @@ -7357,6 +7607,27 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8375,8 +8646,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/turbo-stream": { "version": "2.4.0", @@ -8492,6 +8762,47 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", diff --git a/package.json b/package.json index fe8ac32..84ed1bb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "test": "jest --config jest.config.cjs" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-presence": "^1.1.2", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-toggle": "^1.0.3", diff --git a/src/components/BottomSheet/BottomSheet.example.tsx b/src/components/BottomSheet/BottomSheet.example.tsx new file mode 100644 index 0000000..bfbac3c --- /dev/null +++ b/src/components/BottomSheet/BottomSheet.example.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { BottomSheet } from './BottomSheet'; +import { useBottomSheetStore } from '@/stores/bottomSheetStore'; + +export const BottomSheetExample = () => { + const { setBottomSheet } = useBottomSheetStore(); + + const examples = [ + { height: '50%', label: '50% Height' }, + { height: '380px', label: '380px Height' }, + { height: '75vh', label: '75vh Height' }, + ]; + + return ( +
+ {examples.map(({ height, label }) => ( + + ))} + + {/* The BottomSheet component should be rendered at the app root level */} + +
+ ); +}; diff --git a/src/components/BottomSheet/BottomSheet.tsx b/src/components/BottomSheet/BottomSheet.tsx new file mode 100644 index 0000000..bdd6cab --- /dev/null +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -0,0 +1,108 @@ +import { useRef, useCallback, useEffect } from "react"; +import { useBottomSheetStore } from "@/stores/bottomSheetStore"; +import { bottomSheetConfig } from "@/config/bottomSheetConfig"; + +export const BottomSheet = () => { + const { showBottomSheet, key, height, onDragDown, setBottomSheet } = useBottomSheetStore(); + + const sheetRef = useRef(null); + const dragStartY = useRef(0); + const currentY = useRef(0); + const isDragging = useRef(false); + + const handleTouchStart = useCallback((e: React.TouchEvent) => { + const touch = e.touches[0]; + dragStartY.current = touch.clientY; + currentY.current = 0; + isDragging.current = true; + }, []); + + const handleTouchMove = useCallback((e: TouchEvent) => { + if (!sheetRef.current || !isDragging.current) return; + + const touch = e.touches[0]; + const deltaY = touch.clientY - dragStartY.current; + currentY.current = deltaY; + + if (deltaY > 0) { + sheetRef.current.style.transform = `translateY(${deltaY}px)`; + onDragDown?.(); + } + }, [onDragDown]); + + const handleTouchEnd = useCallback(() => { + if (!sheetRef.current) return; + + isDragging.current = false; + sheetRef.current.style.transform = ""; + + if (currentY.current > 100) { + setBottomSheet(false); + } + }, [setBottomSheet]); + + useEffect(() => { + if (showBottomSheet) { + document.addEventListener("touchmove", handleTouchMove); + document.addEventListener("touchend", handleTouchEnd); + + return () => { + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + }; + } + }, [showBottomSheet, handleTouchMove, handleTouchEnd]); + + const body = key ? bottomSheetConfig[key]?.body : null; + + if (!showBottomSheet || !body) return null; + + // Convert percentage to vh for height if needed + const processedHeight = height.endsWith('%') + ? `${parseFloat(height)}vh` + : height; + + return ( + <> + {/* Overlay */} +
+ + {/* Sheet */} +
+ {/* Handle Bar */} +
+
+
+ + {/* Content */} +
+ {body} +
+
+ + ); +}; diff --git a/src/components/BottomSheet/README.md b/src/components/BottomSheet/README.md new file mode 100644 index 0000000..d68bb0e --- /dev/null +++ b/src/components/BottomSheet/README.md @@ -0,0 +1,122 @@ +# BottomSheet Component + +## Overview +A reusable bottom sheet component with drag-to-dismiss functionality and drag callback support. + +## Features +- Single instance pattern using Zustand store +- Dynamic height support (%, px, vh) +- Theme-aware using Tailwind CSS variables +- Drag gesture support with callback +- Content management through configuration + +## Usage + +### Basic Usage +```tsx +const { setBottomSheet } = useBottomSheetStore(); + +// Show bottom sheet +setBottomSheet(true, 'content-key', '50%'); + +// Hide bottom sheet +setBottomSheet(false); +``` + +### With Drag Callback +```tsx +const handleDragDown = () => { + console.log('Bottom sheet is being dragged down'); + // Your drag down logic here +}; + +setBottomSheet(true, 'content-key', '50%', handleDragDown); +``` + +## State Management + +```typescript +interface BottomSheetState { + showBottomSheet: boolean; // Controls visibility + key: string | null; // Content identifier + height: string; // Sheet height + onDragDown?: () => void; // Optional drag callback + setBottomSheet: ( + show: boolean, + key?: string, + height?: string, + onDragDown?: () => void + ) => void; +} +``` + +## Height Support +- Percentage: '50%' (converted to vh) +- Pixels: '380px' +- Viewport height: '75vh' + +## Gesture Handling + +### Drag to Dismiss +- Drag down on handle bar to dismiss +- Threshold: 100px vertical distance +- Smooth animation on release +- Optional callback during drag + +### Event Cleanup +- Event listeners added only when sheet is shown +- Proper cleanup on sheet close and unmount + +## Styling +Uses Tailwind CSS variables for theme support: +```tsx +className="bg-background" // Theme background +className="bg-muted" // Theme muted color +``` + +## Implementation Details + +### Touch Event Handling +```typescript +const handleTouchMove = (e: TouchEvent) => { + if (!isDragging.current) return; + + const deltaY = e.touches[0].clientY - dragStartY.current; + if (deltaY > 0) { + // Update sheet position + sheetRef.current.style.transform = `translateY(${deltaY}px)`; + // Call drag callback if provided + onDragDown?.(); + } +}; +``` + +### Height Processing +```typescript +const processedHeight = height.endsWith('%') + ? `${parseFloat(height)}vh` + : height; +``` + +## Example + +```tsx +import { useBottomSheetStore } from "@/stores/bottomSheetStore"; + +function MyComponent() { + const { setBottomSheet } = useBottomSheetStore(); + + const handleDragDown = () => { + // Handle drag down event + }; + + const showSheet = () => { + setBottomSheet(true, 'my-content', '50%', handleDragDown); + }; + + return ( + + ); +} diff --git a/src/components/BottomSheet/__tests__/BottomSheet.test.tsx b/src/components/BottomSheet/__tests__/BottomSheet.test.tsx new file mode 100644 index 0000000..c8ba321 --- /dev/null +++ b/src/components/BottomSheet/__tests__/BottomSheet.test.tsx @@ -0,0 +1,105 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { BottomSheet } from "../BottomSheet"; +import { useBottomSheetStore } from "@/stores/bottomSheetStore"; + +// Mock the store and its types +const mockUseBottomSheetStore = useBottomSheetStore as unknown as jest.Mock; +jest.mock("@/stores/bottomSheetStore", () => ({ + useBottomSheetStore: jest.fn() +})); + +// Mock the config +jest.mock("@/config/bottomSheetConfig", () => ({ + bottomSheetConfig: { + 'test-key': { + body:
Test Body Content
+ } + } +})); + +describe("BottomSheet", () => { + const mockSetBottomSheet = jest.fn(); + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + }); + + it("renders body content when showBottomSheet is true", () => { + mockUseBottomSheetStore.mockReturnValue({ + showBottomSheet: true, + key: 'test-key', + height: '380px', + setBottomSheet: mockSetBottomSheet + }); + + render(); + + expect(screen.getByText("Test Body Content")).toBeInTheDocument(); + }); + + it("does not render when showBottomSheet is false", () => { + mockUseBottomSheetStore.mockReturnValue({ + showBottomSheet: false, + key: null, + height: '380px', + setBottomSheet: mockSetBottomSheet + }); + + render(); + + expect(screen.queryByText("Test Body Content")).not.toBeInTheDocument(); + }); + + it("applies custom height from store", () => { + mockUseBottomSheetStore.mockReturnValue({ + showBottomSheet: true, + key: 'test-key', + height: '50%', + setBottomSheet: mockSetBottomSheet + }); + + const { container } = render(); + + const bottomSheet = container.querySelector('[class*="fixed bottom-0"]'); + expect(bottomSheet).toHaveStyle({ height: '50vh' }); + }); + + it("handles drag to dismiss", () => { + mockUseBottomSheetStore.mockReturnValue({ + showBottomSheet: true, + key: 'test-key', + height: '380px', + setBottomSheet: mockSetBottomSheet + }); + + const { container } = render(); + + const handleBar = container.querySelector('[class*="flex flex-col items-center"]'); + expect(handleBar).toBeInTheDocument(); + + // Simulate drag down + fireEvent.touchStart(handleBar!, { touches: [{ clientY: 0 }] }); + fireEvent.touchMove(document, { touches: [{ clientY: 150 }] }); + fireEvent.touchEnd(document); + + expect(mockSetBottomSheet).toHaveBeenCalledWith(false); + }); + + it("does not close when clicking overlay", () => { + mockUseBottomSheetStore.mockReturnValue({ + showBottomSheet: true, + key: 'test-key', + height: '380px', + setBottomSheet: mockSetBottomSheet + }); + + const { container } = render(); + + const overlay = container.querySelector('[class*="fixed inset-0"]'); + expect(overlay).toBeInTheDocument(); + fireEvent.click(overlay!); + + expect(mockSetBottomSheet).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/BottomSheet/index.ts b/src/components/BottomSheet/index.ts new file mode 100644 index 0000000..3f8a2c8 --- /dev/null +++ b/src/components/BottomSheet/index.ts @@ -0,0 +1 @@ +export { BottomSheet } from "./BottomSheet"; diff --git a/src/components/README.md b/src/components/README.md index 12ae5f5..751fd67 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -6,8 +6,9 @@ This directory contains React components following Atomic Component Design princ ``` components/ -├── AddMarketButton/ # Market selection functionality +├── AddMarketButton/ # Market selection functionality ├── BottomNav/ # Bottom navigation bar +├── BottomSheet/ # Bottom sheet ├── Chart/ # Trading chart visualization ├── DurationOptions/ # Trade duration selection ├── TradeButton/ # Trade execution controls diff --git a/src/components/TradeFields/TradeParam.tsx b/src/components/TradeFields/TradeParam.tsx index 1831326..26b6804 100644 --- a/src/components/TradeFields/TradeParam.tsx +++ b/src/components/TradeFields/TradeParam.tsx @@ -3,15 +3,21 @@ import { Card, CardContent } from "@/components/ui/card"; interface TradeParamProps { label: string; value: string; + onClick?: () => void; } -const TradeParam: React.FC = ({ label, value }) => ( - - -
{label}
-
{value}
-
-
-); +const TradeParam: React.FC = ({ label, value, onClick }) => { + return ( + + +
{label}
+
{value}
+
+
+ ); +}; export default TradeParam; diff --git a/src/config/bottomSheetConfig.tsx b/src/config/bottomSheetConfig.tsx new file mode 100644 index 0000000..d5dfe9b --- /dev/null +++ b/src/config/bottomSheetConfig.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; + +export interface BottomSheetConfig { + [key: string]: { + body: ReactNode; + }; +} + +export const bottomSheetConfig: BottomSheetConfig = { + 'stake': { + body: ( +
+
+

Stake

+
+
+ ) + } +}; diff --git a/src/screens/TradePage/TradePage.tsx b/src/screens/TradePage/TradePage.tsx index b6b1848..54259c3 100644 --- a/src/screens/TradePage/TradePage.tsx +++ b/src/screens/TradePage/TradePage.tsx @@ -1,16 +1,18 @@ -import React, { Suspense } from "react"; -import { TradeButton } from "@/components/TradeButton"; -import { Chart } from "@/components/Chart"; -import { AddMarketButton } from "@/components/AddMarketButton"; -import { DurationOptions } from "@/components/DurationOptions"; -import { useTradeStore } from "@/stores/tradeStore"; -import { Card, CardContent } from "@/components/ui/card"; -import TradeParam from "@/components/TradeFields/TradeParam"; -import ToggleButton from "@/components/TradeFields/ToggleButton"; +import React, { Suspense } from "react" +import { TradeButton } from "@/components/TradeButton" +import { Chart } from "@/components/Chart" +import { BottomSheet } from "@/components/BottomSheet" +import { AddMarketButton } from "@/components/AddMarketButton" +import { DurationOptions } from "@/components/DurationOptions" +import { useTradeStore } from "@/stores/tradeStore" +import { useBottomSheetStore } from "@/stores/bottomSheetStore" +import { Card, CardContent } from "@/components/ui/card" +import TradeParam from "@/components/TradeFields/TradeParam" +import ToggleButton from "@/components/TradeFields/ToggleButton" interface MarketInfoProps { - title: string; - subtitle: string; + title: string + subtitle: string } const MarketInfo: React.FC = ({ title, subtitle }) => ( @@ -22,10 +24,15 @@ const MarketInfo: React.FC = ({ title, subtitle }) => (
-); +) export const TradePage: React.FC = () => { - const { stake, duration, allowEquals, toggleAllowEquals } = useTradeStore(); + const { stake, duration, allowEquals, toggleAllowEquals } = useTradeStore() + const { setBottomSheet } = useBottomSheetStore() + + const handleStakeClick = () => { + setBottomSheet(true, 'stake'); + }; return (
@@ -53,7 +60,11 @@ export const TradePage: React.FC = () => {
- +
@@ -67,13 +78,15 @@ export const TradePage: React.FC = () => {
Loading...
}> - +
+ +
Loading...
}> {
+ +
- ); -}; + ) +} diff --git a/src/stores/bottomSheetStore.ts b/src/stores/bottomSheetStore.ts new file mode 100644 index 0000000..a8f3278 --- /dev/null +++ b/src/stores/bottomSheetStore.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand'; + +interface BottomSheetState { + showBottomSheet: boolean; + key: string | null; + height: string; + onDragDown?: () => void; + setBottomSheet: (show: boolean, key?: string, height?: string, onDragDown?: () => void) => void; +} + +export const useBottomSheetStore = create((set) => ({ + showBottomSheet: false, + key: null, + height: '380px', + onDragDown: undefined, + setBottomSheet: (show: boolean, key?: string, height?: string, onDragDown?: () => void) => set({ + showBottomSheet: show, + key: show ? key || null : null, + height: height || '380px', + onDragDown: onDragDown + }), +}));