bugfixing
This commit is contained in:
parent
4a2defe0c3
commit
22dd3cbdc2
@ -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");
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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([
|
||||||
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
"/stockdeck",
|
||||||
return cached.data;
|
"/analyst-summary-rating",
|
||||||
|
"/stock-quote",
|
||||||
|
"/pre-post-quote",
|
||||||
|
"/wiim",
|
||||||
|
"/one-day-price",
|
||||||
|
"/next-earnings",
|
||||||
|
"/earnings-surprise",
|
||||||
|
"/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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,11 +170,11 @@ 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' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user