Bläddra i källkod

fix loading message alignment

Daniel Bohry 9 månader sedan
förälder
incheckning
2d52b1537d

+ 0 - 0
src/routes/Chart.svelte → src/components/Chart.svelte


+ 2 - 2
src/routes/Header.svelte → src/components/Header.svelte

@@ -1,7 +1,7 @@
 <script>
     import {page} from '$app/stores';
     import profile from '$lib/images/profile.png';
-    import {authentication} from "./store.js";
+    import {authentication} from "../routes/store.js";
     import {onMount} from "svelte";
 
     let username = undefined;
@@ -37,7 +37,7 @@
     <nav>
         <ul>
             <li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
-                <a href="/">Home</a>
+                <a href="/static">Home</a>
             </li>
             {#if username !== ""}
                 <li aria-current={$page.url.pathname === '/stocks' ? 'page' : undefined}>

+ 1 - 1
src/routes/+layout.svelte

@@ -1,5 +1,5 @@
 <script>
-	import Header from './Header.svelte';
+	import Header from '../components/Header.svelte';
 	import '../app.css';
 
 	let year = new Date().getFullYear();

+ 14 - 8
src/routes/insights/+page.svelte

@@ -1,29 +1,28 @@
 <script>
-	import Chart from '../Chart.svelte';
+	import Chart from '../../components/Chart.svelte';
 	import { onMount } from 'svelte';
 	import { authentication } from '../store.js';
+	import { fade } from 'svelte/transition';
 
 	let authToken;
 	let data = {};
 	let isLoading = true;
 
 	onMount(() => {
-		const unsubscribe = authentication.subscribe(async ({ token }) => {
+		return authentication.subscribe(async ({ token }) => {
 			if (token) {
 				authToken = token;
 				await fetchPortfolio();
 			}
 		});
-
-		return unsubscribe;
 	});
 
 	async function fetchPortfolio() {
 		try {
 			const response = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios`, {
 				headers: {
-					Authorization: `Bearer ${authToken}`,
-				},
+					Authorization: `Bearer ${authToken}`
+				}
 			});
 
 			if (!response.ok) {
@@ -46,8 +45,15 @@
 	}
 </script>
 
+<svelte:head>
+	<title>Insights</title>
+	<meta name="description" content="Insights" />
+</svelte:head>
+
 {#if isLoading}
-	<p>Loading portfolio...</p>
+	<div in:fade>Loading...</div>
 {:else}
-	<Chart {data} />
+	<div in:fade>
+		<Chart {data} />
+	</div>
 {/if}

+ 330 - 327
src/routes/portfolio/+page.svelte

@@ -1,339 +1,342 @@
 <script>
-    import { authentication } from '../store.js';
-    import { onMount } from 'svelte';
-    import { fade } from 'svelte/transition';
-
-    let portfolioId = undefined;
-    let result = [];
-    let totalValue = 0;
-    let totalAssets = 0;
-    let authToken;
-    let isLoading = true;
-    let showModal = false;
-    let searchStockResult = [];
-    let orderBy = "total";
-
-    onMount(() => {
-        const unsubscribe = authentication.subscribe(value => {
-            if (value?.token) {
-                authToken = value.token;
-                fetchPortfolio();
-            }
-        });
-
-        return () => unsubscribe();
-    });
-
-    async function fetchPortfolio() {
-        try {
-            const response = await fetch(
-              `${import.meta.env.VITE_STOCKS_HOST}/api/portfolios`,
-              {
-                  method: 'GET',
-                  headers: {
-                      Authorization: 'Bearer ' + authToken
-                  }
-              }
-            );
-
-            if (response.ok) {
-                await update(response.json());
-            } else {
-                const error = await response.json();
-                console.error('Failed to find portfolio info:', error);
-                alert('Failed to find portfolio info: ' + error.message);
-            }
-        } catch (err) {
-            console.error('Failed to find portfolio info', err);
-            alert('Failed to find portfolio info');
-        } finally {
-            isLoading = false;
-        }
-    }
-
-    async function update(response) {
-        const portfolio = await response;
-        if (portfolio?.length > 0) {
-            if (orderBy === "code") {
-                result = portfolio[0].stocks.sort((a, b) => a.code.localeCompare(b.code));
-            }
-
-            if (orderBy === "name") {
-                result = portfolio[0].stocks.sort((a, b) => a.name.localeCompare(b.name));
-            }
-
-            if (orderBy === "total") {
-                result = portfolio[0].stocks.sort((a, b) => a.total - b.total);
-            }
-
-            result = portfolio[0].stocks;
-            totalValue = portfolio[0].totalValue;
-            totalAssets = portfolio[0].totalAssets;
-            portfolioId = portfolio[0].id;
-        } else {
-            await createNewPortfolio();
-        }
-    }
-
-    async function createNewPortfolio() {
-        try {
-            const response = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios`, {
-                method: 'POST',
-                headers: {
-                    Authorization: 'Bearer ' + authToken
-                }
-            });
-
-            if (response.status === 400) {
-                alert("Bad request. Invalid code.");
-                return;
-            }
-
-            await fetchPortfolio();
-        } catch (err) {
-            console.error('Update failed', err);
-        }
-    }
-
-    async function updatePortfolio(stocks) {
-        try {
-            const response = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios/${portfolioId}`, {
-                method: 'PUT',
-                headers: {
-                    Authorization: 'Bearer ' + authToken,
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({stocks})
-            });
-
-            if (response.status === 400) {
-                alert("Bad request. Invalid code.");
-                return;
-            }
-
-            await fetchPortfolio();
-        } catch (err) {
-            console.error('Update failed', err);
-        }
-    }
-
-    async function searchStock(code) {
-        if (!code) return;
-
-        try {
-            const res = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/stocks?q=${code}`);
-            return await res.json();
-        } catch (err) {
-            console.error("Search error:", err);
-            return [];
-        }
-    }
-
-    async function handleSubmit(e) {
-        e.preventDefault();
-        const code = new FormData(e.target).get("stock_code").toUpperCase();
-
-        const data = await searchStock(code);
-        if (!data || data.length === 0) {
-            alert("Stock not found.");
-            return;
-        }
-
-        searchStockResult = data;
-        const alreadyInPortfolio = result.some(s => s.code === data[0]?.code);
-
-        if (data.length === 1 && !alreadyInPortfolio) {
-            await addSelectedStock(data[0]);
-            closeOrOpenModal();
-        }
-    }
-
-    async function addSelectedStock(newStock) {
-        const exists = result.some(stock => stock.code === newStock.code);
-        if (exists) return;
-
-        result = [
-            ...result,
-            {
-                code: newStock.code,
-                name: newStock.name,
-                quantity: 0,
-                price: newStock.price,
-                total: 0,
-                totalPercent: 0
-            }
-        ];
-
-        closeOrOpenModal();
-
-        await updatePortfolio(result);
-    }
-
-    function remove(code) {
-        result = result.filter(stock => stock.code !== code);
-        updatePortfolio(result);
-    }
-
-    function formatCurrency(value) {
-        return value.toLocaleString('en-US', {
-            style: 'currency',
-            currency: 'USD'
-        });
-    }
-
-    function calculatePercentage(part, total) {
-        return total ? Math.floor((part / total) * 10000) / 100 : 0;
-    }
-
-    function updateStockQuantity(e) {
-        e.preventDefault();
-        const form = new FormData(e.target);
-        const code = form.get("code");
-        const quantity = parseInt(form.get("quantity")) || 0;
-
-        result = result.map(stock =>
-          stock.code === code ? {...stock, quantity} : stock
-        );
-
-        updatePortfolio(result);
-    }
-
-    function closeOrOpenModal() {
-        searchStockResult = [];
-        showModal = !showModal;
-    }
-
-    function updateOrderBy(event) {
-        orderBy = event.target.value;
-        fetchPortfolio();
-    }
-
-    function handleInputChange(event) {
-        // Update stock quantity with the new value from the input field
-        const form = new FormData(event.target.closest('form'));
-        const code = form.get("code");
-        const quantity = parseInt(form.get("quantity")) || 0;
-
-        // Update the stock array with the new quantity
-        result = result.map(stock =>
-          stock.code === code ? { ...stock, quantity } : stock
-        );
-
-        // Manually trigger form submission
-        event.target.form.requestSubmit();
-    }
+	import { authentication } from '../store.js';
+	import { onMount } from 'svelte';
+	import { fade } from 'svelte/transition';
+
+	let portfolioId = undefined;
+	let result = [];
+	let totalValue = 0;
+	let totalAssets = 0;
+	let authToken;
+	let isLoading = true;
+	let showModal = false;
+	let searchStockResult = [];
+	let orderBy = 'total';
+
+	onMount(() => {
+		const unsubscribe = authentication.subscribe(value => {
+			if (value?.token) {
+				authToken = value.token;
+				fetchPortfolio();
+			}
+		});
+
+		return () => unsubscribe();
+	});
+
+	async function fetchPortfolio() {
+		try {
+			const response = await fetch(
+				`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios`,
+				{
+					method: 'GET',
+					headers: {
+						Authorization: 'Bearer ' + authToken
+					}
+				}
+			);
+
+			if (response.ok) {
+				await update(response.json());
+			} else {
+				const error = await response.json();
+				console.error('Failed to find portfolio info:', error);
+				alert('Failed to find portfolio info: ' + error.message);
+			}
+		} catch (err) {
+			console.error('Failed to find portfolio info', err);
+			alert('Failed to find portfolio info');
+		} finally {
+			isLoading = false;
+		}
+	}
+
+	async function update(response) {
+		const portfolio = await response;
+		if (portfolio?.length > 0) {
+			if (orderBy === 'code') {
+				result = portfolio[0].stocks.sort((a, b) => a.code.localeCompare(b.code));
+			}
+
+			if (orderBy === 'name') {
+				result = portfolio[0].stocks.sort((a, b) => a.name.localeCompare(b.name));
+			}
+
+			if (orderBy === 'total') {
+				result = portfolio[0].stocks.sort((a, b) => a.total - b.total);
+			}
+
+			result = portfolio[0].stocks;
+			totalValue = portfolio[0].totalValue;
+			totalAssets = portfolio[0].totalAssets;
+			portfolioId = portfolio[0].id;
+		} else {
+			await createNewPortfolio();
+		}
+	}
+
+	async function createNewPortfolio() {
+		try {
+			const response = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios`, {
+				method: 'POST',
+				headers: {
+					Authorization: 'Bearer ' + authToken
+				}
+			});
+
+			if (response.status === 400) {
+				alert('Bad request. Invalid code.');
+				return;
+			}
+
+			await fetchPortfolio();
+		} catch (err) {
+			console.error('Update failed', err);
+		}
+	}
+
+	async function updatePortfolio(stocks) {
+		try {
+			const response = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios/${portfolioId}`, {
+				method: 'PUT',
+				headers: {
+					Authorization: 'Bearer ' + authToken,
+					'Content-Type': 'application/json'
+				},
+				body: JSON.stringify({ stocks })
+			});
+
+			if (response.status === 400) {
+				alert('Bad request. Invalid code.');
+				return;
+			}
+
+			await fetchPortfolio();
+		} catch (err) {
+			console.error('Update failed', err);
+		}
+	}
+
+	async function searchStock(code) {
+		if (!code) return;
+
+		try {
+			const res = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/stocks?q=${code}`);
+			return await res.json();
+		} catch (err) {
+			console.error('Search error:', err);
+			return [];
+		}
+	}
+
+	async function handleSubmit(e) {
+		e.preventDefault();
+		const code = new FormData(e.target).get('stock_code').toUpperCase();
+
+		const data = await searchStock(code);
+		if (!data || data.length === 0) {
+			alert('Stock not found.');
+			return;
+		}
+
+		searchStockResult = data;
+		const alreadyInPortfolio = result.some(s => s.code === data[0]?.code);
+
+		if (data.length === 1 && !alreadyInPortfolio) {
+			await addSelectedStock(data[0]);
+			closeOrOpenModal();
+		}
+	}
+
+	async function addSelectedStock(newStock) {
+		const exists = result.some(stock => stock.code === newStock.code);
+		if (exists) return;
+
+		result = [
+			...result,
+			{
+				code: newStock.code,
+				name: newStock.name,
+				quantity: 0,
+				price: newStock.price,
+				total: 0,
+				totalPercent: 0
+			}
+		];
+
+		closeOrOpenModal();
+
+		await updatePortfolio(result);
+	}
+
+	function remove(code) {
+		result = result.filter(stock => stock.code !== code);
+		updatePortfolio(result);
+	}
+
+	function formatCurrency(value) {
+		return value.toLocaleString('en-US', {
+			style: 'currency',
+			currency: 'USD'
+		});
+	}
+
+	function calculatePercentage(part, total) {
+		return total ? Math.floor((part / total) * 10000) / 100 : 0;
+	}
+
+	function updateStockQuantity(e) {
+		e.preventDefault();
+		const form = new FormData(e.target);
+		const code = form.get('code');
+		const quantity = parseInt(form.get('quantity')) || 0;
+
+		result = result.map(stock =>
+			stock.code === code ? { ...stock, quantity } : stock
+		);
+
+		updatePortfolio(result);
+	}
+
+	function closeOrOpenModal() {
+		searchStockResult = [];
+		showModal = !showModal;
+	}
+
+	function updateOrderBy(event) {
+		orderBy = event.target.value;
+		fetchPortfolio();
+	}
+
+	function handleInputChange(event) {
+		// Update stock quantity with the new value from the input field
+		const form = new FormData(event.target.closest('form'));
+		const code = form.get('code');
+		const quantity = parseInt(form.get('quantity')) || 0;
+
+		// Update the stock array with the new quantity
+		result = result.map(stock =>
+			stock.code === code ? { ...stock, quantity } : stock
+		);
+
+		// Manually trigger form submission
+		event.target.form.requestSubmit();
+	}
 </script>
 
 <svelte:head>
-    <title>Stocks</title>
-    <meta name="description" content="About"/>
+	<title>Stocks</title>
+	<meta name="description" content="Portfolio" />
 </svelte:head>
 
 {#if isLoading}
-    <div in:fade>Loading...</div>
+	<div in:fade>Loading...</div>
 {:else if portfolioId}
-    <div class="button-container">
-        <button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#exampleModal" on:click={closeOrOpenModal}>Add</button>
-
-        <!-- Dropdown for ordering the list -->
-        <select class="form-control order-select" on:change={updateOrderBy}>
-            <option value="code">Order by Code</option>
-            <option value="name">Order by Name</option>
-            <option value="total" selected>Order by Total</option>
-        </select>
-    </div>
-
-    {#if showModal}
-        <div class="modal-container">
-            <div class="modal-content">
-                <form on:submit|preventDefault={handleSubmit}>
-                    <div class="row">
-                        <div class="col">
-                            <input type="text" class="form-control" placeholder="stock code or name"
-                                   name="stock_code"
-                                   oninput="this.value = this.value.toUpperCase()"
-                                   autocomplete="off" autofocus>
-                        </div>
-                        <div class="col">
-                            <input type="reset" value="cancel" class="btn btn-danger" on:click={closeOrOpenModal}/>
-                            <input type="submit" value="search" class="btn btn-primary"/>
-                        </div>
-                    </div>
-                </form>
-
-                {#if searchStockResult.length > 0}
-                    <div class="modal-result">
-                        <div class="card" style="width: 100%;">
-                            <ul class="list-group list-group-flush">
-                                {#each searchStockResult as result}
-                                    <li class="list-group-item d-flex justify-content-between align-items-center"
-                                        on:click={addSelectedStock(result)}>
-                                        ({result.code}) {result.name}
-                                        <button class="btn btn-primary btn-sm">+</button>
-                                    </li>
-                                {/each}
-                            </ul>
-                        </div>
-                    </div>
-                {/if}
-            </div>
-        </div>
-    {/if}
-
-    <div in:fade class="table-container">
-        <table class="stock-table">
-            <thead>
-            <tr>
-                <th>Total Value</th>
-                <th>Total Assets</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr>
-                <td class="code">{formatCurrency(totalValue)}</td>
-                <td class="code">{totalAssets}</td>
-            </tr>
-            </tbody>
-        </table>
-    </div>
-
-    <div in:fade class="table-container">
-        <table class="stock-table">
-            <thead>
-            <tr>
-                <th>Code</th>
-                <th>Name</th>
-                <th>Qty</th>
-                <th>Price</th>
-                <th>Total</th>
-                <th>% of Portfolio</th>
-                <th scope="col"></th>
-            </tr>
-            </thead>
-            <tbody>
-            {#each result as stock}
-                <tr>
-                    <td class="code">{stock.code}</td>
-                    <td class="name">{stock.name}</td>
-                    <td class="qty-edit">
-                        <form id="updateQuantity" on:submit|preventDefault={updateStockQuantity}>
-                            <input type="hidden" name="code" value="{stock.code}"/>
-                            <input type="number" class="qty-input" name="quantity" value="{stock.quantity}" on:input={handleInputChange}/>
-                        </form>
-                    </td>
-                    <td class="price">{formatCurrency(stock.price)}</td>
-                    <td class="total">{formatCurrency(stock.total)}</td>
-                    <td class="percent">{calculatePercentage(stock.total, totalValue)}%</td>
-                    <td>
-                        <button class="remove-btn" on:click={() => remove(stock.code)} title="remove"></button>
-                    </td>
-                </tr>
-            {/each}
-            </tbody>
-        </table>
-    </div>
+	<div class="button-container">
+		<button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#exampleModal" on:click={closeOrOpenModal}>
+			Add
+		</button>
+
+		<!-- Dropdown for ordering the list -->
+		<select class="form-control order-select" on:change={updateOrderBy}>
+			<option value="code">Order by Code</option>
+			<option value="name">Order by Name</option>
+			<option value="total" selected>Order by Total</option>
+		</select>
+	</div>
+
+	{#if showModal}
+		<div class="modal-container">
+			<div class="modal-content">
+				<form on:submit|preventDefault={handleSubmit}>
+					<div class="row">
+						<div class="col">
+							<input type="text" class="form-control" placeholder="stock code or name"
+										 name="stock_code"
+										 oninput="this.value = this.value.toUpperCase()"
+										 autocomplete="off" autofocus>
+						</div>
+						<div class="col">
+							<input type="reset" value="cancel" class="btn btn-danger" on:click={closeOrOpenModal} />
+							<input type="submit" value="search" class="btn btn-primary" />
+						</div>
+					</div>
+				</form>
+
+				{#if searchStockResult.length > 0}
+					<div class="modal-result">
+						<div class="card" style="width: 100%;">
+							<ul class="list-group list-group-flush">
+								{#each searchStockResult as result}
+									<li class="list-group-item d-flex justify-content-between align-items-center"
+											on:click={addSelectedStock(result)}>
+										({result.code}) {result.name}
+										<button class="btn btn-primary btn-sm">+</button>
+									</li>
+								{/each}
+							</ul>
+						</div>
+					</div>
+				{/if}
+			</div>
+		</div>
+	{/if}
+
+	<div in:fade class="table-container">
+		<table class="stock-table">
+			<thead>
+			<tr>
+				<th>Total Value</th>
+				<th>Total Assets</th>
+			</tr>
+			</thead>
+			<tbody>
+			<tr>
+				<td class="code">{formatCurrency(totalValue)}</td>
+				<td class="code">{totalAssets}</td>
+			</tr>
+			</tbody>
+		</table>
+	</div>
+
+	<div in:fade class="table-container">
+		<table class="stock-table">
+			<thead>
+			<tr>
+				<th>Code</th>
+				<th>Name</th>
+				<th>Qty</th>
+				<th>Price</th>
+				<th>Total</th>
+				<th>% of Portfolio</th>
+				<th scope="col"></th>
+			</tr>
+			</thead>
+			<tbody>
+			{#each result as stock}
+				<tr>
+					<td class="code">{stock.code}</td>
+					<td class="name">{stock.name}</td>
+					<td class="qty-edit">
+						<form id="updateQuantity" on:submit|preventDefault={updateStockQuantity}>
+							<input type="hidden" name="code" value="{stock.code}" />
+							<input type="number" class="qty-input" name="quantity" value="{stock.quantity}"
+										 on:input={handleInputChange} />
+						</form>
+					</td>
+					<td class="price">{formatCurrency(stock.price)}</td>
+					<td class="total">{formatCurrency(stock.total)}</td>
+					<td class="percent">{calculatePercentage(stock.total, totalValue)}%</td>
+					<td>
+						<button class="remove-btn" on:click={() => remove(stock.code)} title="remove"></button>
+					</td>
+				</tr>
+			{/each}
+			</tbody>
+		</table>
+	</div>
 {:else}
-    <div>No portfolio data available.</div>
+	<div>No portfolio data available.</div>
 {/if}