From 86c46abe7e193af33eb647b13a8066e87465affa Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:14:17 +0800 Subject: [PATCH 1/7] BottomSheet component --- package-lock.json | 315 +++++++++++++++++- package.json | 2 + .../BottomSheet/BottomSheet.example.tsx | 30 ++ src/components/BottomSheet/BottomSheet.tsx | 101 ++++++ .../__tests__/BottomSheet.test.tsx | 89 +++++ src/components/BottomSheet/index.ts | 1 + src/config/bottomSheetConfig.tsx | 19 ++ src/screens/TradePage/TradePage.tsx | 54 +-- src/stores/bottomSheetStore.ts | 19 ++ 9 files changed, 606 insertions(+), 24 deletions(-) create mode 100644 src/components/BottomSheet/BottomSheet.example.tsx create mode 100644 src/components/BottomSheet/BottomSheet.tsx create mode 100644 src/components/BottomSheet/__tests__/BottomSheet.test.tsx create mode 100644 src/components/BottomSheet/index.ts create mode 100644 src/config/bottomSheetConfig.tsx create mode 100644 src/stores/bottomSheetStore.ts 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..d474266 --- /dev/null +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -0,0 +1,101 @@ +import { useRef, useCallback } from "react"; +import { useBottomSheetStore } from "@/stores/bottomSheetStore"; +import { bottomSheetConfig } from "@/config/bottomSheetConfig"; + +export const BottomSheet = () => { + const { showBottomSheet, key, height, 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; + + document.addEventListener("touchmove", handleTouchMove); + document.addEventListener("touchend", handleTouchEnd); + }, []); + + 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)`; + } + }, []); + + const handleTouchEnd = useCallback(() => { + if (!sheetRef.current) return; + + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + isDragging.current = false; + + sheetRef.current.style.transform = ""; + + if (currentY.current > 100) { + setBottomSheet(false); + } + }, [setBottomSheet]); + + 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/__tests__/BottomSheet.test.tsx b/src/components/BottomSheet/__tests__/BottomSheet.test.tsx new file mode 100644 index 0000000..583f9f4 --- /dev/null +++ b/src/components/BottomSheet/__tests__/BottomSheet.test.tsx @@ -0,0 +1,89 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { BottomSheet } from "../BottomSheet"; + +describe("BottomSheet", () => { + const mockHeader =
Test Header
; + const mockBody =
Test Body
; + const mockFooter =
Test Footer
; + const mockOnClose = jest.fn(); + + it("renders header, body, and footer correctly", () => { + render( + + ); + + expect(screen.getByText("Test Header")).toBeInTheDocument(); + expect(screen.getByText("Test Body")).toBeInTheDocument(); + expect(screen.getByText("Test Footer")).toBeInTheDocument(); + }); + + it("applies custom height when provided", () => { + const { container } = render( + + ); + + const bottomSheet = container.firstChild as HTMLElement; + expect(bottomSheet).toHaveStyle({ height: "50%" }); + }); + + it("applies full screen height when isFullScreen is true", () => { + const { container } = render( + + ); + + const bottomSheet = container.firstChild as HTMLElement; + expect(bottomSheet).toHaveStyle({ height: "100vh" }); + }); + + it("calls onClose when backdrop is clicked", () => { + render( + + ); + + const backdrop = screen.getByTestId("bottom-sheet-backdrop"); + fireEvent.click(backdrop); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it("does not render when isOpen is false", () => { + render( + + ); + + expect(screen.queryByText("Test Header")).not.toBeInTheDocument(); + expect(screen.queryByText("Test Body")).not.toBeInTheDocument(); + expect(screen.queryByText("Test Footer")).not.toBeInTheDocument(); + }); +}); 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/config/bottomSheetConfig.tsx b/src/config/bottomSheetConfig.tsx new file mode 100644 index 0000000..f0eb5e1 --- /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 = { + 'rise-contract': { + body: ( +
+
+

Rise Contract

+
+
+ ) + } +}; diff --git a/src/screens/TradePage/TradePage.tsx b/src/screens/TradePage/TradePage.tsx index b6b1848..f700b73 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,11 @@ 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() return (
@@ -67,13 +70,18 @@ export const TradePage: React.FC = () => {
Loading...
}> - +
setBottomSheet(true, 'rise-contract')} + > + +
Loading...
}> {
+ + - ); -}; + ) +} diff --git a/src/stores/bottomSheetStore.ts b/src/stores/bottomSheetStore.ts new file mode 100644 index 0000000..5f89a56 --- /dev/null +++ b/src/stores/bottomSheetStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +interface BottomSheetState { + showBottomSheet: boolean; + key: string | null; + height: string; + setBottomSheet: (show: boolean, key?: string, height?: string) => void; +} + +export const useBottomSheetStore = create((set) => ({ + showBottomSheet: false, + key: null, + height: '400px', + setBottomSheet: (show: boolean, key?: string, height?: string) => set({ + showBottomSheet: show, + key: show ? key || null : null, + height: height || '400px' + }), +})); From 34292e184c4cb7453948b73ea907387e259eb243 Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:48:25 +0800 Subject: [PATCH 2/7] setting it to stake --- .../__tests__/BottomSheet.test.tsx | 166 ++++++++++-------- src/components/TradeFields/TradeParam.tsx | 22 ++- src/config/bottomSheetConfig.tsx | 4 +- src/screens/TradePage/TradePage.tsx | 15 +- 4 files changed, 117 insertions(+), 90 deletions(-) diff --git a/src/components/BottomSheet/__tests__/BottomSheet.test.tsx b/src/components/BottomSheet/__tests__/BottomSheet.test.tsx index 583f9f4..c8ba321 100644 --- a/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +++ b/src/components/BottomSheet/__tests__/BottomSheet.test.tsx @@ -1,89 +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 mockHeader =
Test Header
; - const mockBody =
Test Body
; - const mockFooter =
Test Footer
; - const mockOnClose = jest.fn(); - - it("renders header, body, and footer correctly", () => { - render( - - ); - - expect(screen.getByText("Test Header")).toBeInTheDocument(); - expect(screen.getByText("Test Body")).toBeInTheDocument(); - expect(screen.getByText("Test Footer")).toBeInTheDocument(); + const mockSetBottomSheet = jest.fn(); + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); }); - it("applies custom height when provided", () => { - const { container } = render( - - ); - - const bottomSheet = container.firstChild as HTMLElement; - expect(bottomSheet).toHaveStyle({ height: "50%" }); + 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 full screen height when isFullScreen is true", () => { - const { container } = render( - - ); - - const bottomSheet = container.firstChild as HTMLElement; - expect(bottomSheet).toHaveStyle({ height: "100vh" }); + 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("calls onClose when backdrop is clicked", () => { - render( - - ); - - const backdrop = screen.getByTestId("bottom-sheet-backdrop"); - fireEvent.click(backdrop); - expect(mockOnClose).toHaveBeenCalled(); + 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 render when isOpen is false", () => { - render( - - ); - - expect(screen.queryByText("Test Header")).not.toBeInTheDocument(); - expect(screen.queryByText("Test Body")).not.toBeInTheDocument(); - expect(screen.queryByText("Test Footer")).not.toBeInTheDocument(); + 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/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 index f0eb5e1..d5dfe9b 100644 --- a/src/config/bottomSheetConfig.tsx +++ b/src/config/bottomSheetConfig.tsx @@ -7,11 +7,11 @@ export interface BottomSheetConfig { } export const bottomSheetConfig: BottomSheetConfig = { - 'rise-contract': { + 'stake': { body: (
-

Rise Contract

+

Stake

) diff --git a/src/screens/TradePage/TradePage.tsx b/src/screens/TradePage/TradePage.tsx index f700b73..54259c3 100644 --- a/src/screens/TradePage/TradePage.tsx +++ b/src/screens/TradePage/TradePage.tsx @@ -30,6 +30,10 @@ export const TradePage: React.FC = () => { const { stake, duration, allowEquals, toggleAllowEquals } = useTradeStore() const { setBottomSheet } = useBottomSheetStore() + const handleStakeClick = () => { + setBottomSheet(true, 'stake'); + }; + return (
{
- +
@@ -70,10 +78,7 @@ export const TradePage: React.FC = () => {
Loading...
}> -
setBottomSheet(true, 'rise-contract')} - > +
Date: Tue, 4 Feb 2025 12:50:30 +0800 Subject: [PATCH 3/7] trigger From 6e9d869e9e44c55539601b2882721afb4483d75e Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:24:13 +0800 Subject: [PATCH 4/7] review changes --- src/components/BottomSheet/BottomSheet.tsx | 26 ++- src/components/README.md | 216 ++++++++++++--------- 2 files changed, 137 insertions(+), 105 deletions(-) diff --git a/src/components/BottomSheet/BottomSheet.tsx b/src/components/BottomSheet/BottomSheet.tsx index d474266..29fa8c9 100644 --- a/src/components/BottomSheet/BottomSheet.tsx +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -1,4 +1,4 @@ -import { useRef, useCallback } from "react"; +import { useRef, useCallback, useEffect } from "react"; import { useBottomSheetStore } from "@/stores/bottomSheetStore"; import { bottomSheetConfig } from "@/config/bottomSheetConfig"; @@ -15,9 +15,6 @@ export const BottomSheet = () => { dragStartY.current = touch.clientY; currentY.current = 0; isDragging.current = true; - - document.addEventListener("touchmove", handleTouchMove); - document.addEventListener("touchend", handleTouchEnd); }, []); const handleTouchMove = useCallback((e: TouchEvent) => { @@ -35,10 +32,7 @@ export const BottomSheet = () => { const handleTouchEnd = useCallback(() => { if (!sheetRef.current) return; - document.removeEventListener("touchmove", handleTouchMove); - document.removeEventListener("touchend", handleTouchEnd); isDragging.current = false; - sheetRef.current.style.transform = ""; if (currentY.current > 100) { @@ -46,6 +40,18 @@ export const BottomSheet = () => { } }, [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; @@ -59,7 +65,7 @@ export const BottomSheet = () => { <> {/* Overlay */}
{/* Sheet */} @@ -72,7 +78,7 @@ export const BottomSheet = () => { max-w-[800px] w-full mx-auto - bg-white + bg-background rounded-t-[16px] animate-in fade-in-0 slide-in-from-bottom duration-300 @@ -87,7 +93,7 @@ export const BottomSheet = () => { onTouchStart={handleTouchStart} >
diff --git a/src/components/README.md b/src/components/README.md index 12ae5f5..e6b8da3 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -1,101 +1,127 @@ -# Component Architecture +# Components Documentation + +## Table of Contents +- [BottomSheet](#bottomsheet) +- [TradeParam](#tradeparam) +- [TradeButton](#tradebutton) +- [Chart](#chart) +- [DurationOptions](#durationoptions) +- [AddMarketButton](#addmarketbutton) + +## BottomSheet + +A reusable bottom sheet component with drag-to-dismiss functionality. + +### Key Features +- Single instance pattern using Zustand store +- Dynamic height support (%, px, vh) +- Theme-aware using Tailwind CSS variables +- Drag gesture support with proper event cleanup +- Content management through configuration + +### Usage + +```tsx +// 1. Configure content in bottomSheetConfig.tsx +export const bottomSheetConfig = { + 'my-key': { + body: + } +}; + +// 2. Place component at root level + + +// 3. Control from anywhere using the store +const { setBottomSheet } = useBottomSheetStore(); +setBottomSheet(true, 'my-key', '50%'); +``` + +### State Management +```typescript +interface BottomSheetState { + showBottomSheet: boolean; + key: string | null; + height: string; + setBottomSheet: (show: boolean, key?: string, height?: string) => void; +} +``` -This directory contains React components following Atomic Component Design principles. The components are organized to be modular, self-contained, and independently testable. +### Recent Changes +- Removed click-outside-to-close behavior +- Added proper event listener cleanup +- Migrated to theme-aware colors using Tailwind CSS variables +- Improved touch event handling +- Added dynamic height support -## Component Organization +## TradeParam +A card component for displaying trade parameters. + +### Usage +```tsx + ``` -components/ -├── AddMarketButton/ # Market selection functionality -├── BottomNav/ # Bottom navigation bar -├── Chart/ # Trading chart visualization -├── DurationOptions/ # Trade duration selection -├── TradeButton/ # Trade execution controls -├── TradeFields/ # Trade parameter inputs -└── ui/ # Shared UI components + +### Props +```typescript +interface TradeParamProps { + label: string; + value: string; + onClick?: () => void; +} ``` -## Design Principles - -1. **Atomic Design** - - Components are built from smallest to largest - - Each component has a single responsibility - - Components are self-contained with their own styles and logic - -2. **Styling** - - Uses TailwindCSS for consistent styling - - Styles are encapsulated within components - - Follows utility-first CSS principles - -3. **State Management** - - Local state for component-specific logic - - Zustand for shared/global state - - Props for component configuration - -4. **Testing** - - Each component has its own test suite - - Tests cover component rendering and interactions - - Mock external dependencies when needed - -## Component Guidelines - -1. **File Structure** - ``` - ComponentName/ - ├── ComponentName.tsx # Main component implementation - ├── index.ts # Public exports - └── __tests__/ # Test files - └── ComponentName.test.tsx - ``` - -2. **Component Implementation** - ```typescript - import { useState } from 'react'; - import { cn } from '@/lib/utils'; - - interface ComponentProps { - // Props interface - } - - export function Component({ ...props }: ComponentProps) { - // Implementation - } - ``` - -3. **Testing Pattern** - ```typescript - import { render, screen } from '@testing-library/react'; - import { Component } from './Component'; - - describe('Component', () => { - it('should render correctly', () => { - render(); - // Assertions - }); - }); - ``` - -## Best Practices - -1. **Component Design** - - Keep components focused and single-purpose - - Use TypeScript interfaces for props - - Implement proper error boundaries - - Handle loading and error states - -2. **Performance** - - Implement lazy loading where appropriate - - Memoize expensive calculations - - Optimize re-renders using React.memo when needed - -3. **Accessibility** - - Use semantic HTML elements - - Include ARIA attributes where necessary - - Ensure keyboard navigation support - - Maintain proper color contrast - -4. **Code Quality** - - Follow consistent naming conventions - - Document complex logic - - Write comprehensive tests - - Use proper TypeScript types +### Recent Changes +- Made component purely presentational +- Added optional onClick handler +- Cursor pointer only shows when onClick is provided + +## TradeButton + +[Documentation for TradeButton component] + +## Chart + +[Documentation for Chart component] + +## DurationOptions + +[Documentation for DurationOptions component] + +## AddMarketButton + +[Documentation for AddMarketButton component] + +--- + +## Component Design Principles + +### 1. State Management +- Use Zustand for global state +- Keep components as pure as possible +- Pass event handlers from parent components + +### 2. Styling +- Use Tailwind CSS with theme variables +- Follow design system color tokens +- Ensure dark mode compatibility + +### 3. Performance +- Implement proper cleanup +- Use React.memo where beneficial +- Lazy load when appropriate + +### 4. Accessibility +- Follow WCAG guidelines +- Ensure proper keyboard navigation +- Maintain appropriate color contrast + +### 5. Testing +- Write comprehensive unit tests +- Test edge cases +- Mock external dependencies From b00165bb73364d2fc909ea8c4f2cee4f97895965 Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:35:22 +0800 Subject: [PATCH 5/7] readme fix --- src/components/BottomSheet/README.md | 167 +++++++++++++++++++++ src/components/README.md | 217 ++++++++++++--------------- 2 files changed, 263 insertions(+), 121 deletions(-) create mode 100644 src/components/BottomSheet/README.md diff --git a/src/components/BottomSheet/README.md b/src/components/BottomSheet/README.md new file mode 100644 index 0000000..44a58a5 --- /dev/null +++ b/src/components/BottomSheet/README.md @@ -0,0 +1,167 @@ +# BottomSheet Component + +## Overview +A bottom sheet component that slides up from the bottom of the screen with drag-to-dismiss functionality. Uses Zustand for state management and a configuration-based approach for content. + +## Internal Working + +### 1. State Management +```typescript +// bottomSheetStore.ts +interface BottomSheetState { + showBottomSheet: boolean; // Controls visibility + key: string | null; // Content identifier + height: string; // Sheet height + setBottomSheet: (show: boolean, key?: string, height?: string) => void; +} +``` + +The component uses Zustand to maintain a single source of truth for: +- Visibility state +- Current content key +- Sheet height + +### 2. Content Configuration +```typescript +// bottomSheetConfig.tsx +interface BottomSheetConfig { + [key: string]: { + body: ReactNode; + } +} +``` + +Content is configured through a central config file, allowing for: +- Reusable content definitions +- Type-safe content management +- Easy content updates + +### 3. Gesture Handling + +#### Touch Start +```typescript +const handleTouchStart = (e: React.TouchEvent) => { + dragStartY.current = e.touches[0].clientY; + currentY.current = 0; + isDragging.current = true; +} +``` +- Captures initial touch position +- Sets up dragging state + +#### Touch Move +```typescript +const handleTouchMove = (e: TouchEvent) => { + if (!isDragging.current) return; + + const deltaY = e.touches[0].clientY - dragStartY.current; + if (deltaY > 0) { + sheetRef.current.style.transform = `translateY(${deltaY}px)`; + } +} +``` +- Calculates drag distance +- Updates sheet position +- Only allows downward dragging + +#### Touch End +```typescript +const handleTouchEnd = () => { + if (currentY.current > 100) { + setBottomSheet(false); + } + // Reset position and state +} +``` +- Checks if drag distance exceeds threshold +- Closes sheet if threshold met +- Resets position and state + +### 4. Event Cleanup +```typescript +useEffect(() => { + if (showBottomSheet) { + document.addEventListener("touchmove", handleTouchMove); + document.addEventListener("touchend", handleTouchEnd); + + return () => { + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + }; + } +}, [showBottomSheet, handleTouchMove, handleTouchEnd]); +``` +- Adds event listeners when sheet is shown +- Removes listeners when sheet is closed +- Prevents memory leaks + +### 5. Height Management +```typescript +const processedHeight = height.endsWith('%') + ? `${parseFloat(height)}vh` + : height; +``` +Supports multiple height formats: +- Percentage (converted to vh) +- Pixels +- Viewport height + +### 6. Styling +Uses Tailwind CSS variables for theme-aware styling: +```tsx +
// Theme background +
// Theme muted color +``` + +## Usage Example + +```tsx +// 1. Configure content +// bottomSheetConfig.tsx +export const bottomSheetConfig = { + 'trade-options': { + body: + } +}; + +// 2. Place component +// App.tsx + + +// 3. Control sheet +// AnyComponent.tsx +const { setBottomSheet } = useBottomSheetStore(); + +// Open with 50% height +setBottomSheet(true, 'trade-options', '50%'); + +// Close +setBottomSheet(false); +``` + +## Key Features + +1. **Single Instance** + - One bottom sheet instance for entire app + - Content switched through configuration + - Prevents multiple sheets + +2. **Gesture Support** + - Drag to dismiss + - Smooth animations + - Touch event cleanup + +3. **Theme Integration** + - Uses Tailwind CSS variables + - Dark mode support + - Consistent styling + +4. **Dynamic Height** + - Percentage values + - Pixel values + - Viewport height + +5. **Performance** + - Event listener cleanup + - Optimized re-renders + - Efficient state updates diff --git a/src/components/README.md b/src/components/README.md index e6b8da3..751fd67 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -1,127 +1,102 @@ -# Components Documentation - -## Table of Contents -- [BottomSheet](#bottomsheet) -- [TradeParam](#tradeparam) -- [TradeButton](#tradebutton) -- [Chart](#chart) -- [DurationOptions](#durationoptions) -- [AddMarketButton](#addmarketbutton) - -## BottomSheet - -A reusable bottom sheet component with drag-to-dismiss functionality. - -### Key Features -- Single instance pattern using Zustand store -- Dynamic height support (%, px, vh) -- Theme-aware using Tailwind CSS variables -- Drag gesture support with proper event cleanup -- Content management through configuration - -### Usage - -```tsx -// 1. Configure content in bottomSheetConfig.tsx -export const bottomSheetConfig = { - 'my-key': { - body: - } -}; - -// 2. Place component at root level - - -// 3. Control from anywhere using the store -const { setBottomSheet } = useBottomSheetStore(); -setBottomSheet(true, 'my-key', '50%'); -``` - -### State Management -```typescript -interface BottomSheetState { - showBottomSheet: boolean; - key: string | null; - height: string; - setBottomSheet: (show: boolean, key?: string, height?: string) => void; -} -``` +# Component Architecture -### Recent Changes -- Removed click-outside-to-close behavior -- Added proper event listener cleanup -- Migrated to theme-aware colors using Tailwind CSS variables -- Improved touch event handling -- Added dynamic height support +This directory contains React components following Atomic Component Design principles. The components are organized to be modular, self-contained, and independently testable. -## TradeParam +## Component Organization -A card component for displaying trade parameters. - -### Usage -```tsx - ``` - -### Props -```typescript -interface TradeParamProps { - label: string; - value: string; - onClick?: () => void; -} +components/ +├── AddMarketButton/ # Market selection functionality +├── BottomNav/ # Bottom navigation bar +├── BottomSheet/ # Bottom sheet +├── Chart/ # Trading chart visualization +├── DurationOptions/ # Trade duration selection +├── TradeButton/ # Trade execution controls +├── TradeFields/ # Trade parameter inputs +└── ui/ # Shared UI components ``` -### Recent Changes -- Made component purely presentational -- Added optional onClick handler -- Cursor pointer only shows when onClick is provided - -## TradeButton - -[Documentation for TradeButton component] - -## Chart - -[Documentation for Chart component] - -## DurationOptions - -[Documentation for DurationOptions component] - -## AddMarketButton - -[Documentation for AddMarketButton component] - ---- - -## Component Design Principles - -### 1. State Management -- Use Zustand for global state -- Keep components as pure as possible -- Pass event handlers from parent components - -### 2. Styling -- Use Tailwind CSS with theme variables -- Follow design system color tokens -- Ensure dark mode compatibility - -### 3. Performance -- Implement proper cleanup -- Use React.memo where beneficial -- Lazy load when appropriate - -### 4. Accessibility -- Follow WCAG guidelines -- Ensure proper keyboard navigation -- Maintain appropriate color contrast - -### 5. Testing -- Write comprehensive unit tests -- Test edge cases -- Mock external dependencies +## Design Principles + +1. **Atomic Design** + - Components are built from smallest to largest + - Each component has a single responsibility + - Components are self-contained with their own styles and logic + +2. **Styling** + - Uses TailwindCSS for consistent styling + - Styles are encapsulated within components + - Follows utility-first CSS principles + +3. **State Management** + - Local state for component-specific logic + - Zustand for shared/global state + - Props for component configuration + +4. **Testing** + - Each component has its own test suite + - Tests cover component rendering and interactions + - Mock external dependencies when needed + +## Component Guidelines + +1. **File Structure** + ``` + ComponentName/ + ├── ComponentName.tsx # Main component implementation + ├── index.ts # Public exports + └── __tests__/ # Test files + └── ComponentName.test.tsx + ``` + +2. **Component Implementation** + ```typescript + import { useState } from 'react'; + import { cn } from '@/lib/utils'; + + interface ComponentProps { + // Props interface + } + + export function Component({ ...props }: ComponentProps) { + // Implementation + } + ``` + +3. **Testing Pattern** + ```typescript + import { render, screen } from '@testing-library/react'; + import { Component } from './Component'; + + describe('Component', () => { + it('should render correctly', () => { + render(); + // Assertions + }); + }); + ``` + +## Best Practices + +1. **Component Design** + - Keep components focused and single-purpose + - Use TypeScript interfaces for props + - Implement proper error boundaries + - Handle loading and error states + +2. **Performance** + - Implement lazy loading where appropriate + - Memoize expensive calculations + - Optimize re-renders using React.memo when needed + +3. **Accessibility** + - Use semantic HTML elements + - Include ARIA attributes where necessary + - Ensure keyboard navigation support + - Maintain proper color contrast + +4. **Code Quality** + - Follow consistent naming conventions + - Document complex logic + - Write comprehensive tests + - Use proper TypeScript types From 486ae856ec00d5340851da06a5299b7707100e55 Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:46:47 +0800 Subject: [PATCH 6/7] adding refs in llms.txt --- llms.txt | 1 + 1 file changed, 1 insertion(+) 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 From 90eb2edbfd44c35d53dd3b03f93ed32e408c62d0 Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:14:08 +0800 Subject: [PATCH 7/7] add onDragDown callback --- src/components/BottomSheet/BottomSheet.tsx | 5 +- src/components/BottomSheet/README.md | 209 ++++++++------------- src/stores/bottomSheetStore.ts | 11 +- 3 files changed, 92 insertions(+), 133 deletions(-) diff --git a/src/components/BottomSheet/BottomSheet.tsx b/src/components/BottomSheet/BottomSheet.tsx index 29fa8c9..bdd6cab 100644 --- a/src/components/BottomSheet/BottomSheet.tsx +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -3,7 +3,7 @@ import { useBottomSheetStore } from "@/stores/bottomSheetStore"; import { bottomSheetConfig } from "@/config/bottomSheetConfig"; export const BottomSheet = () => { - const { showBottomSheet, key, height, setBottomSheet } = useBottomSheetStore(); + const { showBottomSheet, key, height, onDragDown, setBottomSheet } = useBottomSheetStore(); const sheetRef = useRef(null); const dragStartY = useRef(0); @@ -26,8 +26,9 @@ export const BottomSheet = () => { if (deltaY > 0) { sheetRef.current.style.transform = `translateY(${deltaY}px)`; + onDragDown?.(); } - }, []); + }, [onDragDown]); const handleTouchEnd = useCallback(() => { if (!sheetRef.current) return; diff --git a/src/components/BottomSheet/README.md b/src/components/BottomSheet/README.md index 44a58a5..d68bb0e 100644 --- a/src/components/BottomSheet/README.md +++ b/src/components/BottomSheet/README.md @@ -1,167 +1,122 @@ # BottomSheet Component ## Overview -A bottom sheet component that slides up from the bottom of the screen with drag-to-dismiss functionality. Uses Zustand for state management and a configuration-based approach for content. +A reusable bottom sheet component with drag-to-dismiss functionality and drag callback support. -## Internal Working +## 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 -### 1. State Management ```typescript -// bottomSheetStore.ts interface BottomSheetState { showBottomSheet: boolean; // Controls visibility key: string | null; // Content identifier height: string; // Sheet height - setBottomSheet: (show: boolean, key?: string, height?: string) => void; + onDragDown?: () => void; // Optional drag callback + setBottomSheet: ( + show: boolean, + key?: string, + height?: string, + onDragDown?: () => void + ) => void; } ``` -The component uses Zustand to maintain a single source of truth for: -- Visibility state -- Current content key -- Sheet height +## Height Support +- Percentage: '50%' (converted to vh) +- Pixels: '380px' +- Viewport height: '75vh' -### 2. Content Configuration -```typescript -// bottomSheetConfig.tsx -interface BottomSheetConfig { - [key: string]: { - body: ReactNode; - } -} -``` +## Gesture Handling -Content is configured through a central config file, allowing for: -- Reusable content definitions -- Type-safe content management -- Easy content updates +### Drag to Dismiss +- Drag down on handle bar to dismiss +- Threshold: 100px vertical distance +- Smooth animation on release +- Optional callback during drag -### 3. Gesture Handling +### Event Cleanup +- Event listeners added only when sheet is shown +- Proper cleanup on sheet close and unmount -#### Touch Start -```typescript -const handleTouchStart = (e: React.TouchEvent) => { - dragStartY.current = e.touches[0].clientY; - currentY.current = 0; - isDragging.current = true; -} +## Styling +Uses Tailwind CSS variables for theme support: +```tsx +className="bg-background" // Theme background +className="bg-muted" // Theme muted color ``` -- Captures initial touch position -- Sets up dragging state -#### Touch Move +## 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?.(); } -} -``` -- Calculates drag distance -- Updates sheet position -- Only allows downward dragging - -#### Touch End -```typescript -const handleTouchEnd = () => { - if (currentY.current > 100) { - setBottomSheet(false); - } - // Reset position and state -} -``` -- Checks if drag distance exceeds threshold -- Closes sheet if threshold met -- Resets position and state - -### 4. Event Cleanup -```typescript -useEffect(() => { - if (showBottomSheet) { - document.addEventListener("touchmove", handleTouchMove); - document.addEventListener("touchend", handleTouchEnd); - - return () => { - document.removeEventListener("touchmove", handleTouchMove); - document.removeEventListener("touchend", handleTouchEnd); - }; - } -}, [showBottomSheet, handleTouchMove, handleTouchEnd]); +}; ``` -- Adds event listeners when sheet is shown -- Removes listeners when sheet is closed -- Prevents memory leaks -### 5. Height Management +### Height Processing ```typescript const processedHeight = height.endsWith('%') ? `${parseFloat(height)}vh` : height; ``` -Supports multiple height formats: -- Percentage (converted to vh) -- Pixels -- Viewport height - -### 6. Styling -Uses Tailwind CSS variables for theme-aware styling: -```tsx -
// Theme background -
// Theme muted color -``` -## Usage Example +## Example ```tsx -// 1. Configure content -// bottomSheetConfig.tsx -export const bottomSheetConfig = { - 'trade-options': { - body: - } -}; - -// 2. Place component -// App.tsx - - -// 3. Control sheet -// AnyComponent.tsx -const { setBottomSheet } = useBottomSheetStore(); - -// Open with 50% height -setBottomSheet(true, 'trade-options', '50%'); +import { useBottomSheetStore } from "@/stores/bottomSheetStore"; -// Close -setBottomSheet(false); -``` - -## Key Features +function MyComponent() { + const { setBottomSheet } = useBottomSheetStore(); -1. **Single Instance** - - One bottom sheet instance for entire app - - Content switched through configuration - - Prevents multiple sheets + const handleDragDown = () => { + // Handle drag down event + }; -2. **Gesture Support** - - Drag to dismiss - - Smooth animations - - Touch event cleanup + const showSheet = () => { + setBottomSheet(true, 'my-content', '50%', handleDragDown); + }; -3. **Theme Integration** - - Uses Tailwind CSS variables - - Dark mode support - - Consistent styling - -4. **Dynamic Height** - - Percentage values - - Pixel values - - Viewport height - -5. **Performance** - - Event listener cleanup - - Optimized re-renders - - Efficient state updates + return ( + + ); +} diff --git a/src/stores/bottomSheetStore.ts b/src/stores/bottomSheetStore.ts index 5f89a56..a8f3278 100644 --- a/src/stores/bottomSheetStore.ts +++ b/src/stores/bottomSheetStore.ts @@ -4,16 +4,19 @@ interface BottomSheetState { showBottomSheet: boolean; key: string | null; height: string; - setBottomSheet: (show: boolean, key?: string, height?: string) => void; + onDragDown?: () => void; + setBottomSheet: (show: boolean, key?: string, height?: string, onDragDown?: () => void) => void; } export const useBottomSheetStore = create((set) => ({ showBottomSheet: false, key: null, - height: '400px', - setBottomSheet: (show: boolean, key?: string, height?: string) => set({ + height: '380px', + onDragDown: undefined, + setBottomSheet: (show: boolean, key?: string, height?: string, onDragDown?: () => void) => set({ showBottomSheet: show, key: show ? key || null : null, - height: height || '400px' + height: height || '380px', + onDragDown: onDragDown }), }));