From a3728a62e8213522bbd0264d2c88a99f957d675a Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Mon, 24 Mar 2025 22:51:48 +0100 Subject: [PATCH] add bulk download feature --- src/routes/api/bulk-download/+server.ts | 45 +++++++ src/routes/watchlist/stocks/+page.svelte | 161 ++++++++++++++++++++--- 2 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 src/routes/api/bulk-download/+server.ts diff --git a/src/routes/api/bulk-download/+server.ts b/src/routes/api/bulk-download/+server.ts new file mode 100644 index 00000000..051266e5 --- /dev/null +++ b/src/routes/api/bulk-download/+server.ts @@ -0,0 +1,45 @@ +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, locals }) => { + const data = await request.json(); + const tickers = data?.tickers; // tickers assumed to be an array + const { apiURL, apiKey, user, pb } = locals; + + // Check if user has enough credits + if (user?.credits < tickers?.length) { + return new Response(JSON.stringify({ error: "Insufficient credits" }), { status: 400 }); + } + + const postData = { tickers }; + const response = await fetch(apiURL + "/bulk-download", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + body: JSON.stringify(postData), + }); + + // Deduct credits after a successful request + await pb.collection('users').update(user?.id, { + 'credits': user?.credits - tickers?.length + }); + + const contentType = response.headers.get("content-type") || ""; + + if (contentType.includes("application/zip")) { + // If the backend returned a zip file, forward the binary response + return new Response(response.body, { + headers: { + "Content-Type": "application/zip", + "Content-Disposition": "attachment; filename=bulk_data.zip" + } + }); + } else { + // Otherwise, assume a JSON response + const json = await response.json(); + return new Response(JSON.stringify(json), { + headers: { "Content-Type": "application/json" } + }); + } +}; diff --git a/src/routes/watchlist/stocks/+page.svelte b/src/routes/watchlist/stocks/+page.svelte index 04675537..79548b86 100644 --- a/src/routes/watchlist/stocks/+page.svelte +++ b/src/routes/watchlist/stocks/+page.svelte @@ -29,6 +29,7 @@ let searchQuery = ""; let switchWatchlist = false; let editMode = false; + let realtimeUpdates = true; let numberOfChecked = 0; let activeIdx = 0; let rawTabData = []; @@ -1046,6 +1047,38 @@ // Sort and update the originalData and stockList watchList = [...originalData].sort(compareValues)?.slice(0, 50); }; + + async function downloadHistoricalData() { + const tickers = watchList?.map((item) => item?.symbol); // example tickers + if (data?.user?.credits > tickers?.length && tickers?.length > 0) { + data.user.credits = data?.user?.credits - tickers?.length; + const response = await fetch("/api/bulk-download", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tickers }), + }); + + if (response.ok) { + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "historical_data.zip"; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + } else if (tickers?.length === 0) { + toast.error("Add tickers first to your watchlist", { + style: `border-radius: 5px; background: #fff; color: #000; border-color: ${$mode === "light" ? "#F9FAFB" : "#4B5563"}; font-size: 15px;`, + }); + } else { + toast.error("Not enough credits", { + style: `border-radius: 5px; background: #fff; color: #000; border-color: ${$mode === "light" ? "#F9FAFB" : "#4B5563"}; font-size: 15px;`, + }); + } + } {#if isLoaded}
@@ -1107,27 +1140,33 @@ - + @@ -1255,7 +1294,7 @@
@@ -1294,7 +1333,7 @@
+ +
+ + + + + + + + + + + + + + + + + + +
@@ -1630,7 +1750,8 @@ >{item[row?.rule]} {:else if item[row?.rule] === "Hold"} - {item[row?.rule]} {/if}