StockPriceHistory.svelte 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <script>
  2. import { onMount } from 'svelte';
  3. import { Chart, registerables } from 'chart.js';
  4. import { getRequest } from '../utils/api.js';
  5. import { fade } from 'svelte/transition';
  6. import ChartDataLabels from 'chartjs-plugin-datalabels';
  7. Chart.register(...registerables, ChartDataLabels);
  8. export let code;
  9. const BASE_URL = import.meta.env.VITE_STOCKS_HOST;
  10. let chartCanvas;
  11. let chartInstance;
  12. let selectedRange = '5d';
  13. let isLoading = true;
  14. let currentPrice = null;
  15. let currentCurrency = '';
  16. let priceChange = null;
  17. let lastUpdated = null;
  18. const ranges = ['5d', '30d', '6m', '1y'];
  19. async function fetchData(range) {
  20. try {
  21. const res = await getRequest(`${BASE_URL}/api/stocks/${code}/history?range=${range}`, {}, null);
  22. if (!res.ok) {
  23. console.error(`Failed to fetch price history: ${res.status}`);
  24. return [];
  25. }
  26. return await res.json();
  27. } catch (err) {
  28. console.error('Error fetching stock history:', err);
  29. return [];
  30. } finally {
  31. isLoading = false;
  32. }
  33. }
  34. function renderChart(data) {
  35. const labels = data.map((item) => {
  36. const date = new Date(item.createdAt);
  37. const dd = String(date.getDate()).padStart(2, '0');
  38. const mm = String(date.getMonth() + 1).padStart(2, '0');
  39. const yyyy = date.getFullYear();
  40. return `${dd}/${mm}/${yyyy}`;
  41. });
  42. const prices = data.map((item) => item.price);
  43. if (chartInstance) {
  44. chartInstance.destroy();
  45. }
  46. chartInstance = new Chart(chartCanvas, {
  47. type: 'line',
  48. data: {
  49. labels,
  50. datasets: [
  51. {
  52. label: `Price (${data[0]?.currency || ''})`,
  53. data: prices,
  54. fill: false,
  55. borderWidth: 2
  56. }
  57. ]
  58. },
  59. options: {
  60. responsive: true,
  61. scales: {
  62. x: {
  63. title: {
  64. display: true,
  65. text: 'Date'
  66. }
  67. },
  68. y: {
  69. title: {
  70. display: true,
  71. text: 'Price'
  72. }
  73. }
  74. },
  75. plugins: {
  76. legend: {
  77. display: false
  78. },
  79. tooltip: {
  80. callbacks: {
  81. title: (context) => {
  82. const idx = context[0].dataIndex;
  83. const raw = data[idx];
  84. const dt = new Date(raw.createdAt);
  85. return dt.toLocaleString('en-GB', {
  86. day: '2-digit',
  87. month: '2-digit',
  88. year: 'numeric',
  89. hour: '2-digit',
  90. minute: '2-digit',
  91. second: '2-digit',
  92. hour12: false
  93. });
  94. },
  95. label: (context) => {
  96. return `Price: ${context.parsed.y.toFixed(2)}`;
  97. }
  98. }
  99. },
  100. datalabels: {
  101. color: '#fff',
  102. anchor: 'end',
  103. align: 'top',
  104. font: {
  105. weight: 'bold'
  106. },
  107. formatter: (value) => value.toFixed(2)
  108. }
  109. }
  110. }
  111. });
  112. }
  113. async function updateChart(range) {
  114. selectedRange = range;
  115. isLoading = true;
  116. const [history, latestRes] = await Promise.all([
  117. fetchData(range),
  118. getRequest(`${BASE_URL}/api/stocks/${code}`, {}, null)
  119. ]);
  120. isLoading = false;
  121. let data = history;
  122. if (latestRes?.ok) {
  123. const latest = await latestRes.json();
  124. const lastHist = history[history.length - 1];
  125. if (new Date(latest.createdAt) > new Date(lastHist.createdAt)) {
  126. data = [...history, latest];
  127. }
  128. }
  129. if (data.length > 0) {
  130. const first = data[0].price;
  131. const lastEntry = data[data.length - 1];
  132. currentPrice = lastEntry.price;
  133. currentCurrency = lastEntry.currency || '';
  134. priceChange = first !== 0 ? ((lastEntry.price - first) / first) * 100 : null;
  135. lastUpdated = new Date(lastEntry.createdAt);
  136. renderChart(data);
  137. }
  138. }
  139. onMount(() => {
  140. updateChart(selectedRange);
  141. });
  142. </script>
  143. {#if isLoading}
  144. <div in:fade class="flex justify-center items-center py-10">
  145. <svg
  146. class="animate-spin h-8 w-8 text-blue-500 dark:text-blue-300"
  147. xmlns="http://www.w3.org/2000/svg"
  148. fill="none"
  149. viewBox="0 0 24 24"
  150. >
  151. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
  152. ></circle>
  153. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
  154. </svg>
  155. </div>
  156. {:else}
  157. <div class="w-full mt-10 max-w-6xl mx-auto bg-white dark:bg-gray-900 p-6 rounded-xl shadow-md">
  158. <h3 class="text-xl font-semibold text-gray-800 dark:text-gray-100 mb-4">
  159. Price History
  160. {#if currentPrice !== null}
  161. <span class="ml-2 text-blue-500 dark:text-blue-300 text-lg font-medium">
  162. ({currentCurrency} {currentPrice.toFixed(2)}
  163. {#if priceChange !== null}
  164. <span class="{priceChange >= 0 ? 'text-green-500' : 'text-red-500'} text-sm font-semibold ml-1 align-middle">
  165. {priceChange >= 0 ? '+' : ''}{priceChange.toFixed(2)}%
  166. </span>
  167. {/if}
  168. )
  169. </span>
  170. {/if}
  171. </h3>
  172. {#if lastUpdated}
  173. <p class="text-xs text-gray-500 dark:text-gray-600 mb-4">
  174. Last updated: {lastUpdated.toLocaleString('en-GB', {
  175. day: '2-digit',
  176. month: '2-digit',
  177. year: 'numeric',
  178. hour: '2-digit',
  179. minute: '2-digit',
  180. hour12: true
  181. })}
  182. </p>
  183. {/if}
  184. <canvas bind:this={chartCanvas}></canvas>
  185. <div class="flex justify-center gap-4 mt-6">
  186. {#each ranges as range}
  187. <button
  188. class="px-4 py-2 rounded-md text-sm font-medium transition cursor-pointer
  189. {selectedRange === range
  190. ? 'bg-blue-500 text-white'
  191. : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-100'}"
  192. on:click={() => updateChart(range)}
  193. >
  194. {range.toUpperCase()}
  195. </button>
  196. {/each}
  197. </div>
  198. </div>
  199. {/if}