frontend/src/routes/options-flow/+page.svelte
2025-02-10 12:24:26 +01:00

1865 lines
70 KiB
Svelte

<script lang="ts">
import notifySound from "$lib/audio/options-flow-reader.mp3";
import { getCache, setCache, isOpen } from "$lib/store";
import { cn } from "$lib/utils";
import { onMount, onDestroy } from "svelte";
import toast from "svelte-french-toast";
import { DateFormatter, type DateValue } from "@internationalized/date";
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
import * as Popover from "$lib/components/shadcn/popover/index.js";
import { Button } from "$lib/components/shadcn/button/index.js";
import { Calendar } from "$lib/components/shadcn/calendar/index.js";
import CalendarIcon from "lucide-svelte/icons/calendar";
import UpgradeToPro from "$lib/components/UpgradeToPro.svelte";
import SEO from "$lib/components/SEO.svelte";
import { page } from "$app/stores";
import OptionsFlowTable from "$lib/components/Table/OptionsFlowTable.svelte";
import { writable } from "svelte/store";
export let data;
let shouldLoadWorker = writable(false);
let optionsWatchlist = data?.getOptionsWatchlist;
let ruleOfList = data?.getPredefinedCookieRuleOfList || [];
let displayRules = [];
let filteredData = [];
let filterQuery = $page.url.searchParams.get("query") || "";
let socket: WebSocket | null = null; // Initialize socket as null
let syncWorker: Worker | undefined;
let ruleName = "";
let searchTerm = "";
let showFilters = true;
let filteredRows = [];
const df = new DateFormatter("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
});
let selectedDate: DateValue | undefined = undefined;
let infoText = {};
let tooltipTitle;
const allRules = {
size: {
label: "Size",
step: ["50K", "20K", "10K", "5K", "2K", "1K", "100", "0"],
defaultCondition: "over",
defaultValue: "any",
},
volume: {
label: "Volume",
step: ["100K", "50K", "20K", "10K", "5K", "2K", "1K", "100", "0"],
defaultCondition: "over",
defaultValue: "any",
},
open_interest: {
label: "Open Interest",
step: ["100K", "10K", "1K"],
defaultCondition: "over",
defaultValue: "any",
},
volumeOIRatio: {
label: "Volume / Open Interest",
step: ["100%", "80%", "60%", "50%", "30%", "15%", "10%", "5%"],
defaultCondition: "over",
defaultValue: "any",
},
sizeOIRatio: {
label: "Size / Open Interest",
step: ["100%", "80%", "60%", "50%", "30%", "15%", "10%", "5%"],
defaultCondition: "over",
defaultValue: "any",
},
cost_basis: {
label: "Premium",
step: [
"10M",
"5M",
"1M",
"500K",
"400K",
"300K",
"200K",
"100K",
"50K",
"10K",
"5K",
],
defaultCondition: "over",
defaultValue: "any",
},
moneyness: {
label: "Moneyness",
step: ["ITM", "OTM"],
defaultValue: "any",
},
flowType: {
label: "Flow Type",
step: ["Repeated Flow"],
defaultValue: "any",
},
put_call: {
label: "Contract Type",
step: ["Calls", "Puts"],
defaultValue: "any",
},
sentiment: {
label: "Sentiment",
step: ["Bullish", "Neutral", "Bearish"],
defaultValue: "any",
},
execution_estimate: {
label: "Execution",
step: ["Above Ask", "Below Bid", "At Ask", "At Bid", "At Midpoint"],
defaultValue: "any",
},
option_activity_type: {
label: "Option Type",
step: ["Sweep", "Trade"],
defaultValue: "any",
},
date_expiration: {
label: "Date Expiration",
step: ["250", "180", "100", "80", "60", "50", "30", "20", "10", "5", "0"],
defaultCondition: "over",
defaultValue: "any",
},
underlying_type: {
label: "Asset Type",
step: ["Stock", "ETF"],
defaultValue: "any",
},
};
const categoricalRules = [
"moneyness",
"flowType",
"put_call",
"sentiment",
"execution_estimate",
"option_activity_type",
"underlying_type",
];
// Generate allRows from allRules
$: allRows = Object?.entries(allRules)
?.sort(([, a], [, b]) => a.label.localeCompare(b.label)) // Sort by label
?.map(([ruleName, ruleProps]) => ({
rule: ruleName,
...ruleProps,
}));
let ruleCondition = {};
let valueMappings = {};
Object.keys(allRules).forEach((ruleName) => {
ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
// Check if the default condition is "between"
if (allRules[ruleName].defaultCondition === "between") {
valueMappings[ruleName] = allRules[ruleName].defaultValue || [null, null];
} else {
valueMappings[ruleName] = allRules[ruleName].defaultValue;
}
});
// Update ruleCondition and valueMappings based on existing rules
ruleOfList.forEach((rule) => {
ruleCondition[rule.name] =
rule.condition || allRules[rule.name].defaultCondition;
valueMappings[rule.name] = rule.value || allRules[rule.name].defaultValue;
});
async function handleDeleteRule(state) {
for (let i = 0; i < ruleOfList.length; i++) {
if (ruleOfList[i].name === state) {
ruleOfList.splice(i, 1); // Remove the element at index i from the ruleOfList
ruleOfList = [...ruleOfList];
break; // Exit the loop after deleting the element
}
}
if (ruleOfList?.length === 0) {
ruleName = "";
} else if (state === ruleName) {
ruleName = "";
}
displayRules = allRows?.filter((row) =>
ruleOfList?.some((rule) => rule.name === row.rule),
);
shouldLoadWorker.set(true);
await saveCookieRuleOfList();
}
async function handleResetAll() {
ruleOfList = [];
filteredData = [];
ruleOfList = [...ruleOfList];
ruleName = "";
filterQuery = "";
checkedItems = new Set();
Object?.keys(allRules).forEach((ruleName) => {
ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
valueMappings[ruleName] = allRules[ruleName].defaultValue;
});
displayRules = allRows?.filter((row) =>
ruleOfList.some((rule) => rule.name === row.rule),
);
displayedData = rawData;
await saveCookieRuleOfList();
}
function changeRule(state: string) {
searchTerm = "";
ruleName = state;
handleAddRule();
//const closePopup = document.getElementById("ruleModal");
//closePopup?.dispatchEvent(new MouseEvent('click'))
}
function handleAddRule() {
if (ruleName === "") {
toast.error("Please select a rule", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
return;
}
let newRule;
switch (ruleName) {
case "moneyness":
case "flowType":
case "put_call":
case "sentiment":
case "execution_estimate":
case "option_activity_type":
case "underlying_type":
newRule = {
name: ruleName,
value: Array.isArray(valueMappings[ruleName])
? valueMappings[ruleName]
: [valueMappings[ruleName]],
}; // Ensure value is an array
break;
default:
newRule = {
name: ruleName,
condition: ruleCondition[ruleName],
value: valueMappings[ruleName],
};
break;
}
handleRule(newRule);
}
async function handleRule(newRule) {
const existingRuleIndex = ruleOfList.findIndex(
(rule) => rule.name === newRule.name,
);
if (existingRuleIndex !== -1) {
const existingRule = ruleOfList[existingRuleIndex];
if (existingRule.name === newRule.name) {
// Remove the rule instead of showing an error
ruleOfList.splice(existingRuleIndex, 1);
ruleOfList = [...ruleOfList]; // Trigger reactivity
Object.keys(allRules).forEach((ruleName) => {
ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
valueMappings[ruleName] = allRules[ruleName].defaultValue;
});
displayRules = allRows?.filter((row) =>
ruleOfList.some((rule) => rule.name === row.rule),
);
} else {
ruleOfList[existingRuleIndex] = newRule;
ruleOfList = [...ruleOfList]; // Trigger reactivity
}
} else {
ruleOfList = [...ruleOfList, newRule];
/*
toast.success('Rule added', {
style: 'border-radius: 200px; background: #2A2E39; color: #fff;'
});
*/
shouldLoadWorker.set(true);
}
}
const loadWorker = async () => {
syncWorker.postMessage({ rawData, ruleOfList, filterQuery });
};
const handleMessage = (event) => {
displayRules = allRows?.filter((row) =>
ruleOfList.some((rule) => rule.name === row.rule),
);
filteredData = event.data?.filteredData ?? [];
displayedData = filteredData;
console.log("handle Message");
calculateStats(displayedData);
//console.log(displayedData)
};
async function changeRuleCondition(name: string, state: string) {
ruleName = name;
if (
ruleCondition[ruleName] === "between" &&
["over", "under"]?.includes(state?.toLowerCase())
) {
valueMappings[ruleName] = "";
}
ruleCondition[ruleName] = state?.toLowerCase();
await handleChangeValue(valueMappings[ruleName]);
}
let checkedItems = new Set(ruleOfList.flatMap((rule) => rule.value));
function isChecked(item) {
return checkedItems.has(item);
}
async function getInfoText(parameter, title) {
tooltipTitle = title;
const cachedData = getCache(parameter, "getInfoText");
if (cachedData) {
infoText = cachedData;
} else {
const postData = { parameter };
const response = await fetch("/api/info-text", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
infoText = await response.json();
setCache(parameter, infoText, "getInfoText");
}
}
function parseValue(val) {
if (typeof val === "string") {
// Handle percentage values
if (val.endsWith("%")) {
return parseFloat(val);
}
// Handle values with suffixes like K (thousand), M (million), B (billion)
const suffixMap = {
K: 1e3,
M: 1e6,
B: 1e9,
};
const suffix = val.slice(-1).toUpperCase();
const numberPart = parseFloat(val);
if (suffix in suffixMap) {
return numberPart * suffixMap[suffix];
}
}
return parseFloat(val);
}
// Custom sorting function
function customSort(a, b) {
return parseValue(a) - parseValue(b);
}
async function handleChangeValue(value, { shouldSort = true } = {}) {
// Toggle checkedItems logic
if (checkedItems.has(value)) {
checkedItems.delete(value);
} else {
checkedItems.add(value);
}
// Specific rule handling for options-related rules
if (categoricalRules?.includes(ruleName)) {
// Ensure valueMappings[ruleName] is initialized as an array
if (!Array.isArray(valueMappings[ruleName])) {
valueMappings[ruleName] = [];
}
// Similar logic to the original function for adding/removing values
const index = valueMappings[ruleName].indexOf(value);
if (index === -1) {
valueMappings[ruleName].push(value);
// Sort the array when a new value is added, respecting shouldSort parameter
if (shouldSort) {
valueMappings[ruleName] = valueMappings[ruleName].sort(customSort);
}
} else {
valueMappings[ruleName].splice(index, 1);
}
// Set to "any" if no values are selected
if (valueMappings[ruleName].length === 0) {
valueMappings[ruleName] = "any";
}
} else if (ruleName in valueMappings) {
// For rules that require sorting (like range or numeric values)
if (ruleCondition[ruleName] === "between" && Array.isArray(value)) {
// Sort the array for between conditions, respecting shouldSort parameter
valueMappings[ruleName] = shouldSort ? value.sort(customSort) : value;
} else {
// Handle non-specific rules as single values
valueMappings[ruleName] = value;
}
} else {
console.warn(`Unhandled rule: ${ruleName}`);
}
// Update ruleOfList (if applicable)
const ruleToUpdate = ruleOfList?.find((rule) => rule.name === ruleName);
if (ruleToUpdate) {
ruleToUpdate.value = valueMappings[ruleToUpdate.name];
ruleToUpdate.condition = ruleCondition[ruleToUpdate.name];
ruleOfList = [...ruleOfList];
}
// Trigger worker load and save cookie
shouldLoadWorker.set(true);
await saveCookieRuleOfList();
}
async function stepSizeValue(value, condition) {
const match = value.toString().match(/^(-?[\d.]+)([KMB%]?)$/);
if (!match) return value;
let [_, number, suffix] = match;
number = parseFloat(number);
let step = 1;
number += condition === "add" ? step : -step;
// Round to 2 decimal places for consistency
number = parseFloat(number?.toFixed(2));
const newValue = suffix ? `${number}${suffix}` : Math?.round(number);
await handleChangeValue(newValue);
}
async function handleValueInput(event, ruleName, index = null) {
const newValue = event.target.value;
if (ruleCondition[ruleName] === "between") {
const currentValues = [...(valueMappings[ruleName] || ["", ""])];
currentValues[index] = newValue;
await handleChangeValue(currentValues, { shouldSort: false });
} else {
await handleChangeValue(newValue);
}
}
const currentTime = new Date(
new Date().toLocaleString("en-US", { timeZone: "America/New_York" }),
)?.getTime();
const nyseDate = new Date(
data?.getOptionsFlowFeed?.at(0)?.date ?? null,
)?.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "Europe/Berlin",
});
let rawData = data?.getOptionsFlowFeed?.filter((item) =>
Object?.values(item)?.every(
(value) =>
value !== null &&
value !== undefined &&
(typeof value !== "object" ||
Object?.values(value)?.every(
(subValue) => subValue !== null && subValue !== undefined,
)),
),
);
rawData?.forEach((item) => {
item.dte = daysLeft(item?.date_expiration);
});
let displayedData = [];
let flowSentiment;
let putCallRatio;
let displayCallVolume;
let displayPutVolume;
let callPercentage;
let putPercentage;
let audio;
let muted = false;
let newData = [];
let previousVolume = 0; //This is needed to play the sound only if it changes.
let notFound = false;
let isLoaded = false;
$: mode = $isOpen === true ? true : false;
function toggleMode() {
if ($isOpen) {
mode = !mode;
if (mode === true && selectedDate !== undefined) {
selectedDate = undefined;
rawData = data?.getOptionsFlowFeed;
displayedData = [...rawData];
shouldLoadWorker.set(true);
}
} else {
toast.error(`Market is closed`, {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
}
}
async function websocketRealtimeData() {
if (data?.user?.tier === "Pro") {
newData = [];
try {
socket = new WebSocket(data?.wsURL + "/options-flow-reader");
socket.addEventListener("message", (event) => {
const totalVolume = displayCallVolume + displayPutVolume;
if (mode === true) {
try {
newData = JSON?.parse(event.data) ?? [];
if (newData?.length > 0) {
newData?.forEach((item) => {
item.dte = daysLeft(item?.date_expiration);
});
//calculateStats(newData);
//console.log(previousVolume);
if (
newData?.length > rawData?.length &&
previousVolume !== totalVolume
) {
//console.log(previousVolume,totalVolume,);
rawData = newData;
shouldLoadWorker.set(true);
//console.log('loading worker')
//displayedData = rawData;
if (!muted) {
audio?.play();
}
}
}
/*
if (previousCallVolume !== displayCallVolume && !muted && audio) {
audio?.play();
}
*/
} catch (e) {
console.error("Error processing WebSocket message:", e);
}
newData = [];
previousVolume = totalVolume;
}
});
socket.addEventListener("close", (event) => {
console.log("WebSocket connection closed:", event.reason);
// Explicitly nullify the socket and remove all event listeners
if (socket) {
socket.onmessage = null;
socket.onopen = null;
socket.onclose = null;
socket.onerror = null;
socket = null;
}
});
socket.addEventListener("error", (error) => {
console.error("WebSocket error:", error);
// Handle WebSocket errors here
});
} catch (error) {
console.error("WebSocket connection error:", error);
// Handle connection errors here
setTimeout(() => websocketRealtimeData(), 400);
}
}
}
function daysLeft(targetDate) {
const targetTime = new Date(targetDate).getTime();
const difference = targetTime - currentTime;
const millisecondsPerDay = 1000 * 60 * 60 * 24;
const daysLeft = Math?.ceil(difference / millisecondsPerDay);
return daysLeft;
}
async function saveCookieRuleOfList() {
const postData = {
ruleOfList: ruleOfList,
};
const response = await fetch("/api/options-flow-filter-cookie", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
}); // make a POST request to the server with the FormData object
}
$: if ($isOpen) {
websocketRealtimeData();
}
onMount(async () => {
if (filterQuery?.length > 0) {
shouldLoadWorker.set(true);
}
if (ruleOfList?.length !== 0) {
shouldLoadWorker.set(true);
console.log("initial filter");
}
displayRules = allRows?.filter((row) =>
ruleOfList?.some((rule) => rule?.name === row?.rule),
);
audio = new Audio(notifySound);
displayedData = rawData;
calculateStats(rawData);
if (!syncWorker) {
const SyncWorker = await import("./workers/filterWorker?worker");
syncWorker = new SyncWorker.default();
syncWorker.onmessage = handleMessage;
}
shouldLoadWorker.subscribe(async (value) => {
if (value) {
isLoaded = false;
await loadWorker();
shouldLoadWorker.set(false); // Reset after worker is loaded
isLoaded = true;
}
});
isLoaded = true;
});
onDestroy(async () => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close();
console.log("WebSocket connection closed safely.");
}
if (audio) {
audio?.pause();
audio = null;
}
});
function calculateStats(data) {
const {
callVolumeSum,
putVolumeSum,
bullishCount,
bearishCount,
neutralCount,
} = data?.reduce(
(acc, item) => {
const volume = parseInt(item?.volume);
if (item?.put_call === "Calls") {
acc.callVolumeSum += volume;
} else if (item?.put_call === "Puts") {
acc.putVolumeSum += volume;
}
if (item?.sentiment === "Bullish") {
acc.bullishCount += 1;
} else if (item?.sentiment === "Bearish") {
acc.bearishCount += 1;
} else if (item?.sentiment === "Neutral") {
acc.neutralCount += 1;
}
return acc;
},
{
callVolumeSum: 0,
putVolumeSum: 0,
bullishCount: 0,
bearishCount: 0,
neutralCount: 0,
},
);
if (bullishCount > bearishCount) {
flowSentiment = "Bullish";
} else if (bullishCount < bearishCount) {
flowSentiment = "Bearish";
} else if (neutralCount > bearishCount && neutralCount > bullishCount) {
flowSentiment = "Neutral";
} else {
flowSentiment = "-";
}
putCallRatio = callVolumeSum !== 0 ? putVolumeSum / callVolumeSum : 0;
callPercentage =
callVolumeSum + putVolumeSum !== 0
? Math.floor((callVolumeSum / (callVolumeSum + putVolumeSum)) * 100)
: 0;
putPercentage =
callVolumeSum + putVolumeSum !== 0 ? 100 - callPercentage : 0;
displayCallVolume = callVolumeSum;
displayPutVolume = putVolumeSum;
}
const getHistoricalFlow = async () => {
// Create a delay using setTimeout wrapped in a Promise
if (data?.user?.tier === "Pro") {
mode = false;
isLoaded = false;
displayRules = allRows?.filter((row) =>
ruleOfList.some((rule) => rule.name === row.rule),
);
displayedData = [];
calculateStats(displayedData);
await new Promise((resolve) => setTimeout(resolve, 300));
// Make the POST request after the delay
const convertDate = new Date(df.format(selectedDate?.toDate()));
const year = convertDate?.getFullYear();
const month = String(convertDate?.getMonth() + 1).padStart(2, "0");
const day = String(convertDate?.getDate()).padStart(2, "0");
const postData = { selectedDate: `${year}-${month}-${day}` };
try {
const response = await fetch("/api/options-historical-flow", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
rawData = await response?.json();
if (rawData?.length !== 0) {
rawData?.forEach((item) => {
item.dte = daysLeft(item?.date_expiration);
});
shouldLoadWorker.set(true);
}
} catch (error) {
console.error("Error fetching historical flow:", error);
rawData = [];
} finally {
isLoaded = true;
}
} else {
toast.error("Only for Pro Members", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
}
};
function handleInput(event) {
filterQuery = event.target.value;
setTimeout(() => {
shouldLoadWorker.set(true);
}, 0);
}
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const debouncedHandleInput = debounce(handleInput, 300);
$: {
if (searchTerm) {
filteredRows = allRows?.filter((row) =>
row?.label?.toLowerCase()?.includes(searchTerm?.toLowerCase()),
);
}
}
$: {
if (ruleOfList) {
const ruleToUpdate = ruleOfList?.find((rule) => rule?.name === ruleName);
if (ruleToUpdate) {
ruleToUpdate.value = valueMappings[ruleToUpdate.name];
ruleToUpdate.condition = ruleCondition[ruleToUpdate.name];
ruleOfList = [...ruleOfList];
//shouldLoadWorker.set(true);
}
}
}
</script>
<SEO
title="Options Flow Feed"
description="Explore unusual options from big institutional traders and hedge funds."
/>
<body class="overflow-y-auto">
<section
class="w-full max-w-screen sm:max-w-7xl sm:max-w-[1400px] flex justify-center items-center bg-default pb-20 10 p-3 sm:p-0"
>
<div class="w-full m-auto min-h-screen">
<!--
<div class="text-sm sm:text-[1rem] breadcrumbs mb-5">
<ul>
<li><a href="/" class="text-gray-300">Home</a></li>
<li class="text-gray-300">Options Flow</li>
</ul>
</div>
-->
{#if !$isOpen}
<div
class="text-white text-sm sm:text-[1rem] italic text-center sm:text-start w-full ml-2 mb-3"
>
Live flow of {data?.user?.tier === "Pro" && selectedDate
? df.format(selectedDate?.toDate())
: nyseDate} (NYSE Time)
</div>
{/if}
<div class="rounded-md border border-gray-700 bg-primary p-2">
<div
class="flex flex-col sm:flex-row items-center pt-3 sm:pt-1 pb-3 sm:border-b sm:border-gray-600"
>
<div
class="flex flex-row items-center justify-center sm:justify-start"
>
<label
data-tip="Audio Preference"
on:click={() => (muted = !muted)}
class="xl:tooltip xl:tooltip-bottom flex flex-col items-center mr-3 cursor-pointer"
>
<div
class="rounded-full w-10 h-10 relative bg-[#000] flex items-center justify-center"
>
{#if !muted}
<svg
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
><path
fill="#fff"
d="M9 2.5a.5.5 0 0 0-.849-.358l-2.927 2.85H3.5a1.5 1.5 0 0 0-1.5 1.5v2.99a1.5 1.5 0 0 0 1.5 1.5h1.723l2.927 2.875A.5.5 0 0 0 9 13.5zm1.111 2.689a.5.5 0 0 1 .703-.08l.002.001l.002.002l.005.004l.015.013l.046.04c.036.034.085.08.142.142c.113.123.26.302.405.54c.291.48.573 1.193.573 2.148c0 .954-.282 1.668-.573 2.148a3.394 3.394 0 0 1-.405.541a2.495 2.495 0 0 1-.202.196l-.008.007h-.001s-.447.243-.703-.078a.5.5 0 0 1 .075-.7l.002-.002l-.001.001l.002-.001h-.001l.018-.016c.018-.017.048-.045.085-.085a2.4 2.4 0 0 0 .284-.382c.21-.345.428-.882.428-1.63c0-.747-.218-1.283-.428-1.627a2.382 2.382 0 0 0-.368-.465a.5.5 0 0 1-.096-.717m1.702-2.08a.5.5 0 1 0-.623.782l.011.01l.052.045c.047.042.116.107.201.195c.17.177.4.443.63.794c.46.701.92 1.733.92 3.069a5.522 5.522 0 0 1-.92 3.065c-.23.35-.46.614-.63.79a3.922 3.922 0 0 1-.252.24l-.011.01h-.001a.5.5 0 0 0 .623.782l.033-.027l.075-.065c.063-.057.15-.138.253-.245a6.44 6.44 0 0 0 .746-.936a6.522 6.522 0 0 0 1.083-3.614a6.542 6.542 0 0 0-1.083-3.618a6.517 6.517 0 0 0-.745-.938a4.935 4.935 0 0 0-.328-.311l-.023-.019l-.007-.006l-.002-.002zM10.19 5.89l-.002-.001Z"
/></svg
>
{:else}
<svg
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="#fff"
d="M3.28 2.22a.75.75 0 1 0-1.06 1.06L6.438 7.5H4.25A2.25 2.25 0 0 0 2 9.749v4.497a2.25 2.25 0 0 0 2.25 2.25h3.68a.75.75 0 0 1 .498.19l4.491 3.994c.806.716 2.081.144 2.081-.934V16.06l5.72 5.72a.75.75 0 0 0 1.06-1.061zm13.861 11.74l1.138 1.137A6.974 6.974 0 0 0 19 12a6.973 6.973 0 0 0-.84-3.328a.75.75 0 0 0-1.32.714c.42.777.66 1.666.66 2.614c0 .691-.127 1.351-.359 1.96m2.247 2.246l1.093 1.094A9.956 9.956 0 0 0 22 12a9.959 9.959 0 0 0-1.96-5.946a.75.75 0 0 0-1.205.892A8.459 8.459 0 0 1 20.5 12a8.458 8.458 0 0 1-1.112 4.206M9.52 6.338l5.48 5.48V4.25c0-1.079-1.274-1.65-2.08-.934z"
/></svg
>
{/if}
</div>
</label>
<span
class="text-xs sm:text-sm sm:text-lg {!mode
? 'text-white'
: 'text-gray-400'} mr-3"
>
{$isOpen ? "Paused" : "Market Closed"}
</span>
<label
class="inline-flex items-center cursor-pointer focus-none focus:outline-none"
>
<input
on:click={toggleMode}
type="checkbox"
checked={mode}
value={mode}
disabled={!$isOpen}
class="sr-only peer"
/>
<div
class="relative w-11 h-6 bg-gray-600 focus:outline-none peer-focus:outline-none peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#00A96E]"
></div>
</label>
<div class="ml-3 flex flex-col items-start">
<span
class="text-xs sm:text-sm sm:text-lg {mode
? 'text-white'
: 'text-gray-400'}"
>
Live Flow
</span>
</div>
</div>
<div class="sm:ml-auto w-full sm:w-fit pt-5">
<div class="relative flex flex-col sm:flex-row items-center">
<div
class="relative w-full sm:w-fit pl-3 sm:mr-5 mb-4 sm:mb-0 flex-auto text-center bg-secondary rounded-md border border-gray-600"
>
<label class="flex flex-row items-center">
<input
id="modal-search"
type="search"
class="text-white sm:ml-2 text-[1rem] placeholder-gray-300 border-transparent focus:border-transparent focus:ring-0 flex items-center justify-center w-full px-0 py-1.5 bg-[inherit]"
placeholder="Stock or ETF symbol..."
bind:value={filterQuery}
on:input={debouncedHandleInput}
autocomplete="off"
/>
{#if filterQuery?.length > 0}
<label
class="cursor-pointer"
on:click={() => {
filterQuery = "";
shouldLoadWorker.set(true);
}}
>
<svg
class="ml-auto h-6 w-6 inline-block mr-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="white"
d="m6.4 18.308l-.708-.708l5.6-5.6l-5.6-5.6l.708-.708l5.6 5.6l5.6-5.6l.708.708l-5.6 5.6l5.6 5.6l-.708.708l-5.6-5.6z"
/></svg
>
</label>
{:else}
<svg
class="ml-auto h-7 w-7 sm:h-8 sm:w-8 inline-block mr-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="#fff"
d="m19.485 20.154l-6.262-6.262q-.75.639-1.725.989t-1.96.35q-2.402 0-4.066-1.663T3.808 9.503T5.47 5.436t4.064-1.667t4.068 1.664T15.268 9.5q0 1.042-.369 2.017t-.97 1.668l6.262 6.261zM9.539 14.23q1.99 0 3.36-1.37t1.37-3.361t-1.37-3.36t-3.36-1.37t-3.361 1.37t-1.37 3.36t1.37 3.36t3.36 1.37"
/></svg
>
{/if}
</label>
{#if notFound === true}
<span
class="absolute left-1 -bottom-6 label-text text-error text-[0.65rem] mt-2"
>
No Results Found
</span>
{/if}
</div>
<Popover.Root>
<Popover.Trigger asChild let:builder>
<Button
class={cn(
"w-full sm:w-[160px] truncate sm:mr-3 py-3 bg-[#000] sm:hover:bg-[#000] sm:hover:text-white text-white justify-center sm:justify-start text-center sm:text-left font-normal border-none rounded-md",
!selectedDate && "text-gray-300",
)}
builders={[builder]}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{selectedDate
? df.format(selectedDate?.toDate())
: "Pick a date"}
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0 border-gray-500">
<Calendar
class="bg-default text-white"
bind:value={selectedDate}
initialFocus
onValueChange={getHistoricalFlow}
/>
</Popover.Content>
</Popover.Root>
</div>
</div>
</div>
<div
class="mr-1 flex items-center justify-between lg:mr-2 pb-1.5 border-b border-gray-600 mt-1.5"
>
<button
on:click={() => (showFilters = !showFilters)}
class="flex cursor-pointer items-center text-lg sm:text-xl font-semibold text-white"
title="Hide Filter Area"
>
<svg
class="-mb-0.5 h-6 w-6 {showFilters ? '' : '-rotate-90'} "
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
>
<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>
{ruleOfList?.length} Filters
</button>
</div>
{#if showFilters}
<div
class="sm:mt-3 flex flex-col gap-y-2.5 sm:flex-row lg:gap-y-2 pb-1"
>
<label
for="ruleModal"
class="inline-flex cursor-pointer items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:bg-default/60 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller"
>
<svg
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
></path>
</svg>
<div>Add Filters</div>
</label>
{#if ruleOfList?.length !== 0}
<label
on:click={handleResetAll}
class="sm:ml-3 cursor-pointer inline-flex items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:text-red-500 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller"
>
<svg
class="h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 21 21"
><g
fill="none"
fill-rule="evenodd"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M3.578 6.487A8 8 0 1 1 2.5 10.5" /><path
d="M7.5 6.5h-4v-4"
/></g
></svg
>
<div>Reset All</div>
</label>
{/if}
</div>
<div
class="sm:grid sm:gap-x-2.5 md:grid-cols-2 lg:grid-cols-3 w-full mt-3 border-t border-b border-gray-600"
>
{#each displayRules as row (row?.rule)}
<!--Start Added Rules-->
<div
class="flex items-center justify-between space-x-2 px-1 py-1.5 text-smaller leading-tight text-white"
>
<div class="hide-scroll text-white">
{row?.label?.length > 20
? row?.label?.slice(0, 20)?.replace("[%]", "") + "..."
: row?.label?.replace("[%]", "")}
<span class="relative" role="tooltip"
><label
for="mobileTooltip"
on:click={() =>
getInfoText(row?.rule, row?.label?.replace("[%]", ""))}
class="relative"
role="tooltip"
>
<span
class="absolute -right-[15px] -top-[3px] cursor-pointer p-1 text-gray-300 sm:hover:text-white"
>
<svg
class="h-[10.5px] w-[10.5px]"
viewBox="0 0 4 16"
fill="currentColor"
style="max-width:20px"
><path
d="M0 6h4v10h-4v-10zm2-6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"
></path></svg
>
</span>
</label></span
>
</div>
<div class="flex items-center">
<button
on:click={() => handleDeleteRule(row?.rule)}
class="mr-1.5 cursor-pointer text-gray-300 sm:hover:text-red-500 focus:outline-none"
title="Remove filter"
>
<svg
class="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="CurrentColor"
style="max-width:40px"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</button>
<div class="relative inline-block text-left">
<div on:click={() => (ruleName = row?.rule)}>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="bg-[#000] h-[40px] flex flex-row justify-between items-center w-[150px] xs:w-[140px] sm:w-[150px] px-3 text-white rounded-md truncate"
>
<span class="truncate ml-2 text-sm sm:text-[1rem]">
{#if valueMappings[row?.rule] === "any"}
Any
{:else if ruleCondition[row?.rule] === "between"}
{Array.isArray(valueMappings[row?.rule])
? `${valueMappings[row?.rule][0]}-${valueMappings[row?.rule][1] ?? "Any"}`
: "Any"}
{:else}
{ruleCondition[row?.rule]
?.replace("under", "Under")
?.replace("over", "Over")
?.replace("exactly", "Exactly") ?? ""}
{valueMappings[row?.rule]}
{/if}
</span>
<svg
class=" ml-1 h-6 w-6 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-64 min-h-auto max-h-72 overflow-y-auto scroller"
>
{#if !categoricalRules?.includes(row?.rule)}
<DropdownMenu.Label
class="absolute mt-2 h-11 border-gray-800 border-b -top-1 z-20 fixed sticky bg-default"
>
<div
class="flex items-center justify-start gap-x-1"
>
<!--Start Dropdown for Condition-->
<div
class="-ml-2 relative inline-block text-left"
>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder
><Button
builders={[builder]}
class="w-fit -mt-1 -ml-2 bg-default flex flex-row justify-between items-center text-white"
>
<span
class="truncate ml-2 text-sm sm:text-[1rem]"
>
{ruleCondition[ruleName]
?.replace("under", "Under")
?.replace("over", "Over")
?.replace("between", "Between")
?.replace("exactly", "Exactly")}
</span>
<svg
class="mt-1 -mr-1 ml-1 h-5 w-5 xs:ml-2 !ml-0 sm:ml-0 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>
<DropdownMenu.Group>
{#each ["Over", "Under", "Between", "Exactly"] as item}
<DropdownMenu.Item
on:click={() =>
changeRuleCondition(
row?.rule,
item,
)}
class="cursor-pointer text-[1rem] font-normal"
>{item}</DropdownMenu.Item
>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{#if ruleCondition[row?.rule] === "between"}
<div class="flex gap-x-1 -ml-2 z-10 -mt-1">
<input
type="text"
placeholder="Min"
value={Array.isArray(
valueMappings[row?.rule],
)
? (valueMappings[row?.rule][0] ?? "")
: ""}
on:input={(e) =>
handleValueInput(e, row?.rule, 0)}
class="ios-zoom-fix block max-w-[3.5rem] rounded-sm placeholder:text-gray-200 font-normal p-1 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-primary"
/>
<span
class="text-white text-[1rem] font-normal mt-1"
>
&
</span>
<input
type="text"
placeholder="Max"
value={Array.isArray(
valueMappings[row?.rule],
)
? (valueMappings[row?.rule][1] ?? "")
: ""}
on:input={(e) =>
handleValueInput(e, row?.rule, 1)}
class="ios-zoom-fix block max-w-[3.5rem] rounded-sm placeholder:text-gray-200 font-normal p-1 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-primary"
/>
</div>
{:else}
<input
type="text"
placeholder="Value"
value={valueMappings[row?.rule] === "any"
? ""
: valueMappings[row?.rule]}
on:input={(e) =>
handleValueInput(e, row?.rule)}
class="ios-zoom-fix block max-w-[4.8rem] rounded-sm placeholder:text-gray-200 font-normal p-1 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-primary"
/>
{/if}
{#if ["over", "under", "exactly"]?.includes(ruleCondition[ruleName]?.toLowerCase())}
<div
class="ml-2 flex touch-manipulation flex-row items-center gap-x-1.5"
>
<button
on:click={() =>
stepSizeValue(
valueMappings[row?.rule],
"add",
)}
><svg
class="size-6 cursor-pointer text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="max-width:40px"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
></button
>
<button
on:click={() =>
stepSizeValue(
valueMappings[row?.rule],
"minus",
)}
><svg
class="size-6 cursor-pointer text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="max-width:40px"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
></button
>
</div>
{/if}
<!--End Dropdown for Condition-->
</div>
</DropdownMenu.Label>
{:else}
<div
class="relative sticky z-40 focus:outline-none -top-1"
tabindex="0"
role="menu"
style=""
></div>
{/if}
<DropdownMenu.Group class="min-h-10 mt-2">
{#if !categoricalRules?.includes(row?.rule)}
{#each row?.step as newValue, index}
{#if ruleCondition[row?.rule] === "between"}
{#if newValue && row?.step[index + 1]}
<DropdownMenu.Item
class="sm:hover:bg-primary"
>
<button
on:click={() => {
handleChangeValue([
row?.step[index],
row?.step[index + 1],
]);
}}
class="block w-full border-b border-gray-600 px-4 py-1.5 text-left text-sm sm:text-[1rem] rounded text-white last:border-0 sm:hover:bg-primary focus:bg-blue-100 focus:text-gray-900 focus:outline-none"
>
{ruleCondition[row?.rule]?.replace(
"between",
"Between",
)}
{row?.step[index + 1]} - {row?.step[
index
]}
</button>
</DropdownMenu.Item>
{/if}
{:else}
<DropdownMenu.Item
class="sm:hover:bg-primary"
>
<button
on:click={() => {
handleChangeValue(newValue);
}}
class="block w-full border-b border-gray-600 px-4 py-1.5 text-left text-sm sm:text-[1rem] rounded text-white last:border-0 sm:hover:bg-primary focus:bg-blue-100 focus:text-gray-900 focus:outline-none"
>
{ruleCondition[row?.rule]
?.replace("under", "Under")
?.replace("over", "Over")
?.replace("exactly", "Exactly")}
{newValue}
</button>
</DropdownMenu.Item>
{/if}
{/each}
{:else if categoricalRules?.includes(row?.rule)}
{#each row?.step as item}
<DropdownMenu.Item
class="sm:hover:bg-[#2A2E39]"
>
<div
class="flex items-center"
on:click|capture={(event) =>
event.preventDefault()}
>
<label
on:click={() => {
handleChangeValue(item);
}}
class="cursor-pointer text-white"
for={item}
>
<input
type="checkbox"
class="rounded"
checked={isChecked(item)}
/>
<span class="ml-2">{item}</span>
</label>
</div>
</DropdownMenu.Item>
{/each}
{/if}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
</div>
<!--End Added Rules-->
{/each}
</div>
{/if}
</div>
{#if isLoaded}
<div class="w-full mt-5 m-auto flex justify-center items-center">
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-y-3 gap-x-3">
<!--Start Flow Sentiment-->
<div
class="flex flex-row items-center flex-wrap w-full px-5 bg-primary border border-gray-600 rounded-md h-20"
>
<div class="flex flex-col items-start">
<span class="font-semibold text-gray-200 text-sm sm:text-[1rem]"
>Flow Sentiment</span
>
<span
class="text-start text-[1rem] font-semibold {flowSentiment ===
'Bullish'
? 'text-[#00FC50]'
: flowSentiment === 'Bearish'
? 'text-[#FF2F1F]'
: flowSentiment === 'Neutral'
? 'text-[#fff]'
: 'text-white'}">{flowSentiment}</span
>
</div>
</div>
<!--End Flow Sentiment-->
<!--Start Put/Call-->
<div
class="flex flex-row items-center flex-wrap w-full px-5 bg-primary border border-gray-600 rounded-md h-20"
>
<div class="flex flex-col items-start">
<span class="font-semibold text-gray-200 text-sm sm:text-[1rem]"
>Put/Call</span
>
<span class="text-start text-[1rem] font-semibold text-white">
{putCallRatio?.toFixed(3)}
</span>
</div>
<!-- Circular Progress -->
<div class="relative size-14 ml-auto">
<svg
class="size-full w-14 h-14"
viewBox="0 0 36 36"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background Circle -->
<circle
cx="18"
cy="18"
r="16"
fill="none"
class="stroke-current text-[#3E3E3E]"
stroke-width="3"
></circle>
<!-- Progress Circle inside a group with rotation -->
<g class="origin-center -rotate-90 transform">
<circle
cx="18"
cy="18"
r="16"
fill="none"
class="stroke-current text-blue-500"
stroke-width="3"
stroke-dasharray="100"
stroke-dashoffset={putCallRatio >= 1
? 0
: 100 - (putCallRatio * 100)?.toFixed(2)}
></circle>
</g>
</svg>
<!-- Percentage Text -->
<div
class="absolute top-1/2 start-1/2 transform -translate-y-1/2 -translate-x-1/2"
>
<span class="text-center text-white text-sm"
>{putCallRatio?.toFixed(2)}</span
>
</div>
</div>
<!-- End Circular Progress -->
</div>
<!--End Put/Call-->
<!--Start Call Flow-->
<div
class="flex flex-row items-center flex-wrap w-full px-5 bg-primary border border-gray-600 rounded-md h-20"
>
<div class="flex flex-col items-start">
<span class="font-semibold text-gray-200 text-sm sm:text-[1rem]"
>Call Flow</span
>
<span class="text-start text-[1rem] font-semibold text-white">
{new Intl.NumberFormat("en", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(displayCallVolume)}
</span>
</div>
<!-- Circular Progress -->
<div class="relative size-14 ml-auto">
<svg
class="size-full w-14 h-14"
viewBox="0 0 36 36"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background Circle -->
<circle
cx="18"
cy="18"
r="16"
fill="none"
class="stroke-current text-[#3E3E3E]"
stroke-width="3"
></circle>
<!-- Progress Circle inside a group with rotation -->
<g class="origin-center -rotate-90 transform">
<circle
cx="18"
cy="18"
r="16"
fill="none"
class="stroke-current text-[#00FC50]"
stroke-width="3"
stroke-dasharray="100"
stroke-dashoffset={100 - callPercentage?.toFixed(2)}
></circle>
</g>
</svg>
<!-- Percentage Text -->
<div
class="absolute top-1/2 start-1/2 transform -translate-y-1/2 -translate-x-1/2"
>
<span class="text-center text-white text-sm"
>{callPercentage}%</span
>
</div>
</div>
<!-- End Circular Progress -->
</div>
<!--End Call Flow-->
<!--Start Put Flow-->
<div
class="flex flex-row items-center flex-wrap w-full px-5 bg-primary border border-gray-600 rounded-md h-20"
>
<div class="flex flex-col items-start">
<span class="font-semibold text-gray-200 text-sm sm:text-[1rem]"
>Put Flow</span
>
<span class="text-start text-[1rem] font-semibold text-white">
{new Intl.NumberFormat("en", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(displayPutVolume)}
</span>
</div>
<!-- Circular Progress -->
<div class="relative size-14 ml-auto">
<svg
class="size-full w-14 h-14"
viewBox="0 0 36 36"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background Circle -->
<circle
cx="18"
cy="18"
r="16"
fill="none"
class="stroke-current text-[#3E3E3E]"
stroke-width="3"
></circle>
<!-- Progress Circle inside a group with rotation -->
<g class="origin-center -rotate-90 transform">
<circle
cx="18"
cy="18"
r="16"
fill="none"
class="stroke-current text-[#EE5365]"
stroke-width="3"
stroke-dasharray="100"
stroke-dashoffset={100 - putPercentage?.toFixed(2)}
></circle>
</g>
</svg>
<!-- Percentage Text -->
<div
class="absolute top-1/2 start-1/2 transform -translate-y-1/2 -translate-x-1/2"
>
<span class="text-center text-white text-sm"
>{putPercentage}%</span
>
</div>
</div>
<!-- End Circular Progress -->
</div>
</div>
</div>
<!-- Page wrapper -->
<div class="flex w-full m-auto h-full overflow-hidden">
{#if displayedData?.length !== 0}
<div class="mt-8 w-full overflow-x-auto h-[850px] overflow-hidden">
<OptionsFlowTable
{data}
{optionsWatchlist}
{displayedData}
{filteredData}
{rawData}
/>
<UpgradeToPro {data} />
</div>
{:else}
<div
class="text-white text-center p-3 sm:p-5 mb-10 mt-5 rounded-md sm:flex sm:flex-row sm:items-center border border-gray-600 text-sm sm:text-[1rem]"
>
<svg
class="w-6 h-6 flex-shrink-0 inline-block sm:mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
><path
fill="#fff"
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m-4 48a12 12 0 1 1-12 12a12 12 0 0 1 12-12m12 112a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 0 16"
/></svg
>
Looks like your taste is one-of-a-kind! No matches found... yet!
</div>
{/if}
</div>
{:else}
<div class="flex justify-center items-center h-80">
<div class="relative">
<label
class="bg-primary rounded-md 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>
</section>
</body>
<!--Start Choose Rule Modal-->
<input type="checkbox" id="ruleModal" class="modal-toggle" />
<dialog id="ruleModal" class="modal p-2 sm:p-0">
<label
id="ruleModal"
for="ruleModal"
on:click={() => (searchTerm = "")}
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.8]"
></label>
<div
class="modal-box relative bg-primary z-20 mx-2 min-h-[30vh] h-[800px] rounded bg-default opacity-100 border border-gray-600 bp:mx-3 sm:mx-4 w-full max-w-6xl overflow-y-auto"
>
<div class="relative flex flex-col w-full">
<!-- Sticky Header -->
<div
class="fixed w-full h-fit sticky -top-6 z-40 bg-primary shadow-sm opacity-100 pb-6 pt-5 border-gray-600 border-b"
>
<div class="flex flex-row items-center justify-between mb-2">
<h1 class="text-white text-[1rem] sm:text-xl font-semibold">
Select screener filters ({allRows?.length} total)
</h1>
<label
for="ruleModal"
class="inline-block cursor-pointer absolute right-0 top-3 text-[1.3rem] sm:text-[1.8rem] text-white"
>
<svg
class="w-6 h-6 sm:w-8 sm:h-8"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="white"
d="m6.4 18.308l-.708-.708l5.6-5.6l-5.6-5.6l.708-.708l5.6 5.6l5.6-5.6l.708.708l-5.6 5.6l5.6 5.6l-.708.708l-5.6-5.6z"
/></svg
>
</label>
</div>
<!-- Start Search bar -->
<form
class="w-full h-8"
on:keydown={(e) => (e?.key === "Enter" ? e.preventDefault() : "")}
>
<label for="search" class="text-sm font-medium text-gray-200 sr-only"
>Search</label
>
<div class="relative w-full max-w-sm">
<div
class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"
>
<svg
class="w-4 h-4 text-gray-200"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<div
class="absolute inset-y-0 right-0 flex items-center pr-2 {searchTerm?.length >
0
? ''
: 'hidden'}"
>
<button
on:click={() => (searchTerm = "")}
class="cursor-pointer text-gray-200 sm:hover:text-white"
tabindex="0"
><svg
class="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="max-width:40px"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path></svg
></button
>
</div>
<input
autocomplete="off"
type="search"
id="search"
class="placeholder-gray-300 block w-full p-2 ps-10 text-sm text-gray-200 border border-gray-600 rounded-md bg-primary border border-blue-500"
placeholder="Search"
bind:value={searchTerm}
/>
</div>
</form>
<!-- End Search bar -->
</div>
<!-- Content -->
<div class="text-white mt-5">
<div class="flex flex-wrap">
{#each searchTerm?.length !== 0 ? filteredRows : allRows as row}
<div
class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1"
>
{#if row?.rule === "score" && data?.user?.tier !== "Pro"}
<label id={row?.rule} on:click={() => changeRule(row?.rule)}>
<svg
class="w-4 h-4 mb-1 inline-block text-[#A3A3A3] sm:hover:text-white cursor-pointer"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="currentColor"
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
>
</label>
{:else}
<input
on:click={() => changeRule(row?.rule)}
id={row?.rule}
type="checkbox"
checked={ruleOfList?.find((rule) => rule?.name === row?.rule)}
class="h-[18px] w-[18px] rounded-sm ring-offset-0 lg:h-4 lg:w-4"
/>
{/if}
<div class="-mt-0.5">
<label for={row?.rule} class="cursor-pointer text-[1rem]"
>{row?.label}</label
>
</div>
</div>
{/each}
</div>
{#if searchTerm?.length > 0 && filteredRows?.length === 0}
<div class="text-white mt-5 font-semibold text-[1rem] sm:text-lg">
Nothing found
</div>
{/if}
</div>
</div>
</div>
</dialog>
<!--End Choose Rule Modal-->
<!--Start Options Detail Desktop Modal-->
<input type="checkbox" id="mobileTooltip" class="modal-toggle" />
<dialog id="mobileTooltip" class="modal p-3">
<label
for="mobileTooltip"
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.8]"
></label>
<!-- Desktop modal content -->
<div
class="modal-box rounded-md border border-gray-600 w-full bg-primary flex flex-col items-center"
>
<div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">{tooltipTitle}</h3>
<span class="text-white text-[1rem] font-normal"
>{infoText?.text ?? "n/a"}</span
>
{#if infoText?.equation !== undefined}
<div class="w-5/6 m-auto mt-5"></div>
<div class="text-[1rem] w-full pt-3 pb-3 m-auto text-white">
{infoText?.equation}
</div>
{/if}
</div>
<div class="border-t border-gray-600 mt-2 w-full">
<label
for="mobileTooltip"
class="mt-4 font-semibold text-white text-xl m-auto flex justify-center cursor-pointer"
>
Close
</label>
</div>
</div>
</dialog>