mirror of
https://github.com/Tomato6966/investment-portfolio-simulator.git
synced 2025-04-12 09:48:42 +02:00
store savingsplansimulator
This commit is contained in:
parent
1562d3a120
commit
1546b6b20e
2 changed files with 194 additions and 79 deletions
|
@ -239,7 +239,7 @@ export const SavingsPlanSimulator = ({
|
||||||
|
|
||||||
// Add some randomness to make the returns vary month to month
|
// Add some randomness to make the returns vary month to month
|
||||||
const monthFactor = month % 12; // To create some seasonal variation
|
const monthFactor = month % 12; // To create some seasonal variation
|
||||||
const randomFactor = 0.5 + Math.random(); // Between 0.5 and 1.5
|
const randomFactor = 1; // Between 0.5 and 1.5
|
||||||
const seasonalFactor = 1 + (Math.sin(monthFactor / 12 * Math.PI * 2) * 0.2); // +/- 20% seasonal effect
|
const seasonalFactor = 1 + (Math.sin(monthFactor / 12 * Math.PI * 2) * 0.2); // +/- 20% seasonal effect
|
||||||
|
|
||||||
const monthlyReturn = baseMonthlyReturn * randomFactor * seasonalFactor;
|
const monthlyReturn = baseMonthlyReturn * randomFactor * seasonalFactor;
|
||||||
|
@ -325,7 +325,7 @@ export const SavingsPlanSimulator = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-6 mb-6 dark:border dark:border-slate-700">
|
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-6 mb-6 dark:border dark:border-slate-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-gray-200">Savings Plan Simulator</h2>
|
<h2 className="text-xl font-semibold mb-4 dark:text-gray-200">Simple Savings Plan Simulator</h2>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { format, subYears, subMonths } from "date-fns";
|
import { format, subYears, subMonths } from "date-fns";
|
||||||
import { ChevronDown, ChevronLeft, Circle, Filter, Heart, Plus, RefreshCw, Search, X } from "lucide-react";
|
import { ChevronDown, ChevronLeft, Circle, Filter, Heart, Plus, RefreshCw, Search, X } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Link, useSearchParams } from "react-router-dom";
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
import { useDarkMode } from "../hooks/useDarkMode";
|
import { useDarkMode } from "../hooks/useDarkMode";
|
||||||
import { EQUITY_TYPES, getHistoricalData, searchAssets } from "../services/yahooFinanceService";
|
import { EQUITY_TYPES, getHistoricalData, searchAssets } from "../services/yahooFinanceService";
|
||||||
import { Asset } from "../types";
|
import { Asset } from "../types";
|
||||||
|
@ -15,6 +14,7 @@ import { intervalBasedOnDateRange } from "../utils/calculations/intervalBasedOnD
|
||||||
import { useLivePrice } from '../hooks/useLivePrice';
|
import { useLivePrice } from '../hooks/useLivePrice';
|
||||||
import { SortableTable } from '../components/SortableTable';
|
import { SortableTable } from '../components/SortableTable';
|
||||||
import { SavingsPlanSimulator } from '../components/SavingsPlanSimulator';
|
import { SavingsPlanSimulator } from '../components/SavingsPlanSimulator';
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
// Extended time period options
|
// Extended time period options
|
||||||
const TIME_PERIODS: { [key: string]: string } = {
|
const TIME_PERIODS: { [key: string]: string } = {
|
||||||
|
@ -65,6 +65,10 @@ const StockExplorer = () => {
|
||||||
startDate: subYears(new Date(), 1),
|
startDate: subYears(new Date(), 1),
|
||||||
endDate: new Date()
|
endDate: new Date()
|
||||||
});
|
});
|
||||||
|
const [pendingCustomRange, setPendingCustomRange] = useState({
|
||||||
|
startDate: subYears(new Date(), 1),
|
||||||
|
endDate: new Date()
|
||||||
|
});
|
||||||
const [stockData, setStockData] = useState<any[]>([]);
|
const [stockData, setStockData] = useState<any[]>([]);
|
||||||
const [stockColors, setStockColors] = useState<Record<string, string>>({});
|
const [stockColors, setStockColors] = useState<Record<string, string>>({});
|
||||||
const { isDarkMode } = useDarkMode();
|
const { isDarkMode } = useDarkMode();
|
||||||
|
@ -76,12 +80,16 @@ const StockExplorer = () => {
|
||||||
monthlyAmount: number;
|
monthlyAmount: number;
|
||||||
projectionYears: number;
|
projectionYears: number;
|
||||||
allocations: string;
|
allocations: string;
|
||||||
|
customStartDate: string;
|
||||||
|
customEndDate: string;
|
||||||
}>({
|
}>({
|
||||||
stocks: [],
|
stocks: [],
|
||||||
period: '',
|
period: '',
|
||||||
monthlyAmount: 0,
|
monthlyAmount: 0,
|
||||||
projectionYears: 0,
|
projectionYears: 0,
|
||||||
allocations: ''
|
allocations: '',
|
||||||
|
customStartDate: '',
|
||||||
|
customEndDate: ''
|
||||||
});
|
});
|
||||||
const [savingsPlanParams, setSavingsPlanParams] = useState({
|
const [savingsPlanParams, setSavingsPlanParams] = useState({
|
||||||
monthlyAmount: 1000,
|
monthlyAmount: 1000,
|
||||||
|
@ -89,6 +97,10 @@ const StockExplorer = () => {
|
||||||
allocations: {} as Record<string, number>
|
allocations: {} as Record<string, number>
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [setDebouncedSearchParams] = useDebounce((params: Record<string, string>) => {
|
||||||
|
setSearchParams(params);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
// On mount: Read URL query params and update states if found.
|
// On mount: Read URL query params and update states if found.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialFetch.current) return;
|
if (initialFetch.current) return;
|
||||||
|
@ -97,7 +109,33 @@ const StockExplorer = () => {
|
||||||
const periodParam = searchParams.get("period");
|
const periodParam = searchParams.get("period");
|
||||||
if (periodParam && Object.keys(TIME_PERIODS).includes(periodParam)) {
|
if (periodParam && Object.keys(TIME_PERIODS).includes(periodParam)) {
|
||||||
setTimePeriod(periodParam as keyof typeof TIME_PERIODS);
|
setTimePeriod(periodParam as keyof typeof TIME_PERIODS);
|
||||||
updateTimePeriod(periodParam as keyof typeof TIME_PERIODS);
|
|
||||||
|
// If it's a custom period, look for the date parameters
|
||||||
|
if (periodParam === "CUSTOM") {
|
||||||
|
const startDateParam = searchParams.get("startDate");
|
||||||
|
const endDateParam = searchParams.get("endDate");
|
||||||
|
|
||||||
|
if (startDateParam && endDateParam) {
|
||||||
|
const startDate = new Date(startDateParam);
|
||||||
|
const endDate = new Date(endDateParam);
|
||||||
|
|
||||||
|
// Validate dates
|
||||||
|
if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
|
||||||
|
const newCustomRange = { startDate, endDate };
|
||||||
|
setCustomDateRange(newCustomRange);
|
||||||
|
setPendingCustomRange(newCustomRange);
|
||||||
|
setDateRange(newCustomRange);
|
||||||
|
} else {
|
||||||
|
// Fall back to default
|
||||||
|
updateTimePeriod(periodParam as keyof typeof TIME_PERIODS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to default custom range
|
||||||
|
updateTimePeriod(periodParam as keyof typeof TIME_PERIODS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateTimePeriod(periodParam as keyof typeof TIME_PERIODS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get savings plan params first so they're ready when stocks load
|
// Get savings plan params first so they're ready when stocks load
|
||||||
|
@ -251,6 +289,12 @@ const StockExplorer = () => {
|
||||||
}
|
}
|
||||||
if (timePeriod) {
|
if (timePeriod) {
|
||||||
params.period = timePeriod.toString();
|
params.period = timePeriod.toString();
|
||||||
|
|
||||||
|
// Add custom date range params if using custom period
|
||||||
|
if (timePeriod === "CUSTOM") {
|
||||||
|
params.startDate = format(customDateRange.startDate, 'yyyy-MM-dd');
|
||||||
|
params.endDate = format(customDateRange.endDate, 'yyyy-MM-dd');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (savingsPlanParams.monthlyAmount > 0) {
|
if (savingsPlanParams.monthlyAmount > 0) {
|
||||||
params.monthlyAmount = savingsPlanParams.monthlyAmount.toString();
|
params.monthlyAmount = savingsPlanParams.monthlyAmount.toString();
|
||||||
|
@ -273,9 +317,16 @@ const StockExplorer = () => {
|
||||||
|
|
||||||
// Check if anything actually changed before updating URL
|
// Check if anything actually changed before updating URL
|
||||||
const prevParams = previousParamsRef.current;
|
const prevParams = previousParamsRef.current;
|
||||||
|
const hasCustomDateChanged =
|
||||||
|
timePeriod === "CUSTOM" && (
|
||||||
|
format(customDateRange.startDate, 'yyyy-MM-dd') !== prevParams.customStartDate ||
|
||||||
|
format(customDateRange.endDate, 'yyyy-MM-dd') !== prevParams.customEndDate
|
||||||
|
);
|
||||||
|
|
||||||
const hasChanged =
|
const hasChanged =
|
||||||
stocksParam !== prevParams.stocks.join(",") ||
|
stocksParam !== prevParams.stocks.join(",") ||
|
||||||
timePeriod.toString() !== prevParams.period ||
|
timePeriod.toString() !== prevParams.period ||
|
||||||
|
hasCustomDateChanged ||
|
||||||
savingsPlanParams.monthlyAmount !== prevParams.monthlyAmount ||
|
savingsPlanParams.monthlyAmount !== prevParams.monthlyAmount ||
|
||||||
savingsPlanParams.years !== prevParams.projectionYears ||
|
savingsPlanParams.years !== prevParams.projectionYears ||
|
||||||
allocationsParam !== prevParams.allocations;
|
allocationsParam !== prevParams.allocations;
|
||||||
|
@ -288,13 +339,23 @@ const StockExplorer = () => {
|
||||||
period: timePeriod.toString(),
|
period: timePeriod.toString(),
|
||||||
monthlyAmount: savingsPlanParams.monthlyAmount,
|
monthlyAmount: savingsPlanParams.monthlyAmount,
|
||||||
projectionYears: savingsPlanParams.years,
|
projectionYears: savingsPlanParams.years,
|
||||||
allocations: allocationsParam
|
allocations: allocationsParam,
|
||||||
|
customStartDate: timePeriod === "CUSTOM" ? format(customDateRange.startDate, 'yyyy-MM-dd') : '',
|
||||||
|
customEndDate: timePeriod === "CUSTOM" ? format(customDateRange.endDate, 'yyyy-MM-dd') : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the URL params
|
// Use debounced version for savings plan changes only
|
||||||
setSearchParams(params);
|
if (stocksParam === prevParams.stocks.join(",") &&
|
||||||
|
timePeriod.toString() === prevParams.period &&
|
||||||
|
!hasCustomDateChanged) {
|
||||||
|
// Only savings plan params changed - use debounced update
|
||||||
|
setDebouncedSearchParams(params);
|
||||||
|
} else {
|
||||||
|
// Stock selection or time period changed - immediate update
|
||||||
|
setSearchParams(params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [selectedStocks, timePeriod, savingsPlanParams, setSearchParams]);
|
}, [selectedStocks, timePeriod, customDateRange, savingsPlanParams, setSearchParams, setDebouncedSearchParams]);
|
||||||
|
|
||||||
// Handle search
|
// Handle search
|
||||||
const handleSearch = useCallback(async () => {
|
const handleSearch = useCallback(async () => {
|
||||||
|
@ -637,15 +698,6 @@ const StockExplorer = () => {
|
||||||
// Don't include refreshStockData in dependencies
|
// Don't include refreshStockData in dependencies
|
||||||
}, [selectedStocks.length, dateRange]);
|
}, [selectedStocks.length, dateRange]);
|
||||||
|
|
||||||
// Update custom date range
|
|
||||||
const handleCustomDateChange = useCallback((start: Date, end: Date) => {
|
|
||||||
const newRange = { startDate: start, endDate: end };
|
|
||||||
setCustomDateRange(newRange);
|
|
||||||
if (timePeriod === "CUSTOM") {
|
|
||||||
setDateRange(newRange);
|
|
||||||
}
|
|
||||||
}, [timePeriod]);
|
|
||||||
|
|
||||||
// Ensure processStockData is called immediately when selectedStocks changes
|
// Ensure processStockData is called immediately when selectedStocks changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedStocks.length > 0) {
|
if (selectedStocks.length > 0) {
|
||||||
|
@ -654,38 +706,108 @@ const StockExplorer = () => {
|
||||||
}
|
}
|
||||||
}, [selectedStocks, processStockData]);
|
}, [selectedStocks, processStockData]);
|
||||||
|
|
||||||
const LivePriceCell = ({ symbol }: { symbol: string }) => {
|
// Handle pending custom date change
|
||||||
const { livePrice, isLoading, lastUpdated, lastPrice, currency } = useLivePrice({
|
const handlePendingCustomDateChange = (input: Date | string, isStartDate: boolean) => {
|
||||||
symbol,
|
try {
|
||||||
refreshInterval: 60000, // 1 minute
|
// Make sure we have a valid date
|
||||||
enabled: true
|
const newDate = input instanceof Date ? input : new Date(input);
|
||||||
});
|
|
||||||
|
// Check if the date is valid
|
||||||
return (
|
if (isNaN(newDate.getTime())) {
|
||||||
<div>
|
// Don't update if invalid date
|
||||||
{isLoading ? (
|
return;
|
||||||
<span className="inline-block w-4 h-4 border-2 border-t-transparent border-blue-500 rounded-full animate-spin"></span>
|
}
|
||||||
) : livePrice ? (
|
|
||||||
<div>
|
// Update the appropriate date in the pending range
|
||||||
<div className="flex p-2 text-right justify-end items-center gap-1">
|
if (isStartDate) {
|
||||||
<Circle size={16} className="text-green-500" />
|
setPendingCustomRange(prev => ({ ...prev, startDate: newDate }));
|
||||||
<div>{formatCurrency(livePrice, currency)}</div>
|
} else {
|
||||||
<div className="text-xs text-gray-500">
|
setPendingCustomRange(prev => ({ ...prev, endDate: newDate }));
|
||||||
{lastUpdated ? `Updated: ${format(lastUpdated, 'HH:mm:ss')}` : ''}
|
}
|
||||||
</div>
|
} catch (error) {
|
||||||
</div>
|
console.error("Invalid date input:", error);
|
||||||
</div>
|
// Don't update state if there's an error
|
||||||
) : (
|
}
|
||||||
<div className="flex p-2 text-right justify-end items-center gap-1">
|
|
||||||
<Circle size={16} className="text-red-500" /> Market Closed, last Price: {formatCurrency(lastPrice, currency)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define column configurations for the sortable table
|
// Apply custom date range
|
||||||
const tableColumns = [
|
const applyCustomDateRange = () => {
|
||||||
|
// Validate dates
|
||||||
|
if (isNaN(pendingCustomRange.startDate.getTime()) || isNaN(pendingCustomRange.endDate.getTime())) {
|
||||||
|
toast.error("Invalid date format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingCustomRange.startDate > pendingCustomRange.endDate) {
|
||||||
|
toast.error("Start date cannot be after end date");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomDateRange(pendingCustomRange);
|
||||||
|
setDateRange(pendingCustomRange);
|
||||||
|
|
||||||
|
// Make sure time period is set to CUSTOM
|
||||||
|
if (timePeriod !== "CUSTOM") {
|
||||||
|
setTimePeriod("CUSTOM");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memoize table data calculation to prevent unnecessary recalculations
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
return selectedStocks.map(stock => {
|
||||||
|
const metrics = calculatePerformanceMetrics(stock);
|
||||||
|
const historicalData = Array.from(stock.historicalData.entries());
|
||||||
|
const currentPrice = historicalData.length > 0
|
||||||
|
? historicalData[historicalData.length - 1][1]
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: stock.id,
|
||||||
|
name: stock.name,
|
||||||
|
symbol: stock.symbol,
|
||||||
|
total: metrics.total,
|
||||||
|
annualized: metrics.annualized,
|
||||||
|
currentPrice,
|
||||||
|
currency: stock.currency
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [selectedStocks, calculatePerformanceMetrics]);
|
||||||
|
|
||||||
|
// Memoize the LivePriceCell component to prevent unnecessary re-renders
|
||||||
|
const MemoizedLivePriceCell = useMemo(() => {
|
||||||
|
return ({ symbol }: { symbol: string }) => {
|
||||||
|
const { livePrice, isLoading, lastUpdated, lastPrice, currency } = useLivePrice({
|
||||||
|
symbol,
|
||||||
|
refreshInterval: 60000, // 1 minute
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="inline-block w-4 h-4 border-2 border-t-transparent border-blue-500 rounded-full animate-spin"></span>
|
||||||
|
) : livePrice ? (
|
||||||
|
<div>
|
||||||
|
<div className="flex p-2 text-right justify-end items-center gap-1">
|
||||||
|
<Circle size={16} className="text-green-500" />
|
||||||
|
<div>{formatCurrency(livePrice, currency)}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{lastUpdated ? `Updated: ${format(lastUpdated, 'HH:mm:ss')}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex p-2 text-right justify-end items-center gap-1">
|
||||||
|
<Circle size={16} className="text-red-500" /> Market Closed, last Price: {formatCurrency(lastPrice, currency)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Memoize column configurations for the sortable table
|
||||||
|
const tableColumns = useMemo(() => [
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
label: 'Stock',
|
label: 'Stock',
|
||||||
|
@ -719,28 +841,16 @@ const StockExplorer = () => {
|
||||||
key: 'symbol',
|
key: 'symbol',
|
||||||
label: 'Live Price',
|
label: 'Live Price',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
render: (value: string) => <LivePriceCell symbol={value} />
|
render: (value: string) => <MemoizedLivePriceCell symbol={value} />
|
||||||
}
|
}
|
||||||
];
|
], [stockColors, MemoizedLivePriceCell]);
|
||||||
|
|
||||||
// Prepare data for the table
|
// Memoize the SortableTable component to prevent unnecessary re-renders
|
||||||
const tableData = selectedStocks.map(stock => {
|
const MemoizedSortableTable = useMemo(() => {
|
||||||
const metrics = calculatePerformanceMetrics(stock);
|
return (
|
||||||
const historicalData = Array.from(stock.historicalData.entries());
|
<SortableTable data={tableData} columns={tableColumns} />
|
||||||
const currentPrice = historicalData.length > 0
|
);
|
||||||
? historicalData[historicalData.length - 1][1]
|
}, [tableData, tableColumns]);
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: stock.id,
|
|
||||||
name: stock.name,
|
|
||||||
symbol: stock.symbol,
|
|
||||||
total: metrics.total,
|
|
||||||
annualized: metrics.annualized,
|
|
||||||
currentPrice,
|
|
||||||
currency: stock.currency
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-slate-900 min-h-screen w-full">
|
<div className="dark:bg-slate-900 min-h-screen w-full">
|
||||||
|
@ -939,17 +1049,15 @@ const StockExplorer = () => {
|
||||||
|
|
||||||
{/* Custom date range selector (if CUSTOM is selected) */}
|
{/* Custom date range selector (if CUSTOM is selected) */}
|
||||||
{timePeriod === "CUSTOM" && (
|
{timePeriod === "CUSTOM" && (
|
||||||
<div className="flex gap-4 mt-4">
|
<div className="flex flex-col md:flex-row gap-4 mt-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">
|
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
Start Date
|
Start Date
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={format(customDateRange.startDate, 'yyyy-MM-dd')}
|
value={format(pendingCustomRange.startDate, 'yyyy-MM-dd')}
|
||||||
onChange={(e) =>
|
onChange={(e) => handlePendingCustomDateChange(e.target.value, true)}
|
||||||
handleCustomDateChange(new Date(e.target.value), customDateRange.endDate)
|
|
||||||
}
|
|
||||||
className="border p-2 rounded dark:bg-slate-700 dark:text-white dark:border-slate-600"
|
className="border p-2 rounded dark:bg-slate-700 dark:text-white dark:border-slate-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -959,14 +1067,21 @@ const StockExplorer = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={format(customDateRange.endDate, 'yyyy-MM-dd')}
|
value={format(pendingCustomRange.endDate, 'yyyy-MM-dd')}
|
||||||
onChange={(e) =>
|
onChange={(e) => handlePendingCustomDateChange(e.target.value, false)}
|
||||||
handleCustomDateChange(customDateRange.startDate, new Date(e.target.value))
|
|
||||||
}
|
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
max={format(new Date(), 'yyyy-MM-dd')}
|
||||||
className="border p-2 rounded dark:bg-slate-700 dark:text-white dark:border-slate-600"
|
className="border p-2 rounded dark:bg-slate-700 dark:text-white dark:border-slate-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="self-end mt-2 md:mt-0">
|
||||||
|
<button
|
||||||
|
onClick={applyCustomDateRange}
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
disabled={loading || pendingCustomRange.startDate > pendingCustomRange.endDate || pendingCustomRange.startDate === pendingCustomRange.endDate || pendingCustomRange.startDate === dateRange.startDate && pendingCustomRange.endDate === dateRange.endDate}
|
||||||
|
>
|
||||||
|
Apply Date Range
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1033,7 +1148,7 @@ const StockExplorer = () => {
|
||||||
|
|
||||||
{/* Performance metrics table */}
|
{/* Performance metrics table */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<SortableTable data={tableData} columns={tableColumns} />
|
{MemoizedSortableTable}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Add table
Reference in a new issue