diff --git a/src/lib/components/Table/OptionsFlowTable.svelte b/src/lib/components/Table/OptionsFlowTable.svelte index 61d4e047..50cf253e 100644 --- a/src/lib/components/Table/OptionsFlowTable.svelte +++ b/src/lib/components/Table/OptionsFlowTable.svelte @@ -94,7 +94,8 @@ } } else { toast.error("Only for Pro Members", { - style: "border-radius: 200px; background: #2A2E39; color: #fff;", + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", }); } } diff --git a/src/routes/api/update-options-watchlist/+server.ts b/src/routes/api/update-options-watchlist/+server.ts index 1e08d37d..4e4dec79 100644 --- a/src/routes/api/update-options-watchlist/+server.ts +++ b/src/routes/api/update-options-watchlist/+server.ts @@ -3,11 +3,12 @@ import { serialize } from "object-to-formdata"; export const POST: RequestHandler = async ({ request, locals }) => { const data = await request.json(); - const { pb } = locals; + const { pb, user } = locals; let output; - try { + if(user?.tier === 'Pro') { + try { // Ensure itemIdList is always an array. const itemIdList = Array.isArray(data?.itemIdList) ? data?.itemIdList @@ -48,4 +49,11 @@ export const POST: RequestHandler = async ({ request, locals }) => { } return new Response(JSON.stringify(output?.id)); + + + } else { + return new Response(JSON.stringify("failure")); + + } + }; diff --git a/src/routes/api/update-watchlist/+server.ts b/src/routes/api/update-watchlist/+server.ts index 95e609b2..646b6063 100644 --- a/src/routes/api/update-watchlist/+server.ts +++ b/src/routes/api/update-watchlist/+server.ts @@ -5,39 +5,95 @@ export const POST = (async ({ request, locals }) => { const { user, pb } = locals; const data = await request.json(); - const ticker = data?.ticker; // This can be a string (single ticker) or an array (list of tickers) + // `tickerInput` can be a string (single ticker) or an array (list of tickers) + const tickerInput = data?.ticker || []; const watchListId = data?.watchListId; let output; + + // Determine the ticker limit: 50 for Pro users, 5 for non-Pro users. + const isProUser = user?.tier === "Pro"; + const tickerLimit = isProUser ? 100 : 5; + try { const watchList = await pb.collection("watchlist").getOne(watchListId); + // Ensure current tickers are in an array. + let currentTickers = watchList?.ticker; + if (typeof currentTickers === "string") { + try { + currentTickers = JSON.parse(currentTickers); + } catch (err) { + currentTickers = []; + } + } + currentTickers = currentTickers || []; - if (Array.isArray(ticker)) { - // If `ticker` is a list, update the watchlist directly with the new list of tickers. - output = await pb.collection("watchlist").update(watchListId, { - ticker: ticker, + if (Array.isArray(tickerInput)) { + // When replacing the entire ticker list: + if(data?.mode === 'delete') { + output = await pb.collection("watchlist").update(watchListId, { + ticker: tickerInput, }); - } else if (watchList?.ticker?.includes(ticker)) { - // Remove single ticker from the watchlist if it's already present. - const newTickerList = watchList?.ticker.filter((item) => item !== ticker); - output = await pb - .collection("watchlist") - .update(watchListId, { ticker: newTickerList }); + + } else { + + if (tickerInput.length > tickerLimit) { + return new Response( + JSON.stringify({ + error: isProUser + ? `You can only have up to ${tickerLimit} stocks in your watchlist.` + : `Upgrade to Pro to add unlimited stocks!`, + }), + { status: 403 } + ); + } + + output = await pb.collection("watchlist").update(watchListId, { + ticker: tickerInput, + }); + + + } + } else { - // Add single ticker to the watchlist if it's not present. - const newTickerList = [...watchList?.ticker, ticker]; - output = await pb - .collection("watchlist") - .update(watchListId, { ticker: newTickerList }); + // Single ticker update. + if (currentTickers?.includes(tickerInput)) { + // Remove the ticker if it's already present. + const newTickerList = currentTickers?.filter((item) => item !== tickerInput); + output = await pb.collection("watchlist").update(watchListId, { ticker: newTickerList }); + } else { + // Add the ticker if not already present. + const newTickerList = [...currentTickers, tickerInput]; + if (newTickerList.length > tickerLimit) { + return new Response( + JSON.stringify({ + error: isProUser + ? `You can only have up to ${tickerLimit} stocks in your watchlist.` + : `Upgrade to Pro to add unlimited stocks`, + }), + { status: 403 } + ); + } + output = await pb.collection("watchlist").update(watchListId, { ticker: newTickerList }); + } } } catch (e) { - // If the watchlist doesn't exist, create a new one with either the single ticker or list. + // If the watchlist doesn't exist, create a new one. + const tickersArray = Array.isArray(tickerInput) ? tickerInput : [tickerInput]; + if (tickersArray.length > tickerLimit) { + return new Response( + JSON.stringify({ + error: isProUser + ? `You can only have up to ${tickerLimit} stocks in your watchlist.` + : `Upgrade to Pro to add unlimited stocks!`, + }), + { status: 403 } + ); + } output = await pb.collection("watchlist").create( serialize({ user: user?.id, - ticker: Array.isArray(ticker) - ? JSON.stringify(ticker) - : JSON.stringify([ticker]), + ticker: JSON.stringify(tickersArray), title: "Favorites", }) ); diff --git a/src/routes/etf/[tickerID]/+layout.svelte b/src/routes/etf/[tickerID]/+layout.svelte index f7cf0e57..07cc662d 100644 --- a/src/routes/etf/[tickerID]/+layout.svelte +++ b/src/routes/etf/[tickerID]/+layout.svelte @@ -83,57 +83,65 @@ async function toggleUserWatchlist(watchListId: string) { try { - isTickerIncluded = !isTickerIncluded; - + // Find the index of the watchlist const watchlistIndex = userWatchList?.findIndex( (item) => item?.id === watchListId, ); - if (watchlistIndex !== -1) { - const existingTickerIndex = - userWatchList[watchlistIndex]?.ticker?.indexOf($etfTicker); + if (watchlistIndex !== -1 && watchlistIndex !== undefined) { + const watchlist = userWatchList[watchlistIndex]; + const existingTickerIndex = watchlist?.ticker?.indexOf($etfTicker); + + let updatedTickers = [...(watchlist?.ticker || [])]; // Ensure we don't mutate directly if (existingTickerIndex !== -1) { - // If the $etfTicker exists, remove it from the array - userWatchList[watchlistIndex]?.ticker?.splice(existingTickerIndex, 1); + // Remove the ticker if it exists + updatedTickers.splice(existingTickerIndex, 1); } else { - // If the $etfTicker doesn't exist, add it to the array - userWatchList[watchlistIndex]?.ticker?.push($etfTicker); + // Add the ticker if it doesn't exist + updatedTickers.push($etfTicker); + + // Check tier limits + if (data?.user?.tier !== "Pro" && updatedTickers.length > 5) { + toast.error("Upgrade to Pro to add unlimited stocks!", { + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", + }); + return; + } } - // Update the userWatchList - userWatchList = [...userWatchList]; - } + // Update the local state immutably + userWatchList = userWatchList.map((item, idx) => + idx === watchlistIndex ? { ...item, ticker: updatedTickers } : item, + ); - const postData = { - watchListId: watchListId, - ticker: $etfTicker, - }; + // Send API request + const response = await fetch("/api/update-watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ watchListId, ticker: $etfTicker }), + }); - const response = await fetch("/api/update-watchlist", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(postData), - }); + const output = await response.json(); - if (!response.ok) { - throw new Error("Network response was not ok"); - } - - const output = await response.json(); - - // Update the userWatchList with the response from the server - if (watchlistIndex !== -1) { - userWatchList[watchlistIndex] = output; - userWatchList = [...userWatchList]; + // Update userWatchList based on API response + userWatchList = userWatchList.map((item) => + item.id === watchListId ? output : item, + ); } else { + // If watchlist doesn't exist, create a new entry + const response = await fetch("/api/update-watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ watchListId, ticker: $etfTicker }), + }); + + const output = await response.json(); userWatchList = [...userWatchList, output]; } } catch (error) { console.error("An error occurred:", error); - // Handle the error appropriately (e.g., show an error message to the user) } } diff --git a/src/routes/index/[tickerID]/+layout.svelte b/src/routes/index/[tickerID]/+layout.svelte index d6925031..c09bec64 100644 --- a/src/routes/index/[tickerID]/+layout.svelte +++ b/src/routes/index/[tickerID]/+layout.svelte @@ -81,57 +81,65 @@ async function toggleUserWatchlist(watchListId: string) { try { - isTickerIncluded = !isTickerIncluded; - + // Find the index of the watchlist const watchlistIndex = userWatchList?.findIndex( (item) => item?.id === watchListId, ); - if (watchlistIndex !== -1) { - const existingTickerIndex = - userWatchList[watchlistIndex]?.ticker?.indexOf($indexTicker); + if (watchlistIndex !== -1 && watchlistIndex !== undefined) { + const watchlist = userWatchList[watchlistIndex]; + const existingTickerIndex = watchlist?.ticker?.indexOf($indexTicker); + + let updatedTickers = [...(watchlist?.ticker || [])]; // Ensure we don't mutate directly if (existingTickerIndex !== -1) { - // If the $indexTicker exists, remove it from the array - userWatchList[watchlistIndex]?.ticker?.splice(existingTickerIndex, 1); + // Remove the ticker if it exists + updatedTickers.splice(existingTickerIndex, 1); } else { - // If the $indexTicker doesn't exist, add it to the array - userWatchList[watchlistIndex]?.ticker?.push($indexTicker); + // Add the ticker if it doesn't exist + updatedTickers.push($indexTicker); + + // Check tier limits + if (data?.user?.tier !== "Pro" && updatedTickers.length > 5) { + toast.error("Upgrade to Pro to add unlimited stocks!", { + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", + }); + return; + } } - // Update the userWatchList - userWatchList = [...userWatchList]; - } + // Update the local state immutably + userWatchList = userWatchList.map((item, idx) => + idx === watchlistIndex ? { ...item, ticker: updatedTickers } : item, + ); - const postData = { - watchListId: watchListId, - ticker: $indexTicker, - }; + // Send API request + const response = await fetch("/api/update-watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ watchListId, ticker: $indexTicker }), + }); - const response = await fetch("/api/update-watchlist", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(postData), - }); + const output = await response.json(); - if (!response.ok) { - throw new Error("Network response was not ok"); - } - - const output = await response.json(); - - // Update the userWatchList with the response from the server - if (watchlistIndex !== -1) { - userWatchList[watchlistIndex] = output; - userWatchList = [...userWatchList]; + // Update userWatchList based on API response + userWatchList = userWatchList.map((item) => + item.id === watchListId ? output : item, + ); } else { + // If watchlist doesn't exist, create a new entry + const response = await fetch("/api/update-watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ watchListId, ticker: $indexTicker }), + }); + + const output = await response.json(); userWatchList = [...userWatchList, output]; } } catch (error) { console.error("An error occurred:", error); - // Handle the error appropriately (e.g., show an error message to the user) } } @@ -317,7 +325,8 @@ } } $: isTickerIncluded = userWatchList?.some( - (item) => item.user === data?.user?.id && item.ticker?.includes($indexTicker), + (item) => + item.user === data?.user?.id && item.ticker?.includes($indexTicker), ); $: charNumber = $screenWidth < 640 ? 25 : 40; @@ -372,7 +381,10 @@ class="flex-1 flex-shrink-0 flex flex-row items-center justify-between -mt-2" > + href={/^\/(stocks|etf|index)/.test($previousPage || "") + ? "/" + : $previousPage || "/"} + > - shareContent("https://stocknear.com/index/" + $indexTicker)} + shareContent( + "https://stocknear.com/index/" + $indexTicker, + )} > - - changeSection("history")} diff --git a/src/routes/stocks/[tickerID]/+layout.svelte b/src/routes/stocks/[tickerID]/+layout.svelte index 1b8c086f..4a0da21b 100644 --- a/src/routes/stocks/[tickerID]/+layout.svelte +++ b/src/routes/stocks/[tickerID]/+layout.svelte @@ -92,57 +92,65 @@ async function toggleUserWatchlist(watchListId: string) { try { - isTickerIncluded = !isTickerIncluded; - + // Find the index of the watchlist const watchlistIndex = userWatchList?.findIndex( (item) => item?.id === watchListId, ); - if (watchlistIndex !== -1) { - const existingTickerIndex = - userWatchList[watchlistIndex]?.ticker?.indexOf($stockTicker); + if (watchlistIndex !== -1 && watchlistIndex !== undefined) { + const watchlist = userWatchList[watchlistIndex]; + const existingTickerIndex = watchlist?.ticker?.indexOf($stockTicker); + + let updatedTickers = [...(watchlist?.ticker || [])]; // Ensure we don't mutate directly if (existingTickerIndex !== -1) { - // If the $stockTicker exists, remove it from the array - userWatchList[watchlistIndex]?.ticker?.splice(existingTickerIndex, 1); + // Remove the ticker if it exists + updatedTickers.splice(existingTickerIndex, 1); } else { - // If the $stockTicker doesn't exist, add it to the array - userWatchList[watchlistIndex]?.ticker?.push($stockTicker); + // Add the ticker if it doesn't exist + updatedTickers.push($stockTicker); + + // Check tier limits + if (data?.user?.tier !== "Pro" && updatedTickers.length > 5) { + toast.error("Upgrade to Pro to add unlimited stocks!", { + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", + }); + return; + } } - // Update the userWatchList - userWatchList = [...userWatchList]; - } + // Update the local state immutably + userWatchList = userWatchList.map((item, idx) => + idx === watchlistIndex ? { ...item, ticker: updatedTickers } : item, + ); - const postData = { - watchListId: watchListId, - ticker: $stockTicker, - }; + // Send API request + const response = await fetch("/api/update-watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ watchListId, ticker: $stockTicker }), + }); - const response = await fetch("/api/update-watchlist", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(postData), - }); + const output = await response.json(); - if (!response.ok) { - throw new Error("Network response was not ok"); - } - - const output = await response.json(); - - // Update the userWatchList with the response from the server - if (watchlistIndex !== -1) { - userWatchList[watchlistIndex] = output; - userWatchList = [...userWatchList]; + // Update userWatchList based on API response + userWatchList = userWatchList.map((item) => + item.id === watchListId ? output : item, + ); } else { + // If watchlist doesn't exist, create a new entry + const response = await fetch("/api/update-watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ watchListId, ticker: $stockTicker }), + }); + + const output = await response.json(); userWatchList = [...userWatchList, output]; } } catch (error) { console.error("An error occurred:", error); - // Handle the error appropriately (e.g., show an error message to the user) } } @@ -389,7 +397,9 @@ class="flex-1 flex-shrink-0 flex flex-row items-center justify-between -mt-2" > item?.symbol), watchListId: displayWatchList?.id, + mode: "delete", }; const response = await fetch("/api/update-watchlist", { @@ -513,57 +514,86 @@ } displayList = rawTabData?.slice(0, 8); } - async function handleAddTicker(event, ticker) { - // Ensure inputValue is reset - if (!watchList?.some((item) => item?.symbol === ticker)) { - } else { - toast.error(`This symbol is already in your watchlist`, { + async function handleAddTicker(event, ticker) { + event.preventDefault(); // Prevent the default form submission behavior + + // Check if the ticker is already in the watchlist. + if (watchList?.some((item) => item?.symbol === ticker)) { + toast.error("This symbol is already in your watchlist", { style: "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", }); - inputValue = ""; - event.preventDefault(); return; } - // Exit edit mode + // Exit edit mode. editMode = false; - // Prepare the data to send to the API + // Prepare the data to send to the API. const postData = { ticker: ticker, watchListId: displayWatchList?.id, }; - // Send the updated watchlist to the server - const response = await fetch("/api/update-watchlist", { + // Create a promise for the fetch request. + const promise = fetch("/api/update-watchlist", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(postData), - }); - - // Update the allList with the new watchlist - allList = allList?.map((item) => { - if (item?.id === displayWatchList?.id) { - return { ...item, tickers: watchList }; // Update tickers in the watchlist + }).then(async (response) => { + const output = await response.json(); + // If the response is not OK, throw an error with the message from the API. + if (!response.ok) { + throw new Error(output.error || "Failed to update watchlist"); } - return item; // Return unchanged item + return output; }); - // Refresh the displayWatchList with the updated watchlist - displayWatchList = allList?.find( - (item) => item?.id === displayWatchList?.id, + // Use toast.promise to display notifications based on the promise's state. + toast.promise( + promise, + { + loading: "Updating watchlist...", + success: "Watchlist updated successfully!", + error: (err) => err.message || "Failed to update watchlist", + }, + { + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", + }, ); - // Fetch the updated watchlist data (assuming this function refreshes the UI or state) - await getWatchlistData(); + try { + // Await the promise, which returns the updated watchlist data. + const updatedData = await promise; - inputValue = ""; - event?.preventDefault(); + // Update the local allList with the updated watchlist. + // (Assuming updatedData.ticker contains the new ticker list.) + allList = allList?.map((item) => { + if (item?.id === displayWatchList?.id) { + return { ...item, tickers: updatedData.ticker }; + } + return item; + }); + + // Refresh displayWatchList from the updated list. + displayWatchList = allList?.find( + (item) => item?.id === displayWatchList?.id, + ); + + // Refresh the watchlist data (UI or state refresh). + await getWatchlistData(); + + // Reset the input value. + inputValue = ""; + } catch (error) { + console.error("Error updating watchlist:", error); + // Note: The error toast is already displayed by toast.promise. + } } async function handleResetAll() {