diff --git a/frontend/app/account/[address]/AmountChart.tsx b/frontend/app/account/[address]/AmountChart.tsx index e05e4f98..ab7f8324 100644 --- a/frontend/app/account/[address]/AmountChart.tsx +++ b/frontend/app/account/[address]/AmountChart.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useRef } from 'react' import { Chart as ChartJS, CategoryScale, @@ -13,6 +13,7 @@ import { ChartData, TooltipItem, } from 'chart.js' +import type { Chart } from 'chart.js' import { Line } from 'react-chartjs-2' import 'chartjs-adapter-moment' import moment from 'moment' @@ -59,9 +60,23 @@ const AmountAnalysisChart: React.FC = ({ const [selectedYear, setSelectedYear] = useState( moment().format('YYYY'), ) + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const dropdownRef = useRef(null) + const chartRef = useRef | null>(null) + const [isChartReady, setIsChartReady] = useState(false) useEffect(() => { setMounted(true) + + // Add a small delay to ensure container is properly sized + const timer = setTimeout(() => { + setIsChartReady(true) + if (chartRef.current) { + chartRef.current.resize() + } + }, 150) + + return () => clearTimeout(timer) }, []) // Available years (last 7 years) @@ -74,9 +89,9 @@ const AmountAnalysisChart: React.FC = ({ return { gridColor: actualTheme === 'dark' ? '#292929' : '#e2e8f0', tickColor: actualTheme === 'dark' ? '#FFFFFF' : '#374151', - tooltipBg: actualTheme === 'dark' ? '#272729' : '#f8fafc', + tooltipBg: actualTheme === 'dark' ? '#24252A' : '#f8fafc', tooltipText: actualTheme === 'dark' ? '#FFFFFF' : '#374151', - selectorBg: actualTheme === 'dark' ? '#272729' : '#f1f5f9', + selectorBg: actualTheme === 'dark' ? '#24252A' : '#f1f5f9', selectorText: actualTheme === 'dark' ? '#8E9BAE' : '#64748b', selectBg: actualTheme === 'dark' ? '#2D2F34' : '#f8fafc', selectText: actualTheme === 'dark' ? '#9CA3AF' : '#6b7280', @@ -106,13 +121,15 @@ const AmountAnalysisChart: React.FC = ({ ? moment().startOf('day') : moment().startOf('day').year(selectedYearInt) - // Generate 24 hourly data points - for (let i = 0; i < 24; i++) { - const date = moment(dayStart).add(i, 'hours').toDate() + // Generate data points every 30 minutes (48 points total) + for (let i = 0; i < 48; i++) { + const date = moment(dayStart) + .add(i * 30, 'minutes') + .toDate() // Small random changes for hourly data - only after mount if (mounted) { - baseValue += (Math.random() - 0.5) * 20 + baseValue += (Math.random() - 0.5) * 15 } data.push({ @@ -134,19 +151,21 @@ const AmountAnalysisChart: React.FC = ({ .startOf('day') .year(selectedYearInt) - // Generate 7 daily data points - for (let i = 0; i < 7; i++) { - const date = moment(weekStart).add(i, 'days').toDate() + // Generate data points every 4 hours (42 points total) + for (let i = 0; i < 42; i++) { + const date = moment(weekStart) + .add(i * 4, 'hours') + .toDate() // Slightly larger changes for daily data - only after mount if (mounted) { - baseValue += (Math.random() - 0.5) * 50 + baseValue += (Math.random() - 0.5) * 30 } data.push({ date, year: moment(date).format('YYYY'), - displayDate: moment(date).format('MMM DD'), + displayDate: moment(date).format('MMM DD HH:mm'), value: Math.max(50, Math.round(baseValue)), }) } @@ -156,14 +175,14 @@ const AmountAnalysisChart: React.FC = ({ // For 1M, create data for the last 30 days but in the selected year const monthStart = selectedYearInt === currentYear - ? moment().subtract(15, 'days').startOf('day') + ? moment().subtract(29, 'days').startOf('day') : moment() - .subtract(15, 'days') + .subtract(29, 'days') .startOf('day') .year(selectedYearInt) // Generate 30 daily data points - for (let i = 0; i < 15; i++) { + for (let i = 0; i < 30; i++) { const date = moment(monthStart).add(i, 'days').toDate() // More noticeable changes for monthly view - only after mount @@ -181,18 +200,20 @@ const AmountAnalysisChart: React.FC = ({ break case '3M': - // For 3M, create data for the last 12 weeks but in the selected year + // For 3M, create data for the last 90 days but in the selected year const quarterStart = selectedYearInt === currentYear - ? moment().subtract(11, 'weeks').startOf('week') + ? moment().subtract(89, 'days').startOf('day') : moment() - .subtract(11, 'weeks') - .startOf('week') + .subtract(89, 'days') + .startOf('day') .year(selectedYearInt) - // Generate 12 weekly data points - for (let i = 0; i < 12; i++) { - const date = moment(quarterStart).add(i, 'weeks').toDate() + // Generate data points every 3 days (30 points total) + for (let i = 0; i < 30; i++) { + const date = moment(quarterStart) + .add(i * 3, 'days') + .toDate() // Larger changes for 3-month view - only after mount if (mounted) { @@ -308,6 +329,11 @@ const AmountAnalysisChart: React.FC = ({ return { responsive: true, maintainAspectRatio: false, + backgroundColor: 'transparent', + resizeDelay: 0, + layout: { + padding: 0, + }, scales: { x: { type: 'time', @@ -323,14 +349,18 @@ const AmountAnalysisChart: React.FC = ({ }, }, grid: { - display: true, - color: colors.gridColor, - lineWidth: 1, - drawOnChartArea: true, - drawTicks: true, + display: false, // Hide vertical grid lines for cleaner look }, ticks: { color: colors.tickColor, + maxTicksLimit: 6, // Limit number of x-axis labels + font: { + size: 11, + }, + padding: 8, // Add spacing between labels and chart + }, + border: { + display: false, }, }, y: { @@ -338,47 +368,72 @@ const AmountAnalysisChart: React.FC = ({ grid: { display: true, color: colors.gridColor, - lineWidth: 1, + lineWidth: 0.5, // Thinner grid lines drawOnChartArea: true, - drawTicks: true, + drawTicks: false, }, ticks: { color: colors.tickColor, + maxTicksLimit: 5, // Limit number of y-axis labels + font: { + size: 11, + }, + padding: 12, // Add spacing between price labels and chart + callback: function (value: string | number) { + // Simplify y-axis labels + if (typeof value === 'number') { + if (value >= 1000) { + return (value / 1000).toFixed(1) + 'K' + } + return value.toFixed(0) + } + return value + }, + }, + border: { + display: false, }, }, }, plugins: { legend: { - display: false, // Hide legend since we only have one dataset + display: false, }, tooltip: { + enabled: true, mode: 'index', - intersect: false, // Allow tooltip when near point + intersect: false, backgroundColor: colors.tooltipBg, - cornerRadius: 10, + cornerRadius: 8, titleColor: colors.tooltipText, bodyColor: colors.tooltipText, - borderColor: '#6F2FCE', - borderWidth: 0, - padding: 12, - displayColors: true, - boxWidth: 8, - boxHeight: 8, - usePointStyle: true, // This makes the legend markers circular - // @ts-expect-error Chart.js types don't properly define pointStyle as allowing 'circle' - pointStyle: 'circle', - boxPadding: 3, + borderColor: 'rgba(111, 47, 206, 0.2)', + borderWidth: 1, + padding: 8, + displayColors: false, // Remove color box for cleaner tooltip + titleFont: { + size: 11, + weight: 500, + }, + bodyFont: { + size: 12, + weight: 600, + }, callbacks: { + title: () => '', // Remove title for cleaner look label: (context: TooltipItem<'line'>): string => { - let label = 'Price: ' if ( context.parsed && context.parsed.y !== null && context.parsed.y !== undefined ) { - label += `${context.parsed.y.toLocaleString()}` + const value = context.parsed.y + if (value >= 1000) { + return `$${(value / 1000).toFixed(1)}K` + } + return `$${value.toFixed(0)}` } - return label + return '' }, }, }, @@ -390,17 +445,18 @@ const AmountAnalysisChart: React.FC = ({ }, elements: { point: { - radius: 3, // Center point size + radius: 0, // Hide points by default for cleaner look hoverRadius: 4, - backgroundColor: '#6F2FCE', // Center point color - borderColor: 'rgba(0, 0, 0, 0)', // Transparent space - borderWidth: 5, // Width of transparent space + backgroundColor: '#6F2FCE', + borderColor: 'white', + borderWidth: 2, hoverBackgroundColor: '#6F2FCE', - hoverBorderColor: '#6F2FCE', - hoverBorderWidth: 5, + hoverBorderColor: 'white', + hoverBorderWidth: 2, }, line: { - tension: 0.4, // Smooth curve + tension: 0.3, // Slightly less curved for more professional look + borderWidth: 2, }, }, } @@ -417,32 +473,63 @@ const AmountAnalysisChart: React.FC = ({ label: 'Price', data: data.map((item) => item.value), borderColor: '#6F2FCE', - backgroundColor: 'rgba(111, 47, 206, 0.1)', // Area fill color - fill: true, - tension: 0.4, - // Point styling - pointRadius: 3, // Center point size - pointHoverRadius: 4, // Slightly larger on hover - pointBackgroundColor: '#6F2FCE', // Center point color + backgroundColor: 'transparent', // No background fill + fill: false, // Disable area fill completely + tension: 0.3, + borderWidth: 2, + // Point styling - hidden by default, shown on hover + pointRadius: 0, + pointHoverRadius: 4, + pointBackgroundColor: '#6F2FCE', pointHoverBackgroundColor: '#6F2FCE', - pointBorderColor: 'rgba(0, 0, 0, 0)', // Transparent space - pointBorderWidth: 5, // Width of transparent space - pointHoverBorderColor: 'rgba(0, 0, 0, 0)', - pointHoverBorderWidth: 5, - // We'll use a custom point style with the outer border + pointBorderColor: 'white', + pointBorderWidth: 2, + pointHoverBorderColor: 'white', + pointHoverBorderWidth: 2, pointStyle: 'circle', - // Additional styling for tooltip - hoverBackgroundColor: '#6F2FCE', }, ], } } // Handle year selection change - const handleYearChange = (e: React.ChangeEvent) => { - setSelectedYear(e.target.value) + const handleYearChange = (year: string) => { + setSelectedYear(year) + setIsDropdownOpen(false) } + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsDropdownOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + // Handle window resize to ensure chart stays properly sized + useEffect(() => { + const handleResize = () => { + if (chartRef.current) { + // Small delay to ensure container has updated dimensions + setTimeout(() => { + chartRef.current?.resize() + }, 100) + } + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [mounted]) + // Handle date range change const handleDateRangeChange = (range: DateRangeType) => { setDateRange(range) @@ -454,6 +541,15 @@ const AmountAnalysisChart: React.FC = ({ const data = generateChartData(dateRange, selectedYear) setChartData(prepareChartData(data)) setChartOptions(getChartOptions(dateRange)) + + // Force chart resize after data update + const timer = setTimeout(() => { + if (chartRef.current) { + chartRef.current.resize() + } + }, 50) + + return () => clearTimeout(timer) // eslint-disable-next-line react-hooks/exhaustive-deps }, [dateRange, selectedYear, actualTheme, mounted]) @@ -479,10 +575,18 @@ const AmountAnalysisChart: React.FC = ({ if (!mounted) { return ( -
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
) @@ -491,90 +595,167 @@ const AmountAnalysisChart: React.FC = ({ const colors = getThemeColors() return ( -
-
+
+
-

+

Price Analysis

- {/* Desktop: Range buttons */} -
- {dateRangeOptions.map((range) => ( - - ))} -
-
- -
- + {/* Ultra Modern Custom Dropdown */} +
+
+ dateRange !== 'ALL' && setIsDropdownOpen(!isDropdownOpen) + } + className={`relative rounded-xl px-4 py-2.5 text-sm font-bold cursor-pointer transition-all duration-300 border-2 ${ + dateRange === 'ALL' + ? 'cursor-not-allowed opacity-40 border-gray-300' + : isDropdownOpen + ? 'border-primary shadow-lg shadow-primary/20 scale-105' + : 'border-transparent hover:border-primary/30 hover:shadow-md hover:scale-102' + }`} + style={{ + background: + dateRange !== 'ALL' + ? isDropdownOpen + ? 'linear-gradient(135deg, rgba(111, 47, 206, 0.1) 0%, rgba(111, 47, 206, 0.05) 50%, rgba(111, 47, 206, 0.1) 100%)' + : `linear-gradient(135deg, ${colors.selectBg} 0%, rgba(111, 47, 206, 0.03) 100%)` + : colors.selectBg, + color: dateRange !== 'ALL' ? '#6F2FCE' : colors.selectText, + minWidth: '80px', + backdropFilter: 'blur(10px)', + }} > - - +
+ {selectedYear} +
+ + + +
+
+
+ + {/* Custom Dropdown Menu */} + {isDropdownOpen && dateRange !== 'ALL' && ( +
+ {availableYears.map((year, index) => ( +
handleYearChange(year)} + className={`px-4 py-3 text-sm font-semibold cursor-pointer transition-all duration-200 ${ + year === selectedYear + ? 'bg-primary text-white shadow-inner' + : 'hover:bg-primary/10 hover:text-primary' + }`} + style={{ + color: + year === selectedYear ? 'white' : colors.selectText, + borderBottom: + index < availableYears.length - 1 + ? `1px solid rgba(111, 47, 206, 0.1)` + : 'none', + }} + > +
+ {year} + {year === selectedYear && ( + + + + )} +
+
+ ))} +
+ )}
- + {isChartReady && mounted ? ( + { + // Chart resize handler - ensures proper dimensions + }, + plugins: { + ...chartOptions.plugins, + filler: { + propagate: false, + }, + }, + }} + /> + ) : ( +
+
+
+
+
+
+ )}
diff --git a/frontend/app/account/[address]/Navbar.tsx b/frontend/app/account/[address]/Navbar.tsx index ab822861..c64ec78a 100644 --- a/frontend/app/account/[address]/Navbar.tsx +++ b/frontend/app/account/[address]/Navbar.tsx @@ -95,7 +95,7 @@ const Navbar: React.FC = ({ ] return ( -