bugfixing

This commit is contained in:
MuslemRahimi 2025-02-22 21:54:34 +01:00
parent 4a2defe0c3
commit 22dd3cbdc2
3 changed files with 159 additions and 93 deletions

View File

@ -19,6 +19,7 @@
let searchOpen = false; let searchOpen = false;
let searchBarModalChecked = false; // Initialize it to false let searchBarModalChecked = false; // Initialize it to false
let inputElement; let inputElement;
let isNavigating = false;
const popularList = [ const popularList = [
{ {
@ -49,12 +50,14 @@
]; ];
async function handleSearch(symbol, assetType) { async function handleSearch(symbol, assetType) {
if (isNavigating) return; // Prevent double calls
isNavigating = true;
// Find the matching ticker data // Find the matching ticker data
const newSearchItem = searchBarData?.find( const newSearchItem = searchBarData?.find(
(item) => item?.symbol === symbol?.toUpperCase(), (item) => item?.symbol === symbol?.toUpperCase(),
); );
if (newSearchItem) { if (newSearchItem) {
// Ensure `upperState` matches the case of `item.symbol`
updatedSearchHistory = [ updatedSearchHistory = [
newSearchItem, newSearchItem,
...(searchHistory?.filter( ...(searchHistory?.filter(
@ -63,28 +66,39 @@
].slice(0, 5); ].slice(0, 5);
} }
searchBarTicker(symbol); // Close search modal
searchOpen = false;
if ($screenWidth < 640) {
const closePopup = document.getElementById("searchBarModal");
closePopup?.dispatchEvent(new MouseEvent("click"));
}
nextPage = true; await goto(
goto(
`/${assetType === "ETF" ? "etf" : assetType === "Index" ? "index" : "stocks"}/${symbol}`, `/${assetType === "ETF" ? "etf" : assetType === "Index" ? "index" : "stocks"}/${symbol}`,
); );
// Reset the flag after navigation
setTimeout(() => {
isNavigating = false;
}, 100);
} }
async function popularTicker(state) { async function popularTicker(state) {
if (isNavigating) return;
isNavigating = true;
searchOpen = false; searchOpen = false;
if (!state) return; if (!state) {
isNavigating = false;
return;
}
// Convert state to uppercase once
const upperState = state.toUpperCase(); const upperState = state.toUpperCase();
// Find the matching ticker data
const newSearchItem = searchBarData?.find( const newSearchItem = searchBarData?.find(
({ symbol }) => symbol === upperState, ({ symbol }) => symbol === upperState,
); );
if (newSearchItem) { if (newSearchItem) {
// Ensure `upperState` matches the case of `item.symbol`
updatedSearchHistory = [ updatedSearchHistory = [
newSearchItem, newSearchItem,
...(searchHistory?.filter( ...(searchHistory?.filter(
@ -93,14 +107,19 @@
].slice(0, 5); ].slice(0, 5);
} }
// Close search modal
const closePopup = document.getElementById("searchBarModal"); const closePopup = document.getElementById("searchBarModal");
closePopup?.dispatchEvent(new MouseEvent("click")); closePopup?.dispatchEvent(new MouseEvent("click"));
// Reset the flag after a short delay
setTimeout(() => {
isNavigating = false;
}, 100);
} }
function searchBarTicker(state) { function searchBarTicker(state) {
if (isNavigating) return;
showSuggestions = false; showSuggestions = false;
// Early return if state is empty or ticker not found
if ( if (
!state || !state ||
!searchBarData?.find((item) => item?.symbol === state?.toUpperCase()) !searchBarData?.find((item) => item?.symbol === state?.toUpperCase())
@ -109,29 +128,23 @@
const closePopup = document.getElementById("searchBarModal"); const closePopup = document.getElementById("searchBarModal");
closePopup?.dispatchEvent(new MouseEvent("click")); closePopup?.dispatchEvent(new MouseEvent("click"));
} }
return; return;
} }
// Convert state to uppercase once
const upperState = state.toUpperCase(); const upperState = state.toUpperCase();
// Find the matching ticker data
const newSearchItem = searchBarData?.find( const newSearchItem = searchBarData?.find(
({ symbol }) => symbol === upperState, ({ symbol }) => symbol === upperState,
); );
if (newSearchItem) { if (newSearchItem) {
// Ensure `upperState` matches the case of `item.symbol`
updatedSearchHistory = [ updatedSearchHistory = [
newSearchItem, newSearchItem,
...(searchHistory?.filter( ...(searchHistory?.filter(
(item) => item?.symbol?.toUpperCase() !== upperState, (item) => item?.symbol?.toUpperCase() !== upperState,
) || []), ) || []),
]?.slice(0, 5); ].slice(0, 5);
} }
// Close search modal
searchOpen = false; searchOpen = false;
if ($screenWidth < 640) { if ($screenWidth < 640) {
const closePopup = document.getElementById("searchBarModal"); const closePopup = document.getElementById("searchBarModal");
@ -160,6 +173,8 @@
} }
function handleKeyDown(symbol) { function handleKeyDown(symbol) {
if (isNavigating) return;
const list = Array.from( const list = Array.from(
new Map( new Map(
[...searchHistory, ...searchBarData, ...popularList].map((item) => [ [...searchHistory, ...searchBarData, ...popularList].map((item) => [
@ -172,8 +187,11 @@
if (!list?.length) return; if (!list?.length) return;
const newData = list.find((item) => item?.symbol === symbol); const newData = list.find((item) => item?.symbol === symbol);
handleSearch(newData?.symbol, newData?.type); if (newData) {
handleSearch(newData?.symbol, newData?.type);
}
} }
const handleControlK = async (event) => { const handleControlK = async (event) => {
if (event.ctrlKey && event.key === "k") { if (event.ctrlKey && event.key === "k") {
const keyboardSearch = document.getElementById("combobox-input"); const keyboardSearch = document.getElementById("combobox-input");

View File

@ -428,7 +428,7 @@
<a <a
href={`/stocks/${item?.ticker}`} href={`/stocks/${item?.ticker}`}
class="inline-block badge rounded-sm ml-1 px-2 m-auto text-blue-400 sm:hover:text-white" class="inline-block badge duration-0 rounded-sm ml-1 px-2 m-auto text-blue-400 sm:hover:text-white"
>{item?.ticker}</a >{item?.ticker}</a
> >
</li> </li>
@ -441,7 +441,7 @@
<a <a
href={`/stocks/${item?.ticker}`} href={`/stocks/${item?.ticker}`}
class="inline-block badge rounded-sm ml-1 px-2 m-auto text-blue-400 sm:hover:text-white" class="inline-block badge duration-0 rounded-sm ml-1 px-2 m-auto text-blue-400 sm:hover:text-white"
>{item?.ticker}</a >{item?.ticker}</a
> >
</li> </li>
@ -489,14 +489,16 @@
<div class="text-white mt-4"> <div class="text-white mt-4">
According to {analystReport?.numOfAnalyst} analyst ratings, the According to {analystReport?.numOfAnalyst} analyst ratings, the
average rating for average rating for
<a class="text-blue-400 sm:hover:text-white cursor-pointer" <a
href={`/stocks/${analystReport?.symbol}`}
class="text-blue-400 sm:hover:text-white cursor-pointer"
>{analystReport?.symbol}</a >{analystReport?.symbol}</a
> >
stock is "{analystReport?.consensusRating}" The 12-month stock stock is "{analystReport?.consensusRating}" The 12-month stock
price forecast is ${analystReport?.highPriceTarget}, which is price forecast is ${analystReport?.highPriceTarget}, which is
an {analystReport?.highPriceChange > 0 an {analystReport?.highPriceChange > 0
? "increase" ? "increase"
: "decreas"} of {analystReport?.highPriceChange}% from the : "decrease"} of {analystReport?.highPriceChange}% from the
latest price. latest price.
</div> </div>
<table <table

View File

@ -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,81 +12,126 @@ 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 CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds // Memoized string cleaning function
const REQUEST_TIMEOUT = 5000; // 5 seconds const cleanString = (() => {
const cache = new Map(); const cache = new Map();
return (input) => {
if (!input) return '';
if (cache.has(input)) return cache.get(input);
const cleaned = input.replace(REMOVE_PATTERNS.pattern, '').trim();
cache.set(input, cleaned);
return cleaned;
};
})();
const fetchData = async (apiURL, apiKey, endpoint, ticker) => { // Constants
const cacheKey = `${endpoint}-${ticker}`; const CACHE_DURATION = 5 * 60 * 1000;
const cached = cache.get(cacheKey); const REQUEST_TIMEOUT = 5000;
const ENDPOINTS = Object.freeze([
"/stockdeck",
"/analyst-summary-rating",
"/stock-quote",
"/pre-post-quote",
"/wiim",
"/one-day-price",
"/next-earnings",
"/earnings-surprise",
"/stock-news"
]);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { // LRU Cache implementation with automatic cleanup
return cached.data; 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;
}
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 controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); const timeoutId = setTimeout(() => controller.abort(), timeout);
const endpoints = [
"/stockdeck",
"/analyst-summary-rating",
"/stock-quote",
"/pre-post-quote",
"/wiim",
"/one-day-price",
"/next-earnings",
"/earnings-surprise",
"/stock-news",
]
try { try {
const response = await fetch(`${apiURL}${endpoint}`, { const response = await fetch(url, {
method: "POST", ...options,
headers: {
"Content-Type": "application/json",
"X-API-KEY": apiKey,
},
body: JSON.stringify({ticker, endpoints}),
signal: controller.signal signal: controller.signal
}); });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
} finally {
clearTimeout(timeoutId);
}
};
if (!response.ok) { // Main data fetching function
throw new Error(`HTTP error! status: ${response.status}`); const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
} const cacheKey = `${endpoint}-${ticker}`;
const cachedData = dataCache.get(cacheKey);
if (cachedData) return cachedData;
const data = await response.json(); const options = {
cache.set(cacheKey, { data, timestamp: Date.now() }); 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) {
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
throw new Error(`Request timeout for ${endpoint}`); throw new Error(`Request timeout for ${endpoint}`);
} }
throw error; throw error;
} finally {
clearTimeout(timeoutId);
} }
}; };
// 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;
@ -95,24 +141,24 @@ export const load = async ({ params, locals }) => {
} }
try { try {
// Fetch combined stock data from the '/stock-data' endpoint // Fetch data in parallel
const getStockData = await fetchData(apiURL, apiKey, "/bulk-data", tickerID); const [stockData, userWatchlist] = await Promise.all([
fetchData(apiURL, apiKey, "/bulk-data", tickerID),
fetchWatchlist(pb, user?.id)
]);
// Destructure the returned object to assign friendly names // Destructure with default empty object to prevent undefined errors
const { const {
'/stockdeck': getStockDeck, '/stockdeck': getStockDeck = {},
'/analyst-summary-rating': getAnalystSummary, '/analyst-summary-rating': getAnalystSummary = {},
'/stock-quote': getStockQuote, '/stock-quote': getStockQuote = {},
'/pre-post-quote': getPrePostQuote, '/pre-post-quote': getPrePostQuote = {},
'/wiim': getWhyPriceMoved, '/wiim': getWhyPriceMoved = {},
'/one-day-price': getOneDayPrice, '/one-day-price': getOneDayPrice = {},
'/next-earnings': getNextEarnings, '/next-earnings': getNextEarnings = {},
'/earnings-surprise': getEarningsSurprise, '/earnings-surprise': getEarningsSurprise = {},
'/stock-news': getNews, '/stock-news': getNews = {}
} = getStockData; } = stockData;
// Optionally, you can still fetch additional data like the watchlist:
const getUserWatchlist = await fetchWatchlist(pb, user?.id).catch(() => []);
return { return {
getStockDeck, getStockDeck,
@ -124,9 +170,9 @@ export const load = async ({ params, locals }) => {
getNextEarnings, getNextEarnings,
getEarningsSurprise, getEarningsSurprise,
getNews, getNews,
getUserWatchlist, getUserWatchlist: userWatchlist,
companyName: cleanString(getStockDeck?.companyName), companyName: cleanString(getStockDeck?.companyName),
getParams: tickerID, getParams: tickerID
}; };
} catch (error) { } catch (error) {
return { error: 'Failed to load stock data' }; return { error: 'Failed to load stock data' };