mirror of
https://github.com/Tomato6966/investment-portfolio-simulator.git
synced 2025-04-07 11:50:36 +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:
|
||||
VITE_BASE_URL: '/${{ github.event.repository.name }}'
|
||||
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
|
||||
run: cp dist/index.html dist/404.html
|
||||
|
|
|
@ -12,8 +12,8 @@ Why this Project?
|
|||
- 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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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 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">
|
||||
<PortfolioChart/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="col-span-3 lg:col-span-1">
|
||||
<InvestmentFormWrapper />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -86,6 +86,7 @@ export const AddAssetModal = ({ onClose }: { onClose: () => void }) => {
|
|||
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"
|
||||
value={search}
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
debouncedSearch(e.target.value);
|
||||
|
|
|
@ -19,7 +19,7 @@ export const InvestmentFormWrapper = () => {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<h2 className="text-xl font-bold dark:text-gray-200">Add Investment</h2>
|
||||
{assets.length > 0 && (
|
||||
|
@ -50,8 +50,10 @@ export const InvestmentFormWrapper = () => {
|
|||
</div>
|
||||
{
|
||||
selectedAsset && (
|
||||
<div className="overflow-y-scroll scrollbar-styled max-h-[380px] p-6 pr-3">
|
||||
<InvestmentForm assetId={selectedAsset} />
|
||||
<div className="flex-1 h-[calc(100%-120px)] overflow-hidden">
|
||||
<div className="p-6 pr-3 pt-0">
|
||||
<InvestmentForm assetId={selectedAsset} clearSelectedAsset={() => setSelectedAsset(null)} />
|
||||
</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 [amount, setAmount] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
|
@ -114,6 +116,7 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
|||
}
|
||||
// Reset form
|
||||
setAmount('');
|
||||
clearSelectedAsset();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -134,6 +137,7 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
|||
<label className="block text-sm font-medium mb-1">Amount (€)</label>
|
||||
<input
|
||||
type="number"
|
||||
autoFocus
|
||||
value={amount}
|
||||
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"
|
||||
|
@ -168,7 +172,7 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
|||
required
|
||||
/>
|
||||
</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
|
||||
type="date"
|
||||
value={date}
|
||||
|
|
|
@ -213,6 +213,12 @@ export const PortfolioChart = () => {
|
|||
tickFormatter={(value) => `${value.toFixed(2)}%`}
|
||||
/>
|
||||
<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) => {
|
||||
const assetKey = name.split('_')[0] as keyof typeof assets;
|
||||
const processedKey = `${assets.find(a => a.name === name.replace(" (%)", ""))?.id}_price`;
|
||||
|
|
|
@ -177,12 +177,12 @@ export const PortfolioTable = () => {
|
|||
{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">
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
Sparplan
|
||||
SavingsPlan
|
||||
</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">
|
||||
<ShoppingBag className="w-4 h-4 mr-1" />
|
||||
Einmalig
|
||||
OneTime
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
|
|
@ -74,3 +74,51 @@ body {
|
|||
html, body {
|
||||
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[]> => {
|
||||
try {
|
||||
|
@ -47,11 +48,11 @@ export const searchAssets = async (query: string): Promise<Asset[]> => {
|
|||
type: 'equity,etf',
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_BASE}/v1/finance/lookup?${params}`, {
|
||||
headers: {
|
||||
'Origin': import.meta.env.VITE_YAHOO_ORIGIN || window.location.origin
|
||||
}
|
||||
});
|
||||
const url = isDev
|
||||
? `${API_BASE}/v1/finance/lookup?${params}`
|
||||
: `${API_BASE}/v1/finance/lookup?${params}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const data = await response.json() as YahooSearchResponse;
|
||||
|
@ -96,7 +97,11 @@ export const getHistoricalData = async (symbol: string, startDate: string, endDa
|
|||
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');
|
||||
|
||||
const data = await response.json();
|
|
@ -5,6 +5,7 @@ import react from "@vitejs/plugin-react";
|
|||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
const isDev = mode === 'development';
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
|
@ -12,16 +13,16 @@ export default defineConfig(({ mode }) => {
|
|||
exclude: ['lucide-react'],
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
proxy: isDev ? {
|
||||
'/yahoo': {
|
||||
target: env.VITE_YAHOO_API_URL || 'https://query1.finance.yahoo.com',
|
||||
target: 'https://query1.finance.yahoo.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/yahoo/, ''),
|
||||
headers: {
|
||||
'Origin': env.VITE_YAHOO_ORIGIN || 'https://finance.yahoo.com'
|
||||
'Origin': 'https://finance.yahoo.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
} : undefined
|
||||
},
|
||||
base: env.VITE_BASE_URL || '/',
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue