533 lines
17 KiB
Svelte
533 lines
17 KiB
Svelte
<script lang="ts">
|
|
import { screenWidth } from "$lib/store";
|
|
import { abbreviateNumber } from "$lib/utils";
|
|
|
|
import VirtualList from "svelte-tiny-virtual-list";
|
|
import HoverStockChart from "$lib/components/HoverStockChart.svelte";
|
|
|
|
export let data;
|
|
export let displayedData = [];
|
|
export let filteredData = [];
|
|
export let rawData = [];
|
|
|
|
function formatToNewYorkTime(isoString) {
|
|
const date = new Date(isoString);
|
|
|
|
// Get the date components in New York time zone
|
|
const options = {
|
|
year: "numeric",
|
|
month: "numeric",
|
|
day: "numeric",
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
second: "numeric",
|
|
timeZone: "America/New_York",
|
|
hour12: false,
|
|
};
|
|
|
|
// Format date for New York timezone
|
|
const formatter = new Intl.DateTimeFormat("en-US", options);
|
|
const parts = formatter.formatToParts(date);
|
|
|
|
const year = parts.find((p) => p.type === "year").value;
|
|
const day = parts.find((p) => p.type === "day").value;
|
|
const hour = parts.find((p) => p.type === "hour").value.padStart(2, "0");
|
|
const minute = parts
|
|
.find((p) => p.type === "minute")
|
|
.value.padStart(2, "0");
|
|
const second = parts
|
|
.find((p) => p.type === "second")
|
|
.value.padStart(2, "0");
|
|
|
|
return `${day}/${year} ${hour}:${minute}:${second}`;
|
|
}
|
|
|
|
let sortOrders = {
|
|
date: "none",
|
|
ticker: "none",
|
|
price: "none",
|
|
premium: "none",
|
|
assetType: "none",
|
|
volume: "none",
|
|
size: "none",
|
|
};
|
|
|
|
// Generalized sorting function
|
|
function sortData(key) {
|
|
// Reset all other keys to 'none' except the current key
|
|
for (const k in sortOrders) {
|
|
if (k !== key) {
|
|
sortOrders[k] = "none";
|
|
}
|
|
}
|
|
|
|
// Cycle through 'none', 'asc', 'desc' for the clicked key
|
|
const orderCycle = ["none", "asc", "desc"];
|
|
const currentOrderIndex = orderCycle.indexOf(sortOrders[key]);
|
|
sortOrders[key] = orderCycle[(currentOrderIndex + 1) % orderCycle.length];
|
|
|
|
const sortOrder = sortOrders[key];
|
|
const originalData =
|
|
filteredData?.length !== 0 ? [...filteredData] : [...rawData];
|
|
|
|
// Reset to original data when 'none'
|
|
if (sortOrder === "none") {
|
|
displayedData = originalData;
|
|
return;
|
|
}
|
|
|
|
const compareFunctions = {
|
|
ticker: (a, b) => {
|
|
const tickerA = a.ticker.toUpperCase();
|
|
const tickerB = b.ticker.toUpperCase();
|
|
return sortOrder === "asc"
|
|
? tickerA.localeCompare(tickerB)
|
|
: tickerB.localeCompare(tickerA);
|
|
},
|
|
date: (a, b) => {
|
|
const timeA = new Date(a.date);
|
|
const timeB = new Date(b.date);
|
|
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
|
|
},
|
|
price: (a, b) => {
|
|
const priceA = parseFloat(a.price);
|
|
const priceB = parseFloat(b.price);
|
|
return sortOrder === "asc" ? priceA - priceB : priceB - priceA;
|
|
},
|
|
premium: (a, b) => {
|
|
const premiumA = parseFloat(a.premium);
|
|
const premiumB = parseFloat(b.premium);
|
|
return sortOrder === "asc" ? premiumA - premiumB : premiumB - premiumA;
|
|
},
|
|
size: (a, b) => {
|
|
const volA = parseFloat(a?.size);
|
|
const volB = parseFloat(b?.size);
|
|
return sortOrder === "asc" ? volA - volB : volB - volA;
|
|
},
|
|
volume: (a, b) => {
|
|
const volA = parseFloat(a.volume);
|
|
const volB = parseFloat(b.volume);
|
|
return sortOrder === "asc" ? volA - volB : volB - volA;
|
|
},
|
|
dailyVolume: (a, b) => {
|
|
const volA = parseFloat(a.dailyVolumePercentage);
|
|
const volB = parseFloat(b.dailyVolumePercentage);
|
|
return sortOrder === "asc" ? volA - volB : volB - volA;
|
|
},
|
|
avgVolume: (a, b) => {
|
|
const volA = parseFloat(a.avgVolumePercentage);
|
|
const volB = parseFloat(b.avgVolumePercentage);
|
|
return sortOrder === "asc" ? volA - volB : volB - volA;
|
|
},
|
|
assetType: (a, b) => {
|
|
const typeOrder = { SWEEP: 1, TRADE: 2 };
|
|
const typeA = typeOrder[a.assetType?.toUpperCase()] || 3;
|
|
const typeB = typeOrder[b.assetType?.toUpperCase()] || 3;
|
|
return sortOrder === "asc" ? typeA - typeB : typeB - typeA;
|
|
},
|
|
};
|
|
|
|
// Sort using the appropriate comparison function
|
|
displayedData = originalData.sort(compareFunctions[key]);
|
|
}
|
|
</script>
|
|
|
|
<div class="table-container">
|
|
<div class="table">
|
|
<VirtualList
|
|
width="100%"
|
|
height={$screenWidth < 640 ? 550 : 850}
|
|
itemCount={displayedData.length}
|
|
itemSize={40}
|
|
>
|
|
<div
|
|
slot="header"
|
|
class="tr th m-auto sticky z-40 top-0 border-b border-gray-800 shadow-xl"
|
|
>
|
|
<!-- Table headers -->
|
|
<div
|
|
on:click={() => sortData("date")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Date
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['date'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: ''} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
<div
|
|
on:click={() => sortData("ticker")}
|
|
class="td cursor-pointer select-none bg-[#121217] font-bold text-slate-300 text-xs text-start uppercase"
|
|
>
|
|
Symbol
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['ticker'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['ticker'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
on:click={() => sortData("price")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Price
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['price'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['price'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
<div
|
|
on:click={() => sortData("premium")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Premium
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['premium'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['premium'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
on:click={() => sortData("size")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Size
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['size'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['size'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
<div
|
|
on:click={() => sortData("volume")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Volume
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['volume'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['volume'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
on:click={() => sortData("dailyVolume")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
% Daily Volume
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders[
|
|
'dailyVolume'
|
|
] === 'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['dailyVolume'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
on:click={() => sortData("avgVolume")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
% 30D Volume
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders[
|
|
'avgVolume'
|
|
] === 'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['avgVolume'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
on:click={() => sortData("sector")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Sector
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['sector'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['sector'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
on:click={() => sortData("vol")}
|
|
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
|
|
>
|
|
Issue Type
|
|
<svg
|
|
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders['vol'] ===
|
|
'asc'
|
|
? 'rotate-180'
|
|
: sortOrders['vol'] === 'desc'
|
|
? ''
|
|
: 'hidden'} "
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
style="max-width:50px"
|
|
><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
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
slot="item"
|
|
let:index
|
|
let:style
|
|
{style}
|
|
class="tr {index % 2 === 0 ? 'bg-[#19191F]' : 'bg-[#121217]'}"
|
|
>
|
|
<!-- Row data -->
|
|
|
|
<div
|
|
style="justify-content: center; "
|
|
class=" td w-full text-white text-xs sm:text-sm whitespace-nowrap m-auto"
|
|
>
|
|
{formatToNewYorkTime(displayedData[index]?.date)}
|
|
</div>
|
|
<div
|
|
on:click|stopPropagation
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:hover:text-white sm:text-[1rem] text-blue-400 font-normal"
|
|
>
|
|
<HoverStockChart
|
|
symbol={displayedData[index]?.ticker}
|
|
assetType={displayedData[index]?.assetType}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-start text-white"
|
|
>
|
|
{displayedData[index]?.price}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-start text-white"
|
|
>
|
|
{@html abbreviateNumber(displayedData[index]?.premium, true, true)}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-white text-end"
|
|
>
|
|
{new Intl.NumberFormat("en", {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(displayedData[index]?.size)}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-white text-end"
|
|
>
|
|
{@html abbreviateNumber(displayedData[index]?.volume, false, true)}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-white text-end"
|
|
>
|
|
{displayedData[index]?.dailyVolumePercentage > 0.01
|
|
? displayedData[index]?.dailyVolumePercentage?.toFixed(2) + "%"
|
|
: "< 0.01%"}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-white text-end"
|
|
>
|
|
{displayedData[index]?.avgVolume > 0.01
|
|
? displayedData[index]?.avgVolume?.toFixed(2) + "%"
|
|
: "< 0.01%"}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: start;"
|
|
class="td text-sm sm:text-[1rem] text-white text-start"
|
|
>
|
|
{displayedData[index]?.sector?.length > 13
|
|
? displayedData[index]?.sector?.slice(0, 13) + "..."
|
|
: displayedData[index]?.sector}
|
|
</div>
|
|
|
|
<div
|
|
style="justify-content: center;"
|
|
class="td text-sm sm:text-[1rem] text-white text-end"
|
|
>
|
|
{displayedData[index]?.assetType}
|
|
</div>
|
|
</div>
|
|
</VirtualList>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.table-container {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.table :global(.virtual-list-inner) {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.table {
|
|
width: 1000px;
|
|
}
|
|
}
|
|
|
|
.table .virtual-list-inner {
|
|
flex-flow: column nowrap;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.th {
|
|
display: none;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.th > .td {
|
|
white-space: nowrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
.tr {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-flow: row nowrap;
|
|
}
|
|
|
|
.tr.even {
|
|
background-color: #2a2e39;
|
|
}
|
|
|
|
.tr.odd {
|
|
background-color: #09090b;
|
|
}
|
|
|
|
.td {
|
|
display: flex;
|
|
flex-flow: row nowrap;
|
|
flex-grow: 1;
|
|
flex-basis: 0;
|
|
padding: 0.5em;
|
|
word-break: break-word;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
min-width: 0px;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|