From 8d36f38fcf4b5f355965346648d747d9d93b928d Mon Sep 17 00:00:00 2001 From: Milan Steiner Date: Tue, 10 Jun 2025 13:01:49 +0200 Subject: [PATCH 1/4] Enhance StrategiesTable and StrategyEntry components with improved styling and animations - Updated the animation properties for the StrategiesTable component for smoother transitions. - Refactored the card styles in StrategiesTable to include a gradient background, hover effects, and improved layout. - Added a scrollbar customization for better visibility in the StrategiesTable. - Enhanced the StrategyEntry component with new styles, including a gradient background and hover effects. - Improved mobile layout for both StrategiesTable and StrategyEntry, ensuring better usability. - Added conditional rendering for displaying strategy counts and loading states. - Updated typography styles for better readability and visual hierarchy. - Refined the layout of strategy details, including TVL, APR, and user stake information. --- packages/core/app/pools/page.tsx | 12 +- packages/ui/src/Earn/FilterBar/FilterBar.tsx | 313 +++--- .../Earn/StrategiesTable/StrategiesTable.tsx | 318 +++--- .../Earn/StrategiesTable/StrategyEntry.tsx | 923 ++++++++++-------- 4 files changed, 880 insertions(+), 686 deletions(-) diff --git a/packages/core/app/pools/page.tsx b/packages/core/app/pools/page.tsx index 597ff9b2..d3c8efd3 100644 --- a/packages/core/app/pools/page.tsx +++ b/packages/core/app/pools/page.tsx @@ -250,14 +250,10 @@ export default function Page() { return ( {/* Hacky Title Injector - Waiting for Next Helmet for Next15 */} diff --git a/packages/ui/src/Earn/FilterBar/FilterBar.tsx b/packages/ui/src/Earn/FilterBar/FilterBar.tsx index 6abf720d..eefe8d9a 100644 --- a/packages/ui/src/Earn/FilterBar/FilterBar.tsx +++ b/packages/ui/src/Earn/FilterBar/FilterBar.tsx @@ -11,6 +11,7 @@ import { useMediaQuery, useTheme, SelectChangeEvent, + FormControl, } from "@mui/material"; import { motion } from "framer-motion"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; @@ -19,6 +20,7 @@ import { typography, spacing, borderRadius, + cardStyles, } from "../../Theme/styleConstants"; interface FilterBarProps { @@ -59,9 +61,9 @@ export const FilterBar = ({ return ( {/* Assets Filter */} @@ -99,50 +103,46 @@ export const FilterBar = ({ gap: spacing.xs, flexShrink: 0, position: "relative", - zIndex: 1, + zIndex: 2, + background: `linear-gradient(135deg, ${colors.neutral[800]}40 0%, ${colors.neutral[900]}60 100%)`, + borderRadius: borderRadius.md, + p: spacing.xs, + border: `1px solid ${colors.neutral[700]}`, }} > + + + ) : ( - - - ) : ( - - )} - - ) : ( - /* Desktop Layout */ - - {/* Assets Column */} - - - {assets.map((asset, idx) => ( - + )} + + ) : ( + /* Desktop Layout */ + + {/* Assets Column */} + + + {assets.map((asset, idx) => ( - - {asset.name} - - - ))} - - - - {/* Strategy Name */} - - - - {name} - - - - - {/* TVL */} - - - {formatCurrencyStatic.format(tvl)} - - - - {/* APR */} - - - Up to {(apr * 100).toFixed(1)}% - - + + + + + {asset.name} + + + ))} + + - {/* Reward Token */} - - - - {rewardToken.name} - - + {/* Strategy Name */} + + + + {name} + + + - {/* Your Stake - only show if joined */} - {hasJoined && ( + {/* TVL */} - Your Stake + {formatCurrencyStatic.format(tvl)} + + + {/* APR */} + - {formatCurrencyStatic.format(userStake)} + Up to {((apr * 100) / 2).toFixed(1)}% - )} - {/* Claimable rewards - only show if joined */} - {hasJoined && ( - - + - Claimable - - - 0 - ? colors.success[300] - : colors.neutral[300], - }} - > - {userRewards.toFixed(2)} {rewardToken.name} - - - )} - - {/* Unbond Time - only show if not joined */} - {!hasJoined && ( - - {formatUnbondTime(unbondTime)} + {rewardToken.name} - )} - {/* Action Button Column - Updated for desktop */} - - {hasJoined ? ( - - - - - ) : ( - - - + Claimable + + + + + + 0 + ? colors.success[300] + : colors.neutral[300], + }} + > + {userRewards.toFixed(2)} {rewardToken.name} + + + + )} + + {/* Unbond Time - only show if not joined */} + {!hasJoined && ( + + + {formatUnbondTime(unbondTime)} + + )} + + {/* Action Button Column - Updated for desktop */} + + {hasJoined ? ( + + + + + ) : ( + + + + )} + - - )} + )} + - + ); }; From 1d4740047bd8e7efd01060c22feccb0a4bdf03fd Mon Sep 17 00:00:00 2001 From: Milan Steiner Date: Tue, 10 Jun 2025 13:36:13 +0200 Subject: [PATCH 2/4] Enhance Transactions Table and Volume Chart UI - Updated TransactionsHeader component to improve hover effects and styling. - Refactored TransactionsTable to utilize new card styles and improve responsiveness. - Enhanced tab selection styles in VolumeChart for better user interaction. - Improved tooltip design in VolumeChart for clearer data presentation. - Adjusted spacing and layout across components for a more cohesive design. --- packages/core/app/history/page.tsx | 192 ++++--- .../FinancialChart/FinancialChart.tsx | 210 ++++--- .../TransactionsTable/FilterMenu.tsx | 172 +++++- .../TransactionsTable/TransactionEntry.tsx | 543 +++++++++++------- .../TransactionsTable/TransactionsHeader.tsx | 37 +- .../TransactionsTable/TransactionsTable.tsx | 350 ++++++----- .../Transactions/VolumeChart/VolumeChart.tsx | 308 +++++----- 7 files changed, 1077 insertions(+), 735 deletions(-) diff --git a/packages/core/app/history/page.tsx b/packages/core/app/history/page.tsx index 59b9041f..f83ea371 100644 --- a/packages/core/app/history/page.tsx +++ b/packages/core/app/history/page.tsx @@ -14,6 +14,13 @@ import { import { API, constants, TradeAPi } from "@phoenix-protocol/utils"; import { fetchAllTrades, scaToToken } from "@phoenix-protocol/utils"; import { motion } from "framer-motion"; +import { + cardStyles, + colors, + spacing, + borderRadius, + typography, +} from "@phoenix-protocol/ui/src/Theme/styleConstants"; import { PriceHistoryResponse, @@ -36,6 +43,7 @@ export default function Page() { const [data, setData] = useState<{ timestamp: string; volume: number }[]>([]); const [totalVolume, setTotalVolume] = useState(0); const [period, setPeriod] = useState<"W" | "M" | "Y">("W"); + const [volumeTimeframe, setVolumeTimeframe] = useState<"D" | "M" | "A">("D"); // Independent state for volume chart const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(true); const [historicalPrices, setHistoricalPrices] = @@ -52,9 +60,6 @@ export default function Page() { | undefined >("date"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); - const [selectedTimeEpoch, setSelectedTimeEpoch] = useState<"D" | "M" | "A">( - "D" - ); const [activeView, setActiveView] = useState<"personal" | "all">("all"); const [pools, setPools] = useState([]); const [activeFilters, setActiveFilters] = useState({ @@ -364,8 +369,8 @@ export default function Page() { }, [pageSize, activeView]); useEffect(() => { - loadVolumeData(selectedTimeEpoch); - }, [selectedTimeEpoch, selectedPoolForVolume]); + loadVolumeData(volumeTimeframe); // Use independent volume timeframe + }, [volumeTimeframe, selectedPoolForVolume]); useEffect(() => { loadPriceData(period); @@ -379,10 +384,10 @@ export default function Page() { return ( Volume ( - {selectedTimeEpoch === "D" + {volumeTimeframe === "D" ? "24h" - : selectedTimeEpoch === "M" + : volumeTimeframe === "M" ? "30d" : "1y"} ) @@ -539,45 +544,50 @@ export default function Page() { - - {/* Charts Section */} - - + + {/* Charts Section */} + + + + {" "} + {/* Increased spacing between charts */} - setSelectedTimeEpoch(e)} - selectedTab={selectedTimeEpoch} - totalVolume={totalVolume} - /> + {data.length > 0 && ( + + )} - + {historicalPrices.length > 0 && ( - - - + )} - - + + + + {/* Transactions Section */} + + {!historyLoading ? ( )} + + {/* Load More Button */} - - Load More Transactions - + + + Load More Transactions + + - + ); } diff --git a/packages/ui/src/Transactions/FinancialChart/FinancialChart.tsx b/packages/ui/src/Transactions/FinancialChart/FinancialChart.tsx index d84daffc..e4c6d542 100644 --- a/packages/ui/src/Transactions/FinancialChart/FinancialChart.tsx +++ b/packages/ui/src/Transactions/FinancialChart/FinancialChart.tsx @@ -8,6 +8,7 @@ import { YAxis, } from "recharts"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { motion } from "framer-motion"; import { format } from "date-fns"; import { PriceHistoryResponse } from "@phoenix-protocol/utils"; import { @@ -29,23 +30,33 @@ type DataPoint = { const tabUnselectedStyles = { display: "flex", - width: "2.75rem", - height: "2.3125rem", - padding: "1.125rem 1.5rem", + minWidth: "44px", + height: "40px", + padding: `${spacing.sm} ${spacing.md}`, justifyContent: "center", alignItems: "center", - gap: "0.625rem", - borderRadius: "1rem", + borderRadius: borderRadius.md, cursor: "pointer", - color: "var(--neutral-300, #D4D4D4)", - background: "var(--neutral-900, #171717)", - border: "1px solid var(--neutral-700, #404040)", + fontFamily: typography.fontFamily, + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.medium, + transition: "all 0.3s ease", + color: colors.neutral[300], + background: `linear-gradient(145deg, ${colors.neutral[850]} 0%, ${colors.neutral[800]} 100%)`, + border: `1px solid ${colors.neutral[700]}`, + "&:hover": { + background: `linear-gradient(135deg, ${colors.primary.main}15 0%, ${colors.primary.dark}08 100%)`, + border: `1px solid ${colors.primary.main}30`, + color: colors.neutral[100], + transform: "translateY(-1px)", + }, }; const tabSelectedStyles = { - borderRadius: "1rem", - background: "rgba(226, 73, 26, 0.10)", - color: "var(--neutral-50, #FAFAFA)", + background: `linear-gradient(135deg, ${colors.primary.main}25 0%, ${colors.primary.dark}15 100%)`, + border: `1px solid ${colors.primary.main}80`, + color: colors.neutral[50], + boxShadow: `0 0 20px ${colors.primary.main}40`, }; // Helper function to format data @@ -84,9 +95,22 @@ const GlowingChart = ({ alignItems: "flex-start", gap: spacing.lg, borderRadius: borderRadius.lg, - background: colors.neutral[900], + background: `linear-gradient(145deg, ${colors.neutral[850]} 0%, ${colors.neutral[800]} 100%)`, border: `1px solid ${colors.neutral[700]}`, height: "100%", + position: "relative", + overflow: "hidden", + "&::before": { + content: '""', + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + background: `linear-gradient(135deg, ${colors.primary.main}03 0%, ${colors.primary.dark}02 100%)`, + borderRadius: borderRadius.lg, + pointerEvents: "none", + }, }} > @@ -152,90 +178,98 @@ const GlowingChart = ({ - setSelected("W")} - > - W - - setSelected("M")} - > - M - - + setSelected("W")} + > + W + + + + setSelected("M")} + > + M + + + + setSelected("Y")} + > + A + + + + + + + setSelected("Y")} > - A - - + + + + + + + tick.toFixed(2)} + tick={{ fontSize: isMobile ? 10 : 12 }} + width={isMobile ? 35 : 45} + /> + format(new Date(tick), "MM/dd/yy")} + tick={{ fontSize: isMobile ? 10 : 12 }} + tickMargin={isMobile ? 5 : 10} + /> + + } /> + + - - - - - - - - - tick.toFixed(2)} - tick={{ fontSize: isMobile ? 10 : 12 }} - width={isMobile ? 35 : 45} - /> - format(new Date(tick), "MM/dd/yy")} - tick={{ fontSize: isMobile ? 10 : 12 }} - tickMargin={isMobile ? 5 : 10} - /> - - } /> - - ); }; diff --git a/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx b/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx index a67fe04a..d3217d9c 100644 --- a/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx @@ -107,15 +107,39 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { return (
-
diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx index 1ff1a3cc..793289f1 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx @@ -2,259 +2,398 @@ import React from "react"; import { Box, Grid, Typography, useMediaQuery } from "@mui/material"; import { ArrowForward } from "@mui/icons-material"; import LaunchIcon from "@mui/icons-material/Launch"; +import { motion } from "framer-motion"; import { TransactionTableEntryProps } from "@phoenix-protocol/types"; +import { + colors, + typography, + spacing, + borderRadius, + cardStyles, +} from "../../Theme/styleConstants"; const TransactionEntry = ( props: TransactionTableEntryProps & { isMobile: boolean } ) => { - const { isMobile } = props; // Destructure isMobile - const BoxStyle = { - p: 3, - borderRadius: "12px", // Adjusted border radius - background: "var(--neutral-900, #171717)", // Adjusted background - border: "1px solid var(--neutral-700, #404040)", // Adjusted border - position: "relative", - overflow: "hidden", - boxShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)", + const { isMobile } = props; + + // Format date with time + const formatDateTime = (timestamp: number) => { + const date = new Date(timestamp); + return { + date: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }), + time: date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + }; }; - return ( - - { + if (hash.length <= 16) return hash; + return `${hash.slice(0, 8)}...${hash.slice(-8)}`; + }; - width: "20%", - height: "auto", - opacity: 0.1, - transform: "translateY(-50%)", - ...(isMobile ? { right: 80 } : { left: -40 }), - }} - /> + // Format amount for better display + const formatAmount = (amount: number | string) => { + const num = typeof amount === "string" ? parseFloat(amount) : amount; + if (isNaN(num)) return amount; + + if (num >= 1000000) { + return `${(num / 1000000).toFixed(1)}M`; + } else if (num >= 1000) { + return `${(num / 1000).toFixed(1)}K`; + } + return num.toLocaleString("en-US", { maximumFractionDigits: 2 }); + }; + const { date, time } = formatDateTime(props.date); + + return ( + - - - - {isMobile && ( - - Date - - )} - - {new Date(props.date).toLocaleString()} - - + + {/* Date/Time Column */} + + {isMobile && ( + + Date & Time + + )} + + + {date} + + + {time} + + + - - {isMobile && ( - - Swap Details - - )} - + {/* Swap Details Column */} + + {isMobile && ( + + Swap Details + + )} + {/* From Asset */} + + + + + + {formatAmount(props.fromAmount)} + + + {props.fromAsset.name} + + + + + {/* Arrow */} + - - {props.fromAmount} - + + + + + + {formatAmount(props.toAmount)} + + + {props.toAsset.name} + + + + + + + {/* Trade Value Column */} + + {isMobile && ( - {props.fromAsset.name} + Trade Value (USD) - - + )} - - {props.toAmount} + ${formatAmount(props.tradeValue)} + + + + {/* Transaction ID Column */} + + {isMobile && ( - {props.toAsset.name} + Transaction ID - - - - - {isMobile && ( - - Trade Value (USD) - - )} - - ${props.tradeValue} - - - - {isMobile && ( - + window.open( + `https://stellar.expert/explorer/public/tx/${props.txHash}`, + "_blank" + ) + } > - Transaction ID - - )} - - window.open( - `https://stellar.expert/explorer/public/tx/${props.txHash}`, - "_blank" - ) - } - sx={{ - display: "flex", - alignItems: "center", - fontSize: isMobile ? "12px" : "14px", - fontWeight: "400", - textDecoration: "underline", - textDecorationStyle: "dotted", - color: "var(--neutral-300, #D4D4D4)", // Adjusted color - "&:hover": { - textDecoration: "underline", - cursor: "pointer", - }, - }} - > - - {props.txHash} - + + + {formatTxHash(props.txHash)} + + + - - + + ); }; diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx index 466b02d4..2fc03c3e 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Box, Typography } from "@mui/material"; import { ArrowDownward, SwapVert } from "@mui/icons-material"; -import { colors, typography } from "../../Theme/styleConstants"; +import { colors, typography, spacing } from "../../Theme/styleConstants"; function convertToCamelCase(input: string): string { return input @@ -23,6 +23,21 @@ const TransactionHeader = ({ display: "flex", alignItems: "center", cursor: label !== "Actions" ? "pointer" : "default", + p: spacing.xs, + borderRadius: "6px", + transition: "all 0.2s ease", + "&:hover": + label !== "Actions" + ? { + background: `linear-gradient(135deg, ${colors.primary.main}10 0%, ${colors.primary.dark}05 100%)`, + "& .header-text": { + color: colors.neutral[100], + }, + "& .header-icon": { + color: colors.primary.main, + }, + } + : {}, }} onClick={() => { if (label !== "Actions") { @@ -31,14 +46,16 @@ const TransactionHeader = ({ }} > {label} @@ -46,18 +63,22 @@ const TransactionHeader = ({ {label !== "Actions" && (active ? ( ) : ( ))} diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx index 24f7dc3f..b9b44c3b 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx @@ -1,16 +1,16 @@ import React from "react"; -import { Box, Grid, Tooltip, Typography, useMediaQuery } from "@mui/material"; // Import useMediaQuery -import { motion } from "framer-motion"; // Import Framer Motion +import { Box, Grid, Tooltip, Typography, useMediaQuery } from "@mui/material"; +import { motion } from "framer-motion"; import FilterMenu from "./FilterMenu"; import { TransactionsTableProps } from "@phoenix-protocol/types"; import TransactionEntry from "./TransactionEntry"; import TransactionHeader from "./TransactionsHeader"; -import { maxWidth } from "@mui/system"; import { colors, typography, spacing, borderRadius, + cardStyles, } from "../../Theme/styleConstants"; const customSpacing = { @@ -19,104 +19,6 @@ const customSpacing = { md: spacing.md, }; -const classes = { - root: { - marginTop: customSpacing.md, - padding: `${customSpacing.md} ${customSpacing.md}`, - borderRadius: "20px", - background: ` - linear-gradient(135deg, rgba(23, 23, 23, 0.95) 0%, rgba(38, 38, 38, 0.85) 100%) - `, - backdropFilter: "blur(20px)", - border: "1px solid rgba(249, 115, 22, 0.2)", - overflowX: "auto", - boxShadow: - "0 20px 40px rgba(0, 0, 0, 0.4), 0 0 30px rgba(249, 115, 22, 0.1)", - position: "relative", - "&::before": { - content: '""', - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - background: - "linear-gradient(135deg, rgba(249, 115, 22, 0.03) 0%, rgba(234, 88, 12, 0.02) 100%)", - borderRadius: "20px", - pointerEvents: "none", - }, - }, - tabUnselected: { - display: "flex", - width: "auto", - minWidth: "80px", - height: "40px", - padding: `12px 20px`, - justifyContent: "center", - alignItems: "center", - gap: "0.625rem", - borderRadius: "12px", - cursor: "pointer", - color: "rgba(255, 255, 255, 0.7)", - background: "rgba(23, 23, 23, 0.8)", - backdropFilter: "blur(10px)", - opacity: 0.9, - textAlign: "center", - fontFeatureSettings: "'clig' off, 'liga' off", - fontFamily: typography.fontFamily, - fontSize: "14px", - fontStyle: "normal", - fontWeight: typography.fontWeights.medium, - lineHeight: "1.25rem", - border: "1px solid rgba(255, 255, 255, 0.1)", - transition: "all 0.3s ease", - "&:hover": { - background: "rgba(249, 115, 22, 0.15)", - border: "1px solid rgba(249, 115, 22, 0.3)", - color: "rgba(255, 255, 255, 0.9)", - transform: "translateY(-1px)", - boxShadow: "0 4px 12px rgba(249, 115, 22, 0.2)", - }, - }, - tabSelected: { - display: "flex", - minWidth: "80px", - height: "40px", - padding: `12px 20px`, - justifyContent: "center", - alignItems: "center", - gap: "0.625rem", - flex: "1 0 0", - borderRadius: "12px", - border: "1px solid rgba(249, 115, 22, 0.8)", - background: ` - linear-gradient(135deg, rgba(249, 115, 22, 0.25) 0%, rgba(234, 88, 12, 0.15) 100%) - `, - backdropFilter: "blur(10px)", - color: "#FAFAFA", - textAlign: "center", - fontFeatureSettings: "'clig' off, 'liga' off", - fontFamily: typography.fontFamily, - fontSize: "14px", - fontStyle: "normal", - fontWeight: typography.fontWeights.bold, - lineHeight: "1.25rem", - boxShadow: - "0 0 20px rgba(249, 115, 22, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1)", - animation: "glow 2s ease-in-out infinite alternate", - "@keyframes glow": { - "0%": { - boxShadow: - "0 0 20px rgba(249, 115, 22, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1)", - }, - "100%": { - boxShadow: - "0 0 30px rgba(249, 115, 22, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2)", - }, - }, - }, -}; - const TransactionsTable = ({ activeView, setActiveView, @@ -128,7 +30,7 @@ const TransactionsTable = ({ entries, }: TransactionsTableProps) => { const [renderedEntries, setRenderedEntries] = React.useState(0); - const isMobile = useMediaQuery((theme) => theme.breakpoints.down("md")); // Check if the device is mobile + const isMobile = useMediaQuery((theme) => theme.breakpoints.down("md")); // Render entries one by one with a delay React.useEffect(() => { @@ -137,7 +39,7 @@ const TransactionsTable = ({ setRenderedEntries((prevCount) => prevCount < entries.length ? prevCount + 1 : prevCount ); - }, 200); // Adjust the delay between each entry here (in milliseconds) + }, 200); return () => clearInterval(interval); } @@ -145,77 +47,160 @@ const TransactionsTable = ({ return ( - {/* @ts-ignore */} - + + {/* Header with Tabs and Filter */} - setActiveView("all")} - whileHover={{ scale: 1.05 }} - > - All + + setActiveView("all")} + > + All + + (loggedIn ? setActiveView("personal") : null)} - whileHover={{ scale: 1.05 }} + whileHover={{ scale: loggedIn ? 1.02 : 1 }} + whileTap={{ scale: loggedIn ? 0.98 : 1 }} > - Personal + (loggedIn ? setActiveView("personal") : null)} + > + Personal + + - {!isMobile && ( // Hide header on mobile + {/* Desktop Header */} + {!isMobile && ( @@ -234,11 +218,9 @@ const TransactionsTable = ({ @@ -267,25 +249,30 @@ const TransactionsTable = ({ handleSort={handleSort} label="Transaction ID" active={ - activeSort.column === "date" ? activeSort.direction : false + activeSort.column === "actions" + ? activeSort.direction + : false } /> )} - + + {/* Transactions List */} + {entries.map((entry, index) => ( ))} + + {/* Empty State */} {entries.length === 0 && ( - - - 📊 - - + 📊 + + {activeView === "personal" @@ -338,8 +322,8 @@ const TransactionsTable = ({ { const tabUnselectedStyles = { display: "flex", - width: "2.75rem", - height: "2.3125rem", - padding: "1.125rem 1.5rem", + minWidth: "44px", + height: "40px", + padding: `${spacing.sm} ${spacing.md}`, justifyContent: "center", alignItems: "center", - gap: "0.625rem", borderRadius: borderRadius.md, cursor: "pointer", + fontFamily: typography.fontFamily, + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.medium, + transition: "all 0.3s ease", color: colors.neutral[300], - background: colors.neutral[900], + background: `linear-gradient(145deg, ${colors.neutral[850]} 0%, ${colors.neutral[800]} 100%)`, border: `1px solid ${colors.neutral[700]}`, + "&:hover": { + background: `linear-gradient(135deg, ${colors.primary.main}15 0%, ${colors.primary.dark}08 100%)`, + border: `1px solid ${colors.primary.main}30`, + color: colors.neutral[100], + transform: "translateY(-1px)", + }, }; const tabSelectedStyles = { - borderRadius: borderRadius.md, - background: "rgba(226, 73, 26, 0.10)", + background: `linear-gradient(135deg, ${colors.primary.main}25 0%, ${colors.primary.dark}15 100%)`, + border: `1px solid ${colors.primary.main}80`, color: colors.neutral[50], + boxShadow: `0 0 20px ${colors.primary.main}40`, }; const VolumeChart = ({ @@ -143,24 +154,39 @@ const VolumeChart = ({ sx={{ display: "flex", width: "100%", - padding: "1.5rem", + padding: spacing.lg, flexDirection: "column", justifyContent: "center", alignItems: "flex-start", - gap: "1.5625rem", - borderRadius: "1.5rem", - background: colors.neutral[900], // Adjusted background - border: `1px solid ${colors.neutral[700]}`, // Adjusted border + gap: spacing.lg, + borderRadius: borderRadius.lg, + background: `linear-gradient(145deg, ${colors.neutral[850]} 0%, ${colors.neutral[800]} 100%)`, + border: `1px solid ${colors.neutral[700]}`, + position: "relative", + overflow: "hidden", + "&::before": { + content: '""', + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + background: `linear-gradient(135deg, ${colors.primary.main}03 0%, ${colors.primary.dark}02 100%)`, + borderRadius: borderRadius.lg, + pointerEvents: "none", + }, }} > @@ -216,139 +242,153 @@ const VolumeChart = ({ - setSelectedTab("D")} - > - D - - setSelectedTab("M")} - > - M - - setSelectedTab("A")} - > - A - + + setSelectedTab("D")} + > + D + + + + setSelectedTab("M")} + > + M + + + + setSelectedTab("A")} + > + A + + - - - - - - - - - - - - - - - - - - - - - - - { - if (active && payload && payload.length) { - return ( -
-

- {label} -

-

+ + + + + + + + + + + + + + + + + + + + + + + { + if (active && payload && payload.length) { + return ( +

- Volume: - {formatCurrencyStatic.format(Number(payload[0].value))} -

-
- ); - } - return null; - }} - /> +

+ {label} +

+

+ Volume: + {formatCurrencyStatic.format(Number(payload[0].value))} +

+
+ ); + } + return null; + }} + /> - - {data.map((entry, index) => ( - - ))} - -
-
+ + {data.map((entry, index) => ( + + ))} + + + +
); }; From c36ff8ec4b4264e54fe96baffd9f2b925e2ec26f Mon Sep 17 00:00:00 2001 From: Milan Steiner Date: Tue, 10 Jun 2025 14:10:11 +0200 Subject: [PATCH 3/4] feat: enhance trade loading and filtering logic with improved sorting and pagination --- packages/core/app/history/page.tsx | 216 ++++++++++-------- .../TransactionsTable/TransactionsHeader.tsx | 29 ++- packages/utils/src/api/fetchTrades.ts | 6 +- packages/utils/src/trade_api/trade_api.ts | 5 +- packages/utils/src/trade_api/types.ts | 2 + 5 files changed, 152 insertions(+), 106 deletions(-) diff --git a/packages/core/app/history/page.tsx b/packages/core/app/history/page.tsx index f83ea371..fea02571 100644 --- a/packages/core/app/history/page.tsx +++ b/packages/core/app/history/page.tsx @@ -26,11 +26,12 @@ import { PriceHistoryResponse, TradingVolumeResponse, } from "@phoenix-protocol/utils/build/api/types"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; export default function Page() { const appStore = useAppStore(); const appStorePersist = usePersistStore(); + const isSorting = useRef(false); const tradeApi = new TradeAPi.API(constants.TRADING_API_URL); @@ -71,29 +72,128 @@ export default function Page() { string | undefined >(); - const applyFilters = useCallback((newFilters: ActiveFilters) => { - setActiveFilters(newFilters); - loadAllTrades(newFilters); - }, []); + const loadAllTrades = useCallback( + async (newFilters: ActiveFilters = activeFilters) => { + setHistoryLoading(true); + + try { + let from, to; + if (newFilters.dateRange.from) { + from = (newFilters.dateRange.from.getTime() / 1000).toFixed(0); + } + if (newFilters.dateRange.to) { + to = (newFilters.dateRange.to.getTime() / 1000).toFixed(0); + } + + const trades = await fetchAllTrades( + appStore, + pageSize, + undefined, + from, + to, + activeView === "personal" + ? appStorePersist.wallet.address + : undefined, + sortBy, + sortOrder + ); + + // Apply client-side filtering for tradeSize and tradeValue + let filteredTrades = trades; + + if ( + newFilters.tradeSize.from !== undefined || + newFilters.tradeSize.to !== undefined + ) { + filteredTrades = filteredTrades.filter((trade) => { + // Use fromAmount as the trade size + const size = trade.fromAmount; + if ( + newFilters.tradeSize.from !== undefined && + newFilters.tradeSize.to !== undefined + ) { + return ( + size >= newFilters.tradeSize.from && + size <= newFilters.tradeSize.to + ); + } else if (newFilters.tradeSize.from !== undefined) { + return size >= newFilters.tradeSize.from; + } else if (newFilters.tradeSize.to !== undefined) { + return size <= newFilters.tradeSize.to; + } + return true; + }); + } + + if ( + newFilters.tradeValue.from !== undefined || + newFilters.tradeValue.to !== undefined + ) { + filteredTrades = filteredTrades.filter((trade) => { + const value = parseFloat(trade.tradeValue); + if ( + newFilters.tradeValue.from !== undefined && + newFilters.tradeValue.to !== undefined + ) { + return ( + value >= newFilters.tradeValue.from && + value <= newFilters.tradeValue.to + ); + } else if (newFilters.tradeValue.from !== undefined) { + return value >= newFilters.tradeValue.from; + } else if (newFilters.tradeValue.to !== undefined) { + return value <= newFilters.tradeValue.to; + } + return true; + }); + } + + // Server-side sorting applied, no need for client-side sorting + setHistory(filteredTrades); + } catch (error) { + console.error("Error loading trades:", error); + } finally { + setHistoryLoading(false); + } + }, + [ + pageSize, + activeView, + appStorePersist.wallet.address, + activeFilters, + sortBy, + sortOrder, + ] + ); + + const applyFilters = useCallback( + (newFilters: ActiveFilters) => { + setActiveFilters(newFilters); + loadAllTrades(newFilters); + }, + [loadAllTrades] + ); const loadMore = () => { setPageSize((prev) => prev + 10); }; - const handleSortChange = ( - newSortBy: - | "tradeType" - | "asset" - | "tradeSize" - | "tradeValue" - | "date" - | "actions" - | undefined, - newSortOrder: "asc" | "desc" - ) => { - setSortBy(newSortBy); + const handleSortChange = (column: string) => { + let newSortOrder: "asc" | "desc"; + + // If clicking the same column, toggle the sort order + if (sortBy === column) { + newSortOrder = sortOrder === "asc" ? "desc" : "asc"; + } else { + // If clicking a different column, start with descending order (except for date which should start with desc to show newest first) + newSortOrder = column === "date" ? "desc" : "asc"; + } + + // Update sort state + setSortBy(column as any); setSortOrder(newSortOrder); - applyFilters(activeFilters); + + // The loadAllTrades function will be automatically called due to dependency array including sortBy and sortOrder }; const loadVolumeData = async (timeEpoch: "D" | "M" | "A") => { @@ -293,80 +393,9 @@ export default function Page() { setHistoricalPrices(graph); }; - const loadAllTrades = async (newFilters: ActiveFilters = activeFilters) => { - let from, to; - if (newFilters.dateRange.from) { - from = (newFilters.dateRange.from.getTime() / 1000).toFixed(0); - } - if (newFilters.dateRange.to) { - to = (newFilters.dateRange.to.getTime() / 1000).toFixed(0); - } - - const trades = await fetchAllTrades( - appStore, - pageSize, - undefined, - from, - to, - activeView === "personal" ? appStorePersist.wallet.address : undefined - ); - - // Apply client-side filtering for tradeSize and tradeValue - let filteredTrades = trades; - - if ( - newFilters.tradeSize.from !== undefined || - newFilters.tradeSize.to !== undefined - ) { - filteredTrades = filteredTrades.filter((trade) => { - // Use fromAmount as the trade size - const size = trade.fromAmount; - if ( - newFilters.tradeSize.from !== undefined && - newFilters.tradeSize.to !== undefined - ) { - return ( - size >= newFilters.tradeSize.from && size <= newFilters.tradeSize.to - ); - } else if (newFilters.tradeSize.from !== undefined) { - return size >= newFilters.tradeSize.from; - } else if (newFilters.tradeSize.to !== undefined) { - return size <= newFilters.tradeSize.to; - } - return true; - }); - } - - if ( - newFilters.tradeValue.from !== undefined || - newFilters.tradeValue.to !== undefined - ) { - filteredTrades = filteredTrades.filter((trade) => { - const value = parseFloat(trade.tradeValue); - if ( - newFilters.tradeValue.from !== undefined && - newFilters.tradeValue.to !== undefined - ) { - return ( - value >= newFilters.tradeValue.from && - value <= newFilters.tradeValue.to - ); - } else if (newFilters.tradeValue.from !== undefined) { - return value >= newFilters.tradeValue.from; - } else if (newFilters.tradeValue.to !== undefined) { - return value <= newFilters.tradeValue.to; - } - return true; - }); - } - - setHistory(filteredTrades); - setHistoryLoading(false); - }; - useEffect(() => { loadAllTrades(); - }, [pageSize, activeView]); + }, [loadAllTrades]); useEffect(() => { loadVolumeData(volumeTimeframe); // Use independent volume timeframe @@ -604,12 +633,7 @@ export default function Page() { applyFilters={(newFilters: ActiveFilters) => applyFilters(newFilters) } - handleSort={(column) => - handleSortChange( - column as any, - sortOrder === "asc" ? "desc" : "asc" - ) - } + handleSort={(column) => handleSortChange(column)} /> ) : ( diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx index 2fc03c3e..7cb97392 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx @@ -3,10 +3,19 @@ import { Box, Typography } from "@mui/material"; import { ArrowDownward, SwapVert } from "@mui/icons-material"; import { colors, typography, spacing } from "../../Theme/styleConstants"; -function convertToCamelCase(input: string): string { - return input - .toLowerCase() - .replace(/\s+(\w)/g, (_, match) => match.toUpperCase()); +function mapLabelToColumn(label: string): string { + switch (label) { + case "Date & Time": + return "date"; + case "Swap Details": + return "asset"; + case "Trade Value (USD)": + return "tradeValue"; + case "Transaction ID": + return "actions"; // This should not be sortable, but keeping for consistency + default: + return label.toLowerCase().replace(/\s+/g, ""); + } } const TransactionHeader = ({ @@ -22,12 +31,15 @@ const TransactionHeader = ({ sx={{ display: "flex", alignItems: "center", - cursor: label !== "Actions" ? "pointer" : "default", + cursor: + label !== "Actions" && label !== "Transaction ID" + ? "pointer" + : "default", p: spacing.xs, borderRadius: "6px", transition: "all 0.2s ease", "&:hover": - label !== "Actions" + label !== "Actions" && label !== "Transaction ID" ? { background: `linear-gradient(135deg, ${colors.primary.main}10 0%, ${colors.primary.dark}05 100%)`, "& .header-text": { @@ -40,8 +52,8 @@ const TransactionHeader = ({ : {}, }} onClick={() => { - if (label !== "Actions") { - handleSort(convertToCamelCase(label)); + if (label !== "Actions" && label !== "Transaction ID") { + handleSort(mapLabelToColumn(label)); } }} > @@ -61,6 +73,7 @@ const TransactionHeader = ({ {label}
{label !== "Actions" && + label !== "Transaction ID" && (active ? ( { - if (name === "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" || name === "CDIKURWHYS4FFTR5KOQK6MBFZA2K3E26WGBQI6PXBYWZ4XIOPJHDFJKP") { + if ( + name === "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" || + name === "CDIKURWHYS4FFTR5KOQK6MBFZA2K3E26WGBQI6PXBYWZ4XIOPJHDFJKP" + ) { return 1; } return this.get(`/price/${name}`); diff --git a/packages/utils/src/trade_api/types.ts b/packages/utils/src/trade_api/types.ts index fc8da36a..199dc125 100644 --- a/packages/utils/src/trade_api/types.ts +++ b/packages/utils/src/trade_api/types.ts @@ -70,6 +70,8 @@ export interface AdvancedTradesParams { limit?: number; startTime?: number; endTime?: number; + sortBy?: string; + sortOrder?: "asc" | "desc"; } export interface TradeHistoryParams { From 623fffa0d6bd35713d87863e73276450e3685a68 Mon Sep 17 00:00:00 2001 From: Milan Steiner Date: Tue, 10 Jun 2025 14:11:17 +0200 Subject: [PATCH 4/4] feat: add label to date picker in FilterMenu for better user guidance --- .../Transactions/TransactionsTable/FilterMenu.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx b/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx index d3217d9c..74b41316 100644 --- a/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/FilterMenu.tsx @@ -176,6 +176,18 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { }} > + + From +