frontend/src/routes/stocks/[tickerID]/+page.svelte
MuslemRahimi 4309cfdb4d ui fixes
2024-12-01 20:44:03 +01:00

1465 lines
53 KiB
Svelte

<script lang="ts">
import { AreaSeries, Chart, PriceLine } from "svelte-lightweight-charts";
import { TrackingModeExitMode, ColorType } from "lightweight-charts";
import {
getCache,
setCache,
numberOfUnreadNotification,
globalForm,
realtimePrice,
priceIncrease,
wsBidPrice,
wsAskPrice,
currentPortfolioPrice,
stockTicker,
displayCompanyName,
shouldUpdatePriceChart,
priceChartData,
} from "$lib/store";
import { onDestroy, onMount } from "svelte";
import BullBearSay from "$lib/components/BullBearSay.svelte";
import WIIM from "$lib/components/WIIM.svelte";
import News from "$lib/components/News.svelte";
import NextEarnings from "$lib/components/NextEarnings.svelte";
import EarningsSurprise from "$lib/components/EarningsSurprise.svelte";
import DividendAnnouncement from "$lib/components/DividendAnnouncement.svelte";
import Sidecard from "$lib/components/Sidecard.svelte";
import { convertTimestamp, abbreviateNumber } from "$lib/utils";
import { Button } from "$lib/components/shadcn/button/index.js";
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
import { goto } from "$app/navigation";
export let data;
export let form;
let stockDeck = {};
$: previousClose = data?.getStockQuote?.previousClose;
//============================================//
const intervals = ["1D", "1W", "1M", "6M", "1Y", "MAX"];
let chart = null;
async function checkChart() {
if (chart) {
clearInterval(intervalId);
fitContentChart();
}
}
//const startTimeTracking = performance.now();
//==========================//
$: {
if (output !== null) {
let change;
let graphChange;
let currentDataRow;
let currentDataRowOneDay;
let baseClose = previousClose;
let graphBaseClose;
const length = oneDayPrice?.length;
for (let i = length - 1; i >= 0; i--) {
if (!isNaN(oneDayPrice[i]?.close)) {
currentDataRowOneDay = oneDayPrice[i];
break;
}
}
// Determine current data row and base close price based on displayData
switch (displayData) {
case "1W":
currentDataRow = oneWeekPrice?.at(-1); // Latest entry for 1 week
graphBaseClose = oneWeekPrice?.at(0)?.close;
break;
case "1M":
currentDataRow = oneMonthPrice?.at(-1); // Latest entry for 1 month
graphBaseClose = oneMonthPrice?.at(0)?.close;
break;
case "6M":
currentDataRow = sixMonthPrice?.at(-1); // Latest entry for 6 months
graphBaseClose = sixMonthPrice?.at(0)?.close;
break;
case "1Y":
currentDataRow = oneYearPrice?.at(-1); // Latest entry for 1 year
graphBaseClose = oneYearPrice?.at(0)?.close;
break;
case "MAX":
currentDataRow = maxPrice?.at(-1); // Latest entry for MAX range
graphBaseClose = maxPrice?.at(0)?.close;
break;
}
// Calculate percentage change if baseClose and currentDataRow are valid
const closeValue =
$realtimePrice !== null
? $realtimePrice
: (currentDataRowOneDay?.close ?? currentDataRowOneDay?.value);
const graphCloseValue =
$realtimePrice !== null
? $realtimePrice
: (currentDataRow?.close ?? currentDataRow?.value);
if (closeValue && baseClose) {
change = ((closeValue / baseClose - 1) * 100).toFixed(2);
}
if (graphCloseValue && graphBaseClose) {
graphChange = ((graphCloseValue / graphBaseClose - 1) * 100).toFixed(2);
}
// Format date
const date = new Date(currentDataRowOneDay?.time * 1000);
const options = {
day: "2-digit",
month: "short",
year: "numeric",
hour: "numeric",
minute: "2-digit",
timeZone: "UTC",
};
const formattedDate = date?.toLocaleString("en-US", options);
const safeFormattedDate =
formattedDate === "Invalid Date"
? convertTimestamp(data?.getStockQuote?.timestamp)
: formattedDate;
// Set display legend
displayLegend = {
close:
currentDataRowOneDay?.close?.toFixed(2) ??
data?.getStockQuote?.price?.toFixed(2),
date: safeFormattedDate,
change,
graphChange: displayData === "1D" ? change : graphChange,
};
}
}
//==========================//
$: {
if ($stockTicker && typeof window !== "undefined") {
// add a check to see if running on client-side
if ($realtimePrice !== null && $realtimePrice !== 0) {
$currentPortfolioPrice = $realtimePrice;
} else if ($realtimePrice === null || $realtimePrice === 0) {
$realtimePrice = data?.getStockQuote?.price;
$currentPortfolioPrice = $realtimePrice;
} else if (oneDayPrice?.length !== 0) {
const length = oneDayPrice?.length;
for (let i = length - 1; i >= 0; i--) {
if (!isNaN(oneDayPrice[i]?.close)) {
$currentPortfolioPrice = oneDayPrice[i]?.close;
break;
}
}
}
}
}
let displayData;
let colorChange;
let topColorChange;
let bottomColorChange;
let lastValue;
async function changeData(state) {
switch (state) {
case "1D":
displayData = "1D";
if (oneDayPrice?.length !== 0) {
displayLastLogicalRangeValue = oneDayPrice?.at(0)?.close; //previousClose
const length = oneDayPrice?.length;
for (let i = length - 1; i >= 0; i--) {
if (!isNaN(oneDayPrice[i]?.close)) {
lastValue = oneDayPrice[i]?.close;
break;
}
}
} else {
displayLastLogicalRangeValue = null;
lastValue = null;
}
break;
case "1W":
displayData = "1W";
await historicalPrice("one-week");
if (oneWeekPrice?.length !== 0) {
displayLastLogicalRangeValue = oneWeekPrice?.at(0)?.close;
lastValue = oneWeekPrice?.slice(-1)?.at(0)?.close;
} else {
displayLastLogicalRangeValue = null;
lastValue = null;
}
break;
case "1M":
displayData = "1M";
await historicalPrice("one-month");
if (oneMonthPrice?.length !== 0) {
displayLastLogicalRangeValue = oneMonthPrice?.at(0)?.close;
lastValue = oneMonthPrice.slice(-1)?.at(0)?.close;
} else {
displayLastLogicalRangeValue = null;
lastValue = null;
}
break;
case "6M":
displayData = "6M";
await historicalPrice("six-months");
if (sixMonthPrice?.length !== 0) {
displayLastLogicalRangeValue = sixMonthPrice?.at(0)?.close;
lastValue = sixMonthPrice?.slice(-1)?.at(0)?.close;
} else {
displayLastLogicalRangeValue = null;
lastValue = null;
}
break;
case "1Y":
displayData = "1Y";
await historicalPrice("one-year");
if (oneYearPrice?.length !== 0) {
displayLastLogicalRangeValue = oneYearPrice?.at(0)?.close;
lastValue = oneYearPrice.slice(-1)?.at(0)?.close;
} else {
displayLastLogicalRangeValue = null;
lastValue = null;
}
break;
case "MAX":
displayData = "MAX";
await historicalPrice("max");
if (maxPrice?.length !== 0) {
displayLastLogicalRangeValue = maxPrice?.at(0)?.close;
lastValue = maxPrice.slice(-1)?.at(0)?.close;
} else {
displayLastLogicalRangeValue = null;
lastValue = null;
}
break;
default:
return;
}
colorChange =
lastValue < displayLastLogicalRangeValue ? "#FF2F1F" : "#00FC50";
topColorChange =
lastValue < displayLastLogicalRangeValue
? "rgb(255, 47, 31, 0.2)"
: "rgb(16, 219, 6, 0.2)";
bottomColorChange =
lastValue < displayLastLogicalRangeValue
? "rgb(255, 47, 31, 0.001)"
: "rgb(16, 219, 6, 0.001)";
fitContentChart();
//trackButtonClick('Time Period: '+ state)
}
let output = null;
//====================================//
let intervalId = null;
let oneDayPrice = [];
let oneWeekPrice = [];
let oneMonthPrice = [];
let sixMonthPrice = [];
let oneYearPrice = [];
let maxPrice = [];
async function historicalPrice(timePeriod: string) {
const cachedData = getCache($stockTicker, "historicalPrice" + timePeriod);
if (cachedData) {
switch (timePeriod) {
case "one-week":
oneWeekPrice = cachedData;
break;
case "one-month":
oneMonthPrice = cachedData;
break;
case "six-months":
sixMonthPrice = cachedData;
break;
case "one-year":
oneYearPrice = cachedData;
break;
case "max":
maxPrice = cachedData;
break;
default:
console.log(`Unsupported time period: ${timePeriod}`);
}
} else {
output = null;
const postData = {
ticker: $stockTicker,
timePeriod: timePeriod,
};
const response = await fetch("/api/historical-price", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
output = (await response?.json()) ?? [];
const mapData = (data) =>
data?.map(({ time, open, high, low, close }) => ({
time: ["1D", "1W", "1M"]?.includes(displayData)
? Date?.parse(time + "Z") / 1000
: time,
open,
high,
low,
close,
}));
const mappedData = mapData(output);
try {
switch (timePeriod) {
case "one-week":
oneWeekPrice = mappedData;
break;
case "one-month":
oneMonthPrice = mappedData;
break;
case "six-months":
sixMonthPrice = mappedData;
break;
case "one-year":
oneYearPrice = mappedData;
break;
case "max":
maxPrice = mappedData;
break;
default:
console.log(`Unsupported time period: ${timePeriod}`);
}
setCache($stockTicker, mappedData, "historicalPrice" + timePeriod);
} catch (e) {
console.log(e);
}
}
}
async function initializePrice() {
output = null;
if (intervalId) {
clearInterval(intervalId);
}
intervalId = setInterval(checkChart, 0);
try {
output = [...data?.getOneDayPrice] ?? [];
oneDayPrice = output?.map((item) => ({
time: Date?.parse(item?.time + "Z") / 1000,
open: item?.open !== null ? item?.open : NaN,
high: item?.high !== null ? item?.high : NaN,
low: item?.low !== null ? item?.low : NaN,
close: item?.close !== null ? item?.close : NaN,
}));
displayData =
oneDayPrice?.length === 0 && sixMonthPrice?.length !== 0 ? "6M" : "1D";
//lastValue = oneDayPrice[oneDayPrice?.length - 1]?.value;
if (displayData === "1D") {
const length = oneDayPrice?.length;
for (let i = length - 1; i >= 0; i--) {
if (!isNaN(oneDayPrice[i]?.close)) {
lastValue = oneDayPrice[i]?.close;
break;
}
}
} else if (displayData === "6M") {
lastValue = sixMonthPrice?.slice(-1)?.at(0)?.close;
}
displayLastLogicalRangeValue =
oneDayPrice?.length === 0 && sixMonthPrice?.length !== 0
? sixMonthPrice?.at(0)?.close
: oneDayPrice?.at(0)?.close; //previousClose;
//colorChange = lastValue < displayLastLogicalRangeValue ? "#CC3636" : "#367E18";
colorChange =
lastValue < displayLastLogicalRangeValue ? "#FF2F1F" : "#00FC50";
topColorChange =
lastValue < displayLastLogicalRangeValue
? "rgb(255, 47, 31, 0.2)"
: "rgb(16, 219, 6, 0.2)";
bottomColorChange =
lastValue < displayLastLogicalRangeValue
? "rgb(255, 47, 31, 0.001)"
: "rgb(16, 219, 6, 0.001)";
} catch (e) {
console.log(e);
}
}
let displayLegend = { close: "-", date: "-" };
let displayLastLogicalRangeValue;
const fitContentChart = async () => {
if (displayData === "1Y" && oneYearPrice?.length === 0) {
} else if (chart !== null && typeof window !== "undefined") {
chart?.timeScale().fitContent();
chart?.applyOptions({
trackingMode: {
exitMode: TrackingModeExitMode.OnTouchEnd,
},
});
}
};
let width = 580;
//Initial height of graph
let height = 350;
let observer;
let ref;
ref = (element) => {
if (observer) {
observer?.disconnect();
}
if (!element) {
return;
}
observer = new ResizeObserver(([entry]) => {
width = entry.contentRect.width;
height = entry.contentRect.height;
});
observer.observe(element);
};
//===============================================//
function defaultTickMarkFormatter(timePoint, tickMarkType, locale) {
const formatOptions = {
timeZone: "UTC",
};
switch (tickMarkType) {
case 0: // TickMarkType.Year:
formatOptions.year = "numeric";
break;
case 1: // TickMarkType.Month:
formatOptions.month = "short";
break;
case 2: // TickMarkType.DayOfMonth:
formatOptions.day = "numeric";
break;
case 3: // TickMarkType.Time:
formatOptions.hour12 = true; // Use 12-hour clock
formatOptions.hour = "numeric"; // Use numeric hour without leading zero
break;
case 4: // TickMarkType.TimeWithSeconds:
formatOptions.hour12 = true; // Use 12-hour clock
formatOptions.hour = "numeric"; // Use numeric hour without leading zero
formatOptions.minute = "2-digit"; // Always show minutes with leading zero
formatOptions.second = "2-digit"; // Always show seconds with leading zero
break;
default:
// Ensure this default case handles unexpected tickMarkType values
}
if ([3, 4]?.includes(tickMarkType)) {
const date = new Date(timePoint?.timestamp * 1000);
return new Intl.DateTimeFormat(locale, formatOptions)?.format(date);
} else {
const date = new Date(timePoint?.timestamp);
return new Intl.DateTimeFormat(locale, formatOptions)?.format(date);
}
}
$: options = {
width: width,
height: height,
layout: {
background: {
type: ColorType.Solid,
color: "#09090B",
},
textColor: "#d1d4dc",
},
grid: {
vertLines: {
color: "#09090B",
visible: false,
},
horzLines: {
color: "#09090B",
visible: false,
},
},
crosshair: {
horzLine: {
visible: true,
labelBackgroundColor: "#fff",
},
vertLine: {
labelVisible: true,
labelBackgroundColor: "#fff",
style: 0,
},
},
priceScale: {
autoScale: true,
scaleMargins: {
top: 0.3,
bottom: 0.25,
},
},
rightPriceScale: {
scaleMargins: {
top: 0.3,
bottom: 0.25,
borderVisible: false,
},
visible: true,
borderVisible: false,
mode: 1, // Keeps price scale fixed
},
leftPriceScale: {
visible: false,
borderColor: "rgba(197, 203, 206, 0.8)",
},
handleScale: {
mouseWheel: false,
pinch: false, // Disables scaling via pinch gestures
axisPressedMouseMove: false, // Disables scaling by dragging the axis with the mouse
},
handleScroll: {
mouseWheel: false,
horzTouchDrag: false,
vertTouchDrag: false,
pressedMouseMove: false,
},
timeScale: {
borderColor: "#fff",
textColor: "#fff",
borderVisible: false,
visible: true,
fixLeftEdge: true,
fixRightEdge: true,
timeVisible: ["1D", "1W", "1M"].includes(displayData),
secondsVisible: false,
tickMarkFormatter: (time, tickMarkType, locale) => {
return defaultTickMarkFormatter(
{ timestamp: time },
tickMarkType,
locale,
);
},
},
};
onDestroy(async () => {
$priceIncrease = null;
$globalForm = [];
shouldUpdatePriceChart.set(false);
});
$: dataMapping = {
"1D": oneDayPrice,
"1W": oneWeekPrice,
"1M": oneMonthPrice,
"6M": sixMonthPrice,
"1Y": oneYearPrice,
MAX: maxPrice,
};
$: {
if (form) {
$globalForm = form;
}
}
async function exportData(timePeriod: string) {
if (data?.user?.tier === "Pro") {
let exportList = [];
const response = await fetch("/api/export-price-data", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ ticker: $stockTicker, timePeriod: timePeriod }),
});
exportList = await response.json();
exportList = exportList?.map(
({ time, open, high, low, close, date, volume }) => ({
date: timePeriod === "max" ? time : date, // Use 'time' if timePeriod is "max", otherwise use 'date'
open,
high,
low,
close,
volume,
}),
);
const csvRows = [];
// Add headers row
csvRows.push("time,open,high,low,close, volume");
// Add data rows
for (const row of exportList) {
const csvRow = `${row.date},${row.open},${row.high},${row.low},${row.close},${row.volume}`;
csvRows.push(csvRow);
}
// Create CSV blob and trigger download
const csv = csvRows.join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.setAttribute("hidden", "");
a.setAttribute("href", url);
a.setAttribute("download", `${$stockTicker}_${timePeriod}.csv`);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} else {
goto("/pricing");
}
}
function updateClosePrice(data, extendPriceChart) {
const newDateParsedUTC = Date?.parse(extendPriceChart?.time + "Z") / 1000; // Parse the incoming time
const closePrice = extendPriceChart?.price; // Store the close price for easier reference
let foundMatch = false;
let lastNonNullCloseIndex = null;
// Iterate through data to find the right time slot
for (let i = 0; i < data.length; i++) {
// Check if the timestamp matches
if (data[i].time === newDateParsedUTC) {
data[i].close = closePrice; // Update the existing close price
foundMatch = true;
break; // Exit loop once matched
}
// Keep track of the last non-null close price
if (data[i].close !== null) {
lastNonNullCloseIndex = i;
}
}
// If no matching timestamp was found, add new data
if (!foundMatch) {
// Only update the last non-null close if it exists
if (lastNonNullCloseIndex !== null) {
data[lastNonNullCloseIndex].close = closePrice; // Update with the latest close price
} else {
// If there's no previous close data, add a new entry
data.push({ time: newDateParsedUTC, close: closePrice }); // Add new data
}
}
return data; // Return updated data
}
onMount(() => {
shouldUpdatePriceChart.subscribe(async (value) => {
if (
value &&
chart !== null &&
$realtimePrice !== null &&
oneDayPrice?.length > 0 &&
$priceChartData?.time !== null &&
$priceChartData?.price !== null
) {
// Create a new array and update oneDayPrice to trigger reactivity
const updatedPrice = updateClosePrice(oneDayPrice, $priceChartData);
oneDayPrice = [...updatedPrice]; // Reassign the updated array to trigger reactivity
try {
chart?.update(oneDayPrice); // Update the chart with the new prices
} catch (e) {
// Handle error if chart update fails
//console.error("Chart update error:", e);
}
shouldUpdatePriceChart.set(false); // Reset the update flag
}
});
});
$: {
if ($stockTicker && typeof window !== "undefined") {
// add a check to see if running on client-side
shouldUpdatePriceChart.set(false);
oneDayPrice = [];
oneWeekPrice = [];
oneMonthPrice = [];
oneYearPrice = [];
maxPrice = [];
output = null;
stockDeck = data?.getStockDeck; // Essential otherwise chart will not be updated since we wait until #layout.server.ts server response is finished
initializePrice();
}
}
</script>
<svelte:head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""}
{data?.companyName} ({$stockTicker}) Stock Price, Quote & News · stocknear
</title>
<meta
name="description"
content={`Get a real-time ${data?.companyName} (${$stockTicker}) stock chart, price quote with breaking news, financials, statistics, charts and more.`}
/>
<!-- Other meta tags -->
<meta
property="og:title"
content={`${data?.companyName} (${$stockTicker}) Stock Price, Quote & News · stocknear`}
/>
<meta
property="og:description"
content={`Get a real-time ${data?.companyName} (${$stockTicker}) stock chart, price quote with breaking news, financials, statistics, charts and more.`}
/>
<!--<meta property="og:image" content="https://stocknear-pocketbase.s3.amazonaws.com/logo/meta_logo.jpg"/>-->
<meta property="og:type" content="website" />
<!-- Add more Open Graph meta tags as needed -->
<!-- Twitter specific meta tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content={`${data?.companyName} (${$stockTicker}) Stock Price, Quote & News · stocknear`}
/>
<meta
name="twitter:description"
content={`Get a real-time ${data?.companyName} (${$stockTicker}) stock chart, price quote with breaking news, financials, statistics, charts and more.`}
/>
<!--<meta name="twitter:image" content="https://stocknear-pocketbase.s3.amazonaws.com/logo/meta_logo.jpg"/>-->
<!-- Add more Twitter meta tags as needed -->
</svelte:head>
<section class="bg-[#09090B] min-h-screen pb-40 overflow-hidden w-full">
<div class="w-full m-auto overflow-hidden">
<div
class="md:flex md:justify-between md:divide-x md:divide-slate-800 w-full"
>
<!-- Main content -->
<div class="pb-12 md:pb-20 w-full sm:pr-6 xl:pr-0">
<div class="xl:pr-10">
<div
class="hidden sm:flex flex-row items-center pl-1 sm:pl-6 w-full mt-4"
>
{#if !$stockTicker?.includes(".")}
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="ml-auto border-gray-600 border bg-[#09090B] sm:hover:bg-[#27272A] ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
>
<span class="truncate text-white">Export</span>
<svg
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 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>
</svg>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-fit h-fit max-h-72 overflow-y-auto scroller"
>
<DropdownMenu.Label class="text-gray-400">
Historical Stock Price
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<!--
<DropdownMenu.Item on:click={exportData} class="cursor-pointer sm:hover:bg-[#27272A]">
<svg class="w-3.5 h-3.5 mr-1 {data?.user?.tier === 'Pro' ? 'hidden' : ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#A3A3A3" d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"/></svg>
1 min
</DropdownMenu.Item>
<DropdownMenu.Item on:click={exportData} class="cursor-pointer sm:hover:bg-[#27272A]">
<svg class="w-3.5 h-3.5 mr-1 {data?.user?.tier === 'Pro' ? 'hidden' : ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#A3A3A3" d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"/></svg>
5 min
</DropdownMenu.Item>
<DropdownMenu.Item on:click={exportData} class="cursor-pointer sm:hover:bg-[#27272A]">
<svg class="w-3.5 h-3.5 mr-1 {data?.user?.tier === 'Pro' ? 'hidden' : ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#A3A3A3" d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"/></svg>
15 min
</DropdownMenu.Item>
-->
<DropdownMenu.Item
on:click={() => exportData("30min")}
class="cursor-pointer sm:hover:bg-[#27272A]"
>
<svg
class="w-3.5 h-3.5 mr-1 {data?.user?.tier === 'Pro'
? 'hidden'
: ''}"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="#A3A3A3"
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
/></svg
>
30 min
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => exportData("1hour")}
class="cursor-pointer sm:hover:bg-[#27272A]"
>
<svg
class="w-3.5 h-3.5 mr-1 {data?.user?.tier === 'Pro'
? 'hidden'
: ''}"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="#A3A3A3"
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
/></svg
>
1 hour
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => exportData("max")}
class="cursor-pointer sm:hover:bg-[#27272A]"
>
<svg
class="w-3.5 h-3.5 mr-1 {data?.user?.tier === 'Pro'
? 'hidden'
: ''}"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="#A3A3A3"
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
/></svg
>
1 day
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
</div>
<!--End Time Interval-->
<!--End Ticker Section-->
<!-- Start Graph -->
<div class="sm:pl-7 mt-4 mb-5 lg:flex lg:flex-row lg:gap-x-4 w-full">
{#if dataMapping[displayData]?.length === 0}
<div
class="order-1 lg:order-5 m-auto grow overflow-hidden border-gray-600 py-0.5 xs:py-1 sm:px-0.5 sm:pb-3 sm:pt-2.5 lg:mb-0 lg:border-0 lg:border-l lg:border-sharp lg:px-0 lg:py-0 lg:pl-5 md:mb-4 md:border-b"
>
<div class="flex items-center justify-between py-1 sm:pt-0.5">
<div class="hide-scroll overflow-x-auto">
<ul
class="flex space-x-[3px] whitespace-nowrap pl-0.5 xs:space-x-1"
>
{#each intervals as interval}
<li>
<button
on:click={() => changeData(interval)}
class="px-1 py-1 text-sm sm:text-[1rem] xs:px-[3px] bp:px-1.5 sm:px-2 xxxl:px-3"
>
<span
class="block {displayData === interval
? 'text-white'
: 'text-gray-400'}">{interval}</span
>
<div
class="{displayData === interval
? `bg-[${colorChange}] `
: 'bg-[#09090B]'} mt-1 h-[3px] w-[1.5rem] m-auto rounded-full"
/>
</button>
</li>
{/each}
</ul>
</div>
</div>
<div class="h-[250px] sm:h-[350px]">
<div
class="flex h-full w-full flex-col items-center justify-center rounded-sm border border-gray-800 p-6 text-center md:p-12"
>
<div
class="mb-4 text-white text-[1rem] sm:text-xl font-semibold"
>
No {displayData} chart data available
</div>
</div>
</div>
</div>
{:else}
<div
class="order-1 lg:order-5 grow overflow-hidden border-gray-600 py-0.5 xs:py-1 sm:px-0.5 sm:pb-3 sm:pt-2.5 lg:mb-0 lg:border-0 lg:border-l lg:border-sharp lg:px-0 lg:py-0 lg:pl-5 md:mb-4 md:border-b"
>
<div class="flex items-center justify-between py-1 sm:pt-0.5">
<div class="hide-scroll overflow-x-auto">
<ul
class="flex space-x-[3px] whitespace-nowrap pl-0.5 xs:space-x-1"
>
{#each intervals as interval}
<li>
<button
on:click={() => changeData(interval)}
class="px-1 py-1 text-sm sm:text-[1rem] xs:px-[3px] bp:px-1.5 sm:px-2 xxxl:px-3"
>
<span
class="block {displayData === interval
? 'text-white'
: 'text-gray-400'}">{interval}</span
>
<div
class="{displayData === interval
? `bg-[${colorChange}] `
: 'bg-[#09090B]'} mt-1 h-[3px] w-[1.5rem] m-auto rounded-full"
/>
</button>
</li>
{/each}
</ul>
</div>
<div
class="flex shrink flex-row space-x-1 pr-1 text-sm sm:text-[1rem]"
>
<span
class={displayLegend?.graphChange >= 0
? "before:content-['+'] text-[#00FC50]"
: "text-[#FF2F1F]"}
>
{displayLegend?.graphChange}%
</span>
<span class="hidden text-gray-200 sm:block"
>({displayData})</span
>
</div>
</div>
{#if output !== null && dataMapping[displayData]?.length !== 0}
<Chart
{...options}
autoSize={true}
ref={(api) => (chart = api)}
>
{#if displayData === "1D"}
<AreaSeries
reactive={true}
data={oneDayPrice?.map(({ time, close }) => ({
time,
value: close,
}))}
lineWidth={1.5}
priceScaleId="right"
lineColor={colorChange}
topColor={topColorChange}
bottomColor={bottomColorChange}
priceLineVisible={false}
>
<PriceLine
price={oneDayPrice?.at(0)?.close}
lineWidth={1}
color="#fff"
/>
</AreaSeries>
{:else if displayData === "1W"}
<AreaSeries
data={oneWeekPrice?.map(({ time, close }) => ({
time,
value: close,
}))}
lineWidth={1.5}
priceScaleId="right"
lineColor={colorChange}
topColor={topColorChange}
bottomColor={bottomColorChange}
priceLineVisible={false}
>
<PriceLine
price={oneWeekPrice?.at(0)?.close}
lineWidth={1}
color="#fff"
/>
</AreaSeries>
{:else if displayData === "1M"}
<AreaSeries
data={oneMonthPrice?.map(({ time, close }) => ({
time: time,
value: close,
}))}
lineWidth={1.5}
priceScaleId="right"
lineColor={colorChange}
topColor={topColorChange}
bottomColor={bottomColorChange}
priceLineVisible={false}
>
<PriceLine
price={oneMonthPrice?.at(0)?.close}
lineWidth={1}
color="#fff"
/>
</AreaSeries>
{:else if displayData === "6M"}
<AreaSeries
data={sixMonthPrice?.map(({ time, close }) => ({
time,
value: close,
}))}
lineWidth={1.5}
priceScaleId="right"
lineColor={colorChange}
topColor={topColorChange}
bottomColor={bottomColorChange}
priceLineVisible={false}
>
<PriceLine
price={sixMonthPrice?.at(0)?.close}
lineWidth={1}
color="#fff"
/>
</AreaSeries>
{:else if displayData === "1Y"}
<AreaSeries
data={oneYearPrice?.map(({ time, close }) => ({
time,
value: close,
}))}
lineWidth={1.5}
priceScaleId="right"
lineColor={colorChange}
topColor={topColorChange}
bottomColor={bottomColorChange}
priceLineVisible={false}
>
<PriceLine
price={oneYearPrice?.at(0)?.close}
lineWidth={1}
color="#fff"
/>
</AreaSeries>
{:else if displayData === "MAX"}
<AreaSeries
data={maxPrice?.map(({ time, close }) => ({
time,
value: close,
}))}
lineWidth={1.5}
priceScaleId="right"
lineColor={colorChange}
topColor={topColorChange}
bottomColor={bottomColorChange}
priceLineVisible={false}
>
<PriceLine
price={maxPrice?.at(0)?.close}
lineWidth={1}
color="#fff"
/>
</AreaSeries>
{/if}
</Chart>
{:else}
<div
class="flex justify-center w-full sm:w-[650px] h-[350px] items-center"
>
<div class="relative">
<label
class="bg-[#09090B] rounded-xl h-14 w-14 flex justify-center items-center absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
>
<span
class="loading loading-spinner loading-md text-gray-400"
></span>
</label>
</div>
</div>
{/if}
</div>
{/if}
<div
class="mt-10 lg:mt-0 order-5 lg:order-1 flex flex-row space-x-2 sm:space-x-3 xs:space-x-4"
>
<table
class="w-[50%] text-sm text-white sm:text-[1rem] lg:w-full lg:min-w-[210px]"
>
<tbody
><tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Bid</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{$wsBidPrice !== 0 && $wsBidPrice !== null
? $wsBidPrice
: ((data?.getStockQuote?.bid !== 0
? data?.getStockQuote?.bid
: "-") ?? "n/a")}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Market Cap</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{abbreviateNumber(data?.getStockQuote?.marketCap)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Revenue (ttm)</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.revenueTTM !== null &&
stockDeck?.revenueTTM !== 0
? abbreviateNumber(stockDeck?.revenueTTM)
: "n/a"}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Net Income (ttm)</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.netIncomeTTM !== null
? abbreviateNumber(stockDeck?.netIncomeTTM)
: "n/a"}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>EPS (ttm)</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.eps}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>PE Ratio (ttm)</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.pe}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Forward PE</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.forwardPE ?? "n/a"}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Shares Out
</td>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.sharesOutstanding !== null
? abbreviateNumber(
data?.getStockQuote?.sharesOutstanding,
)
: "n/a"}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Short % of Shares Out</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.shortOutStandingPercent !== null
? stockDeck?.shortOutStandingPercent + "%"
: "n/a"}</td
></tr
>
</tbody>
</table>
<table
class="w-[50%] text-sm text-white lg:w-auto lg:min-w-[210px]"
>
<tbody
><tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Ask</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{$wsAskPrice !== 0 && $wsAskPrice !== null
? $wsAskPrice
: ((data?.getStockQuote?.ask !== 0
? data?.getStockQuote?.ask
: "-") ?? "n/a")}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Volume</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.volume?.toLocaleString(
"en-us",
)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Open</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.open?.toFixed(2)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Previous Close</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.previousClose?.toFixed(2)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Day's Range</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.dayLow?.toFixed(2)} - {data?.getStockQuote?.dayHigh?.toFixed(
2,
)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>52-Week Range</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{data?.getStockQuote?.yearLow?.toFixed(2)} - {data?.getStockQuote?.yearHigh?.toFixed(
2,
)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Beta</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.beta?.toFixed(2)}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Shares Float
</td>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.floatShares !== null
? abbreviateNumber(stockDeck?.floatShares)
: "n/a"}</td
></tr
>
<tr
class="flex flex-col border-b border-gray-600 py-1 sm:table-row sm:py-0"
><td
class="whitespace-nowrap px-0.5 py-[1px] xs:px-1 sm:py-2 text-[1rem]"
>Short % of Float</td
>
<td
class="whitespace-nowrap px-0.5 py-[1px] text-left text-sm font-semibold xs:px-1 sm:py-2 sm:text-right sm:text-[1rem]"
>{stockDeck?.shortFloatPercent !== null
? stockDeck?.shortFloatPercent + "%"
: "n/a"}</td
></tr
>
</tbody>
</table>
</div>
</div>
<!--End Graph-->
<div
class="mt-6 flex flex-col lg:flex-row gap-x-14 items-start w-full"
>
<div
class="lg:space-y-6 lg:order-2 lg:pt-1 sm:pl-7 lg:pl-0 w-full lg:w-[45%] sm:ml-auto"
>
<Sidecard {data} />
<div class="lg:sticky lg:top-20"></div>
</div>
<div class="w-full">
<div
class="w-full mt-10 sm:mt-0 m-auto sm:pl-6 sm:pb-6 {Object?.keys(
data?.getEarningsSurprise || {},
)?.length !== 0
? ''
: 'hidden'}"
>
<EarningsSurprise {data} />
</div>
<div
class="w-full mt-10 sm:mt-0 m-auto sm:pl-6 sm:pb-6 {Object?.keys(
data?.getNextEarnings || {},
)?.length !== 0
? ''
: 'hidden'}"
>
<NextEarnings {data} />
</div>
<div
class="w-full mt-10 sm:mt-0 m-auto sm:pl-6 sm:pb-6 {Object?.keys(
data?.getDividendAnnouncement || {},
)?.length !== 0
? ''
: 'hidden'}"
>
<DividendAnnouncement {data} />
</div>
<div
class="w-full mt-10 sm:mt-0 m-auto sm:pl-6 sm:pb-6 {Object?.keys(
data?.getBullBearSay || {},
)?.length !== 0
? ''
: 'hidden'}"
>
<BullBearSay {data} />
</div>
<div
class="w-full mt-10 sm:mt-0 m-auto sm:pl-6 sm:pb-6 {data
?.getWhyPriceMoved?.length !== 0
? ''
: 'hidden'}"
>
<WIIM {data} />
</div>
<div class="w-full mt-10 sm:mt-0 m-auto sm:pl-6 sm:pb-6 sm:pt-6">
<News {data} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!--End-Indicator-Modal-->
<style lang="scss">
canvas {
width: 100%;
height: 100%;
max-width: 800px;
max-height: 450px;
}
.pulse {
position: relative;
animation: pulse-animation 1s forwards cubic-bezier(0.5, 0, 0.5, 1);
}
@keyframes pulse-animation {
0% {
transform: scale(0.9);
opacity: 1;
}
100% {
transform: scale(0.9);
opacity: 0;
}
}
:root {
--date-picker-background: #09090b;
--date-picker-foreground: #f7f7f7;
}
</style>