add download button
This commit is contained in:
parent
f3d75ea951
commit
415029d0ac
@ -1,187 +1,303 @@
|
||||
<script lang="ts">
|
||||
import { etfTicker, numberOfUnreadNotification, displayCompanyName} from '$lib/store';
|
||||
|
||||
import { screenWidth } from '$lib/store';
|
||||
import { abbreviateNumber, formatString } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
etfTicker,
|
||||
numberOfUnreadNotification,
|
||||
displayCompanyName,
|
||||
} from "$lib/store";
|
||||
import { Button } from "$lib/components/shadcn/button/index.js";
|
||||
import { screenWidth } from "$lib/store";
|
||||
import { abbreviateNumber, formatString } from "$lib/utils";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import TableHeader from "$lib/components/Table/TableHeader.svelte";
|
||||
|
||||
export let data;
|
||||
let rawData = data?.getETFHoldings;
|
||||
let holdings = rawData?.slice(0,50);
|
||||
let holdings = rawData?.slice(0, 50);
|
||||
|
||||
let category = 'weights';
|
||||
let order = 'highToLow';
|
||||
function changeOrder(newCategory: string) {
|
||||
if (newCategory === category) {
|
||||
order = order === 'highToLow' ? 'lowToHigh' : 'highToLow';
|
||||
} else {
|
||||
category = newCategory;
|
||||
order = 'highToLow';
|
||||
}
|
||||
holdings = sortElements(category, rawData)?.slice(0,50);
|
||||
}
|
||||
|
||||
const sortElements = (category, tickerList) => {
|
||||
return tickerList?.sort((a, b) => {
|
||||
const aValue = category === 'weights' ? a?.weightPercentage : a?.sharesNumber;
|
||||
const bValue = category === 'weights' ? b?.weightPercentage : b?.sharesNumber;
|
||||
return order === 'highToLow' ? bValue - aValue : aValue - bValue;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleScroll() {
|
||||
async function handleScroll() {
|
||||
const scrollThreshold = document.body.offsetHeight * 0.8; // 80% of the website height
|
||||
const isBottom = window.innerHeight + window.scrollY >= scrollThreshold;
|
||||
if (isBottom && holdings?.length !== rawData?.length) {
|
||||
const nextIndex = holdings?.length;
|
||||
const filteredNewResults = rawData?.slice(nextIndex, nextIndex + 25);
|
||||
holdings = [...holdings, ...filteredNewResults];
|
||||
const nextIndex = holdings?.length;
|
||||
const filteredNewResults = rawData?.slice(nextIndex, nextIndex + 25);
|
||||
holdings = [...holdings, ...filteredNewResults];
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
$: charNumber = $screenWidth < 640 ? 20 : 30;
|
||||
|
||||
})
|
||||
let columns = [
|
||||
{ key: "asset", label: "Symbol", align: "left" },
|
||||
{ key: "name", label: "Name", align: "left" },
|
||||
{ key: "sharesNumber", label: "Shares", align: "right" },
|
||||
{ key: "weightPercentage", label: "% Weight", align: "right" },
|
||||
];
|
||||
|
||||
$: charNumber = $screenWidth < 640 ? 20 : 30
|
||||
let sortOrders = {
|
||||
asset: { order: "none", type: "string" },
|
||||
name: { order: "none", type: "string" },
|
||||
sharesNumber: { order: "none", type: "number" },
|
||||
weightPercentage: { order: "none", type: "number" },
|
||||
};
|
||||
|
||||
const sortData = (key) => {
|
||||
// Reset all other keys to 'none' except the current key
|
||||
for (const k in sortOrders) {
|
||||
if (k !== key) {
|
||||
sortOrders[k].order = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through 'none', 'asc', 'desc' for the clicked key
|
||||
const orderCycle = ["none", "asc", "desc"];
|
||||
|
||||
let originalData = rawData;
|
||||
|
||||
const currentOrderIndex = orderCycle.indexOf(sortOrders[key].order);
|
||||
sortOrders[key].order =
|
||||
orderCycle[(currentOrderIndex + 1) % orderCycle.length];
|
||||
const sortOrder = sortOrders[key].order;
|
||||
|
||||
// Reset to original data when 'none' and stop further sorting
|
||||
if (sortOrder === "none") {
|
||||
holdings = [...originalData]?.slice(0, 50); // Reset to original data (spread to avoid mutation)
|
||||
return;
|
||||
}
|
||||
|
||||
// Define a generic comparison function
|
||||
const compareValues = (a, b) => {
|
||||
const { type } = sortOrders[key];
|
||||
let valueA, valueB;
|
||||
|
||||
switch (type) {
|
||||
case "date":
|
||||
valueA = new Date(a[key]);
|
||||
valueB = new Date(b[key]);
|
||||
break;
|
||||
case "string":
|
||||
valueA = a[key].toUpperCase();
|
||||
valueB = b[key].toUpperCase();
|
||||
return sortOrder === "asc"
|
||||
? valueA.localeCompare(valueB)
|
||||
: valueB.localeCompare(valueA);
|
||||
case "number":
|
||||
default:
|
||||
valueA = parseFloat(a[key]);
|
||||
valueB = parseFloat(b[key]);
|
||||
break;
|
||||
}
|
||||
|
||||
if (sortOrder === "asc") {
|
||||
return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
||||
} else {
|
||||
return valueA > valueB ? -1 : valueA < valueB ? 1 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Sort using the generic comparison function
|
||||
holdings = [...originalData].sort(compareValues)?.slice(0, 50);
|
||||
};
|
||||
|
||||
const exportData = (format = "csv") => {
|
||||
if (data?.user?.tier === "Pro") {
|
||||
// Add headers row
|
||||
const csvRows = [];
|
||||
csvRows.push("Symbol,Name,Shares,Weight");
|
||||
|
||||
// Add data rows
|
||||
rawData.forEach((item) => {
|
||||
const csvRow = `${item?.asset},${item?.name},${item?.sharesNumber},${item?.weightPercentage}`;
|
||||
csvRows.push(csvRow);
|
||||
});
|
||||
|
||||
// Create CSV blob and trigger download
|
||||
const csv = csvRows.join("\n");
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.setAttribute("hidden", "");
|
||||
a.setAttribute("href", url);
|
||||
a.setAttribute("download", `${$etfTicker}_holdings.csv`);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
} else {
|
||||
goto("/pricing");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<svelte:head>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>
|
||||
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ''} {$displayCompanyName} ({$etfTicker}) Holdings List · stocknear
|
||||
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""}
|
||||
{$displayCompanyName} ({$etfTicker}) Holdings List · stocknear
|
||||
</title>
|
||||
<meta name="description" content={`Get the Holdings List of ${$displayCompanyName} (${$etfTicker}).`} />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content={`Get the Holdings List of ${$displayCompanyName} (${$etfTicker}).`}
|
||||
/>
|
||||
|
||||
<!-- Other meta tags -->
|
||||
<meta property="og:title" content={`${$displayCompanyName} (${$etfTicker}) Holdings List · stocknear`}/>
|
||||
<meta property="og:description" content={`Get the Holdings List of ${$displayCompanyName} (${$etfTicker}).`} />
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${$displayCompanyName} (${$etfTicker}) Holdings List · stocknear`}
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content={`Get the Holdings List of ${$displayCompanyName} (${$etfTicker}).`}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<!-- Add more Open Graph meta tags as needed -->
|
||||
|
||||
<!-- Twitter specific meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image"/>
|
||||
<meta name="twitter:title" content={`${$displayCompanyName} (${$etfTicker}) Holdings List · stocknear`}/>
|
||||
<meta name="twitter:description" content={`Get the Holdings List of ${$displayCompanyName} (${$etfTicker}).`} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={`${$displayCompanyName} (${$etfTicker}) Holdings List · stocknear`}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={`Get the Holdings List of ${$displayCompanyName} (${$etfTicker}).`}
|
||||
/>
|
||||
<!-- Add more Twitter meta tags as needed -->
|
||||
|
||||
</svelte:head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#09090B] overflow-hidden text-white h-full mb-40 sm:mb-0 w-full">
|
||||
<section
|
||||
class="bg-[#09090B] overflow-hidden text-white h-full mb-40 sm:mb-0 w-full"
|
||||
>
|
||||
<div class="flex justify-center m-auto h-full overflow-hidden w-full">
|
||||
<div class="relative flex justify-center items-center overflow-hidden w-full">
|
||||
<div class="sm:p-7 w-full m-auto mt-2 sm:mt-0">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl sm:text-3xl text-gray-200 font-bold mb-4">
|
||||
ETF Holdings
|
||||
</h2>
|
||||
<div
|
||||
class="relative flex justify-center items-center overflow-hidden w-full"
|
||||
>
|
||||
<div class="sm:p-7 w-full m-auto mt-2 sm:mt-0">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl sm:text-3xl text-gray-200 font-bold mb-4">
|
||||
ETF Holdings
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="text-white p-3 sm:p-5 mb-10 rounded-lg sm:flex sm:flex-row sm:items-center border border-slate-800 text-sm sm:text-[1rem]"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 flex-shrink-0 inline-block sm:mr-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
><path
|
||||
fill="#a474f6"
|
||||
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m-4 48a12 12 0 1 1-12 12a12 12 0 0 1 12-12m12 112a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 0 16"
|
||||
/></svg
|
||||
>
|
||||
|
||||
<div class="text-white p-3 sm:p-5 mb-10 rounded-lg sm:flex sm:flex-row sm:items-center border border-slate-800 text-sm sm:text-[1rem]">
|
||||
<svg class="w-6 h-6 flex-shrink-0 inline-block sm:mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#a474f6" d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m-4 48a12 12 0 1 1-12 12a12 12 0 0 1 12-12m12 112a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 0 16"/></svg>
|
||||
|
||||
{#if rawData?.length !== 0}
|
||||
The {$displayCompanyName} holds {rawData?.length} different assets
|
||||
and the largest one in the portfolio is {formatString(rawData?.at(0)?.name)}, making up {rawData?.at(0)?.weightPercentage?.toFixed(2)}% of the total.
|
||||
{:else}
|
||||
No information available for {$displayCompanyName}.
|
||||
{/if}
|
||||
{#if rawData?.length !== 0}
|
||||
The {$displayCompanyName} holds {rawData?.length} different assets
|
||||
and the largest one in the portfolio is {formatString(
|
||||
rawData?.at(0)?.name,
|
||||
)}, making up {rawData?.at(0)?.weightPercentage?.toFixed(2)}% of
|
||||
the total.
|
||||
{:else}
|
||||
No information available for {$displayCompanyName}.
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{#if holdings?.length !== 0}
|
||||
<div
|
||||
class="flex flex-row items-center justify-end w-fit sm:w-[50%] md:w-auto ml-auto"
|
||||
>
|
||||
<Button
|
||||
on:click={() => exportData("csv")}
|
||||
class="w-fit border-gray-600 border bg-[#09090B] sm:hover:bg-[#27272A] ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-lg truncate"
|
||||
>
|
||||
<span class="truncate text-white">Download</span>
|
||||
<svg
|
||||
class="{data?.user?.tier === 'Pro'
|
||||
? 'hidden'
|
||||
: ''} ml-1 -mt-0.5 w-3.5 h-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="#A3A3A3"
|
||||
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
||||
/></svg
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{#if holdings?.length !== 0}
|
||||
<div class="w-full overflow-x-auto mt-5">
|
||||
<table
|
||||
class="table table-sm table-compact rounded-none sm:rounded-md w-full bg-[#09090B] border-bg-[#09090B]"
|
||||
>
|
||||
<thead>
|
||||
<TableHeader {columns} {sortOrders} {sortData} />
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each holdings as item}
|
||||
<!-- row -->
|
||||
{#if item?.asset !== null}
|
||||
<tr
|
||||
class="w-full sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] odd:bg-[#27272A]"
|
||||
>
|
||||
<td
|
||||
class="text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B]"
|
||||
>
|
||||
<a
|
||||
href={item?.asset?.length !== 0 &&
|
||||
!["BTC", "USD"].includes(item?.asset)
|
||||
? `/stocks/${item?.asset}`
|
||||
: item?.asset === "BTC"
|
||||
? "/crypto/BTCUSD"
|
||||
: ""}
|
||||
class="sm:hover:text-white text-blue-400"
|
||||
>{item?.asset?.length !== 0 ? item?.asset : "-"}</a
|
||||
>
|
||||
</td>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
|
||||
|
||||
<table class="table table-pin-cols table-sm table-compact rounded-none sm:rounded-md w-full border-bg-[#09090B] m-auto mt-4 overflow-x-auto">
|
||||
<thead>
|
||||
<tr class="">
|
||||
<td class="text-white border-b border-[#09090B] bg-[#09090B] font-semibold text-sm sm:text-[1rem] whitespace-nowrap">Symbol</td>
|
||||
<td class="text-white border-b border-[#09090B] bg-[#09090B] font-semibold text-sm sm:text-[1rem] whitespace-nowrap ">Name</td>
|
||||
<td on:click={() => changeOrder('shares')} class="text-white border-b border-[#09090B] bg-[#09090B] font-semibold text-end text-sm sm:text-[1rem] whitespace-nowrap cursor-pointer">
|
||||
Shares
|
||||
<svg class="w-5 h-5 inline-block {order === 'highToLow' && category === 'shares' ? 'rotate-180' : ''}" viewBox="0 0 20 20" fill="currentColor" style="max-width:40px"><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>
|
||||
</td>
|
||||
<td on:click={() => changeOrder('weights')} class="text-white border-b border-[#09090B] bg-[#09090B] font-semibold text-end text-sm sm:text-[1rem] whitespace-nowrap cursor-pointer">
|
||||
% Weight
|
||||
<svg class="w-5 h-5 inline-block {order === 'highToLow' && category === 'weights' ? '' : 'rotate-180'}" viewBox="0 0 20 20" fill="currentColor" style="max-width:40px"><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>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each holdings as item}
|
||||
<!-- row -->
|
||||
{#if item?.asset !== null}
|
||||
<tr class="w-full sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] odd:bg-[#27272A]">
|
||||
|
||||
<td class="text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B]">
|
||||
<a href={item?.asset?.length !== 0 && !['BTC', 'USD'].includes(item?.asset) ? `/stocks/${item?.asset}` : item?.asset === 'BTC' ? '/crypto/BTCUSD' : '' } class="sm:hover:text-white text-blue-400">{item?.asset?.length !== 0 ? item?.asset : '-'}</a>
|
||||
</td>
|
||||
|
||||
<td class="text-white text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B]">
|
||||
{item?.name?.length > charNumber ? formatString(item?.name?.slice(0,charNumber)) + "..." : formatString(item?.name)}
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
<td class="text-white text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B] text-end">
|
||||
{abbreviateNumber(item?.sharesNumber)}
|
||||
</td>
|
||||
|
||||
|
||||
<td class="text-white text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B] text-end">
|
||||
{item?.weightPercentage >= 0.01 ? item?.weightPercentage?.toFixed(2) : '< 0.01'}%
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
{/if}
|
||||
{/each}
|
||||
<td
|
||||
class="text-white text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B]"
|
||||
>
|
||||
{item?.name?.length > charNumber
|
||||
? formatString(item?.name?.slice(0, charNumber)) +
|
||||
"..."
|
||||
: formatString(item?.name)}
|
||||
</td>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<td
|
||||
class="text-white text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B] text-end"
|
||||
>
|
||||
{abbreviateNumber(item?.sharesNumber)}
|
||||
</td>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="pl-4 pr-4 flex justify-center items-center text-md sm:text-lg text-center text-slate-200">
|
||||
No holdings are available for {$displayCompanyName}.
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<td
|
||||
class="text-white text-sm sm:text-[1rem] whitespace-nowrap border-b border-[#09090B] text-end"
|
||||
>
|
||||
{item?.weightPercentage >= 0.01
|
||||
? item?.weightPercentage?.toFixed(2)
|
||||
: "< 0.01"}%
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<h2
|
||||
class="pl-4 pr-4 flex justify-center items-center text-md sm:text-lg text-center text-slate-200"
|
||||
>
|
||||
No holdings are available for {$displayCompanyName}.
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user