bugfixing
This commit is contained in:
parent
22dd3cbdc2
commit
08798d15c3
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { stockTicker, displayCompanyName } from "$lib/store";
|
import { stockTicker, displayCompanyName } from "$lib/store";
|
||||||
import { abbreviateNumber } from "$lib/utils";
|
import { abbreviateNumber, removeCompanyStrings } from "$lib/utils";
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
let rawData = {};
|
let rawData = {};
|
||||||
@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($stockTicker && typeof window !== "undefined") {
|
if ($stockTicker) {
|
||||||
rawData = data?.getEarningsSurprise;
|
rawData = data?.getEarningsSurprise;
|
||||||
epsRatio =
|
epsRatio =
|
||||||
rawData?.epsPrior === 0
|
rawData?.epsPrior === 0
|
||||||
@ -44,83 +44,80 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if Object?.keys(rawData)?.length !== 0}
|
<div class="space-y-3 overflow-hidden">
|
||||||
<div class="space-y-3 overflow-hidden">
|
<!--Start Content-->
|
||||||
<!--Start Content-->
|
<div class="w-auto lg:w-full p-1 flex flex-col m-auto">
|
||||||
<div class="w-auto lg:w-full p-1 flex flex-col m-auto">
|
<div class="flex flex-col items-center w-full mb-3">
|
||||||
<div class="flex flex-col items-center w-full mb-3">
|
<div class="flex flex-row justify-start mr-auto items-center">
|
||||||
<div class="flex flex-row justify-start mr-auto items-center">
|
<!--<img class="h-10 inline-block mr-2" src={copilotIcon} />-->
|
||||||
<!--<img class="h-10 inline-block mr-2" src={copilotIcon} />-->
|
<div class="flex flex-row items-center">
|
||||||
<div class="flex flex-row items-center">
|
<h3
|
||||||
<h3
|
class="mr-1 flex flex-row items-center text-white text-2xl font-bold"
|
||||||
class="mr-1 flex flex-row items-center text-white text-2xl font-bold"
|
>
|
||||||
>
|
Earnings Surprise
|
||||||
Earnings Surprise
|
</h3>
|
||||||
</h3>
|
<label
|
||||||
<label
|
class="{latestInfoDate(rawData?.date)
|
||||||
class="{latestInfoDate(rawData?.date)
|
? ''
|
||||||
? ''
|
: 'hidden'} text-black bg-[#fff] ml-2 font-semibold not-italic text-xs rounded px-2 py-0.5"
|
||||||
: 'hidden'} text-black bg-[#fff] ml-2 font-semibold not-italic text-xs rounded px-2 py-0.5"
|
>New</label
|
||||||
>New</label
|
>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
|
||||||
class="text-white text-[1rem] {latestInfoDate(rawData?.date)
|
<div
|
||||||
? 'bg-[#F9AB00] bg-opacity-[0.1] p-3 rounded-md'
|
class="text-white text-[1rem] {latestInfoDate(rawData?.date)
|
||||||
: 'bg-default pl-1'} "
|
? 'bg-[#F9AB00] bg-opacity-[0.1] p-3 rounded-md'
|
||||||
>
|
: 'bg-default pl-1'} "
|
||||||
<div class="mt-1">
|
>
|
||||||
{$displayCompanyName} has released their quartely earnings on {new Date(
|
<div class="mt-1">
|
||||||
rawData?.date,
|
{removeCompanyStrings($displayCompanyName)} has released their quartely earnings
|
||||||
)?.toLocaleString("en-US", {
|
on {new Date(rawData?.date)?.toLocaleString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
daySuffix: "2-digit",
|
daySuffix: "2-digit",
|
||||||
})}:
|
})}:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="ml-[20px] sm:ml-[30px]"
|
class="ml-[20px] sm:ml-[30px]"
|
||||||
style="color: #fff; line-height: 22px; margin-top:10px; margin-bottom: 10px; list-style-type: disc;"
|
style="color: #fff; line-height: 22px; margin-top:10px; margin-bottom: 10px; list-style-type: disc;"
|
||||||
>
|
>
|
||||||
Revenue of <span class="font-semibold"
|
Revenue of <span class=""
|
||||||
>{abbreviateNumber(rawData?.revenue, true)}</span
|
>{abbreviateNumber(rawData?.revenue, true)}</span
|
||||||
>
|
>
|
||||||
{rawData?.revenueSurprise > 0 ? "exceeds" : "misses"} estimates by {abbreviateNumber(
|
{rawData?.revenueSurprise > 0 ? "exceeds" : "misses"} estimates by {abbreviateNumber(
|
||||||
Math.abs(rawData?.revenueSurprise),
|
Math.abs(rawData?.revenueSurprise),
|
||||||
true,
|
true,
|
||||||
)}, with
|
)}, with
|
||||||
<span
|
<span
|
||||||
class="font-semibold {revenueRatio > 0
|
class=" {revenueRatio > 0
|
||||||
? "before:content-['+'] text-[#00FC50]"
|
? "before:content-['+'] text-[#00FC50]"
|
||||||
: 'text-[#FF2F1F]'}">{revenueRatio}%</span
|
: 'text-[#FF2F1F]'}">{revenueRatio}%</span
|
||||||
>
|
>
|
||||||
YoY {revenueRatio < 0 ? "decline" : "growth"}.
|
YoY {revenueRatio < 0 ? "decline" : "growth"}.
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="ml-[20px] sm:ml-[30px]"
|
class="ml-[20px] sm:ml-[30px]"
|
||||||
style="color: #fff; line-height: 22px; margin-top: 0px; margin-bottom: 0px; list-style-type: disc;"
|
style="color: #fff; line-height: 22px; margin-top: 0px; margin-bottom: 0px; list-style-type: disc;"
|
||||||
>
|
>
|
||||||
EPS of <span class="font-semibold">{rawData?.eps}</span>
|
EPS of <span class="">{rawData?.eps}</span>
|
||||||
{rawData?.epsSurprise > 0 ? "exceeds" : "misses"} estimates by {rawData?.epsSurprise?.toFixed(
|
{rawData?.epsSurprise > 0 ? "exceeds" : "misses"} estimates by {rawData?.epsSurprise?.toFixed(
|
||||||
2,
|
2,
|
||||||
)}, with
|
)}, with
|
||||||
<span
|
<span
|
||||||
class="font-semibold {epsRatio === null
|
class=" {epsRatio === null
|
||||||
? 'text-white'
|
? 'text-white'
|
||||||
: epsRatio > 0
|
: epsRatio > 0
|
||||||
? 'text-[#00FC50]'
|
? 'text-[#00FC50]'
|
||||||
: 'text-[#FF2F1F]'}"
|
: 'text-[#FF2F1F]'}"
|
||||||
>
|
>
|
||||||
{epsRatio === null ? "n/a" : `${epsRatio}%`}
|
{epsRatio === null ? "n/a" : `${epsRatio}%`}
|
||||||
</span>
|
</span>
|
||||||
YoY {epsRatio === null ? "" : epsRatio < 0 ? "decline" : "growth"}.
|
YoY {epsRatio === null ? "" : epsRatio < 0 ? "decline" : "growth"}.
|
||||||
</li>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
const cleanString = (input) => {
|
// Pre-compile regex pattern and substrings for cleaning
|
||||||
const substringsToRemove = [
|
const REMOVE_PATTERNS = {
|
||||||
|
pattern: new RegExp(`\\b(${[
|
||||||
"Depositary",
|
"Depositary",
|
||||||
"Inc.",
|
"Inc.",
|
||||||
"Incorporated",
|
"Incorporated",
|
||||||
@ -11,73 +12,154 @@ const cleanString = (input) => {
|
|||||||
"Oyj",
|
"Oyj",
|
||||||
"Company",
|
"Company",
|
||||||
"The",
|
"The",
|
||||||
"plc",
|
"plc"
|
||||||
];
|
].join("|")})\\b|,`, "gi")
|
||||||
const pattern = new RegExp(`\\b(${substringsToRemove.join("|")})\\b|,`, "gi");
|
|
||||||
return input?.replace(pattern, "").trim();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
|
// Memoized string cleaning function
|
||||||
try {
|
const cleanString = (() => {
|
||||||
const response = await fetch(`${apiURL}${endpoint}`, {
|
const cache = new Map();
|
||||||
method: "POST",
|
return (input) => {
|
||||||
headers: {
|
if (!input) return '';
|
||||||
"Content-Type": "application/json",
|
if (cache.has(input)) return cache.get(input);
|
||||||
"X-API-KEY": apiKey,
|
const cleaned = input.replace(REMOVE_PATTERNS.pattern, '').trim();
|
||||||
},
|
cache.set(input, cleaned);
|
||||||
body: JSON.stringify({ ticker }),
|
return cleaned;
|
||||||
});
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
if (!response.ok) {
|
// Constants
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
const CACHE_DURATION = 5 * 60 * 1000;
|
||||||
|
const REQUEST_TIMEOUT = 5000;
|
||||||
|
const ENDPOINTS = Object.freeze([
|
||||||
|
"/etf-profile",
|
||||||
|
"/etf-holdings",
|
||||||
|
"/etf-sector-weighting",
|
||||||
|
"/stock-dividend",
|
||||||
|
"/stock-quote",
|
||||||
|
"/pre-post-quote",
|
||||||
|
"/wiim",
|
||||||
|
"/one-day-price",
|
||||||
|
"/stock-news"
|
||||||
|
]);
|
||||||
|
|
||||||
|
// LRU Cache implementation with automatic cleanup
|
||||||
|
class LRUCache {
|
||||||
|
constructor(maxSize = 100) {
|
||||||
|
this.cache = new Map();
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
const item = this.cache.get(key);
|
||||||
|
if (!item) return null;
|
||||||
|
if (Date.now() - item.timestamp >= CACHE_DURATION) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
return item.data;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
set(key, data) {
|
||||||
|
if (this.cache.size >= this.maxSize) {
|
||||||
|
const oldestKey = this.cache.keys().next().value;
|
||||||
|
this.cache.delete(oldestKey);
|
||||||
|
}
|
||||||
|
this.cache.set(key, { data, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataCache = new LRUCache();
|
||||||
|
|
||||||
|
// Optimized fetch function with AbortController and timeout
|
||||||
|
const fetchWithTimeout = async (url, options, timeout) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main data fetching function
|
||||||
|
const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
|
||||||
|
const cacheKey = `${endpoint}-${ticker}`;
|
||||||
|
const cachedData = dataCache.get(cacheKey);
|
||||||
|
if (cachedData) return cachedData;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-KEY": apiKey
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ticker, endpoints: ENDPOINTS })
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchWithTimeout(
|
||||||
|
`${apiURL}${endpoint}`,
|
||||||
|
options,
|
||||||
|
REQUEST_TIMEOUT
|
||||||
|
);
|
||||||
|
dataCache.set(cacheKey, data);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching ${endpoint}:`, error);
|
if (error.name === 'AbortError') {
|
||||||
|
throw new Error(`Request timeout for ${endpoint}`);
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Optimized watchlist fetching with error boundary
|
||||||
const fetchWatchlist = async (pb, userId) => {
|
const fetchWatchlist = async (pb, userId) => {
|
||||||
let output;
|
if (!userId) return [];
|
||||||
try {
|
try {
|
||||||
output = await pb.collection("watchlist").getFullList({
|
return await pb.collection("watchlist").getFullList({
|
||||||
filter: `user="${userId}"`,
|
filter: `user="${userId}"`
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
//console.log(e)
|
return [];
|
||||||
output = [];
|
|
||||||
}
|
}
|
||||||
return output;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Main load function with parallel fetching
|
||||||
export const load = async ({ params, locals }) => {
|
export const load = async ({ params, locals }) => {
|
||||||
const { apiURL, apiKey, pb, user } = locals;
|
const { apiURL, apiKey, pb, user } = locals;
|
||||||
const { tickerID } = params;
|
const { tickerID } = params;
|
||||||
|
|
||||||
|
if (!tickerID) {
|
||||||
|
return getDefaultResponse(tickerID);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoints = [
|
const [bulkData, userWatchlist] = await Promise.all([
|
||||||
"/etf-profile",
|
fetchData(apiURL, apiKey, "/bulk-data", tickerID),
|
||||||
"/etf-holdings",
|
fetchWatchlist(pb, user?.id)
|
||||||
"/etf-sector-weighting",
|
]);
|
||||||
"/stock-dividend",
|
|
||||||
"/stock-quote",
|
|
||||||
"/pre-post-quote",
|
|
||||||
"/wiim",
|
|
||||||
"/one-day-price",
|
|
||||||
"/stock-news",
|
|
||||||
];
|
|
||||||
|
|
||||||
const promises = [
|
// Destructure with default empty arrays to prevent undefined errors
|
||||||
...endpoints.map((endpoint) =>
|
const {
|
||||||
fetchData(apiURL, apiKey, endpoint, tickerID),
|
'/etf-profile': getETFProfile = [],
|
||||||
),
|
'/etf-holdings': getETFHoldings = [],
|
||||||
fetchWatchlist(pb, user?.id),
|
'/etf-sector-weighting': getETFSectorWeighting = [],
|
||||||
];
|
'/stock-dividend': getStockDividend = [],
|
||||||
|
'/stock-quote': getStockQuote = [],
|
||||||
|
'/pre-post-quote': getPrePostQuote = [],
|
||||||
|
'/wiim': getWhyPriceMoved = [],
|
||||||
|
'/one-day-price': getOneDayPrice = [],
|
||||||
|
'/stock-news': getNews = []
|
||||||
|
} = bulkData;
|
||||||
|
|
||||||
const [
|
return {
|
||||||
getETFProfile,
|
getETFProfile,
|
||||||
getETFHoldings,
|
getETFHoldings,
|
||||||
getETFSectorWeighting,
|
getETFSectorWeighting,
|
||||||
@ -87,38 +169,27 @@ export const load = async ({ params, locals }) => {
|
|||||||
getWhyPriceMoved,
|
getWhyPriceMoved,
|
||||||
getOneDayPrice,
|
getOneDayPrice,
|
||||||
getNews,
|
getNews,
|
||||||
getUserWatchlist,
|
getUserWatchlist: userWatchlist,
|
||||||
] = await Promise.all(promises);
|
|
||||||
|
|
||||||
return {
|
|
||||||
getETFProfile: getETFProfile || [],
|
|
||||||
getETFHoldings: getETFHoldings || [],
|
|
||||||
getETFSectorWeighting: getETFSectorWeighting || [],
|
|
||||||
getStockDividend: getStockDividend || [],
|
|
||||||
getStockQuote: getStockQuote || [],
|
|
||||||
getPrePostQuote: getPrePostQuote || [],
|
|
||||||
getWhyPriceMoved: getWhyPriceMoved || [],
|
|
||||||
getOneDayPrice: getOneDayPrice || [],
|
|
||||||
getNews: getNews || [],
|
|
||||||
getUserWatchlist: getUserWatchlist || [],
|
|
||||||
companyName: cleanString(getETFProfile?.at(0)?.name),
|
companyName: cleanString(getETFProfile?.at(0)?.name),
|
||||||
getParams: params.tickerID,
|
getParams: tickerID
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in load function:', error);
|
return getDefaultResponse(tickerID);
|
||||||
return {
|
|
||||||
getETFProfile: [],
|
|
||||||
getETFHoldings: [],
|
|
||||||
getETFSectorWeighting: [],
|
|
||||||
getStockDividend: [],
|
|
||||||
getStockQuote: [],
|
|
||||||
getPrePostQuote: [],
|
|
||||||
getWhyPriceMoved: [],
|
|
||||||
getOneDayPrice: [],
|
|
||||||
getNews: [],
|
|
||||||
getUserWatchlist: [],
|
|
||||||
companyName: '',
|
|
||||||
getParams: params.tickerID,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to generate default response
|
||||||
|
const getDefaultResponse = (tickerID) => ({
|
||||||
|
getETFProfile: [],
|
||||||
|
getETFHoldings: [],
|
||||||
|
getETFSectorWeighting: [],
|
||||||
|
getStockDividend: [],
|
||||||
|
getStockQuote: [],
|
||||||
|
getPrePostQuote: [],
|
||||||
|
getWhyPriceMoved: [],
|
||||||
|
getOneDayPrice: [],
|
||||||
|
getNews: [],
|
||||||
|
getUserWatchlist: [],
|
||||||
|
companyName: '',
|
||||||
|
getParams: tickerID
|
||||||
|
});
|
||||||
@ -380,11 +380,11 @@
|
|||||||
class="bg-default w-full max-w-screen sm:max-w-[1250px] min-h-screen overflow-hidden"
|
class="bg-default w-full max-w-screen sm:max-w-[1250px] min-h-screen overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- Page wrapper -->
|
<!-- Page wrapper -->
|
||||||
<div class="mt-5 flex flex-col w-full relative w-full">
|
<div class="mt-5 flex flex-col w-full relative w-full sm:max-w-[1250px]">
|
||||||
<main class="grow w-full">
|
<main class="grow w-full">
|
||||||
<section class="">
|
<section class="">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="sm:flex sm:justify-start w-full sm:max-w-[1250px]">
|
<div class="sm:flex sm:justify-start w-full">
|
||||||
<!--Start Mobile Navbar-->
|
<!--Start Mobile Navbar-->
|
||||||
<div class="fixed top-0 left-0 right-0 z-20 bg-default sm:hidden">
|
<div class="fixed top-0 left-0 right-0 z-20 bg-default sm:hidden">
|
||||||
<div class="navbar w-full px-4 py-2">
|
<div class="navbar w-full px-4 py-2">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user