mirror of
https://github.com/Tomato6966/investment-portfolio-simulator.git
synced 2025-04-12 04:38:42 +02:00
Change origin and styles
This commit is contained in:
parent
626e6944ac
commit
5224f25093
12 changed files with 88 additions and 23 deletions
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
VITE_BASE_URL: '/${{ github.event.repository.name }}'
|
VITE_BASE_URL: '/${{ github.event.repository.name }}'
|
||||||
VITE_YAHOO_API_URL: 'https://query1.finance.yahoo.com'
|
VITE_YAHOO_API_URL: 'https://query1.finance.yahoo.com'
|
||||||
VITE_YAHOO_ORIGIN: 'https://finance.yahoo.com'
|
VITE_YAHOO_ORIGIN: '/${{ github.event.repository.name }}'
|
||||||
|
|
||||||
- name: Create 404.html
|
- name: Create 404.html
|
||||||
run: cp dist/index.html dist/404.html
|
run: cp dist/index.html dist/404.html
|
||||||
|
|
|
@ -12,8 +12,8 @@ Why this Project?
|
||||||
- Portfolio Performance & Value
|
- Portfolio Performance & Value
|
||||||
- All assets (except the TTWOR and Portfolio-Value) are scaled by percentage of their price. Thus their referenced, scale is on the right. The referenced scale on the left is only for the portfolio-value
|
- All assets (except the TTWOR and Portfolio-Value) are scaled by percentage of their price. Thus their referenced, scale is on the right. The referenced scale on the left is only for the portfolio-value
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
BIN
docs/dark-mode.png
Normal file
BIN
docs/dark-mode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
BIN
docs/light-mode.png
Normal file
BIN
docs/light-mode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 KiB |
|
@ -39,11 +39,11 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-8 mb-8 dark:text-gray-300">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 mb-8 dark:text-gray-300">
|
||||||
<div className="col-span-3">
|
<div className="col-span-3">
|
||||||
<PortfolioChart/>
|
<PortfolioChart/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-3 lg:col-span-1">
|
||||||
<InvestmentFormWrapper />
|
<InvestmentFormWrapper />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,6 +86,7 @@ export const AddAssetModal = ({ onClose }: { onClose: () => void }) => {
|
||||||
placeholder="Search by symbol or name..."
|
placeholder="Search by symbol or name..."
|
||||||
className="w-full p-2 pr-10 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
|
className="w-full p-2 pr-10 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
|
||||||
value={search}
|
value={search}
|
||||||
|
autoFocus
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
debouncedSearch(e.target.value);
|
debouncedSearch(e.target.value);
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const InvestmentFormWrapper = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow h-full dark:shadow-black/60">
|
<div className="bg-white dark:bg-slate-800 rounded-lg shadow h-full dark:shadow-black/60">
|
||||||
<div className="p-6">
|
<div className="p-6 pb-2">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold dark:text-gray-200">Add Investment</h2>
|
<h2 className="text-xl font-bold dark:text-gray-200">Add Investment</h2>
|
||||||
{assets.length > 0 && (
|
{assets.length > 0 && (
|
||||||
|
@ -50,8 +50,10 @@ export const InvestmentFormWrapper = () => {
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
selectedAsset && (
|
selectedAsset && (
|
||||||
<div className="overflow-y-scroll scrollbar-styled max-h-[380px] p-6 pr-3">
|
<div className="flex-1 h-[calc(100%-120px)] overflow-hidden">
|
||||||
<InvestmentForm assetId={selectedAsset} />
|
<div className="p-6 pr-3 pt-0">
|
||||||
|
<InvestmentForm assetId={selectedAsset} clearSelectedAsset={() => setSelectedAsset(null)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +61,7 @@ export const InvestmentFormWrapper = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clearSelectedAsset: () => void }) => {
|
||||||
const [type, setType] = useState<'single' | 'periodic'>('single');
|
const [type, setType] = useState<'single' | 'periodic'>('single');
|
||||||
const [amount, setAmount] = useState('');
|
const [amount, setAmount] = useState('');
|
||||||
const [date, setDate] = useState('');
|
const [date, setDate] = useState('');
|
||||||
|
@ -114,6 +116,7 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
||||||
}
|
}
|
||||||
// Reset form
|
// Reset form
|
||||||
setAmount('');
|
setAmount('');
|
||||||
|
clearSelectedAsset();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -134,6 +137,7 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
||||||
<label className="block text-sm font-medium mb-1">Amount (€)</label>
|
<label className="block text-sm font-medium mb-1">Amount (€)</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
autoFocus
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
|
className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
|
||||||
|
@ -168,7 +172,7 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="block text-sm font-medium mb-1">Sparplan-Start Date</label>
|
<label className="block text-sm font-medium mb-1">SavingsPlan-Start Date</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={date}
|
value={date}
|
||||||
|
|
|
@ -213,6 +213,12 @@ export const PortfolioChart = () => {
|
||||||
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: isDarkMode ? '#1e293b' : '#fff',
|
||||||
|
border: 'none',
|
||||||
|
color: isDarkMode ? '#d1d5d1' : '#000000',
|
||||||
|
boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.5)',
|
||||||
|
}}
|
||||||
formatter={(value: number, name: string, item) => {
|
formatter={(value: number, name: string, item) => {
|
||||||
const assetKey = name.split('_')[0] as keyof typeof assets;
|
const assetKey = name.split('_')[0] as keyof typeof assets;
|
||||||
const processedKey = `${assets.find(a => a.name === name.replace(" (%)", ""))?.id}_price`;
|
const processedKey = `${assets.find(a => a.name === name.replace(" (%)", ""))?.id}_price`;
|
||||||
|
|
|
@ -177,12 +177,12 @@ export const PortfolioTable = () => {
|
||||||
{investment?.type === 'periodic' ? (
|
{investment?.type === 'periodic' ? (
|
||||||
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full">
|
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full">
|
||||||
<RefreshCw className="w-4 h-4 mr-1" />
|
<RefreshCw className="w-4 h-4 mr-1" />
|
||||||
Sparplan
|
SavingsPlan
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded-full">
|
<span className="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded-full">
|
||||||
<ShoppingBag className="w-4 h-4 mr-1" />
|
<ShoppingBag className="w-4 h-4 mr-1" />
|
||||||
Einmalig
|
OneTime
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -74,3 +74,51 @@ body {
|
||||||
html, body {
|
html, body {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling für Investment Form */
|
||||||
|
.scrollbar-styled {
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
|
overflow-y: scroll !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-styled::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-styled::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-styled::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #94a3b8;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-styled::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
.dark .scrollbar-styled::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .scrollbar-styled::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
.scrollbar-styled {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #94a3b8 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .scrollbar-styled {
|
||||||
|
scrollbar-color: #475569 transparent;
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@ interface YahooChartResult {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_YAHOO_API_URL || '/yahoo';
|
const isDev = import.meta.env.DEV;
|
||||||
|
const API_BASE = isDev ? '/yahoo' : 'https://api.allorigins.win/raw?url=' + encodeURIComponent('https://query1.finance.yahoo.com');
|
||||||
|
|
||||||
export const searchAssets = async (query: string): Promise<Asset[]> => {
|
export const searchAssets = async (query: string): Promise<Asset[]> => {
|
||||||
try {
|
try {
|
||||||
|
@ -47,11 +48,11 @@ export const searchAssets = async (query: string): Promise<Asset[]> => {
|
||||||
type: 'equity,etf',
|
type: 'equity,etf',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/v1/finance/lookup?${params}`, {
|
const url = isDev
|
||||||
headers: {
|
? `${API_BASE}/v1/finance/lookup?${params}`
|
||||||
'Origin': import.meta.env.VITE_YAHOO_ORIGIN || window.location.origin
|
: `${API_BASE}/v1/finance/lookup?${params}`;
|
||||||
}
|
|
||||||
});
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error('Network response was not ok');
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
|
||||||
const data = await response.json() as YahooSearchResponse;
|
const data = await response.json() as YahooSearchResponse;
|
||||||
|
@ -96,7 +97,11 @@ export const getHistoricalData = async (symbol: string, startDate: string, endDa
|
||||||
interval: '1d',
|
interval: '1d',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`/yahoo/v8/finance/chart/${symbol}?${params}`);
|
const url = isDev
|
||||||
|
? `/yahoo/v8/finance/chart/${symbol}?${params}`
|
||||||
|
: `${API_BASE}/v8/finance/chart/${symbol}?${params}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error('Network response was not ok');
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
|
@ -5,6 +5,7 @@ import react from "@vitejs/plugin-react";
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
|
const isDev = mode === 'development';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
@ -12,16 +13,16 @@ export default defineConfig(({ mode }) => {
|
||||||
exclude: ['lucide-react'],
|
exclude: ['lucide-react'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: isDev ? {
|
||||||
'/yahoo': {
|
'/yahoo': {
|
||||||
target: env.VITE_YAHOO_API_URL || 'https://query1.finance.yahoo.com',
|
target: 'https://query1.finance.yahoo.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/yahoo/, ''),
|
rewrite: (path) => path.replace(/^\/yahoo/, ''),
|
||||||
headers: {
|
headers: {
|
||||||
'Origin': env.VITE_YAHOO_ORIGIN || 'https://finance.yahoo.com'
|
'Origin': 'https://finance.yahoo.com'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} : undefined
|
||||||
},
|
},
|
||||||
base: env.VITE_BASE_URL || '/',
|
base: env.VITE_BASE_URL || '/',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue