Skip to content

Commit 6c99422

Browse files
hh
1 parent dcd995b commit 6c99422

11 files changed

Lines changed: 557 additions & 157 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"use client";
2+
3+
import { useMemo, useState } from "react";
4+
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
5+
6+
const toTimestampKey = ( value ) => {
7+
const date = new Date( value );
8+
if ( Number.isNaN( date.getTime() ) ) return null;
9+
return date.toISOString();
10+
};
11+
12+
const toDayKey = ( value ) => {
13+
const date = new Date( value );
14+
if ( Number.isNaN( date.getTime() ) ) return null;
15+
return date.toISOString().split( "T" )[ 0 ];
16+
};
17+
18+
const formatDateLabel = ( value ) =>
19+
new Date( value ).toLocaleDateString( "en-US", { month: "short", day: "numeric" } );
20+
21+
const currencyFormatter = new Intl.NumberFormat( "en-US", {
22+
style: "currency",
23+
currency: "USD",
24+
maximumFractionDigits: 0,
25+
} );
26+
27+
const RANGE_OPTIONS = [
28+
{ id: "1W", label: "1W", days: 7 },
29+
{ id: "1M", label: "1M", days: 30 },
30+
{ id: "3M", label: "3M", days: 90 },
31+
{ id: "1Y", label: "1Y", days: 365 },
32+
{ id: "ALL", label: "All", days: null },
33+
];
34+
35+
const CollectionValueChart = ( { valueHistory = [], currentValue = 0 } ) => {
36+
const [ rangeId, setRangeId ] = useState( "1M" );
37+
const formattedData = useMemo( () => {
38+
if ( !Array.isArray( valueHistory ) ) return [];
39+
40+
const cleaned = valueHistory
41+
.map( ( entry ) => {
42+
const date = toTimestampKey( entry?.date );
43+
const value = Number( entry?.value );
44+
if ( !date || !Number.isFinite( value ) ) return null;
45+
return { date, value };
46+
} )
47+
.filter( Boolean )
48+
.sort( ( a, b ) => new Date( a.date ) - new Date( b.date ) );
49+
50+
if ( cleaned.length === 0 && Number.isFinite( currentValue ) && currentValue > 0 ) {
51+
const now = toTimestampKey( new Date() );
52+
if ( now ) {
53+
return [ { date: now, value: currentValue } ];
54+
}
55+
}
56+
57+
const today = toDayKey( new Date() );
58+
if ( cleaned.length > 0 && today ) {
59+
const lastEntry = cleaned[ cleaned.length - 1 ];
60+
const lastDay = toDayKey( lastEntry.date );
61+
if ( lastDay !== today ) {
62+
const lastValue = lastEntry.value;
63+
const nextValue = Number.isFinite( currentValue ) && currentValue > 0 ? currentValue : lastValue;
64+
const now = toTimestampKey( new Date() );
65+
if ( now ) {
66+
cleaned.push( { date: now, value: nextValue } );
67+
}
68+
}
69+
}
70+
71+
return cleaned;
72+
}, [ currentValue, valueHistory ] );
73+
74+
const filteredData = useMemo( () => {
75+
if ( rangeId === "ALL" ) return formattedData;
76+
77+
const range = RANGE_OPTIONS.find( ( option ) => option.id === rangeId );
78+
if ( !range?.days || formattedData.length === 0 ) {
79+
return formattedData;
80+
}
81+
82+
const endDate = new Date( formattedData[ formattedData.length - 1 ].date );
83+
if ( Number.isNaN( endDate.getTime() ) ) {
84+
return formattedData;
85+
}
86+
87+
const startDate = new Date( endDate );
88+
startDate.setDate( endDate.getDate() - range.days + 1 );
89+
90+
return formattedData.filter( ( entry ) => new Date( entry.date ) >= startDate );
91+
}, [ formattedData, rangeId ] );
92+
93+
if ( formattedData.length === 0 ) {
94+
return (
95+
<p className="text-sm text-white/70">
96+
No collection value history yet. Refresh prices to start tracking.
97+
</p>
98+
);
99+
}
100+
101+
return (
102+
<div className="flex h-full w-full flex-col">
103+
<div className="mb-2 flex flex-wrap gap-2">
104+
{ RANGE_OPTIONS.map( ( option ) => {
105+
const isActive = option.id === rangeId;
106+
return (
107+
<button
108+
key={ option.id }
109+
type="button"
110+
onClick={ () => setRangeId( option.id ) }
111+
className={ `rounded-full border px-3 py-1 text-xs font-semibold transition ${ isActive
112+
? "border-emerald-400/80 bg-emerald-500/20 text-emerald-100"
113+
: "border-white/20 bg-white/10 text-white/80 hover:border-white/40"
114+
}` }
115+
aria-pressed={ isActive }
116+
>
117+
{ option.label }
118+
</button>
119+
);
120+
} ) }
121+
</div>
122+
<div className="min-h-0 flex-1">
123+
{ filteredData.length === 0 ? (
124+
<p className="text-sm text-white/70">No data in this range yet.</p>
125+
) : (
126+
<ResponsiveContainer className="text-black h-full w-full max-w-[85%] mx-auto glass text-shadow backdrop rounded-md -p-10" width="100%" height="100%">
127+
<LineChart data={ filteredData }>
128+
<XAxis
129+
dataKey="date"
130+
tick={ { fill: "white" } }
131+
tickFormatter={ formatDateLabel }
132+
/>
133+
<YAxis
134+
tick={ { fill: "white" } }
135+
tickFormatter={ ( value ) => currencyFormatter.format( value ) }
136+
/>
137+
<Tooltip
138+
formatter={ ( value ) => currencyFormatter.format( value ) }
139+
labelFormatter={ formatDateLabel }
140+
/>
141+
<Line type="monotone" dataKey="value" stroke="#34d399" strokeWidth={ 2 } dot={ false } />
142+
</LineChart>
143+
</ResponsiveContainer>
144+
) }
145+
</div>
146+
</div>
147+
);
148+
};
149+
150+
export default CollectionValueChart;

hooks/usePagination.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const usePagination = <T>( data: T[], itemsPerPage: number ) => {
1111
return data.slice( startIndex, endIndex );
1212
}, [ data, currentPage, itemsPerPage ] );
1313

14-
// Reset currentPage if data shrinks and currentPage is now invalid
14+
// Reset currentPage if data shrinks and currentPage is now invalid
1515
useEffect( () => {
1616
if ( currentPage > totalPages ) {
1717
setCurrentPage( 1 );

0 commit comments

Comments
 (0)