add bulk download feature
This commit is contained in:
parent
986b1c681a
commit
a3728a62e8
45
src/routes/api/bulk-download/+server.ts
Normal file
45
src/routes/api/bulk-download/+server.ts
Normal file
@ -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" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -29,6 +29,7 @@
|
|||||||
let searchQuery = "";
|
let searchQuery = "";
|
||||||
let switchWatchlist = false;
|
let switchWatchlist = false;
|
||||||
let editMode = false;
|
let editMode = false;
|
||||||
|
let realtimeUpdates = true;
|
||||||
let numberOfChecked = 0;
|
let numberOfChecked = 0;
|
||||||
let activeIdx = 0;
|
let activeIdx = 0;
|
||||||
let rawTabData = [];
|
let rawTabData = [];
|
||||||
@ -1046,6 +1047,38 @@
|
|||||||
// Sort and update the originalData and stockList
|
// Sort and update the originalData and stockList
|
||||||
watchList = [...originalData].sort(compareValues)?.slice(0, 50);
|
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;`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
@ -1064,7 +1097,7 @@
|
|||||||
<main class="w-full">
|
<main class="w-full">
|
||||||
{#if isLoaded}
|
{#if isLoaded}
|
||||||
<div
|
<div
|
||||||
class="flex w-full sm:w-[50%] md:w-auto mb-10 {!data?.user
|
class="flex w-full sm:w-[50%] md:w-auto mb-5 {!data?.user
|
||||||
? 'hidden'
|
? 'hidden'
|
||||||
: 'md:block'}"
|
: 'md:block'}"
|
||||||
>
|
>
|
||||||
@ -1107,15 +1140,18 @@
|
|||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-56 h-fit max-h-72 overflow-y-auto scroller"
|
class="w-56 h-fit max-h-72 overflow-y-auto scroller"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label class="text-gray-400">
|
<DropdownMenu.Label>
|
||||||
<DropdownMenu.Trigger asChild let:builder>
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
<Button
|
<Button
|
||||||
on:click={handleWatchlistModal}
|
|
||||||
builders={[builder]}
|
builders={[builder]}
|
||||||
class="p-0 -mb-2 -mt-2 text-sm inline-flex cursor-pointer items-center justify-center space-x-1 bg-white dark:bg-default whitespace-nowrap focus:outline-hidden sm:text-smaller"
|
class="p-0 -mb-2 -mt-2 text-sm inline-flex cursor-pointer items-center justify-center space-x-1 bg-white dark:bg-default whitespace-nowrap focus:outline-hidden"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="addWatchlist"
|
||||||
|
class="flex flex-row items-center cursor-pointer"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="h-4 w-4"
|
class="h-4 w-4 mr-1"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
style="max-width:40px"
|
style="max-width:40px"
|
||||||
@ -1127,7 +1163,10 @@
|
|||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-sm text-start">New Watchlist</div>
|
<div class="text-sm text-start">
|
||||||
|
New Watchlist
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
@ -1255,7 +1294,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
on:input={search}
|
on:input={search}
|
||||||
class="text-sm sm:text-[1rem] controls-input shadow-sm focus:outline-hidden border border-gray-300 dark:border-gray-600 rounded placeholder:/80 px-3 py-2 pl-8 xs:pl-10 grow w-full sm:min-w-56 max-w-xs"
|
class="text-sm sm:text-[1rem] controls-input shadow-sm focus:outline-hidden border border-gray-300 dark:border-gray-600 rounded placeholder:text-gray-600 dark:placeholder:text-gray-200 px-3 py-2 pl-8 xs:pl-10 grow w-full sm:min-w-56 max-w-xs"
|
||||||
placeholder="Add new stock"
|
placeholder="Add new stock"
|
||||||
aria-label="Add new stock"
|
aria-label="Add new stock"
|
||||||
/>
|
/>
|
||||||
@ -1294,7 +1333,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="order-0 sm:order-4 w-full {displayWatchList?.title ===
|
class="order-0 sm:order-4 w-full sm:mr-3 {displayWatchList?.title ===
|
||||||
undefined
|
undefined
|
||||||
? 'hidden'
|
? 'hidden'
|
||||||
: ''}"
|
: ''}"
|
||||||
@ -1462,6 +1501,87 @@
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="order-3 sm:order-last {displayWatchList?.title ===
|
||||||
|
undefined
|
||||||
|
? 'hidden'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
|
<Button
|
||||||
|
builders={[builder]}
|
||||||
|
class="shadow-sm min-w-[110px] w-full sm:w-fit border-gray-300 dark:border-gray-600 border sm:hover:bg-gray-200 dark:sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2.5 rounded truncate"
|
||||||
|
>
|
||||||
|
<span class="truncate text-sm sm:text-[1rem]"
|
||||||
|
>Options</span
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="-mr-1 ml-2 h-5 w-5 inline-block"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
style="max-width:40px"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-auto max-w-60 max-h-[400px] overflow-y-auto scroller relative"
|
||||||
|
>
|
||||||
|
<!-- Dropdown items -->
|
||||||
|
<DropdownMenu.Group class="pb-2">
|
||||||
|
<!-- Added padding to avoid overlapping with Reset button -->
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="sm:hover:bg-gray-200 dark:sm:hover:bg-primary cursor-pointer mt-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click={downloadHistoricalData}
|
||||||
|
class="flex items-center cursor-pointer"
|
||||||
|
>
|
||||||
|
Bulk Download <span class="ml-2 text-xs"
|
||||||
|
>({data?.user?.credits} Credits left)</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="sm:hover:bg-gray-200 dark:sm:hover:bg-primary"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
on:click|capture={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
realtimeUpdates = !realtimeUpdates;
|
||||||
|
}}
|
||||||
|
class="inline-flex justify-between w-full items-center cursor-pointer"
|
||||||
|
>
|
||||||
|
<span class="mr-2 text-sm">Realtime Updates</span>
|
||||||
|
<div class="relative ml-auto">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={realtimeUpdates}
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-9 h-5 bg-gray-400 rounded-full peer peer-checked:bg-blue-600
|
||||||
|
after:content-[''] after:absolute after:top-0.5 after:left-[2px]
|
||||||
|
after:bg-white after:border-gray-300 after:border
|
||||||
|
after:rounded-full after:h-4 after:w-4 after:transition-all
|
||||||
|
peer-checked:after:translate-x-full"
|
||||||
|
></div>
|
||||||
|
</div></label
|
||||||
|
>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1630,7 +1750,8 @@
|
|||||||
>{item[row?.rule]}</span
|
>{item[row?.rule]}</span
|
||||||
>
|
>
|
||||||
{:else if item[row?.rule] === "Hold"}
|
{:else if item[row?.rule] === "Hold"}
|
||||||
<span class="text-[#FFA838]"
|
<span
|
||||||
|
class="text-orange-700 dark:text-[#FFA838]"
|
||||||
>{item[row?.rule]}</span
|
>{item[row?.rule]}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user