| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- <script>
- import { onMount } from 'svelte';
- import { Chart, registerables } from 'chart.js';
- import { getRequest } from '../utils/api.js';
- import { fade } from 'svelte/transition';
- import ChartDataLabels from 'chartjs-plugin-datalabels';
- Chart.register(...registerables, ChartDataLabels);
- export let code;
- const BASE_URL = import.meta.env.VITE_STOCKS_HOST;
- let chartCanvas;
- let chartInstance;
- let selectedRange = '5d';
- let isLoading = true;
- let currentPrice = null;
- let currentCurrency = '';
- let priceChange = null;
- let lastUpdated = null;
- const ranges = ['5d', '30d', '6m', '1y'];
- async function fetchData(range) {
- try {
- const res = await getRequest(`${BASE_URL}/api/stocks/${code}/history?range=${range}`, {}, null);
- if (!res.ok) {
- console.error(`Failed to fetch price history: ${res.status}`);
- return [];
- }
- return await res.json();
- } catch (err) {
- console.error('Error fetching stock history:', err);
- return [];
- } finally {
- isLoading = false;
- }
- }
- function renderChart(data) {
- const labels = data.map((item) => {
- const date = new Date(item.createdAt);
- const dd = String(date.getDate()).padStart(2, '0');
- const mm = String(date.getMonth() + 1).padStart(2, '0');
- const yyyy = date.getFullYear();
- return `${dd}/${mm}/${yyyy}`;
- });
- const prices = data.map((item) => item.price);
- if (chartInstance) {
- chartInstance.destroy();
- }
- chartInstance = new Chart(chartCanvas, {
- type: 'line',
- data: {
- labels,
- datasets: [
- {
- label: `Price (${data[0]?.currency || ''})`,
- data: prices,
- fill: false,
- borderWidth: 2
- }
- ]
- },
- options: {
- responsive: true,
- scales: {
- x: {
- title: {
- display: true,
- text: 'Date'
- }
- },
- y: {
- title: {
- display: true,
- text: 'Price'
- }
- }
- },
- plugins: {
- legend: {
- display: false
- },
- tooltip: {
- callbacks: {
- title: (context) => {
- const idx = context[0].dataIndex;
- const raw = data[idx];
- const dt = new Date(raw.createdAt);
- return dt.toLocaleString('en-GB', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false
- });
- },
- label: (context) => {
- return `Price: ${context.parsed.y.toFixed(2)}`;
- }
- }
- },
- datalabels: {
- color: '#fff',
- anchor: 'end',
- align: 'top',
- font: {
- weight: 'bold'
- },
- formatter: (value) => value.toFixed(2)
- }
- }
- }
- });
- }
- async function updateChart(range) {
- selectedRange = range;
- isLoading = true;
- const [history, latestRes] = await Promise.all([
- fetchData(range),
- getRequest(`${BASE_URL}/api/stocks/${code}`, {}, null)
- ]);
- isLoading = false;
- let data = history;
- if (latestRes?.ok) {
- const latest = await latestRes.json();
- const lastHist = history[history.length - 1];
- if (new Date(latest.createdAt) > new Date(lastHist.createdAt)) {
- data = [...history, latest];
- }
- }
- if (data.length > 0) {
- const first = data[0].price;
- const lastEntry = data[data.length - 1];
- currentPrice = lastEntry.price;
- currentCurrency = lastEntry.currency || '';
- priceChange = first !== 0 ? ((lastEntry.price - first) / first) * 100 : null;
- lastUpdated = new Date(lastEntry.createdAt);
- renderChart(data);
- }
- }
- onMount(() => {
- updateChart(selectedRange);
- });
- </script>
- {#if isLoading}
- <div in:fade class="flex justify-center items-center py-10">
- <svg
- class="animate-spin h-8 w-8 text-blue-500 dark:text-blue-300"
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- >
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
- ></circle>
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
- </svg>
- </div>
- {:else}
- <div class="w-full mt-10 max-w-6xl mx-auto bg-white dark:bg-gray-900 p-6 rounded-xl shadow-md">
- <h3 class="text-xl font-semibold text-gray-800 dark:text-gray-100 mb-4">
- Price History
- {#if currentPrice !== null}
- <span class="ml-2 text-blue-500 dark:text-blue-300 text-lg font-medium">
- ({currentCurrency} {currentPrice.toFixed(2)}
- {#if priceChange !== null}
- <span class="{priceChange >= 0 ? 'text-green-500' : 'text-red-500'} text-sm font-semibold ml-1 align-middle">
- {priceChange >= 0 ? '+' : ''}{priceChange.toFixed(2)}%
- </span>
- {/if}
- )
- </span>
- {/if}
- </h3>
- {#if lastUpdated}
- <p class="text-xs text-gray-500 dark:text-gray-600 mb-4">
- Last updated: {lastUpdated.toLocaleString('en-GB', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- hour12: true
- })}
- </p>
- {/if}
- <canvas bind:this={chartCanvas}></canvas>
- <div class="flex justify-center gap-4 mt-6">
- {#each ranges as range}
- <button
- class="px-4 py-2 rounded-md text-sm font-medium transition cursor-pointer
- {selectedRange === range
- ? 'bg-blue-500 text-white'
- : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-100'}"
- on:click={() => updateChart(range)}
- >
- {range.toUpperCase()}
- </button>
- {/each}
- </div>
- </div>
- {/if}
|