update watchlist

This commit is contained in:
MuslemRahimi 2024-10-15 12:49:43 +02:00
parent e3dc2347fe
commit e307dd19d8

View File

@ -10,6 +10,9 @@ import { Button } from "$lib/components/shadcn/button/index.js";
import { Combobox } from "bits-ui"; import { Combobox } from "bits-ui";
export let data; export let data;
let currentWatchlistLocalStorage = '1.0'; // Increment this whenever the structure of allRows changes
let searchQuery = ''; let searchQuery = '';
let editMode = false; let editMode = false;
let numberOfChecked = 0; let numberOfChecked = 0;
@ -19,33 +22,61 @@ let watchList: any[] = [];
let news = []; let news = [];
let checkedItems; let checkedItems;
let allRows = [ let allRows = [
{ name: 'Volume', rule: 'volume' }, { name: 'Volume', rule: 'volume', type: 'int'},
{ name: 'Market Cap', rule: 'marketCap' }, { name: 'Market Cap', rule: 'marketCap', type: 'int'},
{ name: 'Price', rule: 'price' }, { name: 'Price', rule: 'price', type: 'float'},
{ name: 'Change', rule: 'changesPercentage' }, { name: 'Change', rule: 'changesPercentage', type: 'percentSign'},
{ name: 'EPS', rule: 'eps' }, { name: 'EPS', rule: 'eps', type: 'float'},
{ name: 'PE', rule: 'pe' }, { name: 'PE', rule: 'pe', type: 'float'},
{ name: 'AI Score', rule: 'score' }, { name: 'AI Score', rule: 'score', type: 'rating'},
{ name: 'Revenue', rule: 'revenue'}, { name: 'Revenue', rule: 'revenue', type: 'int'},
{ name: 'Net Income', rule: 'netIncome'}, { name: 'EBITDA', rule: 'ebitda', type: 'int'},
{ name: 'Free Cash Flow', rule: 'freeCashFlow'}, { name: 'Net Income', rule: 'netIncome', type: 'int'},
{ name: 'Industry', rule: 'industry'}, { name: 'FCF', rule: 'freeCashFlow', type: 'int'},
{ name: 'Sector', rule: 'sector'}, { name: 'Industry', rule: 'industry', type: 'str'},
{ name: 'Price Change 1W', rule: 'change1W' }, { name: 'Sector', rule: 'sector', type: 'str'},
{ name: 'Price Change 1M', rule: 'change1M' }, { name: 'Price Change 1W', rule: 'change1W', type: 'percentSign'},
{ name: 'Price Change 3M', rule: 'change3M' }, { name: 'Price Change 1M', rule: 'change1M', type: 'percentSign'},
{ name: 'Price Change 6M', rule: 'change6M' }, { name: 'Price Change 3M', rule: 'change3M', type: 'percentSign'},
{ name: 'Price Change 1Y', rule: 'change1Y' }, { name: 'Price Change 6M', rule: 'change6M',type: 'percentSign'},
{ name: 'Price Change 1Y', rule: 'change1Y', type: 'percentSign'},
{ name: 'Enterprise Value', rule: 'enterpriseValue', type: 'int'},
{ name: 'Forward PE', rule: 'forwardPE', type: 'float'},
{ name: 'Forward PS', rule: 'forwardPS', type: 'float'},
{ name: 'Dividend Yield', rule: 'dividendYield', type: 'percent'},
{ name: 'Current Ratio', rule: 'currentRatio', type: 'float'},
{ name: 'Quick Ratio', rule: 'quickRatio', type: 'float'},
{ name: 'Analyst Rating', rule: 'analystRating', type: 'rating'},
{ name: 'Country', rule: 'country', type: 'str'},
{ name: 'Gross Profit', rule: 'grossProfit', type: 'int'},
{ name: 'Revenue Growth', rule: 'growthRevenue', type: 'percentSign'},
{ name: 'Gross Profit Growth', rule: 'growthGrossProfit', type: 'percentSign'},
{ name: 'Net Income Growth', rule: 'growthNetIncome', type: 'percentSign'},
{ name: 'EBITDA Growth', rule: 'growthEBITDA', type: 'percentSign'},
{ name: 'EPS Growth', rule: 'growthEPS', type: 'percentSign'},
{ name: 'Total Debt', rule: 'totalDebt', type: 'int'},
{ name: 'Return on Assets', rule: 'returnOnAssets', type: 'int'},
{ name: 'Return on Equity', rule: 'returnOnEquity', type: 'int'},
{ name: 'Value-at-Risk', rule: 'var', type: 'percentSign'},
{ name: 'Asset Turnover', rule: 'assetTurnover', type: 'int'},
{ name: 'Earnings Yield', rule: 'earningsYield', type: 'percent'},
{ name: 'Altman-Z-Score Yield', rule: 'altmanZScore', type: 'float'},
{ name: 'Piotroski F-Score', rule: 'piotroskiScore', type: 'float'},
{ name: 'Total Liabilities', rule: 'totalLiabilities', type: 'int'},
{ name: 'Short Ratio', rule: 'shortRatio', type: 'int'},
{ name: 'FCF Yield', rule: 'freeCashFlowYield', type: 'percent'},
{ name: 'Employees', rule: 'employees', type: 'int'},
{ name: 'Debt Ratio', rule: 'debtRatio', type: 'float'},
{ name: 'Debt / Equity', rule: 'debtEquityRatio', type: 'int'},
]; ];
let ruleOfList = [ let ruleOfList = [
{ name: 'Volume', rule: 'volume' }, { name: 'Volume', rule: 'volume', type: 'int' },
{ name: 'Market Cap', rule: 'marketCap' }, { name: 'Market Cap', rule: 'marketCap', type: 'int' },
{ name: 'Price', rule: 'price' }, { name: 'Price', rule: 'price', type: 'float' },
{ name: 'Change', rule: 'changesPercentage' }, { name: 'Change', rule: 'changesPercentage', type: 'percent' },
]; ];
const excludedRules = new Set(['volume', 'price', 'changesPercentage', 'eps']); const excludedRules = new Set(['volume', 'price', 'changesPercentage', 'eps']);
@ -348,7 +379,19 @@ if (!watchList?.some(item => item?.symbol === ticker)) {
} }
async function handleResetAll() {
searchQuery = '';
ruleOfList = [
{ name: 'Volume', rule: 'volume', type: 'int' },
{ name: 'Market Cap', rule: 'marketCap', type: 'int' },
{ name: 'Price', rule: 'price', type: 'float' },
{ name: 'Change', rule: 'changesPercentage', type: 'percent' },
];
ruleOfList = [...ruleOfList];
checkedItems = new Set(ruleOfList.map(item => item.name));
allRows = sortIndicatorCheckMarks(allRows);
saveRules()
}
function changeWatchList(newWatchList) function changeWatchList(newWatchList)
{ {
@ -359,49 +402,57 @@ function changeWatchList(newWatchList)
function saveRules() { function saveRules() {
try { try {
// Save the version along with the rules
localStorage?.setItem('watchlist-ruleOfList', JSON?.stringify(ruleOfList)); localStorage?.setItem('watchlist-ruleOfList', JSON?.stringify(ruleOfList));
} catch(e) { localStorage?.setItem('watchlist-ruleOfList-version', currentWatchlistLocalStorage); // Save the current version
console.log('Failed saving indicator rules: ', e) } catch (e) {
console.log('Failed saving indicator rules: ', e);
} }
} }
onMount(async () => { onMount(async () => {
try { try {
const savedRules = localStorage?.getItem('watchlist-ruleOfList'); const savedRules = localStorage?.getItem('watchlist-ruleOfList');
if (savedRules) { const savedVersion = localStorage?.getItem('watchlist-ruleOfList-version');
// If the version doesn't match, reset the local storage
if (savedVersion !== currentWatchlistLocalStorage) {
localStorage?.removeItem('watchlist-ruleOfList'); // Clear old data
localStorage?.setItem('watchlist-ruleOfList-version', currentWatchlistLocalStorage); // Save new version
localStorage?.setItem('watchlist-ruleOfList', JSON?.stringify(ruleOfList)); // Save new rules
} else if (savedRules) {
ruleOfList = JSON.parse(savedRules); ruleOfList = JSON.parse(savedRules);
// Check for the user's tier and filter out paywalled features
if (data?.user?.tier !== 'Pro') { if (data?.user?.tier !== 'Pro') {
//Check if user was Pro Member and has past checks of previous paywalled features. If so remove them from the ruleOfList
ruleOfList = ruleOfList.filter(item => excludedRules.has(item?.rule)); // Use Set to filter ruleOfList = ruleOfList.filter(item => excludedRules.has(item?.rule)); // Use Set to filter
console.log(ruleOfList)
} }
} }
} catch(e) {
console.log(e)
}
checkedItems = new Set(ruleOfList.map(item => item.name)) // Update checked items and sort the indicators
allRows = sortIndicatorCheckMarks(allRows) checkedItems = new Set(ruleOfList.map(item => item.name));
allRows = sortIndicatorCheckMarks(allRows);
if(allList?.length !== 0) // Display the first watchlist if available
{ if (allList?.length !== 0) {
displayWatchList = allList?.at(0) displayWatchList = allList?.at(0);
} } else {
else {
displayWatchList = ''; displayWatchList = '';
} }
await getWatchlistData();
if (!downloadWorker) {
const DownloadWorker = await import('./workers/downloadWorker?worker');
downloadWorker = new DownloadWorker.default();
downloadWorker.onmessage = handleDownloadMessage;
}
isLoaded = true; await getWatchlistData();
// Initialize the download worker if not already done
if (!downloadWorker) {
const DownloadWorker = await import('./workers/downloadWorker?worker');
downloadWorker = new DownloadWorker.default();
downloadWorker.onmessage = handleDownloadMessage;
}
isLoaded = true;
} catch (e) {
console.log(e);
}
}); });
onDestroy( () => { onDestroy( () => {
@ -704,51 +755,64 @@ function search() {
<div class="order-0 sm:order-4 w-full {displayWatchList?.title === undefined ? 'hidden' : ''}"> <div class="order-0 sm:order-4 w-full {displayWatchList?.title === undefined ? 'hidden' : ''}">
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder> <DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} class="sm:ml-auto min-w-[110px] w-full sm: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.5 text-white rounded-md truncate"> <Button builders={[builder]} class="sm:ml-auto min-w-[110px] w-full sm: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.5 text-white rounded-md truncate">
<span class="truncate text-white text-sm sm:text-[1rem]"> <span class="truncate text-white text-sm sm:text-[1rem]">Indicators</span>
Indicators
</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"> <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> <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> </svg>
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56 h-fit max-h-72 overflow-y-auto scroller">
<div class="relative sticky z-40 focus:outline-none -top-1"
tabindex="0" role="menu" style="">
<input bind:value={searchQuery}
on:input={handleInput}
autocomplete="off"
class="text-sm absolute fixed sticky w-full border-0 bg-[#09090B] border-b border-gray-200
focus:border-gray-200 focus:ring-0 text-white placeholder:text-gray-300"
type="search"
placeholder="Search...">
</div>
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#each (searchQuery?.length !== 0 ? testList : allRows) as item}
<DropdownMenu.Item class="sm:hover:bg-[#27272A]" >
<div class="flex items-center">
{#if (data?.user?.tier === 'Pro') || excludedRules?.has(item?.rule)}
<label on:click|capture={(event) => { event.preventDefault(); handleChangeValue(item?.name) }} class="cursor-pointer text-white" for={item?.name}>
<input type="checkbox" class="rounded" checked={isChecked(item?.name)}>
<span class="ml-2">{item?.name}</span>
</label>
{:else}
<a href="/pricing" class="cursor-pointer text-white">
<svg class="h-[18px] w-[18px] inline-block text-icon group-hover:text-dark-400" viewBox="0 0 20 20" fill="currentColor" style="max-width:40px">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>
</svg>
<span class="ml-2">{item?.name}</span>
</a>
{/if}
</div> <DropdownMenu.Content class="w-60 max-h-[400px] overflow-y-auto scroller relative">
</DropdownMenu.Item> <!-- Search Input -->
<div class="sticky fixed -top-1 z-40 bg-[#09090B] p-2 border-b border-gray-600">
<div class="relative w-full">
<!-- Input Field -->
<input bind:value={searchQuery} on:input={handleInput} autocomplete="off" autofocus=""
class="text-sm w-full border-0 bg-[#09090B] focus:border-gray-200 focus:ring-0 text-white placeholder:text-gray-300 pr-8"
type="text" placeholder="">
<!-- Clear Button - Shown only when searchQuery has input -->
{#if searchQuery?.length > 0}
<button on:click={() => searchQuery = ''} aria-label="Clear" title="Clear" tabindex="0" class="absolute right-2 top-1/2 transform -translate-y-1/2">
<svg class="h-5 w-5 text-icon cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
{/if}
</div>
</div>
<!-- Dropdown items -->
<DropdownMenu.Group class="pb-2"> <!-- Added padding to avoid overlapping with Reset button -->
{#each (searchQuery?.length !== 0 ? testList : allRows) as item}
<DropdownMenu.Item class="sm:hover:bg-[#27272A]">
<div class="flex items-center">
{#if (data?.user?.tier === 'Pro') || excludedRules?.has(item?.rule)}
<label on:click|capture={(event) => { event.preventDefault(); handleChangeValue(item?.name) }} class="cursor-pointer text-white" for={item?.name}>
<input type="checkbox" class="rounded" checked={isChecked(item?.name)}>
<span class="ml-2">{item?.name}</span>
</label>
{:else}
<a href="/pricing" class="cursor-pointer text-white">
<svg class="h-[18px] w-[18px] inline-block text-icon group-hover:text-dark-400" viewBox="0 0 20 20" fill="currentColor" style="max-width:40px">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>
</svg>
<span class="ml-2">{item?.name}</span>
</a>
{/if}
</div>
</DropdownMenu.Item>
{/each} {/each}
</DropdownMenu.Group> </DropdownMenu.Group>
<!-- Reset Selection button -->
<div class="sticky -bottom-1 bg-[#09090B] z-50 p-2 border-t border-gray-600 w-full">
<label on:click={handleResetAll} class="w-full sm:hover:text-white text-gray-300 bg-[#09090B] text-start text-sm cursor-pointer">
Reset Selection
</label>
</div>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>
@ -825,19 +889,21 @@ function search() {
{#if isChecked(row?.name)} {#if isChecked(row?.name)}
<td class="whitespace-nowrap text-sm sm:text-[1rem] text-end text-white border-b-[#09090B]"> <td class="whitespace-nowrap text-sm sm:text-[1rem] text-end text-white border-b-[#09090B]">
{#if item?.[row?.rule] !== undefined && item?.[row?.rule] !== null} {#if item?.[row?.rule] !== undefined && item?.[row?.rule] !== null}
{#if ['marketCap', 'volume','revenue','netIncome','freeCashFlow'].includes(row?.rule)} {#if row?.type === 'int'}
{abbreviateNumber(item[row?.rule])} {abbreviateNumber(item[row?.rule])}
{:else if ['industry','sector'].includes(row?.rule)} {:else if row?.type === 'str'}
{item[row?.rule] !== null ? item[row?.rule] : '-'} {item[row?.rule] !== null ? item[row?.rule] : '-'}
{:else if ['eps', 'pe', 'price','freeCashFlow'].includes(row?.rule)} {:else if row?.type === 'float'}
{item[row?.rule] !== null ? item[row?.rule]?.toFixed(2) : '-'} {item[row?.rule] !== null ? item[row?.rule]?.toFixed(2) : '-'}
{:else if ['changesPercentage','change1W','change1M','change3M','change6M','change1Y','change3Y'].includes(row?.rule)} {:else if row?.type === 'percent'}
{item[row?.rule] !== null ? item[row?.rule]?.toFixed(2)+'%' : '-'}
{:else if row?.type === 'percentSign'}
{#if item[row?.rule] >= 0} {#if item[row?.rule] >= 0}
<span class="text-[#37C97D]">+{item[row?.rule]?.toFixed(2)}%</span> <span class="text-[#37C97D]">+{item[row?.rule]?.toFixed(2)}%</span>
{:else} {:else}
<span class="text-[#FF2F1F]">{item[row?.rule]?.toFixed(2)}%</span> <span class="text-[#FF2F1F]">{item[row?.rule]?.toFixed(2)}%</span>
{/if} {/if}
{:else if "score" === row?.rule} {:else if row?.type === 'rating'}
{#if ['Strong Buy', 'Buy'].includes(item[row?.rule])} {#if ['Strong Buy', 'Buy'].includes(item[row?.rule])}
<span class="text-[#37C97D]">{item[row?.rule]}</span> <span class="text-[#37C97D]">{item[row?.rule]}</span>
{:else if ['Strong Sell', 'Sell'].includes(item[row?.rule])} {:else if ['Strong Sell', 'Sell'].includes(item[row?.rule])}