mirror of
https://github.com/Tomato6966/investment-portfolio-simulator.git
synced 2025-04-11 08:00:34 +02:00
add a little responsiveness
This commit is contained in:
parent
3629e5a1d9
commit
1adcad1855
2 changed files with 173 additions and 134 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { memo } from "react";
|
import { memo, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
@ -13,6 +13,14 @@ interface AssetPerformanceModalProps {
|
||||||
export const AssetPerformanceModal = memo(({ assetName, performances, onClose }: AssetPerformanceModalProps) => {
|
export const AssetPerformanceModal = memo(({ assetName, performances, onClose }: AssetPerformanceModalProps) => {
|
||||||
const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
|
const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
|
||||||
|
|
||||||
|
// Prevent body scroll when modal is open
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const CustomizedDot = (props: any) => {
|
const CustomizedDot = (props: any) => {
|
||||||
const { cx, cy, payload } = props;
|
const { cx, cy, payload } = props;
|
||||||
return (
|
return (
|
||||||
|
@ -26,83 +34,94 @@ export const AssetPerformanceModal = memo(({ assetName, performances, onClose }:
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/50 z-50 md:flex md:items-center md:justify-center">
|
||||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg w-[80vw] max-w-4xl">
|
<div className="h-full md:h-auto w-full md:w-[80vw] bg-white dark:bg-slate-800 md:rounded-lg overflow-hidden">
|
||||||
<div className="flex justify-between items-center mb-6">
|
{/* Header - Fixed */}
|
||||||
<h2 className="text-xl font-bold">{assetName} - Yearly Performance</h2>
|
<div className="sticky top-0 z-10 bg-white dark:bg-slate-800 p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded">
|
<h2 className="text-xl font-bold dark:text-gray-300">{assetName} - Yearly Performance</h2>
|
||||||
<X className="w-6 h-6" />
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6 dark:text-gray-300" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[400px]">
|
|
||||||
<ResponsiveContainer>
|
{/* Content - Scrollable */}
|
||||||
<LineChart data={sortedPerformances}>
|
<div className="overflow-y-auto h-[calc(100vh-64px)] md:h-[calc(80vh-64px)] p-6">
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
{/* Chart */}
|
||||||
<XAxis dataKey="year" />
|
<div className="h-[400px] mb-6">
|
||||||
<YAxis
|
<ResponsiveContainer>
|
||||||
yAxisId="left"
|
<LineChart data={sortedPerformances}>
|
||||||
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
/>
|
<XAxis dataKey="year" />
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="right"
|
yAxisId="left"
|
||||||
orientation="right"
|
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
||||||
tickFormatter={(value) => `€${value.toFixed(2)}`}
|
/>
|
||||||
/>
|
<YAxis
|
||||||
<Tooltip
|
yAxisId="right"
|
||||||
formatter={(value: number, name: string) => {
|
orientation="right"
|
||||||
if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
|
tickFormatter={(value) => `€${value.toFixed(2)}`}
|
||||||
return [`€${value.toFixed(2)}`, name];
|
/>
|
||||||
}}
|
<Tooltip
|
||||||
labelFormatter={(year) => `Year ${year}`}
|
formatter={(value: number, name: string) => {
|
||||||
/>
|
if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
|
||||||
<Line
|
return [`€${value.toFixed(2)}`, name];
|
||||||
type="monotone"
|
}}
|
||||||
dataKey="percentage"
|
labelFormatter={(year) => `Year ${year}`}
|
||||||
name="Performance"
|
/>
|
||||||
stroke="url(#colorGradient)"
|
<Line
|
||||||
dot={<CustomizedDot />}
|
type="monotone"
|
||||||
strokeWidth={2}
|
dataKey="percentage"
|
||||||
yAxisId="left"
|
name="Performance"
|
||||||
/>
|
stroke="url(#colorGradient)"
|
||||||
<Line
|
dot={<CustomizedDot />}
|
||||||
type="monotone"
|
strokeWidth={2}
|
||||||
dataKey="price"
|
yAxisId="left"
|
||||||
name="Price"
|
/>
|
||||||
stroke="#666"
|
<Line
|
||||||
strokeDasharray="5 5"
|
type="monotone"
|
||||||
dot={false}
|
dataKey="price"
|
||||||
yAxisId="right"
|
name="Price"
|
||||||
/>
|
stroke="#666"
|
||||||
<defs>
|
strokeDasharray="5 5"
|
||||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
dot={false}
|
||||||
<stop offset="0%" stopColor="#22c55e" />
|
yAxisId="right"
|
||||||
<stop offset="100%" stopColor="#ef4444" />
|
/>
|
||||||
</linearGradient>
|
<defs>
|
||||||
</defs>
|
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
</LineChart>
|
<stop offset="0%" stopColor="#22c55e" />
|
||||||
</ResponsiveContainer>
|
<stop offset="100%" stopColor="#ef4444" />
|
||||||
</div>
|
</linearGradient>
|
||||||
<div className="mt-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
</defs>
|
||||||
{sortedPerformances.map(({ year, percentage, price }) => (
|
</LineChart>
|
||||||
<div
|
</ResponsiveContainer>
|
||||||
key={year}
|
</div>
|
||||||
className={`p-3 rounded-lg ${
|
|
||||||
percentage >= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
|
{/* Performance Cards */}
|
||||||
}`}
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 dark:text-gray-300">
|
||||||
>
|
{sortedPerformances.map(({ year, percentage, price }) => (
|
||||||
<div className="text-sm font-medium">{year}</div>
|
<div
|
||||||
<div className={`text-lg font-bold ${
|
key={year}
|
||||||
percentage >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
|
className={`p-3 rounded-lg ${
|
||||||
}`}>
|
percentage >= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
|
||||||
{percentage.toFixed(2)}%
|
}`}
|
||||||
</div>
|
>
|
||||||
{price && (
|
<div className="text-sm font-medium">{year}</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
<div className={`text-lg font-bold ${
|
||||||
€{price.toFixed(2)}
|
percentage >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
|
||||||
|
}`}>
|
||||||
|
{percentage.toFixed(2)}%
|
||||||
</div>
|
</div>
|
||||||
)}
|
{price && (
|
||||||
</div>
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
))}
|
€{price.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { memo } from "react";
|
import { memo, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
@ -11,6 +11,15 @@ interface PortfolioPerformanceModalProps {
|
||||||
|
|
||||||
export const PortfolioPerformanceModal = memo(({ performances, onClose }: PortfolioPerformanceModalProps) => {
|
export const PortfolioPerformanceModal = memo(({ performances, onClose }: PortfolioPerformanceModalProps) => {
|
||||||
const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
|
const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
|
||||||
|
|
||||||
|
// Prevent body scroll when modal is open
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const CustomizedDot = (props: any) => {
|
const CustomizedDot = (props: any) => {
|
||||||
const { cx, cy, payload } = props;
|
const { cx, cy, payload } = props;
|
||||||
return (
|
return (
|
||||||
|
@ -24,69 +33,80 @@ export const PortfolioPerformanceModal = memo(({ performances, onClose }: Portfo
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/50 z-50 md:flex md:items-center md:justify-center">
|
||||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg w-[80vw] max-w-4xl">
|
<div className="h-full md:h-auto w-full md:w-[80vw] bg-white dark:bg-slate-800 md:rounded-lg overflow-hidden">
|
||||||
<div className="flex justify-between items-center mb-6">
|
{/* Header - Fixed */}
|
||||||
|
<div className="sticky top-0 z-10 bg-white dark:bg-slate-800 p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
<h2 className="text-xl font-bold dark:text-gray-300">Portfolio Performance History</h2>
|
<h2 className="text-xl font-bold dark:text-gray-300">Portfolio Performance History</h2>
|
||||||
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded">
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded"
|
||||||
|
>
|
||||||
<X className="w-6 h-6 dark:text-gray-300" />
|
<X className="w-6 h-6 dark:text-gray-300" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[400px]">
|
|
||||||
<ResponsiveContainer>
|
{/* Content - Scrollable */}
|
||||||
<LineChart data={sortedPerformances}>
|
<div className="overflow-y-auto h-[calc(100vh-64px)] md:h-[calc(80vh-64px)] p-6">
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
{/* Chart */}
|
||||||
<XAxis dataKey="year" />
|
<div className="h-[400px] mb-6">
|
||||||
<YAxis
|
<ResponsiveContainer>
|
||||||
yAxisId="left"
|
<LineChart data={sortedPerformances}>
|
||||||
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
/>
|
<XAxis dataKey="year" />
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="right"
|
yAxisId="left"
|
||||||
orientation="right"
|
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
||||||
tickFormatter={(value) => `€${value.toLocaleString()}`}
|
/>
|
||||||
/>
|
<YAxis
|
||||||
<Tooltip
|
yAxisId="right"
|
||||||
formatter={(value: number, name: string) => {
|
orientation="right"
|
||||||
if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
|
tickFormatter={(value) => `€${value.toLocaleString()}`}
|
||||||
return [`€${value.toLocaleString()}`, 'Portfolio Value'];
|
/>
|
||||||
}}
|
<Tooltip
|
||||||
labelFormatter={(year) => `Year ${year}`}
|
formatter={(value: number, name: string) => {
|
||||||
/>
|
if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
|
||||||
<Line
|
return [`€${value.toLocaleString()}`, 'Portfolio Value'];
|
||||||
type="monotone"
|
}}
|
||||||
dataKey="percentage"
|
labelFormatter={(year) => `Year ${year}`}
|
||||||
name="Performance"
|
/>
|
||||||
stroke="url(#colorGradient)"
|
<Line
|
||||||
dot={<CustomizedDot />}
|
type="monotone"
|
||||||
strokeWidth={2}
|
dataKey="percentage"
|
||||||
yAxisId="left"
|
name="Performance"
|
||||||
/>
|
stroke="url(#colorGradient)"
|
||||||
<defs>
|
dot={<CustomizedDot />}
|
||||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
strokeWidth={2}
|
||||||
<stop offset="0%" stopColor="#22c55e" />
|
yAxisId="left"
|
||||||
<stop offset="100%" stopColor="#ef4444" />
|
/>
|
||||||
</linearGradient>
|
<defs>
|
||||||
</defs>
|
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
</LineChart>
|
<stop offset="0%" stopColor="#22c55e" />
|
||||||
</ResponsiveContainer>
|
<stop offset="100%" stopColor="#ef4444" />
|
||||||
</div>
|
</linearGradient>
|
||||||
<div className="mt-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 dark:text-gray-300">
|
</defs>
|
||||||
{sortedPerformances.map(({ year, percentage }) => (
|
</LineChart>
|
||||||
<div
|
</ResponsiveContainer>
|
||||||
key={year}
|
</div>
|
||||||
className={`p-3 rounded-lg ${
|
|
||||||
percentage >= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
|
{/* Performance Cards */}
|
||||||
}`}
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 dark:text-gray-300">
|
||||||
>
|
{sortedPerformances.map(({ year, percentage }) => (
|
||||||
<div className="text-sm font-medium">{year}</div>
|
<div
|
||||||
<div className={`text-lg font-bold ${
|
key={year}
|
||||||
percentage >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
|
className={`p-3 rounded-lg ${
|
||||||
}`}>
|
percentage >= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
|
||||||
{percentage.toFixed(2)}%
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{year}</div>
|
||||||
|
<div className={`text-lg font-bold ${
|
||||||
|
percentage >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
|
||||||
|
}`}>
|
||||||
|
{percentage.toFixed(2)}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue