diff --git a/public/fall.png b/public/fall.png new file mode 100644 index 0000000..64b660e Binary files /dev/null and b/public/fall.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..2241778 Binary files /dev/null and b/public/logo.png differ diff --git a/public/rise.png b/public/rise.png new file mode 100644 index 0000000..81dce09 Binary files /dev/null and b/public/rise.png differ diff --git a/src/components/BalanceDisplay/BalanceDisplay.tsx b/src/components/BalanceDisplay/BalanceDisplay.tsx index ef89889..413c165 100644 --- a/src/components/BalanceDisplay/BalanceDisplay.tsx +++ b/src/components/BalanceDisplay/BalanceDisplay.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { useClientStore } from '@/stores/clientStore'; -import { useOrientationStore } from '@/stores/orientationStore'; +import React from "react"; +import { useClientStore } from "@/stores/clientStore"; +import { useOrientationStore } from "@/stores/orientationStore"; interface BalanceDisplayProps { onDeposit?: () => void; @@ -11,34 +11,53 @@ interface BalanceDisplayProps { export const BalanceDisplay: React.FC = ({ onDeposit, - depositLabel = 'Deposit', - className = '', - loginUrl = '/login', + depositLabel = "Deposit", + className = "", + loginUrl = "/login", }) => { const { isLoggedIn, balance, currency } = useClientStore(); const { isLandscape } = useOrientationStore(); if (!isLoggedIn) { return ( -
- + {!isLandscape && ( + + Champion Trader Logo + + )} +
- Log in - -
+ + Log in + +
+ ); } return ( -
+
- Real - {balance} {currency} + Real + + {balance} {currency} +
- - + )} + ); diff --git a/src/components/BottomNav/__tests__/BottomNav.test.tsx b/src/components/BottomNav/__tests__/BottomNav.test.tsx index 5998850..90ace36 100644 --- a/src/components/BottomNav/__tests__/BottomNav.test.tsx +++ b/src/components/BottomNav/__tests__/BottomNav.test.tsx @@ -27,12 +27,6 @@ describe('BottomNav', () => { useClientStore.getState().isLoggedIn = false; }); - it('does not render navigation button when user is logged out', () => { - renderWithRouter(); - // Expect that the navigation button with test ID "bottom-nav-menu" is not present. - expect(screen.queryByTestId('bottom-nav-menu')).toBeNull(); - }); - it('renders navigation button when user is logged in', () => { useClientStore.getState().isLoggedIn = true; renderWithRouter(); diff --git a/src/components/DurationOptions/DurationOptions.tsx b/src/components/DurationOptions/DurationOptions.tsx index 7e96e5e..48dfc72 100644 --- a/src/components/DurationOptions/DurationOptions.tsx +++ b/src/components/DurationOptions/DurationOptions.tsx @@ -1,4 +1,4 @@ -import { BarChart2, Clock, Expand } from 'lucide-react'; +import { AreaChart } from 'lucide-react'; interface DurationOptionsProps { className?: string; @@ -6,7 +6,13 @@ interface DurationOptionsProps { export const DurationOptions: React.FC = ({ className = '' }) => { return ( -
+
+
+ +
+
@@ -14,17 +20,6 @@ export const DurationOptions: React.FC = ({ className = ''
-
- - - -
); }; diff --git a/src/components/DurationOptions/__tests__/DurationOptions.test.tsx b/src/components/DurationOptions/__tests__/DurationOptions.test.tsx index 5deaef3..0eb69b6 100644 --- a/src/components/DurationOptions/__tests__/DurationOptions.test.tsx +++ b/src/components/DurationOptions/__tests__/DurationOptions.test.tsx @@ -16,7 +16,5 @@ describe('DurationOptions', () => { render(); expect(screen.getByRole('button', { name: /chart/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /clock/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /expand/i })).toBeInTheDocument(); }); }); diff --git a/src/components/HowToTrade/HowToTrade.tsx b/src/components/HowToTrade/HowToTrade.tsx new file mode 100644 index 0000000..24689b3 --- /dev/null +++ b/src/components/HowToTrade/HowToTrade.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { useBottomSheetStore } from "@/stores/bottomSheetStore"; + +export const HowToTrade: React.FC = () => { + const { setBottomSheet } = useBottomSheetStore(); + + const handleClick = () => { + setBottomSheet(true, "how-to-trade", '800px'); + }; + + return ( + + ); +}; + +export default HowToTrade; diff --git a/src/components/HowToTrade/__tests__/HowToTrade.test.tsx b/src/components/HowToTrade/__tests__/HowToTrade.test.tsx new file mode 100644 index 0000000..3541e82 --- /dev/null +++ b/src/components/HowToTrade/__tests__/HowToTrade.test.tsx @@ -0,0 +1,28 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import HowToTrade from '../HowToTrade'; + +const mockSetBottomSheet = jest.fn(); + +// Mock the bottomSheetStore +jest.mock('@/stores/bottomSheetStore', () => ({ + useBottomSheetStore: () => ({ + setBottomSheet: mockSetBottomSheet + }) +})); + +describe('HowToTrade', () => { + beforeEach(() => { + mockSetBottomSheet.mockClear(); + }); + + it('renders correctly', () => { + render(); + expect(screen.getByText('How to trade Rise/Fall?')).toBeInTheDocument(); + }); + + it('opens bottom sheet when clicked', () => { + render(); + fireEvent.click(screen.getByText('How to trade Rise/Fall?')); + expect(mockSetBottomSheet).toHaveBeenCalledWith(true, 'how-to-trade', '800px'); + }); +}); diff --git a/src/components/HowToTrade/index.ts b/src/components/HowToTrade/index.ts new file mode 100644 index 0000000..b12d982 --- /dev/null +++ b/src/components/HowToTrade/index.ts @@ -0,0 +1 @@ +export { default } from './HowToTrade'; diff --git a/src/components/SideNav/SideNav.tsx b/src/components/SideNav/SideNav.tsx index c43fab2..4955a83 100644 --- a/src/components/SideNav/SideNav.tsx +++ b/src/components/SideNav/SideNav.tsx @@ -11,38 +11,49 @@ export const SideNav: React.FC = () => { const { isLandscape } = useOrientationStore(); const handleMenuClick = () => { - navigate(location.pathname === '/menu' ? '/trade' : '/menu'); + navigate(location.pathname === "/menu" ? "/trade" : "/menu"); }; return ( -
); diff --git a/src/layouts/MainLayout/__tests__/Footer.test.tsx b/src/layouts/MainLayout/__tests__/Footer.test.tsx index 3832bfa..c1dcffd 100644 --- a/src/layouts/MainLayout/__tests__/Footer.test.tsx +++ b/src/layouts/MainLayout/__tests__/Footer.test.tsx @@ -22,12 +22,6 @@ describe('Footer', () => { useClientStore.getState().isLoggedIn = false; }); - it('does not render when user is logged out', () => { - renderWithRouter(); - // In logout view, Footer should not render, so "Menu" should not be present. - expect(screen.queryByText('Menu')).toBeNull(); - }); - it('renders navigation items when user is logged in', () => { useClientStore.getState().isLoggedIn = true; renderWithRouter(); diff --git a/src/screens/MenuPage/MenuPage.tsx b/src/screens/MenuPage/MenuPage.tsx index f5876b8..cd602b3 100644 --- a/src/screens/MenuPage/MenuPage.tsx +++ b/src/screens/MenuPage/MenuPage.tsx @@ -1,37 +1,49 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import { useClientStore } from "@/stores/clientStore"; +import ToggleButton from "@/components/TradeFields/ToggleButton"; +import { Home, Moon, LogOut, ExternalLink } from "lucide-react"; export const MenuPage: React.FC = () => { const navigate = useNavigate(); const { logout, isLoggedIn } = useClientStore(); const handleLogout = () => { - localStorage.removeItem('loginToken'); + localStorage.removeItem("loginToken"); logout(); - navigate('/trade'); + navigate("/trade"); }; return (

Menu

- - - +
+ + Theme + {}} + /> +
{isLoggedIn && ( <> -
- diff --git a/src/screens/MenuPage/__tests__/MenuPage.test.tsx b/src/screens/MenuPage/__tests__/MenuPage.test.tsx index 4a04a32..1f8ddba 100644 --- a/src/screens/MenuPage/__tests__/MenuPage.test.tsx +++ b/src/screens/MenuPage/__tests__/MenuPage.test.tsx @@ -14,9 +14,8 @@ describe('MenuPage', () => { it('renders menu items', () => { renderWithRouter(); - expect(screen.getByText('Settings')).toBeInTheDocument(); - expect(screen.getByText('Help Center')).toBeInTheDocument(); - expect(screen.getByText('About')).toBeInTheDocument(); + expect(screen.getByText('Go to Home')).toBeInTheDocument(); + expect(screen.getByText('Theme')).toBeInTheDocument(); }); it('renders page title', () => { diff --git a/src/screens/TradePage/TradePage.tsx b/src/screens/TradePage/TradePage.tsx index a707fab..64bc184 100644 --- a/src/screens/TradePage/TradePage.tsx +++ b/src/screens/TradePage/TradePage.tsx @@ -1,12 +1,11 @@ -import React, { Suspense, lazy } from "react" -import { useOrientationStore } from "@/stores/orientationStore" -import { BalanceDisplay } from "@/components/BalanceDisplay" -import { BottomSheet } from "@/components/BottomSheet" -import { AddMarketButton } from "@/components/AddMarketButton" -import { DurationOptions } from "@/components/DurationOptions" -import { Card, CardContent } from "@/components/ui/card" -import { TradeFormController } from "./components/TradeFormController" - +import React, { Suspense, lazy } from "react"; +import { useOrientationStore } from "@/stores/orientationStore"; +import { BalanceDisplay } from "@/components/BalanceDisplay"; +import { BottomSheet } from "@/components/BottomSheet"; +// import { AddMarketButton } from "@/components/AddMarketButton"; +import { DurationOptions } from "@/components/DurationOptions"; +import { Card, CardContent } from "@/components/ui/card"; +import { TradeFormController } from "./components/TradeFormController"; const Chart = lazy(() => import("@/components/Chart").then((module) => ({ @@ -15,8 +14,8 @@ const Chart = lazy(() => ); interface MarketInfoProps { - title: string - subtitle: string + title: string; + subtitle: string; } const MarketInfo: React.FC = ({ title, subtitle }) => ( @@ -28,13 +27,18 @@ const MarketInfo: React.FC = ({ title, subtitle }) => (
-) +); export const TradePage: React.FC = () => { - const { isLandscape } = useOrientationStore() - + const { isLandscape } = useOrientationStore(); + return ( -
+
{isLandscape && (
{ >
- Loading...
}> + {/* Loading...
}> - + */}
@@ -52,20 +56,24 @@ export const TradePage: React.FC = () => {
)} -
+
{!isLandscape && (
- Loading...
}> + {/* Loading...
}> - + */}
)}
-
+
Loading...
}> @@ -81,5 +89,5 @@ export const TradePage: React.FC = () => {
- ) -} + ); +}; diff --git a/src/screens/TradePage/__tests__/TradePage.test.tsx b/src/screens/TradePage/__tests__/TradePage.test.tsx index b73f4ed..e752330 100644 --- a/src/screens/TradePage/__tests__/TradePage.test.tsx +++ b/src/screens/TradePage/__tests__/TradePage.test.tsx @@ -145,7 +145,6 @@ describe('TradePage', () => { // Balance display should not be visible in portrait mode expect(screen.queryByTestId('balance-display')).not.toBeInTheDocument(); expect(screen.getByTestId('bottom-sheet')).toBeInTheDocument(); - expect(screen.getByTestId('add-market-button')).toBeInTheDocument(); expect(screen.getByTestId('duration-options')).toBeInTheDocument(); // Check layout classes @@ -163,7 +162,6 @@ describe('TradePage', () => { // Balance display should be visible in landscape mode expect(screen.getByTestId('balance-display')).toBeInTheDocument(); expect(screen.getByTestId('bottom-sheet')).toBeInTheDocument(); - expect(screen.getByTestId('add-market-button')).toBeInTheDocument(); expect(screen.getByTestId('duration-options')).toBeInTheDocument(); // Check layout classes diff --git a/src/screens/TradePage/components/TradeFormController.tsx b/src/screens/TradePage/components/TradeFormController.tsx index 6c65ad4..6fa8811 100644 --- a/src/screens/TradePage/components/TradeFormController.tsx +++ b/src/screens/TradePage/components/TradeFormController.tsx @@ -1,37 +1,38 @@ -import React, { Suspense, lazy, useEffect, useState } from "react" -import { TradeButton } from "@/components/TradeButton" -import { ResponsiveTradeParamLayout } from "@/components/ui/responsive-trade-param-layout" -import { MobileTradeFieldCard } from "@/components/ui/mobile-trade-field-card" -import { DesktopTradeFieldCard } from "@/components/ui/desktop-trade-field-card" -import { useTradeStore } from "@/stores/tradeStore" -import { tradeTypeConfigs } from "@/config/tradeTypes" -import { useTradeActions } from "@/hooks/useTradeActions" -import { parseDuration, formatDuration } from "@/utils/duration" -import { createSSEConnection } from "@/services/api/sse/createSSEConnection" -import { useClientStore } from "@/stores/clientStore" -import { WebSocketError } from "@/services/api/websocket/types" +import React, { Suspense, lazy, useEffect, useState } from "react"; +import { TradeButton } from "@/components/TradeButton"; +import { ResponsiveTradeParamLayout } from "@/components/ui/responsive-trade-param-layout"; +import { MobileTradeFieldCard } from "@/components/ui/mobile-trade-field-card"; +import { DesktopTradeFieldCard } from "@/components/ui/desktop-trade-field-card"; +import { useTradeStore } from "@/stores/tradeStore"; +import { tradeTypeConfigs } from "@/config/tradeTypes"; +import { useTradeActions } from "@/hooks/useTradeActions"; +import { parseDuration, formatDuration } from "@/utils/duration"; +import { createSSEConnection } from "@/services/api/sse/createSSEConnection"; +import { useClientStore } from "@/stores/clientStore"; +import { WebSocketError } from "@/services/api/websocket/types"; +import HowToTrade from "@/components/HowToTrade"; // Lazy load components -const DurationField = lazy(() => - import("@/components/Duration").then(module => ({ - default: module.DurationField +const DurationField = lazy(() => + import("@/components/Duration").then((module) => ({ + default: module.DurationField, })) ); -const StakeField = lazy(() => - import("@/components/Stake").then(module => ({ - default: module.StakeField +const StakeField = lazy(() => + import("@/components/Stake").then((module) => ({ + default: module.StakeField, })) ); -const EqualTradeController = lazy(() => - import("@/components/EqualTrade").then(module => ({ - default: module.EqualTradeController +const EqualTradeController = lazy(() => + import("@/components/EqualTrade").then((module) => ({ + default: module.EqualTradeController, })) ); interface TradeFormControllerProps { - isLandscape: boolean + isLandscape: boolean; } interface ButtonState { @@ -43,7 +44,9 @@ interface ButtonState { type ButtonStates = Record; -export const TradeFormController: React.FC = ({ isLandscape }) => { +export const TradeFormController: React.FC = ({ + isLandscape, +}) => { const { trade_type, duration, setPayouts, stake } = useTradeStore(); const { token, currency } = useClientStore(); const tradeActions = useTradeActions(); @@ -52,101 +55,109 @@ export const TradeFormController: React.FC = ({ isLand const [buttonStates, setButtonStates] = useState(() => { // Initialize states for all buttons in the current trade type const initialStates: ButtonStates = {}; - tradeTypeConfigs[trade_type].buttons.forEach(button => { + tradeTypeConfigs[trade_type].buttons.forEach((button) => { initialStates[button.actionName] = { loading: true, error: null, payout: 0, - reconnecting: false + reconnecting: false, }; }); return initialStates; }); // Parse duration for API call - const { value: apiDurationValue, type: apiDurationType } = parseDuration(duration); + const { value: apiDurationValue, type: apiDurationType } = + parseDuration(duration); useEffect(() => { // Create SSE connections for each button's contract type - const cleanupFunctions = tradeTypeConfigs[trade_type].buttons.map(button => { - return createSSEConnection({ - params: { - action: 'contract_price', - duration: formatDuration(Number(apiDurationValue), apiDurationType), - trade_type: button.contractType, - instrument: "R_100", - currency: currency, - payout: stake, - strike: stake - }, - headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, - onMessage: (priceData) => { - // Update button state for this specific button - setButtonStates(prev => ({ - ...prev, - [button.actionName]: { - loading: false, - error: null, - payout: Number(priceData.price), - reconnecting: false - } - })); + const cleanupFunctions = tradeTypeConfigs[trade_type].buttons.map( + (button) => { + return createSSEConnection({ + params: { + action: "contract_price", + duration: formatDuration(Number(apiDurationValue), apiDurationType), + trade_type: button.contractType, + instrument: "R_100", + currency: currency, + payout: stake, + strike: stake, + }, + headers: token ? { Authorization: `Bearer ${token}` } : undefined, + onMessage: (priceData) => { + // Update button state for this specific button + setButtonStates((prev) => ({ + ...prev, + [button.actionName]: { + loading: false, + error: null, + payout: Number(priceData.price), + reconnecting: false, + }, + })); - // Update payouts in store - const payoutValue = Number(priceData.price); - - // Create a map of button action names to their payout values - const payoutValues = Object.keys(buttonStates).reduce((acc, key) => { - acc[key] = key === button.actionName ? payoutValue : buttonStates[key]?.payout || 0; - return acc; - }, {} as Record); + // Update payouts in store + const payoutValue = Number(priceData.price); - setPayouts({ - max: 50000, - values: payoutValues - }); - }, - onError: (error) => { - // Update only this button's state on error - setButtonStates(prev => ({ - ...prev, - [button.actionName]: { - ...prev[button.actionName], - loading: false, - error, - reconnecting: true - } - })); - }, - onOpen: () => { - // Reset error and reconnecting state on successful connection - setButtonStates(prev => ({ - ...prev, - [button.actionName]: { - ...prev[button.actionName], - error: null, - reconnecting: false - } - })); - } - }); - }); + // Create a map of button action names to their payout values + const payoutValues = Object.keys(buttonStates).reduce( + (acc, key) => { + acc[key] = + key === button.actionName + ? payoutValue + : buttonStates[key]?.payout || 0; + return acc; + }, + {} as Record + ); + + setPayouts({ + max: 50000, + values: payoutValues, + }); + }, + onError: (error) => { + // Update only this button's state on error + setButtonStates((prev) => ({ + ...prev, + [button.actionName]: { + ...prev[button.actionName], + loading: false, + error, + reconnecting: true, + }, + })); + }, + onOpen: () => { + // Reset error and reconnecting state on successful connection + setButtonStates((prev) => ({ + ...prev, + [button.actionName]: { + ...prev[button.actionName], + error: null, + reconnecting: false, + }, + })); + }, + }); + } + ); return () => { - cleanupFunctions.forEach(cleanup => cleanup()); + cleanupFunctions.forEach((cleanup) => cleanup()); }; - }, [duration, stake, currency, token]); // Reset loading states when duration or trade type changes useEffect(() => { const initialStates: ButtonStates = {}; - tradeTypeConfigs[trade_type].buttons.forEach(button => { + tradeTypeConfigs[trade_type].buttons.forEach((button) => { initialStates[button.actionName] = { loading: true, error: null, payout: buttonStates[button.actionName]?.payout || 0, - reconnecting: false + reconnecting: false, }; }); setButtonStates(initialStates); @@ -169,18 +180,25 @@ export const TradeFormController: React.FC = ({ isLand }, [trade_type, config]); return ( -
+
+ +
{isLandscape ? ( // Desktop layout -
{ // When clicking anywhere in the trade fields section, hide any open controllers - const event = new MouseEvent('mousedown', { + const event = new MouseEvent("mousedown", { bubbles: true, cancelable: true, }); @@ -211,24 +229,32 @@ export const TradeFormController: React.FC = ({ isLand {config.fields.duration && ( Loading duration field...
}> - { - const durationField = document.querySelector('button[aria-label^="Duration"]'); - if (durationField) { - (durationField as HTMLButtonElement).click(); - } - }}> + { + const durationField = document.querySelector( + 'button[aria-label^="Duration"]' + ); + if (durationField) { + (durationField as HTMLButtonElement).click(); + } + }} + > )} {config.fields.stake && ( Loading stake field...
}> - { - const stakeField = document.querySelector('button[aria-label^="Stake"]'); - if (stakeField) { - (stakeField as HTMLButtonElement).click(); - } - }}> + { + const stakeField = document.querySelector( + 'button[aria-label^="Stake"]' + ); + if (stakeField) { + (stakeField as HTMLButtonElement).click(); + } + }} + > @@ -245,21 +271,34 @@ export const TradeFormController: React.FC = ({ isLand )}
-
+
{config.buttons.map((button) => ( Loading...
}> div]:px-2 [&_span]:text-sm' : '' + isLandscape ? "h-[48px] py-3 [&>div]:px-2 [&_span]:text-sm" : "" }`} title={button.title} label={button.label} - value={buttonStates[button.actionName]?.loading - ? 'Loading...' - : `${buttonStates[button.actionName]?.payout || 0} ${currency}`} + value={ + buttonStates[button.actionName]?.loading + ? "Loading..." + : `${ + buttonStates[button.actionName]?.payout || 0 + } ${currency}` + } title_position={button.position} - disabled={buttonStates[button.actionName]?.loading || buttonStates[button.actionName]?.error !== null} - loading={buttonStates[button.actionName]?.loading || buttonStates[button.actionName]?.reconnecting} + disabled={ + buttonStates[button.actionName]?.loading || + buttonStates[button.actionName]?.error !== null + } + loading={ + buttonStates[button.actionName]?.loading || + buttonStates[button.actionName]?.reconnecting + } error={buttonStates[button.actionName]?.error} onClick={() => { const action = tradeActions[button.actionName]; @@ -272,5 +311,5 @@ export const TradeFormController: React.FC = ({ isLand ))}
- ) -} + ); +};