initial code commit
This commit is contained in:
parent
7bea123595
commit
b1006aa97d
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/package
|
||||
*.svelte-kit
|
||||
vite.config.js.timestamp-*
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.npmrc
|
||||
.prettierignore
|
||||
.prettierrc
|
||||
.env*
|
||||
13
components.json
Normal file
13
components.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components/shadcn",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
}
|
||||
17
jsconfig.json
Normal file
17
jsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
8863
package-lock.json
generated
Normal file
8863
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
105
package.json
Normal file
105
package.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"test": "playwright test",
|
||||
"test:unit": "vitest",
|
||||
"lint": "prettier --plugin-search-dir . --check .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"test:chrome": "npx playwright test --project=chromium",
|
||||
"test:firefox": "npx playwright test --project=firefox",
|
||||
"test:safari": "npx playwright test --headed --project=safari"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@rollup/plugin-dynamic-import-vars": "^2.1.2",
|
||||
"@sveltejs/adapter-vercel": "^5.3.0",
|
||||
"@sveltejs/kit": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@types/gtag.js": "^0.0.19",
|
||||
"@types/node": "^20.12.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.10.2",
|
||||
"date-picker-svelte": "^2.12.0",
|
||||
"echarts": "^5.5.0",
|
||||
"object-to-formdata": "^4.5.1",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"sass": "^1.75.0",
|
||||
"svelte": "^4.2.15",
|
||||
"svelte-check": "^3.6.9",
|
||||
"svelte-echarts": "^0.1.1",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"svelte-intersection-observer": "^1.0.0",
|
||||
"svelte-lazy": "^1.2.7",
|
||||
"svelte-preprocess": "^5.1.4",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.10",
|
||||
"vite-plugin-dynamic-import": "^1.5.0",
|
||||
"vitest": "^1.5.1",
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.556.0",
|
||||
"blob-util": "^2.0.2",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"compression": "^1.7.4",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-tz": "^3.1.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"flowbite-svelte": "^0.46.0",
|
||||
"got": "^14.2.1",
|
||||
"greeks": "^1.0.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"https": "^1.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"katex": "^0.16.10",
|
||||
"layercake": "^8.1.2",
|
||||
"lightweight-charts": "^4.1.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.4.4",
|
||||
"marked": "^12.0.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mixpanel": "^0.18.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"parse5": "^7.1.2",
|
||||
"pocketbase": "^0.21.2",
|
||||
"quill": "^2.0.0",
|
||||
"quill-delta-to-html": "^0.12.1",
|
||||
"sharp": "^0.33.3",
|
||||
"showdown": "^2.1.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"svelte-intersection-observer-action": "^0.0.4",
|
||||
"svelte-lightweight-charts": "^2.2.0",
|
||||
"svelte-loading-spinners": "^0.3.6",
|
||||
"svelte-progress-bar": "^3.0.2",
|
||||
"svelte-range-slider-pips": "^2.3.1",
|
||||
"svelte-tags-input": "^6.0.0",
|
||||
"tslib": "^2.6.2",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vaul-svelte": "^0.3.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"description": "UI of stocknear - Stock Analysis & Community Platform for Small Investors.",
|
||||
"main": "svelte.config.js",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
19
playwright.config.ts
Normal file
19
playwright.config.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineConfig, devices } from '@playwright/test'; // import devices
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests',
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'safari',
|
||||
use: {
|
||||
...devices['Iphone 6'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
9
postcss.config.cjs
Normal file
9
postcss.config.cjs
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
|
||||
autoprefixer: {},
|
||||
|
||||
},
|
||||
}
|
||||
141
src/app.css
Normal file
141
src/app.css
Normal file
@ -0,0 +1,141 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
|
||||
|
||||
.table {
|
||||
@apply rounded-box text-left text-sm rtl:text-right;
|
||||
:where(th, td) {
|
||||
@apply px-4 py-3 align-middle;
|
||||
}
|
||||
tr.active,
|
||||
tr.active:nth-child(even),
|
||||
&-zebra tbody tr:nth-child(even) {
|
||||
@apply bg-[#191E24];
|
||||
}
|
||||
tr.hover,
|
||||
tr.hover:nth-child(even) {
|
||||
@apply [@media(hover:hover)]:hover:bg-[#000];
|
||||
}
|
||||
|
||||
&-zebra {
|
||||
tr.active,
|
||||
tr.active:nth-child(even),
|
||||
&-zebra tbody tr:nth-child(even) {
|
||||
@apply bg-[#000];
|
||||
}
|
||||
}
|
||||
&-zebra tr.hover,
|
||||
&-zebra tr.hover:nth-child(even) {
|
||||
@apply [@media(hover:hover)]:hover:bg-[#000];
|
||||
}
|
||||
|
||||
:where(thead, tbody) {
|
||||
:where(tr:not(:last-child)),
|
||||
:where(tr:first-child:last-child) {
|
||||
@apply border-b-[#000] border-b;
|
||||
}
|
||||
}
|
||||
|
||||
:where(thead, tfoot) {
|
||||
@apply text-base-content/60 whitespace-nowrap text-xs;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@layer utilities {
|
||||
/* Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.shake-ticker:hover img {
|
||||
animation-name: shake;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(10deg); }
|
||||
50% { transform: rotate(0deg); }
|
||||
75% { transform: rotate(-10deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
border-radius: 50%;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation: load7 0.8s infinite ease-in-out;
|
||||
animation: load7 0.8s infinite ease-in-out;
|
||||
}
|
||||
.loader {
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
margin: 80px auto;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.loader:before {
|
||||
left: -3.5em;
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.loader:after {
|
||||
left: 3.5em;
|
||||
}
|
||||
@-webkit-keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
@keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
23
src/app.d.ts
vendored
Normal file
23
src/app.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
|
||||
// src/app.d.ts
|
||||
/*
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
declare global {
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
pb: PocketBase
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
23
src/app.html
Normal file
23
src/app.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" class="bg-[#0F0F0F]">
|
||||
|
||||
|
||||
<head>
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
%sveltekit.head%
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<!-- Global background color -->
|
||||
<body data-sveltekit-preload-data="hover" class="bg-[#0F0F0F] overflow-x-hidden">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
64
src/hooks.server.ts
Normal file
64
src/hooks.server.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
import { serializeNonPOJOs } from '$lib/utils';
|
||||
//import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
|
||||
|
||||
export const handle = async ({ event, resolve }) => {
|
||||
|
||||
|
||||
|
||||
|
||||
//let distinctUserId= uuidv4()
|
||||
|
||||
|
||||
//event.cookies.set('mixpanelUserId', distinctUserId, { path: '/', httpOnly: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 365});
|
||||
|
||||
event.locals.region = decodeURIComponent(
|
||||
event?.request?.headers?.get('x-vercel-id') ?? 'fra1::fra1::8t4xg-1700258428633-157d82fdfcc7',
|
||||
);
|
||||
|
||||
const userRegion = event?.locals?.region?.split("::")?.at(0)?.split("::")?.at(0)
|
||||
|
||||
let pbUrl = import.meta.env.VITE_EU_POCKETBASE_URL; // Set a default API URL
|
||||
|
||||
if (usRegion?.includes(userRegion)) {
|
||||
pbUrl = import.meta.env.VITE_USEAST_POCKETBASE_URL;
|
||||
} else {
|
||||
pbUrl = import.meta.env.VITE_EU_POCKETBASE_URL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
event.locals.pb = new PocketBase(pbUrl);
|
||||
event.locals.pb.authStore.loadFromCookie(event?.request?.headers?.get('cookie') || '');
|
||||
|
||||
try {
|
||||
if (event?.locals?.pb?.authStore?.isValid) {
|
||||
await event?.locals?.pb?.collection('users')?.authRefresh();
|
||||
event.locals.user = serializeNonPOJOs(event?.locals?.pb?.authStore?.model);
|
||||
}
|
||||
} catch(_) {
|
||||
event?.locals?.pb?.authStore?.clear();
|
||||
event.locals.user = undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const response = await resolve(event);
|
||||
|
||||
|
||||
|
||||
response.headers.append('set-cookie', event?.locals?.pb?.authStore?.exportToCookie({ httpOnly: true, path: '/', sameSite: 'lax', secure: true, maxAge: 60 * 60 * 24 * 365}));
|
||||
|
||||
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
7
src/index.test.js
Normal file
7
src/index.test.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
||||
4
src/lib/assets/Info.svelte
Normal file
4
src/lib/assets/Info.svelte
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 inline-block" viewBox="0 0 16 16"><g fill="white"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="m8.93 6.588l-2.29.287l-.082.38l.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319c.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246c-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 456 B |
1
src/lib/assets/badEmoji.svg
Normal file
1
src/lib/assets/badEmoji.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#ffce52;}.cls-2{fill:url(#radial-gradient);}.cls-3{fill:#ffb32b;}.cls-4{fill:#273941;}.cls-5{fill:#141e21;}.cls-6{fill:#f6fafd;}.cls-7{fill:#3bc5f6;}.cls-8{fill:#00a3e1;}</style><radialGradient id="radial-gradient" cx="-27.957" cy="8.563" r="6" gradientTransform="translate(107.871 -4.408) scale(3 2.5)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5987dd"/><stop offset="0.118" stop-color="#638bd6"/><stop offset="0.316" stop-color="#7d98c3"/><stop offset="0.572" stop-color="#a8aba3"/><stop offset="0.871" stop-color="#e3c678"/><stop offset="1" stop-color="#ffd364"/></radialGradient></defs><title>27-feel bad</title><g id="_27-feel_bad" data-name="27-feel bad"><circle class="cls-1" cx="24" cy="24" r="23"/><ellipse class="cls-2" cx="24" cy="17" rx="18" ry="15"/><path class="cls-3" d="M46,23c0,10.493-9.85,19-22,19S2,33.493,2,23H1.025c-.014.332-.025.665-.025,1a23,23,0,0,0,46,0c0-.335-.011-.668-.025-1Z"/><ellipse class="cls-4" cx="33" cy="23" rx="3" ry="4"/><ellipse class="cls-5" cx="33" cy="23" rx="2" ry="3"/><circle class="cls-6" cx="34" cy="22" r="1"/><ellipse class="cls-4" cx="15" cy="23" rx="3" ry="4"/><ellipse class="cls-5" cx="15" cy="23" rx="2" ry="3"/><circle class="cls-6" cx="16" cy="22" r="1"/><path class="cls-5" d="M10,19V17c3.722,0,6-1.295,6-2h2C18,17.626,13.976,19,10,19Z"/><path class="cls-5" d="M38,19c-3.976,0-8-1.374-8-4h2c0,.705,2.278,2,6,2Z"/><path class="cls-4" d="M24,35H18.5A5.265,5.265,0,0,1,24,30a5.265,5.265,0,0,1,5.5,5Z"/><path class="cls-5" d="M24,31a5.4,5.4,0,0,1,5.39,4h.11A5.265,5.265,0,0,0,24,30a5.265,5.265,0,0,0-5.5,5h.11A5.4,5.4,0,0,1,24,31Z"/><path class="cls-7" d="M47,12a4,4,0,0,1-8,0c0-2.209,3-8,4-8S47,9.791,47,12Z"/><path class="cls-8" d="M43,4c-1,0-4,5.791-4,8a4,4,0,0,0,8,0C47,9.791,44,4,43,4Zm0,8.5a2.848,2.848,0,0,1-3-2.667C40,8.361,42.25,4.5,43,4.5s3,3.861,3,5.333A2.848,2.848,0,0,1,43,12.5Z"/><path class="cls-3" d="M43,16a3.991,3.991,0,0,1-3.861-3.008A4.661,4.661,0,0,0,39,14a4,4,0,0,0,6.846,2.807q-.255-.777-.562-1.529A3.982,3.982,0,0,1,43,16Z"/><ellipse class="cls-6" cx="44" cy="11" rx="0.825" ry="1.148" transform="translate(5.117 34.348) rotate(-45.02)"/><ellipse class="cls-6" cx="44.746" cy="9.5" rx="0.413" ry="0.574" transform="translate(6.397 34.436) rotate(-45.02)"/><ellipse class="cls-6" cx="36" cy="9" rx="0.825" ry="1.148" transform="translate(4.187 28.103) rotate(-45.02)"/><ellipse class="cls-6" cx="29.746" cy="4.5" rx="0.413" ry="0.574" transform="translate(5.536 22.36) rotate(-45.02)"/><ellipse class="cls-6" cx="33" cy="7" rx="1.65" ry="2.297" transform="translate(4.722 25.394) rotate(-45.02)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
src/lib/assets/discord_animation.json
Normal file
1
src/lib/assets/discord_animation.json
Normal file
File diff suppressed because one or more lines are too long
186
src/lib/assets/driver.css
Normal file
186
src/lib/assets/driver.css
Normal file
@ -0,0 +1,186 @@
|
||||
.driver-active .driver-overlay, .driver-active * {
|
||||
pointer-events: none;
|
||||
}
|
||||
.driver-active .driver-active-element, .driver-active .driver-active-element *, .driver-popover, .driver-popover * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
@keyframes animate-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}.driver-fade .driver-overlay {
|
||||
animation: animate-fade-in .2s ease-in-out;
|
||||
}
|
||||
.driver-fade .driver-popover {
|
||||
animation: animate-fade-in .2s;
|
||||
}
|
||||
.driver-popover {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
border-radius: 0px;
|
||||
min-width: 250px;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 1px 10px #0006;
|
||||
z-index: 1000000000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: #202327;
|
||||
}
|
||||
.driver-popover * {
|
||||
font-family: Helvetica Neue, Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.driver-popover-title {
|
||||
font: 19px/normal sans-serif;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
position: relative;
|
||||
line-height: 1.5;
|
||||
zoom: 1;
|
||||
margin: 0;
|
||||
}
|
||||
.driver-popover-close-btn {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #d2d2d2;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
transition: color;
|
||||
transition-duration: .2s;
|
||||
}
|
||||
.driver-popover-close-btn:hover, .driver-popover-close-btn:focus {
|
||||
color: #2d2d2d;
|
||||
}
|
||||
.driver-popover-title[style*=block]+.driver-popover-description {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.driver-popover-description {
|
||||
margin-bottom: 0;
|
||||
font: 16px/normal sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
zoom: 1;
|
||||
}
|
||||
.driver-popover-footer {
|
||||
margin-top: 40px;
|
||||
text-align: right;
|
||||
zoom: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.driver-popover-progress-text {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
zoom: 1;
|
||||
}
|
||||
.driver-popover-footer button {
|
||||
all: unset;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 20px;
|
||||
text-decoration: none;
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
background-color: #fff;
|
||||
color: #2d2d2d;
|
||||
font: 14px/normal sans-serif;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
zoom: 1;
|
||||
line-height: 1.3;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.driver-popover-footer .driver-popover-btn-disabled {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
:not(body):has(>.driver-active-element) {
|
||||
overflow: hidden!important;
|
||||
}
|
||||
.driver-no-interaction, .driver-no-interaction * {
|
||||
pointer-events: none!important;
|
||||
}
|
||||
.driver-popover-footer button:hover, .driver-popover-footer button:focus {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
.driver-popover-navigation-btns {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.driver-popover-navigation-btns button+button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.driver-popover-arrow {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: 5px solid #fff;
|
||||
}
|
||||
.driver-popover-arrow-side-over {
|
||||
display: none;
|
||||
}
|
||||
.driver-popover-arrow-side-left {
|
||||
left: 100%;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
.driver-popover-arrow-side-right {
|
||||
right: 100%;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
.driver-popover-arrow-side-top {
|
||||
top: 100%;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
.driver-popover-arrow-side-bottom {
|
||||
bottom: 100%;
|
||||
border-left-color: transparent;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
.driver-popover-arrow-side-center {
|
||||
display: none;
|
||||
}
|
||||
.driver-popover-arrow-side-left.driver-popover-arrow-align-start, .driver-popover-arrow-side-right.driver-popover-arrow-align-start {
|
||||
top: 15px;
|
||||
}
|
||||
.driver-popover-arrow-side-top.driver-popover-arrow-align-start, .driver-popover-arrow-side-bottom.driver-popover-arrow-align-start {
|
||||
left: 15px;
|
||||
}
|
||||
.driver-popover-arrow-align-end.driver-popover-arrow-side-left, .driver-popover-arrow-align-end.driver-popover-arrow-side-right {
|
||||
bottom: 15px;
|
||||
}
|
||||
.driver-popover-arrow-side-top.driver-popover-arrow-align-end, .driver-popover-arrow-side-bottom.driver-popover-arrow-align-end {
|
||||
right: 15px;
|
||||
}
|
||||
.driver-popover-arrow-side-left.driver-popover-arrow-align-center, .driver-popover-arrow-side-right.driver-popover-arrow-align-center {
|
||||
top: 50%;
|
||||
margin-top: -5px;
|
||||
}
|
||||
.driver-popover-arrow-side-top.driver-popover-arrow-align-center, .driver-popover-arrow-side-bottom.driver-popover-arrow-align-center {
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
}
|
||||
.driver-popover-arrow-none {
|
||||
display: none;
|
||||
}
|
||||
1
src/lib/assets/goodEmoji.svg
Normal file
1
src/lib/assets/goodEmoji.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#ffce52;}.cls-2{fill:#273941;}.cls-3{fill:#ffe369;}.cls-4{fill:#ffb32b;}.cls-5{fill:#f6fafd;}</style></defs><title>03-smile</title><g id="_03-smile" data-name="03-smile"><circle class="cls-1" cx="24" cy="24" r="23"/><path class="cls-2" d="M24,39c-7.168,0-13-4.935-13-11h2c0,4.962,4.935,9,11,9s11-4.038,11-9h2C37,34.065,31.168,39,24,39Z"/><path class="cls-2" d="M20,21H18c0-2.206-1.346-4-3-4s-3,1.794-3,4H10c0-3.309,2.243-6,5-6S20,17.691,20,21Z"/><path class="cls-2" d="M38,21H36c0-2.206-1.346-4-3-4s-3,1.794-3,4H28c0-3.309,2.243-6,5-6S38,17.691,38,21Z"/><path class="cls-3" d="M24,4c12.15,0,22,8.507,22,19h.975a23,23,0,0,0-45.95,0H2C2,12.507,11.85,4,24,4Z"/><path class="cls-4" d="M46,23c0,10.493-9.85,19-22,19S2,33.493,2,23H1.025c-.014.332-.025.665-.025,1a23,23,0,0,0,46,0c0-.335-.011-.668-.025-1Z"/><ellipse class="cls-5" cx="37" cy="9" rx="0.825" ry="1.148" transform="translate(4.48 28.81) rotate(-45.02)"/><ellipse class="cls-5" cx="30.746" cy="4.5" rx="0.413" ry="0.574" transform="translate(5.829 23.067) rotate(-45.02)"/><ellipse class="cls-5" cx="34" cy="7" rx="1.65" ry="2.297" transform="translate(5.015 26.102) rotate(-45.02)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
src/lib/assets/index.js
Normal file
3
src/lib/assets/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Info from './Info.svelte';
|
||||
|
||||
export {Info};
|
||||
1
src/lib/assets/landing_page.json
Normal file
1
src/lib/assets/landing_page.json
Normal file
File diff suppressed because one or more lines are too long
1014
src/lib/assets/style_quill.css
Normal file
1014
src/lib/assets/style_quill.css
Normal file
File diff suppressed because it is too large
Load Diff
1
src/lib/assets/veryBadEmoji.svg
Normal file
1
src/lib/assets/veryBadEmoji.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#cf4054;}.cls-2{fill:#f45269;}.cls-3{fill:#ae2d4c;}.cls-4{fill:#f6fafd;}.cls-5{fill:#141e21;}.cls-6{fill:#273941;}</style></defs><title>28-angry</title><g id="_28-angry" data-name="28-angry"><circle class="cls-1" cx="24" cy="24" r="23"/><path class="cls-2" d="M24,4c12.15,0,22,8.507,22,19h.975a23,23,0,0,0-45.95,0H2C2,12.507,11.85,4,24,4Z"/><path class="cls-3" d="M46,23c0,10.493-9.85,19-22,19S2,33.493,2,23H1.025c-.014.332-.025.665-.025,1a23,23,0,0,0,46,0c0-.335-.011-.668-.025-1Z"/><ellipse class="cls-4" cx="36.5" cy="8.5" rx="0.825" ry="1.148" transform="translate(4.687 28.31) rotate(-45.02)"/><ellipse class="cls-4" cx="30.246" cy="4" rx="0.413" ry="0.574" transform="translate(6.037 22.567) rotate(-45.02)"/><ellipse class="cls-4" cx="33.5" cy="6.5" rx="1.65" ry="2.297" transform="translate(5.222 25.602) rotate(-45.02)"/><ellipse class="cls-3" cx="24" cy="25" rx="10" ry="2"/><path class="cls-5" d="M39.447,16.9l-.894-1.79-4.934,2.467h0l-2.927,1.464-.136.068.015.03a.982.982,0,0,0-.4.55A5.335,5.335,0,0,0,30,21c0,2.243,1.317,4,3,4s3-1.757,3-4a5.011,5.011,0,0,0-.483-2.14Z"/><path class="cls-5" d="M17.432,19.135l.015-.03-.136-.068-2.927-1.464h0L9.447,15.105,8.553,16.9l3.93,1.965A5.011,5.011,0,0,0,12,21c0,2.243,1.317,4,3,4s3-1.757,3-4a5.335,5.335,0,0,0-.168-1.315A.982.982,0,0,0,17.432,19.135Z"/><path class="cls-6" d="M16.86,19.93A4.07,4.07,0,0,1,17,21c0,1.66-.9,3-2,3s-2-1.34-2-3a3.516,3.516,0,0,1,.94-2.53Z"/><path class="cls-6" d="M35,21c0,1.66-.9,3-2,3s-2-1.34-2-3a4.07,4.07,0,0,1,.14-1.07l2.92-1.46A3.516,3.516,0,0,1,35,21Z"/><path class="cls-5" d="M31,36H29c0-1.812-2.23-4-5-4s-5,2.188-5,4H17c0-2.832,2.993-6,7-6S31,33.168,31,36Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/lib/assets/veryGoodEmoji.svg
Normal file
1
src/lib/assets/veryGoodEmoji.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#ffce52;}.cls-2{fill:#ffe369;}.cls-3{fill:#ffb32b;}.cls-4{fill:#f6fafd;}.cls-5{fill:#273941;}.cls-6{fill:#ae2d4c;}.cls-7{fill:#cf4054;}.cls-8{fill:#141e21;}.cls-9{fill:#8a293d;}.cls-10{fill:#fbb40a;}</style></defs><title>13-love</title><g id="_13-love" data-name="13-love"><circle class="cls-1" cx="24" cy="24" r="23"/><path class="cls-2" d="M24,4c12.15,0,22,8.507,22,19h.975a23,23,0,0,0-45.95,0H2C2,12.507,11.85,4,24,4Z"/><path class="cls-3" d="M46,23c0,10.493-9.85,19-22,19S2,33.493,2,23H1.025c-.014.332-.025.665-.025,1a23,23,0,0,0,46,0c0-.335-.011-.668-.025-1Z"/><ellipse class="cls-4" cx="37" cy="9" rx="0.825" ry="1.148" transform="translate(4.48 28.81) rotate(-45.02)"/><ellipse class="cls-4" cx="30.746" cy="4.5" rx="0.413" ry="0.574" transform="translate(5.829 23.067) rotate(-45.02)"/><ellipse class="cls-4" cx="34" cy="7" rx="1.65" ry="2.297" transform="translate(5.015 26.102) rotate(-45.02)"/><path class="cls-5" d="M34,39c0-2.76-4.47-5-10-5s-10,2.24-10,5l-.1.13A10.727,10.727,0,0,1,9,30.15,2.025,2.025,0,0,1,10.87,28c1.88,1.08,6.39,1,13.13,1s11.25.08,13.12-1A2.026,2.026,0,0,1,39,30.15a10.727,10.727,0,0,1-4.9,8.98Z"/><path class="cls-6" d="M34,39l.1.13A17.882,17.882,0,0,1,24,42a17.882,17.882,0,0,1-10.1-2.87L14,39c0-2.76,4.47-5,10-5S34,36.24,34,39Z"/><path class="cls-7" d="M16.5,9A4.465,4.465,0,0,1,21,13.8C21,21,13.5,25,12,25c-.72,0-8.38-3.7-8.97-10.39Q3,14.205,3,13.8A4.451,4.451,0,0,1,6.58,9.1,4.053,4.053,0,0,1,7.5,9c2.25,0,3.75,1.6,4.5,4C12.75,10.6,14.25,9,16.5,9Z"/><path class="cls-7" d="M45,13.8q0,.4-.03.81C44.44,21.3,37.44,25,36,25c-.75,0-9-4-9-11.2A4.465,4.465,0,0,1,31.5,9c2.25,0,3.75,1.6,4.5,4,.75-2.4,2.25-4,4.5-4a4.053,4.053,0,0,1,.92.1A4.451,4.451,0,0,1,45,13.8Z"/><path class="cls-8" d="M10.87,30c1.88,1.08,6.39,1,13.13,1s11.25.08,13.12-1a1.926,1.926,0,0,1,1.793,1.536A11.043,11.043,0,0,0,39,30.15,2.026,2.026,0,0,0,37.12,28c-1.87,1.08-6.38,1-13.12,1s-11.25.08-13.13-1A2.025,2.025,0,0,0,9,30.15a11.015,11.015,0,0,0,.087,1.385A1.92,1.92,0,0,1,10.87,30Z"/><path class="cls-9" d="M33.531,37.486A18.171,18.171,0,0,1,24,40a18.171,18.171,0,0,1-9.531-2.514A2.809,2.809,0,0,0,14,39l-.1.13A17.882,17.882,0,0,0,24,42a17.882,17.882,0,0,0,10.1-2.87L34,39A2.809,2.809,0,0,0,33.531,37.486Z"/><path class="cls-10" d="M36,25c-.71,0-8.131-3.59-8.921-10.081A6,6,0,0,0,27,15.8C27,23,35.25,27,36,27c1.44,0,8.44-3.7,8.97-10.39Q45,16.2,45,15.8a6.079,6.079,0,0,0-.07-.907C44.225,21.4,37.419,25,36,25Z"/><path class="cls-10" d="M12,25c-.71,0-8.131-3.59-8.921-10.081A6,6,0,0,0,3,15.8C3,23,11.25,27,12,27c1.44,0,8.44-3.7,8.97-10.39Q21,16.2,21,15.8a6.079,6.079,0,0,0-.07-.907C20.225,21.4,13.419,25,12,25Z"/><path class="cls-6" d="M40.5,9c-2.25,0-3.75,1.6-4.5,4,.583-1.8,1.75-3,3.5-3A3.408,3.408,0,0,1,43,13.6C43,19,37.167,22,36,22c-.56,0-6.518-2.775-6.977-7.793-.015-.2-.023-.405-.023-.607a3.366,3.366,0,0,1,2.784-3.525A3.243,3.243,0,0,1,32.5,10c1.75,0,2.917,1.2,3.5,3-.75-2.4-2.25-4-4.5-4a4.053,4.053,0,0,0-.92.1A4.451,4.451,0,0,0,27,13.8q0,.4.03.81C27.62,21.3,35.28,25,36,25c1.5,0,9-4,9-11.2A4.465,4.465,0,0,0,40.5,9Z"/><path class="cls-6" d="M16.5,9c-2.25,0-3.75,1.6-4.5,4,.583-1.8,1.75-3,3.5-3A3.408,3.408,0,0,1,19,13.6C19,19,13.167,22,12,22c-.56,0-6.518-2.775-6.977-7.793C5.008,14.005,5,13.8,5,13.6a3.366,3.366,0,0,1,2.784-3.525A3.243,3.243,0,0,1,8.5,10c1.75,0,2.917,1.2,3.5,3-.75-2.4-2.25-4-4.5-4a4.053,4.053,0,0,0-.92.1A4.451,4.451,0,0,0,3,13.8q0,.4.03.81C3.62,21.3,11.28,25,12,25c1.5,0,9-4,9-11.2A4.465,4.465,0,0,0,16.5,9Z"/><ellipse class="cls-4" cx="42" cy="13" rx="0.825" ry="1.148" transform="translate(3.116 33.519) rotate(-45.02)"/><ellipse class="cls-4" cx="40.746" cy="11.5" rx="0.413" ry="0.574" transform="translate(3.809 32.193) rotate(-45.02)"/><ellipse class="cls-4" cx="18" cy="13" rx="0.825" ry="1.148" transform="translate(-3.919 16.543) rotate(-45.02)"/><ellipse class="cls-4" cx="16.746" cy="11.5" rx="0.413" ry="0.574" transform="translate(-3.226 15.216) rotate(-45.02)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/lib/audio/options-flow-reader.mp3
Normal file
BIN
src/lib/audio/options-flow-reader.mp3
Normal file
Binary file not shown.
392
src/lib/components/AddPortfolio.svelte
Normal file
392
src/lib/components/AddPortfolio.svelte
Normal file
@ -0,0 +1,392 @@
|
||||
<script lang='ts'>
|
||||
import toast from 'svelte-french-toast';
|
||||
import { userRegion, stockTicker, etfTicker, cryptoTicker, assetType, screenWidth } from "$lib/store";
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let data;
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let fastifyURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let isClicked = false;
|
||||
|
||||
|
||||
async function createPortfolio(event) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const postData = {'userId': data?.user?.id}
|
||||
|
||||
const response = await fetch(fastifyURL+'/create-portfolio', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
const output = await response.json();
|
||||
|
||||
|
||||
if ( output?.message === 'success')
|
||||
{
|
||||
isClicked = true;
|
||||
toast.success('Portfolio created successfully!', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'});
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
const clicked = document.getElementById('addPortfolio');
|
||||
clicked.dispatchEvent(new MouseEvent('click'));
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
|
||||
if ($page.url.pathname === '/portfolio')
|
||||
{
|
||||
anchor.href = '/portfolio';
|
||||
}
|
||||
else {
|
||||
if ($assetType === 'etf')
|
||||
{
|
||||
anchor.href = `/etf/${$etfTicker}`;
|
||||
}
|
||||
else if ($assetType === 'crypto')
|
||||
{
|
||||
anchor.href = `/crypto/${$cryptoTicker}`;
|
||||
}
|
||||
else {
|
||||
anchor.href = `/stocks/${$stockTicker}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
anchor.dataset.sveltekitReload = true;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.dispatchEvent(new MouseEvent('click'));
|
||||
}, 500);
|
||||
}
|
||||
else {
|
||||
toast.error('Something went wrong. Please try again.', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
let participationCheckbox = false;
|
||||
let ageCheckbox = false;
|
||||
|
||||
function handleAge() {
|
||||
ageCheckbox = !ageCheckbox; // Toggle the value using the ! operator
|
||||
}
|
||||
|
||||
function handleParticipation() {
|
||||
participationCheckbox = !participationCheckbox; // Toggle the value using the ! operator
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if $screenWidth >= 640}
|
||||
<input type="checkbox" id="addPortfolio" class="modal-toggle" />
|
||||
|
||||
<dialog id="addPortfolio" class="modal modal-bottom sm:modal-middle overflow-hidden">
|
||||
|
||||
|
||||
<label for="addPortfolio" class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
|
||||
|
||||
<div class="modal-box w-full bg-[#202020] overflow-hidden" >
|
||||
|
||||
|
||||
<div class="flex flex-col w-full mt-10 sm:mt-0">
|
||||
|
||||
|
||||
<div class="text-white text-3xl font-bold mb-5">
|
||||
Portfolio Tournament 🔥🚀
|
||||
</div>
|
||||
|
||||
<div class="text-white text-lg font-medium mb-3">
|
||||
Terms and Conditions of Participation
|
||||
</div>
|
||||
|
||||
<div class="text-white text-sm border bg-[#313131] border-gray-800 p-3 rounded-lg overflow-y-scroll h-56">
|
||||
|
||||
<ol class="text-white list-decimal ml-3 p-2">
|
||||
<li class="p-1">The organizer of the Prize competition is stocknear.</li>
|
||||
<li class="p-1">Participation is free of charge.</li>
|
||||
<li class="p-1">
|
||||
All individuals worldwide who have reached the age of 18 and comply with the laws and regulations of their respective countries are eligible to participate. Persons involved in the conception and implementation of this prize competition are excluded - this explicitly does not apply to voluntary moderators or other voluntary members who have a special status on the platform.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The prerequisite for participation in the competition is the creation of a portfolio whereby the user can buy and sell shares. Each user starts with a play money of 100,000 $. To be considered in the final draw and eligible for any prizes, participants must make at least one trade (buy or sell shares) during the tournament period.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The prize competition will take place automated once a month for an indefinite period of time. The potential winners are drawn automatically, randomly on the first day of each month, whereby the chance of winning depends on the overall return of the portfolio. If a participant has collected too little return of the portfolio, the chance can also be 0 percent.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The prizes to be awarded may change from month to month.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
After the draw, the accounts of the winners will be checked for possible fraud attempts. If no abnormalities are found, the potential winners will be notified by email from stocknear.
|
||||
If any abnormalities are found, the draw will be repeated. The organizer reserves the right to refuse and reclaim prizes even after the fact and to permanently exclude participants from competitions in case of suspicion of attempted fraud or manipulation for their own benefit or the benefit of third parties as well as to temporarily or permanently block their accounts.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The potential winner must respond to the respective prize notification within 14 days,
|
||||
which will be sent via email, in order to claim the prize.
|
||||
Only a response via email will be considered.
|
||||
Otherwise, unclaimed prizes will be included in the next monthly draw.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The organizer points out that the availability and functionality of the prize draw cannot be guaranteed.
|
||||
The organizer is not responsible for entries not being included due to technical failures or other reasons.
|
||||
The prize competition may be terminated or removed due to external circumstances and constraints
|
||||
without any claims arising for the participants against the organizer.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The organizer reserves the right to change, discontinue, or suspend the game
|
||||
and the draw in whole or in part without prior notice in the event
|
||||
of unforeseen circumstances. These circumstances include, but are not limited to,
|
||||
the appearance of a computer virus, a program error, unauthorized intervention by third parties,
|
||||
or mechanical or technical problems beyond the control and influence of the organizer.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The organizer reserves the right to terminate the prize draw at any time without prior notice.
|
||||
In this case, any outstanding prizes will be properly awarded.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
Should individual provisions of these terms and conditions of participation be or become invalid,
|
||||
inadmissible, or unenforceable, this shall not affect the validity of the remaining terms
|
||||
and conditions. In place of the invalid, inadmissible, or unenforceable clause, provisions
|
||||
shall be deemed to have been agreed upon that come as close as possible to the economic objectives.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
Furthermore, the <a href="/imprint" class="text-blue-700 hover:underline">Imprint</a>, <a href="/terms-of-use" class="text-blue-700 hover:underline">
|
||||
Terms of Use
|
||||
</a> and our
|
||||
<a href="/privacy-policy" class="text-blue-700 hover:underline">
|
||||
Privacy Policy
|
||||
</a>
|
||||
apply.
|
||||
The applicable law and jurisdiction for this prize draw shall be determined based on the participant's
|
||||
country of residence. In case of any legal disputes, the competent courts of the participant's
|
||||
country of residence shall have exclusive jurisdiction.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The right of recourse to the courts is excluded.
|
||||
</li>
|
||||
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-5">
|
||||
<label class="label cursor-pointer flex flex-row">
|
||||
<input on:click={handleParticipation} checked={participationCheckbox} aria-describedby="helper-checkbox-text" type="checkbox" value="" class="w-6 h-6 bg-[#333333] border-slate-500 focus-none rounded ring-1">
|
||||
<span class="label-text text-white ml-3">I accept the Terms and Conditions of Participation
|
||||
in the monthly portfolio tournament.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-control ">
|
||||
<label class="label cursor-pointer">
|
||||
<input on:click={handleAge} checked={ageCheckbox} aria-describedby="helper-checkbox-text" type="checkbox" value="" class="w-6 h-6 bg-[#333333] border-slate-500 focus-none rounded ring-1">
|
||||
<span class="label-text text-white ml-3 mr-auto">I'm at least 18 years old.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if ageCheckbox && participationCheckbox && !isClicked}
|
||||
<form
|
||||
on:submit={createPortfolio}
|
||||
class="w-full max-w-lg pt-5 m-auto pb-8"
|
||||
>
|
||||
<button type="submit" class="btn bg-blue-700 hover:bg-blue-600 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Create Portfolio
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="w-full max-w-lg pt-5 m-auto pb-8">
|
||||
<label class="opacity-[0.4] cursor-not-allowed btn bg-blue-700 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
{#if !isClicked}
|
||||
Create Portfolio
|
||||
{:else}
|
||||
<div class="flex flex-row m-auto">
|
||||
<span class="loading loading-infinity"></span>
|
||||
<span class="text-white ml-2">Loading</span>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<!--Start Drawer Sidewise for mobile-->
|
||||
|
||||
|
||||
<!--Start ESG Modal-->
|
||||
<div class="drawer drawer-end overflow-hidden w-screen" style="z-index: 9999;">
|
||||
<input id="addPortfolio" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen px-5 pt-16 w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<h1 class="text-white sm:hidden font-bold text-2xl mb-5">
|
||||
Portfolio Tournament 🔥🚀
|
||||
</h1>
|
||||
|
||||
|
||||
<div class="text-white text-lg font-medium mb-3">
|
||||
Terms and Conditions of Participation
|
||||
</div>
|
||||
|
||||
<div class="text-white text-sm border bg-[#202020] border-gray-800 p-3 rounded-lg overflow-y-scroll h-56">
|
||||
|
||||
<ol class="text-white list-decimal ml-3 p-2">
|
||||
<li class="p-1">The organizer of the Prize competition is stocknear.</li>
|
||||
<li class="p-1">Participation is free of charge.</li>
|
||||
<li class="p-1">
|
||||
All individuals worldwide who have reached the age of 18 and comply with the laws and regulations of their respective countries are eligible to participate. Persons involved in the conception and implementation of this prize competition are excluded - this explicitly does not apply to voluntary moderators or other voluntary members who have a special status on the platform.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The prerequisite for participation in the competition is the creation of a portfolio whereby the user can buy and sell shares. Each user starts with a play money of 100,000 $. To be considered in the final draw and eligible for any prizes, participants must make at least one trade (buy or sell shares) during the tournament period.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The prize competition will take place automated once a month for an indefinite period of time. The potential winners are drawn automatically, randomly on the first day of each month, whereby the chance of winning depends on the overall return of the portfolio. If a participant has collected too little return of the portfolio, the chance can also be 0 percent.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The prizes to be awarded may change from month to month.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
After the draw, the accounts of the winners will be checked for possible fraud attempts. If no abnormalities are found, the potential winners will be notified by email from stocknear.
|
||||
If any abnormalities are found, the draw will be repeated. The organizer reserves the right to refuse and reclaim prizes even after the fact and to permanently exclude participants from competitions in case of suspicion of attempted fraud or manipulation for their own benefit or the benefit of third parties as well as to temporarily or permanently block their accounts.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The potential winner must respond to the respective prize notification within 14 days,
|
||||
which will be sent via email, in order to claim the prize.
|
||||
Only a response via email will be considered.
|
||||
Otherwise, unclaimed prizes will be included in the next monthly draw.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The organizer points out that the availability and functionality of the prize draw cannot be guaranteed.
|
||||
The organizer is not responsible for entries not being included due to technical failures or other reasons.
|
||||
The prize competition may be terminated or removed due to external circumstances and constraints
|
||||
without any claims arising for the participants against the organizer.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The organizer reserves the right to change, discontinue, or suspend the game
|
||||
and the draw in whole or in part without prior notice in the event
|
||||
of unforeseen circumstances. These circumstances include, but are not limited to,
|
||||
the appearance of a computer virus, a program error, unauthorized intervention by third parties,
|
||||
or mechanical or technical problems beyond the control and influence of the organizer.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The organizer reserves the right to terminate the prize draw at any time without prior notice.
|
||||
In this case, any outstanding prizes will be properly awarded.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
Should individual provisions of these terms and conditions of participation be or become invalid,
|
||||
inadmissible, or unenforceable, this shall not affect the validity of the remaining terms
|
||||
and conditions. In place of the invalid, inadmissible, or unenforceable clause, provisions
|
||||
shall be deemed to have been agreed upon that come as close as possible to the economic objectives.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
Furthermore, the <a href="/imprint" class="text-blue-700 hover:underline">Imprint</a>, <a href="/terms-of-use" class="text-blue-700 hover:underline">
|
||||
Terms of Use
|
||||
</a> and our
|
||||
<a href="/privacy-policy" class="text-blue-700 hover:underline">
|
||||
Privacy Policy
|
||||
</a>
|
||||
apply.
|
||||
The applicable law and jurisdiction for this prize draw shall be determined based on the participant's
|
||||
country of residence. In case of any legal disputes, the competent courts of the participant's
|
||||
country of residence shall have exclusive jurisdiction.
|
||||
</li>
|
||||
<li class="p-1">
|
||||
The right of recourse to the courts is excluded.
|
||||
</li>
|
||||
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-5">
|
||||
<label class="label cursor-pointer flex flex-row">
|
||||
<input on:click={handleParticipation} checked={participationCheckbox} aria-describedby="helper-checkbox-text" type="checkbox" value="" class="w-6 h-6 bg-[#333333] border-slate-500 focus-none rounded ring-1">
|
||||
<span class="label-text text-white ml-3">I accept the Terms and Conditions of Participation
|
||||
in the monthly portfolio tournament.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<input on:click={handleAge} checked={ageCheckbox} aria-describedby="helper-checkbox-text" type="checkbox" value="" class="w-6 h-6 bg-[#333333] border-slate-500 focus-none rounded ring-1">
|
||||
<span class="label-text text-white ml-3 mr-auto">I'm at least 18 years old.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if ageCheckbox && participationCheckbox && !isClicked}
|
||||
<form
|
||||
on:submit={createPortfolio}
|
||||
class="w-full max-w-lg m-auto pb-8 mt-10"
|
||||
>
|
||||
<button type="submit" class="btn bg-blue-700 hover:bg-blue-600 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Create Portfolio
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="w-full max-w-lg m-auto pb-8 mt-10">
|
||||
<label class="opacity-[0.4] cursor-not-allowed btn bg-blue-700 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
{#if !isClicked}
|
||||
Create Portfolio
|
||||
{:else}
|
||||
<div class="flex flex-row m-auto">
|
||||
<span class="loading loading-infinity"></span>
|
||||
<span class="text-white ml-2">Loading</span>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<label for="addPortfolio" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
4
src/lib/components/AddToWatchlist.svelte
Normal file
4
src/lib/components/AddToWatchlist.svelte
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
<p class="text-md font-medium">Add to Watchlist</p>
|
||||
<svg class="w-10 h-10 mt-1.5 ml-auto" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" fill="#D6D6DC" stroke="#1A1A27"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>star-solid</title> <g id="Layer_2" data-name="Layer 2"> <g id="invisible_box" data-name="invisible box"> <rect width="48" height="48" fill="none"></rect> </g> <g id="icons_Q2" data-name="icons Q2"> <path d="M24,3a2.1,2.1,0,0,0-1.8,1.1L16.5,15.7,3.7,17.5A2.1,2.1,0,0,0,2.6,21l9.2,8.9L9.7,42.7A2,2,0,0,0,11.6,45l1-.2,11.4-6,11.4,6,1,.2a2,2,0,0,0,1.9-2.3L36.2,29.9,45.4,21a2.1,2.1,0,0,0-1.1-3.5L31.5,15.7,25.8,4.1A2.1,2.1,0,0,0,24,3Z"></path> </g> </g> </g></svg>
|
||||
304
src/lib/components/AnalystCard.svelte
Normal file
304
src/lib/components/AnalystCard.svelte
Normal file
@ -0,0 +1,304 @@
|
||||
<script lang='ts'>
|
||||
//import ProgressBar from 'progressbar.js';
|
||||
import {currentPortfolioPrice, stockTicker, screenWidth} from '$lib/store';
|
||||
import { abbreviateNumber } from '$lib/utils';
|
||||
|
||||
export let analystRating = {};
|
||||
|
||||
|
||||
let buyCount = 0;
|
||||
let holdCount = 0;
|
||||
let sellCount = 0;
|
||||
let priceTarget = 'n/a';
|
||||
let numOfAnalyst = 0;
|
||||
let consensusRating = 'n/a';
|
||||
let changesPercentage = 0;
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
if ($stockTicker && typeof window !== 'undefined' && typeof analystRating !== 'undefined' && Object?.keys(analystRating)?.length !== 0)
|
||||
{
|
||||
numOfAnalyst = analystRating?.numOfAnalyst;
|
||||
buyCount = (analystRating?.Buy/numOfAnalyst * 100)?.toFixed(2);
|
||||
holdCount = (analystRating?.Hold/numOfAnalyst * 100)?.toFixed(2);
|
||||
sellCount = (analystRating?.Sell/numOfAnalyst * 100)?.toFixed(2);
|
||||
priceTarget = analystRating?.priceTarget
|
||||
consensusRating = analystRating?.consensusRating;
|
||||
|
||||
try {
|
||||
changesPercentage = ((priceTarget/$currentPortfolioPrice -1)*100)?.toFixed(2) ?? 0;
|
||||
} catch(e) {
|
||||
changesPercentage = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Analyst Card -->
|
||||
<div class="space-y-3 sm:pt-5 hidden sm:block sm:{Object?.keys(analystRating)?.length !== 0 ? '' : 'hidden'}">
|
||||
<div class="rounded-2xl shadow-lg bg-[#000] sm:bg-[#202020] sm:border sm:border-slate-800 h-auto {$screenWidth < 640 ? 'w-screen pt-16' : ''} md:w-96 -mx-1 sm:mx-0">
|
||||
|
||||
<!--Start Content-->
|
||||
<div class="w-auto lg:w-full p-1 flex flex-col m-auto pb-14 sm:pb-10 px-2 sm:px-0">
|
||||
<h2 class="text-start text-2xl font-semibold text-white p-3 mt-3 ml-1">
|
||||
Analyst Rating
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col mt-5 w-full">
|
||||
<div class="flex flex-row m-auto w-full">
|
||||
<span class="text-start mr-auto ml-5 text-white font-medium text-xl">
|
||||
Signal
|
||||
</span>
|
||||
<span class="mr-5 text-white font-medium text-xl">
|
||||
Price Target
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-row m-auto w-full">
|
||||
{#if consensusRating === 'Buy' || consensusRating === 'Strong Buy'}
|
||||
<span class="text-start font-semibold text-[#10DB06] mr-auto ml-5 mt-2 text-xl">
|
||||
{consensusRating}
|
||||
</span>
|
||||
{:else if consensusRating === 'Sell' || consensusRating === 'Strong Sell' }
|
||||
<span class="text-start font-semibold text-[#FF2F1F] mr-auto ml-5 mt-2 text-xl">
|
||||
{consensusRating}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-start font-semibold text-[#FF2F1F] mr-auto ml-5 mt-2 text-xl">
|
||||
{consensusRating}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="mr-5 mt-2 font-semibold text-xl text-white">
|
||||
{#if priceTarget !== 'n/a'}
|
||||
${priceTarget}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white pl-4 pr-4 mt-6">
|
||||
{#if changesPercentage < 0 }
|
||||
The Stock Price has a downside of
|
||||
<span style="color: #FF2F1F; font-weight: 500">{abbreviateNumber(Math.abs(changesPercentage))}%</span>
|
||||
{:else if changesPercentage >= 0 }
|
||||
The Stock Price has an upside of
|
||||
<span style="color: #10DB06; font-weight: 500">{abbreviateNumber(Math.abs(changesPercentage))}%</span>
|
||||
{/if}
|
||||
based on <span style="font-weight: 600">{numOfAnalyst}</span> analysts in the past 12 months.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="mt-5 w-full rounded-full flex justify-center items-center mb-5">
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<!--Start Progress-->
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex flex-row items-center w-11/12 mt-5 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Buy
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{buyCount}%
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 [&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]" value={buyCount} max="100"></progress>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex flex-row items-center w-11/12 mt-5 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Hold
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{holdCount}%
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 [&::-webkit-progress-value]:bg-[#fff] [&::-moz-progress-bar]:bg-[#fff]" value={holdCount} max="100"></progress>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex flex-row items-center w-11/12 mt-5 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Sell
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{sellCount}%
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 [&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]" value={sellCount} max="100"></progress>
|
||||
</div>
|
||||
|
||||
|
||||
<!--End Progress-->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href={`/stocks/${$stockTicker}/analyst`} class="rounded-lg cursor-pointer w-11/12 py-2 h-full mt-6 text-lg text-center font-bold text-white m-auto hover:bg-[#3C74D4] bg-[#3C74D4] bg-opacity-[0.6]">
|
||||
Analyst Ratings
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--End Analyst Card-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Mobile Analyst Card-->
|
||||
<div class="space-y-3 sm:pt-5 sm:hidden">
|
||||
|
||||
<div class="bg-[#000] h-auto w-screen">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#202020] w-full p-1 flex flex-col items-center pb-5 h-auto rounded-b-[30px]">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
Analyst Rating
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
|
||||
<div class="flex flex-row justify-between items-center w-full mb-5">
|
||||
|
||||
|
||||
<div class="flex flex-col items-start ml-3 ">
|
||||
<span class="text-white text-lg font-medium inline-block">
|
||||
Recommendation:
|
||||
</span>
|
||||
<span class="text-white text-2xl font-medium inline-block">
|
||||
{#if consensusRating === 'Buy' || consensusRating === 'Strong Buy'}
|
||||
<span class="text-start font-medium text-[#10DB06] text-2xl">
|
||||
{consensusRating}
|
||||
</span>
|
||||
{:else if consensusRating === 'Sell' || consensusRating === 'Strong Sell' }
|
||||
<span class="text-start font-medium text-[#FF2F1F] text-2xl">
|
||||
{consensusRating}
|
||||
</span>
|
||||
{:else if consensusRating === 'Hold'}
|
||||
<span class="text-start font-medium text-[#FF2F1F] text-2xl">
|
||||
{consensusRating ?? 'n/a'}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-start font-medium text-white text-2xl">
|
||||
n/a
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col items-start mr-5">
|
||||
<span class="text-white ml-auto text-lg font-medium inline-block">
|
||||
Price Target:
|
||||
</span>
|
||||
<span class="text-white ml-auto text-2xl font-medium inline-block">
|
||||
{#if priceTarget !== 'n/a'}
|
||||
${priceTarget}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{#if changesPercentage < 0 }
|
||||
<div class="text-white p-4 text-center">
|
||||
The Stock Price has a downside of
|
||||
<span class="text-[#FF2F1F] font-medium">{abbreviateNumber(Math?.abs(changesPercentage))} %</span>
|
||||
based on <span class="font-semibold">{numOfAnalyst}</span> analysts.
|
||||
</div>
|
||||
{:else if changesPercentage >= 0 }
|
||||
<div class="text-white p-4 text-center">
|
||||
The Stock Price has an upside of
|
||||
<span class="text-[#10DB06] font-medium">{abbreviateNumber(Math?.abs(changesPercentage))} %</span>
|
||||
based on <span class="font-semibold">{numOfAnalyst}</span> analysts.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
{#if numOfAnalyst !== 0}
|
||||
<div class="mt-5 flex flex-col m-auto items-center rounded-lg w-full mb-16 p-3">
|
||||
|
||||
|
||||
<div class="shadow-lg bg-[#202020] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Buy
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{buyCount}%
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-full {buyCount >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={buyCount} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-lg bg-[#202020] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Hold
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{holdCount}%
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-full {holdCount >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={holdCount} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-lg bg-[#202020] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Sell
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{sellCount}
|
||||
</div>
|
||||
<progress class="progress w-full {sellCount >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={sellCount} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" mt-20 flex justify-center items-center text-3xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Mobile Analyst Card-->
|
||||
|
||||
312
src/lib/components/AnalystEstimate.svelte
Normal file
312
src/lib/components/AnalystEstimate.svelte
Normal file
@ -0,0 +1,312 @@
|
||||
<script lang='ts'>
|
||||
import {screenWidth} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
|
||||
import { LayerCake, Html } from 'layercake';
|
||||
|
||||
import Scatter from '$lib/components/Scatter//Scatter.html.svelte';
|
||||
import AxisX from '$lib/components/Scatter//AxisX.html.svelte';
|
||||
import AxisY from '$lib/components/Scatter//AxisY.html.svelte';
|
||||
|
||||
|
||||
export let analystEstimateList;
|
||||
export let data;
|
||||
|
||||
let deactivateContent = data?.user?.tier === 'Pro' ? false : true;
|
||||
|
||||
let dataset = [];
|
||||
let xData = [];
|
||||
|
||||
let displayData = 'Revenue';
|
||||
let displayRevenueUnit = 'Billions';
|
||||
let displayNetIncomeUnit = 'Billions';
|
||||
let displayEBITDAUnit = 'Billions';
|
||||
|
||||
|
||||
function changeStatement(event)
|
||||
{
|
||||
displayData = event.target.value;
|
||||
}
|
||||
|
||||
|
||||
function determineDisplayUnit(value) {
|
||||
if (Math?.abs(value) >= 1e10) {
|
||||
return { unit: '100 Billions', denominator: 10e10 };
|
||||
} else if (Math?.abs(value) >= 1e9) {
|
||||
return { unit: '10 Billions', denominator: 1e10 };
|
||||
} else if (Math?.abs(value) >= 1e7) {
|
||||
return { unit: 'Billions', denominator: 1e9 };
|
||||
} else if (Math?.abs(value) >= 1e5) {
|
||||
return { unit: 'Millions', denominator: 1e6 };
|
||||
} else {
|
||||
return { unit: '', denominator: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
const xKey = 'FY';
|
||||
const yKey = 'val';
|
||||
const r = 5;
|
||||
const padding = 2.5;
|
||||
|
||||
let tableDataActual = [];
|
||||
let tableDataForecast = []
|
||||
|
||||
//To-do: Optimize this piece of shit
|
||||
$: {
|
||||
if (displayData && analystEstimateList?.length !== 0)
|
||||
{
|
||||
dataset = [];
|
||||
tableDataActual = [];
|
||||
tableDataForecast = [];
|
||||
|
||||
xData = analystEstimateList?.slice(-7)?.map(({ date }) => Number(String(date)?.slice(-2)));
|
||||
|
||||
if(displayData === 'Revenue') {
|
||||
|
||||
const { unit, denominator } = determineDisplayUnit(analystEstimateList?.at(-1)?.estimatedRevenueAvg);
|
||||
displayRevenueUnit = unit;
|
||||
|
||||
analystEstimateList?.slice(-7)?.forEach(item => {
|
||||
|
||||
tableDataActual?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.revenue/ denominator)?.toFixed(2)});
|
||||
tableDataForecast?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.estimatedRevenueAvg/denominator)?.toFixed(2)});
|
||||
|
||||
if (item?.revenue !== null) {
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.revenue/denominator)?.toFixed(2), 'dataset': 'actual' });
|
||||
}
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.estimatedRevenueAvg !== null ? (item?.estimatedRevenueAvg / denominator)?.toFixed(2) : null, 'dataset': 'forecast' });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
else if(displayData === 'Net Income') {
|
||||
|
||||
const { unit, denominator } = analystEstimateList?.at(-2)?.estimatedNetIncomeAvg !== 0 ? determineDisplayUnit(analystEstimateList?.at(-2)?.estimatedNetIncomeAvg) : determineDisplayUnit(analystEstimateList?.at(-1)?.netIncome);
|
||||
displayNetIncomeUnit = unit;
|
||||
|
||||
analystEstimateList?.slice(-7)?.forEach(item => {
|
||||
|
||||
tableDataActual?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.netIncome / denominator)?.toFixed(2)});
|
||||
tableDataForecast?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.estimatedNetIncomeAvg / denominator)?.toFixed(2)});
|
||||
|
||||
if (item?.netIncome !== null) {
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.netIncome / denominator)?.toFixed(2), 'dataset': 'actual' });
|
||||
}
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.estimatedNetIncomeAvg !== null ? (item?.estimatedNetIncomeAvg / denominator)?.toFixed(2) : null, 'dataset': 'forecast' });
|
||||
});
|
||||
}
|
||||
|
||||
else if(displayData === 'EPS') {
|
||||
analystEstimateList?.slice(-7)?.forEach(item => {
|
||||
|
||||
tableDataActual?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.eps?.toFixed(2) ?? null});
|
||||
tableDataForecast?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.estimatedEpsAvg?.toFixed(2)});
|
||||
if (item?.eps !== null) {
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.eps, 'dataset': 'actual' });
|
||||
}
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.estimatedEpsAvg !== null ? item?.estimatedEpsAvg : null, 'dataset': 'forecast' });
|
||||
});
|
||||
}
|
||||
|
||||
else if(displayData === 'EBITDA') {
|
||||
const { unit, denominator } = determineDisplayUnit(analystEstimateList?.at(-1)?.estimatedEbitdaAvg);
|
||||
displayEBITDAUnit = unit;
|
||||
|
||||
analystEstimateList?.slice(-7)?.forEach(item => {
|
||||
|
||||
tableDataActual?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.ebitda/ denominator)?.toFixed(2)});
|
||||
tableDataForecast?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.estimatedEbitdaAvg/ denominator)?.toFixed(2)});
|
||||
|
||||
if (item?.ebitda !== null) {
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': (item?.ebitda / denominator)?.toFixed(2), 'dataset': 'actual' });
|
||||
}
|
||||
dataset?.push({ 'FY': Number(String(item?.date)?.slice(-2)), 'val': item?.estimatedEbitdaAvg !== null ? (item?.estimatedEbitdaAvg / denominator)?.toFixed(2) : null, 'dataset': 'forecast' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden text-white h-full sm:mb-0">
|
||||
<div class="flex justify-center w-fit m-auto h-full overflow-hidden">
|
||||
<div class="relative flex justify-center items-center overflow-hidden">
|
||||
<main>
|
||||
<div class="w-fit sm:w-full sm:max-w-2xl m-auto mt-5 sm:mt-0">
|
||||
|
||||
|
||||
{#if analystEstimateList?.length !== 0}
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="predictiveFundamentalsInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Predictive Fundamentals
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Predictive Fundamentals"}
|
||||
content={`If quarterly earnings for a year are incomplete, we offer a summarized view based on available data. For instance, if the Q4 report is missing, we display revenue as X, reflecting the sum of Q1-Q3 only. Q4 data will be added later when available.`}
|
||||
id={"predictiveFundamentalsInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full sm:w-5/6">
|
||||
We analyze insights from various analysts to offer both historical and future fundamental data forecasts.
|
||||
</div>
|
||||
|
||||
|
||||
<select class="mt-5 mb-5 sm:mb-0 sm:mt-3 ml-1 w-44 select select-bordered select-sm p-0 pl-5 overflow-y-auto bg-[#2A303C]" on:change={changeStatement}>
|
||||
<option disabled>Choose Fundamental Data</option>
|
||||
<option disabled={deactivateContent} value="EPS">
|
||||
{!deactivateContent ? 'EPS' : 'EPS (Pro Only)'}
|
||||
</option>
|
||||
<option disabled={deactivateContent} value="EBITDA">
|
||||
{!deactivateContent ? `EBITDA in ${displayEBITDAUnit}` : 'EBITDA (Pro Only)'}
|
||||
</option>
|
||||
<option value="Net Income">
|
||||
Net Income in {displayNetIncomeUnit}
|
||||
</option>
|
||||
<option value="Revenue" selected>Revenue in {displayRevenueUnit}</option>
|
||||
</select>
|
||||
|
||||
|
||||
<div class="flex flex-row items-center ml-2 sm:ml-0 justify-start w-[90vw] sm:w-[560px] h-[220px] sm:h-[250px] sm:pl-3 sm:pr-3 pt-4 pb-5 mt-5 sm:mt-10">
|
||||
|
||||
|
||||
<div class="chart-container h-[250px] ">
|
||||
<LayerCake
|
||||
ssr={true}
|
||||
percentRange={true}
|
||||
padding={{ top: 0, right: 0, bottom: 30, left: 0 }}
|
||||
x={xKey}
|
||||
y={yKey}
|
||||
xPadding={[padding, padding]}
|
||||
yPadding={[padding, padding]}
|
||||
data={dataset}
|
||||
position={'relative'}
|
||||
|
||||
>
|
||||
|
||||
<Html>
|
||||
<AxisX ticks={xData}/>
|
||||
<AxisY/>
|
||||
<Scatter
|
||||
{r}
|
||||
/>
|
||||
</Html>
|
||||
|
||||
</LayerCake>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-row items-center justify-between m-auto mt-10">
|
||||
<div class="flex flex-row items-center w-1/2 sm:w-full justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#00BBFF] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-sm sm:text-md sm:font-medium inline-block">
|
||||
Analyst Forecast
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center w-1/2 sm:w-full justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#0FC008] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-sm sm:text-md sm:font-medium inline-block">
|
||||
Actual
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="no-scrollbar flex justify-start items-center w-screen sm:w-full mt-6 m-auto rounded-none sm:rounded-lg overflow-hidden overflow-x-scroll pr-10">
|
||||
<table class="table table-sm shaodow table-pin-cols table-compact rounded-none sm:rounded-md w-full bg-[#0F0F0F] border-bg-[#0F0F0F]">
|
||||
<thead class="">
|
||||
<tr class="">
|
||||
<th class="bg-[#0F0F0F] border-b border-[#000] shadow-md text-white font-medium text-sm text-start">Year</th>
|
||||
{#each ($screenWidth >= 640 ? xData?.slice(-6) : xData) as item}
|
||||
<td class="z-20 bg-[#0F0F0F] border-b border-[#000] shadow-md text-white font-medium text-sm text-center bg-[#0F0F0F]">{'FY'+item}</td>
|
||||
{/each}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="shadow-md">
|
||||
|
||||
<tr class="bg-[#0F0F0F] border-b-[#0F0F0F]">
|
||||
<th class="text-white text-start font-medium bg-[#0F0F0F] border-b border-[#0F0F0F]">
|
||||
Forecast
|
||||
</th>
|
||||
{#each ($screenWidth >= 640 ? tableDataForecast?.slice(-6) : tableDataForecast) as item}
|
||||
<td class="text-white text-center font-medium border-b border-[#0F0F0F]">
|
||||
{(item?.val === '0.00' || item?.val === null) ? '-' : item?.val}
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
</tr>
|
||||
|
||||
<tr class="bg-[#0F0F0F] border-b-[#0F0F0F]">
|
||||
<th class="text-white text-start font-medium bg-[#0F0F0F] border-b border-[#0F0F0F]">
|
||||
Actual
|
||||
</th>
|
||||
{#each ($screenWidth >= 640 ? tableDataActual?.slice(-6) : tableDataActual) as item}
|
||||
<td class="text-white text-center font-medium border-b border-[#0F0F0F]">
|
||||
{(item?.val === '0.00' || item?.val === null) ? '-' : item?.val}
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class=" mt-10 justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
/*
|
||||
The wrapper div needs to have an explicit width and height in CSS.
|
||||
It can also be a flexbox child or CSS grid element.
|
||||
The point being it needs dimensions since the <LayerCake> element will
|
||||
expand to fill it.
|
||||
*/
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
28
src/lib/components/BottomNavigation.svelte
Normal file
28
src/lib/components/BottomNavigation.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<script lang='ts'>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
|
||||
export let data;
|
||||
|
||||
|
||||
function handleClick() {
|
||||
const label = document.getElementById('animated-label');
|
||||
label.classList.add('animate-bounce');
|
||||
|
||||
goto('/community/create-post');
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div class="sm:hidden fixed z-50 w-full h-16 max-w-3xl -right-5 bottom-5">
|
||||
<div class="h-full max-w-3xl mx-auto">
|
||||
<div class="flex items-center justify-end mr-10">
|
||||
<label id="animated-label" on:click={handleClick} class="inline-flex items-center justify-center w-14 h-14 font-medium bg-[#3C74D4] bg-opacity-[0.8] rounded-full cursor-pointer">
|
||||
<svg class="w-4 h-4 text-white inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="white" d="m362.7 19.3l-48.4 48.4l130 130l48.4-48.4c25-25 25-65.5 0-90.5l-39.4-39.5c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2c-2.5 8.5-.2 17.6 6 23.8s15.3 8.5 23.7 6.1L151 475.7c14.1-4.2 27-11.8 37.4-22.2l233.3-233.2l-130-130z"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
107
src/lib/components/BullBearSay.svelte
Normal file
107
src/lib/components/BullBearSay.svelte
Normal file
@ -0,0 +1,107 @@
|
||||
<script lang='ts'>
|
||||
import { stockTicker, displayCompanyName } from "$lib/store";
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
export let marketMoods = {};
|
||||
|
||||
let rawData = marketMoods;
|
||||
let mode = 'bullish';
|
||||
let showFullText = false;
|
||||
// Function to split text into paragraphs
|
||||
|
||||
function splitIntoParagraphs(text) {
|
||||
// Split the text into paragraphs based on periods followed by spaces, excluding numbers with decimals
|
||||
return text?.replace('U.S.', 'US')?.split(/(?<!\d\.\d)(?<!vs)\.\s/);
|
||||
}
|
||||
|
||||
let paragraphs = splitIntoParagraphs(rawData?.bullSays);
|
||||
|
||||
function changeMode(state:string) {
|
||||
mode = state;
|
||||
if(mode === 'bullish') {
|
||||
paragraphs = splitIntoParagraphs(rawData?.bullSays);
|
||||
}
|
||||
else if (mode === 'bearish') {
|
||||
paragraphs = splitIntoParagraphs(rawData?.bearSays);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined') {
|
||||
rawData = marketMoods;
|
||||
mode = 'bullish';
|
||||
showFullText = false;
|
||||
paragraphs = splitIntoParagraphs(rawData?.bullSays);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{#if Object?.keys(marketMoods)?.length !== 0}
|
||||
|
||||
<div class="space-y-3 overflow-hidden">
|
||||
<!--Start Content-->
|
||||
<div class="w-auto lg:w-full p-1 flex flex-col m-auto">
|
||||
|
||||
<div class="flex flex-col items-center w-full mb-6">
|
||||
<div class="flex flex-row justify-start mr-auto items-center">
|
||||
<!--<img class="h-10 inline-block mr-2" src={copilotIcon} />-->
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="pricePredictionInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Market Moods
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Market Moods"}
|
||||
content={`Before investing, examine both perspectives. We offer brief analyst report summaries, highlighting both positive ("Bulls Say") and negative ("Bears Say") viewpoints on ${$displayCompanyName}`}
|
||||
id={"pricePredictionInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--Start Header-->
|
||||
<div role="tablist" class="-mt-2 w-11/12 sm:w-56">
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="tabs flex flex-row justify-start items-center w-full">
|
||||
<button class="w-fit text-[0.9rem] sm:text-[1rem] mr-10 rounded-md transition font-medium hover:text-white {mode === 'bullish' ? ' text-white' : 'text-[#9A9996]'}" on:click={() => (changeMode('bullish'))} >
|
||||
Bulls Say
|
||||
<div class="{mode === 'bullish' ? 'bg-[#75D377]' : 'bg-[#0F0F0F]'} mt-1 h-[3px] rounded-full w-[4rem] rounded-full" />
|
||||
</button>
|
||||
|
||||
<button class="w-fit text-[0.9rem] sm:text-[1rem] sm:mr-10 rounded-md transition font-medium hover:text-white {mode === 'bearish' ? ' text-white' : 'text-[#9A9996]'}" on:click={() => (changeMode('bearish'))} >
|
||||
Bears Say
|
||||
<div class="{mode === 'bearish' ? 'bg-[#FF2F1F]' : 'bg-[#0F0F0F]'} mt-1 h-[3px] rounded-full w-[4rem] rounded-full" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!--End Header-->
|
||||
<span class="text-gray-200 text-xs sm:text-[0.85rem] italic mt-6 sm:ml-auto">
|
||||
Updated {rawData?.date}
|
||||
</span>
|
||||
<div class="flex mt-5 h-auto">
|
||||
|
||||
<div class="{mode === 'bullish' ? 'bg-[#10DB06]' : 'bg-[#FF2F1F]'} w-3.5 rounded-l-xl" />
|
||||
<span class="text-gray-100 ml-3 text-sm ">
|
||||
{#if showFullText}
|
||||
{#each (showFullText ? paragraphs : paragraphs?.slice(0,1)) as paragraph, index}
|
||||
<p class="{index !== 0 ? 'mt-1' : ''} pr-1">{paragraph} {paragraphs?.length <= index+1 ? '' : '.'}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
{paragraphs?.at(0)?.slice(0,250) + '...'}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label on:click={() => showFullText = !showFullText} class="cursor-pointer m-auto flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullText ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
532
src/lib/components/BuyTrade.svelte
Normal file
532
src/lib/components/BuyTrade.svelte
Normal file
@ -0,0 +1,532 @@
|
||||
<script lang='ts'>
|
||||
import toast from 'svelte-french-toast';
|
||||
import {userRegion, screenWidth, currentPortfolioPrice, displayCompanyName, traded, stockTicker, etfTicker, cryptoTicker, assetType} from '$lib/store';
|
||||
|
||||
export let data;
|
||||
export let availableCash;
|
||||
export let holdingShares;
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let fastifyURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let estimatedTotal = 0;
|
||||
let numberOfShares = 0;
|
||||
let commissionPrice = 0;
|
||||
|
||||
let displayTab = 'changeOrder';
|
||||
|
||||
function handleMaxOrder()
|
||||
{
|
||||
if ($currentPortfolioPrice > availableCash)
|
||||
{
|
||||
toast.error(`Not enough money to buy ${$stockTicker} shares`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
numberOfShares = Math.floor(availableCash/$currentPortfolioPrice);
|
||||
estimatedTotal = (numberOfShares * $currentPortfolioPrice);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleInputChange(event) {
|
||||
const inputValue = event.target.value;
|
||||
|
||||
if (!isNaN(inputValue) && Number(inputValue) >= 0) {
|
||||
numberOfShares = Number(inputValue);
|
||||
} else {
|
||||
numberOfShares = 0;
|
||||
}
|
||||
|
||||
estimatedTotal = numberOfShares * $currentPortfolioPrice;
|
||||
}
|
||||
|
||||
function handleAppendNumber(num) {
|
||||
// Append the clicked number to the current numberOfShares
|
||||
numberOfShares = numberOfShares * 10 + num;
|
||||
}
|
||||
|
||||
function handleRemoveNumber() {
|
||||
// Remove the last digit from numberOfShares or set to zero if NaN
|
||||
const numStr = numberOfShares.toString();
|
||||
numberOfShares = numStr.length > 1 ? parseInt(numStr.slice(0, -1), 10) : 0;
|
||||
}
|
||||
|
||||
|
||||
function changeTab() {
|
||||
|
||||
if(displayTab === 'changeOrder' && numberOfShares > 0 && estimatedTotal <= availableCash)
|
||||
{
|
||||
displayTab = 'buyOrder';
|
||||
|
||||
}
|
||||
|
||||
else if (displayTab === 'buyOrder')
|
||||
{
|
||||
displayTab = 'changeOrder';
|
||||
}
|
||||
|
||||
else {
|
||||
toast.error('Please check your Input values again.', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBuyOrder()
|
||||
{
|
||||
|
||||
const postData = {
|
||||
'userId': data?.user?.id,
|
||||
'symbol': $assetType === 'stock' ? $stockTicker : $assetType === 'etf' ? $etfTicker : $cryptoTicker,
|
||||
'assetType': $assetType,
|
||||
'name': $displayCompanyName,
|
||||
'numberOfShares': numberOfShares,
|
||||
'estimatedTotal': estimatedTotal,
|
||||
'boughtPrice': $currentPortfolioPrice,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Make the POST request to the endpoint
|
||||
const response = await fetch(fastifyURL+'/buy-stock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
const output = (await response.json())?.items;
|
||||
|
||||
|
||||
if (output === 'success')
|
||||
{
|
||||
|
||||
toast.success(`Successfully bought ${numberOfShares} ${$stockTicker} shares`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
|
||||
numberOfShares = 0;
|
||||
estimatedTotal = 0;
|
||||
displayTab = 'changeOrder';
|
||||
|
||||
$traded = true;
|
||||
|
||||
const closePopup = document.getElementById("buyTradeModal");
|
||||
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
|
||||
}
|
||||
else if (output === 'failure')
|
||||
{
|
||||
toast.error(`Something went wrong. Please try again.`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
else if (output === 'marketClosed')
|
||||
{
|
||||
toast.error(`The market is closed. Please try again later.`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if(numberOfShares)
|
||||
{
|
||||
estimatedTotal = (numberOfShares * $currentPortfolioPrice);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||
</svelte:head>
|
||||
|
||||
{#if $screenWidth >= 640}
|
||||
|
||||
<!--Start Trade Modal-->
|
||||
<input type="checkbox" id="buyTradeModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="buyTradeModal" class="modal modal-bottom sm:modal-middle ">
|
||||
|
||||
|
||||
<label for="buyTradeModal" class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.08]"></label>
|
||||
|
||||
|
||||
<div class="modal-box rounded-none w-full bg-[#000] h-[500px]" >
|
||||
|
||||
<!--Start Trade Modal-->
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
{#if displayTab === 'changeOrder'}
|
||||
|
||||
<div class= "flex flex-col">
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#202020] mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<span class="mb-1">
|
||||
{$displayCompanyName?.length > 35 ? $displayCompanyName?.slice(0, 35) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
<span class="mb-1 text-sm font-medium">
|
||||
Current Price: ${$currentPortfolioPrice}
|
||||
</span>
|
||||
</div>
|
||||
{#if holdingShares !== 0}
|
||||
<span class="text-blue-400 text-sm ml-auto font-medium">
|
||||
Holding Shares: {holdingShares}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
|
||||
<div class="flex flex-col sm:w-64">
|
||||
<label class="label font-medium">
|
||||
<span class="text-white label-text">Number of Shares</span>
|
||||
</label>
|
||||
|
||||
<input bind:value={numberOfShares} on:input={handleInputChange} type="text" placeholder="Number of Shares" class="text-slate-200 input {estimatedTotal > availableCash ? 'input-error' : ''} bg-gray-900 input-md w-54 sm:w-full max-w-xs" />
|
||||
</div>
|
||||
<label on:click={handleMaxOrder} class="mt-9 btn bg-[#000] hover:bg-[#fff] hover:text-black text-white cursor-pointer sm:px-5 border rounded-lg">
|
||||
Show Max
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="{estimatedTotal < availableCash ? 'hidden' : ''} label-text-alt text-error mt-1">
|
||||
Not enough money to buy the amount
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-8 w-full">
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 font-medium text-sm">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Available Cash:
|
||||
</span>
|
||||
<span>
|
||||
{availableCash?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 text-sm">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Estimated Total:
|
||||
</span>
|
||||
<span>
|
||||
{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="w-5/6 max-w-lg pt-10 pb-20 m-auto">
|
||||
<button on:click={changeTab} class="btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Preview Buy Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if displayTab === 'buyOrder'}
|
||||
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-gray-900 mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<span class="mb-1 font-medium text-xl">
|
||||
Buying {numberOfShares} shares of {$displayCompanyName?.length > 35 ? $displayCompanyName?.slice(0, 35) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Price per Share</td>
|
||||
<td class="bg-[#000] whitespace-normal">${$currentPortfolioPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Number of Shares</td>
|
||||
<td class="bg-[#000] whitespace-normal">{numberOfShares}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Commission</td>
|
||||
<td class="bg-[#000] whitespace-normal">${commissionPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Estimated Total</td>
|
||||
<td class="bg-[#000] whitespace-normal">{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Available Cash After</td>
|
||||
<td class="bg-[#000] whitespace-normal">{(availableCash-estimatedTotal-commissionPrice)?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="max-w-lg pt-10 m-auto pb-5 flex flex-row justify-center items-center">
|
||||
<button on:click={changeTab} class="w-3/4 mr-8 btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md rounded-lg text-white font-bold text-md">
|
||||
Change Order
|
||||
</button>
|
||||
|
||||
<button on:click={handleBuyOrder} class="w-3/4 btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md rounded-lg text-white font-bold text-md">
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--End Trade Modal-->
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<!--Start Drawer Sidewise for mobile-->
|
||||
|
||||
|
||||
<div class="drawer drawer-end z-40">
|
||||
<input id="buyTradeModal" type="checkbox" class="drawer-toggle"/>
|
||||
|
||||
<div class="drawer-side">
|
||||
|
||||
|
||||
<div class="modal-box overflow-hidden rounded-xl bg-[#000] min-h-screen w-screen" >
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col w-full mt-14">
|
||||
|
||||
{#if displayTab === 'changeOrder'}
|
||||
|
||||
<div class= "flex flex-col">
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#202020] mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<span class="mb-1">
|
||||
{$displayCompanyName?.length > 35 ? $displayCompanyName?.slice(0, 35) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
<span class="mb-1 text-sm font-medium">
|
||||
Current Price: ${$currentPortfolioPrice}
|
||||
</span>
|
||||
</div>
|
||||
{#if holdingShares !== 0}
|
||||
<span class="text-blue-400 text-sm ml-auto font-medium">
|
||||
Holding Shares: {holdingShares}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
|
||||
<div class="flex flex-col sm:w-64">
|
||||
<label class="label font-medium">
|
||||
<span class="text-white label-text">Number of Shares</span>
|
||||
</label>
|
||||
|
||||
<input inputmode="tel" pattern="[0-9]*" bind:value={numberOfShares} on:input={handleInputChange} type="text" placeholder="Number of Shares" class="text-slate-200 input {estimatedTotal > availableCash ? 'input-error' : ''} bg-gray-900 input-md w-54 sm:w-full max-w-xs" />
|
||||
</div>
|
||||
<label on:click={handleMaxOrder} class="mt-9 btn bg-[#000] hover:bg-[#fff] hover:text-black text-white cursor-pointer sm:px-5 border rounded-lg">
|
||||
Show Max
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="{estimatedTotal < availableCash ? 'hidden' : ''} label-text-alt text-error mt-1">
|
||||
Not enough money to buy the amount
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-8 w-full">
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 font-medium text-sm">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Available Cash:
|
||||
</span>
|
||||
<span>
|
||||
{availableCash?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 text-sm">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Estimated Total:
|
||||
</span>
|
||||
<span>
|
||||
{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
<div class="grid grid-cols-3 gap-x-6 gap-y-2 text-white font-bold text-lg m-auto mt-14">
|
||||
{#each Array.from({ length: 10 }, (_, index) => index+1) as num}
|
||||
{#if num < 10}
|
||||
<label on:click={() => handleAppendNumber(num)} class="rounded-full flex justify-center items-center w-16 h-16">
|
||||
{num}
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="rounded-full flex justify-center items-center w-16 h-16">
|
||||
</div>
|
||||
<div class="rounded-full flex justify-center items-center w-16 h-16 hover:bg-gray-600 hover:bg-opacity-[0.4]">
|
||||
0
|
||||
</div>
|
||||
<label on:click={() => handleRemoveNumber()} class="rounded-full flex justify-center items-center w-16 h-16 hover:bg-gray-600 hover:bg-opacity-[0.4]">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(-90 12 12)"><path fill="white" d="M13 7.828V20h-2V7.828l-5.364 5.364l-1.414-1.414L12 4l7.778 7.778l-1.414 1.414L13 7.828Z"/></g></svg>
|
||||
</label>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="w-5/6 max-w-lg pt-16 pb-10 m-auto">
|
||||
<button on:click={changeTab} class="btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Preview Buy Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if displayTab === 'buyOrder'}
|
||||
|
||||
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-gray-900 mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<span class="mb-1 font-medium text-xl">
|
||||
Buying {numberOfShares} shares of {$displayCompanyName?.length > 35 ? $displayCompanyName?.slice(0, 35) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Price per Share</td>
|
||||
<td class="bg-[#000] whitespace-normal">${$currentPortfolioPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Number of Shares</td>
|
||||
<td class="bg-[#000] whitespace-normal">{numberOfShares}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Commission</td>
|
||||
<td class="bg-[#000] whitespace-normal">${commissionPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Estimated Total</td>
|
||||
<td class="bg-[#000] whitespace-normal">{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Available Cash After</td>
|
||||
<td class="bg-[#000] whitespace-normal">{(availableCash-estimatedTotal-commissionPrice)?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="w-5/6 max-w-lg pt-10 m-auto pb-5 flex flex-col items-center">
|
||||
<button on:click={changeTab} class="btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Change Order
|
||||
</button>
|
||||
|
||||
<button on:click={handleBuyOrder} class="mt-6 btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Buy Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<label for="buyTradeModal" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
65
src/lib/components/Chart.svelte
Normal file
65
src/lib/components/Chart.svelte
Normal file
@ -0,0 +1,65 @@
|
||||
<script lang="ts" context="module">
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export { echarts }
|
||||
|
||||
|
||||
|
||||
export type EChartsOptions = echarts.EChartsOption
|
||||
export type EChartsTheme = string | object
|
||||
export type EChartsRenderer = 'canvas' | 'svg'
|
||||
|
||||
export type ChartOptions = {
|
||||
theme?: EChartsTheme
|
||||
renderer?: EChartsRenderer
|
||||
options: EChartsOptions
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Partial<ChartOptions> = {
|
||||
theme: undefined,
|
||||
renderer: 'canvas',
|
||||
}
|
||||
|
||||
export function chartable(element: HTMLElement, echartOptions: ChartOptions) {
|
||||
const { theme, renderer, options } = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...echartOptions,
|
||||
}
|
||||
const echartsInstance = echarts.init(element, theme, { renderer })
|
||||
echartsInstance.setOption(options)
|
||||
|
||||
function handleResize() {
|
||||
echartsInstance.resize()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
echartsInstance.dispose()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
},
|
||||
update(newOptions: ChartOptions) {
|
||||
echartsInstance.setOption({
|
||||
...echartOptions.options,
|
||||
...newOptions.options,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let options: echarts.EChartsOption
|
||||
export let { theme, renderer } = DEFAULT_OPTIONS
|
||||
|
||||
</script>
|
||||
|
||||
<div class="chart" use:chartable={{ renderer, theme, options }} />
|
||||
|
||||
<style>
|
||||
.chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
187
src/lib/components/Circle/Circle.html.svelte
Normal file
187
src/lib/components/Circle/Circle.html.svelte
Normal file
@ -0,0 +1,187 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an HTML circle pack chart using [d3-hierarchy](https://github.com/d3/d3-hierarchy).
|
||||
-->
|
||||
<script>
|
||||
import { stratify, pack, hierarchy } from 'd3-hierarchy'
|
||||
import { getContext } from 'svelte';
|
||||
import { format } from 'd3-format';
|
||||
|
||||
const { width, height, data } = getContext('LayerCake');
|
||||
|
||||
/** @type {String} [idKey='id'] - The key on each object where the id value lives. */
|
||||
export let idKey = 'id';
|
||||
|
||||
/** @type {String} [parentKey] - Set this if you want to define one parent circle. This will give you a [nested](https://layercake.graphics/example/CirclePackNested) graphic versus a [grouping of circles](https://layercake.graphics/example/CirclePack). */
|
||||
export let parentKey = undefined;
|
||||
|
||||
/** @type {String} [valueKey='value'] - The key on each object where the data value lives. */
|
||||
export let valueKey = 'value';
|
||||
|
||||
/** @type {Function} [labelVisibilityThreshold=r => r > 25] - By default, only show the text inside a circle if its radius exceeds a certain size. Provide your own function for different behavior. */
|
||||
export let labelVisibilityThreshold = r => r > 25;
|
||||
|
||||
|
||||
/** @type {String} [stroke='#999'] - The circle's stroke color. */
|
||||
export let stroke = '#000';
|
||||
|
||||
|
||||
/** @type {String} [textColor='#333'] - The label text color. */
|
||||
export let textColor = '#000';
|
||||
|
||||
/** @type {String} [textStroke='#000'] - The label text's stroke color. */
|
||||
export let textStroke = '#000';
|
||||
|
||||
/** @type {Number} [textStrokeWidth=0] - The label text's stroke width, in pixels. */
|
||||
export let textStrokeWidth = 0;
|
||||
|
||||
/** @type {Function} [sortBy=(a, b) => b.value - a.value] - The order in which circle's are drawn. Sorting on the `depth` key is also a popular choice. */
|
||||
export let sortBy = (a, b) => b.value - a.value; // 'depth' is also a popular choice
|
||||
|
||||
/** @type {Number} [spacing=0] - Whitespace padding between each circle, in pixels. */
|
||||
export let spacing = 0;
|
||||
|
||||
/* --------------------------------------------
|
||||
* This component will automatically group your data
|
||||
* into one group if no `parentKey` was passed in.
|
||||
* Stash $data here so we can add our own parent
|
||||
* if there's no `parentKey`
|
||||
*/
|
||||
let parent = {};
|
||||
$: dataset = $data;
|
||||
|
||||
$: if (parentKey === undefined) {
|
||||
parent = { [idKey]: 'all' };
|
||||
dataset = [...dataset, parent]
|
||||
}
|
||||
|
||||
$: stratifier = stratify()
|
||||
.id(d => d[idKey])
|
||||
.parentId(d => {
|
||||
if (d[idKey] === parent[idKey]) return '';
|
||||
return d[parentKey] || parent[idKey];
|
||||
});
|
||||
|
||||
$: packer = pack()
|
||||
.size([$width, $height])
|
||||
.padding(spacing);
|
||||
|
||||
$: stratified = stratifier(dataset);
|
||||
|
||||
$: root = hierarchy(stratified)
|
||||
.sum((d, i) => {
|
||||
return d.data[valueKey] || 1;
|
||||
})
|
||||
.sort(sortBy);
|
||||
|
||||
$: packed = packer(root);
|
||||
|
||||
$: descendants = packed.descendants();
|
||||
|
||||
|
||||
const titleCase = d => d.replace(/^\w/, w => w.toUpperCase());
|
||||
const commas = format(',');
|
||||
</script>
|
||||
|
||||
<div class="circle-pack" data-has-parent-key="{parentKey !== undefined}">
|
||||
{#each descendants as d, index}
|
||||
<div
|
||||
class="circle-group"
|
||||
data-id="{d.data.id}"
|
||||
data-visible="{labelVisibilityThreshold(d.r)}"
|
||||
>
|
||||
<div
|
||||
class="circle"
|
||||
style="left:{d.x}px;top:{d.y}px;width:{d.r * 2}px;height:{d.r * 2}px;background-color:{(index === 1 && d.data.id === 'puts') ? '#FF2F1F' : (index === 1 && d.data.id === 'calls') ? '#0FB307' : '#1E1E1E'}; border: 0px solid #000;"
|
||||
/>
|
||||
<div
|
||||
class="text-group"
|
||||
style="
|
||||
color:{textColor};
|
||||
text-shadow:
|
||||
-{textStrokeWidth}px -{textStrokeWidth}px 0 {textStroke},
|
||||
{textStrokeWidth}px -{textStrokeWidth}px 0 {textStroke},
|
||||
-{textStrokeWidth}px {textStrokeWidth}px 0 {textStroke},
|
||||
{textStrokeWidth}px {textStrokeWidth}px 0 {textStroke};
|
||||
left:{d.x}px;
|
||||
top:{d.y - (labelVisibilityThreshold(d.r) ? 0 : (d.r + 4))}px;
|
||||
"
|
||||
>
|
||||
|
||||
<div class="flex flex-col items-center m-auto">
|
||||
{#if d.data.data[valueKey]}
|
||||
<div class="{(index === 1) ? 'text-xl font-semibold text-black' : 'text-[1rem] font-semibold text-[#6B6C70]'} text-center" >{commas(d.data.data[valueKey])}%</div>
|
||||
{/if}
|
||||
|
||||
<div class="{(index === 1) ? 'text-xl font-semibold text-black' : 'text-sm font-semibold text-[#6B6C70]'} text-center">{titleCase(d.data.id)}</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.circle-pack {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.circle,
|
||||
.text-group {
|
||||
position: absolute;
|
||||
}
|
||||
.circle {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
/* Hide the root node if we want, useful if we are creating our own root */
|
||||
.circle-pack[data-has-parent-key="false"] .circle-group[data-id="all"] {
|
||||
display: none;
|
||||
}
|
||||
/* .circle-group:hover {
|
||||
z-index: 9999;
|
||||
} */
|
||||
.circle-group[data-visible="false"] .text-group {
|
||||
display: none;
|
||||
padding: 4px 7px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
transform: translate(-50%, -100%);
|
||||
top: -4px;
|
||||
}
|
||||
.circle-group[data-visible="false"]:hover .text-group {
|
||||
z-index: 999;
|
||||
display: block !important;
|
||||
/* On hover, set the text color to black and eliminate the shadow */
|
||||
text-shadow: none !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
.circle-group[data-visible="false"]:hover .circle {
|
||||
border-color: #000 !important;
|
||||
}
|
||||
.text-group {
|
||||
width: auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
}
|
||||
.text {
|
||||
width: 100%;
|
||||
font-size: 19px;
|
||||
/* text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; */
|
||||
}
|
||||
.text.value{
|
||||
font-size: 15px;
|
||||
}
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
510
src/lib/components/CommentSection.svelte
Normal file
510
src/lib/components/CommentSection.svelte
Normal file
@ -0,0 +1,510 @@
|
||||
<script lang='ts'>
|
||||
|
||||
import {getImageURL, formatDate} from '$lib/utils';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { userRegion, commentAdded, commentIdDeleted, screenWidth, replyCommentClicked, editCommentClicked } from '$lib/store';
|
||||
import TextEditor from '$lib/components/TextEditor.svelte';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export let moderators
|
||||
export let comment;
|
||||
export let data;
|
||||
export let postId;
|
||||
export let opUserId;
|
||||
|
||||
export let upvoteButtonClicked
|
||||
export let downvoteButtonClicked
|
||||
export let upvoteCounter;
|
||||
export let downvoteCounter;
|
||||
export let userAlreadyVoted;
|
||||
|
||||
if (userAlreadyVoted) {
|
||||
upvoteButtonClicked = comment?.expand['alreadyVoted(comment)']?.find(item => item?.user === data?.user?.id)?.type === 'upvote';
|
||||
downvoteButtonClicked = comment?.expand['alreadyVoted(comment)']?.find(item => item?.user === data?.user?.id)?.type === 'downvote';
|
||||
} else {
|
||||
upvoteButtonClicked = false;
|
||||
downvoteButtonClicked = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function removeDuplicateClasses(str) {
|
||||
return str.replace(/class="([^"]*)"/g, (match, classAttr) => {
|
||||
return `class="${[...new Set(classAttr.split(' '))].join(' ')}"`;
|
||||
});
|
||||
}
|
||||
|
||||
function addClassesToHtml(htmlString) {
|
||||
// Helper function to add a class to a specific tag
|
||||
function addClassToTag(tag, className) {
|
||||
// Add class if the tag doesn't already have a class attribute
|
||||
const regex = new RegExp(`<${tag}(?![^>]*\\bclass=)([^>]*)>`, 'g');
|
||||
htmlString = htmlString.replace(regex, `<${tag} class="${className}"$1>`);
|
||||
|
||||
// Append the new class to tags that already have a class attribute, ensuring no duplicates
|
||||
const regexWithClass = new RegExp(`(<${tag}[^>]*\\bclass=["'][^"']*)(?!.*\\b${className}\\b)([^"']*)["']`, 'g');
|
||||
htmlString = htmlString.replace(regexWithClass, `$1 ${className}$2"`);
|
||||
}
|
||||
|
||||
// Add classes to headings
|
||||
addClassToTag('h1', 'text-lg');
|
||||
addClassToTag('h2', 'text-lg');
|
||||
addClassToTag('h3', 'text-lg');
|
||||
addClassToTag('h4', 'text-lg');
|
||||
addClassToTag('h5', 'text-lg');
|
||||
addClassToTag('h6', 'text-lg');
|
||||
|
||||
// Add classes to anchor tags
|
||||
addClassToTag('a', 'text-blue-400 hover:text-white underline');
|
||||
|
||||
// Add classes to ordered lists
|
||||
addClassToTag('ol', 'list-decimal ml-10 text-sm');
|
||||
|
||||
// Add classes to unordered lists
|
||||
addClassToTag('ul', 'list-disc ml-10 text-sm -mt-5');
|
||||
|
||||
// Add classes to blockquotes and their paragraphs
|
||||
function addClassToBlockquote() {
|
||||
// Add class to blockquote
|
||||
htmlString = htmlString.replace(
|
||||
/<blockquote/g,
|
||||
'<blockquote class="pl-4 pr-4 rounded-lg bg-[#323232]"'
|
||||
);
|
||||
|
||||
// Add class to p inside blockquote
|
||||
htmlString = htmlString.replace(
|
||||
/<blockquote([^>]*)>\s*<p/g,
|
||||
`<blockquote$1>\n<p class="text-sm font-medium leading-relaxed text-white"`
|
||||
);
|
||||
}
|
||||
|
||||
addClassToBlockquote();
|
||||
|
||||
// Remove duplicate classes after all modifications
|
||||
htmlString = removeDuplicateClasses(htmlString);
|
||||
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleUpvote = async (event) => {
|
||||
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
|
||||
const commentId = event.target.commentId.value;
|
||||
const postData = {
|
||||
'commentId': commentId,
|
||||
'userId': data?.user?.id,
|
||||
};
|
||||
|
||||
upvoteButtonClicked = !upvoteButtonClicked;
|
||||
|
||||
|
||||
if (upvoteButtonClicked) {
|
||||
if (downvoteButtonClicked) {
|
||||
upvoteCounter += 1;
|
||||
downvoteCounter -= 1;
|
||||
downvoteButtonClicked = false;
|
||||
} else {
|
||||
upvoteCounter++;
|
||||
}
|
||||
} else {
|
||||
upvoteCounter--;
|
||||
}
|
||||
const response = await fetch(fastifyURL+'/upvote-comment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
};
|
||||
|
||||
|
||||
const handleDownvote = async (event) => {
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
|
||||
const commentId = event.target.commentId.value;
|
||||
const postData = {
|
||||
'commentId': commentId,
|
||||
'userId': data?.user?.id,
|
||||
};
|
||||
|
||||
downvoteButtonClicked = !downvoteButtonClicked;
|
||||
|
||||
|
||||
if (downvoteButtonClicked) {
|
||||
if (upvoteButtonClicked) {
|
||||
downvoteCounter += 1;
|
||||
upvoteCounter -= 1;
|
||||
upvoteButtonClicked = false;
|
||||
} else {
|
||||
downvoteCounter++;
|
||||
}
|
||||
} else {
|
||||
downvoteCounter--;
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(fastifyURL+'/downvote-comment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
};
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let fastifyURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let deleteCommentId = comment?.id
|
||||
|
||||
function isModerator(comment) {
|
||||
return moderators?.some(moderator => comment?.user === moderator?.user);
|
||||
}
|
||||
|
||||
|
||||
function repeatedCharacters(str) {
|
||||
// This regex matches any character (.) followed by itself at least five times
|
||||
const regex = /(.)\1{10,}/;
|
||||
|
||||
// Test the string against the regex
|
||||
return regex?.test(str);
|
||||
}
|
||||
|
||||
|
||||
const handleDeleteComment = async () => {
|
||||
|
||||
|
||||
const postData = {
|
||||
'userId': data?.user?.id,
|
||||
'commentId': comment?.id,
|
||||
'commentUser': comment?.user
|
||||
}
|
||||
|
||||
|
||||
|
||||
const response = await fetch(fastifyURL+'/delete-comment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
const output = (await response.json())?.message;
|
||||
if (output === 'success')
|
||||
{
|
||||
$commentIdDeleted = comment?.id;
|
||||
|
||||
}
|
||||
|
||||
if (output === 'success') {
|
||||
toast.success('Comment deleted', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
} else {
|
||||
toast.error('Something went wrong', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleReportComment = async () => {
|
||||
|
||||
toast.success('Comment reported.', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff'
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
const toggle = (state) => {
|
||||
if (state === 'reply') {
|
||||
$replyCommentClicked[comment.id] = !$replyCommentClicked[comment.id]
|
||||
$editCommentClicked[comment.id] = false;
|
||||
}
|
||||
else if (state === 'edit') {
|
||||
$editCommentClicked[comment.id] = !$editCommentClicked[comment.id];
|
||||
$replyCommentClicked[comment.id] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$replyCommentClicked[comment?.id] = false
|
||||
$editCommentClicked[comment?.id] = false
|
||||
|
||||
/*
|
||||
$: {
|
||||
if($commentAdded?.length !== 0) {
|
||||
console.log('yes')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$: {
|
||||
if($commentIdDeleted === comment?.id)
|
||||
{
|
||||
upvoteCounter = {};
|
||||
downvoteCounter= {};
|
||||
upvoteButtonClicked= {};
|
||||
downvoteButtonClicked= {};
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="comment border-l border-gray-500 mt-8">
|
||||
<div class="flex flex-row justify-start items-center -ml-4">
|
||||
<a href={'/community/user/'+comment?.expand?.user?.id} class="flex flex-row items-center justify-start">
|
||||
<label class="avatar w-7 h-7 flex-shrink-0 text-white text-xs sm:text-sm ml-1">
|
||||
<img class="flex-shrink-0 inline-block bg-slate-300 rounded-full"
|
||||
src={comment?.expand?.user?.avatar
|
||||
? getImageURL(comment?.expand?.user?.collectionId, comment?.expand?.user?.id, comment?.expand?.user?.avatar)
|
||||
: `https://api.dicebear.com/7.x/thumbs/svg?seed=${comment?.expand?.user?.username}`}
|
||||
alt="User avatar" />
|
||||
</label>
|
||||
<span class="text-white ml-2 inline-block text-xs sm:text-sm">
|
||||
{comment?.expand?.user?.username}
|
||||
</span>
|
||||
{#if isModerator(comment)}
|
||||
<svg class="inline-block ml-1 w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#75d377" d="M256 32C174 69.06 121.38 86.46 32 96c0 77.59 5.27 133.36 25.29 184.51a348.86 348.86 0 0 0 71.43 112.41c49.6 52.66 104.17 80.4 127.28 87.08c23.11-6.68 77.68-34.42 127.28-87.08a348.86 348.86 0 0 0 71.43-112.41C474.73 229.36 480 173.59 480 96c-89.38-9.54-142-26.94-224-64Z"/></svg>
|
||||
{/if}
|
||||
{#if comment?.user === opUserId}
|
||||
<span class="text-[#756EFF] text-sm font-semibold ml-1">
|
||||
OP
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-white font-bold ml-1 mr-1">
|
||||
·
|
||||
</span>
|
||||
<span class="text-white text-xs">
|
||||
{formatDate(comment?.created)} ago
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="text-md text-slate-400 mb-1 pl-7 pt-3 whitespace-pre-wrap w-11/12">
|
||||
|
||||
|
||||
<div class="text-sm text-[#D7DADC] whitespace-pre-line {repeatedCharacters(comment?.comment) === true ? 'break-all' : ''}">
|
||||
{#if !$editCommentClicked[comment?.id]}
|
||||
{@html addClassesToHtml(marked(comment?.comment))}
|
||||
{:else}
|
||||
<TextEditor
|
||||
data={data}
|
||||
postId={postId}
|
||||
commentId={comment?.id}
|
||||
inputValue={comment?.comment}
|
||||
placeholder={"Reply to the comment of @"+comment?.expand?.user?.username+"..."}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if comment?.image?.length !== 0}
|
||||
<div class="relative mr-auto -mt-5">
|
||||
<img
|
||||
src={getImageURL(comment?.collectionId, comment?.id, comment?.image)}
|
||||
class="w-auto max-w-36 max-h-[350px] sm:max-h-[550px] mr-auto"
|
||||
alt="comment Image"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col items-start w-full">
|
||||
<div class="pl-5 flex flex-row items-center {comment?.image?.length === 0 ? '-mt-8' : ''} ">
|
||||
<!--Start Voting-->
|
||||
<!--Start Upvote -->
|
||||
<form on:submit={handleUpvote}>
|
||||
<input type="hidden" name="commentId" value={comment?.id}>
|
||||
{#if !data?.user}
|
||||
<label for="userLogin" class="text-[#A6ADBB] cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
||||
<svg class="rotate-180 w-4 h-4" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
||||
</label>
|
||||
{:else}
|
||||
<button type="submit" class="{upvoteButtonClicked ? 'text-[#0076FE] bg-[#31304D]' : 'text-[#A6ADBB]'} cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
||||
<svg class="rotate-180 w-4 h-4" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
||||
</button >
|
||||
{/if}
|
||||
</form>
|
||||
<!--End Upvote-->
|
||||
<!--Start Downvote-->
|
||||
<span class="text-gray-200 text-sm ml-1.5 mr-1.5">
|
||||
{upvoteCounter - downvoteCounter }
|
||||
</span>
|
||||
<form on:submit={handleDownvote}>
|
||||
<input type="hidden" name="commentId" value={comment?.id} />
|
||||
{#if !data?.user}
|
||||
<label for="userLogin" class="mr-2 cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mt-0.5" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
||||
</label>
|
||||
{:else}
|
||||
<button type="submit" class="{downvoteButtonClicked ? 'text-[#0076FE] bg-[#31304D]' : 'text-[#A6ADBB]'} mr-2 cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mt-0.5" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End Downvote-->
|
||||
<!--End Voting-->
|
||||
|
||||
<label class="mr-3 cursor-pointer text-[12.5px] font-bold text-[#8C8C8C]" for={!data?.user ? 'userLogin' : ''} on:click={() => toggle('reply')}>
|
||||
Reply
|
||||
</label>
|
||||
|
||||
{#if data?.user?.id === comment?.expand?.user?.id}
|
||||
<label class="mr-3 cursor-pointer text-[12.5px] font-bold text-[#8C8C8C]" for={!data?.user ? 'userLogin' : ''} on:click={() => toggle('edit')}>
|
||||
Edit
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if data?.user?.id === comment?.expand?.user?.id || data?.user?.id === moderators?.at(0).user}
|
||||
<label for={deleteCommentId} class="cursor-pointer text-[12.5px] font-bold text-[#8C8C8C]">
|
||||
Delete
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pl-8 w-full">
|
||||
|
||||
{#if $replyCommentClicked[comment?.id]}
|
||||
<div class="mt-3 -ml-3">
|
||||
{#if data?.user}
|
||||
<TextEditor
|
||||
data={data}
|
||||
postId={postId}
|
||||
commentId={comment?.id}
|
||||
placeholder={"Reply to the comment of @"+comment?.expand?.user?.username+"..."}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{#if comment?.children}
|
||||
<div class="ml-2">
|
||||
{#each comment.children as comment}
|
||||
<svelte:self
|
||||
{moderators}
|
||||
{comment}
|
||||
{data}
|
||||
{postId}
|
||||
{opUserId}
|
||||
upvoteCounter={comment?.upvote}
|
||||
downvoteCounter={comment?.downvote}
|
||||
userAlreadyVoted={comment?.expand['alreadyVoted(comment)']?.some(item => item?.user === data?.user?.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--Start Delete Modal-->
|
||||
<input type="checkbox" id={deleteCommentId} class="modal-toggle" />
|
||||
|
||||
<dialog id={deleteCommentId} class="modal modal-bottom sm:modal-middle border border-slate-800">
|
||||
|
||||
<label for={deleteCommentId} class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.05]"></label>
|
||||
|
||||
<div class="modal-box bg-[#202020] p-5 border border-slate-600 shadow-none" >
|
||||
|
||||
<h3 class="font-bold text-md sm:text-lg sm:mb-10 text-white mt-5">
|
||||
Are you sure you want to delete the comment?
|
||||
</h3>
|
||||
|
||||
<div class="modal-action pb-4">
|
||||
<label for={deleteCommentId} class="cursor-pointer text-sm px-3 py-3 rounded-lg m-auto text-white mr-5 bg-[#646464]">
|
||||
No, cancel
|
||||
</label>
|
||||
<label on:click={handleDeleteComment} for={deleteCommentId} class="cursor-pointer text-sm px-3 py-3 rounded-lg m-auto text-white mr-5 bg-blue-700">
|
||||
Yes, I'm sure
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<!--End Delete Modal-->
|
||||
|
||||
|
||||
|
||||
<!--Start Delete Modal-->
|
||||
<input type="checkbox" id="reportComment" class="modal-toggle" />
|
||||
|
||||
<dialog id="reportComment" class="modal modal-bottom sm:modal-middle">
|
||||
|
||||
|
||||
<div class="modal-box" >
|
||||
<label for="reportComment" class="{$screenWidth < 640 ? 'hidden' : ''} btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</label>
|
||||
|
||||
<h3 class="font-bold text-md sm:text-lg sm:mb-10">
|
||||
Are you sure you want to report the comment?
|
||||
</h3>
|
||||
|
||||
<div class="modal-action">
|
||||
<label for="reportComment" class="btn text-xs text-white mr-5">
|
||||
No, cancel
|
||||
</label>
|
||||
<label on:click={handleReportComment} for="reportComment" class="btn bg-red-700 hover:bg-red-800 text-xs text-white mr-5">
|
||||
Yes, I'm sure
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<!--End Delete Modal-->
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.comment {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
.comment.lines {
|
||||
position: relative;
|
||||
padding-inline-start: 1rem;
|
||||
}
|
||||
|
||||
/* Media query for mobile devices */
|
||||
@media (max-width: 640px) {
|
||||
.comment {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
.comment.lines {
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
44
src/lib/components/Cookie.svelte
Normal file
44
src/lib/components/Cookie.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script lang='ts'>
|
||||
import { showCookieConsent } from "$lib/store";
|
||||
|
||||
async function cookieConsent(state:string) {
|
||||
const postData = {
|
||||
'consent': state
|
||||
}
|
||||
|
||||
const response = await fetch('/api/cookies', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
const output = await response.json();
|
||||
$showCookieConsent = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section style="z-index: 9999" class="shadow-lg fixed max-w-4xl md:max-w-2xl p-4 mx-auto md:gap-x-4 md:left-60 md:bottom-10 bg-gray-900 md:flex md:items-center justify-center border-gray-700 rounded-t-md md:rounded-2xl inset-x-0 md:inset-auto bottom-0">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<span class="inline-flex p-2 text-white rounded-lg shrink-0 bg-gray-800">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.9803 8.5468C17.5123 8.69458 17.0197 8.7931 16.5271 8.7931C14.2118 8.76847 12.3399 6.89655 12.3153 4.58128C12.3153 4.13793 12.3892 3.69458 12.537 3.27586C11.9951 2.68473 11.6995 1.92118 11.6995 1.13301C11.6995 0.812808 11.7488 0.492611 11.8473 0.172414C11.2315 0.0738918 10.6158 0 10 0C4.48276 0 0 4.48276 0 10C0 15.5172 4.48276 20 10 20C15.5172 20 20 15.5172 20 10C20 9.77833 20 9.55665 19.9754 9.33498C19.2611 9.26108 18.5468 8.99015 17.9803 8.5468ZM4.58128 7.31527C6.30542 7.31527 6.30542 10.0246 4.58128 10.0246C2.85714 10.0246 2.61084 7.31527 4.58128 7.31527ZM6.05912 15.7635C4.08867 15.7635 4.08867 12.8079 6.05912 12.8079C8.02956 12.8079 8.02956 15.7635 6.05912 15.7635ZM9.01478 1.33005C10.7389 1.33005 10.7389 4.28571 9.01478 4.28571C7.29064 4.28571 7.04434 1.33005 9.01478 1.33005ZM10.2463 8.84237C11.7241 8.84237 11.7241 10.8128 10.2463 10.8128C8.76848 10.8128 9.01478 8.84237 10.2463 8.84237ZM11.9704 16.9458C10.4926 16.9458 10.4926 14.9754 11.9704 14.9754C13.4483 14.9754 13.202 16.9458 11.9704 16.9458ZM16.6503 13.1034C15.4187 13.1034 15.4187 11.133 16.6503 11.133C17.8818 11.133 17.8818 13.1034 16.6503 13.1034Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-gray-300">We use cookies to ensure that we give you the best experience on our website. <a href="/privacy-policy" class="text-blue-500 hover:underline">Read Privacy Policy</a>. </p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end sm:justify-start mt-2 mb-5 sm:mb-0 gap-x-4 shrink-0 lg:mt-0 ">
|
||||
<label for="cookieConsent" on:click={() => cookieConsent('false')} class="cursor-pointer w-auto text-sm text-gray-800 underline transition-colors duration-300 md:w-auto text-white hover:text-gray-400 focus:outline-none">
|
||||
Reject All
|
||||
</label>
|
||||
|
||||
<label for="cookieConsent" on:click={() => cookieConsent('true')} class="text-xs cursor-pointer w-auto md:w-auto font-medium bg-gray-800 rounded-lg hover:bg-gray-700 text-white px-4 py-2.5 duration-300 transition-colors focus:outline-none">
|
||||
Accept All Cookies
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
131
src/lib/components/Correlation.svelte
Normal file
131
src/lib/components/Correlation.svelte
Normal file
@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { stockTicker, etfTicker, assetType, screenWidth } from '$lib/store';
|
||||
import { abbreviateNumber, formatString } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
export let correlationList;
|
||||
|
||||
let charNumber = 20;
|
||||
let showFullStats = false;
|
||||
|
||||
|
||||
|
||||
async function stockSelector(symbol)
|
||||
{
|
||||
window?.scroll({ top: 0, left: 0, behavior: 'smooth' });
|
||||
|
||||
if($assetType === 'etf')
|
||||
{
|
||||
etfTicker.update(value => symbol);
|
||||
goto(`/etf/${symbol}`)
|
||||
}
|
||||
else {
|
||||
stockTicker.update(value => symbol);
|
||||
goto(`/stocks/${symbol}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
$: {
|
||||
if (($assetType === 'etf' ? $etfTicker : $stockTicker) && typeof window !== 'undefined' && typeof correlationList !== 'undefined' && correlationList?.length !== 0 ) {
|
||||
showFullStats = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if ($screenWidth < 640)
|
||||
{
|
||||
charNumber = 10;
|
||||
}
|
||||
else {
|
||||
charNumber = 20;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<section class="overflow-hidden w-full">
|
||||
|
||||
<main>
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="correlationInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Most Correlated Stocks
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Most Correlated Stocks"}
|
||||
content={"Correlation between -1 and +1 shows how two stocks move together. +1 means they move in sync, while -1 means they move in opposite directions. Zero means no clear relationship."}
|
||||
id={"correlationInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
{#if correlationList?.length !== 0}
|
||||
|
||||
{#each (showFullStats ? correlationList : correlationList?.slice(0, 3)) as item, index}
|
||||
|
||||
|
||||
<div class="shadow-lg bg-[#202020] w-full rounded-lg p-4 sm:p-3 mb-5 flex flex-row items-center {index === 0 ? 'mt-4' : ''} {index === 2 && !showFullStats && correlationList?.length > 2 ? 'opacity-[0.3]' : '' }">
|
||||
|
||||
<div on:click={() => stockSelector(item?.symbol)} class="flex-shrink-0 mr-3 rounded-full w-8 h-8 sm:w-10 sm:h-10 relative bg-[#0F0F0F]">
|
||||
<img
|
||||
class="avatar rounded-full w-5 h-5 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={`https://financialmodelingprep.com/image-stock/${item?.symbol}.png`}
|
||||
alt=" " />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col -mt-3 sm:-mt-5s w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<div class="mr-auto mt-2 mb-2 text-sm sm:text-md">
|
||||
<span class="text-blue-400">
|
||||
{item?.symbol}
|
||||
</span>
|
||||
<span class="text-white">
|
||||
· {item?.name?.length > charNumber ? item?.name?.slice(0,charNumber)+'...' : item?.name}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-white text-sm sm:text-md font-medium ml-auto">
|
||||
{(item?.value)?.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
{#if item?.value >= 0}
|
||||
<progress class="progress [&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]" value={(item?.value)} max="1"></progress>
|
||||
{:else}
|
||||
<progress class="progress [&::-webkit-progress-value]:bg-[#C7271A] [&::-moz-progress-bar]:bg-[#C7271A]" value={-(item?.value)} min="1"></progress>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
|
||||
<label on:click={() => showFullStats = !showFullStats} class="{correlationList?.length < 4 ? 'hidden' : ''} cursor-pointer flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullStats ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
|
||||
{:else}
|
||||
<h2 class=" mt-10 flex justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
</Lazy>
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
128
src/lib/components/CountrySegmentation.svelte
Normal file
128
src/lib/components/CountrySegmentation.svelte
Normal file
@ -0,0 +1,128 @@
|
||||
<script lang='ts'>
|
||||
import { etfTicker, screenWidth} from '$lib/store';
|
||||
import { formatString } from '$lib/utils';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { countryList} from '$lib/country-list.ts';
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
export let geographicList;
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if($etfTicker && typeof window !== 'undefined' && typeof geographicList !== 'undefined' && geographicList?.length !== 0)
|
||||
{
|
||||
showFullStats = false;
|
||||
|
||||
// Create an index for quick lookups
|
||||
const countryIndex = countryList?.reduce((acc, country) => {
|
||||
acc[country.long] = country.short;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Update the originalList with the "code" property
|
||||
geographicList = geographicList?.map(item => ({
|
||||
...item,
|
||||
code: countryIndex[item.country]?.toLowerCase() || 'xx'
|
||||
}));
|
||||
|
||||
geographicList = [...geographicList];
|
||||
|
||||
}
|
||||
}
|
||||
let showFullStats = false;
|
||||
let charNumber = 40;
|
||||
|
||||
$: {
|
||||
if($screenWidth < 640)
|
||||
{
|
||||
charNumber = 25;
|
||||
}
|
||||
else {
|
||||
charNumber =40;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden text-white h-full sm:mb-0">
|
||||
<div class="flex justify-center m-auto h-full overflow-hidden">
|
||||
<div class="relative flex justify-center items-center overflow-hidden w-full">
|
||||
<div class="w-full max-w-2xl">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="countryWeightingInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Country Weighting Breakdown
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Country Weighting Breakdown"}
|
||||
content={"Explore the degree to which your ETF's performance is influenced by individual countries."}
|
||||
id={"countryWeightingInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
{#if geographicList?.length !== 0}
|
||||
<div class="text-white text-md mt-3">
|
||||
The ETF is build on top of {geographicList?.length} regions:
|
||||
</div>
|
||||
|
||||
<div class="w-full rounded-full flex justify-center items-center">
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<!--Start Progress-->
|
||||
{#each (showFullStats ? geographicList : geographicList?.slice(0,3)) as item,index}
|
||||
|
||||
<div class="shadow-lg bg-[#202020] w-full rounded-lg p-4 sm:p-3 mb-5 flex flex-row items-center {index === 0 ? 'mt-4' : ''} {index === 2 && !showFullStats && geographicList?.length > 2 ? 'opacity-[0.3]' : '' }">
|
||||
|
||||
<div class="flex-shrink-0 mr-3 rounded-full w-10 h-10 relative bg-[#0F0F0F]">
|
||||
<img
|
||||
class="flex-shrink-0 rounded-full w-7 h-7 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={`https://hatscripts.github.io/circle-flags/flags/${item?.code}.svg`}
|
||||
alt="Country Logo" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col -mt-3 sm:-mt-5 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-sm sm:text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
{item?.country?.length > charNumber ? formatString(item?.country)?.slice(0,charNumber) + "..." : formatString(item?.country)}
|
||||
</span>
|
||||
<span class="text-white text-sm sm:text-md font-medium ml-auto">
|
||||
{item?.weightPercentage <= 0.01 ? "< 0.01%" : item?.weightPercentage?.toFixed(2)+'%'}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress [&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]" value={item?.weightPercentage} max="100"></progress>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/each}
|
||||
<!--End Progress-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if geographicList?.length > 2}
|
||||
<label on:click={() => showFullStats = !showFullStats} class="cursor-pointer m-auto flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullStats ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
</Lazy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
46
src/lib/components/CreateNewPost.svelte
Normal file
46
src/lib/components/CreateNewPost.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<script lang='ts'>
|
||||
import {getImageURL} from '$lib/utils';
|
||||
|
||||
export let data;
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Create Post -->
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col">
|
||||
<!-- Item -->
|
||||
<div class="border border-gray-700 sm:hover:border-gray-600 rounded-none sm:rounded-md bg-[#202020] rounded-[4px] sm:rounded-[8px]">
|
||||
<div class="flex h-14 sm:h-16 justify-start items-center">
|
||||
<div class="ml-2 sm:ml-3 avatar relative">
|
||||
<div class="w-8 sm:w-10 rounded-full">
|
||||
<img src={data?.avatar
|
||||
? getImageURL(data?.collectionId, data?.id, data?.avatar)
|
||||
: `https://api.dicebear.com/7.x/thumbs/svg?seed=${data?.user?.username}`} />
|
||||
</div>
|
||||
<div class="w-3.5 h-3.5 sm:w-4 sm:h-4 bg-green-400 border border-2 border-slate-800 rounded-full absolute bottom-0 right-0"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<a href="/community/create-post/" class="pl-3 sm:pl-5 text-white text-sm rounded-md h-10 sm:h-12 w-full bg-[#313131] cursor-pointer ml-3 flex justify-start items-center">
|
||||
Create Post
|
||||
</a>
|
||||
<div class="flex justify-start items-center ml-2">
|
||||
|
||||
<a href="/community/create-post" class="text-white mr-2">
|
||||
<svg class="w-6 h-6 inline-block ml-1 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path opacity="0.4" d="M22.0206 16.8198L18.8906 9.49978C18.3206 8.15978 17.4706 7.39978 16.5006 7.34978C15.5406 7.29978 14.6106 7.96978 13.9006 9.24978L12.0006 12.6598C11.6006 13.3798 11.0306 13.8098 10.4106 13.8598C9.78063 13.9198 9.15063 13.5898 8.64063 12.9398L8.42063 12.6598C7.71063 11.7698 6.83063 11.3398 5.93063 11.4298C5.03063 11.5198 4.26063 12.1398 3.75063 13.1498L2.02063 16.5998C1.40063 17.8498 1.46063 19.2998 2.19063 20.4798C2.92063 21.6598 4.19063 22.3698 5.58063 22.3698H18.3406C19.6806 22.3698 20.9306 21.6998 21.6706 20.5798C22.4306 19.4598 22.5506 18.0498 22.0206 16.8198Z" fill="#A6ADBB" ></path> <path d="M6.96984 8.38012C8.83657 8.38012 10.3498 6.86684 10.3498 5.00012C10.3498 3.13339 8.83657 1.62012 6.96984 1.62012C5.10312 1.62012 3.58984 3.13339 3.58984 5.00012C3.58984 6.86684 5.10312 8.38012 6.96984 8.38012Z" fill="#E5E7EB"></path> </g></svg>
|
||||
</a>
|
||||
|
||||
<a href="/community/create-post" class="text-white mr-2">
|
||||
<svg class="w-6 h-6 inline-block ml-1 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#A6ADBB"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0zm2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z" fill="#A6ADBB"></path></g></svg>
|
||||
</a>
|
||||
|
||||
<!--
|
||||
<a href="#" class="text-gray-300">
|
||||
<svg class="w-6 h-6 inline-block" fill="#545B61" viewBox="-32 0 512 512" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M448 432V80c0-26.5-21.5-48-48-48H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48zM112 192c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h128c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16H112zm0 96c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h224c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16H112zm0 96c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h64c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16h-64z"></path></g></svg>
|
||||
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
52
src/lib/components/CryptoKeyInformation.svelte
Normal file
52
src/lib/components/CryptoKeyInformation.svelte
Normal file
@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
|
||||
import CryptoProfileCard from '$lib/components/CryptoProfileCard.svelte';
|
||||
|
||||
export let cryptoProfile;
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<section class="mt-4">
|
||||
|
||||
|
||||
<div class="grid grid-cols-3 gap-x-4 gap-y-2">
|
||||
<label for="tickerModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Details
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Crypto Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="tickerModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="tickerModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<CryptoProfileCard cryptoProfile = {cryptoProfile}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Crypto Modal-->
|
||||
|
||||
|
||||
|
||||
169
src/lib/components/CryptoProfileCard.svelte
Normal file
169
src/lib/components/CryptoProfileCard.svelte
Normal file
@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import {cryptoTicker, screenWidth, displayCompanyName} from '$lib/store';
|
||||
|
||||
import { abbreviateNumber} from '$lib/utils';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { goto } from '$app/navigation';
|
||||
import defaultImage from '$lib/images/etf/cover/default.jpg';
|
||||
|
||||
export let cryptoProfile;
|
||||
|
||||
let cloudFrontUrl = import.meta.env.VITE_IMAGE_URL; // Set a default API URL
|
||||
|
||||
|
||||
let info;
|
||||
let imageUrl;
|
||||
let marketCap = '-';
|
||||
let totalVolume = '-';
|
||||
let circulatingSupply = '-';
|
||||
let maxSupply = '-';
|
||||
let description = '';
|
||||
let snippet;
|
||||
let website;
|
||||
let athPrice;
|
||||
let athDate;
|
||||
|
||||
|
||||
let showFullText = false;
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if ($cryptoTicker && typeof $cryptoTicker !== 'undefined' && typeof window !== 'undefined' && typeof cryptoProfile !== 'undefined' && Object?.keys(cryptoProfile)?.length !== 0)
|
||||
{
|
||||
|
||||
marketCap = cryptoProfile?.market_cap;
|
||||
totalVolume = cryptoProfile?.total_volume;
|
||||
description = cryptoProfile?.description ?? 'A detailed description of the company is not yet available.';
|
||||
snippet= description?.slice(0, 150) + "...";
|
||||
circulatingSupply = cryptoProfile?.circulating_supply
|
||||
maxSupply = cryptoProfile?.max_supply
|
||||
website = cryptoProfile?.website;
|
||||
athPrice = cryptoProfile?.ath;
|
||||
athDate = cryptoProfile?.ath_date
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sm:space-y-3">
|
||||
<div class="lg:rounded-2xl shadow-lg sm:border sm:border-slate-800 bg-[#000] lg:bg-[#202020] h-auto h-auto w-screen pt-16 sm:w-full lg:w-96 lg:pt-0">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="sm:rounded-t-2xl w-full h-[130px] bg-[#000] p-3 flex flex-col bg-cover bg-center bg-no-repeat" style="background-image: url({`${cloudFrontUrl}/stocks/cover/${$cryptoTicker?.toUpperCase()}.jpg`});">
|
||||
|
||||
<div class="flex flex-row pt-1 pb-2">
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex flex-row justify-end items-center mt-2 relative z-10 -my-5">
|
||||
<div style={`background-image: url(${sTier}`} class="animate-bounce circle-background mr-5 w-20 h-20 z-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-28 h-28" viewBox="0 0 106 34">
|
||||
<g class="sparkles">
|
||||
<path style="--duration: 4s;" d="M2.5740361 5.33344622s1.1875777-6.20179466 2.24320232 0c0 0 5.9378885 1.05562462 0 2.11124925 0 0-1.05562463 6.33374774-2.24320233 0-3.5627331-.6597654-3.29882695-1.31953078 0-2.11124925z" />
|
||||
<path style="--duration: 3.2s;" d="M33.5173993 29.97263826s1.03464615-5.40315215 1.95433162 0c0 0 5.17323078.91968547 0 1.83937095 0 0-.91968547 5.51811283-1.95433162 0-3.10393847-.57480342-2.8740171-1.14960684 0-1.83937095z" />
|
||||
<path style="--duration: 2.5s;" d="M69.03038108 1.71240809s.73779281-3.852918 1.39360864 0c0 0 3.68896404.65581583 0 1.31163166 0 0-.65581583 3.93489497-1.39360864 0-2.21337842-.4098849-2.04942447-.81976979 0-1.31163166z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div class="tier-text italic m-auto text-xl">
|
||||
<span class="text-4xl" style="color: #FFFFFF}">
|
||||
{tierList ?? 'n/a'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
<!--Start Content-->
|
||||
<div class="w-full flex flex-wrap border-t border-slate-800">
|
||||
<h2 class="text-start ml-4 text-2xl font-bold text-white pb-2 mt-3">Crypto Info</h2>
|
||||
<div class="flex items-center w-full">
|
||||
<table class="table table-md table-compact">
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Name</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020]">{$displayCompanyName?.length > 30 ? $displayCompanyName?.slice(0,30) + '...' : $displayCompanyName}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Ticker</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020]">{$cryptoTicker}</td>
|
||||
</tr>
|
||||
<!-- row 2 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Mkt Cap</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{abbreviateNumber(marketCap,true)}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Total Volume</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{abbreviateNumber(totalVolume)}</td>
|
||||
</tr>
|
||||
<!-- row 2 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Circulating Supply</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{abbreviateNumber(circulatingSupply)}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Max Supply</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{maxSupply !== null ? abbreviateNumber(maxSupply) : 'Uncapped'}</td>
|
||||
</tr>
|
||||
<!-- row 2 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">ATH Price</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">${new Intl.NumberFormat("en", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(athPrice)}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">ATH Date</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{new Date(athDate).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', daySuffix: '2-digit' })}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="text-start ml-2 text-xl font-bold text-white pb-2 pt-6 lg:pt-3">
|
||||
Description
|
||||
</h2>
|
||||
|
||||
|
||||
<p class="text-gray-100 ml-2 text-sm whitespace-normal">
|
||||
{#if showFullText}
|
||||
<div transition:fade={{ delay: 0, duration: 80 }} in={showFullText}>
|
||||
{description}
|
||||
</div>
|
||||
{:else if $screenWidth <= 800}
|
||||
{description}
|
||||
{:else}
|
||||
{snippet}
|
||||
{/if}
|
||||
</p>
|
||||
{#if description.length !== 0 }
|
||||
<div class="flex flex-row w-full items-center mt-4 pb-2 mb-2">
|
||||
<label on:click={() => showFullText = !showFullText} class="hidden lg:block ml-3 w-full text-md mt-1 cursor-pointer font-medium text-white sm:hover:text-blue-400 sm:hover:underline">
|
||||
{#if showFullText}
|
||||
Show less
|
||||
{:else}
|
||||
Show more
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<div class="flex justify-end w-full relative bottom-0 right-0 mr-3">
|
||||
<a target ="_blank" href={website} class="inline-flex text-sm font-medium text-white sm:hover:text-blue-400 sm:hover:underline">
|
||||
Go to website
|
||||
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
325
src/lib/components/DCF.svelte
Normal file
325
src/lib/components/DCF.svelte
Normal file
@ -0,0 +1,325 @@
|
||||
<script lang='ts'>
|
||||
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import {currentPortfolioPrice, stockTicker, screenWidth} from '$lib/store';
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
//export let quantData;
|
||||
export let fairPrice;
|
||||
|
||||
|
||||
let lastPrice:Number;
|
||||
|
||||
|
||||
|
||||
let optionsBarChart;
|
||||
|
||||
let change:Number;
|
||||
|
||||
const contentModal = `<span class="text-white">
|
||||
Discounted Cash Flow (DCF) is a core method for valuing a company's true worth. It starts by predicting the company's growth and how it affects its future cash flow.
|
||||
<br>
|
||||
<br>
|
||||
Then, it adjusts these future cash flows to their present value using a discount rate. This considers the risk associated with future cash flow predictions. Simply put, higher discount rates signal greater risk.
|
||||
</span>`
|
||||
|
||||
|
||||
const plotBarChart = () => {
|
||||
|
||||
|
||||
const options = {
|
||||
grid: {
|
||||
left: '0%',
|
||||
right: '0%',
|
||||
top: '0%',
|
||||
bottom: '0%',
|
||||
containLabel: true,
|
||||
},
|
||||
silent: true,
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
show: false, // Hide the x-axis labels
|
||||
},
|
||||
axisLine: {
|
||||
show: false, // Hide the y-axis lines
|
||||
},
|
||||
splitLine: {
|
||||
show: false, // Hide the grid lines on the y-axis
|
||||
},
|
||||
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
show: false, // Hide the x-axis labels
|
||||
},
|
||||
axisLine: {
|
||||
show: true, // Hide the y-axis lines
|
||||
},
|
||||
splitLine: {
|
||||
show: false, // Hide the grid lines on the y-axis
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Current Price',
|
||||
type: 'bar',
|
||||
barWidth: $screenWidth < 640 ? '30%' : '30%',
|
||||
smooth: true,
|
||||
stack: change < 0 ? 'lastPriceStack' : '',
|
||||
data: [lastPrice],
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: function (params) {
|
||||
return [
|
||||
'{a|Current Price}',
|
||||
'{b|' + '$' + params.value + '}',
|
||||
].join('\n');
|
||||
},
|
||||
rich: {
|
||||
a: {
|
||||
color: 'white',
|
||||
fontSize: $screenWidth < 640 ? 15 : 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
b: {
|
||||
color: 'white',
|
||||
fontSize: $screenWidth < 640 ? 24 : 30,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
},
|
||||
markLine: {
|
||||
symbol: 'none',
|
||||
label: {
|
||||
position: 'middle',
|
||||
formatter: '{b}',
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'white',
|
||||
fontWeight: 'bold', // Make the mark line bold
|
||||
type: 'dashed',
|
||||
width: 2, // Increase the dashed line width
|
||||
},
|
||||
data: [{ type: 'average', name: '' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fair Price',
|
||||
type: 'bar',
|
||||
barWidth: $screenWidth < 640 ? '30%' : '30%',
|
||||
smooth: true,
|
||||
stack: change > 0 ? 'fairPriceStack' : '',
|
||||
data: [fairPrice],
|
||||
itemStyle: {
|
||||
color: '#2DC97E',
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: lastPrice > fairPrice ? 'outside': 'inside',
|
||||
formatter: function (params) {
|
||||
return [
|
||||
'{a|Fair Price}',
|
||||
'{b|' + '$' + params.value + '}',
|
||||
].join('\n');
|
||||
},
|
||||
rich: {
|
||||
a: {
|
||||
color: 'white',
|
||||
fontSize: $screenWidth < 640 ? 15 : 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
b: {
|
||||
color: 'white',
|
||||
fontSize: $screenWidth < 640 ? 24 : 30,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
markLine: {
|
||||
symbol: 'none',
|
||||
label: {
|
||||
position: 'middle',
|
||||
formatter: '{b}',
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'white',
|
||||
fontWeight: 'bold', // Make the mark line bold
|
||||
type: 'dashed',
|
||||
width: 2, // Increase the dashed line width
|
||||
},
|
||||
data: [{ type: 'average', name: '' }],
|
||||
},
|
||||
|
||||
},
|
||||
// Add the new bar chart on top
|
||||
|
||||
{
|
||||
name: 'Difference',
|
||||
type: 'bar',
|
||||
barWidth: '100%', // Set the width to cover the other bars
|
||||
smooth: true,
|
||||
stack: change > 0 ? 'fairPriceStack' : 'lastPriceStack', // Stack this bar on top of the others
|
||||
data: change > 0 ? [lastPrice - fairPrice] : [fairPrice - lastPrice], // Set the value to 200
|
||||
itemStyle: {
|
||||
color: '#FF2F1F',
|
||||
},
|
||||
|
||||
},
|
||||
],
|
||||
aria: {
|
||||
enabled: true,
|
||||
decal: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
if ($stockTicker && typeof window !== 'undefined')
|
||||
{
|
||||
|
||||
lastPrice = Number($currentPortfolioPrice);
|
||||
change = ( (1 - fairPrice/lastPrice ) * 100)?.toFixed(2);
|
||||
|
||||
optionsBarChart = plotBarChart()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden text-white h-full sm:mb-0 ">
|
||||
<div class="flex justify-center w-fit m-auto h-full overflow-hidden">
|
||||
<div class="relative flex justify-center items-center overflow-hidden">
|
||||
<main>
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="dcfInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Discounted Cashflow Model
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Discounted Cashflow Model"}
|
||||
content={contentModal}
|
||||
id={"dcfInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-fit sm:w-full sm:max-w-2xl text-[1rem]">
|
||||
{#if fairPrice !== null}
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
|
||||
<div class="mt-4 text-white text-[1rem] sm:text-xl pb-4 sm:pb-0 m-auto text-start">
|
||||
The DCF model signals a
|
||||
|
||||
{#if change < -3 }
|
||||
<span class="text-[#10DB06]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#10db06" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"><path d="m3 17l6-6l4 4l8-8"/><path d="M17 7h4v4"/></g></svg>
|
||||
Buy
|
||||
</span>
|
||||
|
||||
{:else if change > 3 }
|
||||
<span class="text-[#E57C34]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#ff2f1f" d="M244 136v64a12 12 0 0 1-12 12h-64a12 12 0 0 1 0-24h35l-67-67l-31.51 31.52a12 12 0 0 1-17 0l-72-72a12 12 0 0 1 17-17L96 127l31.51-31.52a12 12 0 0 1 17 0L220 171v-35a12 12 0 0 1 24 0Z"/></svg>
|
||||
Sell
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-[#FF2F1F]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#e57c34" d="m22 12l-4-4v3H3v2h15v3l4-4Z"/></svg>
|
||||
Hold
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
{#if change > 0 }
|
||||
<div class="text-white">
|
||||
The Stock Price is
|
||||
<span class="text-[#FF2F1F] sm:text-lg">{Math?.abs(change)}% overvalued</span>.
|
||||
</div>
|
||||
{:else if change < 0 }
|
||||
<div class="text-white">
|
||||
The Stock Price is
|
||||
<span class="text-[#10DB06]">{Math?.abs(change)}% undervalued</span>.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-white text-md mt-2">
|
||||
<span class="text-blue-400">${$stockTicker}</span>
|
||||
(${lastPrice}) is trading {change < 0 ? 'below' : 'above'}
|
||||
our estimate of fair value (${fairPrice}).
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-white text-md mb-10 -mt-3">
|
||||
What is the Fair Price of <span class="font-normal text-blue-400">${$stockTicker}</span> when looking at its future cash flows? For this estimate we use a Discounted Cash Flow model (DCF).
|
||||
</div>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full m-auto mb-5">
|
||||
<Chart options={optionsBarChart} class="chart w-full" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
{#if Math?.abs(change) > 30}
|
||||
<div class=" mb-5 text-gray-100 text-sm sm:text-[1rem] sm:rounded-lg h-auto border border-slate-800 p-4">
|
||||
<svg class="w-5 h-5 inline-block mr-0.5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#60a5fa" 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>
|
||||
Caution: The DCF model may not be reliable for <span class="text-blue-400">${$stockTicker}</span> due to significant deviation between intrinsic value and current price.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class=" mt-10 justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
33
src/lib/components/DiscordBanner.svelte
Normal file
33
src/lib/components/DiscordBanner.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang='ts'>
|
||||
|
||||
|
||||
let discordURL = import.meta.env.VITE_DISCORD_URL;
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Create Post -->
|
||||
|
||||
<div class="">
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col p-1 sm:p-0">
|
||||
<!-- Item -->
|
||||
<div class="rounded-none sm:rounded-md bg-[#5C6BC0]">
|
||||
<div class="flex h-14 sm:h-16 justify-center items-center">
|
||||
|
||||
|
||||
<a href={discordURL} rel="noopener noreferrer" target="_blank" class="flex flex-row items-center">
|
||||
<span class="text-white font-bold text-3xl font-sans">
|
||||
Join us in Discord
|
||||
</span>
|
||||
<svg class="ml-4 w-10 h-10 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12z"/></svg>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
32
src/lib/components/DiscountBanner.svelte
Normal file
32
src/lib/components/DiscountBanner.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang='ts'>
|
||||
import {screenWidth} from '$lib/store';
|
||||
import discountBanner from '$lib/images/discountBanner.jpg';
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<input type="checkbox" id="discountBanner" class="modal-toggle" />
|
||||
|
||||
<dialog id="discountBanner" class="modal modal-bottom sm:modal-middle">
|
||||
|
||||
|
||||
<label id="discountBanner" for="discountBanner" class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.05]"></label>
|
||||
|
||||
|
||||
<div class="modal-box w-full bg-[#000]">
|
||||
|
||||
<div class="card bg-[#000] w-full">
|
||||
<figure><img class="rounded-xl" src={discountBanner} alt="discount" /></figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Limited Offer: </h2>
|
||||
<p>If a dog chews shoes whose shoes does he choose?</p>
|
||||
<div class="card-actions justify-end">
|
||||
<a href="/pricing" class="btn btn-primary">Buy Now</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
<!--End Login Modal-->
|
||||
|
||||
103
src/lib/components/DividendCard.svelte
Normal file
103
src/lib/components/DividendCard.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
|
||||
import {etfTicker, stockTicker, screenWidth} from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { abbreviateNumber, formatString } from '$lib/utils';
|
||||
import defaultLogo from '$lib/images/stocks/logo/default_logo.png';
|
||||
|
||||
export let dividendList;
|
||||
let eps;
|
||||
let currentPrice;
|
||||
let dividendHistoryList = [];
|
||||
let dividendYield;
|
||||
|
||||
$: {
|
||||
if($etfTicker && typeof window !== 'undefined')
|
||||
{
|
||||
dividendHistoryList = dividendList?.at(0);
|
||||
eps = dividendList?.at(1)
|
||||
currentPrice = dividendList?.at(2);
|
||||
|
||||
if(dividendHistoryList?.length !== 0)
|
||||
{
|
||||
const payoutFrequency = dividendHistoryList?.filter(entry => entry.date.includes('2022'))?.length;
|
||||
const amount = dividendHistoryList[0]?.adjDividend;
|
||||
const annualDividend = amount * payoutFrequency
|
||||
dividendYield = ((annualDividend / currentPrice )*100)?.toFixed(2)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!--Start Similar Stocks Card -->
|
||||
|
||||
<div class="space-y-3 lg:pt-5 lg:{dividendList?.length !== 0 ? '' : 'hidden'}">
|
||||
|
||||
<div class="lg:rounded-2xl shadow-lg bg-[#000] lg:bg-[#202020] lg:border lg:border-slate-800 h-auto {$screenWidth <= 800 ? 'w-screen pt-16' : ''} lg:w-96">
|
||||
|
||||
<div class="w-auto lg:w-full p-1 flex-1 flex flex-wrap pb-5">
|
||||
<div class="flex flex-row items-center w-full ml-2 pb-2 p-3">
|
||||
<span class="font-bold text-white text-2xl">
|
||||
Dividends
|
||||
</span>
|
||||
<span class="text-white font-medium ml-auto text-sm">
|
||||
Dividend Yield {dividendYield ?? '0'}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if dividendList?.at(0)?.length !== 0}
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-start items-center w-full m-auto pl-2">
|
||||
<table class="table lg:table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<thead>
|
||||
<tr class="border-b border-blue-400">
|
||||
<th class="text-white font-semibold text-sm text-start bg-[#000] lg:bg-[#202020] border-b border-blue-400">Ex-Dividend</th>
|
||||
<th class="text-white font-semibold text-sm text-end bg-[#000] lg:bg-[#202020] border-b border-blue-400">Payment Date</th>
|
||||
<th class="text-white font-semibold text-sm text-end bg-[#000] lg:bg-[#202020] border-b border-blue-400">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each dividendHistoryList?.slice(0,5) as item}
|
||||
<tr class="text-white bg-[#000] lg:bg-[#202020] border-b border-[#000] lg:border-[#202020]">
|
||||
<td class="text-start text-sm text-white font-medium">
|
||||
{new Date(item?.date)?.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', daySuffix: '2-digit' })}
|
||||
</td>
|
||||
|
||||
|
||||
<td class="text-end text-sm text-white font-medium">
|
||||
{item?.paymentDate?.length !== 0 ? new Date(item?.paymentDate)?.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', daySuffix: '2-digit' }) : 'n/a'}
|
||||
</td>
|
||||
|
||||
<td class="text-end text-sm text-white font-medium">
|
||||
${item?.adjDividend?.toFixed(2)}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<label for="dividendModal" on:click={() => goto(`/etf/${$etfTicker}/dividends`)} class="rounded-lg cursor-pointer w-11/12 md:w-3/4 lg:w-11/12 py-2 h-full mt-8 lg:mt-6 text-[1rem] text-center font-semibold text-white m-auto hover:bg-[#3C74D4] bg-[#3C74D4] bg-opacity-[0.6]">
|
||||
Full Dividend History
|
||||
</label>
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-20 justify-center items-center text-3xl font-medium text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 lg:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Stocks Card -->
|
||||
|
||||
15
src/lib/components/Downvote.svelte
Normal file
15
src/lib/components/Downvote.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
export let state;
|
||||
let color = state === 'active' ? '#0076FE' : '#D6D6DC'
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-center items-center {state === 'active' ? 'text-[#0076FE] bg-[#31304D] lg:bg-inherit' : 'text-[#A6ADBB]'} rounded-lg w-8 h-8 relative">
|
||||
<svg class="lg:hidden inline-block w-4 h-4 " version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
||||
<svg class="hidden lg:block cursor-pointer inline-block w-9 h-9 hover:text-[#fff] text-[#5C5C5C]" fill={state === 'active' ? color : "currentColor"} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.9188 8.17969H11.6888H6.07877C5.11877 8.17969 4.63877 9.33969 5.31877 10.0197L10.4988 15.1997C11.3288 16.0297 12.6788 16.0297 13.5088 15.1997L15.4788 13.2297L18.6888 10.0197C19.3588 9.33969 18.8788 8.17969 17.9188 8.17969Z" /></svg>
|
||||
</div>
|
||||
|
||||
237
src/lib/components/ESGCard.svelte
Normal file
237
src/lib/components/ESGCard.svelte
Normal file
@ -0,0 +1,237 @@
|
||||
<script lang="ts">
|
||||
//import ProgressBar from 'progressbar.js';
|
||||
//import { onMount } from 'svelte';
|
||||
import {stockTicker, screenWidth} from '$lib/store';
|
||||
|
||||
|
||||
export let stockDeck;
|
||||
|
||||
let info;
|
||||
let esgScore;
|
||||
let socialScore;
|
||||
let environmentalScore;
|
||||
let governanceScore;
|
||||
let esgRiskRating;
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
if ($stockTicker && typeof window !== 'undefined' && typeof stockDeck !== 'undefined' && stockDeck?.length !== 0)
|
||||
{
|
||||
|
||||
info = stockDeck[0]
|
||||
|
||||
|
||||
esgScore = info?.esgScore !== 'n/a' ? Math.round(info?.esgScore.toFixed(1)) : 'n/a';
|
||||
socialScore = info?.socialScore !== 'n/a' ? Math.round(info?.socialScore.toFixed(1)) : 'n/a';
|
||||
environmentalScore = info?.environmentalScore !== 'n/a' ? Math.round(info?.environmentalScore.toFixed(1)) : 'n/a';
|
||||
governanceScore = info?.governanceScore !== 'n/a' ? Math.round(info?.governanceScore.toFixed(1)) : 'n/a';
|
||||
esgRiskRating = info?.esgRiskRating;
|
||||
|
||||
};
|
||||
/*
|
||||
if (esgRatingCircleContainer)
|
||||
{
|
||||
esgRatingAcc(esgScore/100);
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start ESG Card -->
|
||||
<div class="space-y-3 lg:pt-5 hidden lg:block lg:{esgScore && esgRiskRating && environmentalScore && governanceScore !== 'n/a' ? '' : 'hidden'}">
|
||||
<div class="rounded-2xl shadow-lg bg-[#000] sm:bg-[#202020] sm:border sm:border-slate-800 h-auto sm:h-[470px] {$screenWidth < 640 ? 'w-screen pt-16' : ''} md:w-96 -mx-1 sm:mx-0">
|
||||
|
||||
<!--Start Content-->
|
||||
<div class="w-auto lg:w-full p-1 flex flex-col m-auto pb-14 sm:pb-10 px-2 sm:px-0">
|
||||
<h2 class="text-start text-2xl font-semibold text-white p-3 mt-3 ml-1">
|
||||
ESG Score
|
||||
</h2>
|
||||
|
||||
{#if esgScore && esgRiskRating && environmentalScore && governanceScore !== 'n/a'}
|
||||
|
||||
<p class="text-white mb-5 ml-4 mr-1">
|
||||
Gain valuable insights into a company's sustainability by evaluating its ESG (Environmental, Social, and Governance) scores.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col m-auto items-center rounded-lg w-full mb-16">
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex flex-row items-center w-11/12 mt-2 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Total
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{esgScore}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 {esgScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={esgScore} max="100"></progress>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col items-center w-full mt-2">
|
||||
<div class="flex flex-row items-center w-11/12 mt-5 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Environment
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{environmentalScore}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 {environmentalScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={environmentalScore} max="100"></progress>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex flex-row items-center w-11/12 mt-5 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Social
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{socialScore}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 {socialScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={socialScore} max="100"></progress>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex flex-row items-center w-11/12 mt-5 mb-2">
|
||||
<span class="text-white font-medium text-start mr-auto">
|
||||
Governance
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{governanceScore}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-11/12 {governanceScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={governanceScore} max="100"></progress>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-20 justify-center items-center text-3xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--End ESG Card-->
|
||||
|
||||
|
||||
|
||||
<!--Start Mobile ESG Card-->
|
||||
<div class="lg:hidden space-y-3 sm:pt-5">
|
||||
|
||||
<div class="bg-[#000] h-auto w-screen">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#191919] w-full p-1 flex flex-col items-center pb-5 h-auto rounded-b-[30px]">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
ESG Score
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
<span class="text-white text-center text-md text-opacity-[0.8]">
|
||||
Gain valuable insights into a company's sustainability by evaluating its ESG (Environmental, Social, and Governance) scores.
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row justify-center items-center w-full mt-5">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
{#if esgScore && esgRiskRating && environmentalScore && governanceScore !== 'n/a'}
|
||||
<div class="mt-5 flex flex-col m-auto items-center rounded-lg w-full mb-16 p-3">
|
||||
|
||||
|
||||
<div class="shadow-lg bg-[#191919] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Total
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{esgScore}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-full {esgScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={esgScore} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-lg bg-[#191919] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Environment
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{environmentalScore}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress w-full {environmentalScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={environmentalScore} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-lg bg-[#191919] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Social
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{socialScore}
|
||||
</div>
|
||||
<progress class="progress w-full {socialScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={socialScore} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-lg bg-[#191919] w-full rounded-lg p-4 mb-5 flex flex-row items-center">
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
Governance
|
||||
</span>
|
||||
<span class="text-white text-md font-medium ml-auto">
|
||||
{governanceScore}
|
||||
</div>
|
||||
<progress class="progress w-full {governanceScore >= 50 ? '[&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]' : '[&::-webkit-progress-value]:bg-[#FF2F1F] [&::-moz-progress-bar]:bg-[#FF2F1F]'}" value={governanceScore} max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" mt-20 flex justify-center items-center text-2xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Mobile ESG Card-->
|
||||
|
||||
176
src/lib/components/ETFKeyInformation.svelte
Normal file
176
src/lib/components/ETFKeyInformation.svelte
Normal file
@ -0,0 +1,176 @@
|
||||
<script lang="ts">
|
||||
|
||||
import ETFProfileCard from '$lib/components/ETFProfileCard.svelte';
|
||||
import SimilarETFCard from '$lib/components/SimilarETFCard.svelte';
|
||||
import TopHoldingCard from '$lib/components/TopHoldingCard.svelte';
|
||||
import { similarTickerClicked } from '$lib/store';
|
||||
import DividendCard from './DividendCard.svelte';
|
||||
|
||||
export let etfProfile;
|
||||
export let data;
|
||||
export let similarTicker;
|
||||
export let topHoldingList;
|
||||
export let dividendList;
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if($similarTickerClicked)
|
||||
{
|
||||
const closePopup = document.getElementById("similarTickerModal");
|
||||
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
$similarTickerClicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<section class="mt-4">
|
||||
|
||||
|
||||
<div class="grid grid-cols-3 gap-x-4 gap-y-2">
|
||||
<label for="tickerModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Details
|
||||
</label>
|
||||
|
||||
<label for="topHoldingModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Holdings
|
||||
</label>
|
||||
|
||||
<label for="dividendModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Dividends
|
||||
</label>
|
||||
<label for="similarTickerModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Similar
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start ETF Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="tickerModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="tickerModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<ETFProfileCard etfProfile = {etfProfile}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End ETF Modal-->
|
||||
|
||||
|
||||
<!--Start Similar Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="similarTickerModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="similarTickerModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
<SimilarETFCard similarTicker={similarTicker}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Modal-->
|
||||
|
||||
|
||||
<!--Start Top Holding Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="topHoldingModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="topHoldingModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#202020] w-full p-1 flex flex-col items-center pb-5 h-auto rounded-b-[30px]">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
Top Holdings
|
||||
</h2>
|
||||
<div class="flex flex-col items-center mt-8 w-full">
|
||||
<span class="text-white text-center text-md text-opacity-[0.8] pl-8 pr-8">
|
||||
Discover the primary holdings within the ETF's composition.
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row justify-center items-center w-full mt-5">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<TopHoldingCard topHoldingList = {topHoldingList}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Top Holding Modal-->
|
||||
|
||||
|
||||
<!--Start Dividend Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="dividendModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="dividendModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<DividendCard dividendList = {dividendList}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Dividend Modal-->
|
||||
178
src/lib/components/ETFProfileCard.svelte
Normal file
178
src/lib/components/ETFProfileCard.svelte
Normal file
@ -0,0 +1,178 @@
|
||||
<script lang="ts">
|
||||
import {etfTicker, screenWidth, displayCompanyName} from '$lib/store';
|
||||
|
||||
import { abbreviateNumber, formatETFName } from '$lib/utils';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { goto } from '$app/navigation';
|
||||
import defaultImage from '$lib/images/etf/cover/default.jpg';
|
||||
|
||||
export let logoUrl;
|
||||
export let etfProfile;
|
||||
|
||||
|
||||
let info;
|
||||
let imageUrl;
|
||||
|
||||
let tierList = 'S';
|
||||
let assetClass = '-';
|
||||
let aum = '-';
|
||||
let avgVolume = '-';
|
||||
let inceptionDate = '-';
|
||||
let holdingsCount = '-';
|
||||
let exchange = '-';
|
||||
let provider = '-';
|
||||
let sectorExposed;
|
||||
let country;
|
||||
let description = '';
|
||||
let website = '-';
|
||||
let snippet;
|
||||
|
||||
|
||||
let showFullText = false;
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if ($etfTicker && typeof $etfTicker !== 'undefined' && typeof window !== 'undefined' && typeof etfProfile !== 'undefined' && etfProfile?.length !== 0)
|
||||
{
|
||||
|
||||
info = etfProfile?.at(0)
|
||||
assetClass = info?.assetClass;
|
||||
sectorExposed = info?.sectorsList?.length;
|
||||
aum = info?.aum;
|
||||
country = info?.domicile;
|
||||
inceptionDate = new Date(info?.inceptionDate)?.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }).replace(/\//g, '.');
|
||||
holdingsCount = info?.holdingsCount;
|
||||
avgVolume = info?.avgVolume;
|
||||
provider = info?.etfProvider;
|
||||
country = info?.domicile ?? '-';
|
||||
description = info?.description ?? 'A detailed description of the company is not yet available.';
|
||||
website = info?.website;
|
||||
snippet= description?.slice(0, 150) + "...";
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sm:space-y-3">
|
||||
<div class="lg:rounded-2xl shadow-lg sm:border sm:border-slate-800 bg-[#000] lg:bg-[#202020] h-auto h-auto w-screen pt-16 sm:w-full lg:w-96 lg:pt-0">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="lg:rounded-t-2xl w-full h-[130px] bg-[#0F0F0F] p-3 flex flex-col bg-cover bg-center bg-no-repeat" style="background-image: url('{defaultImage}');">
|
||||
|
||||
<div class="flex flex-row pt-1 pb-2">
|
||||
<div class="badge badge-error gap-2 mt-2 font-medium text-sm text-white">
|
||||
Asset Class - {assetClass}
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex flex-row justify-end items-center mt-2 relative z-10 -my-5">
|
||||
<div style={`background-image: url(${sTier}`} class="animate-bounce circle-background mr-5 w-20 h-20 z-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-28 h-28" viewBox="0 0 106 34">
|
||||
<g class="sparkles">
|
||||
<path style="--duration: 4s;" d="M2.5740361 5.33344622s1.1875777-6.20179466 2.24320232 0c0 0 5.9378885 1.05562462 0 2.11124925 0 0-1.05562463 6.33374774-2.24320233 0-3.5627331-.6597654-3.29882695-1.31953078 0-2.11124925z" />
|
||||
<path style="--duration: 3.2s;" d="M33.5173993 29.97263826s1.03464615-5.40315215 1.95433162 0c0 0 5.17323078.91968547 0 1.83937095 0 0-.91968547 5.51811283-1.95433162 0-3.10393847-.57480342-2.8740171-1.14960684 0-1.83937095z" />
|
||||
<path style="--duration: 2.5s;" d="M69.03038108 1.71240809s.73779281-3.852918 1.39360864 0c0 0 3.68896404.65581583 0 1.31163166 0 0-.65581583 3.93489497-1.39360864 0-2.21337842-.4098849-2.04942447-.81976979 0-1.31163166z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div class="tier-text italic m-auto text-xl">
|
||||
<span class="text-4xl" style="color: #FFFFFF}">
|
||||
{tierList ?? 'n/a'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
<!--Start Content-->
|
||||
<div class="w-full flex flex-wrap border-t border-slate-800">
|
||||
<h2 class="text-start ml-4 text-2xl font-bold text-white pb-2 mt-3">ETF Info</h2>
|
||||
<div class="flex items-center w-full">
|
||||
<table class="table table-md table-compact">
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Name</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020]">{$displayCompanyName?.length > 30 ? $displayCompanyName?.slice(0,30) + '...' : $displayCompanyName}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Ticker</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020]">{$etfTicker}</td>
|
||||
</tr>
|
||||
<!-- row 3 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Provider</td>
|
||||
<td on:click={() => goto(`/etf/etf-providers/${provider}`)} class="text-blue-400 lg:border-b lg:border-[#202020] lg:hover:text-white cursor-pointer bg-[#000] lg:bg-[#202020]">{formatETFName(provider)}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Country</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020]">{country?.length !== 0 ? country : '-'}</td>
|
||||
</tr>
|
||||
<!-- row 2 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Value</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{abbreviateNumber(aum,true)}</td>
|
||||
<td class="text-start lg:border-b lg:border-[#202020] bg-[#000] lg:bg-[#202020] text-white font-medium">Volume</td>
|
||||
<td class="bg-[#000] lg:border-b lg:border-[#202020] lg:bg-[#202020] whitespace-normal">{abbreviateNumber(avgVolume)}</td>
|
||||
</tr>
|
||||
<!-- row 3 -->
|
||||
<tr class="text-white border-b border-[#202020]" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] lg:bg-[#202020] text-white font-medium">Holdings</td>
|
||||
<td class="bg-[#000] lg:bg-[#202020]">{holdingsCount} Assets</td>
|
||||
<td class="text-start bg-[#000] lg:bg-[#202020] text-white font-medium">Inception</td>
|
||||
<td class="bg-[#000] lg:bg-[#202020]">{inceptionDate}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="text-start ml-2 text-xl font-bold text-white pb-2 pt-6 lg:pt-3">
|
||||
Description
|
||||
</h2>
|
||||
|
||||
|
||||
<p class="text-gray-100 ml-2 text-sm whitespace-normal">
|
||||
{#if showFullText}
|
||||
<div transition:fade={{ delay: 0, duration: 80 }} in={showFullText}>
|
||||
{description}
|
||||
</div>
|
||||
{:else if $screenWidth <= 800}
|
||||
{description}
|
||||
{:else}
|
||||
{snippet}
|
||||
{/if}
|
||||
</p>
|
||||
{#if description.length !== 0 }
|
||||
<div class="flex flex-row w-full items-center mt-4 pb-2 mb-2">
|
||||
<label on:click={() => showFullText = !showFullText} class="hidden lg:block ml-3 w-full text-md mt-1 cursor-pointer font-medium text-white sm:hover:text-blue-400 sm:hover:underline">
|
||||
{#if showFullText}
|
||||
Show less
|
||||
{:else}
|
||||
Show more
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<div class="flex justify-end w-full relative bottom-0 right-0 mr-3">
|
||||
<a target ="_blank" href="{website}" class="inline-flex text-sm font-medium text-white sm:hover:text-blue-400 sm:hover:underline">
|
||||
Go to website
|
||||
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
235
src/lib/components/Enterprise.svelte
Normal file
235
src/lib/components/Enterprise.svelte
Normal file
@ -0,0 +1,235 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker, screenWidth} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import { abbreviateNumber } from "$lib/utils";
|
||||
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
|
||||
export let rawData = [];
|
||||
let optionsData;
|
||||
|
||||
|
||||
function normalizer(value) {
|
||||
if (Math?.abs(value) >= 1e18) {
|
||||
return { unit: 'Q', denominator: 1e18 };
|
||||
} else if (Math?.abs(value) >= 1e12) {
|
||||
return { unit: 'T', denominator: 1e12 };
|
||||
} else if (Math?.abs(value) >= 1e9) {
|
||||
return { unit: 'B', denominator: 1e9 };
|
||||
} else if (Math?.abs(value) >= 1e6) {
|
||||
return { unit: 'M', denominator: 1e6 };
|
||||
} else if (Math?.abs(value) >= 1e5) {
|
||||
return { unit: 'K', denominator: 1e5 };
|
||||
} else {
|
||||
return { unit: '', denominator: 1 };
|
||||
}
|
||||
}
|
||||
function getPlotOptions() {
|
||||
let dates = [];
|
||||
let enterpriseValue = [];
|
||||
let numberOfShares = [];
|
||||
let marketCapitalization = [];
|
||||
let addTotalDebt = [];
|
||||
let cashEquivalent = [];
|
||||
// Iterate over the data and extract required information
|
||||
rawData?.forEach(item => {
|
||||
// Extract year and convert it to fiscal year format
|
||||
const fiscalYear = item?.date;
|
||||
dates?.push(fiscalYear);
|
||||
|
||||
// Extract totalValue
|
||||
enterpriseValue?.push(item?.enterpriseValue);
|
||||
marketCapitalization?.push(item?.marketCapitalization);
|
||||
addTotalDebt?.push(item?.addTotalDebt);
|
||||
cashEquivalent?.push(item?.minusCashAndCashEquivalents)
|
||||
|
||||
});
|
||||
|
||||
const {unit, denominator } = normalizer(Math.max(...enterpriseValue) ?? 0)
|
||||
|
||||
|
||||
const option = {
|
||||
silent: true,
|
||||
grid: {
|
||||
left: $screenWidth < 640 ? '0%' : '2%',
|
||||
right: $screenWidth < 640 ? '5%' : '2%',
|
||||
bottom: $screenWidth < 640 ? '0%' : '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
data: dates,
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false, // Disable x-axis grid lines
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#6E7079', // Change label color to white
|
||||
formatter: function (value) {
|
||||
value = Math.max(value, 0);
|
||||
return '$'+(value / denominator)?.toFixed(1) + unit; // Format value in millions
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: cashEquivalent,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#fff' // Change bar color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: addTotalDebt,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#FF2F1F' // Change bar color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: marketCapitalization,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#5470C6' // Change bar color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: enterpriseValue,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#F8901E' // Change bar color to white
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined' && rawData?.length !== 0) {
|
||||
|
||||
optionsData = getPlotOptions()
|
||||
|
||||
// Calculate total number of contracts
|
||||
/*
|
||||
avgTotalValue = Math.floor((rawData?.reduce((sum, item) => sum + item?.totalValue, 0))/rawData?.length);
|
||||
const { year:yearWithMaxLobbying, totalValue: maxLobbying } = rawData?.reduce((max, item) => item?.totalValue > max?.totalValue ? item : max, rawData?.at(0));
|
||||
displayYear = yearWithMaxLobbying;
|
||||
displayMaxLobbying = maxLobbying
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="enterpriseValueInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Enterprise Value
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Enterprise Value"}
|
||||
content={"Enterprise value (EV) is a comprehensive measure of a company's total value in the stock market, considering market capitalization, debt, cash, and minority interests. It helps in M&A analysis, comparative analysis, valuation metrics, and capital structure assessment, aiding investors, analysts, and companies in financial decision-making."}
|
||||
id={"enterpriseValueInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if rawData?.length !== 0}
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
{$displayCompanyName}'s' enterprise value provides a comprehensive snapshot of its total worth, crucial for assessing its financial health and making informed investment decisions.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{'/stocks/'+$stockTicker+'/stats/income'}" class="text-blue-400 hover:text-white flex justify-end mt-3 text-sm sm:text-[1rem]">
|
||||
Full report
|
||||
</a>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full h-[300px] ">
|
||||
<Chart options={optionsData} class="chart" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
<div class="flex flex-row items-center justify-between mx-auto mt-5 w-full sm:w-11/12">
|
||||
<div class="mt-3.5 sm:mt-0 flex flex-col sm:flex-row items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#F8901E] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-center sm:text-start text-xs sm:text-md inline-block">
|
||||
Enterprise Value
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#5470C6] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-xs sm:text-md sm:font-medium inline-block">
|
||||
Mkt Cap
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#FF2F1F] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-xs sm:text-md inline-block">
|
||||
Debt
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3.5 sm:mt-0 flex flex-col sm:flex-row items-center sm:items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#fff] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-center sm:text-start text-xs sm:text-md inline-block">
|
||||
Cash Equivalents
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
194
src/lib/components/ExecutiveCard.svelte
Normal file
194
src/lib/components/ExecutiveCard.svelte
Normal file
@ -0,0 +1,194 @@
|
||||
<script lang="ts">
|
||||
import {userRegion, executiveClicked, stockTicker, clientSideCache, } from '$lib/store';
|
||||
import { afterUpdate } from 'svelte';
|
||||
|
||||
|
||||
//export let executiveList;
|
||||
let executiveList = [];
|
||||
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let apiURL;
|
||||
|
||||
userRegion?.subscribe(value => {
|
||||
if (usRegion?.includes(value)) {
|
||||
apiURL = import.meta.env.VITE_USEAST_API_URL;
|
||||
} else {
|
||||
apiURL = import.meta.env.VITE_EU_API_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let isLoaded = false;
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
let numberOfFemales = 0;
|
||||
let numberOfMales = 0;
|
||||
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
if($clientSideCache[$stockTicker]?.getExecutives)
|
||||
{
|
||||
executiveList = $clientSideCache[$stockTicker]?.getExecutives;
|
||||
}
|
||||
else {
|
||||
const postData = { ticker: $stockTicker};
|
||||
const response = await fetch(apiURL+'/get-executives', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
executiveList = await response.json();
|
||||
$clientSideCache[$stockTicker].getExecutives = executiveList;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
afterUpdate(async() => {
|
||||
if($stockTicker && typeof window !== 'undefined' && $executiveClicked === true) {
|
||||
$executiveClicked = false;
|
||||
await fetchData()
|
||||
for (const executive of executiveList) {
|
||||
switch (executive?.gender) {
|
||||
case 'female':
|
||||
numberOfFemales++;
|
||||
break;
|
||||
case 'male':
|
||||
numberOfMales++;
|
||||
break;
|
||||
// Handle other gender cases if needed
|
||||
}
|
||||
}
|
||||
|
||||
isLoaded = true;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="space-y-3 sm:pt-5">
|
||||
<div class="bg-[#000] h-auto w-screen">
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#202020] w-full p-1 flex flex-col items-center pb-5 h-auto rounded-b-[30px]">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
Executives
|
||||
</h2>
|
||||
|
||||
<div class="{!isLoaded ? 'hidden' : ''} flex flex-col items-center mt-10 w-full">
|
||||
<span class="text-white text-md text-opacity-[0.55]">
|
||||
Female/Total Ratio
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row justify-center items-center w-full mt-5">
|
||||
|
||||
|
||||
<div class="flex flex-row items-center ml-5">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#0FC008] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-sm font-medium inline-block">
|
||||
Female <span class="font-medium text-[0.95rem]">({numberOfFemales})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<span class="text-white font-medium text-3xl m-auto">
|
||||
{numberOfFemales === 0 ? 0 : (numberOfFemales/(numberOfFemales+numberOfMales) *100)?.toFixed(2)}%
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row items-center mr-5">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#FF2F1F] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-sm font-medium inline-block">
|
||||
Male <span class="font-medium text-[0.95rem]">({numberOfMales})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
{#if isLoaded}
|
||||
|
||||
{#if executiveList?.length !== 0}
|
||||
<div class="mt-5 w-full">
|
||||
{#each executiveList as item}
|
||||
|
||||
<!--Start Item-->
|
||||
<div class="flex flex-row items-center pl-4 pr-4 w-full mb-3">
|
||||
|
||||
<div class="w-full rounded-md bg-[#202020] shadow-lg h-auto pb-3 pl-3 pt-3">
|
||||
<div class="flex flex-row items-center relative">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row items-center mr-auto mb-2 text-white font-medium text-[1rem] w-56">
|
||||
<span>
|
||||
{item?.name}
|
||||
{#if item?.yearBorn !== null}
|
||||
, {(currentYear-item.yearBorn)}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<span class="text-white text-opacity-[0.6] text-sm w-48">
|
||||
{item?.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if item?.gender === 'male'}
|
||||
<div class="flex flex-row items-center ml-auto absolute right-3 top-0">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<div class="w-2 h-2 bg-[#FF2F1F] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-[0.85rem] inline-block">
|
||||
Male
|
||||
</span>
|
||||
</div>
|
||||
{:else if item?.gender === 'female'}
|
||||
<div class="flex flex-row items-center ml-auto absolute right-3 top-0">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-2 h-2 bg-[#0FC008] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-[0.85rem] inline-block">
|
||||
Female
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Item-->
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" mt-20 flex justify-center items-center text-2xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<div class="flex justify-center items-center h-80">
|
||||
<div class="relative">
|
||||
<label class="bg-[#202020] 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"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--End Similar Stocks Card -->
|
||||
|
||||
|
||||
247
src/lib/components/Feedback.svelte
Normal file
247
src/lib/components/Feedback.svelte
Normal file
@ -0,0 +1,247 @@
|
||||
<script lang='ts'>
|
||||
import toast from 'svelte-french-toast';
|
||||
import veryGoodEmoji from '$lib/assets/veryGoodEmoji.svg';
|
||||
import goodEmoji from '$lib/assets/goodEmoji.svg';
|
||||
import badEmoji from '$lib/assets/badEmoji.svg';
|
||||
import veryBadEmoji from '$lib/assets/veryBadEmoji.svg';
|
||||
import { userRegion, screenWidth } from '$lib/store';
|
||||
export let data;
|
||||
|
||||
let cloudFrontUrl = import.meta.env.VITE_IMAGE_URL;
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let fastifyURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
let rating = '';
|
||||
let inputValue = '';
|
||||
|
||||
|
||||
async function handleReturn() {
|
||||
rating = '';
|
||||
inputValue = '';
|
||||
|
||||
}
|
||||
async function sendFeedback()
|
||||
{
|
||||
|
||||
if (inputValue?.length === 0) {
|
||||
toast.error('Please enter your feedback', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (rating?.length === 0) {
|
||||
toast.error('Please select an emoji', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = data?.user?.id ?? '';
|
||||
|
||||
const postData = {'user': userId,
|
||||
'rating': rating,
|
||||
'description': inputValue};
|
||||
|
||||
|
||||
const response = await fetch(fastifyURL+'/feedback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
const output = (await response.json())?.items;
|
||||
|
||||
if (output === 'success') {
|
||||
toast.success('Thank you for your feedback', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'});
|
||||
inputValue = '';
|
||||
|
||||
} else {
|
||||
toast.error('Something went wrong. Please try again later!', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'});
|
||||
}
|
||||
|
||||
const closePopup = document.getElementById('feedbackInfo');
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
|
||||
rating = '';
|
||||
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
inputValue = event.target.value;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<svelte:head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
|
||||
<div class="fixed z-40 bottom-8 sm:bottom-10 right-8 sm:right-16">
|
||||
<label for="feedbackInfo" class="inline-flex items-center justify-center w-12 h-12 sm:w-full sm:h-10 font-medium bg-gray-700 sm:bg-[#FFEDE5] ml-1 mr-0 sm:mr-2 rounded-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="sm:hidden sm:ml-4 w-6 h-6 text-white inline-block" viewBox="0 0 256 256"><path fill="white" d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></svg>
|
||||
<span class="text-black hidden sm:block text-md px-3">
|
||||
Feedback
|
||||
</span>
|
||||
<img class="hidden sm:inline-block w-12 -mt-6 opacity-[0.85]" src={cloudFrontUrl+"/assets/feedback_icon.png"} alt="feedback logo"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{#if $screenWidth > 640}
|
||||
<!--Start Create Watchlist Modal-->
|
||||
<input type="checkbox" id="feedbackInfo" class="modal-toggle" />
|
||||
|
||||
<dialog id="feedbackInfo" class="modal modal-bottom sm:modal-middle overflow-hidden">
|
||||
|
||||
|
||||
<label for="feedbackInfo" class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.05]"></label>
|
||||
|
||||
|
||||
<div class="modal-box w-full bg-[#000]" >
|
||||
|
||||
<div class="flex flex-row items-center pt-5">
|
||||
<h1 class="text-white text-xl sm:text-2xl font-bold">
|
||||
Your Feedback matters!
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="p-2 mt-5 w-full ">
|
||||
<textarea
|
||||
class="textarea textarea-bordered placeholder-gray-300 w-full bg-[#202020] text-white border border-gray-600"
|
||||
placeholder="Your feedback..."
|
||||
value={inputValue}
|
||||
on:input={handleInput}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="flex gap-5 justify-center mt-5 mb-5">
|
||||
<li on:click={() => rating= 'Very Good'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Very Good' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-8 h-8 sm:w-10 sm:h-10" src={veryGoodEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
<li on:click={() => rating= 'Good'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Good' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-8 h-8 sm:w-10 sm:h-10" src={goodEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
<li on:click={() => rating= 'Bad'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Bad' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-8 h-8 sm:w-10 sm:h-10" src={badEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
<li on:click={() => rating= 'Very Bad'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Very Bad' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-8 h-8 sm:w-10 sm:h-10" src={veryBadEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="hidden text-white flex justify-center text-lg sm:text-xl font-bold">
|
||||
I think this website is {rating?.length !== 0 ? `"${rating}"` : ''}
|
||||
</span>
|
||||
|
||||
<button on:click={() => sendFeedback()} class="mb-4 btn bg-blue-700 hover:bg-blue-600 {rating?.length !== 0 && inputValue?.length !== 0 ? 'opacity-100 cursor-pointer' : 'opacity-60 cursor-default'} btn-md w-full rounded-full m-auto text-white font-bold text-md">
|
||||
Send Feedback
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
{:else}
|
||||
|
||||
|
||||
<div class="drawer drawer-end z-50 overflow-hidden">
|
||||
<input id="feedbackInfo" type="checkbox" class="drawer-toggle"/>
|
||||
|
||||
<div class="drawer-side overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-hidden">
|
||||
|
||||
<label for="feedbackInfo" on:click={handleReturn} class="absolute left-6 top-4">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-row items-center mt-10 p-7">
|
||||
<h1 class="text-white text-xl sm:text-2xl font-bold">
|
||||
Your Feedback matters!
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pl-7 pr-7 w-full ">
|
||||
<textarea
|
||||
class="textarea textarea-bordered h-24 placeholder-gray-300 w-full bg-[#202020] text-white border border-gray-600"
|
||||
placeholder="Your feedback..."
|
||||
value={inputValue}
|
||||
on:input={handleInput}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="flex gap-5 justify-center mt-5 mb-5">
|
||||
<li on:click={() => rating= 'Very Good'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Very Good' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-10 h-10" src={veryGoodEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
<li on:click={() => rating= 'Good'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Good' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-10 h-10" src={goodEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
<li on:click={() => rating= 'Bad'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Bad' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-10 h-10" src={badEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
<li on:click={() => rating= 'Very Bad'} class="cursor-pointer">
|
||||
<div class="rounded-full w-16 h-16 relative {rating === 'Very Bad' ? 'bg-[#333333]' : ''} hover:bg-[#333333] flex items-center justify-center">
|
||||
<img class="w-10 h-10" src={veryBadEmoji} loading="lazy" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="hidden text-white flex justify-center text-lg sm:text-xl font-bold">
|
||||
I think this website is {rating?.length !== 0 ? `"${rating}"` : ''}
|
||||
</span>
|
||||
|
||||
<button on:click={() => sendFeedback()} class="pl-7 pr-7 mb-4 flex justify-center items-center btn bg-blue-700 hover:bg-blue-600 {rating?.length !== 0 && inputValue?.length !== 0 ? 'opacity-100 cursor-pointer' : 'opacity-60 cursor-default'} btn-md rounded-full w-5/6 m-auto text-white font-bold text-md">
|
||||
Send Feedback
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
165
src/lib/components/Footer.svelte
Normal file
165
src/lib/components/Footer.svelte
Normal file
@ -0,0 +1,165 @@
|
||||
<script lang='ts'>
|
||||
|
||||
let discordURL = import.meta.env.VITE_DISCORD_URL;
|
||||
let cloudFrontUrl = import.meta.env.VITE_IMAGE_URL;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<!--Footer-->
|
||||
<footer class="bg-[#202020] border-t border-slate-800 z-20 sm:z-50 relative bottom-0 left-0 w-full">
|
||||
<div class="container mx-auto px-3">
|
||||
<div class="w-full flex flex-col md:flex-row py-6">
|
||||
<div class="flex-1 mb-8">
|
||||
<a href="/" class="flex justify-start items-center text-pink-600 no-underline hover:no-underline font-bold text-2xl lg:text-4xl">
|
||||
<!--Icon from: http://www.potlabicons.com/ -->
|
||||
<img src={cloudFrontUrl+"/assets/stocknear_logo.png"} class="w-10 mr-3" loading="lazy"/>
|
||||
<span class="self-center text-xl text-white font-bold whitespace-nowrap">stocknear</span>
|
||||
</a>
|
||||
|
||||
<div class="flex flex-row items-center mt-4">
|
||||
|
||||
<a href="https://www.ko-fi.com/stocknear" rel="noopener noreferrer" target="_blank" class="flex flex-row items-center">
|
||||
<svg class="w-5 h-5 ml-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734c4.352.24 7.422-2.831 6.649-6.916m-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09c-.443-.441-3.368-3.049-4.034-3.954c-.709-.965-1.041-2.7-.091-3.71c.951-1.01 3.005-1.086 4.363.407c0 0 1.565-1.782 3.468-.963s1.832 3.011.723 4.311m6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015"/></svg>
|
||||
</a>
|
||||
|
||||
<a href={discordURL} rel="noopener noreferrer" target="_blank" class="flex flex-row items-center">
|
||||
<svg class="w-6 h-6 ml-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12z"/></svg>
|
||||
</a>
|
||||
|
||||
|
||||
<a href="https://twitter.com/intent/follow?screen_name=stocknear" rel="noopener noreferrer" target="_blank" class="flex flex-row items-center">
|
||||
<svg class="w-5 h-5 ml-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M18.205 2.25h3.308l-7.227 8.26l8.502 11.24H16.13l-5.214-6.817L4.95 21.75H1.64l7.73-8.835L1.215 2.25H8.04l4.713 6.231l5.45-6.231Zm-1.161 17.52h1.833L7.045 4.126H5.078L17.044 19.77Z"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="https://www.linkedin.com/company/stocknear" rel="noopener noreferrer" target="_blank" class="flex flex-row items-center ml-3">
|
||||
<svg class="w-5 h-5 ml-1 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77Z"/></svg>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<a href="https://stats.uptimerobot.com/MP0zLix9yk" rel="noopener noreferrer" target="_blank" class="flex flex-row items-center mt-4">
|
||||
<span class="text-blue-400">
|
||||
Server Status
|
||||
</span>
|
||||
<span class="relative flex h-3 w-3 ml-2">
|
||||
<span class="absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-[#0070F3]"></span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<span class="hidden sm:flex flex-row items-center mt-4 text-xs text-white">
|
||||
Made with
|
||||
<svg class="ml-1 mr-1 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#b71c1c" d="m12 21.35l-1.45-1.32C5.4 15.36 2 12.27 2 8.5C2 5.41 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.41 22 8.5c0 3.77-3.4 6.86-8.55 11.53z"/></svg>
|
||||
in Siegen
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mb-2 sm:mb-0">
|
||||
<p class="uppercase text-slate-100 md:mb-6">Services</p>
|
||||
<ul class="list-reset mb-6">
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0" >
|
||||
<a href="/pricing" class="no-underline hover:underline text-gray-300 hover:text-blue-400">Become a Pro</a>
|
||||
</li>
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0" >
|
||||
<a href="https://stocknear.lemonsqueezy.com/affiliates" rel="noopener noreferrer" target="_blank" class="no-underline hover:underline text-gray-300 hover:text-blue-400">Affiliate Program</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mb-2 sm:mb-0">
|
||||
<p class="uppercase text-slate-100 md:mb-6">Legal</p>
|
||||
<ul class="list-reset mb-6">
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0" >
|
||||
<a href="/terms-of-use" class="no-underline hover:underline text-gray-300 hover:text-blue-400">Terms of Use</a>
|
||||
</li>
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0" >
|
||||
<a href="/privacy-policy" class="no-underline hover:underline text-gray-300 hover:text-blue-400">Privacy Policy</a>
|
||||
</li>
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0" >
|
||||
<a href="/imprint" class="no-underline hover:underline text-gray-300 hover:text-blue-400">Imprint</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mb-2 sm:mb-0">
|
||||
<p class="uppercase text-slate-100 md:mb-6">Company</p>
|
||||
<ul class="list-reset mb-6">
|
||||
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0">
|
||||
<a href="/about" class="no-underline hover:underline text-gray-300 hover:text-blue-400">
|
||||
About Us
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0">
|
||||
<a href="/contact" class="no-underline hover:underline text-gray-300 hover:text-blue-400">
|
||||
Contact Us
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0">
|
||||
<a href="/blog" class="no-underline hover:underline text-gray-300 hover:text-blue-400">
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
<!--
|
||||
<li class="mt-2 inline-block mr-2 md:block md:mr-0">
|
||||
<a href="/newsletter" class="no-underline hover:underline text-gray-300 hover:text-blue-400">
|
||||
Newsletter
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex-1">
|
||||
<p class="uppercase text-slate-100 md:mb-6">App</p>
|
||||
<ul class="list-reset mb-6">
|
||||
|
||||
<li class="mt-6 sm:mt-2 inline-block mr-2 md:block md:mr-0">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.stocknear.bundleId" rel="noopener noreferrer" target="_blank" class="flex flex-row items-center no-underline text-white">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="white" d="M48 59.49v393a4.33 4.33 0 0 0 7.37 3.07L260 256L55.37 56.42A4.33 4.33 0 0 0 48 59.49M345.8 174L89.22 32.64l-.16-.09c-4.42-2.4-8.62 3.58-5 7.06l201.13 192.32ZM84.08 472.39c-3.64 3.48.56 9.46 5 7.06l.16-.09L345.8 338l-60.61-57.95ZM449.38 231l-71.65-39.46L310.36 256l67.37 64.43L449.38 281c19.49-10.77 19.49-39.23 0-50"/></svg>
|
||||
<span class="ml-2 text-white text-[0.92rem] sm:text-md">
|
||||
Google Playstore
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mt-2 sm:mt-3 inline-block mr-2 md:block md:mr-0 ">
|
||||
<label class="flex flex-row items-center no-underline text-white">
|
||||
<svg class="w-7 h-7 -ml-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M17.05 20.28c-.98.95-2.05.8-3.08.35c-1.09-.46-2.09-.48-3.24 0c-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8c1.18-.24 2.31-.93 3.57-.84c1.51.12 2.65.72 3.4 1.8c-3.12 1.87-2.38 5.98.48 7.13c-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25c.29 2.58-2.34 4.5-3.74 4.25"/></svg>
|
||||
<span class="ml-2 text-white text-[0.92rem] sm:text-md">
|
||||
App Store (Coming Soon)
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
<span class="sm:hidden flex flex-row items-center text-xs text-white">
|
||||
Made with
|
||||
<svg class="ml-1 mr-1 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#b71c1c" d="m12 21.35l-1.45-1.32C5.4 15.36 2 12.27 2 8.5C2 5.41 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.41 22 8.5c0 3.77-3.4 6.86-8.55 11.53z"/></svg>
|
||||
in Siegen
|
||||
<span class="ml-auto">© 2024 stocknear. All Rights Reserved.</span>
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex flex-col">
|
||||
<div class="border-b border-gray-700 w-full"/>
|
||||
<span class="flex flex-row items-center mt-6 text-md text-gray-300 mb-5">
|
||||
© 2024 stocknear. All Rights Reserved.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
164
src/lib/components/FundamentalAnalysis.svelte
Normal file
164
src/lib/components/FundamentalAnalysis.svelte
Normal file
@ -0,0 +1,164 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
|
||||
export let fundamentalAnalysisDict;
|
||||
export let data;
|
||||
|
||||
let deactivateContent = data?.user?.tier === 'Pro' ? false : true;
|
||||
|
||||
|
||||
//let showMore = false;
|
||||
//let oneMonthResult;
|
||||
let accuracy;
|
||||
let precision;
|
||||
let flowSentiment = 'n/a';
|
||||
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined' && Object?.keys(fundamentalAnalysisDict)?.length !== 0) {
|
||||
flowSentiment = fundamentalAnalysisDict?.sentiment;
|
||||
accuracy = fundamentalAnalysisDict?.accuracy;
|
||||
precision = fundamentalAnalysisDict?.precision;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2 ">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="fundamentalAnalysisInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
AI Fundamental Analysis
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Fundamental Analysis"}
|
||||
content={`We trained our model using historical fundamental data such as revenue, net income, price to book ratio etc. to predict if the price of the next quarter will be higher than the previous one. We computed the precision and accuracy with unseen company data to evaluate its performance.`}
|
||||
id={"fundamentalAnalysisInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if Object?.keys(fundamentalAnalysisDict)?.length !== 0}
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
Our model uses fundamental data only to predict the next quarter. Here are the stats of the model for {$displayCompanyName} to ensure transparency and reliability.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="w-full mt-5 mb-5 flex justify-start items-center">
|
||||
<div class="w-full grid grid-cols-2 lg:grid-cols-3 gap-y-3 gap-x-3 ">
|
||||
<!--Start Flow Sentiment-->
|
||||
<div class="flex flex-row items-center flex-wrap w-full px-3 sm:px-5 bg-[#202020] shadow-lg rounded-2xl h-20">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="font-medium text-gray-200 text-sm">Quarter Sentiment</span>
|
||||
{#if !deactivateContent}
|
||||
<span class="text-start text-[1rem] font-medium {flowSentiment === 'Bullish' ? 'text-[#10DB06]' : 'text-[#FC2120]'}">{flowSentiment}</span>
|
||||
{:else}
|
||||
<a href="/pricing" class="text-blue-400 mt-1 hover:text-white font-medium text-sm flex justify-center items-center">
|
||||
Unlock with Pro
|
||||
<svg class="ml-1 w-5 h-5 inline-block"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>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--End Flow Sentiment-->
|
||||
<!--Start Put/Call-->
|
||||
<div class="flex flex-row items-center flex-wrap w-full px-3 sm:px-5 bg-[#202020] shadow-lg rounded-2xl h-20">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="font-medium text-gray-200 text-sm ">Accuracy</span>
|
||||
<span class="text-start text-sm sm:text-[1rem] font-medium text-white">
|
||||
{accuracy >=65 ? 'Good' : accuracy >= 50 ? 'Moderate' : 'Bad'}
|
||||
</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 {accuracy >= 65 ? 'text-[#00FC50]' : accuracy >= 50 ? 'text-[#F8901E]' : 'text-[#FF2F1F]'}" stroke-width="3" stroke-dasharray="100" stroke-dashoffset={100-(accuracy)}></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">{accuracy}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Circular Progress -->
|
||||
|
||||
</div>
|
||||
<!--End Put/Call-->
|
||||
|
||||
<!--Start Precision-->
|
||||
<div class="flex flex-row items-center flex-wrap w-full px-3 sm:px-5 bg-[#202020] shadow-lg rounded-2xl h-20">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="font-medium text-gray-200 text-sm ">Precision</span>
|
||||
<span class="text-start text-sm sm:text-[1rem] font-medium text-white">
|
||||
{precision >=65 ? 'Good' : precision >= 50 ? 'Moderate' : 'Bad'}
|
||||
</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 {precision >= 65 ? 'text-[#00FC50]' : precision >= 50 ? 'text-[#F8901E]' : 'text-[#FF2F1F]'}" stroke-width="3" stroke-dasharray="100" stroke-dashoffset={100-precision}></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">{precision}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Circular Progress -->
|
||||
|
||||
</div>
|
||||
<!--End Precision-->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-4 sm:mt-7 ml-1">
|
||||
Over the next quarter the model forecasts a
|
||||
{#if !deactivateContent}
|
||||
<span class="font-semibold {flowSentiment === 'Bullish' ? 'text-[#10DB06]' : 'text-[#FC2120]'}">{flowSentiment}</span> price movement.
|
||||
{:else}
|
||||
<a href="/pricing" class="text-blue-400 mt-1 hover:text-white font-medium text-sm">
|
||||
Unlock Prediction with Pro
|
||||
<svg class="w-5 h-5 inline-block"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>
|
||||
</a>
|
||||
price movement.
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
39
src/lib/components/GetAccess.svelte
Normal file
39
src/lib/components/GetAccess.svelte
Normal file
@ -0,0 +1,39 @@
|
||||
<script lang='ts'>
|
||||
import { goto } from "$app/navigation";
|
||||
import backgroundLogo from '$lib/images/hide_content.jpg'
|
||||
|
||||
let opacityValue = 0.2
|
||||
</script>
|
||||
|
||||
|
||||
<section class="w-full max-w-5xl overflow-hidden relative mt-4">
|
||||
|
||||
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat blur-[6px]"
|
||||
style="
|
||||
opacity: {opacityValue};
|
||||
background-image: url('{backgroundLogo}');
|
||||
">
|
||||
</div>
|
||||
|
||||
<label on:click={() => goto('/login')} class="p-5 relative cursor-pointer flex flex-col items-center justify-center mt-10 overflow-hidden">
|
||||
|
||||
<div class="rounded-full w-14 h-14 bg-[#202020] relative">
|
||||
<svg class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="white" d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240zm460 600H232V536h560v304zM484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53a48.01 48.01 0 1 0-56 0z"/></svg>
|
||||
</div>
|
||||
<span class="text-slate-300 font-medium text-md sm:text-lg mt-3 w-3/4 text-center">
|
||||
Get full access for a more in-depth Stock Analysis
|
||||
</span>
|
||||
|
||||
<label class="mt-4 w-5/6 sm:w-56 bg-[#fff] cursor-pointer py-2.5 px-4 normal-case font-medium text-center text-black rounded-full">
|
||||
<div class="flex flex-row items-center justify-center">
|
||||
<svg class="w-6 h-6 sm:w-8 sm:h-8 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><g fill="none" fill-rule="evenodd" stroke="black" stroke-linecap="round" stroke-linejoin="round"><path d="m15.5 4l3 4l-8 10l-8-10l3.009-4zm-13 4h16m-11 0l3 10m3-10l-3 10"/><path d="M5.509 4L7.5 8l3-4l3 4l2-4"/></g></svg>
|
||||
<span class="text-black text-md sm:text-[1rem] ml-2">
|
||||
Sign Up for Free
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
|
||||
</label>
|
||||
</section>
|
||||
169
src/lib/components/GovernmentContract.svelte
Normal file
169
src/lib/components/GovernmentContract.svelte
Normal file
@ -0,0 +1,169 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker, screenWidth} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
|
||||
export let contractList;
|
||||
|
||||
let optionsData;
|
||||
let avgNumberOfContracts = 0;
|
||||
let displayMaxContracts = 0;
|
||||
let displayYear = 'n/a';
|
||||
|
||||
function normalizer(value) {
|
||||
if (Math?.abs(value) >= 1e9) {
|
||||
return { unit: 'B', denominator: 1e9 };
|
||||
} else if (Math?.abs(value) >= 1e6) {
|
||||
return { unit: 'M', denominator: 1e6 };
|
||||
} else if (Math?.abs(value) >= 1e5) {
|
||||
return { unit: 'K', denominator: 1e5 };
|
||||
} else {
|
||||
return { unit: '', denominator: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPlotOptions() {
|
||||
let dates = [];
|
||||
let valueList = [];
|
||||
|
||||
// Iterate over the data and extract required information
|
||||
contractList?.forEach(item => {
|
||||
// Extract year and convert it to fiscal year format
|
||||
const fiscalYear = "FY" + item?.year?.slice(2);
|
||||
dates?.push(fiscalYear);
|
||||
|
||||
// Extract totalValue
|
||||
valueList?.push(item.totalValue);
|
||||
});
|
||||
|
||||
const {unit, denominator } = normalizer(Math.max(...valueList) ?? 0)
|
||||
|
||||
const option = {
|
||||
silent: true,
|
||||
grid: {
|
||||
left: $screenWidth < 640 ? '0%' : '2%',
|
||||
right: $screenWidth < 640 ? '5%' : '2%',
|
||||
bottom: $screenWidth < 640 ? '0%' : '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
data: dates,
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false, // Disable x-axis grid lines
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#6E7079', // Change label color to white
|
||||
formatter: function (value) {
|
||||
return '$'+(value / denominator)?.toFixed(1) + unit; // Format value in millions
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: valueList,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: 'rgb(255,255,255,0.8)' // Change bar color to white
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined' && contractList?.length !== 0) {
|
||||
|
||||
optionsData = getPlotOptions()
|
||||
|
||||
// Calculate total number of contracts
|
||||
avgNumberOfContracts = Math.floor((contractList?.reduce((sum, contract) => sum + contract?.numOfContracts, 0))/contractList?.length);
|
||||
const { year:yearWithMaxContracts, numOfContracts:maxContracts } = contractList?.reduce((max, contract) => contract?.numOfContracts > max?.numOfContracts ? contract : max, contractList?.at(0));
|
||||
displayYear = yearWithMaxContracts;
|
||||
displayMaxContracts = maxContracts
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2 ">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="governmentContractsInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Government Contracts
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Government Contracts"}
|
||||
content={"Government contracts are agreements between the local government and companies for goods or services. They can be substantial revenue sources for companies, particularly in sectors like defense, technology, and infrastructure. Winning contracts can enhance a company's stability and credibility, but it often involves competitive bidding and compliance with strict regulations."}
|
||||
id={"governmentContractsInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if contractList?.length !== 0}
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
Gain insights into government spending on {$displayCompanyName} and determine the annual value of significant government contracts awarded to the company.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full h-[300px] ">
|
||||
<Chart options={optionsData} class="chart" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
<div class="w-full text-white text-sm sm:text-[1rem] mt-6">
|
||||
The company averaged {avgNumberOfContracts} contracts annually, peaking at {displayMaxContracts} in {displayYear}.
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
215
src/lib/components/H1BVisa.svelte
Normal file
215
src/lib/components/H1BVisa.svelte
Normal file
@ -0,0 +1,215 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker, screenWidth} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import { abbreviateNumber } from "$lib/utils";
|
||||
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
|
||||
export let h1bVisaList;
|
||||
let optionsData;
|
||||
|
||||
|
||||
function getPlotOptions() {
|
||||
let dates = [];
|
||||
let engineerApplication = [];
|
||||
let saleApplication = [];
|
||||
let marketingApplication = [];
|
||||
let managerApplication = [];
|
||||
// Iterate over the data and extract required information
|
||||
h1bVisaList?.forEach(item => {
|
||||
// Extract year and convert it to fiscal year format
|
||||
const fiscalYear = "FY" + String(item?.year)?.slice(2);
|
||||
dates?.push(fiscalYear);
|
||||
|
||||
// Extract totalValue
|
||||
engineerApplication?.push(item?.engineerApplication);
|
||||
saleApplication?.push(item?.saleApplication);
|
||||
marketingApplication?.push(item?.marketingApplication);
|
||||
managerApplication?.push(item?.managerApplication);
|
||||
});
|
||||
|
||||
|
||||
|
||||
const option = {
|
||||
silent: true,
|
||||
grid: {
|
||||
left: $screenWidth < 640 ? '0%' : '2%',
|
||||
right: $screenWidth < 640 ? '5%' : '2%',
|
||||
bottom: $screenWidth < 640 ? '0%' : '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
data: dates,
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false, // Disable x-axis grid lines
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#6E7079', // Change label color to white
|
||||
formatter: function (value) {
|
||||
return '#'+value // Format value in millions
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: saleApplication,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#fff' // Change bar color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: marketingApplication,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#5470C6' // Change bar color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: managerApplication,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#FF2F1F' // Change bar color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: engineerApplication,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#F8901E' // Change bar color to white
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined' && h1bVisaList?.length !== 0) {
|
||||
|
||||
optionsData = getPlotOptions()
|
||||
|
||||
// Calculate total number of contracts
|
||||
/*
|
||||
avgTotalValue = Math.floor((h1bVisaList?.reduce((sum, item) => sum + item?.totalValue, 0))/h1bVisaList?.length);
|
||||
const { year:yearWithMaxLobbying, totalValue: maxLobbying } = h1bVisaList?.reduce((max, item) => item?.totalValue > max?.totalValue ? item : max, h1bVisaList?.at(0));
|
||||
displayYear = yearWithMaxLobbying;
|
||||
displayMaxLobbying = maxLobbying
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="h1bVisaInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Talent Acquisition
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"H1B Visa"}
|
||||
content={"Add explanation"}
|
||||
id={"h1bVisaInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if h1bVisaList?.length !== 0}
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
Explore {$displayCompanyName} talent acquisition strategies for attracting top global talent, securing exceptional employees worldwide through the H1B Visa program, spanning sales to engineering roles.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{'/stocks/'+$stockTicker+'/stats/employees'}" class="text-blue-400 hover:text-white flex justify-end mt-3 text-sm sm:text-[1rem]">
|
||||
Full Report
|
||||
</a>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full h-[300px] ">
|
||||
<Chart options={optionsData} class="chart" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
<div class="flex flex-row items-center justify-between mx-auto mt-5 w-full sm:w-5/6">
|
||||
<div class="flex flex-col sm:flex-row items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#F8901E] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-sm sm:text-md inline-block">
|
||||
Engineer
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-center w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#fff] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-sm sm:text-md sm:font-medium inline-block">
|
||||
Sale
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#5470C6] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-sm sm:text-md inline-block">
|
||||
Marketing
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-center ml-3 sm:ml-0 w-1/2 justify-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-3 h-3 bg-[#FF2F1F] border-4 box-content border-gray-900 rounded-full transform sm:-translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="mt-2 sm:mt-0 text-white text-sm sm:text-md inline-block">
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
325
src/lib/components/InfiniteLoading.svelte
Normal file
325
src/lib/components/InfiniteLoading.svelte
Normal file
@ -0,0 +1,325 @@
|
||||
<!--Source: https://github.com/skayo/svelte-infinite-loading/blob/master/src/InfiniteLoading.svelte-->
|
||||
|
||||
<script context="module">
|
||||
|
||||
const THROTTLE_LIMIT = 50;
|
||||
const LOOP_CHECK_TIMEOUT = 1000;
|
||||
const LOOP_CHECK_MAX_CALLS = 10;
|
||||
|
||||
const ERROR_INFINITE_LOOP = [
|
||||
`executed the callback function more than ${LOOP_CHECK_MAX_CALLS} times for a short time, it looks like searched a wrong scroll wrapper that doest not has fixed height or maximum height, please check it. If you want to force to set a element as scroll wrapper rather than automatic searching, you can do this:`,
|
||||
'<!-- add a special attribute for the real scroll wrapper (can also be data-infinite-wrapper) -->',
|
||||
'<div infinite-wrapper>',
|
||||
' ...',
|
||||
' <!-- set forceUseInfiniteWrapper -->',
|
||||
' <InfiniteLoading forceUseInfiniteWrapper>',
|
||||
'</div>',
|
||||
'or',
|
||||
'<div class="infinite-wrapper">',
|
||||
' ...',
|
||||
' <!-- set forceUseInfiniteWrapper as css selector of the real scroll wrapper -->',
|
||||
' <InfiniteLoading forceUseInfiniteWrapper=".infinite-wrapper" />',
|
||||
'</div>',
|
||||
].join('\n');
|
||||
|
||||
|
||||
/**
|
||||
* the third argument for event bundler
|
||||
* @see https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
|
||||
*/
|
||||
const thirdEventArg = (() => {
|
||||
let supportsPassive = false;
|
||||
|
||||
try {
|
||||
const opts = Object.defineProperty({}, 'passive', {
|
||||
get() {
|
||||
supportsPassive = { passive: true };
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
window.addEventListener('testPassive', null, opts);
|
||||
window.removeEventListener('testPassive', null, opts);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
return supportsPassive;
|
||||
})();
|
||||
|
||||
|
||||
const throttler = {
|
||||
timers: [],
|
||||
caches: [],
|
||||
|
||||
throttle(fn) {
|
||||
if (this.caches.indexOf(fn) === -1) {
|
||||
// cache current handler
|
||||
this.caches.push(fn);
|
||||
|
||||
// save timer for current handler
|
||||
this.timers.push(setTimeout(() => {
|
||||
fn();
|
||||
|
||||
// empty cache and timer
|
||||
this.caches.splice(this.caches.indexOf(fn), 1);
|
||||
this.timers.shift();
|
||||
}, THROTTLE_LIMIT));
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
// reset all timers
|
||||
this.timers.forEach((timer) => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
this.timers.length = 0;
|
||||
|
||||
// empty caches
|
||||
this.caches = [];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const loopTracker = {
|
||||
isChecked: false,
|
||||
timer: null,
|
||||
times: 0,
|
||||
|
||||
track() {
|
||||
// record track times
|
||||
this.times += 1;
|
||||
|
||||
// try to mark check status
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.isChecked = true;
|
||||
}, LOOP_CHECK_TIMEOUT);
|
||||
|
||||
// throw warning if the times of continuous calls large than the maximum times
|
||||
if (this.times > LOOP_CHECK_MAX_CALLS) {
|
||||
console.error(ERROR_INFINITE_LOOP);
|
||||
this.isChecked = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const scrollBarStorage = {
|
||||
key: '_infiniteScrollHeight',
|
||||
|
||||
getScrollElement(element) {
|
||||
return element === window ? document.documentElement : element;
|
||||
},
|
||||
|
||||
save(element) {
|
||||
const target = this.getScrollElement(element);
|
||||
|
||||
// save scroll height on the scroll parent
|
||||
target[this.key] = target.scrollHeight;
|
||||
},
|
||||
|
||||
restore(element) {
|
||||
const target = this.getScrollElement(element);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (typeof target[this.key] === 'number') {
|
||||
target.scrollTop = target.scrollHeight - target[this.key] + target.scrollTop;
|
||||
}
|
||||
|
||||
this.remove(target);
|
||||
},
|
||||
|
||||
remove(element) {
|
||||
if (element[this.key] !== undefined) {
|
||||
// remove scroll height
|
||||
delete element[this.key]; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function isVisible(element) {
|
||||
return element && (element.offsetWidth + element.offsetHeight) > 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const STATUS = {
|
||||
READY: 0,
|
||||
LOADING: 1,
|
||||
COMPLETE: 2,
|
||||
ERROR: 3,
|
||||
};
|
||||
|
||||
export let distance = 100;
|
||||
export let direction = 'bottom';
|
||||
export let forceUseInfiniteWrapper = false;
|
||||
export let identifier = +new Date();
|
||||
|
||||
let isFirstLoad = true; // save the current loading whether it is the first loading
|
||||
let status = STATUS.READY;
|
||||
let mounted = false;
|
||||
let thisElement;
|
||||
let scrollParent;
|
||||
|
||||
|
||||
|
||||
const stateChanger = {
|
||||
loaded: async () => {
|
||||
isFirstLoad = false;
|
||||
|
||||
if (direction === 'top') {
|
||||
// wait for DOM updated
|
||||
await tick();
|
||||
|
||||
scrollBarStorage.restore(scrollParent);
|
||||
}
|
||||
|
||||
if (status === STATUS.LOADING) {
|
||||
await tick();
|
||||
await attemptLoad(true);
|
||||
}
|
||||
},
|
||||
|
||||
complete: async () => {
|
||||
status = STATUS.COMPLETE;
|
||||
|
||||
// force re-complation computed properties to fix the problem of get slot text delay
|
||||
await tick();
|
||||
|
||||
scrollParent.removeEventListener('scroll', scrollHandler, thirdEventArg);
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
status = STATUS.READY;
|
||||
isFirstLoad = true;
|
||||
|
||||
scrollBarStorage.remove(scrollParent);
|
||||
|
||||
scrollParent.addEventListener('scroll', scrollHandler, thirdEventArg);
|
||||
|
||||
// wait for list to be empty and the empty action may trigger a scroll event
|
||||
setTimeout(() => {
|
||||
throttler.reset();
|
||||
scrollHandler();
|
||||
}, 1);
|
||||
},
|
||||
|
||||
error: () => {
|
||||
status = STATUS.ERROR;
|
||||
throttler.reset();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function scrollHandler(event) {
|
||||
if (status === STATUS.READY) {
|
||||
if (event && event.constructor === Event && isVisible(thisElement)) {
|
||||
throttler.throttle(attemptLoad);
|
||||
} else {
|
||||
attemptLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to trigger load
|
||||
async function attemptLoad(isContinuousCall) {
|
||||
if (status !== STATUS.COMPLETE && isVisible(thisElement) && getCurrentDistance() <= distance) {
|
||||
status = STATUS.LOADING;
|
||||
|
||||
if (direction === 'top') {
|
||||
// wait for spinner display
|
||||
await tick();
|
||||
|
||||
scrollBarStorage.save(scrollParent);
|
||||
}
|
||||
|
||||
dispatch('infinite', stateChanger);
|
||||
|
||||
if (isContinuousCall && !forceUseInfiniteWrapper && !loopTracker.isChecked) {
|
||||
// check this component whether be in an infinite loop if it is not checked
|
||||
loopTracker.track();
|
||||
}
|
||||
} else if (status === STATUS.LOADING) {
|
||||
status = STATUS.READY;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current distance from the specified direction
|
||||
function getCurrentDistance() {
|
||||
let distance;
|
||||
|
||||
if (direction === 'top') {
|
||||
distance = typeof scrollParent.scrollTop === 'number' ? scrollParent.scrollTop : scrollParent.pageYOffset;
|
||||
} else {
|
||||
const infiniteElementOffsetTopFromBottom = thisElement.getBoundingClientRect().top;
|
||||
const scrollElementOffsetTopFromBottom = scrollParent === window ? window.innerHeight : scrollParent.getBoundingClientRect().bottom;
|
||||
|
||||
distance = infiniteElementOffsetTopFromBottom - scrollElementOffsetTopFromBottom;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
// Get the first scroll parent of an element
|
||||
function getScrollParent(element = thisElement) {
|
||||
let result;
|
||||
|
||||
if (typeof forceUseInfiniteWrapper === 'string') {
|
||||
result = document.querySelector(forceUseInfiniteWrapper);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
if (element.tagName === 'BODY') {
|
||||
result = window;
|
||||
} else if (!forceUseInfiniteWrapper && ['scroll', 'auto'].indexOf(getComputedStyle(element).overflowY) > -1) {
|
||||
result = element;
|
||||
} else if (element.hasAttribute('infinite-wrapper') || element.hasAttribute('data-infinite-wrapper')) {
|
||||
result = element;
|
||||
}
|
||||
}
|
||||
|
||||
return result || getScrollParent(element.parentNode);
|
||||
}
|
||||
|
||||
function updateScrollParent() {
|
||||
if (mounted) scrollParent = getScrollParent();
|
||||
}
|
||||
|
||||
function identifierUpdated() {
|
||||
if (mounted) stateChanger.reset();
|
||||
}
|
||||
|
||||
// Watch forceUseInfiniteWrapper and mounted
|
||||
$: forceUseInfiniteWrapper, mounted, updateScrollParent();
|
||||
|
||||
// Watch identifier and mounted
|
||||
$: identifier, mounted, identifierUpdated();
|
||||
|
||||
onMount(async () => {
|
||||
mounted = true;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollHandler();
|
||||
scrollParent.addEventListener('scroll', scrollHandler, thirdEventArg);
|
||||
}, 1);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (mounted && status !== STATUS.COMPLETE) {
|
||||
throttler.reset();
|
||||
scrollBarStorage.remove(scrollParent);
|
||||
scrollParent.removeEventListener('scroll', scrollHandler, thirdEventArg);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="infinite-loading-container" bind:this={thisElement}>
|
||||
|
||||
|
||||
</div>
|
||||
35
src/lib/components/InfoModal.svelte
Normal file
35
src/lib/components/InfoModal.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
|
||||
|
||||
export let title;
|
||||
export let content;
|
||||
export let id;
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<label for={id} class="cursor-pointer ml-1">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="gray"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title></title> <g id="Complete"> <g id="info-circle"> <g> <circle cx="12" cy="12" data-name="--Circle" fill="none" id="_--Circle" r="10" stroke="gray" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></circle> <line fill="none" stroke="gray" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="12" x2="12" y1="12" y2="16"></line> <line fill="none" stroke="gray" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="12" x2="12" y1="8" y2="8"></line> </g> </g> </g> </g></svg>
|
||||
</label>
|
||||
|
||||
<!-- Desktop modal using dialog component -->
|
||||
<input type="checkbox" id={id} class="modal-toggle" />
|
||||
|
||||
<dialog id={id} class="modal modal-bottom sm:modal-middle">
|
||||
|
||||
|
||||
<label for={id} class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
|
||||
|
||||
<!-- Desktop modal content -->
|
||||
<div class="modal-box w-full bg-[#191919] flex flex-col items-center">
|
||||
<div class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]" />
|
||||
<div class="text-white mb-5 text-center">
|
||||
<h3 class="font-bold text-2xl mb-5">{title}</h3>
|
||||
<span class="text-white text-[1rem] font-normal">{@html content}</span>
|
||||
</div>
|
||||
|
||||
<label for={id} class="sm:hidden cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#0DDE00] text-center text-black text-[1rem] font-normal">OK</label>
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
241
src/lib/components/Input.svelte
Normal file
241
src/lib/components/Input.svelte
Normal file
@ -0,0 +1,241 @@
|
||||
<script lang='ts'>
|
||||
import { onMount } from 'svelte';
|
||||
//import { linkTitle } from '$lib/store';
|
||||
|
||||
export let value = '';
|
||||
export let placeholder = '';
|
||||
export let id;
|
||||
export let label;
|
||||
export let type = 'text';
|
||||
export let disabled = false;
|
||||
export let required = false;
|
||||
export let maxLength = 100;
|
||||
export let showCounter = false;
|
||||
export let hidden = false;
|
||||
export let errors;
|
||||
export let useTitle = false; // new prop
|
||||
|
||||
let videoInput;
|
||||
let showVideo = false;
|
||||
|
||||
onMount( () => {
|
||||
window.addEventListener('dragover', (event) => event.preventDefault());
|
||||
window.addEventListener('drop', (event) => event.preventDefault());
|
||||
})
|
||||
|
||||
const showPreview = (event) => {
|
||||
const target = event.target;
|
||||
const files = target.files;
|
||||
if (files.length > 0) {
|
||||
|
||||
if (['.mp4', '.webm'].some(format => files[0]?.name?.includes(format)) )
|
||||
{
|
||||
videoInput = URL.createObjectURL(files[0]);
|
||||
console.log(videoInput)
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
const src = URL.createObjectURL(files[0]);
|
||||
|
||||
const preview = document.getElementById('image-preview');
|
||||
preview.src = src;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
let inputValue = value;
|
||||
|
||||
|
||||
//$: value = useTitle === false ? $linkTitle : '';
|
||||
|
||||
|
||||
|
||||
let counterColor;
|
||||
|
||||
$: counter = `${inputValue.length}/${maxLength}`;
|
||||
|
||||
$: {
|
||||
if (inputValue.length > maxLength) {
|
||||
counterColor = 'text-error';
|
||||
} else {
|
||||
counterColor = 'text-white';
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
inputValue = event.target.value;
|
||||
|
||||
if(inputValue.includes('.mp4'))
|
||||
{
|
||||
showVideo = true;
|
||||
}
|
||||
/*
|
||||
if (useTitle) {
|
||||
// Check if the input value is a valid URL
|
||||
try {
|
||||
const urlObject = new URL(inputValue);
|
||||
const url = urlObject.href;
|
||||
getTitle(url);
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
// The URL is not valid, so don't do anything
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
$: value = useTitle === false ? $linkTitle : '';
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
async function getTitle(url) {
|
||||
try {
|
||||
|
||||
|
||||
const response = await fetch('/api/create-post', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(url)
|
||||
});
|
||||
|
||||
const output = await response.json();
|
||||
linkTitle.update( value => output);
|
||||
|
||||
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
function handleDrop(e) {
|
||||
e.preventDefault()
|
||||
|
||||
let element_id = e.detail
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleCancel= () => {
|
||||
inputValue = '';
|
||||
showVideo = false;
|
||||
}
|
||||
let isHovering = false;
|
||||
|
||||
|
||||
$: {
|
||||
if(inputValue)
|
||||
{
|
||||
errors = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="form-control w-full max-w-lg mb-2 {hidden ? 'hidden' : ''}">
|
||||
<label for={id} class="label font-medium pb-1">
|
||||
<span class="text-white label-text">{label}</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
{#if type === 'file'}
|
||||
<label for={id} class="flex flex-col items-center bg-[#313131] rounded-md cursor-pointer {inputValue.length === 0 ? 'p-10' : ''} {isHovering ? 'ring-2' : ''}"
|
||||
on:dragenter={() => isHovering = true}
|
||||
on:dragleave={() => isHovering = false}
|
||||
on:drop={handleDrop}
|
||||
>
|
||||
|
||||
|
||||
|
||||
{#if inputValue.length !== 0}
|
||||
<label on:click={handleCancel} class="btn btn-sm btn-circle bg-red-600 absolute right-0 -top-3 z-20">✕</label>
|
||||
|
||||
<div class="absolute inset-0 bg-cover object-fill bg-center bg-[#000]"></div>
|
||||
<img class="w-auto max-h-[400px] object-fill bg-center bg-contain z-10 {showVideo ? 'hidden' : ''} "
|
||||
alt="Image preview"
|
||||
id="image-preview"
|
||||
loading="lazy" />
|
||||
{:else}
|
||||
{#if !isHovering}
|
||||
<svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path opacity="0.4" d="M22.0206 16.8198L18.8906 9.49978C18.3206 8.15978 17.4706 7.39978 16.5006 7.34978C15.5406 7.29978 14.6106 7.96978 13.9006 9.24978L12.0006 12.6598C11.6006 13.3798 11.0306 13.8098 10.4106 13.8598C9.78063 13.9198 9.15063 13.5898 8.64063 12.9398L8.42063 12.6598C7.71063 11.7698 6.83063 11.3398 5.93063 11.4298C5.03063 11.5198 4.26063 12.1398 3.75063 13.1498L2.02063 16.5998C1.40063 17.8498 1.46063 19.2998 2.19063 20.4798C2.92063 21.6598 4.19063 22.3698 5.58063 22.3698H18.3406C19.6806 22.3698 20.9306 21.6998 21.6706 20.5798C22.4306 19.4598 22.5506 18.0498 22.0206 16.8198Z" fill="white" ></path> <path d="M6.96984 8.38012C8.83657 8.38012 10.3498 6.86684 10.3498 5.00012C10.3498 3.13339 8.83657 1.62012 6.96984 1.62012C5.10312 1.62012 3.58984 3.13339 3.58984 5.00012C3.58984 6.86684 5.10312 8.38012 6.96984 8.38012Z" fill="white"></path> </g></svg>
|
||||
<span class="mt-2 mb-4 text-gray-200">Choose a file to upload</span>
|
||||
{:else}
|
||||
<svg class="w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#9D64D9" d="M6 20q-.825 0-1.413-.588T4 18v-3h2v3h12v-3h2v3q0 .825-.588 1.413T18 20H6Zm5-4V7.85l-2.6 2.6L7 9l5-5l5 5l-1.4 1.45l-2.6-2.6V16h-2Z"/></svg>
|
||||
<span class="mt-2 mb-4 text-gray-200">Drop here to upload</span>
|
||||
{/if}
|
||||
<span class="text-xs sm:text-sm text-gray-400 m-auto mb-5 ">
|
||||
We support jpg/jpeg, png, webp, mp4 and gif.
|
||||
</span>
|
||||
<span class="text-xs sm:text-sm text-gray-400 m-auto mb-5 ">
|
||||
File must be smaller than 10MB.
|
||||
</span>
|
||||
<label for={id} class="cursor-pointer rounded-full bg-blue-700 text-sm text-white font-bold w-auto p-3">
|
||||
Choose File
|
||||
</label>
|
||||
|
||||
{/if}
|
||||
<input class="hidden rounded text-gray-300"
|
||||
{type}
|
||||
{placeholder}
|
||||
{required}
|
||||
{disabled}
|
||||
{id}
|
||||
name={id}
|
||||
value={inputValue}
|
||||
accept="image/gif"
|
||||
on:change={showPreview}
|
||||
on:input={handleInput}
|
||||
autocomplete="off"
|
||||
|
||||
/>
|
||||
|
||||
{#if showVideo}
|
||||
<div class="absolute inset-0 bg-cover object-fill bg-center bg-[#000]"></div>
|
||||
<video controls
|
||||
class="w-auto max-h-[500px] z-10"
|
||||
src={videoInput}
|
||||
>
|
||||
</video>
|
||||
{/if}
|
||||
|
||||
|
||||
</label>
|
||||
{:else}
|
||||
<input
|
||||
class="input input-bordered w-full max-w-lg bg-[#313131] placeholder-gray-300 text-white whitespace-normal ring-2"
|
||||
{type}
|
||||
{placeholder}
|
||||
{required}
|
||||
{disabled}
|
||||
{id}
|
||||
name={id}
|
||||
value={inputValue}
|
||||
on:input={handleInput}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{/if}
|
||||
{#if showCounter}
|
||||
<div class="flex justify-end mt-1 -mb-1">
|
||||
<span class={`label-text text-xs ${counterColor}`}>{counter}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if errors}
|
||||
<label for={id} class="label py-0 pt-1">
|
||||
<span class="label-text-alt text-error">
|
||||
{errors}
|
||||
</span>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
25
src/lib/components/Katex.svelte
Normal file
25
src/lib/components/Katex.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import katex from "katex";
|
||||
export let math;
|
||||
export let displayMode = false;
|
||||
export let formula = false;
|
||||
|
||||
const options = {
|
||||
displayMode: displayMode,
|
||||
throwOnError: false
|
||||
}
|
||||
|
||||
$: katexString = katex.renderToString(math, options);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="{formula === false ? 'hidden' : ''} text-gray-200 text-lg mt-3 sm:text-xl underline italic">
|
||||
Formula:
|
||||
</h1>
|
||||
|
||||
<div class="mt-1 text-sm sm:text-[1rem]">
|
||||
{@html katexString}
|
||||
</div>
|
||||
23
src/lib/components/Lazy.svelte
Normal file
23
src/lib/components/Lazy.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { onMount} from 'svelte'
|
||||
|
||||
let el
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry.target !== el) return
|
||||
show = entry.isIntersecting
|
||||
if (show) observer.disconnect()
|
||||
})
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<slot />
|
||||
{:else}
|
||||
<div bind:this={el}/>
|
||||
{/if}
|
||||
173
src/lib/components/Lobbying.svelte
Normal file
173
src/lib/components/Lobbying.svelte
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker, screenWidth} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import { abbreviateNumber } from "$lib/utils";
|
||||
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
|
||||
export let lobbyingList;
|
||||
|
||||
let optionsData;
|
||||
let avgTotalValue = 0;
|
||||
let displayMaxLobbying = 0;
|
||||
let displayYear = 'n/a';
|
||||
|
||||
function normalizer(value) {
|
||||
if (Math?.abs(value) >= 1e9) {
|
||||
return { unit: 'B', denominator: 1e9 };
|
||||
} else if (Math?.abs(value) >= 1e6) {
|
||||
return { unit: 'M', denominator: 1e6 };
|
||||
} else if (Math?.abs(value) >= 1e5) {
|
||||
return { unit: 'K', denominator: 1e3 };
|
||||
} else {
|
||||
return { unit: '', denominator: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPlotOptions() {
|
||||
let dates = [];
|
||||
let valueList = [];
|
||||
// Iterate over the data and extract required information
|
||||
lobbyingList?.forEach(item => {
|
||||
// Extract year and convert it to fiscal year format
|
||||
const fiscalYear = "FY" + String(item?.year)?.slice(2);
|
||||
dates?.push(fiscalYear);
|
||||
|
||||
// Extract totalValue
|
||||
valueList?.push(item.totalValue);
|
||||
});
|
||||
|
||||
|
||||
const {unit, denominator } = normalizer(Math.max(...valueList) ?? 0)
|
||||
|
||||
const option = {
|
||||
silent: true,
|
||||
grid: {
|
||||
left: $screenWidth < 640 ? '0%' : '2%',
|
||||
right: $screenWidth < 640 ? '5%' : '2%',
|
||||
bottom: $screenWidth < 640 ? '0%' : '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
data: dates,
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false, // Disable x-axis grid lines
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#6E7079', // Change label color to white
|
||||
formatter: function (value) {
|
||||
return '$'+(value / denominator)?.toFixed(1) + unit; // Format value in millions
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: valueList,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#F8901E' // Change bar color to white
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined' && lobbyingList?.length !== 0) {
|
||||
|
||||
optionsData = getPlotOptions()
|
||||
|
||||
// Calculate total number of contracts
|
||||
avgTotalValue = Math.floor((lobbyingList?.reduce((sum, item) => sum + item?.totalValue, 0))/lobbyingList?.length);
|
||||
const { year:yearWithMaxLobbying, totalValue: maxLobbying } = lobbyingList?.reduce((max, item) => item?.totalValue > max?.totalValue ? item : max, lobbyingList?.at(0));
|
||||
displayYear = yearWithMaxLobbying;
|
||||
displayMaxLobbying = maxLobbying
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2 ">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="lobbyingInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Senate Lobbying
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Senate Lobbying"}
|
||||
content={"Lobbying the Senate involves special interest groups hiring professional advocates to influence lawmakers and government policies. It is a constitutionally protected activity, but critics argue it can undermine democratic representation by giving disproportionate influence to wealthy and well-organized groups."}
|
||||
id={"lobbyingInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if lobbyingList?.length !== 0}
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
Explore {$displayCompanyName}'s lobbying strategy by analyzing their annual spending to influence lawmakers towards favorable regulation and legislation alignment.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full h-[300px] ">
|
||||
<Chart options={optionsData} class="chart" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
<div class="w-full text-white text-sm sm:text-[1rem] mt-6">
|
||||
The company allocated an average of {abbreviateNumber(avgTotalValue,true)} annually towards lobbying efforts, reaching its peak at {abbreviateNumber(displayMaxLobbying,true)} in {displayYear}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
603
src/lib/components/LoginPopup.svelte
Normal file
603
src/lib/components/LoginPopup.svelte
Normal file
File diff suppressed because one or more lines are too long
239
src/lib/components/Markdown.svelte
Normal file
239
src/lib/components/Markdown.svelte
Normal file
@ -0,0 +1,239 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { markdownValue, markdownClicked } from '$lib/store';
|
||||
import showdown from 'showdown';
|
||||
|
||||
export let id;
|
||||
export let outputCounter;
|
||||
export let name;
|
||||
|
||||
|
||||
|
||||
const converter = new showdown.Converter();
|
||||
|
||||
const toolbarOptions = {
|
||||
container: [
|
||||
['bold'],
|
||||
['italic'],
|
||||
['link'],
|
||||
['image'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
let quill;
|
||||
let html;
|
||||
let value;
|
||||
|
||||
async function initializeQuillEditor()
|
||||
{
|
||||
const container = document.getElementById(id);
|
||||
|
||||
try {
|
||||
const { default: Quill } = await import('quill');
|
||||
|
||||
quill = new Quill(container, {
|
||||
modules: {
|
||||
toolbar: toolbarOptions,
|
||||
},
|
||||
theme: 'snow',
|
||||
placeholder: 'Markdown your code here...',
|
||||
keepFocus: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
|
||||
quill.on('editor-change', function() {
|
||||
const editor = quill.root;
|
||||
// Add text-4xl class to all h1 elements
|
||||
editor.querySelectorAll('h1').forEach((h1) => {
|
||||
h1.classList.add('text-4xl');
|
||||
});
|
||||
editor.querySelectorAll('h2').forEach((h2) => {
|
||||
h2.classList.add('text-3xl');
|
||||
});
|
||||
editor.querySelectorAll('h3').forEach((h3) => {
|
||||
h3.classList.add('text-xl');
|
||||
});
|
||||
editor.querySelectorAll('h4').forEach((h4) => {
|
||||
h4.classList.add('text-md');
|
||||
});
|
||||
editor.querySelectorAll('h5').forEach((h5) => {
|
||||
h5.classList.add('text-sm');
|
||||
});
|
||||
editor.querySelectorAll('h6').forEach((h6) => {
|
||||
h6.classList.add('text-xs');
|
||||
});
|
||||
|
||||
editor.querySelectorAll('a').forEach((a) => {
|
||||
a.classList.add('text-blue-400', 'hover:text-white','underline');
|
||||
});
|
||||
|
||||
editor.querySelectorAll('ol').forEach((ol) => {
|
||||
ol.classList.add('list-decimal','ml-10','mt-3');
|
||||
});
|
||||
|
||||
editor.querySelectorAll('ul').forEach((ul) => {
|
||||
ul.classList.add('list-disc','ml-10','mt-3');
|
||||
});
|
||||
|
||||
|
||||
$: value = editor.innerHTML;
|
||||
|
||||
const contents = editor.innerHTML;
|
||||
markdownValue.update((value) => [
|
||||
...value.filter((item) => item.id !== id),
|
||||
{ id: id , name: name, input: contents},
|
||||
]);
|
||||
|
||||
|
||||
console.log($markdownValue)
|
||||
|
||||
// Force Svelte to update the DOM
|
||||
$: {}
|
||||
});
|
||||
|
||||
// Convert the markdown value to HTML and set it as the content of the Quill editor
|
||||
html = converter.makeHtml($markdownValue[id]?.input || '');
|
||||
quill.setContents(quill.clipboard.convert(html));
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
await initializeQuillEditor()
|
||||
});
|
||||
|
||||
|
||||
onDestroy(() => {
|
||||
if (quill) {
|
||||
quill.off('text-change');
|
||||
quill = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/** Dispatch event on click outside of node */
|
||||
function clickOutside(node) {
|
||||
|
||||
const handleClick = event => {
|
||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||
node.dispatchEvent(
|
||||
new CustomEvent('click_outside', node)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleClick, true);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside(event) {
|
||||
$markdownClicked[id] = false;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
|
||||
$markdownClicked[id] = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function hasContent(html) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html;
|
||||
let content = el.firstChild.innerHTML.trim();
|
||||
// Remove <br> tags
|
||||
content = content.replace(/<br\s*\/?>/gi, '');
|
||||
|
||||
return content !== '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
function keyCombination(event) {
|
||||
|
||||
if (event.key === "Enter" && event.shiftKey) {
|
||||
$markdownClicked[id] = false;
|
||||
event.preventDefault();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$: {
|
||||
if($markdownClicked)
|
||||
{
|
||||
console.log($markdownClicked)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<div on:keydown={keyCombination} on:click={handleClick} use:clickOutside on:click_outside={handleClickOutside} class="text-gray-100 w-full sm:w-3/4 max-w-2xl mt-3 rounded-lg ">
|
||||
|
||||
|
||||
<div class="{$markdownClicked[id] === false ? 'hidden' : '' } text-gray-100 w-full sm:w-3/4 max-w-2xl mb-4 rounded-lg ">
|
||||
|
||||
<textarea bind:value={value} id ={id} class="bg-[#1A1A27] min-h-12 w-full" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="{$markdownClicked[id] === true ? 'hidden' : '' } text-white mt-3 cursor-pointer w-full sm:w-3/4 max-w-2xl text-sm p-2">
|
||||
{#each $markdownValue as mk}
|
||||
{#if mk?.id === id}
|
||||
<div class="flex flex-col">
|
||||
<span class="text-gray-400 mb-4">[{outputCounter}] : </span>
|
||||
<div class="border-b border-slate-800 rounded-md ">
|
||||
{#if hasContent(value)}
|
||||
{@html value}
|
||||
{:else}
|
||||
<p class="italic text-gray-400">Markdown your code here...</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
{/each}
|
||||
</div >
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
@import '/src/lib/assets/style_quill.css';
|
||||
|
||||
</style>
|
||||
202
src/lib/components/Markethour.svelte
Normal file
202
src/lib/components/Markethour.svelte
Normal file
File diff suppressed because one or more lines are too long
171
src/lib/components/MiniPlot.svelte
Normal file
171
src/lib/components/MiniPlot.svelte
Normal file
@ -0,0 +1,171 @@
|
||||
<script>
|
||||
import {ColorType } from 'lightweight-charts';
|
||||
import {Chart, BaselineSeries, PriceLine} from 'svelte-lightweight-charts';
|
||||
import { screenWidth, etfTicker } from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let title;
|
||||
export let priceData;
|
||||
export let changesPercentage;
|
||||
export let previousClose;
|
||||
|
||||
|
||||
const topLineColor = '#71CA96';
|
||||
const topFillColor1 = 'rgba( 38, 166, 154, 0.2)'
|
||||
const bottomLineColor = '#FF7070';
|
||||
const bottomFillColor1 = 'rgba(239, 83, 80, 0.2)';
|
||||
|
||||
let width = ($screenWidth <= 900 && $screenWidth > 700) ? 260 : ($screenWidth <= 700 && $screenWidth >=600 ) ? 200 : ($screenWidth < 600 && $screenWidth >=500 ) ? 150 : 80;
|
||||
|
||||
|
||||
//Initial height of graph
|
||||
let height = 50;
|
||||
|
||||
let observer;
|
||||
let ref;
|
||||
let chart;
|
||||
|
||||
ref = (element) => {
|
||||
if (observer) {
|
||||
observer?.disconnect();
|
||||
}
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
observer = new ResizeObserver(([entry]) => {
|
||||
width = entry.contentRect.width;
|
||||
height = entry.contentRect.height;
|
||||
});
|
||||
observer.observe(element);
|
||||
}
|
||||
|
||||
|
||||
const THEMES = {
|
||||
Dark: {
|
||||
chart: {
|
||||
layout: {
|
||||
background: {
|
||||
type: ColorType.Solid,
|
||||
color: '#202020',
|
||||
},
|
||||
lineColor: '#2B2B43',
|
||||
textColor: '#D9D9D9',
|
||||
},
|
||||
crosshair: {
|
||||
mode: 2,
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
visible: false,
|
||||
},
|
||||
horzLines: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
series: {
|
||||
baseValue: { type: 'price', price: priceData?.at(0)?.value },
|
||||
priceLineVisible: false,
|
||||
baseLineVisible: true,
|
||||
baseLineColor: '#B2B5BE',
|
||||
baseLineWidth: 1,
|
||||
baseLineStyle: 1,
|
||||
lineWidth: 1.5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const AVAILABLE_THEMES = Object?.keys(THEMES);
|
||||
|
||||
let selected = AVAILABLE_THEMES[0];
|
||||
$: theme = THEMES[selected];
|
||||
|
||||
const options = {
|
||||
width: width,
|
||||
height: height,
|
||||
rightPriceScale: {
|
||||
visible: false,
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: '#FFFFFF',
|
||||
textColor: '#FFFFFF',
|
||||
visible: false,
|
||||
fixLeftEdge: true,
|
||||
fixRightEdge: true,
|
||||
},
|
||||
handleScale: {
|
||||
mouseWheel: false,
|
||||
},
|
||||
handleScroll: {
|
||||
mouseWheel: false,
|
||||
horzTouchDrag: false,
|
||||
vertTouchDrag: false,
|
||||
pressedMouseMove: false,
|
||||
},
|
||||
}
|
||||
|
||||
function etfSelector() {
|
||||
if (title === 'S&P500') {
|
||||
etfTicker.update(value => 'SPY');
|
||||
goto('/etf/SPY');
|
||||
}
|
||||
else if (title === 'Nasdaq 100') {
|
||||
etfTicker.update(value => 'QQQ');
|
||||
goto('/etf/QQQ');
|
||||
}
|
||||
else if (title === 'Dow Jones') {
|
||||
etfTicker.update(value => 'DIA');
|
||||
goto('/etf/DIA');
|
||||
}
|
||||
else if (title === 'Russel 2000') {
|
||||
etfTicker.update(value => 'IWM');
|
||||
goto('/etf/IWM');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$: {
|
||||
if(chart && typeof window !== 'undefined')
|
||||
{
|
||||
chart?.timeScale()?.fitContent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<label on:click={etfSelector} class="sm:hover:border-[#3C74D4] duration-200 transition ease-in-out cursor-pointer flex flex-row items-center rounded-lg shadow-lg border border-slate-800 bg-[#202020]">
|
||||
<div class="flex flex-col items-center lg:mr-5">
|
||||
<span class="text-white font-bold text-xs w-20 text-center">{title}</span>
|
||||
<div class="flex flex-row mt-1 items-center">
|
||||
{#if changesPercentage >=0}
|
||||
<svg class="w-5 h-5 -mr-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#10db06" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="text-[#10DB06] text-xs font-medium">+{changesPercentage?.toFixed(2)}%</span>
|
||||
{:else}
|
||||
<svg class="w-5 h-5 -mr-0.5 rotate-180" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#FF2F1F" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="text-[#FF2F1F] text-xs font-medium">{changesPercentage?.toFixed(2)}% </span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Chart {...options} {...theme.chart} autoSize={true} ref={(ref) => chart = ref}>
|
||||
<BaselineSeries
|
||||
data={priceData}
|
||||
{...theme.series}
|
||||
topLineColor ={topLineColor}
|
||||
topFillColor1 ={topFillColor1}
|
||||
bottomLineColor ={bottomLineColor}
|
||||
bottomFillColor1 ={bottomFillColor1}
|
||||
>
|
||||
<PriceLine
|
||||
price={priceData?.at(0)?.value}
|
||||
lineWidth = {1}
|
||||
color="#fff"
|
||||
/>
|
||||
</BaselineSeries>
|
||||
</Chart>
|
||||
</label>
|
||||
|
||||
374
src/lib/components/MobileCommentEditor.svelte
Normal file
374
src/lib/components/MobileCommentEditor.svelte
Normal file
@ -0,0 +1,374 @@
|
||||
<script lang='ts'>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import {commentAdded} from '$lib/store';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
|
||||
export let data;
|
||||
export let postId = '';
|
||||
|
||||
let post = data?.getOnePost;
|
||||
|
||||
let imageId = 'image-input';
|
||||
|
||||
let expandField = false;
|
||||
|
||||
//let characterCount = 0;
|
||||
|
||||
|
||||
|
||||
let inputValue = '';
|
||||
let imageInput = '';
|
||||
let imageComment;
|
||||
|
||||
|
||||
const showPreview = (event) => {
|
||||
const target = event.target;
|
||||
const files = target.files;
|
||||
if (files.length > 0) {
|
||||
const src = URL.createObjectURL(files[0]);
|
||||
const preview = document.getElementById('image-preview');
|
||||
preview.src = src;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const createComment = async (event) => {
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
let output;
|
||||
let newComment;
|
||||
const userId = data?.user?.id;
|
||||
|
||||
if(inputValue.length !== 0)
|
||||
{
|
||||
const postData = {
|
||||
'comment': inputValue,
|
||||
'post': postId,
|
||||
'image': imageInput.length !== 0 ? imageInput[0] : '',
|
||||
'user': userId
|
||||
}
|
||||
|
||||
/*
|
||||
const response = await fetch(fastifyURL+'/create-comment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
output = await response.json()
|
||||
}
|
||||
*/
|
||||
//Create comment in User Browser because i need to learn how to transfer images.
|
||||
try {
|
||||
newComment = await pb.collection('comments').create(postData);
|
||||
|
||||
newComment = await pb.collection("comments").getOne(newComment?.id, {
|
||||
expand: `user`})
|
||||
|
||||
|
||||
//User always upvotes their post in the intial state
|
||||
await pb.collection("comments").update(newComment.id, {
|
||||
"vote+": 1,
|
||||
})
|
||||
|
||||
const opPost = await pb.collection('posts').getOne(postId)
|
||||
//create new record for notifications collections
|
||||
if (userId !== opPost?.user)
|
||||
{
|
||||
let formDataNotifications = new FormData();
|
||||
formDataNotifications.append('opUser', opPost?.user);
|
||||
formDataNotifications.append('user', userId)
|
||||
formDataNotifications.append('post', postId);
|
||||
formDataNotifications.append('comment', newComment?.id)
|
||||
formDataNotifications.append('notifyType', 'comment');
|
||||
|
||||
await pb.collection('notifications').create(formDataNotifications);
|
||||
}
|
||||
output = 'success'
|
||||
|
||||
}
|
||||
catch(e) {
|
||||
console.log(e);
|
||||
output = 'failure'
|
||||
}
|
||||
|
||||
if (output === 'success') {
|
||||
toast.success('Commented successfully', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
|
||||
$commentAdded = newComment;
|
||||
|
||||
//console.log("comment added: ", $commentAdded)
|
||||
inputValue = '';
|
||||
imageInput = '';
|
||||
|
||||
handleCancel()
|
||||
|
||||
|
||||
}
|
||||
else {
|
||||
toast.error('Something went wrong. Please try again...', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
function handleInput(event) {
|
||||
inputValue = event.target.value;
|
||||
|
||||
const textarea = event.target;
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 140) + 'px';
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (editor)
|
||||
{
|
||||
editor.innerHTML = '';
|
||||
}
|
||||
inputValue = '';
|
||||
imageInput = '';
|
||||
const closePopup = document.getElementById("mobileTextEditorModal");
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
|
||||
const textarea = document.querySelector('textarea');
|
||||
textarea.style.height = '36px'; // 48px are h-12 in tailwindcss
|
||||
|
||||
}
|
||||
|
||||
function handleImageInput(event) {
|
||||
imageInput = event.target.files;
|
||||
|
||||
expandField=true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const toolbarOptions = {
|
||||
container: [
|
||||
['bold', 'italic', 'underline','strike'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
['link'],
|
||||
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
let quill;
|
||||
let editor;
|
||||
const regex = /^(\s*<p><br><\/p>\s*)*$/;
|
||||
|
||||
let loadingEditor = false;
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
const { default: Quill } = await import('quill');
|
||||
|
||||
quill = new Quill('.quill-editor', {
|
||||
modules: {
|
||||
toolbar: toolbarOptions,
|
||||
},
|
||||
theme: 'snow',
|
||||
placeholder: "Leave a comment...",
|
||||
keepFocus: true,
|
||||
});
|
||||
|
||||
|
||||
quill.on('text-change', function() {
|
||||
editor = quill.root;
|
||||
// Add text-4xl class to all h1 elements
|
||||
editor.querySelectorAll('h1').forEach((h1) => {
|
||||
h1.classList.add('text-4xl');
|
||||
});
|
||||
editor.querySelectorAll('h2').forEach((h2) => {
|
||||
h2.classList.add('text-3xl');
|
||||
});
|
||||
editor.querySelectorAll('h3').forEach((h3) => {
|
||||
h3.classList.add('text-xl');
|
||||
});
|
||||
editor.querySelectorAll('h4').forEach((h4) => {
|
||||
h4.classList.add('text-md');
|
||||
});
|
||||
editor.querySelectorAll('h5').forEach((h5) => {
|
||||
h5.classList.add('text-sm');
|
||||
});
|
||||
editor.querySelectorAll('h6').forEach((h6) => {
|
||||
h6.classList.add('text-xs');
|
||||
});
|
||||
|
||||
editor.querySelectorAll('a').forEach((a) => {
|
||||
a.classList.add('text-blue-400', 'hover:text-white','underline');
|
||||
});
|
||||
|
||||
editor.querySelectorAll('ol').forEach((ol) => {
|
||||
ol.classList.add('list-decimal', 'ml-10','text-sm');
|
||||
});
|
||||
|
||||
editor.querySelectorAll('ul').forEach((ul) => {
|
||||
ul.classList.add('list-disc', 'ml-10','text-sm');
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
editor.querySelectorAll('pre').forEach((pre) => {
|
||||
pre.classList.add('bg-[#23241F]', 'w-auto','h-auto','max-w-6xl','breaks-all');
|
||||
});
|
||||
*/
|
||||
|
||||
const contents = editor.innerHTML;
|
||||
inputValue = contents;
|
||||
|
||||
console.log(inputValue)
|
||||
|
||||
if (regex?.test(inputValue)) {
|
||||
editor.innerHTML = '';
|
||||
inputValue = '';
|
||||
}
|
||||
|
||||
//console.log(inputValue)
|
||||
|
||||
// Force Svelte to update the DOM
|
||||
$: {}
|
||||
});
|
||||
|
||||
loadingEditor = true;
|
||||
});
|
||||
|
||||
|
||||
onDestroy(() => {
|
||||
if (quill) {
|
||||
quill.off('text-change');
|
||||
quill = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
1
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
<div class="drawer drawer-end overflow-hidden" style="z-index: 9999;">
|
||||
<input id="mobileTextEditorModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-hidden">
|
||||
<div class="rounded-xl bg-[#000] min-h-screen w-screen pb-20 overflow-y-auto overflow-hidden">
|
||||
<!--
|
||||
<h1 class="text-white sm:hidden font-medium text-xl mt-10 pl-2">
|
||||
{@html post?.title}
|
||||
</h1>
|
||||
-->
|
||||
|
||||
<h1 class="text-white text-lg pl-7 pt-16">
|
||||
Share your opinion
|
||||
</h1>
|
||||
|
||||
<!--Start Quill Editor-->
|
||||
<div class="{!loadingEditor ? 'hidden' : ''} mt-3 pl-7 pr-7">
|
||||
<div class="quill-editor min-h-[96px] h-[120px] resize-none focus-none ring-none rounded-none bg-[#2A303C] text-white">
|
||||
<select class="ql-header" aria-label="Header" title="Header">
|
||||
<option selected></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<textarea placeholder="Loading editor..." class="{loadingEditor ? 'hidden' : ''} min-h-[96px] h-[120px] text-sm italic w-full resize-none focus-none ring-none rounded-none bg-[#2A303C] text-white"></textarea>
|
||||
<!--End Quill Editor-->
|
||||
|
||||
|
||||
<textarea
|
||||
class="hidden"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
on:input={handleInput}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row justify-start mt-4 pb-16 pl-7 pr-7">
|
||||
|
||||
<div class="relative">
|
||||
|
||||
|
||||
<label for={imageId} class="{imageInput.length !== 0 ? 'hidden' : ''} inline-flex mr-auto items-center py-2.5 px-4 text-xs font-medium text-center text-white rounded-lg hover:bg-gray-800 cursor-pointer">
|
||||
|
||||
|
||||
<svg class="w-5 h-5 inline-block" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path opacity="0.4" d="M22.0206 16.8198L18.8906 9.49978C18.3206 8.15978 17.4706 7.39978 16.5006 7.34978C15.5406 7.29978 14.6106 7.96978 13.9006 9.24978L12.0006 12.6598C11.6006 13.3798 11.0306 13.8098 10.4106 13.8598C9.78063 13.9198 9.15063 13.5898 8.64063 12.9398L8.42063 12.6598C7.71063 11.7698 6.83063 11.3398 5.93063 11.4298C5.03063 11.5198 4.26063 12.1398 3.75063 13.1498L2.02063 16.5998C1.40063 17.8498 1.46063 19.2998 2.19063 20.4798C2.92063 21.6598 4.19063 22.3698 5.58063 22.3698H18.3406C19.6806 22.3698 20.9306 21.6998 21.6706 20.5798C22.4306 19.4598 22.5506 18.0498 22.0206 16.8198Z" fill="#A6ADBB" ></path> <path d="M6.96984 8.38012C8.83657 8.38012 10.3498 6.86684 10.3498 5.00012C10.3498 3.13339 8.83657 1.62012 6.96984 1.62012C5.10312 1.62012 3.58984 3.13339 3.58984 5.00012C3.58984 6.86684 5.10312 8.38012 6.96984 8.38012Z" fill="#E5E7EB"></path> </g></svg>
|
||||
|
||||
<input class="hidden rounded text-gray-200"
|
||||
type="file"
|
||||
id={imageId}
|
||||
name={imageId}
|
||||
on:input={handleImageInput}
|
||||
on:change={showPreview}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{#if imageInput.length !== 0}
|
||||
<div class="ml-2">
|
||||
<label on:click={() => imageInput = ''} class="btn btn-xs btn-circle bg-red-600 absolute -top-2 -right-2">
|
||||
✕
|
||||
</label>
|
||||
<img class="object-contain w-10 h-10 mx-auto rounded"
|
||||
alt="Image preview"
|
||||
id="image-preview" />
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
<div class="relative flex justify-end items-center ml-auto mr-2">
|
||||
<label on:click={handleCancel} class="inline-flex justify-end items-center py-2.5 px-4 text-xs font-medium text-center text-white rounded-lg hover:bg-gray-800 cursor-pointer mr-3">
|
||||
Cancel
|
||||
</label>
|
||||
<label on:click={createComment} class="inline-flex justify-end items-center bg-blue-700 {inputValue.length !== 0 ? 'opacity-100 cursor-pointer' : 'opacity-60'} py-2.5 px-4 text-xs font-medium text-center text-white rounded-lg focus:ring-purple-300">
|
||||
Post
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label on:click={() => handleCancel()} class="absolute left-6 top-4">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
@import '/src/lib/assets/style_quill.css';
|
||||
|
||||
.quill-editor, textarea {
|
||||
touch-action: none; /* or 'none' if manipulation doesn't work */
|
||||
}
|
||||
|
||||
</style>
|
||||
38
src/lib/components/NotificationBell.svelte
Normal file
38
src/lib/components/NotificationBell.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { numberOfUnreadNotification } from '$lib/store';
|
||||
|
||||
export let data;
|
||||
export let hasUnreadElement;
|
||||
|
||||
|
||||
async function toggle() {
|
||||
hasUnreadElement = false;
|
||||
goto('/notifications')
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--Start Notification Bell-->
|
||||
{#if data?.user}
|
||||
|
||||
<div on:click={toggle} class="relative m-auto ml-1 mr-1 sm:ml-2.5 sm:mr-4">
|
||||
<label class="sm:hover:text-[#756EFF] text-gray-300 hover:text-white cursor-pointer">
|
||||
<svg class="w-7 h-7 inline-block" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M12.0196 2.91016C8.7096 2.91016 6.0196 5.60016 6.0196 8.91016V11.8002C6.0196 12.4102 5.7596 13.3402 5.4496 13.8602L4.2996 15.7702C3.5896 16.9502 4.0796 18.2602 5.3796 18.7002C9.6896 20.1402 14.3396 20.1402 18.6496 18.7002C19.8596 18.3002 20.3896 16.8702 19.7296 15.7702L18.5796 13.8602C18.2796 13.3402 18.0196 12.4102 18.0196 11.8002V8.91016C18.0196 5.61016 15.3196 2.91016 12.0196 2.91016Z" stroke="currentColor" stroke-width="1" stroke-miterlimit="10" stroke-linecap="round"></path><path d="M13.8699 3.19994C13.5599 3.10994 13.2399 3.03994 12.9099 2.99994C11.9499 2.87994 11.0299 2.94994 10.1699 3.19994C10.4599 2.45994 11.1799 1.93994 12.0199 1.93994C12.8599 1.93994 13.5799 2.45994 13.8699 3.19994Z" stroke="currentColor" stroke-width="1" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path><path opacity="0.4" d="M15.0195 19.0601C15.0195 20.7101 13.6695 22.0601 12.0195 22.0601C11.1995 22.0601 10.4395 21.7201 9.89953 21.1801C9.35953 20.6401 9.01953 19.8801 9.01953 19.0601" stroke="currentColor" stroke-width="1" stroke-miterlimit="10"></path></svg>
|
||||
</label>
|
||||
|
||||
{#if hasUnreadElement}
|
||||
<div class="absolute top-2 -right-2 flex">
|
||||
<div class="relative inline-flex text-xs text-white font-bold w-[18px] h-[18px] bg-red-500 border-[1px] border-gray-700 rounded-full -top-3 right-2">
|
||||
<span class="m-auto -my-0.5">{$numberOfUnreadNotification <= 9 ? $numberOfUnreadNotification : '9+' }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
193
src/lib/components/OptionsData.svelte
Normal file
193
src/lib/components/OptionsData.svelte
Normal file
@ -0,0 +1,193 @@
|
||||
<script lang='ts'>
|
||||
import { assetType, stockTicker, etfTicker, displayCompanyName} from "$lib/store";
|
||||
import { abbreviateNumber } from "$lib/utils";
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { LayerCake, Html } from 'layercake';
|
||||
import Circle from '$lib/components/Circle/Circle.html.svelte';
|
||||
|
||||
export let optionsDict = {};
|
||||
export let data;
|
||||
|
||||
let deactivateContent = data?.user?.tier === 'Pro' ? false : true;
|
||||
|
||||
|
||||
function changeStatement(event)
|
||||
{
|
||||
displayTimePeriod = event.target.value;
|
||||
}
|
||||
|
||||
function allValuesZero(obj) {
|
||||
return Object?.values(obj)?.every(value => value === 0);
|
||||
}
|
||||
|
||||
|
||||
let displayTimePeriod = 'oneMonth'
|
||||
let signal = '';
|
||||
let callVolume;
|
||||
let putVolume;
|
||||
let dataset;
|
||||
|
||||
const idKey = 'contract';
|
||||
const valueKey = 'value';
|
||||
let rawData = [];
|
||||
|
||||
let checkIfNotZero:boolean;
|
||||
|
||||
|
||||
$: {
|
||||
if (displayTimePeriod && Object?.keys(optionsDict)?.length !== 0)
|
||||
{
|
||||
checkIfNotZero = false;
|
||||
rawData = [];
|
||||
try {
|
||||
dataset = optionsDict[displayTimePeriod]
|
||||
}
|
||||
catch(e) {
|
||||
dataset = {};
|
||||
}
|
||||
|
||||
|
||||
callVolume = dataset?.callVolume
|
||||
putVolume = dataset?.putVolume
|
||||
|
||||
const totalVolume = dataset?.putVolume + dataset?.callVolume;
|
||||
const callProportion = Math.ceil((dataset.callVolume / totalVolume) * 100);
|
||||
const putProportion = 100- callProportion;
|
||||
|
||||
rawData.push({ 'contract': 'calls', value: callProportion });
|
||||
rawData.push({ 'contract': 'puts', value: putProportion });
|
||||
signal = callProportion >= putProportion ? 'Bullish' : 'Bearish';
|
||||
checkIfNotZero = allValuesZero(dataset);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden text-white h-full w-full sm:mb-10">
|
||||
<div class="flex justify-center w-full m-auto h-full overflow-hidden">
|
||||
<div class="relative flex justify-center items-center overflow-hidden w-full">
|
||||
<main class="w-full">
|
||||
<div class="w-full sm:max-w-2xl m-auto mt-5 sm:mt-0">
|
||||
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="optionsInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Latest Options Activity
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Options Activity"}
|
||||
content={`Monitor real-time unusual options activity from big instiutional traders and hedge funds of ${$displayCompanyName} with a 1-minute delay, which can significantly influence stock prices.`}
|
||||
id={"optionsInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-row items-end justify-between">
|
||||
<select class="mt-5 sm:mb-0 ml-1 w-36 select select-bordered select-sm p-0 pl-5 bg-[#2A303C]" on:change={changeStatement}>
|
||||
<option disabled>Choose a time period</option>
|
||||
<option disabled={deactivateContent} value="oneDay">
|
||||
{!deactivateContent ? 'Last 24h' : 'Last 24h (Pro Only)'}
|
||||
</option>
|
||||
<option disabled={deactivateContent} value="oneWeek">
|
||||
{!deactivateContent ? 'Last Week' : 'Last Week (Pro Only)'}
|
||||
</option>
|
||||
<option value="oneMonth" selected>
|
||||
Last Month
|
||||
</option>
|
||||
<option value="threeMonth">Last 3 Months</option>
|
||||
</select>
|
||||
|
||||
|
||||
<a href={$assetType === 'stock' ? `/stocks/${$stockTicker}/options` : `/etf/${$etfTicker}/options`} class="text-sm sm:text-[1rem] sm:hover:text-white text-blue-400 font-normal">Full report</a>
|
||||
</div>
|
||||
|
||||
{#if !checkIfNotZero}
|
||||
<div class="flex flex-row items-center justify-center m-auto w-full h-auto mt-4">
|
||||
|
||||
<div class="flex flex-col items-start w-full">
|
||||
|
||||
|
||||
|
||||
<div class="chart-container ">
|
||||
<LayerCake
|
||||
padding={{ top: 0, bottom: 0, left: 0, right: 10 }}
|
||||
data={rawData}
|
||||
>
|
||||
<Html>
|
||||
<Circle
|
||||
idKey={idKey}
|
||||
valueKey={valueKey}
|
||||
textStrokeWidth={-0.5}
|
||||
/>
|
||||
</Html>
|
||||
</LayerCake>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
The Options activity signals a
|
||||
{#if signal === 'Bullish' }
|
||||
<span class="text-[#10DB06]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#10db06" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"><path d="m3 17l6-6l4 4l8-8"/><path d="M17 7h4v4"/></g></svg>
|
||||
{signal}
|
||||
</span>
|
||||
|
||||
{:else if signal === 'Bearish' }
|
||||
<span class="text-[#E57C34]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#ff2f1f" d="M244 136v64a12 12 0 0 1-12 12h-64a12 12 0 0 1 0-24h35l-67-67l-31.51 31.52a12 12 0 0 1-17 0l-72-72a12 12 0 0 1 17-17L96 127l31.51-31.52a12 12 0 0 1 17 0L220 171v-35a12 12 0 0 1 24 0Z"/></svg>
|
||||
{signal}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-[#FF2F1F]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#e57c34" d="m22 12l-4-4v3H3v2h15v3l4-4Z"/></svg>
|
||||
Neutral
|
||||
</span>
|
||||
{/if} trend.
|
||||
</div>
|
||||
|
||||
<span class="text-[1rem] text-white">
|
||||
In the past {displayTimePeriod ==='oneDay' ? '24h': displayTimePeriod==='oneWeek' ? 'week' : displayTimePeriod==='oneMonth' ? 'month' : 'three months'}, hedge funds and major institutional traders have bought {abbreviateNumber(callVolume)} calls and {abbreviateNumber(putVolume)} puts with an average DTE of {dataset?.avgDTE} days.
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<div class="flex justify-center items-center m-auto mt-16 mb-6">
|
||||
<div class="text-gray-100 text-sm sm:text-[1rem] sm:rounded-lg h-auto border border-slate-800 p-4">
|
||||
<svg class="w-5 h-5 inline-block sm:mr-2 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#60a5fa" 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>
|
||||
No Options activity found
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
/*
|
||||
The wrapper div needs to have an explicit width and height in CSS.
|
||||
It can also be a flexbox child or CSS grid element.
|
||||
The point being it needs dimensions since the <LayerCake> element will
|
||||
expand to fill it.
|
||||
*/
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
</style>
|
||||
687
src/lib/components/PostSection.svelte
Normal file
687
src/lib/components/PostSection.svelte
Normal file
@ -0,0 +1,687 @@
|
||||
<script lang = 'ts'>
|
||||
|
||||
import {getImageURL, formatDate} from '$lib/utils';
|
||||
import VideoPlayer from '$lib/components/VideoPlayer.svelte';
|
||||
import Share from '$lib/components/Share.svelte';
|
||||
import Downvote from '$lib/components/Downvote.svelte';
|
||||
import Upvote from '$lib/components/Upvote.svelte';
|
||||
|
||||
import {userRegion, tagList, postIdDeleted} from '$lib/store';
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
|
||||
export let posts;
|
||||
export let moderators;
|
||||
export let data;
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let apiURL = import.meta.env.VITE_EU_API_URL; // Set a default API URL
|
||||
|
||||
let fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
apiURL = import.meta.env.VITE_USEAST_API_URL;
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
apiURL = import.meta.env.VITE_EU_API_URL;
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
let deletePostId = posts?.id
|
||||
|
||||
|
||||
|
||||
let isVideoClicked = {};
|
||||
|
||||
|
||||
function isModerator(userId) {
|
||||
return moderators?.some(moderator => userId === moderator?.user);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let upvoteButtonClicked = {};
|
||||
let downvoteButtonClicked = {};
|
||||
let upvoteCounter = {};
|
||||
let downvoteCounter = {};
|
||||
let videoId = {};
|
||||
let userAlreadyVoted;
|
||||
|
||||
upvoteCounter[posts.id] = posts.upvote;
|
||||
downvoteCounter[posts.id] = posts.downvote;
|
||||
|
||||
|
||||
userAlreadyVoted = posts?.expand['alreadyVoted(post)']?.some(item => item?.user === data?.user?.id);
|
||||
|
||||
|
||||
if (userAlreadyVoted) {
|
||||
upvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'upvote';
|
||||
downvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'downvote';
|
||||
} else {
|
||||
upvoteButtonClicked[posts.id] = false;
|
||||
downvoteButtonClicked[posts.id] = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
|
||||
|
||||
await navigator.clipboard.writeText("https://stocknear.com/community/post/"+posts?.id);
|
||||
|
||||
toast.success('Link copied', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleDeletePost = async () => {
|
||||
|
||||
|
||||
const postData = {'postId': posts?.id,
|
||||
'userId': posts?.user
|
||||
};
|
||||
|
||||
const response = await fetch(fastifyURL+'/delete-post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
const output = (await response.json())?.items;
|
||||
if (output === 'success')
|
||||
{
|
||||
$postIdDeleted = posts.id;
|
||||
|
||||
}
|
||||
|
||||
if (output === 'success') {
|
||||
toast.success('Post deleted', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
} else {
|
||||
toast.error('Something went wrong', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleUpvote = async (event) => {
|
||||
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
|
||||
const postId = event.target.postId.value;
|
||||
const postData = {
|
||||
'postId': postId,
|
||||
'userId': data?.user?.id,
|
||||
};
|
||||
|
||||
upvoteButtonClicked[postId] = !upvoteButtonClicked[postId];
|
||||
|
||||
|
||||
if (upvoteButtonClicked[postId]) {
|
||||
if (downvoteButtonClicked[postId]) {
|
||||
upvoteCounter[postId] += 1;
|
||||
downvoteCounter[postId] -= 1;
|
||||
downvoteButtonClicked[postId] = false;
|
||||
} else {
|
||||
upvoteCounter[postId]++;
|
||||
}
|
||||
} else {
|
||||
upvoteCounter[postId]--;
|
||||
}
|
||||
const response = await fetch(fastifyURL+'/upvote', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
};
|
||||
|
||||
const handleDownvote = async (event) => {
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
|
||||
const postId = event.target.postId.value;
|
||||
const postData = {
|
||||
'postId': postId,
|
||||
'userId': data?.user?.id,
|
||||
};
|
||||
|
||||
downvoteButtonClicked[postId] = !downvoteButtonClicked[postId];
|
||||
|
||||
|
||||
if (downvoteButtonClicked[postId]) {
|
||||
if (upvoteButtonClicked[postId]) {
|
||||
downvoteCounter[postId] += 1;
|
||||
upvoteCounter[postId] -= 1;
|
||||
upvoteButtonClicked[postId] = false;
|
||||
} else {
|
||||
downvoteCounter[postId]++;
|
||||
}
|
||||
} else {
|
||||
downvoteCounter[postId]--;
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(fastifyURL+'/downvote', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function checkIfYoutubeVideo() {
|
||||
if (posts.postType === 'link') {
|
||||
const url = new URL(posts.link);
|
||||
if (url.hostname === "www.youtube.com") {
|
||||
const searchParams = url.searchParams;
|
||||
searchParams.delete('t'); // Remove the "t" parameter
|
||||
|
||||
const videoIdMatch = url.search.match(/v=([^&]+)/);
|
||||
if (videoIdMatch) {
|
||||
videoId[posts.id] = videoIdMatch[1];
|
||||
}
|
||||
} else {
|
||||
videoId[posts.id] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clickVideo(event)
|
||||
{
|
||||
event.preventDefault();
|
||||
isVideoClicked[posts.id] = true;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
checkIfYoutubeVideo()
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
if($postIdDeleted.length !== 0)
|
||||
{
|
||||
|
||||
upvoteCounter[posts.id] = posts.upvote;
|
||||
downvoteCounter[posts.id] = posts.downvote;
|
||||
|
||||
|
||||
userAlreadyVoted = posts?.expand['alreadyVoted(post)']?.some(item => item?.user === data?.user?.id);
|
||||
|
||||
|
||||
if (userAlreadyVoted) {
|
||||
upvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'upvote';
|
||||
downvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'downvote';
|
||||
} else {
|
||||
upvoteButtonClicked[posts.id] = false;
|
||||
downvoteButtonClicked[posts.id] = false;
|
||||
}
|
||||
|
||||
checkIfYoutubeVideo()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Posts -->
|
||||
|
||||
<div class="flex flex-row items-start w-full mt-5 relative">
|
||||
<div class="hidden lg:flex flex-col items-center absolute -top-2 -left-[70px]">
|
||||
<!--Start Upvote-->
|
||||
<form on:submit={handleUpvote}>
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Upvote/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full">
|
||||
{#if upvoteButtonClicked[posts?.id]}
|
||||
<Upvote state='active'/>
|
||||
{:else}
|
||||
<Upvote />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End Upvote-->
|
||||
<label class="px-6 py-4 w-14 rounded-lg bg-[#202020] border border-gray-700 text-[1rem] text-bold text-white">
|
||||
{upvoteCounter[posts?.id] - downvoteCounter[posts?.id] }
|
||||
</label>
|
||||
<!--Start Downvote-->
|
||||
<form on:submit={handleDownvote}>
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Downvote />
|
||||
</label>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full">
|
||||
{#if downvoteButtonClicked[posts?.id]}
|
||||
<Downvote state='active'/>
|
||||
{:else}
|
||||
<Downvote/>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End Downvote-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-[#202020] border sm:hover:border-slate-600 border-gray-700 rounded-none sm:rounded-lg">
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col">
|
||||
<!-- Item -->
|
||||
<div class="">
|
||||
<div class=" flex justify-between items-center mt-5 pb-2">
|
||||
|
||||
<div class="pl-2 flex flex-col items-center ">
|
||||
|
||||
{#if posts?.pinned}
|
||||
<div class="flex flex-row items-center mr-auto mb-4">
|
||||
<svg class="w-6 h-6 inline-block ml-2 mr-2" fill="#75D377" viewBox="0 0 1920 1920" xmlns="http://www.w3.org/2000/svg" stroke="#75D377"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M1154.976 0 988.342 166.52c-60.448 60.447-63.436 153.418-15.4 220.646L670.359 689.751c-4.022 4.022-6.55 8.964-9.079 13.79-147.212-61.022-328.671-34.246-444.626 81.709l-98.027 98.141 418.31 418.195-520.129 520.129c-22.41 22.409-22.41 58.724 0 81.248 11.262 11.147 25.972 16.778 40.682 16.778s29.42-5.63 40.567-16.778l520.128-520.129 418.195 418.31 98.142-98.142c75.962-75.847 117.793-176.862 117.793-284.313 0-56.195-12.067-110.208-33.787-160.198 2.758-1.839 5.861-2.988 8.275-5.516l303.963-303.964c29.19 21.145 63.896 33.097 100.67 33.097 46.083 0 89.293-17.928 121.93-50.565L1920 764.909 1154.976 0Z" fill-rule="evenodd"></path> </g></svg>
|
||||
<span class="text-slate-300 text-[0.8rem] sm:text-sm font-bold uppercase">
|
||||
Pinned by moderators
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<a href={'/community/user/'+posts?.expand?.user?.id} class="flex flex-row items-center justify-start mr-auto">
|
||||
<label class="avatar w-7 h-7 flex-shrink-0 text-white text-xs sm:text-sm ml-1">
|
||||
<img class="flex-shrink-0 inline-block bg-slate-300 rounded-full"
|
||||
src={posts?.expand?.user?.avatar
|
||||
? getImageURL(posts?.expand?.user?.collectionId, posts?.expand?.user?.id, posts?.expand?.user?.avatar)
|
||||
: `https://api.dicebear.com/7.x/thumbs/svg?seed=${posts?.expand?.user?.username}`}
|
||||
alt="User avatar" />
|
||||
</label>
|
||||
<span class="text-white ml-2 inline-block text-sm">
|
||||
{posts?.expand?.user?.username}
|
||||
</span>
|
||||
{#if isModerator(posts?.user)}
|
||||
<svg class="inline-block ml-1 w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#75d377" d="M256 32C174 69.06 121.38 86.46 32 96c0 77.59 5.27 133.36 25.29 184.51a348.86 348.86 0 0 0 71.43 112.41c49.6 52.66 104.17 80.4 127.28 87.08c23.11-6.68 77.68-34.42 127.28-87.08a348.86 348.86 0 0 0 71.43-112.41C474.73 229.36 480 173.59 480 96c-89.38-9.54-142-26.94-224-64Z"/></svg>
|
||||
{/if}
|
||||
|
||||
<span class="text-white font-bold ml-1 mr-1">
|
||||
·
|
||||
</span>
|
||||
<span class="text-white text-xs">
|
||||
{formatDate(posts?.created)} ago
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start settings-->
|
||||
<div class="flex justify-end items-center dropdown dropdown-bottom dropdown-end mr-4">
|
||||
|
||||
|
||||
<a href={'/community/post/'+posts?.id} class="hidden lg:block ml-4 mr-4 flex justify-center items-center">
|
||||
<svg class="w-5 h-5 sm:w-4 sm:h-4 mt-1 inline-block mr-3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve" fill="#D6D6DC"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" fill="#D6D6DC" d="M60,0H4C1.789,0,0,1.789,0,4v40c0,2.211,1.789,4,4,4h8v12 c0,1.617,0.973,3.078,2.469,3.695C14.965,63.902,15.484,64,16,64c1.039,0,2.062-0.406,2.828-1.172L33.656,48H60c2.211,0,4-1.789,4-4 V4C64,1.789,62.211,0,60,0z"></path> </g></svg>
|
||||
<span class="text-[1rem] sm:text-sm text-white font-semibold sm:font-medium">
|
||||
{#if posts?.expand['comments(post)']}
|
||||
{posts?.expand['comments(post)']?.length}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<div class="flex-shrink-0 rounded-full hover:bg-white hover:bg-opacity-[0.05] w-8 h-8 relative flex items-center justify-center">
|
||||
<label tabindex="0" class="cursor-pointer flex-shrink-0">
|
||||
<svg class="m-auto w-4 h-4 sm:w-5 sm:h-5 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<path fill="#cacaca" d="M156 128a28 28 0 1 1-28-28a28 28 0 0 1 28 28ZM48 100a28 28 0 1 0 28 28a28 28 0 0 0-28-28Zm160 0a28 28 0 1 0 28 28a28 28 0 0 0-28-28Z"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul tabindex="0" class="border border-gray-700 dropdown-content menu bg-[#313131] rounded-box w-44 z-30">
|
||||
{#if data?.user?.id === posts?.user || isModerator(data?.user?.id)}
|
||||
<li>
|
||||
<label on:click={handleCopyLink} class="text-sm text-white cursor-pointer">
|
||||
<svg class="mr-auto w-5 h-5 inline-block" viewBox="0 -4 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#7B7B7B" stroke="#7B7B7B"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>link_round [#7B7B7B]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-420.000000, -3283.000000)" fill="#7B7B7B"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M370.999774,3133 L369.999774,3133 C367.662774,3133 365.786774,3130.985 366.019774,3128.6 C366.221774,3126.522 368.089774,3125 370.177774,3125 L370.999774,3125 C371.551774,3125 371.999774,3124.552 371.999774,3124 C371.999774,3123.448 371.551774,3123 370.999774,3123 L370.251774,3123 C366.965774,3123 364.100774,3125.532 364.002774,3128.815 C363.900774,3132.213 366.624774,3135 369.999774,3135 L370.999774,3135 C371.551774,3135 371.999774,3134.552 371.999774,3134 C371.999774,3133.448 371.551774,3133 370.999774,3133 M377.747774,3123 L376.999774,3123 C376.447774,3123 375.999774,3123.448 375.999774,3124 C375.999774,3124.552 376.447774,3125 376.999774,3125 L377.821774,3125 C379.909774,3125 381.777774,3126.522 381.979774,3128.6 C382.212774,3130.985 380.336774,3133 377.999774,3133 L376.999774,3133 C376.447774,3133 375.999774,3133.448 375.999774,3134 C375.999774,3135.104 376.999774,3135 377.999774,3135 C381.374774,3135 384.098774,3132.213 383.996774,3128.815 C383.898774,3125.532 381.033774,3123 377.747774,3123 M368.999774,3128 L378.999774,3128 C379.551774,3128 379.999774,3128.448 379.999774,3129 C379.999774,3130.346 379.210774,3130 368.999774,3130 C368.447774,3130 367.999774,3129.552 367.999774,3129 C367.999774,3128.448 368.447774,3128 368.999774,3128" id="link_round-[#7B7B7B]"> </path> </g> </g> </g> </g></svg>
|
||||
Copy Link
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label for={deletePostId} class="text-sm text-[#E1341E] cursor-pointer">
|
||||
<svg class="mr-auto h-5 w-5 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#97372f" d="M7 21q-.825 0-1.413-.588T5 19V6q-.425 0-.713-.288T4 5q0-.425.288-.713T5 4h4q0-.425.288-.713T10 3h4q.425 0 .713.288T15 4h4q.425 0 .713.288T20 5q0 .425-.288.713T19 6v13q0 .825-.588 1.413T17 21H7ZM7 6v13h10V6H7Zm2 10q0 .425.288.713T10 17q.425 0 .713-.288T11 16V9q0-.425-.288-.713T10 8q-.425 0-.713.288T9 9v7Zm4 0q0 .425.288.713T14 17q.425 0 .713-.288T15 16V9q0-.425-.288-.713T14 8q-.425 0-.713.288T13 9v7ZM7 6v13V6Z"/></svg>
|
||||
Delete Post
|
||||
</label>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<label on:click={handleCopyLink} class="text-sm text-white cursor-pointer">
|
||||
<svg class="mr-auto w-5 h-5 inline-block" viewBox="0 -4 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#7B7B7B" stroke="#7B7B7B"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>link_round [#7B7B7B]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-420.000000, -3283.000000)" fill="#7B7B7B"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M370.999774,3133 L369.999774,3133 C367.662774,3133 365.786774,3130.985 366.019774,3128.6 C366.221774,3126.522 368.089774,3125 370.177774,3125 L370.999774,3125 C371.551774,3125 371.999774,3124.552 371.999774,3124 C371.999774,3123.448 371.551774,3123 370.999774,3123 L370.251774,3123 C366.965774,3123 364.100774,3125.532 364.002774,3128.815 C363.900774,3132.213 366.624774,3135 369.999774,3135 L370.999774,3135 C371.551774,3135 371.999774,3134.552 371.999774,3134 C371.999774,3133.448 371.551774,3133 370.999774,3133 M377.747774,3123 L376.999774,3123 C376.447774,3123 375.999774,3123.448 375.999774,3124 C375.999774,3124.552 376.447774,3125 376.999774,3125 L377.821774,3125 C379.909774,3125 381.777774,3126.522 381.979774,3128.6 C382.212774,3130.985 380.336774,3133 377.999774,3133 L376.999774,3133 C376.447774,3133 375.999774,3133.448 375.999774,3134 C375.999774,3135.104 376.999774,3135 377.999774,3135 C381.374774,3135 384.098774,3132.213 383.996774,3128.815 C383.898774,3125.532 381.033774,3123 377.747774,3123 M368.999774,3128 L378.999774,3128 C379.551774,3128 379.999774,3128.448 379.999774,3129 C379.999774,3130.346 379.210774,3130 368.999774,3130 C368.447774,3130 367.999774,3129.552 367.999774,3129 C367.999774,3128.448 368.447774,3128 368.999774,3128" id="link_round-[#7B7B7B]"> </path> </g> </g> </g> </g></svg>
|
||||
Copy Link
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label for="userLogin" class="text-sm cursor-pointer">
|
||||
<svg class="w-5 h-5 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M12 2c5.5 0 10 4.5 10 10s-4.5 10-10 10S2 17.5 2 12S6.5 2 12 2m0 2c-1.9 0-3.6.6-4.9 1.7l11.2 11.2c1-1.4 1.7-3.1 1.7-4.9c0-4.4-3.6-8-8-8m4.9 14.3L5.7 7.1C4.6 8.4 4 10.1 4 12c0 4.4 3.6 8 8 8c1.9 0 3.6-.6 4.9-1.7Z"/></svg>
|
||||
Report
|
||||
</label>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<!--End settings-->
|
||||
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<div class="relative mt-2">
|
||||
<a href={"/community/post/"+posts?.id}>
|
||||
{#if posts.postType === 'text'}
|
||||
<!--Start PostType Text-->
|
||||
<div class="flex flex-wrap md:flex-row">
|
||||
<div class="cursor-pointer flex items-start">
|
||||
<div class="flex-grow w-full sm:w-3/4 max-w-2xl break-all">
|
||||
|
||||
|
||||
<div class="ml-3 mt-2 flex flex-wrap sm:hover:text-[#0099FF] text-start text-[1.1rem] sm:text-xl font-semibold mb-2 flex-shrink w-fit break-normal">
|
||||
{posts?.title}
|
||||
</div>
|
||||
|
||||
{#if !posts?.pinned}
|
||||
<div class="break-normal ml-3 mt-5 pr-6 sm:pr-0">
|
||||
{@html posts?.description?.length > 182 ? posts?.description.slice(0, 182) + "..." : posts?.description}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End PostType Image-->
|
||||
{:else if posts?.postType==='image'}
|
||||
|
||||
|
||||
<!--Start PostType Image-->
|
||||
<div class="w-screen sm:w-full mt-2">
|
||||
<div class="ml-3 mr-3 flex flex-wrap sm:hover:text-[#0099FF] text-[1.1rem] sm:text-xl font-semibold text-white mb-5 relative z-10 whitenormal">
|
||||
{posts?.title}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="relative">
|
||||
{#if posts?.thumbnail && !['webm', 'mp4']?.some(format => posts?.thumbnail?.includes(format))}
|
||||
<div class="absolute inset-0 bg-cover object-fill bg-center bg-[#000]"></div>
|
||||
|
||||
<!--<div class="absolute -inset-3 md:-inset-y-20 md:mt-10 bg-cover object-contain blur-[40px]" style="clip-path: polygon(0 0, 100% 0, 100% 90%, 0 90%); background-image: url('{getImageURL(posts.collectionId, posts.id, posts.thumbnail)}');"></div>-->
|
||||
<img src={getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)} alt = "post image" class="m-auto w-auto relative max-h-[520px] sm:max-h-[700px]" style="position: relative;" loading="lazy"/>
|
||||
{:else}
|
||||
<!--show videos here-->
|
||||
|
||||
|
||||
|
||||
<VideoPlayer src={getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)} />
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--End PostType Image-->
|
||||
{:else if posts?.postType === 'link'}
|
||||
<!--Start PostType Link -->
|
||||
|
||||
{#if videoId[posts?.id]?.length === 0}
|
||||
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-center sm:items-start pr-7 pb-2">
|
||||
<div class="flex flex-col items-start w-full">
|
||||
<!-- Post Title -->
|
||||
<h2 class="pl-3 pt-2 text-start sm:hover:text-[#0099FF] text-[1.1rem] sm:text-xl font-semibold w-full mb-1 pr-5 flex-shrink break-normal">
|
||||
{posts?.title}
|
||||
</h2>
|
||||
<!-- Post Description -->
|
||||
{#if posts?.description !== 'undefined'}
|
||||
<span class="block mt-2 text-start text-gray-100 text-[0.95rem] flex-shrink break-normal pl-3 w-11/12">
|
||||
{posts?.description?.length > 120 ? posts?.description.slice(0, 120) + "..." : posts?.description}
|
||||
</span>
|
||||
{/if}
|
||||
<a href={posts?.link} target="_blank" class="mt-2 text-sm pl-3 text-gray-400 font-semibold">
|
||||
{(new URL(posts?.link))?.hostname.replace('www.','')}
|
||||
<svg class="ml-1 w-4 h-4 inline-block -mt-0.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M21 9L21 3M21 3H15M21 3L13 11M10 5H7.8C6.11984 5 5.27976 5 4.63803 5.32698C4.07354 5.6146 3.6146 6.07354 3.32698 6.63803C3 7.27976 3 8.11984 3 9.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H14.2C15.8802 21 16.7202 21 17.362 20.673C17.9265 20.3854 18.3854 19.9265 18.673 19.362C19 18.7202 19 17.8802 19 16.2V14" stroke="#A6ADBB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if posts?.thumbnail}
|
||||
<div class="cursor-pointer w-full sm:w-56 sm:ml-auto mt-5 sm:mt-2 ml-6">
|
||||
<div class="flex-shrink-0 ">
|
||||
<a href={posts?.link} target="_blank" class="relative block w-full sm:w-56 h-full sm:max-h-[120px] overflow-hidden rounded-2xl">
|
||||
<img src={getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)} class="object-cover bg-contain bg-center bg-no-repeat w-screen sm:w-full max-h-[250px] sm:h-fit rounded-2xl border border-gray-700" alt="news image" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<div class="pt-2 w-full">
|
||||
|
||||
|
||||
<div class="ml-3 pr-7 flex flex-wrap sm:hover:text-[#0099FF] text-[1.1rem] sm:text-xl font-medium text-white mb-5 relative">
|
||||
{posts?.title}
|
||||
</div>
|
||||
|
||||
{#if !isVideoClicked[posts.id] }
|
||||
|
||||
<div class="overflow-hidden bg-cover bg-no-repeat relative w-screen sm:w-full h-36 sm:h-[250px]">
|
||||
<label on:click={clickVideo} class="cursor-pointer transition duration-200 opacity-70 hover:opacity-90">
|
||||
<div class="relative transition duration-500 ease-in-out hover:scale-105">
|
||||
<img src="{getImageURL(posts.collectionId, posts.id, posts.thumbnail)}" alt="image" class="m-auto w-full h-full object-cover" loading="lazy"/>
|
||||
<svg class="w-20 h-20 absolute top-1/3 left-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path fill="none" d="M11 23a1 1 0 0 1-1-1V10a1 1 0 0 1 1.447-.894l12 6a1 1 0 0 1 0 1.788l-12 6A1.001 1.001 0 0 1 11 23Z"/>
|
||||
<path fill="#D6D6DC" d="M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2Zm7.447 14.895l-12 6A1 1 0 0 1 10 22V10a1 1 0 0 1 1.447-.894l12 6a1 1 0 0 1 0 1.788Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<iframe
|
||||
id="videoPlayer"
|
||||
class="w-full h-56 sm:h-[500px]"
|
||||
src={`https://www.youtube.com/embed/${videoId[posts.id]}`}
|
||||
frameborder="0"
|
||||
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
autoplay
|
||||
></iframe>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
<!--End PostType Link-->
|
||||
{/if}
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
<!--Start Post footer-->
|
||||
<div class="flex flex-col justify-start items-center mr-2 mt-8 ml-1.5 mb-4">
|
||||
<!--Start Tags-->
|
||||
<div class="flex flex-row items-center mr-auto ml-2">
|
||||
{#if posts?.tagline?.length !== 0}
|
||||
<label class="bg-[#D3D6DA] mr-3 text-xs h-fit font-medium text-[#171717] p-1 border rounded">
|
||||
<span class="p-0.5">{posts?.tagline}</span>
|
||||
</label>
|
||||
{/if}
|
||||
{#if posts?.tagTopic !== null}
|
||||
{#each $tagList as tag}
|
||||
{#each posts?.tagTopic as tagTopic}
|
||||
{#if tag?.name === tagTopic}
|
||||
<label class="bg-[#D3D6DA] mr-3 text-xs h-fit font-medium text-[#171717] p-1 border rounded">
|
||||
<span class="p-0.5">{tag?.name}</span>
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<!--End Tags-->
|
||||
<!--Start-Up-Down-Vote-->
|
||||
<div class="lg:hidden flex flex-row items-center justify-start ml-14 mt-6 w-full ">
|
||||
<form on:submit={handleUpvote} class="-ml-4 lg:hidden mr-2">
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Upvote/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full">
|
||||
{#if upvoteButtonClicked[posts?.id]}
|
||||
<Upvote state='active'/>
|
||||
{:else}
|
||||
<Upvote />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<span class="text-sm text-semibold lg:hidden">
|
||||
{upvoteCounter[posts?.id] - downvoteCounter[posts?.id] }
|
||||
</span>
|
||||
<form on:submit={handleDownvote} class="lg:hidden">
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full ml-2 mr-2">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Downvote />
|
||||
</label>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full ml-2 mr-2">
|
||||
{#if downvoteButtonClicked[posts?.id]}
|
||||
<Downvote state='active'/>
|
||||
{:else}
|
||||
<Downvote/>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End-Up-Down-Vote-->
|
||||
<!--Start Comment Counter-->
|
||||
<a href={'/community/post/'+posts?.id} class="ml-6 lg:hidden flex justify-center items-center rounded-md">
|
||||
<svg class="w-4 h-4 mt-1 inline-block mr-2 lg:mr-3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve" fill="#D6D6DC"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" fill="#D6D6DC" d="M60,0H4C1.789,0,0,1.789,0,4v40c0,2.211,1.789,4,4,4h8v12 c0,1.617,0.973,3.078,2.469,3.695C14.965,63.902,15.484,64,16,64c1.039,0,2.062-0.406,2.828-1.172L33.656,48H60c2.211,0,4-1.789,4-4 V4C64,1.789,62.211,0,60,0z"></path> </g></svg>
|
||||
<span class="ml-2 text-sm text-white font-semibold lg:font-medium">
|
||||
{#if posts?.expand['comments(post)']}
|
||||
{posts?.expand['comments(post)']?.length}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
<!--End Comment Counter-->
|
||||
<!--Start Share Button-->
|
||||
<Share url ={'https://stocknear.com/community/post/'+posts?.id} />
|
||||
<!--End Share Button-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Post footer-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Delete Modal-->
|
||||
<input type="checkbox" id={deletePostId} class="modal-toggle" />
|
||||
|
||||
<dialog id={deletePostId} class="modal modal-bottom sm:modal-middle ">
|
||||
|
||||
|
||||
<label for={deletePostId} class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
|
||||
<div class="modal-box bg-[#202020] p-10" >
|
||||
|
||||
|
||||
<h3 class="font-bold text-md sm:text-lg sm:mb-10">
|
||||
Are you sure you want to delete the post?
|
||||
</h3>
|
||||
|
||||
<div class="modal-action ">
|
||||
<label on:click={handleDeletePost} for={deletePostId} class="sm:ml-3 btn bg-[#FF3131] hover:bg-[#ff4343] text-white btn-md w-full rounded-lg text-white font-bold text-md">
|
||||
Proceed
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
<!--End Delete Modal-->
|
||||
|
||||
372
src/lib/components/PriceAlert.svelte
Normal file
372
src/lib/components/PriceAlert.svelte
Normal file
@ -0,0 +1,372 @@
|
||||
<script lang='ts'>
|
||||
import toast from 'svelte-french-toast';
|
||||
import {userRegion, screenWidth, openPriceAlert, displayCompanyName, stockTicker, etfTicker, cryptoTicker, assetType} from '$lib/store';
|
||||
import RangeSlider from 'svelte-range-slider-pips';
|
||||
|
||||
export let data;
|
||||
|
||||
let currentPrice = data?.getStockQuote?.price;
|
||||
let values = [0];
|
||||
let displayPrice = (currentPrice*(1+values?.at(0)/100))?.toFixed(2);
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let fastifyURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function handleCreateAlert()
|
||||
{
|
||||
if (values?.at(0) === 0)
|
||||
{
|
||||
toast.error(`Percentage change must not be zero`, {
|
||||
style: 'border-radius: 10px; background: #333; color: #fff; padding: 12px; margin-top: 10px; box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);',
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
const postData = {
|
||||
'userId': data?.user?.id,
|
||||
'symbol': $assetType === 'stock' ? $stockTicker : $assetType === 'etf' ? $etfTicker : $cryptoTicker,
|
||||
'name': $displayCompanyName,
|
||||
'assetType': $assetType,
|
||||
'priceWhenCreated': currentPrice?.toFixed(2),
|
||||
'condition': values?.at(0) < 0 ? 'below' : 'above',
|
||||
'targetPrice': displayPrice,
|
||||
}
|
||||
|
||||
// Make the POST request to the endpoint
|
||||
const response = await fetch(fastifyURL+'/create-price-alert', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
const output = (await response.json())?.items
|
||||
|
||||
if (output === 'success') {
|
||||
toast.success(`Successfully created price alert`, {
|
||||
style: 'border-radius: 10px; background: #333; color: #fff; padding: 12px; margin-top: 10px; box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);',
|
||||
});
|
||||
|
||||
const closePopup = document.getElementById("priceAlertModal");
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
values = [0];
|
||||
displayPrice = currentPrice;
|
||||
|
||||
}
|
||||
else {
|
||||
toast.error(`Something went wrong. Please try again!`, {
|
||||
style: 'border-radius: 10px; background: #333; color: #fff; padding: 12px; margin-top: 10px; box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function numPadInput(number) {
|
||||
|
||||
// Check if the input number is a single digit number
|
||||
if ( number >= 0 && number <= 9) {
|
||||
|
||||
if(Math?.abs(displayPrice -currentPrice) < 0.001) {
|
||||
displayPrice = Number(number);
|
||||
}
|
||||
else {
|
||||
if((String(displayPrice)?.split('.')[1] || '')?.length < 2)
|
||||
{
|
||||
displayPrice = Number(String(displayPrice) + String(number));
|
||||
|
||||
}
|
||||
}
|
||||
values[0] = ((displayPrice/currentPrice-1)*100)
|
||||
|
||||
const button = document.getElementById(`numPad-${number}`);
|
||||
if (button) {
|
||||
button.classList.add('bg-white', 'bg-opacity-[0.05]');
|
||||
setTimeout(() => {
|
||||
button.classList.remove('bg-white', 'bg-opacity-[0.05]');
|
||||
}, 120);
|
||||
}
|
||||
}
|
||||
else if ( number === 'remove') {
|
||||
displayPrice = String(displayPrice).slice(0, -1);
|
||||
// If displayPrice becomes an empty string, set it to 0
|
||||
if (displayPrice === '') {
|
||||
displayPrice = 0;
|
||||
}
|
||||
|
||||
values[0] = ((displayPrice/currentPrice-1)*100)
|
||||
|
||||
const button = document.getElementById(`numPad-remove`);
|
||||
if (button) {
|
||||
button.classList.add('bg-white', 'bg-opacity-[0.05]');
|
||||
setTimeout(() => {
|
||||
button.classList.remove('bg-white', 'bg-opacity-[0.05]');
|
||||
}, 120);
|
||||
}
|
||||
}
|
||||
|
||||
else if ( number === 'dot') {
|
||||
if (!String(displayPrice)?.includes('.') && (String(displayPrice)?.split('.')[1] || '')?.length < 2) {
|
||||
displayPrice = String(displayPrice)+'.';
|
||||
console.log(displayPrice)
|
||||
}
|
||||
|
||||
values[0] = ((displayPrice/currentPrice-1)*100)
|
||||
|
||||
const button = document.getElementById(`numPad-dot`);
|
||||
if (button) {
|
||||
button.classList.add('bg-white', 'bg-opacity-[0.05]');
|
||||
setTimeout(() => {
|
||||
button.classList.remove('bg-white', 'bg-opacity-[0.05]');
|
||||
}, 120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$: isPositive = values[0] < 0;
|
||||
$: origin = isPositive ? '--left: auto; --right: 50%' : '--right: auto; --left: 50%';
|
||||
$: color = isPositive ? '#FF2F1F' : '#0DDE00';
|
||||
|
||||
$: {
|
||||
if ($openPriceAlert === true) {
|
||||
$openPriceAlert = false;
|
||||
currentPrice = data?.getStockQuote?.price
|
||||
values = [0];
|
||||
displayPrice = (currentPrice*(1+values?.at(0)/100))?.toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||
</svelte:head>
|
||||
|
||||
{#if $screenWidth >= 640}
|
||||
|
||||
<!--Start Trade Modal-->
|
||||
<input type="checkbox" id="priceAlertModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="priceAlertModal" class="modal modal-bottom sm:modal-middle ">
|
||||
|
||||
|
||||
<label for="priceAlertModal" class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.02]"></label>
|
||||
|
||||
|
||||
<div class="modal-box rounded-none w-full bg-[#000] h-[450px]" >
|
||||
|
||||
<!--Start Trade Modal-->
|
||||
<label for="priceAlertModal" class="cursor-pointer absolute right-5 top-5 bg-[#000] text-[1.8rem] text-white">
|
||||
<svg class="w-8 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 class="flex flex-col w-full">
|
||||
|
||||
|
||||
<h2 class="text-white font-medium text-lg text-center m-auto mb-5 mt-3">
|
||||
Price Alerts
|
||||
</h2>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center ">
|
||||
|
||||
|
||||
<body class="w-11/12 m-auto ">
|
||||
<div class="mt-8 text-center flex flex-col justify-center items-center mb-14">
|
||||
<div class="text-white font-medium text-5xl">
|
||||
${displayPrice}
|
||||
</div>
|
||||
<div class="text-sm text-white text-opacity-[0.5] mt-2">
|
||||
Current Price: ${currentPrice}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="positive-negative-slider" style="--range: {Math.abs(values[0]/2)}%; {origin}; --range-color: {color};">
|
||||
<RangeSlider id="large" on:change={() => displayPrice = (currentPrice*(1+values?.at(0)/100))?.toFixed(2) } suffix="%" min={-100} max={100} all={false} first={false} last={false} rest={false} hoverable={false} bind:values />
|
||||
</div>
|
||||
{#if values?.at(0) !== 0}
|
||||
<div class="text-white text-center m-auto text-sm">
|
||||
Triggers once when price moves {values?.at(0) > 0 ? 'above' : 'below'}
|
||||
{#if values?.at(0) > 0}
|
||||
<svg class="inline-block w-5 h-5 -mr-1 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#10db06" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="text-[#10DB06] font-medium">+{values?.at(0)?.toFixed(2)}%</span>
|
||||
{:else if values?.at(0) < 0}
|
||||
<svg class="inline-block w-5 h-5 -mr-1 rotate-180" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#FF2F1F" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="inline-block text-[#FF2F1F] font-medium">{values?.at(0)?.toFixed(2)}% </span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-white text-opacity-60 m-auto text-center text-sm">
|
||||
Set a trigger by moving the slider.
|
||||
</div>
|
||||
{/if}
|
||||
</body>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="w-full pt-8 m-auto flex justify-center items-center">
|
||||
<button on:click={handleCreateAlert} class="m-auto cursor-pointer px-7 py-2 rounded-full transition ease-out {values?.at(0) !== 0 ? 'bg-[#0DDE00]' : 'bg-gray-600'} text-center text-black font-medium">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--End Trade Modal-->
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<!--Start Mobile Price Alert-->
|
||||
<div class="drawer drawer-end overflow-hidden w-screen z-40">
|
||||
<input id="priceAlertModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen px-5 w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#000] w-full p-1 flex flex-col items-center pb-5 h-auto">
|
||||
<label for="priceAlertModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
Price Alert
|
||||
</h2>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
<div class="flex justify-between items-center ">
|
||||
|
||||
|
||||
<body class="w-11/12 m-auto">
|
||||
<div class="mt-8 text-center flex flex-col justify-center items-center mb-14">
|
||||
<div class="text-white font-medium text-5xl">
|
||||
${displayPrice}
|
||||
</div>
|
||||
<div class="text-sm text-white text-opacity-[0.5] mt-2">
|
||||
Current Price: ${currentPrice}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center font-medium flex justify-center">
|
||||
{#if ((displayPrice/currentPrice-1)*100) > 100}
|
||||
<span class="text-[#10DB06]">{((displayPrice/currentPrice-1)*100)?.toFixed(2)}%</span>
|
||||
{:else if values?.at(0) > 0}
|
||||
<span class="text-[#10DB06]">{values?.at(0)?.toFixed(2)}%</span>
|
||||
{:else if values?.at(0) < 0}
|
||||
<span class="inline-block text-[#FF2F1F]">{values?.at(0)?.toFixed(2)}% </span>
|
||||
{:else}
|
||||
<span class="inline-block text-white text-opacity-[0.6]">0%</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="positive-negative-slider" style="--range: {Math.abs(values[0]/2)}%; {origin}; --range-color: {color};">
|
||||
<RangeSlider id="large" on:change={() => displayPrice = (currentPrice*(1+values?.at(0)/100))?.toFixed(2) } suffix="%" min={-100} max={100} all={false} first={false} last={false} rest={false} hoverable={false} bind:values />
|
||||
</div>
|
||||
|
||||
<div class="{values?.at(0) !== 0 ? 'invisible' : ''} text-white text-opacity-60 m-auto text-center text-sm">
|
||||
Set a trigger by moving the slider.
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="w-full pt-4 flex justify-end items-center ml-auto pr-8">
|
||||
<button on:click={handleCreateAlert} class="ml-auto cursor-pointer px-5 py-2 rounded-full transition ease-out {values?.at(0) !== 0 ? 'bg-[#0DDE00]' : 'bg-gray-600'} text-center text-black font-medium">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap pl-5 rounded-xl bg-[#000] max-w-sm mx-auto mt-5">
|
||||
{#each Array.from({ length: 9 }, (_, i) => i + 1) as number}
|
||||
<div class="w-1/3">
|
||||
<button id={"numPad-" + number} on:click={() => numPadInput(number)} class="transition ease-out w-20 h-20 text-2xl text-white font-bold rounded-full">{number}</button>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="w-1/3">
|
||||
<button id="numPad-dot" on:click={() => numPadInput('dot')} class="transition ease-out w-20 h-20 text-2xl text-white font-bold rounded-full">.</button>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<button id="numPad-0" on:click={() => numPadInput(0)} class="transition ease-out w-20 h-20 text-2xl text-white font-bold rounded-full">0</button>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<button id="numPad-remove" on:click={() => numPadInput('remove')} class="transition ease-out w-20 h-20 text-2xl text-white font-bold rounded-full">
|
||||
<svg class="w-6 h-6 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="gray" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Mobile Price Alert-->
|
||||
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
#large {
|
||||
font-size: 1em;
|
||||
margin-inline: 0;
|
||||
--range-handle-focus: gray;
|
||||
}
|
||||
#large .rangeFloat {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
#x {
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.positive-negative-slider {
|
||||
background: white;
|
||||
position: relative;
|
||||
--range-slider: transparent;
|
||||
margin-inline: 1em;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.positive-negative-slider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: var(--left);
|
||||
right: var(--right);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--range,0);
|
||||
background-color: var(--range-color);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
292
src/lib/components/PriceAnalysis.svelte
Normal file
292
src/lib/components/PriceAnalysis.svelte
Normal file
@ -0,0 +1,292 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker, etfTicker, cryptoTicker, assetType, screenWidth} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
|
||||
export let priceAnalysisDict;
|
||||
export let data;
|
||||
|
||||
const modalContent = `
|
||||
Our AI model, employing a Bayesian approach, predicts future prices by breaking down trends, seasonality, and holiday effects. It integrates uncertainty to offer forecasts with intervals.<br><br>
|
||||
<span class="font-semibold underline"><span class="italic">R</span><sup>2</sup> Score</span>: How well the regression model fits the data. A high score (close to 100%) is good, indicating a strong fit, while a low score (close to 0%) is bad, suggesting poor fit.
|
||||
<br><br>
|
||||
<span class="font-semibold underline">Mean Absolute Percentage Error (MAPE)</span>: Measures the average percentage difference between predicted and actual values. A lower MAPE indicates better accuracy, while a higher MAPE suggests less accurate predictions.
|
||||
`;
|
||||
|
||||
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
|
||||
//let showMore = false;
|
||||
//let oneMonthResult;
|
||||
let r2Score;
|
||||
let mape;
|
||||
let priceSentiment = 'n/a';
|
||||
let displayData = 'threeMonth';
|
||||
let lastPrice = 'n/a';
|
||||
let oneYearPricePrediction = 'n/a';
|
||||
let optionsData;
|
||||
|
||||
|
||||
function getPlotOptions() {
|
||||
const predictionDate = priceAnalysisDict?.predictionDate;
|
||||
const upperBand = priceAnalysisDict?.upperBand;
|
||||
const lowerBand = priceAnalysisDict?.lowerBand?.map(value => value < 0 ? 0 : value);
|
||||
console.log(lowerBand)
|
||||
const historicalPrice = priceAnalysisDict?.historicalPrice;
|
||||
//const meanPredictionPrice = priceAnalysisDict?.meanResult;
|
||||
|
||||
const option = {
|
||||
silent: true,
|
||||
grid: {
|
||||
left: $screenWidth < 640 ? '0%' : '2%',
|
||||
right: $screenWidth < 640 ? '5%' : '2%',
|
||||
bottom: $screenWidth < 640 ? '0%' : '5%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: predictionDate,
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
// Assuming dates are in the format 'yyyy-mm-dd'
|
||||
// Extract the month and day from the date string and convert the month to its abbreviated name
|
||||
const dateParts = value.split('-');
|
||||
const year = dateParts[0].substring(2); // Extracting the last two digits of the year
|
||||
const monthIndex = parseInt(dateParts[1]) - 1; // Months are zero-indexed in JavaScript Date objects
|
||||
return `${monthNames[monthIndex]} '${year}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: upperBand,
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
areaStyle: {
|
||||
color: 'rgb(60, 116, 212,0.4)',
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60, 116, 212)' // set line color to white
|
||||
}
|
||||
},
|
||||
{
|
||||
data: lowerBand,
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
areaStyle: {
|
||||
color: '#0F0F0F', // color of the background
|
||||
opacity: 1
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60, 116, 212)'
|
||||
}
|
||||
},
|
||||
/*
|
||||
{
|
||||
data: meanPredictionPrice,
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
itemStyle: {
|
||||
color: "rgb(60, 116, 212,0.3)"
|
||||
}
|
||||
},
|
||||
*/
|
||||
{
|
||||
data: historicalPrice,
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
itemStyle: {
|
||||
color: "#C40377"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
$: {
|
||||
if(($assetType === 'stock' ? $stockTicker : $assetType === 'etf' ? $etfTicker : $cryptoTicker) && typeof window !== 'undefined' && Object?.keys(priceAnalysisDict)?.length !== 0) {
|
||||
lastPrice = data?.getStockQuote?.price ?? "n/a";
|
||||
oneYearPricePrediction = priceAnalysisDict?.meanResult?.slice(-1)?.at(0);
|
||||
|
||||
mape = priceAnalysisDict?.mape;
|
||||
r2Score = priceAnalysisDict?.r2Score;
|
||||
priceSentiment = lastPrice < oneYearPricePrediction ? 'Bullish' : 'Bearish';
|
||||
optionsData = getPlotOptions()
|
||||
|
||||
/*
|
||||
const sample = trendList?.filter(item => item?.label === displayData)?.at(0);
|
||||
priceSentiment = sample?.sentiment;
|
||||
r2Score = sample?.r2Score;
|
||||
mape = sample?.mape;
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2 ">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="priceAnalysisInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
AI Price Analysis
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Price Analysis"}
|
||||
content={modalContent}
|
||||
id={"priceAnalysisInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if Object?.keys(priceAnalysisDict)?.length !== 0}
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
Our model predicts future prices by analyzing trends, seasonal variations, and holiday impacts. Here are the stats of the model for {$displayCompanyName} to ensure transparency and reliability.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="w-full mt-5 mb-5 flex justify-start items-center">
|
||||
<div class="w-full grid grid-cols-2 lg:grid-cols-3 gap-y-3 gap-x-3 ">
|
||||
<!--Start Flow Sentiment-->
|
||||
<div class="flex flex-row items-center flex-wrap w-full px-3 sm:px-5 bg-[#202020] shadow-lg rounded-2xl h-20">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="font-medium text-gray-200 text-sm">Price Sentiment</span>
|
||||
<span class="text-start text-[1rem] font-medium {priceSentiment === 'Bullish' ? 'text-[#10DB06]' : 'text-[#FC2120]'}">{priceSentiment}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--End Flow Sentiment-->
|
||||
<!--Start Put/Call-->
|
||||
<div class="flex flex-row items-center flex-wrap w-full px-3 sm:px-5 bg-[#202020] shadow-lg rounded-2xl h-20">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="font-medium text-gray-200 text-sm"><span class="italic">R</span><sup>2</sup> Score</span>
|
||||
<span class="text-start text-sm sm:text-[1rem] font-medium text-white">
|
||||
{r2Score >=65 ? 'Good' : r2Score >= 50 ? 'Moderate' : 'Bad'}
|
||||
</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 {r2Score >= 65 ? 'text-[#00FC50]' : r2Score >= 50 ? 'text-[#F8901E]' : 'text-[#FF2F1F]'}" stroke-width="3" stroke-dasharray="100" stroke-dashoffset={100-(r2Score)}></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">{r2Score}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Circular Progress -->
|
||||
|
||||
</div>
|
||||
<!--End Put/Call-->
|
||||
|
||||
<!--Start mape-->
|
||||
<div class="flex flex-row items-center flex-wrap w-full px-3 sm:px-5 bg-[#202020] shadow-lg rounded-2xl h-20">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="font-medium text-gray-200 text-sm">MAPE</span>
|
||||
<span class="text-start text-sm sm:text-[1rem] font-medium text-white">
|
||||
{mape <=15 ? 'Good' : mape <= 35 ? 'Moderate' : 'Bad'}
|
||||
</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 {mape <= 15 ? 'text-[#00FC50]' : mape <= 35 ? 'text-[#F8901E]' : 'text-[#FF2F1F]'}" stroke-width="3" stroke-dasharray="100" stroke-dashoffset={(100-mape) > 0 ? 100-mape : 1}></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">{mape}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Circular Progress -->
|
||||
|
||||
</div>
|
||||
<!--End mape-->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full h-[300px] ">
|
||||
<Chart options={optionsData} class="chart" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-4 sm:mt-7 ml-1">
|
||||
Over the next 12 months, the model predicts a
|
||||
<span class="font-semibold {priceSentiment === 'Bullish' ? 'text-[#10DB06]' : 'text-[#FC2120]'}">{priceSentiment}</span>
|
||||
trend, suggesting that the future price is expected to {priceSentiment === 'Bullish' ? 'surpass' : 'to be less than'} the previous price of
|
||||
<span class="font-semibold">${lastPrice?.toFixed(2) ?? 'n/a'}</span>, with a mean value of <span class="font-semibold">${oneYearPricePrediction}</span>.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
513
src/lib/components/PricePredictionCard.svelte
Normal file
513
src/lib/components/PricePredictionCard.svelte
Normal file
@ -0,0 +1,513 @@
|
||||
<script lang="ts">
|
||||
import {stockTicker, etfTicker} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import Lazy from 'svelte-lazy';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
|
||||
export let pricePrediction;
|
||||
export let pastPriceList;
|
||||
export let lastPrice;
|
||||
export let assetType;
|
||||
|
||||
|
||||
|
||||
let mode:boolean;
|
||||
let optionsPrediction;
|
||||
let rawData;
|
||||
|
||||
let timePeriod = '1W'
|
||||
let timeFrame = 7;
|
||||
|
||||
function selectTimeInterval(interval) {
|
||||
timePeriod = interval;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let lowPrice;
|
||||
let meanPrice;
|
||||
let highPrice;
|
||||
let changeMin;
|
||||
//let changeMean = ((meanPrice/lastPrice -1)*100) ?? 'n/a'
|
||||
let changeMax;
|
||||
|
||||
let changeMean;
|
||||
|
||||
|
||||
|
||||
const plotPredictionChart = () => {
|
||||
|
||||
if (typeof rawData !== 'undefined')
|
||||
{
|
||||
|
||||
|
||||
const pastDates = rawData.map(item => item.time);
|
||||
const pastPriceData = rawData.map(item => item.close);
|
||||
const nullList = Array.from({ length: pastPriceData.length -1}, () => null);
|
||||
|
||||
|
||||
|
||||
const startDate = new Date(pastDates?.slice(-1));
|
||||
const futureDates = [];
|
||||
|
||||
for (let i = 0; i < timeFrame; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(startDate.getDate() + i);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
futureDates.push(formattedDate);
|
||||
}
|
||||
const futureNullList = Array.from({ length: futureDates.length -2}, () => null);
|
||||
|
||||
const dates = [...new Set([...pastDates, ...futureDates]) ];
|
||||
|
||||
const options = {
|
||||
grid: {
|
||||
left: '0%',
|
||||
right: '0%',
|
||||
top: '10%',
|
||||
bottom: '10%',
|
||||
containLabel: true,
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
// prettier-ignore
|
||||
data: dates,
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitLine: {
|
||||
show: false, // Disable y-axis grid lines
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '${value}', // Display value with a percent sign
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Past Price',
|
||||
type: 'line',
|
||||
itemStyle: {
|
||||
color: 'white', // Set the color to green
|
||||
},
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
smooth: true,
|
||||
// prettier-ignore
|
||||
data: pastPriceData,
|
||||
},
|
||||
{
|
||||
name: 'Average',
|
||||
type: 'line',
|
||||
itemStyle: {
|
||||
color: 'orange' // Set the color to green
|
||||
},
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
type: 'dashed',
|
||||
},
|
||||
smooth: true,
|
||||
connectNulls: true, // Connect null data points
|
||||
// prettier-ignore
|
||||
data: [...nullList, pastPriceData?.at(-1), ...futureNullList, meanPrice],
|
||||
markArea: {
|
||||
itemStyle: {
|
||||
color: 'rgb(31, 31, 35, 1)'
|
||||
},
|
||||
data: [
|
||||
[
|
||||
{
|
||||
name: '',
|
||||
xAxis: pastDates?.at(-1),
|
||||
},
|
||||
{
|
||||
xAxis: futureDates?.at(-1),
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
color: 'green',
|
||||
width: 1.5,
|
||||
type: 'dashed',
|
||||
},
|
||||
smooth: true,
|
||||
connectNulls: true, // Connect null data points
|
||||
// prettier-ignore
|
||||
data: [...nullList, pastPriceData?.at(-1), ...futureNullList, highPrice],
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
color: 'red',
|
||||
width: 1.5,
|
||||
type: 'dashed',
|
||||
},
|
||||
smooth: true,
|
||||
connectNulls: true, // Connect null data points
|
||||
// prettier-ignore
|
||||
data: [...nullList, pastPriceData?.at(-1), ...futureNullList, lowPrice],
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
else {
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if ((assetType === 'stock' ? $stockTicker : $etfTicker ) && typeof window !== 'undefined' && typeof pricePrediction !== 'undefined' && typeof pastPriceList !== 'undefined')
|
||||
{
|
||||
mode = false;
|
||||
if (timePeriod === '3M')
|
||||
{
|
||||
rawData = pastPriceList['6M']?.slice(-80,-1);
|
||||
timeFrame = 90;
|
||||
}
|
||||
else if (timePeriod === '1W')
|
||||
{
|
||||
rawData = pastPriceList['6M']?.slice(-10);
|
||||
timeFrame = 7;
|
||||
}
|
||||
else if (timePeriod === '1M')
|
||||
{
|
||||
rawData = pastPriceList['6M']?.slice(-20,-1);
|
||||
timeFrame = 30;
|
||||
}
|
||||
else if (timePeriod === '6M')
|
||||
{
|
||||
rawData = pastPriceList['1Y'];
|
||||
timeFrame = 180;
|
||||
}
|
||||
|
||||
|
||||
lowPrice = pricePrediction[timePeriod]['min'] ?? 'n/a';
|
||||
meanPrice = pricePrediction[timePeriod]['mean'] ?? 'n/a'
|
||||
highPrice = pricePrediction[timePeriod]['max'] ?? 'n/a'
|
||||
changeMin = ((lowPrice/lastPrice -1)*100) ?? 'n/a'
|
||||
//changeMean = ((meanPrice/lastPrice -1)*100) ?? 'n/a'
|
||||
changeMax = ((highPrice/lastPrice -1)*100) ?? 'n/a'
|
||||
|
||||
changeMean = ((changeMax+changeMin)/2) ?? 'n/a';
|
||||
|
||||
optionsPrediction = plotPredictionChart()
|
||||
|
||||
mode = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<!--Start Copilot Card -->
|
||||
|
||||
<div class="space-y-3 overflow-hidden">
|
||||
<!--Start Content-->
|
||||
<div class="w-auto lg:w-full p-1 flex flex-col m-auto">
|
||||
|
||||
<div class="flex flex-col items-center w-full mb-6">
|
||||
<div class="flex flex-row justify-start mr-auto items-center">
|
||||
<!--<img class="h-10 inline-block mr-2" src={copilotIcon} />-->
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="pricePredictionInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Price Prediction
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Price Prediction"}
|
||||
content={`The Algorithm forecasts the price for various time periods such as one week, one month, three months, and six months using historical data and various machine-learning techniques.`}
|
||||
id={"pricePredictionInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<p class="text-white text-md">
|
||||
Expect prices between ${lowPrice?.toFixed(2)} and ${highPrice?.toFixed(2)} for the next {timePeriod === '1W' ? '7 days' : timePeriod === '1M' ? '1 month' : timePeriod === '3M' ? '3 months' : timePeriod === '6M' ? '6 months' : 'n/a'}.
|
||||
</p>
|
||||
-->
|
||||
<div class="flex flex-row items-center sm:mt-3 mb-5">
|
||||
|
||||
<span class="text-white text-md font-medium">
|
||||
Time Period
|
||||
</span>
|
||||
<div class="ml-auto text-white rounded">
|
||||
|
||||
|
||||
<label for="timePeriodModal" class="pl-3 pr-3 py-1 text-sm sm:text-sm cursor-pointer mr-1 flex flex-row ease-in-out duration-200 rounded-lg bg-[#1E1E1E] hover:bg-[#333333] normal-case cursor-pointer items-center">
|
||||
<div class="flex flex-row">
|
||||
<span class="m-auto mr-1 text-white">{timePeriod}</span>
|
||||
<svg class="inline-block w-4 h-4 ml-1 mt-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<g transform="rotate(180 512 512)">
|
||||
<path fill="#fff" d="m488.832 344.32l-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872l319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{#if mode}
|
||||
|
||||
<div class="text-white flex flex-row items-center justify-between w-full font-medium text-md text-center mt-3">
|
||||
<span class="text-white text-sm m-auto">
|
||||
Past Price
|
||||
</span>
|
||||
<span class="text-white text-end text-sm">
|
||||
Future Price
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-[90vw] sm:w-[50vw] m-auto mb-5">
|
||||
<Chart options={optionsPrediction} class="chart w-full" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
<p class="text-white text-md mb-5">
|
||||
You can anticipate future returns in the next {timePeriod === '1W' ? '7 days' : timePeriod === '1M' ? '1 month' : timePeriod === '3M' ? '3 months' : timePeriod === '6M' ? '6 months' : 'n/a'}, based on the last price, with a 68% confidence level.
|
||||
</p>
|
||||
|
||||
<table class="table table-pin-rows table-sm table-compact w-full mb-5">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Target</th>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Lowest</th>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Average</th>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Highest</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="">
|
||||
<td class="text-white border-b border-[#0F0F0F]">
|
||||
<span class="text-white font-medium">Price</span>
|
||||
</td>
|
||||
<td class="text-white sm:font-medium text-[0.95rem] border-b border-[#0F0F0F]">
|
||||
${lowPrice?.toFixed(2)}
|
||||
</td>
|
||||
<td class="text-white sm:font-medium text-[0.95rem] border-b border-[#0F0F0F]">
|
||||
${meanPrice?.toFixed(2)}
|
||||
</td>
|
||||
<td class="text-white sm:font-medium text-[0.95rem] border-b border-[#0F0F0F]">
|
||||
${highPrice?.toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="border-[#0F0F0F]">
|
||||
<td class="text-white">
|
||||
<span class="text-white sm:font-medium">Change</span>
|
||||
</td>
|
||||
<td class="text-white">
|
||||
<div class="flex flex-row items-center">
|
||||
{#if changeMin >= 0}
|
||||
<svg class="w-2.5 h-2.5 hidden sm:inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path fill="#10DB06" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-[#10DB06] sm:font-medium sm:text-[0.95rem]">+{changeMin?.toFixed(2)}%</span>
|
||||
{:else if changeMin < 0}
|
||||
<svg class="w-2.5 h-2.5 hidden sm:inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(180 12 12)"><path fill="#FF0000" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-[#FF2F1F] sm:font-medium text-[0.95rem]">{changeMin?.toFixed(2)}%</span>
|
||||
{:else}
|
||||
<span class="text-gray-300 sm:font-medium text-[0.95rem] m-auto pr-2">n/a</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-white">
|
||||
<div class="flex flex-row items-center ">
|
||||
{#if changeMean >= 0}
|
||||
<svg class="w-2.5 h-2.5 hidden sm:inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path fill="#10DB06" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-[#10DB06] sm:font-medium text-[0.95rem]">+{changeMean?.toFixed(2)}%</span>
|
||||
{:else if changeMean < 0}
|
||||
<svg class="w-2.5 h-2.5 hidden sm:inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(180 12 12)"><path fill="#FF0000" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-[#FF2F1F] sm:font-medium text-[0.95rem]">{changeMean?.toFixed(2)}%</span>
|
||||
{:else}
|
||||
<span class="text-gray-300 sm:font-medium text-[0.95rem] m-auto pr-2">n/a</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-white">
|
||||
<div class="flex flex-row items-center">
|
||||
{#if changeMax >= 0}
|
||||
<svg class="w-2.5 h-2.5 hidden sm:inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path fill="#10DB06" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-[#10DB06] sm:font-medium text-[0.95rem]">+{changeMax?.toFixed(2)}%</span>
|
||||
{:else if changeMax < 0}
|
||||
<svg class="w-2.5 h-2.5 hidden sm:inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(180 12 12)"><path fill="#FF0000" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-[#FF2F1F] sm:font-medium text-[0.95rem]">{changeMax?.toFixed(2)}%</span>
|
||||
{:else}
|
||||
<span class="text-gray-300 sm:font-medium text-[0.95rem] m-auto pr-2">n/a</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--End Copilot Card-->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Time Period Modal-->
|
||||
<input type="checkbox" id="timePeriodModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="timePeriodModal" class="modal modal-bottom sm:modal-middle">
|
||||
|
||||
|
||||
<label id="timePeriodModal" for="timePeriodModal" class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
|
||||
|
||||
<div class="modal-box w-full bg-[#191919] flex flex-col items-center">
|
||||
<div class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]" />
|
||||
|
||||
|
||||
|
||||
<div class="text-white w-full">
|
||||
<h3 class="font-medium text-lg sm:text-xl mb-6 ml-2">
|
||||
Forecast Time Period
|
||||
</h3>
|
||||
|
||||
|
||||
<div class="flex flex-col items-center w-full max-w-3xl bg-[#191919]">
|
||||
|
||||
|
||||
<label for="timePeriodModal" on:click={() => selectTimeInterval('1W')} class="cursor-pointer w-full flex flex-row justify-start items-center mb-5">
|
||||
|
||||
<div class="flex flex-row items-center w-full bg-[#303030] p-3 rounded-lg {timePeriod === '1W' ? 'ring-2 ring-[#04E000]' : ''}">
|
||||
|
||||
<span class="ml-1 text-white font-medium mr-auto">
|
||||
1 Week
|
||||
</span>
|
||||
|
||||
<div class="rounded-full w-8 h-8 relative border border-[#737373]">
|
||||
{#if timePeriod === '1W'}
|
||||
<svg class="w-full h-full rounded-full" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#0F0F0F000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <title>ic_fluent_checkmark_circle_48_filled</title> <desc>Created with Sketch.</desc> <g id="🔍-Product-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="ic_fluent_checkmark_circle_48_filled" fill="#04E000" fill-rule="nonzero"> <path d="M24,4 C35.045695,4 44,12.954305 44,24 C44,35.045695 35.045695,44 24,44 C12.954305,44 4,35.045695 4,24 C4,12.954305 12.954305,4 24,4 Z M32.6338835,17.6161165 C32.1782718,17.1605048 31.4584514,17.1301307 30.9676119,17.5249942 L30.8661165,17.6161165 L20.75,27.732233 L17.1338835,24.1161165 C16.6457281,23.6279612 15.8542719,23.6279612 15.3661165,24.1161165 C14.9105048,24.5717282 14.8801307,25.2915486 15.2749942,25.7823881 L15.3661165,25.8838835 L19.8661165,30.3838835 C20.3217282,30.8394952 21.0415486,30.8698693 21.5323881,30.4750058 L21.6338835,30.3838835 L32.6338835,19.3838835 C33.1220388,18.8957281 33.1220388,18.1042719 32.6338835,17.6161165 Z" id="🎨-Color"> </path> </g> </g> </g></svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
|
||||
<label for="timePeriodModal" on:click={() => selectTimeInterval('1M')} class="cursor-pointer w-full flex flex-row justify-start items-center mb-5">
|
||||
|
||||
<div class="flex flex-row items-center w-full bg-[#303030] p-3 rounded-lg {timePeriod === '1M' ? 'ring-2 ring-[#04E000]' : ''}">
|
||||
|
||||
<span class="ml-1 text-white font-medium mr-auto">
|
||||
1 Month
|
||||
</span>
|
||||
|
||||
<div class="rounded-full w-8 h-8 relative border border-[#737373]">
|
||||
{#if timePeriod === '1M'}
|
||||
<svg class="w-full h-full rounded-full" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#0F0F0F000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <title>ic_fluent_checkmark_circle_48_filled</title> <desc>Created with Sketch.</desc> <g id="🔍-Product-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="ic_fluent_checkmark_circle_48_filled" fill="#04E000" fill-rule="nonzero"> <path d="M24,4 C35.045695,4 44,12.954305 44,24 C44,35.045695 35.045695,44 24,44 C12.954305,44 4,35.045695 4,24 C4,12.954305 12.954305,4 24,4 Z M32.6338835,17.6161165 C32.1782718,17.1605048 31.4584514,17.1301307 30.9676119,17.5249942 L30.8661165,17.6161165 L20.75,27.732233 L17.1338835,24.1161165 C16.6457281,23.6279612 15.8542719,23.6279612 15.3661165,24.1161165 C14.9105048,24.5717282 14.8801307,25.2915486 15.2749942,25.7823881 L15.3661165,25.8838835 L19.8661165,30.3838835 C20.3217282,30.8394952 21.0415486,30.8698693 21.5323881,30.4750058 L21.6338835,30.3838835 L32.6338835,19.3838835 C33.1220388,18.8957281 33.1220388,18.1042719 32.6338835,17.6161165 Z" id="🎨-Color"> </path> </g> </g> </g></svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
|
||||
<label for="timePeriodModal" on:click={() => selectTimeInterval('3M')} class="cursor-pointer w-full flex flex-row justify-start items-center mb-5">
|
||||
<div class="flex flex-row items-center w-full bg-[#303030] p-3 rounded-lg {timePeriod === '3M' ? 'ring-2 ring-[#04E000]' : ''}">
|
||||
<span class="ml-1 text-white font-medium mr-auto">
|
||||
3 Months
|
||||
</span>
|
||||
<div class="rounded-full w-8 h-8 relative border border-[#737373]">
|
||||
{#if timePeriod === '3M'}
|
||||
<svg class="w-full h-full rounded-full" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#0F0F0F000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <title>ic_fluent_checkmark_circle_48_filled</title> <desc>Created with Sketch.</desc> <g id="🔍-Product-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="ic_fluent_checkmark_circle_48_filled" fill="#04E000" fill-rule="nonzero"> <path d="M24,4 C35.045695,4 44,12.954305 44,24 C44,35.045695 35.045695,44 24,44 C12.954305,44 4,35.045695 4,24 C4,12.954305 12.954305,4 24,4 Z M32.6338835,17.6161165 C32.1782718,17.1605048 31.4584514,17.1301307 30.9676119,17.5249942 L30.8661165,17.6161165 L20.75,27.732233 L17.1338835,24.1161165 C16.6457281,23.6279612 15.8542719,23.6279612 15.3661165,24.1161165 C14.9105048,24.5717282 14.8801307,25.2915486 15.2749942,25.7823881 L15.3661165,25.8838835 L19.8661165,30.3838835 C20.3217282,30.8394952 21.0415486,30.8698693 21.5323881,30.4750058 L21.6338835,30.3838835 L32.6338835,19.3838835 C33.1220388,18.8957281 33.1220388,18.1042719 32.6338835,17.6161165 Z" id="🎨-Color"> </path> </g> </g> </g></svg>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label for="timePeriodModal" on:click={() => selectTimeInterval('6M')} class="cursor-pointer w-full flex flex-row justify-start items-center mb-5">
|
||||
<div class="flex flex-row items-center w-full bg-[#303030] p-3 rounded-lg {timePeriod === '6M' ? 'ring-2 ring-[#04E000]' : ''}">
|
||||
<span class="ml-1 text-white font-medium mr-auto">
|
||||
6 Months
|
||||
</span>
|
||||
<div class="rounded-full w-8 h-8 relative border border-[#737373]">
|
||||
{#if timePeriod === '6M'}
|
||||
<svg class="w-full h-full rounded-full" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#0F0F0F000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <title>ic_fluent_checkmark_circle_48_filled</title> <desc>Created with Sketch.</desc> <g id="🔍-Product-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="ic_fluent_checkmark_circle_48_filled" fill="#04E000" fill-rule="nonzero"> <path d="M24,4 C35.045695,4 44,12.954305 44,24 C44,35.045695 35.045695,44 24,44 C12.954305,44 4,35.045695 4,24 C4,12.954305 12.954305,4 24,4 Z M32.6338835,17.6161165 C32.1782718,17.1605048 31.4584514,17.1301307 30.9676119,17.5249942 L30.8661165,17.6161165 L20.75,27.732233 L17.1338835,24.1161165 C16.6457281,23.6279612 15.8542719,23.6279612 15.3661165,24.1161165 C14.9105048,24.5717282 14.8801307,25.2915486 15.2749942,25.7823881 L15.3661165,25.8838835 L19.8661165,30.3838835 C20.3217282,30.8394952 21.0415486,30.8698693 21.5323881,30.4750058 L21.6338835,30.3838835 L32.6338835,19.3838835 C33.1220388,18.8957281 33.1220388,18.1042719 32.6338835,17.6161165 Z" id="🎨-Color"> </path> </g> </g> </g></svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
<!--End Time Period Modal-->
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
72
src/lib/components/PullToRefresh.svelte
Normal file
72
src/lib/components/PullToRefresh.svelte
Normal file
@ -0,0 +1,72 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let touchStartY = 0;
|
||||
let touchEndY = 0;
|
||||
let isPulling = false;
|
||||
let refreshHeight = 0;
|
||||
|
||||
const onTouchStart = (e) => {
|
||||
touchStartY = e.touches[0].clientY;
|
||||
};
|
||||
|
||||
const onTouchMove = (e) => {
|
||||
touchEndY = e.touches[0].clientY;
|
||||
if (touchEndY > touchStartY) {
|
||||
isPulling = true;
|
||||
refreshHeight = (touchEndY - touchStartY) * 0.5; // Adjust the factor to control sensitivity
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (isPulling && refreshHeight > 150) {
|
||||
// Trigger the refresh action
|
||||
refresh();
|
||||
}
|
||||
resetPull();
|
||||
};
|
||||
|
||||
const resetPull = () => {
|
||||
isPulling = false;
|
||||
refreshHeight = 0;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
// Refresh the page
|
||||
location.reload();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('touchstart', onTouchStart);
|
||||
document.addEventListener('touchmove', onTouchMove);
|
||||
document.addEventListener('touchend', onTouchEnd);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('touchstart', onTouchStart);
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
transition: transform 0.2s ease-out; /* Smooth transition for the content */
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
class="content"
|
||||
style="transform: translateY({refreshHeight}px);"
|
||||
>
|
||||
<div
|
||||
class="refresh-indicator fixed top-0 left-0 right-0 flex justify-center items-center"
|
||||
style="height: {refreshHeight}px;"
|
||||
>
|
||||
{#if isPulling && refreshHeight > 100}
|
||||
<div class="spinner animate-spin rounded-full h-8 w-8 border-t-4 border-blue-500"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<slot></slot> <!-- This will allow the content of the component to be passed in -->
|
||||
</div>
|
||||
|
||||
332
src/lib/components/ReturnCard.svelte
Normal file
332
src/lib/components/ReturnCard.svelte
Normal file
@ -0,0 +1,332 @@
|
||||
<script lang='ts'>
|
||||
|
||||
//import { Chart } from 'svelte-echarts';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import Lazy from "svelte-lazy";
|
||||
|
||||
import {stockTicker, etfTicker, cryptoTicker, assetType} from '$lib/store';
|
||||
|
||||
export let quantData;
|
||||
|
||||
|
||||
|
||||
const months = [
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
];
|
||||
|
||||
let firstElementKey;
|
||||
|
||||
let monthlyReturnsData;
|
||||
let benchmarkMonthlyReturnsData;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const plotAnnualReturn = () => {
|
||||
|
||||
const annualReturnList = [];
|
||||
const annualReturnListBenchmark = [];
|
||||
|
||||
// Find the maximum year in monthlyReturnsData
|
||||
// Find the maximum year in monthlyReturnsData
|
||||
let maxYear = Math?.max(...Object?.keys(monthlyReturnsData)?.map(year => parseInt(year)));
|
||||
|
||||
// Iterate over the last 4 years
|
||||
for (let year = maxYear; year > maxYear - 4; year--) {
|
||||
if (monthlyReturnsData.hasOwnProperty(year.toString())) {
|
||||
const values = monthlyReturnsData[year.toString()];
|
||||
annualReturnList.push(values[values.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the maximum year in benchmarkMonthlyReturnsData
|
||||
maxYear = Math?.max(...Object?.keys(benchmarkMonthlyReturnsData)?.map(year => parseInt(year)));
|
||||
|
||||
// Iterate over the last 4 years
|
||||
for (let year = maxYear; year > maxYear - 4; year--) {
|
||||
if (benchmarkMonthlyReturnsData.hasOwnProperty(year.toString())) {
|
||||
const values = benchmarkMonthlyReturnsData[year.toString()];
|
||||
annualReturnListBenchmark.push(values[values.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const options = {
|
||||
grid: {
|
||||
left: "0%",
|
||||
right: "7%",
|
||||
bottom: '0%',
|
||||
height: "90%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
data: ['2020', '2021', '2022', '2023','2024'],
|
||||
type: 'category',
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false, // Disable y-axis grid lines
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} %', // Display value with a percent sign
|
||||
},
|
||||
splitLine: {
|
||||
show: false, // Disable y-axis grid lines
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: $assetType === 'etf' ? $etfTicker : $assetType === 'crypto' ? $cryptoTicker : $stockTicker,
|
||||
data: annualReturnList,
|
||||
type: 'bar',
|
||||
barWidth: '30%',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#398EC2',
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
name: 'S&P500',
|
||||
data: annualReturnListBenchmark,
|
||||
type: 'bar',
|
||||
barWidth: '30%',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#FEDD78',
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
|
||||
return options;
|
||||
|
||||
}
|
||||
|
||||
const plotMonthlyDistributionReturn = () => {
|
||||
const allReturns = Object?.values(monthlyReturnsData)?.flat();
|
||||
|
||||
let histogramData = [];
|
||||
// Calculate histogram data
|
||||
let binSize = 6;
|
||||
let maxBin = Math.ceil(Math.max(...allReturns) / binSize) * binSize;
|
||||
let minBin = Math.floor(Math.min(...allReturns) / binSize) * binSize;
|
||||
|
||||
for (let i = minBin; i <= maxBin; i += binSize) {
|
||||
let count = allReturns.filter(r => r >= i && r < i + binSize)?.length;
|
||||
histogramData.push([i, count]);
|
||||
}
|
||||
|
||||
const options = {
|
||||
grid: {
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: '0%',
|
||||
height: "90%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
name: 'Return (%)', // X-axis label
|
||||
nameLocation: 'center',
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
|
||||
nameTextStyle: {
|
||||
color: 'white', // Set label color to white
|
||||
padding: [30, 0, 0, 0],
|
||||
},
|
||||
}
|
||||
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: 'Frequency', // X-axis label
|
||||
nameLocation: 'center',
|
||||
splitLine: {
|
||||
show: false, // Disable y-axis grid lines
|
||||
},
|
||||
nameTextStyle: {
|
||||
color: 'white', // Set label color to white
|
||||
padding: [0, 0, 20, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} %', // Display value with a percent sign
|
||||
},
|
||||
splitLine: {
|
||||
show: false, // Disable y-axis grid lines
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Frequency of Return',
|
||||
type: 'bar',
|
||||
barWidth: '100%',
|
||||
data: histogramData
|
||||
}
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
let displayReturn = 'annualReturn';
|
||||
let optionsAnnualReturn;
|
||||
let optionsMonthlyDistributionReturn;
|
||||
|
||||
function changeStatement(event)
|
||||
|
||||
{
|
||||
|
||||
displayReturn = event.target.value;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
if (($assetType === 'etf' ? $etfTicker : $assetType === 'crypto' ? $cryptoTicker : $stockTicker) && typeof window !== 'undefined')
|
||||
{
|
||||
try {
|
||||
firstElementKey = Object?.keys(quantData)[0];
|
||||
monthlyReturnsData = quantData[firstElementKey]['Monthly Return'];
|
||||
benchmarkMonthlyReturnsData = quantData['SPY']['Monthly Return']
|
||||
}
|
||||
|
||||
catch(e) {
|
||||
monthlyReturnsData = [];
|
||||
benchmarkMonthlyReturnsData = [];
|
||||
}
|
||||
|
||||
|
||||
if (displayReturn === 'annualReturn')
|
||||
{
|
||||
optionsAnnualReturn = plotAnnualReturn()
|
||||
}
|
||||
else if(displayReturn === 'monthlyDistributionReturn')
|
||||
{
|
||||
optionsMonthlyDistributionReturn = plotMonthlyDistributionReturn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="mt-4 overflow-hidden text-white h-full">
|
||||
|
||||
<div class="w-full m-auto h-full overflow-hidden">
|
||||
<main>
|
||||
|
||||
<div class="text-start mr-auto w-full">
|
||||
<select class="w-48 select select-bordered select-sm p-0 pl-5 ml-2 mt-2 bg-[#2A303C]" on:change={changeStatement}>
|
||||
<option disabled>Choose your Return</option>
|
||||
<option value="annualReturn" selected>
|
||||
Annual Return
|
||||
</option>
|
||||
<option value="monthlyDistributionReturn" >
|
||||
Distribution of Monthly Returns
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="rounded-2xl pl-3 sm:pr-0 pt-3 w-full m-auto flex flex-col justify-center items-center mt-4">
|
||||
|
||||
{#if displayReturn === 'annualReturn'}
|
||||
<h1 class="text-white m-auto font-bold text-lg text-center">
|
||||
Annual Return (%)
|
||||
</h1>
|
||||
|
||||
<div class="text-white flex flex-row items-center justify-center m-auto font-medium text-md text-center mt-3">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-4 h-4 bg-[#3E9CD5] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white font-medium inline-block">
|
||||
{$assetType === 'etf' ? $etfTicker : $assetType === 'crypto' ? $cryptoTicker : $stockTicker}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center ml-10">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-4 h-4 bg-[#FFF384] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white font-medium inline-block">
|
||||
S&P500
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-[90vw] sm:w-[50vw] mb-20 mt-6">
|
||||
<Chart options={optionsAnnualReturn} class="chart w-full" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
|
||||
{:else if displayReturn === 'monthlyDistributionReturn'}
|
||||
|
||||
<h1 class="text-white m-auto font-bold text-lg sm:-mb-2 text-center">
|
||||
Distribution of Monthly Returns
|
||||
</h1>
|
||||
|
||||
<div class="app w-[90vw] sm:w-[50vw] mb-4">
|
||||
<Chart options={optionsMonthlyDistributionReturn} class="chart w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
307
src/lib/components/RevenueSegmentation.svelte
Normal file
307
src/lib/components/RevenueSegmentation.svelte
Normal file
@ -0,0 +1,307 @@
|
||||
<script lang='ts'>
|
||||
import { stockTicker, screenWidth} from '$lib/store';
|
||||
import { abbreviateNumber, formatString } from '$lib/utils';
|
||||
import Sankey from '$lib/components/Sankey.svelte';
|
||||
import { LayerCake, Svg } from 'layercake';
|
||||
import { countryList} from '$lib/country-list.ts';
|
||||
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
|
||||
export let revenueSegmentation;
|
||||
|
||||
|
||||
|
||||
|
||||
let data = [];
|
||||
let geographicList = [];
|
||||
let totalProductRevenue = 0;
|
||||
let totalGeographicRevenue = 0;
|
||||
|
||||
async function prepareData(output) {
|
||||
const outputProduct = output?.at(0) ?? [];
|
||||
const outputGeographic = output?.at(1) ?? [];
|
||||
|
||||
|
||||
totalProductRevenue = 0;
|
||||
totalGeographicRevenue = 0;
|
||||
|
||||
data = [];
|
||||
geographicList = [];
|
||||
|
||||
if (outputProduct?.length !== 0) {
|
||||
const productData = outputProduct[0];
|
||||
const productKeys = Object?.keys(productData);
|
||||
|
||||
let nodes = [];
|
||||
let links = [];
|
||||
|
||||
for (const key of productKeys) {
|
||||
const values = productData[key];
|
||||
let count = 0;
|
||||
for (const category in values) {
|
||||
if (count >= 4) break; // Stop after the fifth element
|
||||
const value = values[category];
|
||||
if (value !== 0) {
|
||||
nodes?.push({
|
||||
id: (category?.length > 35 ? category?.slice(0,35) + "..." : category) + `- $${abbreviateNumber(value)}`,
|
||||
});
|
||||
links?.push({
|
||||
source: (category?.length > 35 ? category?.slice(0,35) + "..." : category) + `- $${abbreviateNumber(value)}`, target: 'Revenue', value: value
|
||||
});
|
||||
totalProductRevenue += value;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
nodes?.push({'id': 'Revenue'});
|
||||
links = links?.sort((a,b) => b?.value - a?.value);
|
||||
|
||||
data = {
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
};
|
||||
|
||||
|
||||
//console.log(data)
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (outputGeographic?.length !== 0) {
|
||||
const geographicData = outputGeographic[0];
|
||||
const geographicKeys = Object.keys(geographicData);
|
||||
|
||||
for (const key of geographicKeys) {
|
||||
const values = geographicData[key];
|
||||
for (const category in values) {
|
||||
const value = values[category];
|
||||
geographicList?.push({
|
||||
name: category?.replace('Other Geographical', 'Other'),
|
||||
value: value
|
||||
});
|
||||
totalGeographicRevenue += value;
|
||||
}
|
||||
}
|
||||
|
||||
geographicList?.sort((a, b) => b?.value - a?.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if($stockTicker && typeof window !== 'undefined' && typeof revenueSegmentation !== 'undefined' && revenueSegmentation?.length !== 0)
|
||||
{
|
||||
showFullStats = false;
|
||||
data = [];
|
||||
geographicList = [];
|
||||
totalProductRevenue = 0;
|
||||
totalGeographicRevenue = 0;
|
||||
|
||||
|
||||
|
||||
prepareData(revenueSegmentation)
|
||||
|
||||
geographicList?.forEach(item => {
|
||||
if (item?.name === "TAIWAN, PROVINCE OF CHINA") {
|
||||
item.name = "TAIWAN";
|
||||
}
|
||||
else if (item?.name === "KOREA, REPUBLIC OF") {
|
||||
item.name = "South Korea";
|
||||
}
|
||||
});
|
||||
|
||||
geographicList = geographicList.map(item => {
|
||||
return {
|
||||
...item,
|
||||
name: item.name
|
||||
?.replace("Video Game Brands - ", "")
|
||||
};
|
||||
});
|
||||
// Create an index for quick lookups
|
||||
const countryIndex = countryList?.reduce((acc, country) => {
|
||||
acc[country.long] = country.short;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Update the originalList with the "code" property
|
||||
geographicList = geographicList?.map(item => ({
|
||||
...item,
|
||||
code: countryIndex[formatString(item?.name)]?.toLowerCase() || 'xx'
|
||||
}));
|
||||
|
||||
|
||||
geographicList = [...geographicList];
|
||||
|
||||
}
|
||||
}
|
||||
let showFullStats = false;
|
||||
let charNumber = 40;
|
||||
|
||||
$: {
|
||||
if($screenWidth < 640)
|
||||
{
|
||||
charNumber = 25;
|
||||
}
|
||||
else {
|
||||
charNumber =40;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden w-full text-white h-full sm:mb-0">
|
||||
<div class="flex justify-center w-full m-auto h-full overflow-hidden">
|
||||
<div class="w-full relative flex justify-center items-center overflow-hidden">
|
||||
<main class="w-full">
|
||||
<div class="w-fit sm:w-full sm:max-w-2xl m-auto mt-5 sm:mt-0">
|
||||
|
||||
|
||||
{#if Object?.keys(data)?.length !== 0 && totalProductRevenue !== 0}
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="revenueProductSegmentationInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Revenue Breakdown
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Revenue Breakdown"}
|
||||
content={"A revenue stream for a company is how they make money. It can come from selling things, providing services, or other sources. These different ways of making money add up to keep the company running and growing."}
|
||||
id={"revenueProductSegmentationInfo"}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
<div class="text-white text-md mt-3 w-full mb-5">
|
||||
Based on the latest earnings report
|
||||
the main contributors of revenue are:
|
||||
</div>
|
||||
|
||||
<div class="mt-5 w-full rounded-full flex justify-center items-center ">
|
||||
<div class="flex flex-col items-center w-full">
|
||||
|
||||
|
||||
<div class="chart-container">
|
||||
<LayerCake
|
||||
{data}
|
||||
>
|
||||
<Svg>
|
||||
<Sankey
|
||||
colorNodes={d => '#00bbff'}
|
||||
colorLinks={d => '#00bbff35'}
|
||||
/>
|
||||
</Svg>
|
||||
</LayerCake>
|
||||
</div>
|
||||
|
||||
<a href={`/stocks/${$stockTicker}/stats/income`} class="mt-10 mr-3 sm:mr-0 ml-auto text-blue-400 hover:text-white">
|
||||
Full report
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if geographicList?.length !== 0 && totalGeographicRevenue !== 0}
|
||||
<div class="text-white text-md mt-6">
|
||||
The highest revenue was generated in the {geographicList?.length} regions:
|
||||
</div>
|
||||
|
||||
<div class="w-full rounded-full flex justify-center items-center">
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<!--Start Progress-->
|
||||
{#each (showFullStats ? geographicList : geographicList?.slice(0,3)) as item,index}
|
||||
|
||||
<div class="shadow-lg bg-[#202020] w-full rounded-lg p-4 sm:p-3 mb-5 flex flex-row items-center {index === 0 ? 'mt-4' : ''} {index === 2 && !showFullStats && geographicList?.length > 2 ? 'opacity-[0.3]' : '' }">
|
||||
|
||||
<div class="mr-3 rounded-full w-8 h-8 sm:w-10 sm:h-10 relative bg-[#0F0F0F]">
|
||||
{#if item?.name?.toLowerCase() === 'europe'}
|
||||
<svg class="rounded-full w-6 h-6 sm:w-7 sm:h-7 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><mask id="circleFlagsEu0"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#circleFlagsEu0)"><path fill="#0052b4" d="M0 0h512v512H0z"/><path fill="#ffda44" d="m256 100.2l8.3 25.5H291l-21.7 15.7l8.3 25.6l-21.7-15.8l-21.7 15.8l8.3-25.6l-21.7-15.7h26.8zm-110.2 45.6l24 12.2l18.9-19l-4.2 26.5l23.9 12.2l-26.5 4.2l-4.2 26.5l-12.2-24l-26.5 4.3l19-19zM100.2 256l25.5-8.3V221l15.7 21.7l25.6-8.3l-15.8 21.7l15.8 21.7l-25.6-8.3l-15.7 21.7v-26.8zm45.6 110.2l12.2-24l-19-18.9l26.5 4.2l12.2-23.9l4.2 26.5l26.5 4.2l-24 12.2l4.3 26.5l-19-19zM256 411.8l-8.3-25.5H221l21.7-15.7l-8.3-25.6l21.7 15.8l21.7-15.8l-8.3 25.6l21.7 15.7h-26.8zm110.2-45.6l-24-12.2l-18.9 19l4.2-26.5l-23.9-12.2l26.5-4.2l4.2-26.5l12.2 24l26.5-4.3l-19 19zM411.8 256l-25.5 8.3V291l-15.7-21.7l-25.6 8.3l15.8-21.7l-15.8-21.7l25.6 8.3l15.7-21.7v26.8zm-45.6-110.2l-12.2 24l19 18.9l-26.5-4.2l-12.2 23.9l-4.2-26.5l-26.5-4.2l24-12.2l-4.3-26.5l19 19z"/></g></svg>
|
||||
{:else if item?.name?.toLowerCase() === 'taiwan'}
|
||||
<svg class="rounded-full w-6 h-6 sm:w-7 sm:h-7 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#ed4c5c" d="M32 2v30H2c0 16.6 13.4 30 30 30s30-13.4 30-30S48.6 2 32 2"/><path fill="#2a5f9e" d="M32 2C15.4 2 2 15.4 2 32h30z"/><path fill="#fff" d="m24 20.3l5-1.3l-5-1.3l3.7-3.7l-5 1.3l1.3-5l-3.7 3.7L19 9l-1.3 5l-3.7-3.7l1.3 5l-5-1.3l3.7 3.7L9 19l5 1.3l-3.7 3.7l5-1.3l-1.3 5l3.7-3.7l1.3 5l1.3-5l3.7 3.7l-1.3-5l5 1.3z"/><circle cx="19" cy="19" r="5.7" fill="#428bc1"/><circle cx="19" cy="19" r="5" fill="#fff"/></svg>
|
||||
{:else if item?.code == 'xx'}
|
||||
<svg class="rounded-full w-6 h-6 sm:w-7 sm:h-7 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#7cb342" d="M24 4C13 4 4 13 4 24s9 20 20 20s20-9 20-20S35 4 24 4"/><path fill="#0277bd" d="M45 24c0 11.7-9.5 21-21 21S3 35.7 3 24S12.3 3 24 3s21 9.3 21 21m-21.2 9.7c0-.4-.2-.6-.6-.8c-1.3-.4-2.5-.4-3.6-1.5c-.2-.4-.2-.8-.4-1.3c-.4-.4-1.5-.6-2.1-.8h-4.2c-.6-.2-1.1-1.1-1.5-1.7c0-.2 0-.6-.4-.6c-.4-.2-.8.2-1.3 0c-.2-.2-.2-.4-.2-.6c0-.6.4-1.3.8-1.7c.6-.4 1.3.2 1.9.2c.2 0 .2 0 .4.2c.6.2.8 1 .8 1.7v.4c0 .2.2.2.4.2c.2-1.1.2-2.1.4-3.2c0-1.3 1.3-2.5 2.3-2.9c.4-.2.6.2 1.1 0c1.3-.4 4.4-1.7 3.8-3.4c-.4-1.5-1.7-2.9-3.4-2.7c-.4.2-.6.4-1 .6c-.6.4-1.9 1.7-2.5 1.7c-1.1-.2-1.1-1.7-.8-2.3c.2-.8 2.1-3.6 3.4-3.1l.8.8c.4.2 1.1.2 1.7.2c.2 0 .4 0 .6-.2c.2-.2.2-.2.2-.4c0-.6-.6-1.3-1-1.7c-.4-.4-1.1-.8-1.7-1.1c-2.1-.6-5.5.2-7.1 1.7s-2.9 4-3.8 6.1c-.4 1.3-.8 2.9-1 4.4c-.2 1-.4 1.9.2 2.9c.6 1.3 1.9 2.5 3.2 3.4c.8.6 2.5.6 3.4 1.7c.6.8.4 1.9.4 2.9c0 1.3.8 2.3 1.3 3.4c.2.6.4 1.5.6 2.1c0 .2.2 1.5.2 1.7c1.3.6 2.3 1.3 3.8 1.7c.2 0 1-1.3 1-1.5c.6-.6 1.1-1.5 1.7-1.9c.4-.2.8-.4 1.3-.8c.4-.4.6-1.3.8-1.9c.1-.5.3-1.3.1-1.9m.4-19.4c.2 0 .4-.2.8-.4c.6-.4 1.3-1.1 1.9-1.5c.6-.4 1.3-1.1 1.7-1.5c.6-.4 1.1-1.3 1.3-1.9c.2-.4.8-1.3.6-1.9c-.2-.4-1.3-.6-1.7-.8c-1.7-.4-3.1-.6-4.8-.6c-.6 0-1.5.2-1.7.8c-.2 1.1.6.8 1.5 1.1c0 0 .2 1.7.2 1.9c.2 1-.4 1.7-.4 2.7c0 .6 0 1.7.4 2.1zM41.8 29c.2-.4.2-1.1.4-1.5c.2-1 .2-2.1.2-3.1c0-2.1-.2-4.2-.8-6.1c-.4-.6-.6-1.3-.8-1.9c-.4-1.1-1-2.1-1.9-2.9c-.8-1.1-1.9-4-3.8-3.1c-.6.2-1 1-1.5 1.5c-.4.6-.8 1.3-1.3 1.9c-.2.2-.4.6-.2.8c0 .2.2.2.4.2c.4.2.6.2 1 .4c.2 0 .4.2.2.4c0 0 0 .2-.2.2c-1 1.1-2.1 1.9-3.1 2.9c-.2.2-.4.6-.4.8c0 .2.2.2.2.4s-.2.2-.4.4c-.4.2-.8.4-1.1.6c-.2.4 0 1.1-.2 1.5c-.2 1.1-.8 1.9-1.3 2.9c-.4.6-.6 1.3-1 1.9c0 .8-.2 1.5.2 2.1c1 1.5 2.9.6 4.4 1.3c.4.2.8.2 1.1.6c.6.6.6 1.7.8 2.3c.2.8.4 1.7.8 2.5c.2 1 .6 2.1.8 2.9c1.9-1.5 3.6-3.1 4.8-5.2c1.5-1.3 2.1-3 2.7-4.7"/></svg>
|
||||
{:else}
|
||||
<img
|
||||
class="rounded-full w-6 h-6 sm:w-7 sm:h-7 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={`https://hatscripts.github.io/circle-flags/flags/${item?.code}.svg`}
|
||||
alt="Country Logo" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col -mt-3 sm:-mt-5 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-sm sm:text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
{item?.name?.length > charNumber ? formatString(item?.name)?.slice(0,charNumber) + "..." : formatString(item?.name)} · ${abbreviateNumber(item?.value)}
|
||||
</span>
|
||||
<span class="text-white text-sm sm:text-md font-medium ml-auto">
|
||||
{(item?.value/totalGeographicRevenue * 100)?.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress [&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]" value={item?.value/totalGeographicRevenue *100} max="100"></progress>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/each}
|
||||
<!--End Progress-->
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if geographicList?.length > 2}
|
||||
<label on:click={() => showFullStats = !showFullStats} class="cursor-pointer m-auto flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullStats ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
</style>
|
||||
243
src/lib/components/SECFilingsCard.svelte
Normal file
243
src/lib/components/SECFilingsCard.svelte
Normal file
@ -0,0 +1,243 @@
|
||||
<script lang="ts">
|
||||
import {userRegion, secFilingsClicked, stockTicker, clientSideCache, } from '$lib/store';
|
||||
import { afterUpdate } from 'svelte';
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let secFilingsList;
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let apiURL;
|
||||
|
||||
userRegion?.subscribe(value => {
|
||||
if (usRegion?.includes(value)) {
|
||||
apiURL = import.meta.env.VITE_USEAST_API_URL;
|
||||
} else {
|
||||
apiURL = import.meta.env.VITE_EU_API_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let displayList = [];
|
||||
let secType = '8K';
|
||||
let accordionOpen = {};
|
||||
let newData;
|
||||
let isLoaded = false;
|
||||
|
||||
function changeSECType(changeType) {
|
||||
switch (changeType) {
|
||||
case '8K':
|
||||
secType = changeType;
|
||||
prepareData(secFilingsList?.eightK);
|
||||
break;
|
||||
case '10K':
|
||||
secType = changeType;
|
||||
prepareData(secFilingsList?.tenK);
|
||||
break;
|
||||
case '10Q':
|
||||
secType = changeType;
|
||||
prepareData(secFilingsList?.tenQ);
|
||||
break;
|
||||
// Default case in case changeType doesn't match any of the specified cases
|
||||
default:
|
||||
// Handle the default case or leave it empty if not needed
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareData(dataset) {
|
||||
if (dataset?.length !== 0)
|
||||
{
|
||||
newData = {};
|
||||
accordionOpen = {};
|
||||
|
||||
dataset?.forEach(item => {
|
||||
// Extract year from the date
|
||||
const year = new Date(item?.date)?.getFullYear()?.toString();
|
||||
|
||||
// Check if the year already exists in newData
|
||||
if (!newData[year]) {
|
||||
// If not, initialize a new entry for the year
|
||||
newData[year] = {
|
||||
'year': year,
|
||||
'totalReports': 0,
|
||||
'data': []
|
||||
};
|
||||
}
|
||||
// Increment the totalReports count
|
||||
newData[year].totalReports++;
|
||||
|
||||
// Add the current item to the 'data' array
|
||||
newData[year].data?.push(item);
|
||||
});
|
||||
|
||||
// Convert newData object to an array of values
|
||||
displayList = Object?.values(newData);
|
||||
displayList = displayList?.sort((a,b) => b?.year - a?.year);
|
||||
// Output the displayList
|
||||
displayList = [...displayList];
|
||||
//console.log(displayList)
|
||||
}
|
||||
else {
|
||||
displayList = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function handleAccordion(year) {
|
||||
accordionOpen[year] = !accordionOpen[year];
|
||||
}
|
||||
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
if($clientSideCache[$stockTicker]?.getSECFilings)
|
||||
{
|
||||
secFilingsList = $clientSideCache[$stockTicker]?.getSECFilings;
|
||||
}
|
||||
else {
|
||||
const postData = { ticker: $stockTicker};
|
||||
const response = await fetch(apiURL+'/get-sec-filings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
secFilingsList = await response.json();
|
||||
$clientSideCache[$stockTicker].getSECFilings = secFilingsList;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
afterUpdate(async() => {
|
||||
if($stockTicker && typeof window !== 'undefined' && $secFilingsClicked === true) {
|
||||
$secFilingsClicked = false;
|
||||
await fetchData()
|
||||
secType = '8K';
|
||||
accordionOpen = {}
|
||||
prepareData(secFilingsList?.eightK);
|
||||
isLoaded = true;
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="space-y-3 sm:pt-5">
|
||||
|
||||
<div class="bg-[#000] h-auto w-screen">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#000] w-full p-1 flex flex-col items-center pb-5 h-auto">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
SEC Filings
|
||||
</h2>
|
||||
|
||||
|
||||
<div class="w-11/12 mt-5">
|
||||
<div class="relative right-0 bg-[#27272A] rounded-lg">
|
||||
<ul class="relative flex flex-wrap p-1 list-none rounded-lg">
|
||||
<li class="z-30 flex-auto text-center {secType === '8K' ? 'bg-[#00C806] rounded-lg' : ''}">
|
||||
<label on:click={() => changeSECType('8K')} class="border z-30 flex items-center justify-center w-full px-0 py-1 mb-0 border-0 rounded-lg bg-inherit">
|
||||
<span class="ml-1 {secType === '8K' ? 'text-black' : 'text-white'} font-medium">8-K</span>
|
||||
</label>
|
||||
</li>
|
||||
<li class="z-30 flex-auto text-center {secType === '10K' ? 'bg-[#0FC008] rounded-lg' : ''}">
|
||||
<label on:click={() => changeSECType('10K')} class="z-30 flex items-center justify-center w-full px-0 py-1 mb-0 border-0 rounded-lg bg-inherit">
|
||||
<span class="ml-1 {secType === '10K' ? 'text-black' : 'text-white'} font-medium">10-K</span>
|
||||
</label>
|
||||
</li>
|
||||
<li class="z-30 flex-auto text-center {secType === '10Q' ? 'bg-[#0FC008] rounded-lg' : ''}">
|
||||
<label on:click={() => changeSECType('10Q')} class="z-30 flex items-center justify-center w-full px-0 py-1 mb-0 border-0 rounded-lg bg-inherit">
|
||||
<span class="ml-1 {secType === '10Q' ? 'text-black' : 'text-white'} font-medium">10-Q</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
{#if isLoaded}
|
||||
{#if displayList?.length !== 0}
|
||||
<div class="mt-5 w-full">
|
||||
|
||||
{#each displayList as item}
|
||||
<div class="flex flex-col justify-center m-auto items-start rounded-md bg-[#202020] shadow-lg h-auto w-11/12 mb-3" transition:fade={{ delay: 0, duration: 80 }} in={accordionOpen[item?.year]}>
|
||||
|
||||
|
||||
<div class="flex flex-row items-center w-full p-3">
|
||||
|
||||
<div class="flex-shrink-0 mr-2 rounded-full w-12 h-12 relative bg-[#000]">
|
||||
<svg class="rounded-full w-7 h-7 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="m18 22l3-3l-.7-.7l-1.8 1.8V16h-1v4.1l-1.8-1.8l-.7.7zm-6-11.15l5.925-3.425L12 4L6.075 7.425zm-9 6.275V6.875L12 1.7l9 5.175V12h-2V9.1l-7.025 4.05L5 9.1v6.85l6.025 3.475v2.3zM18 24q-2.075 0-3.537-1.463T13 19q0-2.075 1.463-3.537T18 14q2.075 0 3.538 1.463T23 19q0 2.075-1.463 3.538T18 24m-6.975-12.275"/></svg>
|
||||
</div>
|
||||
|
||||
<div on:click={() => handleAccordion(item?.year)} class="flex flex-row items-center w-full">
|
||||
<div class="flex flex-col items-start w-full ">
|
||||
<span class="text-white text-sm sm:text-md font-medium text-start mt-2">
|
||||
{item?.year}
|
||||
</span>
|
||||
<span class="text-white text-opacity-[0.6] text-xs text-start mb-2">
|
||||
Total Reports: {item?.totalReports}
|
||||
</span>
|
||||
</div>
|
||||
<svg class="h-6 w-6 inline-block transform transition-transform mr-5 {accordionOpen[item?.year] ? '' : 'rotate-180'}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#fff" d="m488.832 344.32l-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872l319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"/></svg>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{#if accordionOpen[item?.year]}
|
||||
{#each item?.data as entry}
|
||||
<div class="shadow-lg bg-[#000] bg-opacity-[0.5] w-11/12 rounded-md p-4 mb-3 m-auto flex flex-row justify-center items-center">
|
||||
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-sm sm:text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
{new Date(entry?.date)?.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', daySuffix: '2-digit' })}
|
||||
</span>
|
||||
<span class="text-white text-sm sm:text-md font-medium ml-auto">
|
||||
<a href={entry?.link} rel="noopener noreferrer" target="_blank" >
|
||||
Check Report
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" mt-20 flex justify-center items-center text-2xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<div class="flex justify-center items-center h-80">
|
||||
<div class="relative">
|
||||
<label class="bg-[#202020] 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"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Stocks Card -->
|
||||
|
||||
|
||||
82
src/lib/components/Sankey.svelte
Normal file
82
src/lib/components/Sankey.svelte
Normal file
@ -0,0 +1,82 @@
|
||||
<script lang='ts'>
|
||||
import { getContext } from 'svelte';
|
||||
import * as Sankey from 'd3-sankey';
|
||||
|
||||
const { data, width, height } = getContext('LayerCake');
|
||||
|
||||
/** @type {Function} [colorLinks=d => '#fff'] - A function to return a color for the links. */
|
||||
export let colorLinks = d => '#fff';
|
||||
|
||||
/** @type {Function} [colorNodes=d => '#333'] - A function to return a color for each node. */
|
||||
export let colorNodes = d => '#fff';
|
||||
|
||||
/** @type {Function} [colorText=d => '#263238'] - A function to return a color for each text label. */
|
||||
export let colorText = d => '#fff';
|
||||
|
||||
/** @type {Number} [nodeWidth=5] - The width of each node, in pixels, passed to [`sankey.nodeWidth`](https://github.com/d3/d3-sankey#sankey_nodeWidth). */
|
||||
export let nodeWidth = 10;
|
||||
|
||||
/** @type {Number} [nodePadding=10] - The padding between nodes, passed to [`sankey.nodePadding`](https://github.com/d3/d3-sankey#sankey_nodePadding). */
|
||||
export let nodePadding = 35;
|
||||
|
||||
/** @type {Function} [linkSort=null] - How to sort the links, passed to [`sankey.linkSort`](https://github.com/d3/d3-sankey#sankey_linkSort). */
|
||||
export let linkSort = null;
|
||||
|
||||
/** @type {Function} [nodeId=d => d.id] - The ID field accessor, passed to [`sankey.nodeId`](https://github.com/d3/d3-sankey#sankey_nodeId). */
|
||||
export let nodeId = d => d.id;
|
||||
|
||||
/** @type {Function} [nodeAlign=d3.sankeyLeft] - An alignment function to position the Sankey blocks. See the [d3-sankey documentation](https://github.com/d3/d3-sankey#alignments) for more. */
|
||||
export let nodeAlign = Sankey.sankeyLeft;
|
||||
|
||||
$: sankey = Sankey.sankey()
|
||||
.nodeAlign(nodeAlign)
|
||||
.nodeWidth(nodeWidth)
|
||||
.nodePadding(nodePadding)
|
||||
.nodeId(nodeId)
|
||||
.size([$width, $height])
|
||||
.linkSort(linkSort);
|
||||
|
||||
$: sankeyData = sankey($data);
|
||||
|
||||
$: link = Sankey.sankeyLinkHorizontal();
|
||||
|
||||
$: fontSize = $width <= 320 ? 8 : 16;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
text {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<g class="sankey-layer">
|
||||
<g class='link-group'>
|
||||
{#each sankeyData.links as d}
|
||||
<path
|
||||
d={link(d)}
|
||||
fill='none'
|
||||
stroke={colorLinks(d)}
|
||||
stroke-opacity='0.5'
|
||||
stroke-width={d.width} />
|
||||
{/each}
|
||||
</g>
|
||||
<g class='rect-group'>
|
||||
{#each sankeyData.nodes as d, i}
|
||||
<rect
|
||||
x={d.x0}
|
||||
y={d.y0}
|
||||
height={d.y1 - d.y0}
|
||||
width={d.x1 - d.x0}
|
||||
fill={i ===(sankeyData.nodes.length-1) ? colorNodes(d) : '#EA7CCC'} />
|
||||
<text
|
||||
x={d.x0 < $width / 4 ? d.x1 + 6 : d.x0 - 6}
|
||||
y={(d.y1 + d.y0) / 2}
|
||||
dy="{(fontSize / 2) - 2}"
|
||||
style="fill: {colorText(d)};
|
||||
font-size: {fontSize}px;
|
||||
text-anchor: {d.x0 < $width / 4 ? 'start' : 'end'};">
|
||||
{d.id}
|
||||
</text>
|
||||
{/each}
|
||||
</g>
|
||||
</g>
|
||||
104
src/lib/components/Scatter/AxisX.html.svelte
Normal file
104
src/lib/components/Scatter/AxisX.html.svelte
Normal file
@ -0,0 +1,104 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an HTML x-axis, useful for server-side rendered charts. This component is also configured to detect if your x-scale is an ordinal scale. If so, it will place the markers in the middle of the bandwidth.
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const { xScale } = getContext('LayerCake');
|
||||
|
||||
/** @type {Boolean} [gridlines=true] - Extend lines from the ticks into the chart space. */
|
||||
export let gridlines = false;
|
||||
|
||||
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */
|
||||
export let tickMarks = true;
|
||||
|
||||
/** @type {Boolean} [baseline=false] – Show a solid line at the bottom. */
|
||||
export let baseline = true;
|
||||
|
||||
/** @type {Boolean} [snapTicks=false] - Instead of centering the text on the first and the last items, align them to the edges of the chart. */
|
||||
export let snapTicks = true;
|
||||
|
||||
/** @type {Function} [formatTick=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
|
||||
export let formatTick = d => 'FY'+d;
|
||||
|
||||
/** @type {Number|Array|Function} [ticks] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. If nothing, it uses the default ticks supplied by the D3 function. */
|
||||
export let ticks = undefined
|
||||
|
||||
/** @type {Number} [yTick=7] - The distance from the baseline to place each tick value, in pixels. */
|
||||
export let yTick = 15;
|
||||
|
||||
$: isBandwidth = typeof $xScale.bandwidth === 'function';
|
||||
|
||||
$: tickVals = Array.isArray(ticks) ? ticks :
|
||||
isBandwidth ?
|
||||
$xScale.domain() :
|
||||
typeof ticks === 'function' ?
|
||||
ticks($xScale.ticks()) :
|
||||
$xScale.ticks(ticks);
|
||||
</script>
|
||||
|
||||
<div class='axis x-axis' class:snapTicks>
|
||||
{#each tickVals as tick, i (tick)}
|
||||
{#if gridlines !== false}
|
||||
<div class="gridline" style='left:{$xScale(tick)}%;top: 0px;bottom: 0;'></div>
|
||||
{/if}
|
||||
{#if tickMarks === true}
|
||||
<div class="tick-mark" style='left:{$xScale(tick) + (isBandwidth ? $xScale.bandwidth() / 2 : 0)}%;height:6px;bottom: -6px;'></div>
|
||||
{/if}
|
||||
<div
|
||||
class='tick tick-{ i }'
|
||||
style='left:{$xScale(tick) + (isBandwidth ? $xScale.bandwidth() / 2 : 0)}%;top:100%;'>
|
||||
<div
|
||||
class="text"
|
||||
style='top:{(yTick)}px;'>{formatTick(tick)}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if baseline === true}
|
||||
<div class="baseline" style='top: 100%;width: 100%;'></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.axis,
|
||||
.tick,
|
||||
.tick-mark,
|
||||
.gridline,
|
||||
.baseline {
|
||||
position: absolute;
|
||||
}
|
||||
.axis {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tick {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.gridline {
|
||||
border-left: 1px dashed #fff;
|
||||
}
|
||||
|
||||
.tick-mark {
|
||||
border-left: 1px solid #fff;
|
||||
}
|
||||
.baseline {
|
||||
border-top: 1px solid #fff;
|
||||
}
|
||||
|
||||
.tick .text {
|
||||
color: #fff;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
/* This looks a little better at 40 percent than 50 */
|
||||
.axis.snapTicks .tick:last-child {
|
||||
transform: translateX(-40%);
|
||||
}
|
||||
.axis.snapTicks .tick.tick-0 {
|
||||
transform: translateX(40%);
|
||||
}
|
||||
</style>
|
||||
98
src/lib/components/Scatter/AxisY.html.svelte
Normal file
98
src/lib/components/Scatter/AxisY.html.svelte
Normal file
@ -0,0 +1,98 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an HTML y-axis.
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const { padding, xRange, yScale } = getContext('LayerCake');
|
||||
|
||||
/** @type {Boolean} [gridlines=true] - Extend lines from the ticks into the chart space */
|
||||
export let gridlines = false;
|
||||
|
||||
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */
|
||||
export let tickMarks = false;
|
||||
|
||||
/** @type {Boolean} [baseline=false] – Show a solid line at the bottom. */
|
||||
export let baseline = false;
|
||||
|
||||
/** @type {Function} [formatTick=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
|
||||
export let formatTick = d => d;
|
||||
|
||||
/** @type {Number|Array|Function} [ticks=4] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. */
|
||||
export let ticks = 4;
|
||||
|
||||
/** @type {Number} [xTick=-4] - How far over to position the text marker. */
|
||||
export let xTick = -4;
|
||||
|
||||
/** @type {Number} [yTick=-1] - How far up and down to position the text marker. */
|
||||
export let yTick = -5;
|
||||
|
||||
$: isBandwidth = typeof $yScale.bandwidth === 'function';
|
||||
|
||||
$: tickVals = Array.isArray(ticks) ? ticks :
|
||||
isBandwidth ?
|
||||
$yScale.domain() :
|
||||
typeof ticks === 'function' ?
|
||||
ticks($yScale.ticks()) :
|
||||
$yScale.ticks(ticks);
|
||||
</script>
|
||||
|
||||
<div class='axis y-axis' style='transform:translate(-{$padding.left}px, 0)'>
|
||||
{#each tickVals as tick, i (tick)}
|
||||
<div class='tick tick-{i}' style='top:{$yScale(tick) + (isBandwidth ? $yScale.bandwidth () / 2 : 0)}%;left:{$xRange[0]}%;'>
|
||||
{#if gridlines !== false}
|
||||
<div class="gridline" style='top:0;left:{isBandwidth ? $padding.left : 0}px;right:-{$padding.left + $padding.right}px;'></div>
|
||||
{/if}
|
||||
{#if baseline !== false && i === 0}
|
||||
<div class="gridline baseline" style='top:0;left:{isBandwidth ? $padding.left : 0};right:-{$padding.left + $padding.right}px;'></div>
|
||||
{/if}
|
||||
{#if tickMarks === true}
|
||||
<div class="tick-mark" style='top:0;left:{isBandwidth ? $padding.left - 6 : 0}px;width:6px;'></div>
|
||||
{/if}
|
||||
<div
|
||||
class="text"
|
||||
style='
|
||||
top:{yTick}px;
|
||||
left:{isBandwidth ? ($padding.left + xTick - 4) : 0}px;
|
||||
transform: translate({isBandwidth ? '-100%' : 0}, {isBandwidth ? -50 - Math.floor($yScale.bandwidth() / -2) : '-100'}%);
|
||||
'
|
||||
>{formatTick(tick)}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.axis,
|
||||
.tick,
|
||||
.tick-mark,
|
||||
.gridline,
|
||||
.baseline,
|
||||
.text {
|
||||
position: absolute;
|
||||
}
|
||||
.axis {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tick {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.gridline {
|
||||
border-top: 1px dashed #aaa;
|
||||
}
|
||||
.tick-mark {
|
||||
border-top: 1px solid #aaa;
|
||||
}
|
||||
|
||||
.baseline.gridline {
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.tick .text {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
46
src/lib/components/Scatter/Scatter.html.svelte
Normal file
46
src/lib/components/Scatter/Scatter.html.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an HTML scatter plot. This component can also work if the x- or y-scale is ordinal, i.e. it has a `.bandwidth` method. See the [timeplot chart](https://layercake.graphics/example/Timeplot) for an example.
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const { data, xGet, yGet, xScale, yScale } = getContext('LayerCake');
|
||||
|
||||
/** @type {Number} [r=5] - The circle's radius. */
|
||||
export let r = 5;
|
||||
|
||||
/** @type {String} [fill='#0cf'] - The circle's fill color. */
|
||||
export let fill = '#0cf';
|
||||
|
||||
/** @type {String} [stroke='#000'] - The circle's stroke color. */
|
||||
export let stroke = '#000';
|
||||
|
||||
/** @type {Number} [strokeWidth=1] - The circle's stroke width. */
|
||||
export let strokeWidth = 1;
|
||||
|
||||
</script>
|
||||
|
||||
<div class="scatter-group">
|
||||
{#each $data as d}
|
||||
<div
|
||||
class="circle"
|
||||
style="
|
||||
left: {$xGet(d) + ($xScale.bandwidth ? $xScale.bandwidth() / 2 : 0)}%;
|
||||
top: {$yGet(d) + ($yScale.bandwidth ? $yScale.bandwidth() / 2 : 0)}%;
|
||||
width: {r * 2}px;
|
||||
height: {r * 2}px;
|
||||
background-color: {d?.dataset === 'actual' ? '#10C208' : 'rgb(0,187,255,0.4)'};
|
||||
border: {strokeWidth}px solid {d?.dataset === 'actual' ? '#10C208' : 'rgb(0,187,255,0.4)'};
|
||||
"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.circle {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
32
src/lib/components/ScrollToTop.svelte
Normal file
32
src/lib/components/ScrollToTop.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang='ts'>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
let isScrolled = false;
|
||||
|
||||
function handleScroll() {
|
||||
// Check the scroll position
|
||||
isScrolled = window.scrollY > 250;
|
||||
}
|
||||
|
||||
function handleScrollUp() {
|
||||
window.scrollTo({
|
||||
top: 0, // Scrolls to the top of the page
|
||||
});
|
||||
}
|
||||
onMount(() => {
|
||||
|
||||
window?.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
// Remove the event listener when the component is unmounted
|
||||
window?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="{!isScrolled ? 'hidden' : ''} sm:hidden fixed z-40 bottom-8 sm:bottom-10 right-8 sm:right-16">
|
||||
<label on:click={handleScrollUp} class="inline-flex items-center justify-center w-12 h-12 sm:w-full sm:h-10 font-medium bg-gray-700 sm:bg-[#FFEDE5] ml-1 mr-0 sm:mr-2 rounded-full cursor-pointer">
|
||||
<svg class="sm:hidden sm:ml-4 w-6 h-6 text-white inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path d="M24 0v24H0V0zM12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036c-.01-.003-.019 0-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M19 5a1 1 0 1 0 0-2H5a1 1 0 1 0 0 2zM7.05 12.703a1 1 0 0 0 1.415 0L11 10.167V20a1 1 0 0 0 2 0v-9.833l2.536 2.536a1 1 0 0 0 1.414-1.415l-4.243-4.242a1 1 0 0 0-1.414 0L7.05 11.288a1 1 0 0 0 0 1.415"/></g></svg>
|
||||
</label>
|
||||
</div>
|
||||
651
src/lib/components/Searchbar.svelte
Normal file
651
src/lib/components/Searchbar.svelte
Normal file
@ -0,0 +1,651 @@
|
||||
<script lang="ts">
|
||||
|
||||
import { userRegion, searchBarData, stockTicker, etfTicker, cryptoTicker, screenWidth} from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
let apiURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
apiURL = import.meta.env.VITE_USEAST_API_URL;
|
||||
} else {
|
||||
apiURL = import.meta.env.VITE_EU_API_URL;
|
||||
}
|
||||
});
|
||||
|
||||
let assetType = '';
|
||||
|
||||
let showSuggestions = false;
|
||||
let notFoundTicker = false;
|
||||
let searchQuery = '';
|
||||
|
||||
let searchOpen = false;
|
||||
let searchBarModalChecked = false; // Initialize it to false
|
||||
let inputElement;
|
||||
|
||||
|
||||
function popularTicker(state, assetType) {
|
||||
searchOpen = false;
|
||||
|
||||
if (assetType === 'ETF')
|
||||
{
|
||||
etfTicker.update( value => state?.toUpperCase() );
|
||||
}
|
||||
else if (assetType === 'Stock') {
|
||||
stockTicker.update( value => state?.toUpperCase() );
|
||||
}
|
||||
else if (assetType === 'Crypto') {
|
||||
cryptoTicker.update( value => state?.toUpperCase() );
|
||||
}
|
||||
|
||||
|
||||
const closePopup = document.getElementById("searchBarModal");
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
|
||||
}
|
||||
|
||||
function searchBarTicker(state, assetType)
|
||||
{
|
||||
showSuggestions = false;
|
||||
if(state !== '' && $searchBarData?.find(item => item?.symbol === state?.toUpperCase()))
|
||||
{
|
||||
notFoundTicker = false;
|
||||
if (assetType === 'ETF')
|
||||
{
|
||||
etfTicker.update( value => state?.toUpperCase() );
|
||||
goto(`/etf/${state?.toUpperCase()}`)
|
||||
}
|
||||
else if (assetType === 'Stock') {
|
||||
stockTicker.update( value => state?.toUpperCase() );
|
||||
goto(`/stocks/${state?.toUpperCase()}`)
|
||||
}
|
||||
else if (assetType === 'Crypto') {
|
||||
cryptoTicker.update( value => state?.toUpperCase() );
|
||||
goto(`/crypto/${state?.toUpperCase()}`)
|
||||
}
|
||||
|
||||
searchOpen = false
|
||||
//searchQuery = state.toUpperCase();
|
||||
const closePopup = document.getElementById("searchBarModal");
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
else{
|
||||
notFoundTicker = false;
|
||||
}
|
||||
|
||||
|
||||
searchQuery ="";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
let searchResults = [];
|
||||
|
||||
|
||||
async function search() {
|
||||
const normalizedSearchQuery = searchQuery?.toLowerCase();
|
||||
|
||||
// Define names for which symbols without dots should be prioritized
|
||||
const prioritizeWithoutDotsForNames = ["apple", /* Add more names as needed */];
|
||||
|
||||
const filteredList = $searchBarData?.map(item => ({
|
||||
...item,
|
||||
nameLower: item?.name?.toLowerCase(),
|
||||
symbolLower: item?.symbol?.toLowerCase(),
|
||||
}))?.filter(({ nameLower, symbolLower }) =>
|
||||
nameLower?.includes(normalizedSearchQuery) ||
|
||||
symbolLower?.includes(normalizedSearchQuery)
|
||||
);
|
||||
|
||||
filteredList?.sort((a, b) => {
|
||||
const aSymbolLower = a?.symbolLower;
|
||||
const bSymbolLower = b?.symbolLower;
|
||||
const aNameLower = a?.nameLower;
|
||||
const bNameLower = b?.nameLower;
|
||||
|
||||
// Check for exact symbol matches
|
||||
const isExactMatchA = aSymbolLower === normalizedSearchQuery;
|
||||
const isExactMatchB = bSymbolLower === normalizedSearchQuery;
|
||||
|
||||
if (isExactMatchA && !isExactMatchB) {
|
||||
return -1; // Prioritize exact symbol match for A
|
||||
} else if (!isExactMatchA && isExactMatchB) {
|
||||
return 1; // Prioritize exact symbol match for B
|
||||
}
|
||||
|
||||
const aSymbolIndex = aSymbolLower?.indexOf(normalizedSearchQuery);
|
||||
const bSymbolIndex = bSymbolLower?.indexOf(normalizedSearchQuery);
|
||||
|
||||
const aNameIndex = aNameLower?.indexOf(normalizedSearchQuery);
|
||||
const bNameIndex = bNameLower?.indexOf(normalizedSearchQuery);
|
||||
|
||||
// If no exact symbol match, prioritize based on the combined position in name and symbol
|
||||
const positionComparison = aSymbolIndex + aNameIndex - (bSymbolIndex + bNameIndex);
|
||||
|
||||
// Additional condition for prioritizing symbols without dots for specific names
|
||||
if (prioritizeWithoutDotsForNames.includes(normalizedSearchQuery)) {
|
||||
const aHasDot = aSymbolLower?.includes(".") || false;
|
||||
const bHasDot = bSymbolLower?.includes(".") || false;
|
||||
|
||||
// Prioritize results without dots for the specified names
|
||||
return aHasDot - bHasDot || positionComparison;
|
||||
}
|
||||
|
||||
return positionComparison;
|
||||
});
|
||||
|
||||
searchResults = filteredList?.slice(0, 5);
|
||||
showSuggestions = normalizedSearchQuery !== "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const onKeyPress = e => {
|
||||
if (e?.charCode === 13) {
|
||||
focusedSuggestion = '';
|
||||
|
||||
if (!notFoundTicker) {
|
||||
searchBarTicker(searchQuery, assetType);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let focusedSuggestion = null;
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "ArrowDown" && showSuggestions) {
|
||||
// Move down in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = searchResults?.findIndex(item => item?.symbol === searchQuery);
|
||||
if (currentIndex < searchResults?.length - 1) {
|
||||
searchQuery = searchResults[currentIndex + 1]?.symbol;
|
||||
assetType = searchResults[currentIndex + 1]?.type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
} else if (event.key === "ArrowUp" && showSuggestions) {
|
||||
// Move up in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = searchResults?.findIndex(item => item?.symbol === searchQuery);
|
||||
if (currentIndex > 0) {
|
||||
searchQuery = searchResults[currentIndex - 1]?.symbol;
|
||||
assetType = searchResults[currentIndex - 1]?.type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
}
|
||||
|
||||
else if (event.key === "ArrowDown" && !showSuggestions) {
|
||||
// Move down in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = popularList?.findIndex(item => item?.symbol === searchQuery);
|
||||
if (currentIndex < popularList?.length - 1) {
|
||||
searchQuery = popularList[currentIndex + 1]?.symbol;
|
||||
assetType = popularList[currentIndex + 1]?.type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
}
|
||||
else if (event.key === "ArrowUp" && !showSuggestions) {
|
||||
// Move up in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = popularList?.findIndex(item => item?.symbol === searchQuery);
|
||||
if (currentIndex > 0) {
|
||||
searchQuery = popularList[currentIndex - 1]?.symbol;
|
||||
assetType = popularList[currentIndex - 1]?.type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleControlF = (event) => {
|
||||
if (event.ctrlKey && event.key === 'k') {
|
||||
// Ctrl+F is pressed, open the modal
|
||||
const keyboardSearch = document.getElementById('searchBarModal');
|
||||
keyboardSearch?.dispatchEvent(new MouseEvent('click'))
|
||||
event.preventDefault()
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
// Escape key is pressed, close the modal
|
||||
searchBarModalChecked = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleControlF);
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleControlF);
|
||||
window.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
let charNumber = 20;
|
||||
|
||||
|
||||
|
||||
let popularList = [];
|
||||
const popularSymbols = ['BTCUSD','ETHUSD','SOLUSD','SPY','ADBE','DBX','HOOD','AMZN', 'TSLA', 'AMD','MCD','NVDA','PYPL','AAPL','BYND','KO'];
|
||||
|
||||
|
||||
$: {
|
||||
if ($searchBarData && popularList?.length === 0) {
|
||||
popularList = $searchBarData?.filter(({ symbol }) =>
|
||||
popularSymbols?.includes(symbol)
|
||||
);
|
||||
|
||||
// Fisher-Yates (Knuth) Shuffle Algorithm
|
||||
for (let i = popularList?.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[popularList[i], popularList[j]] = [popularList[j], popularList[i]];
|
||||
}
|
||||
|
||||
popularList = popularList?.slice(0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if( searchBarModalChecked === true && typeof window !== 'undefined')
|
||||
{
|
||||
if($screenWidth > 640)
|
||||
{
|
||||
inputElement.focus();
|
||||
}
|
||||
//Page is not scrollable now
|
||||
document.body.classList.add("overflow-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if( searchBarModalChecked === false && typeof window !== 'undefined')
|
||||
{
|
||||
showSuggestions = searchQuery = "";
|
||||
document.body.classList?.remove("overflow-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if(searchQuery?.length !== 0)
|
||||
{
|
||||
notFoundTicker = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<label for="searchBarModal"
|
||||
class="cursor-pointer w-8 h-8 flex items-center justify-center bg-[#3C74D4] rounded-full">
|
||||
<span class="sr-only">Search</span>
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="fill-current text-white" d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
|
||||
<path class="fill-current text-white" d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
|
||||
<!--
|
||||
<div class="ml-5 w-96 grow">
|
||||
<div class="relative flex items-center">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-2.5">
|
||||
<svg
|
||||
class="text-icon h-5 w-5"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
style="max-width: 40px"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class="rounded-lg py-1.5 pl-10 text-sm placeholder-gray-400 border border-[#313131] delay-100 transition ease-out focus:outline-none focus:ring-1 tiny:pl-8 xs:pl-10 xs:text-base md:py-2 w-full bg-[#313131]"
|
||||
type="text"
|
||||
aria-label="Search"
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-controls="owned_listbox"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
placeholder="Company or stock symbol..."
|
||||
name="q"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-11 z-40 max-h-[265px] w-96 overflow-y-auto border-default bg-[#313131] border border-[#313131]">
|
||||
<h4 class="border-b px-2 py-1.5 text-left text-md font-semibold sm:px-3">
|
||||
Popular
|
||||
</h4>
|
||||
<ul class="text-sm" >
|
||||
{#if !showSuggestions }
|
||||
{#each popularList as item}
|
||||
<li>
|
||||
<a href={`/${item?.type === 'ETF' ? 'etf' : item?.type === 'Crypto' ? 'crypto' : 'stocks'}/${item?.symbol}`} on:click={() => popularTicker(item?.symbol, item?.assetType) } class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'shake-ticker cursor-pointer border-b border-gray-600 flex justify-start items-center p-2 text-white group'} w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<div class="flex flex-col ml-2">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name.length > 150 ? item?.name?.slice(0,150) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-white text-sm font-medium ml-auto">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!--Start Searchbar Modal-->
|
||||
|
||||
{#if $screenWidth > 640}
|
||||
<input type="checkbox" id="searchBarModal" class="modal-toggle" bind:checked={searchBarModalChecked} />
|
||||
|
||||
<dialog id="searchBarModal" class="modal modal-top ">
|
||||
|
||||
|
||||
<label for="searchBarModal" class="cursor-pointer modal-backdrop"></label>
|
||||
|
||||
|
||||
|
||||
<div class="modal-box overflow-hidden rounded-xl bg-[#09090B] sm:my-8 sm:m-auto sm:h-auto w-full sm:w-1/2 md:w-3/4 lg:w-1/2 2xl:w-1/3 " >
|
||||
|
||||
|
||||
<!-- Search layout -->
|
||||
<div class="mt-5 sm:mt-0">
|
||||
<div class="relative">
|
||||
<label for="modal-search" class="sr-only">Search</label>
|
||||
<input
|
||||
id="modal-search"
|
||||
class="rounded-lg w-full text-white bg-[#09090B] border border-slate-800 focus:ring-transparent placeholder-gray-200 py-3 pl-10 pr-4"
|
||||
type="search"
|
||||
placeholder="Search Anything…"
|
||||
bind:value={searchQuery}
|
||||
bind:this={inputElement}
|
||||
on:input={search}
|
||||
on:keydown={handleKeyDown}
|
||||
on:keypress={onKeyPress}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button on:click={() => (searchBarTicker(searchQuery, assetType))} class="absolute inset-0 right-auto group" type="submit" aria-label="Search">
|
||||
<svg class="w-4 h-4 shrink-0 fill-current text-white ml-4 mr-2 text-slate-400" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
|
||||
<path d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $searchBarData?.length !== 0}
|
||||
<div class="py-4">
|
||||
<!-- Popular searches -->
|
||||
<div class="mb-3 last:mb-0 mt-3">
|
||||
{#if notFoundTicker}
|
||||
<p class="text-xs font-semibold text-[#FB6A67] px-2 mb-4">Oh snapp, ticker does not exist in our database</p>
|
||||
{/if}
|
||||
{#if !showSuggestions}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Popular
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="text-sm" >
|
||||
{#if !showSuggestions }
|
||||
{#each popularList as item}
|
||||
<li>
|
||||
<a href={`/${item?.type === 'ETF' ? 'etf' : item?.type === 'Crypto' ? 'crypto' : 'stocks'}/${item?.symbol}`} on:click={() => popularTicker(item?.symbol, item?.assetType) } class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'shake-ticker cursor-pointer bg-[#202020] rounded-lg flex justify-start items-center p-2 text-white group'} w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#000] flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6" src={`https://financialmodelingprep.com/image-stock/${item?.symbol}.png`} loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col ml-2">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name.length > 150 ? item?.name?.slice(0,150) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
|
||||
{:else if showSuggestions && searchResults?.length > 0}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Suggestions
|
||||
</div>
|
||||
{#each searchResults as item}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label data-sveltekit-preload-data="false" on:click={() => (searchBarTicker(item?.symbol, item?.type))} class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'cursor-pointer mb-2 bg-[#202020] rounded-lg flex justify-start items-center p-2 text-white group'}">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name?.length > 150 ? item?.name?.slice(0,150) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
{:else if showSuggestions && searchResults?.length === 0}
|
||||
<li>
|
||||
<label class="flex items-center p-2 text-white hover:text-white hover:bg-[#404040] bg-opacity-[0.25] rounded group" >
|
||||
<svg class="w-3 h-3 fill-slate-400 shrink-0 mr-3 dark:fill-slate-500" width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.953 4.29a.5.5 0 0 0-.454-.292H6.14L6.984.62A.5.5 0 0 0 6.12.173l-6 7a.5.5 0 0 0 .379.825h5.359l-.844 3.38a.5.5 0 0 0 .864.445l6-7a.5.5 0 0 0 .075-.534Z" />
|
||||
</svg>
|
||||
<span>No results found</span>
|
||||
</label>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center m-auto mt-4 py-20">
|
||||
<span class="loading loading-lg loading-spinner text-success"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<label for="searchBarModal" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
{:else}
|
||||
<!--Start Drawer Sidewise for mobile-->
|
||||
|
||||
|
||||
<div class="drawer drawer-end overflow-hidden" style="z-index: 9999">
|
||||
<input id="searchBarModal" type="checkbox" class="drawer-toggle" bind:checked={searchBarModalChecked} />
|
||||
|
||||
<div class="drawer-side overflow-hidden">
|
||||
|
||||
|
||||
|
||||
<div class="modal-box overflow-hidden rounded-xl bg-[#09090B] min-h-screen w-screen pt-10" >
|
||||
|
||||
|
||||
<!-- Search layout -->
|
||||
<div class="mt-5 lg:mt-0">
|
||||
<div class="relative">
|
||||
<label for="modal-search" class="sr-only">Search</label>
|
||||
<input
|
||||
id="modal-search"
|
||||
class="rounded-lg w-full text-white bg-[#09090B] border border-slate-800 focus:ring-transparent placeholder-gray-200 py-3 pl-10 pr-4"
|
||||
type="search"
|
||||
placeholder="Search Anything…"
|
||||
bind:value={searchQuery}
|
||||
bind:this={inputElement}
|
||||
on:input={search}
|
||||
on:keydown={handleKeyDown}
|
||||
on:keypress={onKeyPress}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button on:click={() => (searchBarTicker(searchQuery, assetType))} class="absolute inset-0 right-auto group" type="submit" aria-label="Search">
|
||||
<svg class="w-4 h-4 shrink-0 fill-current text-white ml-4 mr-2 text-slate-400" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <path d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
|
||||
<path d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $searchBarData?.length !== 0}
|
||||
<div class="py-4">
|
||||
<!-- Popular searches -->
|
||||
<div class="mb-3 last:mb-0 mt-3">
|
||||
{#if notFoundTicker}
|
||||
<p class="text-xs font-semibold text-[#FB6A67] px-2 mb-4">Oh snapp, ticker does not exist in our database</p>
|
||||
{/if}
|
||||
{#if !showSuggestions}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Popular
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="text-sm" >
|
||||
{#if !showSuggestions }
|
||||
{#each popularList as item}
|
||||
<li>
|
||||
<a href={`/${item?.type === 'ETF' ? 'etf' : item?.type === 'Crypto' ? 'crypto' : 'stocks'}/${item?.symbol}`} on:click={() => popularTicker(item?.symbol, item?.type) } class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'cursor-pointer bg-[#202020] bg-opacity-[0.4] rounded-lg flex justify-start items-center p-2 text-white group'} w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#000] flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6" src={`https://financialmodelingprep.com/image-stock/${item?.symbol}.png`} loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col ml-2">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name.length > charNumber ? item?.name.slice(0,charNumber) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto mr-2">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
|
||||
{:else if showSuggestions && searchResults?.length > 0}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Suggestions
|
||||
</div>
|
||||
{#each searchResults as item}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label data-sveltekit-preload-data="false" on:click={() => (searchBarTicker(item?.symbol, item?.type))} class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'cursor-pointer mb-2 bg-[#202020] bg-opacity-[0.4] rounded-lg flex justify-start items-center p-2 text-white group'}">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
|
||||
<div class="flex flex-col ml-1">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name.length > charNumber ? item?.name.slice(0,charNumber) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto mr-2">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
{:else if showSuggestions && searchResults?.length === 0}
|
||||
<li>
|
||||
<label class="flex items-center p-2 text-white hover:text-white hover:bg-[#404040] bg-opacity-[0.25] rounded group" >
|
||||
<svg class="w-3 h-3 fill-slate-400 shrink-0 mr-3 dark:fill-slate-500" width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.953 4.29a.5.5 0 0 0-.454-.292H6.14L6.984.62A.5.5 0 0 0 6.12.173l-6 7a.5.5 0 0 0 .379.825h5.359l-.844 3.38a.5.5 0 0 0 .864.445l6-7a.5.5 0 0 0 .075-.534Z" />
|
||||
</svg>
|
||||
<span>No results found</span>
|
||||
</label>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center m-auto mt-4 py-20">
|
||||
<span class="loading loading-lg loading-spinner text-success"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<label for="searchBarModal" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--End Drawer Sidewise for mobile-->
|
||||
{/if}
|
||||
<!--End Searchbar Modal-->
|
||||
164
src/lib/components/SectorSegmentation.svelte
Normal file
164
src/lib/components/SectorSegmentation.svelte
Normal file
@ -0,0 +1,164 @@
|
||||
<script lang='ts'>
|
||||
|
||||
import {etfTicker, screenWidth} from '$lib/store';
|
||||
import { formatString } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte'
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
export let sectorList;
|
||||
|
||||
|
||||
let showFullStats = false;
|
||||
|
||||
const sectorNavigation = [
|
||||
{
|
||||
title: 'Financial Services',
|
||||
link: '/list/financial-sector'
|
||||
},
|
||||
{
|
||||
title: 'Finance',
|
||||
link: '/list/financial-sector'
|
||||
},
|
||||
{
|
||||
title: 'Healthcare',
|
||||
link: '/list/healthcare-sector'
|
||||
},
|
||||
{
|
||||
title: 'Technology',
|
||||
link: '/list/technology-sector'
|
||||
},
|
||||
{
|
||||
title: 'Industrials',
|
||||
link: '/list/industrials-sector'
|
||||
},
|
||||
{
|
||||
title: 'Energy',
|
||||
link: '/list/energy-sector'
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
link: '/list/utilities-sector'
|
||||
},
|
||||
{
|
||||
title: 'Consumer Cyclical',
|
||||
link: '/list/consumer-cyclical-sector'
|
||||
},
|
||||
{
|
||||
title: 'Real Estate',
|
||||
link: '/list/real-estate-sector'
|
||||
},
|
||||
{
|
||||
title: 'Basic Materials',
|
||||
link: '/list/basic-materials-sector'
|
||||
},
|
||||
{
|
||||
title: 'Communication Services',
|
||||
link: '/list/communication-services-sector'
|
||||
},
|
||||
{
|
||||
title: 'Consumer Defensive',
|
||||
link: '/list/consumer-defensive-sector'
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
function sectorSelector(sector) {
|
||||
const selectedSector = sectorNavigation?.find(item => item?.title === sector);
|
||||
|
||||
if (selectedSector) {
|
||||
goto(selectedSector?.link);
|
||||
} else {
|
||||
// Handle the case when the sector is not found
|
||||
console.error(`Sector not found: ${sector}`);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($etfTicker && typeof window !== 'undefined' && typeof sectorList !== 'undefined' && sectorList?.length !== 0)
|
||||
{
|
||||
showFullStats = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden text-white h-full sm:mb-0 w-full">
|
||||
<div class="flex justify-center w-full m-auto h-full overflow-hidden">
|
||||
<div class="relative flex justify-center items-center overflow-hidden w-full">
|
||||
<div class="w-fit sm:w-full sm:max-w-2xl m-auto mt-5 sm:mt-0">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="sectorWeightingInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Sector Weighting Breakdown
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Sector Weighting Breakdown"}
|
||||
content={"Each ETF possesses unique investment characteristics, with allocations in various sectors. Some sectors inherently carry higher risks, yet they also offer the potential for greater returns."}
|
||||
id={"sectorWeightingInfo"}
|
||||
/>
|
||||
</div>
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
{#if sectorList?.length !== 0}
|
||||
|
||||
<div class="text-white text-md mt-3">
|
||||
Which sectors does <span class="text-blue-400">${$etfTicker}</span> invest in? We can break it down into {sectorList?.length} distinct sectors.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="w-full rounded-full flex justify-center items-center">
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<!--Start Progress-->
|
||||
{#each (showFullStats ? sectorList : sectorList?.slice(0,3)) as item,index}
|
||||
|
||||
<div on:click= {() => sectorSelector(item?.industry)} class="shadow-lg bg-[#202020] w-full rounded-lg p-4 mb-5 flex flex-row items-center {index === 0 ? 'mt-4' : ''} {index === 2 && !showFullStats && sectorList?.length > 3 ? 'opacity-[0.5]' : '' }">
|
||||
|
||||
<div class="flex flex-col -mt-2 w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<span class="text-white text-sm sm:text-md font-medium text-start mb-2 mr-auto mt-2">
|
||||
{formatString(item?.industry)}
|
||||
</span>
|
||||
<span class="text-white text-sm sm:text-md font-medium ml-auto">
|
||||
{item?.exposure <= 0.01 ? "< 0.01%" : item?.exposure?.toFixed(2)+'%'}
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress [&::-webkit-progress-value]:bg-[#10DB06] [&::-moz-progress-bar]:bg-[#10DB06]" value={(item?.exposure)} max="100"></progress>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
||||
<!--End Progress-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if sectorList?.length >=4}
|
||||
<label on:click={() => showFullStats = !showFullStats} class="cursor-pointer flex justify-center items-center">
|
||||
<svg class="w-10 h-10 transform {showFullStats ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class=" mt-10 justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
</Lazy>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
503
src/lib/components/SellTrade.svelte
Normal file
503
src/lib/components/SellTrade.svelte
Normal file
@ -0,0 +1,503 @@
|
||||
<script lang='ts'>
|
||||
import toast from 'svelte-french-toast';
|
||||
import {userRegion, currentPortfolioPrice, displayCompanyName, screenWidth, traded, stockTicker, etfTicker, cryptoTicker, assetType} from '$lib/store';
|
||||
|
||||
|
||||
export let data;
|
||||
export let availableCash;
|
||||
export let holdingShares;
|
||||
|
||||
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let fastifyURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
fastifyURL = import.meta.env.VITE_USEAST_FASTIFY_URL;
|
||||
} else {
|
||||
fastifyURL = import.meta.env.VITE_EU_FASTIFY_URL;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
let estimatedTotal = 0;
|
||||
let numberOfShares = 0;
|
||||
let commissionPrice = 0;
|
||||
|
||||
let displayTab = 'changeOrder';
|
||||
|
||||
function handleMaxOrder()
|
||||
{
|
||||
|
||||
numberOfShares = holdingShares;
|
||||
estimatedTotal = (numberOfShares * $currentPortfolioPrice);
|
||||
}
|
||||
|
||||
|
||||
function handleInputChange(event) {
|
||||
const inputValue = event.target.value;
|
||||
|
||||
if (!isNaN(inputValue) && Number(inputValue) >= 0) {
|
||||
numberOfShares = Number(inputValue);
|
||||
} else {
|
||||
numberOfShares = 0;
|
||||
}
|
||||
|
||||
estimatedTotal = numberOfShares * $currentPortfolioPrice;
|
||||
}
|
||||
|
||||
function handleAppendNumber(num) {
|
||||
// Append the clicked number to the current numberOfShares
|
||||
numberOfShares = numberOfShares * 10 + num;
|
||||
}
|
||||
|
||||
function handleRemoveNumber() {
|
||||
// Remove the last digit from numberOfShares or set to zero if NaN
|
||||
const numStr = numberOfShares.toString();
|
||||
numberOfShares = numStr.length > 1 ? parseInt(numStr.slice(0, -1), 10) : 0;
|
||||
}
|
||||
|
||||
|
||||
function changeTab() {
|
||||
|
||||
if(displayTab === 'changeOrder' && numberOfShares > 0 && numberOfShares <= holdingShares)
|
||||
{
|
||||
displayTab = 'sellOrder';
|
||||
|
||||
}
|
||||
|
||||
else if (displayTab === 'sellOrder')
|
||||
{
|
||||
displayTab = 'changeOrder';
|
||||
}
|
||||
|
||||
else {
|
||||
toast.error('Please check your Input values again.', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSellOrder()
|
||||
{
|
||||
|
||||
|
||||
const postData = {
|
||||
'userId': data?.user?.id,
|
||||
'symbol': $assetType === 'stock' ? $stockTicker : $assetType === 'etf' ? $etfTicker : $cryptoTicker,
|
||||
'assetType': $assetType,
|
||||
'name': $displayCompanyName,
|
||||
'numberOfShares': numberOfShares,
|
||||
'estimatedTotal': estimatedTotal,
|
||||
'soldPrice': $currentPortfolioPrice,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Make the POST request to the endpoint
|
||||
const response = await fetch(fastifyURL+'/sell-stock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
const output = (await response.json())?.message;
|
||||
|
||||
|
||||
if (output === 'success')
|
||||
{
|
||||
|
||||
toast.success(`Successfully sold ${numberOfShares} ${$stockTicker} shares`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
|
||||
numberOfShares = 0;
|
||||
estimatedTotal = 0;
|
||||
displayTab = 'changeOrder';
|
||||
|
||||
$traded = true;
|
||||
|
||||
const closePopup = document.getElementById("sellTradeModal");
|
||||
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
|
||||
}
|
||||
else if (output === 'failure')
|
||||
{
|
||||
toast.error(`Something went wrong. Please try again.`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
else if (output === 'marketClosed')
|
||||
{
|
||||
toast.error(`The market is closed. Please try again later.`, {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
$: {
|
||||
if(numberOfShares)
|
||||
{
|
||||
estimatedTotal = (numberOfShares * $currentPortfolioPrice);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
{#if $screenWidth >=640}
|
||||
<!--Start Trade Modal-->
|
||||
<input type="checkbox" id="sellTradeModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="sellTradeModal" class="modal modal-bottom sm:modal-middle">
|
||||
|
||||
|
||||
<label for="sellTradeModal" class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.08]"></label>
|
||||
|
||||
|
||||
<div class="modal-box w-full bg-[#000] border border-slate-600 h-[500px]" >
|
||||
|
||||
<!--Start Trade Modal-->
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
|
||||
{#if displayTab === 'changeOrder'}
|
||||
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-gray-900 mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} />
|
||||
</div>
|
||||
<span class="mb-1">
|
||||
{$displayCompanyName?.length > 30 ? $displayCompanyName?.slice(0, 30) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
<span class="mb-1 text-sm font-medium">
|
||||
Current Price: ${$currentPortfolioPrice}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
|
||||
<div class="flex flex-col sm:w-64">
|
||||
<label class="label font-medium">
|
||||
<span class="text-white label-text">Number of Shares</span>
|
||||
</label>
|
||||
|
||||
<input bind:value={numberOfShares} on:input={handleInputChange} type="text" placeholder="Number of Shares" class="text-slate-200 input {numberOfShares > holdingShares ? 'input-error' : ''} bg-gray-900 input-md w-54 sm:w-full max-w-xs" />
|
||||
</div>
|
||||
<label on:click={handleMaxOrder} class="mt-9 btn bg-[#000] hover:bg-[#fff] hover:text-black text-white cursor-pointer sm:px-5 border rounded-lg">
|
||||
Show Max
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="{numberOfShares <= holdingShares ? 'hidden' : ''} label-text-alt text-error mt-1">
|
||||
Not enough shares to sell the amount
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-8 w-full">
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 text-sm font-medium">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Holding Shares:
|
||||
</span>
|
||||
<span>
|
||||
{holdingShares}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 text-sm">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Estimated Total:
|
||||
</span>
|
||||
<span>
|
||||
{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="w-5/6 max-w-lg pt-10 m-auto pb-20">
|
||||
<button on:click={changeTab} class="btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Preview Sell Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if displayTab === 'sellOrder'}
|
||||
|
||||
<label on:click={changeTab} class="{$screenWidth > 640 ? 'hidden' : ''} rounded-full left-0 -top-4 relative flex justify-center items-center w-10 h-10 hover:bg-gray-600 hover:bg-opacity-[0.4]">
|
||||
<svg class="w-7 h-7" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(-90 12 12)"><path fill="white" d="M13 7.828V20h-2V7.828l-5.364 5.364l-1.414-1.414L12 4l7.778 7.778l-1.414 1.414L13 7.828Z"/></g></svg>
|
||||
</label>
|
||||
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-gray-900 mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} />
|
||||
</div>
|
||||
<span class="mb-1 font-medium text-xl">
|
||||
Selling {numberOfShares} shares of {$displayCompanyName?.length > 30 ? $displayCompanyName?.slice(0, 30) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Price per Share</td>
|
||||
<td class="bg-[#000] whitespace-normal">${$currentPortfolioPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Number of Shares</td>
|
||||
<td class="bg-[#000] whitespace-normal">{numberOfShares}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Commission</td>
|
||||
<td class="bg-[#000] whitespace-normal">${commissionPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Estimated Total</td>
|
||||
<td class="bg-[#000] whitespace-normal">{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Available Cash After</td>
|
||||
<td class="bg-[#000] whitespace-normal">{(availableCash+estimatedTotal-commissionPrice)?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="max-w-lg pt-10 m-auto pb-5 flex flex-row justify-center items-center">
|
||||
<button on:click={changeTab} class="w-3/4 mr-8 btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md rounded-lg text-white font-bold text-md">
|
||||
Change Order
|
||||
</button>
|
||||
|
||||
<button on:click={handleSellOrder} class="w-3/4 btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md rounded-lg text-white font-bold text-md">
|
||||
Sell
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--End Trade Modal-->
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
{:else}
|
||||
<!--Start Drawer Sidewise for mobile-->
|
||||
|
||||
|
||||
<div class="drawer drawer-end z-40">
|
||||
<input id="sellTradeModal" type="checkbox" class="drawer-toggle"/>
|
||||
|
||||
<div class="drawer-side">
|
||||
|
||||
<div class="modal-box overflow-hidden rounded-xl bg-[#000] min-h-screen w-screen">
|
||||
|
||||
<div class="flex flex-col w-full mt-14">
|
||||
|
||||
|
||||
{#if displayTab === 'changeOrder'}
|
||||
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-gray-900 mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} />
|
||||
</div>
|
||||
<span class="mb-1">
|
||||
{$displayCompanyName?.length > 30 ? $displayCompanyName?.slice(0, 30) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
<span class="mb-1 text-sm font-medium">
|
||||
Current Price: ${$currentPortfolioPrice}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
|
||||
<div class="flex flex-col sm:w-64">
|
||||
<label class="label font-medium">
|
||||
<span class="text-white label-text">Number of Shares</span>
|
||||
</label>
|
||||
|
||||
<input inputmode="tel" pattern="[0-9]*" bind:value={numberOfShares} on:input={handleInputChange} type="text" placeholder="Number of Shares" class="text-slate-200 input {numberOfShares > holdingShares ? 'input-error' : ''} bg-gray-900 input-md w-54 sm:w-full max-w-xs" />
|
||||
</div>
|
||||
<label on:click={handleMaxOrder} class="mt-9 btn bg-[#000] hover:bg-[#fff] hover:text-black text-white cursor-pointer sm:px-5 border rounded-lg">
|
||||
Show Max
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="{numberOfShares <= holdingShares ? 'hidden' : ''} label-text-alt text-error mt-1">
|
||||
Not enough shares to sell the amount
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-8 w-full">
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 text-sm font-medium">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Holding Shares:
|
||||
</span>
|
||||
<span>
|
||||
{holdingShares}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row text-blue-400 text-sm">
|
||||
<span class="mr-0 sm:mr-1">
|
||||
Estimated Total:
|
||||
</span>
|
||||
<span>
|
||||
{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="grid grid-cols-3 gap-x-6 gap-y-2 text-white font-bold text-lg m-auto mt-14">
|
||||
{#each Array.from({ length: 10 }, (_, index) => index+1) as num}
|
||||
{#if num < 10}
|
||||
<label on:click={() => handleAppendNumber(num)} class="rounded-full flex justify-center items-center w-16 h-16">
|
||||
{num}
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="rounded-full flex justify-center items-center w-16 h-16">
|
||||
</div>
|
||||
<div class="rounded-full flex justify-center items-center w-16 h-16 hover:bg-gray-600 hover:bg-opacity-[0.4]">
|
||||
0
|
||||
</div>
|
||||
<label on:click={() => handleRemoveNumber()} class="rounded-full flex justify-center items-center w-16 h-16 hover:bg-gray-600 hover:bg-opacity-[0.4]">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(-90 12 12)"><path fill="white" d="M13 7.828V20h-2V7.828l-5.364 5.364l-1.414-1.414L12 4l7.778 7.778l-1.414 1.414L13 7.828Z"/></g></svg>
|
||||
</label>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<div class="w-5/6 max-w-lg pt-16 m-auto pb-20">
|
||||
<button on:click={changeTab} class="btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Preview Sell Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if displayTab === 'sellOrder'}
|
||||
|
||||
<div class="text-white text-md flex flex-col flex-shrink-0">
|
||||
<div class="rounded-full w-10 h-10 relative bg-gray-900 mb-2">
|
||||
<img class="rounded-full w-6 h-6 absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2" src={`https://financialmodelingprep.com/image-stock/${$assetType === 'stock' ? $stockTicker : $etfTicker}.png`} />
|
||||
</div>
|
||||
<span class="mb-1 font-medium text-xl">
|
||||
Selling {numberOfShares} shares of {$displayCompanyName?.length > 30 ? $displayCompanyName?.slice(0, 30) + "..." : $displayCompanyName}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Price per Share</td>
|
||||
<td class="bg-[#000] whitespace-normal">${$currentPortfolioPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Number of Shares</td>
|
||||
<td class="bg-[#000] whitespace-normal">{numberOfShares}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Commission</td>
|
||||
<td class="bg-[#000] whitespace-normal">${commissionPrice}</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Estimated Total</td>
|
||||
<td class="bg-[#000] whitespace-normal">{estimatedTotal?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-white" style="font-size: 0.8rem">
|
||||
<td class="text-start bg-[#000] text-white font-medium">Available Cash After</td>
|
||||
<td class="bg-[#000] whitespace-normal">{(availableCash+estimatedTotal-commissionPrice)?.toLocaleString(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="w-5/6 max-w-lg pt-10 m-auto pb-5 flex flex-col items-center">
|
||||
<button on:click={changeTab} class="btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Change Order
|
||||
</button>
|
||||
<button on:click={handleSellOrder} class="mt-6 btn bg-[#000] hover:bg-[#fff] hover:text-black border border-slate-500 btn-md w-full rounded-lg m-auto text-white font-bold text-md">
|
||||
Sell Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
<label for="sellTradeModal" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
188
src/lib/components/SentimentAnalysis.svelte
Normal file
188
src/lib/components/SentimentAnalysis.svelte
Normal file
@ -0,0 +1,188 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { displayCompanyName, stockTicker, etfTicker, cryptoTicker, assetType} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
|
||||
export let sentimentList = [];
|
||||
export let data;
|
||||
|
||||
let oneMonthResult;
|
||||
let outlook;
|
||||
|
||||
let oneYearResult;
|
||||
|
||||
let dashedLinePosition = {
|
||||
'0': '-mt-[80px]',
|
||||
'1': '-mt-[95px]',
|
||||
'2': '-mt-[105px]',
|
||||
'3': '-mt-[120px]',
|
||||
'4': '-mt-[125px]',
|
||||
'5': '-mt-[140px]',
|
||||
'6': '-mt-[152px]',
|
||||
'7': '-mt-[165px]',
|
||||
'8': '-mt-[178px]',
|
||||
'9': '-mt-[190px]',
|
||||
'10': '-mt-[200px]',
|
||||
}
|
||||
|
||||
$: {
|
||||
if($assetType === 'stock' ? $stockTicker : $assetType === 'etf' ? $etfTicker : $cryptoTicker && typeof window !== 'undefined' && sentimentList?.length !== 0) {
|
||||
oneMonthResult = sentimentList?.at(1)?.value;
|
||||
outlook = oneMonthResult > 5 ? 'Positive' : oneMonthResult < 5 ? 'Negative' : 'Neutral';
|
||||
oneYearResult = sentimentList?.at(-1)?.value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-10 sm:pb-0">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="sentimentAnalysisInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
AI Sentiment Analysis
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"AI Sentiment Analysis"}
|
||||
content={`We trained our AI model on over 1 million news stories and social media postings to analyze the news for various time periods and measure public opinion on ${$displayCompanyName}`}
|
||||
id={"sentimentAnalysisInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if sentimentList?.length !== 0}
|
||||
|
||||
|
||||
<div class="pb-4 w-full mt-5">
|
||||
<div class="w-auto p-4 sm:p-6 bg-[#202020] sm:bg-[#0F0F0F] rounded-lg relative">
|
||||
<h3 class="text-gray-300 font-medium text-sm uppercase mb-3">
|
||||
Average Score
|
||||
</h3>
|
||||
<div class="flex flex-row items-center justify-between ">
|
||||
<!--Start Big Circle-->
|
||||
<div class="relative size-24 sm:size-28">
|
||||
<svg class="size-full w-24 h-24 sm:w-28 sm:h-28" 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-[#303030]" stroke-width="3.5"></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 {oneYearResult > 5 ? 'text-[#10DB06]' : oneYearResult < 5 ? 'text-[#FF2F1F]' : 'text-white'} text-opacity-[0.7]" stroke-width="3.5" stroke-dasharray="100" stroke-dashoffset={100-oneYearResult*10}></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<!--Start Inside Circle-->
|
||||
<div class="absolute top-1/2 start-1/2 transform -translate-y-1/2 -translate-x-1/2">
|
||||
|
||||
<div class="relative size-[60px] sm:size-[70px] ml-auto">
|
||||
<svg class="size-full w-[60px] h-[60px] sm:w-[70px] sm:h-[70px]" 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-[#303030]" stroke-width="4"></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 {oneMonthResult > 5 ? 'text-[#10DB06]' : oneMonthResult < 5 ? 'text-[#FF2F1F]' : 'text-white'} " stroke-width="4" stroke-dasharray="100" stroke-dashoffset={100-oneMonthResult*10}></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-xl font-semibold">
|
||||
{oneMonthResult}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Inside Circle-->
|
||||
</div>
|
||||
<!--End Big Circle-->
|
||||
|
||||
<div class="flex flex-col items-start ml-4 sm:ml-10 mr-auto sm:-top-3 sm:relative">
|
||||
<h3 class="hidden sm:block text-gray-300 text-[1rem] sm:text-lg font-semibold">
|
||||
<span class="{outlook === 'Positive' ? 'text-[#10BC09]' : outlook==='Negative' ? 'text-red-500' : 'text-white'}">{outlook}</span> outlook:
|
||||
</h3>
|
||||
{#if oneMonthResult !== 0 && oneYearResult !== 0}
|
||||
<span class="text-gray-200 text-sm mt-1">
|
||||
In the past month, major news and social media rated the company {oneMonthResult > 5 ? 'positively' : oneMonthResult < 5 ? 'negatively' : 'neutral'} at {oneMonthResult} and
|
||||
with a yearly average of {oneYearResult}.
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-gray-200 text-sm mt-1">
|
||||
Not much news coverage and discussion for {$displayCompanyName}.
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="w-auto p-4 sm:p-6 bg-[#202020] sm:bg-[#0F0F0F] rounded-lg relative">
|
||||
<h3 class="text-gray-300 font-medium text-sm uppercase mb-3">
|
||||
Average Score Trend
|
||||
</h3>
|
||||
<div class="grid grid-cols-8 sm:grid-cols-9 -ml-2 sm:-ml-5 mt-7">
|
||||
{#each sentimentList as item}
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-white font-medium text-[1rem] mb-4">
|
||||
{#if data?.user?.tier === 'Pro'}
|
||||
{item?.value}
|
||||
{:else if ['1M','1Y']?.includes(item?.label)}
|
||||
{item?.value}
|
||||
{:else}
|
||||
<svg class="w-3.5 h-3.5 inline-block" 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>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="w-2 {item?.value === 5 ? 'bg-white' : item?.value < 5 ? 'bg-[#FF2F1F]' : 'bg-[#10DB06]' } rounded-full" style="height: 120px;">
|
||||
{#if data?.user?.tier === 'Pro'}
|
||||
<div class="bg-[#2F2F2F] w-2 rounded-t-full" style="height: {(100-item?.value*10)}%;"></div>
|
||||
{:else if ['1M','1Y']?.includes(item?.label)}
|
||||
<div class="bg-[#2F2F2F] w-2 rounded-t-full" style="height: {(100-item?.value*10)}%;"></div>
|
||||
{:else}
|
||||
<div class="bg-[#2F2F2F] w-2 rounded-t-full" style="height: {(100)}%;"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-gray-400 font-medium text-sm mt-4">
|
||||
{item?.label}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
<!-- Adjusted line -->
|
||||
<div class="sm:-ml-5 border-b absolute border-dashed border-gray-400 w-11/12 sm:w-5/6 left-1/2 transform -translate-x-1/2 {dashedLinePosition[oneMonthResult]}">
|
||||
<div class="w-full flex justify-end mb-2">
|
||||
<label class="text-sm font-semibold px-2 py-1.5 border rounded-full text-black {oneMonthResult === 5 ? 'bg-white border-white' : oneMonthResult < 5 ? 'bg-[#FF2F1F] border-[#FF2F1F]' : 'bg-[#10DB06] border-[#10DB06]'}">
|
||||
Current: {oneMonthResult}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Circular Progress -->
|
||||
|
||||
|
||||
|
||||
<!-- End Circular Progress -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--End AI News Score-->
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
41
src/lib/components/Share.svelte
Normal file
41
src/lib/components/Share.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
<script lang='ts'>
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
|
||||
export let url;
|
||||
|
||||
|
||||
|
||||
function shareContent() {
|
||||
|
||||
|
||||
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: document.title,
|
||||
url,
|
||||
})
|
||||
.then(() => console.log('Content shared successfully.'))
|
||||
.catch((error) => console.log('Error sharing content:', error));
|
||||
} else {
|
||||
toast.error('Sharing is not supported by your device', {
|
||||
style: 'background: #333; color: #fff;'
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<label on:click={shareContent} class="lg:hidden cursor-pointer ml-auto mr-10">
|
||||
<svg class="inline-block w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#d6d6dc" d="M18 22q-1.25 0-2.125-.875T15 19q0-.175.025-.363t.075-.337l-7.05-4.1q-.425.375-.95.588T6 15q-1.25 0-2.125-.875T3 12q0-1.25.875-2.125T6 9q.575 0 1.1.213t.95.587l7.05-4.1q-.05-.15-.075-.337T15 5q0-1.25.875-2.125T18 2q1.25 0 2.125.875T21 5q0 1.25-.875 2.125T18 8q-.575 0-1.1-.212t-.95-.588L8.9 11.3q.05.15.075.338T9 12q0 .175-.025.363T8.9 12.7l7.05 4.1q.425-.375.95-.587T18 16q1.25 0 2.125.875T21 19q0 1.25-.875 2.125T18 22Z"/></svg>
|
||||
<!--
|
||||
<span class="font-medium text-[#D6D6DC] text-sm">
|
||||
Share
|
||||
</span>
|
||||
-->
|
||||
</label>
|
||||
256
src/lib/components/ShareHolders.svelte
Normal file
256
src/lib/components/ShareHolders.svelte
Normal file
@ -0,0 +1,256 @@
|
||||
<script lang='ts'>
|
||||
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import {stockTicker, screenWidth} from '$lib/store';
|
||||
import { formatString } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import { abbreviateNumber } from '$lib/utils';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
export let shareholderList;
|
||||
|
||||
|
||||
let optionsPieChart;
|
||||
let institutionalOwner = 0;
|
||||
let otherOwner = 0;
|
||||
let topHolders = 0;
|
||||
let isLoaded:boolean
|
||||
let showFullStats = false;
|
||||
|
||||
|
||||
|
||||
|
||||
const plotPieChart = () => {
|
||||
|
||||
const options = {
|
||||
grid: {
|
||||
left: '0%',
|
||||
right: '0%',
|
||||
top: '0%',
|
||||
bottom: '10%',
|
||||
containLabel: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Shareholders',
|
||||
type: 'pie',
|
||||
radius: ['40%', '50%'],
|
||||
padAngle: 5,
|
||||
itemStyle: {
|
||||
borderRadius: 3
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
silent: true, // Disable interactivity
|
||||
data: [
|
||||
{ value: institutionalOwner, name: 'Institutions', itemStyle: { color: '#5470C6' } }, // Set color for 'Institutions'
|
||||
{ value: otherOwner, name: 'Others', itemStyle: {color: '#F8901E'} },
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
|
||||
if ($stockTicker && typeof window !== 'undefined' && typeof shareholderList !== 'undefined' && shareholderList?.length !== 0)
|
||||
{
|
||||
isLoaded = false;
|
||||
showFullStats = false;
|
||||
shareholderList = shareholderList?.filter(item => item?.ownership <= 100);
|
||||
topHolders = 0;
|
||||
otherOwner = 0;
|
||||
institutionalOwner = 0;
|
||||
institutionalOwner = shareholderList?.reduce((total, shareholder) => total + shareholder.ownership, 0);
|
||||
|
||||
otherOwner = institutionalOwner === 0 ? 0 : (100-institutionalOwner);
|
||||
topHolders = shareholderList?.slice(0,10)?.reduce((total, shareholder) => total + shareholder.ownership, 0);
|
||||
optionsPieChart = plotPieChart()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let charNumber = 20;
|
||||
|
||||
$: {
|
||||
if($screenWidth < 640)
|
||||
{
|
||||
charNumber = 20;
|
||||
}
|
||||
else {
|
||||
charNumber =20;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="bg-[#0F0F0F] overflow-hidden text-white h-full sm:mb-0">
|
||||
<div class="flex justify-center w-fit m-auto h-full overflow-hidden">
|
||||
<div class="relative flex justify-center items-center overflow-hidden">
|
||||
<main>
|
||||
<div class="w-fit sm:w-full sm:max-w-2xl m-auto mt-5 sm:mt-0">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="shareholdersInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Shareholder Breakdown
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Shareholder Breakdown"}
|
||||
content={"Institutional shareholders are large investment entities like mutual funds and pension funds that invest significant amounts in publicly traded companies. They play a big role in influencing company decisions and stock market trends."}
|
||||
id={"shareholdersInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if shareholderList?.length !== 0}
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
<div class="text-white text-md mt-3">
|
||||
Who owns the most stocks of the company?
|
||||
We can break it down into two categories.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-row items-center sm:-mt-5">
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-56">
|
||||
<Chart options={optionsPieChart} class="chart w-full" />
|
||||
</div>
|
||||
</Lazy>
|
||||
<div class="flex flex-col items-center sm:pt-0 m-auto">
|
||||
|
||||
<div class="flex flex-row items-center mr-auto mb-5">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-4 h-4 bg-[#F8901E] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-sm sm:text-[1rem] font-medium inline-block">
|
||||
Others: {otherOwner >= 99.99 ? 99.99 : otherOwner?.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center mr-auto">
|
||||
<div class="h-full bg-gray-800 transform -translate-x-1/2 " aria-hidden="true"></div>
|
||||
<div class="w-4 h-4 bg-[#5470C6] border-4 box-content border-gray-900 rounded-full transform -translate-x-1/2" aria-hidden="true"></div>
|
||||
<span class="text-white text-sm sm:text-[1rem] font-medium inline-block">
|
||||
Institutions: {institutionalOwner <= 0.01 ? "< 0.01%" : institutionalOwner?.toFixed(2)+'%'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-white font-semibold text-xl sm:text-2xl mb-3 mt-5 sm:-mt-5">
|
||||
Top Shareholders
|
||||
</h1>
|
||||
|
||||
{#if topHolders !== 0}
|
||||
<span class="text-white text-md">
|
||||
The top 10 shareholders own {topHolders <= 0.01 ? "< 0.01%" : topHolders?.toFixed(2)+'%'}
|
||||
of the company.
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="flex justify-start items-center w-full m-auto mt-6 ">
|
||||
<table class="table table-sm table-compact w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-blue-400">
|
||||
<th class="text-white shadow-md font-semibold text-sm text-start bg-[#0F0F0F]">Institute</th>
|
||||
<th class="text-white shadow-md font-semibold text-sm text-start bg-[#0F0F0F]">Ownership</th>
|
||||
<th class="text-white shadow-md font-semibold text-sm text-end hidden sm:table-cell bg-[#0F0F0F]">Shares</th>
|
||||
<th class="text-white shadow-md font-semibold text-sm text-end hidden sm:table-cell bg-[#0F0F0F]">Market Value</th>
|
||||
<th class="text-white shadow-md font-semibold text-sm text-end bg-[#0F0F0F]">Portfolio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each (showFullStats ? shareholderList?.slice(0,10) : shareholderList?.slice(0,3)) as item,index}
|
||||
{#if item?.investorName?.length > 0}
|
||||
<tr on:click={() => goto('/hedge-funds/'+item?.cik)} class="{index === 2 && !showFullStats && shareholderList?.length > 3 ? 'opacity-[0.5]' : '' } sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#0F0F0F] border-b-[#0F0F0F] cursor-pointer">
|
||||
|
||||
<td class="text-white font-medium border-b border-[#0F0F0F]">
|
||||
{item?.investorName?.length > charNumber ? formatString(item?.investorName?.slice(0,charNumber)) + "..." : formatString(item?.investorName)}
|
||||
</td>
|
||||
|
||||
<td class="text-white text-center font-medium border-b border-[#0F0F0F]">
|
||||
{item?.ownership <= 0.01 ? "< 0.01%" : item?.ownership?.toFixed(2)+'%'}
|
||||
</td>
|
||||
|
||||
<td class="text-white text-end hidden sm:table-cell font-medium border-b border-[#0F0F0F]">
|
||||
{item?.sharesNumber !== null ? abbreviateNumber(item?.sharesNumber) : '-'}
|
||||
</td>
|
||||
|
||||
<td class="text-white text-end hidden sm:table-cell font-medium border-b border-[#0F0F0F] ">
|
||||
{item?.marketValue !== null ? "$" + abbreviateNumber(item?.marketValue) : '-'}
|
||||
</td>
|
||||
|
||||
<td class="text-white text-end font-medium border-b border-[#0F0F0F]">
|
||||
{item?.weight <= 0.01 ? "< 0.01%" : item?.weight?.toFixed(2)+'%'}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<label on:click={() => showFullStats = !showFullStats} class="cursor-pointer flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullStats ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<h2 class=" mt-10 justify-center items-center text-3xl font-bold text-slate-700 mb-5 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
33
src/lib/components/Sidecard.svelte
Normal file
33
src/lib/components/Sidecard.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
|
||||
import TickerInfoCard from '$lib/components/TickerInfoCard.svelte';
|
||||
import AnalystCard from '$lib/components/AnalystCard.svelte';
|
||||
import ESGCard from '$lib/components/ESGCard.svelte';
|
||||
import SimilarTickerCard from '$lib/components/SimilarTickerCard.svelte';
|
||||
import TopETFTickerHolder from '$lib/components/TopETFTickerHolder.svelte';
|
||||
|
||||
|
||||
export let logoUrl;
|
||||
export let stockDeck;
|
||||
export let similarstock;
|
||||
export let topETFHolder;
|
||||
export let analystRating
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<TickerInfoCard logoUrl={logoUrl} stockDeck={stockDeck} />
|
||||
|
||||
|
||||
<AnalystCard analystRating={analystRating} />
|
||||
|
||||
|
||||
<ESGCard stockDeck={stockDeck} />
|
||||
|
||||
<SimilarTickerCard similarstock={similarstock}/>
|
||||
|
||||
<TopETFTickerHolder topETFHolder={topETFHolder}/>
|
||||
|
||||
68
src/lib/components/SignalBar.svelte
Normal file
68
src/lib/components/SignalBar.svelte
Normal file
@ -0,0 +1,68 @@
|
||||
<script lang = 'ts'>
|
||||
export let signal;
|
||||
|
||||
</script>
|
||||
|
||||
{#if signal === 'Strong Buy'}
|
||||
|
||||
<div class="relative h-full w-full mt-5">
|
||||
<div class="absolute bottom-0 left-0 flex flex-row items-end">
|
||||
<div class="bg-[#10DB06] mr-0.5 h-2.5 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-3 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-3.5 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-4 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-[18px] w-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if signal === 'Buy'}
|
||||
<div class="relative h-full w-full mt-5">
|
||||
<div class="absolute bottom-0 left-0 flex flex-row items-end">
|
||||
<div class="bg-[#10DB06] mr-0.5 h-2.5 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-3 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-3.5 w-1.5"></div>
|
||||
<div class="bg-[#10DB06] mr-0.5 h-4 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-[18px] w-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if signal === 'Neutral'}
|
||||
<div class="relative h-full w-full mt-5">
|
||||
<div class="absolute bottom-0 left-0 flex flex-row items-end">
|
||||
<div class="bg-[#F8901E] mr-0.5 h-2.5 w-1.5"></div>
|
||||
<div class="bg-[#F8901E] mr-0.5 h-3 w-1.5"></div>
|
||||
<div class="bg-[#F8901E] mr-0.5 h-3.5 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-4 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-[18px] w-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if signal === 'Sell'}
|
||||
<div class="relative h-full w-full mt-5">
|
||||
<div class="absolute bottom-0 left-0 flex flex-row items-end">
|
||||
<div class="bg-[#FF2F1F] mr-0.5 h-2.5 w-1.5"></div>
|
||||
<div class="bg-[#FF2F1F] mr-0.5 h-3 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-3.5 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-4 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-[18px] w-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if signal === 'Strong Sell'}
|
||||
<div class="relative h-full w-full mt-5">
|
||||
<div class="absolute bottom-0 left-0 flex flex-row items-end">
|
||||
<div class="bg-[#FF2F1F] mr-0.5 h-2.5 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-3 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-3.5 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-4 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-[18px] w-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative h-full w-full mt-5">
|
||||
<div class="absolute bottom-0 left-0 flex flex-row items-end">
|
||||
<div class="bg-[#707070] mr-0.5 h-2.5 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-3 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-3.5 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-4 w-1.5"></div>
|
||||
<div class="bg-[#707070] mr-0.5 h-[18px] w-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
187
src/lib/components/SimilarETFCard.svelte
Normal file
187
src/lib/components/SimilarETFCard.svelte
Normal file
@ -0,0 +1,187 @@
|
||||
<script lang="ts">
|
||||
|
||||
import {etfTicker, similarTickerClicked, screenWidth} from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { abbreviateNumber } from '$lib/utils';
|
||||
|
||||
|
||||
export let similarTicker;
|
||||
|
||||
|
||||
|
||||
async function etfSelector(state)
|
||||
{
|
||||
window?.scroll({ top: 0, left: 0, behavior: 'smooth' });
|
||||
etfTicker.update(value => state);
|
||||
$similarTickerClicked = true;
|
||||
goto("/etf/"+state+"/")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<!--Start Similar Stocks Card -->
|
||||
|
||||
<div class="space-y-3 sm:pt-5 hidden lg:block sm:{similarTicker?.length !== 0 ? '' : 'hidden'}">
|
||||
|
||||
<div class="sm:rounded-2xl shadow-lg bg-[#000] sm:bg-[#202020] sm:border sm:border-slate-800 h-auto {$screenWidth < 640 ? 'w-screen pt-16' : ''} md:w-96">
|
||||
|
||||
<div class="w-auto lg:w-full p-1 flex-1 flex flex-wrap pb-5">
|
||||
<h2 class="text-start ml-2 text-2xl font-bold text-white pb-2 p-3">
|
||||
Similar ETFs
|
||||
</h2>
|
||||
{#if similarTicker?.length !== 0}
|
||||
|
||||
|
||||
<div class="flex justify-start items-center w-full m-auto pl-2">
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<thead>
|
||||
<tr class="border-b border-blue-400">
|
||||
<th class="text-white font-semibold text-sm text-start bg-[#000] sm:bg-[#202020] border-b border-blue-400">Fund Name</th>
|
||||
<th class="text-white font-semibold text-sm text-start bg-[#000] sm:bg-[#202020] border-b border-blue-400">Total Assets</th>
|
||||
<th class="text-white font-semibold text-sm text-end bg-[#000] sm:bg-[#202020] border-b border-blue-400">Holdings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each similarTicker as item, index}
|
||||
<tr on:click={() => etfSelector(item?.symbol)} class="shake-ticker text-white cursor-pointer sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#000] sm:bg-[#202020] border-b border-[#000] sm:border-[#202020]">
|
||||
{#if index <=6}
|
||||
|
||||
<td class="text-gray-200">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="rounded-full w-10 h-10 relative flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6 rounded-full" src={`https://financialmodelingprep.com/image-stock/${item.symbol}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<div class="flex flex-col ml-3 w-full">
|
||||
<span class="text-blue-400 text-sm font-medium">{item?.symbol}</span>
|
||||
<span class="text-white text-xs">
|
||||
{#if typeof item?.name !== 'undefined'}
|
||||
{item?.name?.length > 10 ? item?.name?.slice(0,10) + "..." : item?.name}
|
||||
{:else}
|
||||
n/a
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
<td class="text-white text-center font-medium ">
|
||||
{item?.totalAssets !== null ? abbreviateNumber(item?.totalAssets, true) : '-'}
|
||||
</td>
|
||||
|
||||
<td class="text-white font-medium text-end">
|
||||
{abbreviateNumber(item?.numberOfHoldings)}
|
||||
</td>
|
||||
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<h2 class=" mt-20 justify-center items-center text-3xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Stocks Card -->
|
||||
|
||||
|
||||
|
||||
<!--Start Mobile Similar Ticker Card-->
|
||||
<div class="lg:hidden space-y-3 sm:pt-5">
|
||||
|
||||
<div class="bg-[#000] h-auto w-screen">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#202020] w-full p-1 flex flex-col items-center pb-5 h-auto rounded-b-[30px]">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
Similar Ticker
|
||||
</h2>
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
<span class="text-white text-center text-md text-opacity-[0.8] pl-8 pr-8">
|
||||
Identify trends in similar assets and explore superior competing options in your portfolio.
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row justify-center items-center w-full mt-5">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
{#if similarTicker?.length !== 0}
|
||||
<div class="flex justify-start items-center w-full m-auto pl-2 mt-10">
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<thead>
|
||||
<tr class="border-b border-blue-400">
|
||||
<th class="text-white font-bold text-sm text-start bg-[#000] border-b border-blue-400">Company</th>
|
||||
<th class="text-white font-bold text-sm text-center bg-[#000] border-b border-blue-400">Market Cap</th>
|
||||
<th class="text-white font-bold text-sm text-end bg-[#000] border-b border-blue-400">Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each similarTicker as item, index}
|
||||
<tr on:click={() => etfSelector(item?.symbol)} class="shake-ticker text-white cursor-pointer border-b border-[#000]">
|
||||
{#if index <=6}
|
||||
|
||||
<td class="text-gray-200">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="rounded-full w-10 h-10 relative flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6 rounded-full" src={`https://financialmodelingprep.com/image-stock/${item.symbol}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<div class="flex flex-col ml-3 w-full">
|
||||
<span class="text-blue-400 text-sm font-medium">{item?.symbol}</span>
|
||||
<span class="text-white text-xs">
|
||||
{#if typeof item?.name !== 'undefined'}
|
||||
{item?.name?.length > 10 ? item?.name?.slice(0,10) + "..." : item?.name}
|
||||
{:else}
|
||||
n/a
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
<td class="text-white text-center font-medium ">
|
||||
{item?.totalAssets !== null ? abbreviateNumber(item?.totalAssets, true) : '-'}
|
||||
</td>
|
||||
|
||||
<td class="text-white font-medium text-end">
|
||||
{abbreviateNumber(item?.numberOfHoldings)}
|
||||
</td>
|
||||
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-20 flex justify-center items-center text-3xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Mobile Similar Ticker Card-->
|
||||
|
||||
|
||||
183
src/lib/components/SimilarTickerCard.svelte
Normal file
183
src/lib/components/SimilarTickerCard.svelte
Normal file
@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
|
||||
import {stockTicker, similarTickerClicked, screenWidth} from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { abbreviateNumber } from '$lib/utils';
|
||||
|
||||
|
||||
export let similarstock;
|
||||
|
||||
|
||||
|
||||
async function stockSelector(state)
|
||||
{
|
||||
window?.scroll({ top: 0, left: 0, behavior: 'smooth' });
|
||||
stockTicker.update(value => state);
|
||||
$similarTickerClicked = true;
|
||||
goto("/stocks/"+state+"/")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<!--Start Similar Stocks Card -->
|
||||
|
||||
<div class="space-y-3 sm:pt-5 hidden lg:block lg:{similarstock?.length !== 0 ? '' : 'hidden'}">
|
||||
|
||||
<div class="sm:rounded-2xl shadow-lg bg-[#000] sm:bg-[#202020] sm:border sm:border-slate-800 h-auto {$screenWidth < 640 ? 'w-screen pt-16' : ''} md:w-96">
|
||||
|
||||
<div class="w-auto lg:w-full p-1 flex flex-col m-auto pb-14 sm:pb-10 px-2 sm:px-0">
|
||||
<h2 class="text-start text-2xl font-semibold text-white p-3 mt-3 ml-1">
|
||||
Similar Stocks
|
||||
</h2>
|
||||
{#if similarstock?.length !== 0}
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-start items-center w-full m-auto ">
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<thead>
|
||||
<tr class="border-b border-blue-400">
|
||||
<th class="text-white font-semibold text-sm text-start bg-[#000] sm:bg-[#202020] border-b border-blue-400">Company</th>
|
||||
<th class="text-white font-semibold text-sm text-center bg-[#000] sm:bg-[#202020] border-b border-blue-400">Market Cap</th>
|
||||
<th class="text-white font-semibold text-sm text-end bg-[#000] sm:bg-[#202020] border-b border-blue-400">Avg Volume</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each similarstock as item, index}
|
||||
<tr on:click={() => stockSelector(item?.symbol)} class="shake-ticker text-white cursor-pointer sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#000] sm:bg-[#202020] border-b border-[#000] sm:border-[#202020]">
|
||||
{#if index <=6}
|
||||
|
||||
<td class="text-gray-200">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="rounded-full w-10 h-10 relative flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6 rounded-full" src={`https://financialmodelingprep.com/image-stock/${item.symbol}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<div class="flex flex-col ml-3 w-full">
|
||||
<span class="text-blue-400 text-sm font-medium">{item?.symbol}</span>
|
||||
<span class="text-white text-xs">
|
||||
{#if typeof item?.name !== 'undefined'}
|
||||
{item?.name?.length > 10 ? item?.name?.slice(0,10) + "..." : item?.name}
|
||||
{:else}
|
||||
n/a
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
<td class="text-white text-center font-medium ">
|
||||
{item?.marketCap !== null ? abbreviateNumber(item?.marketCap,true) : '-'}
|
||||
</td>
|
||||
|
||||
<td class="text-white font-medium text-end">
|
||||
{item?.avgVolume !== null ? abbreviateNumber(item?.avgVolume) : '-'}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class=" mt-20 flex justify-center items-center text-2xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Stocks Card -->
|
||||
|
||||
|
||||
|
||||
<!--Start Mobile Similar Ticker Card-->
|
||||
<div class="lg:hidden space-y-3 sm:pt-5">
|
||||
|
||||
<div class="bg-[#000] h-auto w-screen">
|
||||
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#202020] w-full p-1 flex flex-col items-center pb-5 h-auto rounded-b-[30px]">
|
||||
<h2 class="text-center m-auto text-[1.1rem] font-medium text-white mt-5">
|
||||
Similar Ticker
|
||||
</h2>
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
<span class="text-white text-center text-md text-opacity-[0.8] pl-8 pr-8">
|
||||
Identify trends in similar assets and explore superior competing options in your portfolio.
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row justify-center items-center w-full mt-5">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
{#if similarstock?.length !== 0}
|
||||
<div class="flex justify-start items-center w-full m-auto mt-10 overflow-hidden">
|
||||
<table class="table table-sm table-compact mt-3 text-start flex justify-start items-center w-full px-3 m-auto">
|
||||
<thead>
|
||||
<tr class="border-b border-blue-400">
|
||||
<th class="text-white font-bold text-sm text-start bg-[#000] border-b border-blue-400">Company</th>
|
||||
<th class="text-white font-bold text-sm text-center bg-[#000] border-b border-blue-400">Market Cap</th>
|
||||
<th class="text-white font-bold text-sm text-end bg-[#000] border-b border-blue-400">Avg Volume</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each similarstock as item, index}
|
||||
<tr on:click={() => stockSelector(item?.symbol)} class="text-white cursor-pointer bg-[#000] border-b border-[#000]">
|
||||
{#if index <=6}
|
||||
|
||||
<td class="text-gray-200">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="rounded-full w-10 h-10 relative flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6 rounded-full" src={`https://financialmodelingprep.com/image-stock/${item.symbol}.png`} loading="lazy"/>
|
||||
</div>
|
||||
<div class="flex flex-col ml-3 w-full">
|
||||
<span class="text-blue-400 text-sm font-medium">{item?.symbol}</span>
|
||||
<span class="text-white text-xs">
|
||||
{#if typeof item?.name !== 'undefined'}
|
||||
{item?.name?.length > 10 ? item?.name?.slice(0,10) + "..." : item?.name}
|
||||
{:else}
|
||||
n/a
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
<td class="text-white text-center font-medium ">
|
||||
{item?.marketCap !== null ? abbreviateNumber(item?.marketCap,true) : '-'}
|
||||
</td>
|
||||
|
||||
<td class="text-white font-medium text-end">
|
||||
{item?.avgVolume !== null ? abbreviateNumber(item?.avgVolume) : '-'}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class=" mt-20 flex justify-center items-center text-2xl font-bold text-slate-700 mb-20 m-auto">
|
||||
No data available
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Mobile Similar Ticker Card-->
|
||||
|
||||
485
src/lib/components/SimpleGraph.svelte
Normal file
485
src/lib/components/SimpleGraph.svelte
Normal file
@ -0,0 +1,485 @@
|
||||
<script lang='ts'>
|
||||
|
||||
export let data;
|
||||
export let displaySection = 'overview';
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Start Graph -->
|
||||
{#if displaySection === 'overview'}
|
||||
{#if output !== null}
|
||||
<div class = 'graph'>
|
||||
<Chart {...options} width={width} height={height} autoSize={true} ref={(ref) => chart = ref} on:crosshairMove={handleCrosshairMove} >
|
||||
{#if displayData === '3M'}
|
||||
<LineSeries data={filteredData.threeMonthsAgo}
|
||||
color={colorChange}
|
||||
lineWidth={1.5}
|
||||
priceScaleId="left"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesReference}
|
||||
priceLineVisible= {false}
|
||||
>
|
||||
<PriceLine
|
||||
price={filteredData.threeMonthsAgo[0].value}
|
||||
lineWidth = {1.5}
|
||||
color="#d4d9d9"
|
||||
/>
|
||||
<!--
|
||||
{#if rawClassifierModel !== null}
|
||||
<PriceLine
|
||||
title="Prediction price"
|
||||
price={rawClassifierModel[1][0].value}
|
||||
lineWidth = {1.5}
|
||||
color="#661AE6"
|
||||
priceScaleId="left"
|
||||
/>
|
||||
{/if}
|
||||
-->
|
||||
<!--Classifier Model Prediction-->
|
||||
<!--
|
||||
{#if rawClassifierModel !==null}
|
||||
<PriceLine
|
||||
price={(filteredData.threeMonthsAgo[filteredData.threeMonthsAgo.length-1].value * (1+priceChangeRangeValue/100)) }
|
||||
lineWidth = {1.5}
|
||||
color="#367E18"
|
||||
title="Price rise"/>
|
||||
{/if}
|
||||
-->
|
||||
</LineSeries>
|
||||
{:else if displayData === '6M'}
|
||||
<LineSeries data={filteredData.sixMonthsAgo}
|
||||
color={colorChange}
|
||||
lineWidth={1.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesReference}
|
||||
priceLineVisible= {false}
|
||||
>
|
||||
<PriceLine
|
||||
price={filteredData.sixMonthsAgo[0].value}
|
||||
lineWidth = {1.5}
|
||||
color="#d4d9d9"
|
||||
/>
|
||||
</LineSeries>
|
||||
{:else if displayData === '1y'}
|
||||
<LineSeries data={filteredData.oneYearAgo}
|
||||
color={colorChange}
|
||||
lineWidth={1.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesReference}
|
||||
priceLineVisible= {false}
|
||||
>
|
||||
<PriceLine
|
||||
price={filteredData.oneYearAgo[0].value}
|
||||
lineWidth = {1.5}
|
||||
color="#d4d9d9"
|
||||
/>
|
||||
</LineSeries>
|
||||
{:else if displayData === '3y'}
|
||||
<LineSeries data={filteredData.threeYearsAgo}
|
||||
color={colorChange}
|
||||
lineWidth={1.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesReference}
|
||||
priceLineVisible= {false}
|
||||
>
|
||||
<PriceLine
|
||||
price={filteredData.threeYearsAgo[0].value}
|
||||
lineWidth = {1.5}
|
||||
color="#d4d9d9"
|
||||
/>
|
||||
</LineSeries>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
<HistogramSeries
|
||||
data={volumeData}
|
||||
priceScaleId=""
|
||||
priceFormat={{type: 'volume'}}
|
||||
scaleMargins={{top: 0.8, bottom: 0}}/>
|
||||
-->
|
||||
|
||||
{#if displayBBANDS}
|
||||
<LineSeries data={bbup}
|
||||
color="#00F13F"
|
||||
lineWidth={1.5}
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
/>
|
||||
<LineSeries data={bbmiddle}
|
||||
color="#00F13F"
|
||||
lineWidth={1.5}
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
/>
|
||||
<LineSeries data={bbdown}
|
||||
color="#00F13F"
|
||||
lineWidth={1.5}
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if displaySMA}
|
||||
<LineSeries data={SMA}
|
||||
color="#039dfc"
|
||||
lineWidth={1.8}
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if displayEMA}
|
||||
<LineSeries data={EMA}
|
||||
color="#fc035a"
|
||||
lineWidth={1.8}
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTrainData && displayData === '3M'}
|
||||
<LineSeries data={linearRegressionTrainData.threeMonthsAgo}
|
||||
color="#9D00A0"
|
||||
lineWidth={4.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTrainData && displayData === '6M'}
|
||||
<LineSeries data={linearRegressionTrainData.sixMonthsAgo}
|
||||
color="#9D00A0"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTrainData && displayData === '1y'}
|
||||
<LineSeries data={linearRegressionTrainData.oneYearAgo}
|
||||
color="#9D00A0"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTrainData && displayData === '3y'}
|
||||
<LineSeries data={linearRegressionTrainData.threeYearsAgo}
|
||||
color="#9D00A0"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
<!--Test Data-->
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '3M'}
|
||||
<LineSeries data={linearRegressionTestData.threeMonthsAgo}
|
||||
color="#A09D00"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '6M'}
|
||||
<LineSeries data={linearRegressionTestData.sixMonthsAgo}
|
||||
color="#A09D00"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '1y'}
|
||||
<LineSeries data={linearRegressionTestData.oneYearAgo}
|
||||
color="#A09D00"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '3y'}
|
||||
<LineSeries data={linearRegressionTestData.threeYearsAgo}
|
||||
color="#A09D00"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!--Pred Data-->
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '3M'}
|
||||
<LineSeries data={linearRegressionPredData.threeMonthsAgo}
|
||||
color="#4287f5"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesPredReference}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '6M'}
|
||||
<LineSeries data={linearRegressionPredData.sixMonthsAgo}
|
||||
color="#4287f5"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesPredReference}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '1y'}
|
||||
<LineSeries data={linearRegressionPredData.oneYearAgo}
|
||||
color="#4287f5"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesPredReference}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
{#if rawRegressionModel !==null && displayTestData && displayData === '3y'}
|
||||
<LineSeries data={linearRegressionPredData.threeYearsAgo}
|
||||
color="#4287f5"
|
||||
lineWidth={3.5}
|
||||
priceScaleId="right"
|
||||
crosshairMarkerVisible={false}
|
||||
ref={handleSeriesPredReference}
|
||||
priceLineVisible= {false}
|
||||
lineStyle = {1}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- <TimeScale
|
||||
fixRightEdge={fixRight}
|
||||
fixLeftEdge={fixLeft}
|
||||
on:visibleLogicalRangeChange={handleChartLogicalRangeChange}
|
||||
ref={(ref) => timescale = ref } shiftVisibleRangeOnNewBar={false}/> -->
|
||||
<!--Site crashes when i add timescale: Need to fix the bug ASAP -->
|
||||
</Chart>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<!-- else output not loaded yet-->
|
||||
<div class="graphLoading" style="width: {width}px; height: 400px;">
|
||||
<div class="flex justify-center items-center m-auto mt-10 py-20">
|
||||
<div class="loader">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<!--End Graph-->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.graph {
|
||||
display:block;
|
||||
position: relative;
|
||||
max-width: 500px;
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
margin-top: auto;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border-width: thin;
|
||||
border-color: #1A1A27;
|
||||
background-color: #1A1A27;
|
||||
bottom:20px;
|
||||
|
||||
}
|
||||
|
||||
.graphLoading {
|
||||
display:block;
|
||||
position: relative;
|
||||
max-width: 500px;
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
margin-top: auto;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border-width: thin;
|
||||
border-color: #1A1A27;
|
||||
background-color: #1A1A27;
|
||||
bottom:20px;
|
||||
}
|
||||
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
border-radius: 50%;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation: load7 0.8s infinite ease-in-out;
|
||||
animation: load7 0.8s infinite ease-in-out;
|
||||
}
|
||||
.loader {
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
margin: 80px auto;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.loader:before {
|
||||
left: -3.5em;
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.loader:after {
|
||||
left: 3.5em;
|
||||
}
|
||||
@-webkit-keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
@keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 450px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 800px;
|
||||
max-height: 450px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
:root {
|
||||
--date-picker-background: #1A1A27;
|
||||
--date-picker-foreground: #f7f7f7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.info-card {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
animation: fade-in 0.5s ease-out, slide-up var(--animation-delay) ease-out;
|
||||
animation-fill-mode: both;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide the scrollbar when the info card is open */
|
||||
.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "pixel";
|
||||
src: url("$lib/fonts/pixel.ttf");
|
||||
}
|
||||
|
||||
.pixel {
|
||||
font-family: "pixel";
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
</style>
|
||||
73
src/lib/components/SiteProgress.svelte
Normal file
73
src/lib/components/SiteProgress.svelte
Normal file
@ -0,0 +1,73 @@
|
||||
<script>
|
||||
import { tweened } from 'svelte/motion';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { navigating } from '$app/stores';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
// progress bar value
|
||||
const p = tweened(0, {
|
||||
duration: 300,
|
||||
easing: cubicOut
|
||||
});
|
||||
|
||||
let isVisible = false;
|
||||
|
||||
function increase() {
|
||||
if ($p >= 0 && $p < 1) {
|
||||
p.update(value => value + 0.005); // Adjust the increment value as needed
|
||||
} else {
|
||||
p.set(0);
|
||||
}
|
||||
|
||||
if ($navigating) {
|
||||
const rand = Math.round(Math.random() * (300 - 50)) + 50;
|
||||
setTimeout(increase, rand);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($navigating) {
|
||||
increase();
|
||||
isVisible = true;
|
||||
}
|
||||
|
||||
if (!$navigating) {
|
||||
p.set(1);
|
||||
setTimeout(() => {
|
||||
isVisible = false;
|
||||
p.set(0);
|
||||
}, 500); // Adjust this timing as needed
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $p > 0 && $p < 1 && isVisible}
|
||||
<progress value={$p} transition:fade={{duration: 0}} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
progress {
|
||||
--bar-color: #0F0F0F;
|
||||
--val-color: rgb(34, 153, 221,0.8);
|
||||
position: fixed;
|
||||
top: 30;
|
||||
z-index: 99999;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
width: 100%; /* Ensure the width covers the entire container */
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Bar */
|
||||
progress::-webkit-progress-bar {background-color: var(--bar-color); width: 100%;}
|
||||
progress {background-color: var(--bar-color);}
|
||||
|
||||
/* Value */
|
||||
progress::-webkit-progress-value {background-color: var(--val-color) !important;}
|
||||
progress::-moz-progress-bar {background-color: var(--val-color) !important;}
|
||||
progress {color: var(--val-color);}
|
||||
</style>
|
||||
|
||||
35
src/lib/components/SkeletonLoading.svelte
Normal file
35
src/lib/components/SkeletonLoading.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
|
||||
<!-- Skeleton Post -->
|
||||
<div class=" mt-5 shadow-lg">
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col">
|
||||
<!-- Item -->
|
||||
<div class="border border-slate-800 rounded-md bg-[#202020]">
|
||||
<div class="flex h-auto justify-between items-center">
|
||||
|
||||
|
||||
|
||||
<div role="status" class="animate-pulse space-x-8 md:flex md:items-center mb-4">
|
||||
<div class="flex items-center justify-center w-48 h-28 sm:w-48 sm:h-48 bg-[#1F1F23] rounded ">
|
||||
<svg class="w-12 h-12 text-gray-200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" viewBox="0 0 640 512"><path d="M480 80C480 35.82 515.8 0 560 0C604.2 0 640 35.82 640 80C640 124.2 604.2 160 560 160C515.8 160 480 124.2 480 80zM0 456.1C0 445.6 2.964 435.3 8.551 426.4L225.3 81.01C231.9 70.42 243.5 64 256 64C268.5 64 280.1 70.42 286.8 81.01L412.7 281.7L460.9 202.7C464.1 196.1 472.2 192 480 192C487.8 192 495 196.1 499.1 202.7L631.1 419.1C636.9 428.6 640 439.7 640 450.9C640 484.6 612.6 512 578.9 512H55.91C25.03 512 .0006 486.1 .0006 456.1L0 456.1z"/></svg>
|
||||
</div>
|
||||
<div class="w-full md:w-96 ">
|
||||
<div class="h-2.5 bg-gray-100 rounded-full w-48 mb-4"></div>
|
||||
<div class="h-2 bg-gray-100 rounded-full w-80 mb-2.5"></div>
|
||||
<div class="h-2 bg-gray-100 rounded-full w-56 mb-2.5"></div>
|
||||
<div class="h-2 bg-gray-100 rounded-full w-80 mb-2.5"></div>
|
||||
<div class="h-2 bg-gray-100 rounded-full w-56 mb-2.5"></div>
|
||||
<div class="h-2 bg-gray-100 rounded-full w-48"></div>
|
||||
</div>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
220
src/lib/components/StockKeyInformation.svelte
Normal file
220
src/lib/components/StockKeyInformation.svelte
Normal file
@ -0,0 +1,220 @@
|
||||
<script lang="ts">
|
||||
|
||||
import TickerInfoCard from '$lib/components/TickerInfoCard.svelte';
|
||||
import ExecutiveCard from '$lib/components/ExecutiveCard.svelte';
|
||||
|
||||
import SimilarTickerCard from '$lib/components/SimilarTickerCard.svelte';
|
||||
import ESGCard from '$lib/components/ESGCard.svelte';
|
||||
import SECFilingsCard from '$lib/components/SECFilingsCard.svelte';
|
||||
import TopETFTickerHolder from '$lib/components/TopETFTickerHolder.svelte';
|
||||
|
||||
|
||||
import { similarTickerClicked, executiveClicked, secFilingsClicked } from '$lib/store';
|
||||
|
||||
//import copilotIcon from '$lib/images/copilot_icon.png';
|
||||
|
||||
|
||||
export let stockDeck;
|
||||
export let data;
|
||||
export let similarstock;
|
||||
export let topETFHolder;
|
||||
|
||||
|
||||
$: {
|
||||
if($similarTickerClicked)
|
||||
{
|
||||
const closePopup = document.getElementById("similarTickerModal");
|
||||
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
$similarTickerClicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="mt-4 ">
|
||||
|
||||
<div class="grid grid-cols-3 gap-x-4 gap-y-2">
|
||||
<label for="tickerModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Details
|
||||
</label>
|
||||
|
||||
<label on:click={() => $executiveClicked = true} for="executiveModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Executives
|
||||
</label>
|
||||
|
||||
<label on:click={() => $secFilingsClicked = true} for="secFilingModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
SEC Filings
|
||||
</label>
|
||||
<label for="esgModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
ESG Score
|
||||
</label>
|
||||
<label for="similarTickerModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
Similar
|
||||
</label>
|
||||
<label for="topETFTickerHolderModal" class="w-auto border border-gray-300 flex px-4 py-2 mb-2 justify-center items-center text-xs font-medium rounded-xl text-gray-200">
|
||||
ETFs Holder
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Executive Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="secFilingModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="secFilingModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<SECFilingsCard secFilingsList={data?.getSECFilings}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Executive Modal-->
|
||||
|
||||
|
||||
|
||||
<!--Start Executive Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="executiveModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="executiveModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<ExecutiveCard />
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Executive Modal-->
|
||||
|
||||
|
||||
<!--Start Ticker Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="tickerModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="tickerModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
|
||||
<TickerInfoCard stockDeck={stockDeck}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Ticker Modal-->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start ESG Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="esgModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="esgModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
<ESGCard stockDeck={stockDeck}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End ESG Modal-->
|
||||
|
||||
|
||||
|
||||
<!--Start Similar Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="similarTickerModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="similarTickerModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
<SimilarTickerCard similarstock={similarstock}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Modal-->
|
||||
|
||||
|
||||
<!--Start Similar Modal-->
|
||||
<div class="drawer drawer-end z-40 overflow-hidden w-screen">
|
||||
<input id="topETFTickerHolderModal" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-side overflow-y-scroll overflow-hidden">
|
||||
|
||||
|
||||
<div class="bg-[#000] min-h-screen w-screen pb-20 overflow-y-scroll overflow-hidden">
|
||||
|
||||
<label for="topETFTickerHolderModal" class="absolute left-6 top-6">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
<div class="w-screen overflow-y-scroll" >
|
||||
<TopETFTickerHolder topETFHolder={topETFHolder}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Similar Modal-->
|
||||
88
src/lib/components/StockSlider.svelte
Normal file
88
src/lib/components/StockSlider.svelte
Normal file
@ -0,0 +1,88 @@
|
||||
<script lang='ts'>
|
||||
|
||||
export let sliderList;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="slider pt-1 sm:pt-0 h-10 sm:h-8 w-full max-w-screen">
|
||||
<div class="slide-track flex flex-row items-center">
|
||||
{#each [...sliderList] as item}
|
||||
<a href="{`stocks/${item?.symbol}`}" class="cursor-pointer transition shadow-lg sm:hover:bg-[#404040] sm:hover:bg-opacity-[0.7] duration-150 sm:w-full flex flex-row items-center rounded-full justify-center py-1.5 text-white text-sm mr-6 sm:mr-8">
|
||||
{item.symbol}
|
||||
{#if item?.changesPercentage >= 0}
|
||||
<svg class="inline-block w-4 h-4 ml-0.5 -mr-0.5 mt-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#10db06" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="text-[#10DB06] text-sm font-medium">+{item?.changesPercentage?.toFixed(2)}%</span>
|
||||
{:else if item?.changesPercentage < 0}
|
||||
<svg class="inline-block w-4 h-4 ml-0.5 -mr-0.5 mt-1 rotate-180" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#FF2F1F" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="text-[#FF2F1F] text-sm font-medium">{item?.changesPercentage?.toFixed(2)}%</span>
|
||||
{/if}
|
||||
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
@mixin white-gradient {
|
||||
background: linear-gradient(to right, rgba(14, 14, 16, 1) 0%, rgba(14, 14, 16, 0) 100%);
|
||||
}
|
||||
|
||||
$animationSpeed: 60s;
|
||||
|
||||
// Animation
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-250px * 7));
|
||||
}
|
||||
}
|
||||
|
||||
// Styling
|
||||
.slider {
|
||||
background: #0F0F0F;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
@include white-gradient;
|
||||
content: "";
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 3%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.slide-track {
|
||||
animation: scroll $animationSpeed linear infinite;
|
||||
display: flex;
|
||||
width: calc(250px * 14);
|
||||
&:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
119
src/lib/components/StockSplits.svelte
Normal file
119
src/lib/components/StockSplits.svelte
Normal file
@ -0,0 +1,119 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import {stockTicker} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
|
||||
export let stockDeck;
|
||||
|
||||
let showFullHistory = false;
|
||||
let stockSplits = [];
|
||||
|
||||
$: {
|
||||
|
||||
if ($stockTicker && typeof window !== 'undefined' && typeof stockDeck !== 'undefined' && stockDeck?.length !== 0)
|
||||
{
|
||||
stockSplits = stockDeck[0]?.stockSplits;
|
||||
showFullHistory = false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="stockSplitsInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Stock Splits
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Stock Splits"}
|
||||
content={"A stock split is when a company increases the number of its shares and lowers the price per share. This makes the stock more accessible to investors but doesn't change the total value of your investment."}
|
||||
id={"stockSplitsInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if stockSplits?.length !== 0}
|
||||
<div class="mt-16">
|
||||
{#each (showFullHistory ? stockSplits : stockSplits?.slice(0,3)) as item, index}
|
||||
|
||||
<div class="w-full -my-10 {index === 2 && !showFullHistory && stockSplits?.length > 3 ? 'opacity-[0.5]' : '' } ">
|
||||
|
||||
<div class="relative py-4 sm:pl-3">
|
||||
<div class="pl-1">
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="absolute left-0 h-20 px-px bg-gray-800 ml-4 sm:ml-5 transform -translate-x-1/2 "></div>
|
||||
<div class="absolute left-0 w-4 h-4 bg-[#10DB06] border-4 box-content border-gray-900 rounded-full ml-4 sm:ml-5 transform -translate-x-1/2"></div>
|
||||
|
||||
<!--Start Item-->
|
||||
<div class="flex flex-row items-center ml-5 w-full mb-6">
|
||||
|
||||
<div class="w-full rounded-lg bg-[#202020] shadow-lg h-16 pl-3 ml-6 sm:ml-3 pt-2">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-slate-400 font-medium text-sm sm:text-md mb-2 mr-auto">
|
||||
Date
|
||||
</span>
|
||||
<span class="text-white text-sm sm:text-md font-medium">
|
||||
{new Date(item?.date)?.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', daySuffix: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col ml-auto pr-3">
|
||||
<div class="text-slate-400 font-medium text-sm sm:text-md mb-2 ml-auto">
|
||||
From
|
||||
<span class="text-white">{item?.denominator}</span>
|
||||
</div>
|
||||
<div class="text-slate-400 font-medium text-sm sm:text-md ml-auto">
|
||||
To
|
||||
<span class="text-white">{item?.numerator}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Item-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{#if stockSplits?.length > 3}
|
||||
<label on:click={() => showFullHistory = !showFullHistory} class="cursor-pointer flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullHistory ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<h2 class="mt-10 mb-5 flex justify-center items-center text-3xl font-bold text-slate-700 m-auto">
|
||||
No data available
|
||||
<svg class="w-10 sm:w-12 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#334155" d="M18.68 12.32a4.49 4.49 0 0 0-6.36.01a4.49 4.49 0 0 0 0 6.36a4.508 4.508 0 0 0 5.57.63L21 22.39L22.39 21l-3.09-3.11c1.13-1.77.87-4.09-.62-5.57m-1.41 4.95c-.98.98-2.56.97-3.54 0c-.97-.98-.97-2.56.01-3.54c.97-.97 2.55-.97 3.53 0c.97.98.97 2.56 0 3.54M10.9 20.1a6.527 6.527 0 0 1-1.48-2.32C6.27 17.25 4 15.76 4 14v3c0 2.21 3.58 4 8 4c-.4-.26-.77-.56-1.1-.9M4 9v3c0 1.68 2.07 3.12 5 3.7v-.2c0-.93.2-1.85.58-2.69C6.34 12.3 4 10.79 4 9m8-6C7.58 3 4 4.79 4 7c0 2 3 3.68 6.85 4h.05c1.2-1.26 2.86-2 4.6-2c.91 0 1.81.19 2.64.56A3.215 3.215 0 0 0 20 7c0-2.21-3.58-4-8-4Z"/></svg>
|
||||
</h2>
|
||||
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
38
src/lib/components/Stocksymbol.svelte
Normal file
38
src/lib/components/Stocksymbol.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script>
|
||||
export let itemLabel;
|
||||
export let highlighted;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
li.autocomplete-items {
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #d4d4d4;
|
||||
z-index: 99;
|
||||
/*position the autocomplete items to be the same width as the container:*/
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
li.autocomplete-items:hover {
|
||||
/*when hovering an item:*/
|
||||
background-color: #FB6A67;
|
||||
color: white;
|
||||
}
|
||||
|
||||
li.autocomplete-items:active {
|
||||
/*when navigating through the items using the arrow keys:*/
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.autocomplete-active {
|
||||
/*when navigating through the items using the arrow keys:*/
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<li class="autocomplete-items" class:autocomplete-active={highlighted} on:click>{@html itemLabel}</li>
|
||||
154
src/lib/components/TARating.svelte
Normal file
154
src/lib/components/TARating.svelte
Normal file
@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { stockTicker, etfTicker, cryptoTicker, assetType} from '$lib/store';
|
||||
import SignalBar from '$lib/components/SignalBar.svelte'
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import Lazy from 'svelte-lazy';
|
||||
|
||||
export let taRating = {};
|
||||
|
||||
|
||||
let signalList = [];
|
||||
let showFullStats = false;
|
||||
let overallSignal = 'n/a'
|
||||
let buyCount = 0;
|
||||
let sellCount=0;
|
||||
let neutralCount=0;
|
||||
|
||||
|
||||
const modalContent = `
|
||||
Momentum indicators gauge the speed at which a financial instrument's price is changing, indicating the strength of its recent price movements.<br><br>
|
||||
On the other hand, trend indicators help identify the direction in which the price of an asset is moving over time, highlighting whether it's in an upward, downward, or sideways trend.<br><br>
|
||||
Together, these indicators assist in evaluating the overall market sentiment and potential future price movements.
|
||||
`;
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if (($assetType === 'stock' ? $stockTicker : $assetType === 'etf' ? $etfTicker : $cryptoTicker) && typeof window !== 'undefined' && Object?.keys(taRating)?.lenght !== 0) {
|
||||
showFullStats = false;
|
||||
overallSignal = taRating?.overallSignal;
|
||||
signalList = taRating?.signalList ?? []
|
||||
buyCount = 0;
|
||||
sellCount = 0;
|
||||
neutralCount=0;
|
||||
|
||||
signalList?.forEach(item => {
|
||||
switch (item?.signal) {
|
||||
case "Strong Buy":
|
||||
buyCount++;
|
||||
break;
|
||||
case "Buy":
|
||||
buyCount++;
|
||||
break;
|
||||
case "Sell":
|
||||
sellCount++;
|
||||
break;
|
||||
case "Strong Sell":
|
||||
sellCount++;
|
||||
break;
|
||||
default:
|
||||
neutralCount++;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<section class="overflow-hidden w-full">
|
||||
|
||||
<main>
|
||||
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="technicalIndicatorInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
TA Indicators
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"TA Indicators"}
|
||||
content={modalContent}
|
||||
id={"technicalIndicatorInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{#if overallSignal !== 'n/a'}
|
||||
<div class="text-white text-[1rem] sm:text-lg mt-3 mb-8 text-start">
|
||||
Out of 11 indicators, <span class="font-semibold text-[#10DB06]">{buyCount}</span> indicates a Buy,
|
||||
<span class="font-semibold text-[#F8901E]">{neutralCount}</span> are Neutral and <span class="font-semibold text-[#FF2F1F]">{sellCount}</span> indicate a Sell.
|
||||
On average, the signal is to
|
||||
{#if overallSignal === 'Buy' || overallSignal === 'Strong Buy'}
|
||||
<span class="text-[#10DB06] sm:font-medium">
|
||||
<svg class="w-7 h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#10db06" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"><path d="m3 17l6-6l4 4l8-8"/><path d="M17 7h4v4"/></g></svg>
|
||||
Buy.
|
||||
</span>
|
||||
|
||||
{:else if overallSignal === 'Neutral'}
|
||||
<span class="text-[#E57C34] sm:font-medium">
|
||||
<svg class="w-7 h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#e57c34" d="m22 12l-4-4v3H3v2h15v3l4-4Z"/></svg>
|
||||
Hold.
|
||||
</span>
|
||||
{:else if overallSignal === 'Sell' || overallSignal === 'Strong Sell'}
|
||||
<span class="text-[#FF2F1F] sm:font-medium">
|
||||
<svg class="w-7 h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#ff2f1f" d="M244 136v64a12 12 0 0 1-12 12h-64a12 12 0 0 1 0-24h35l-67-67l-31.51 31.52a12 12 0 0 1-17 0l-72-72a12 12 0 0 1 17-17L96 127l31.51-31.52a12 12 0 0 1 17 0L220 171v-35a12 12 0 0 1 24 0Z"/></svg>
|
||||
Sell.
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-[#FF2F1F] sm:font-medium">
|
||||
n/a.
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<!--Start Momentum Indicators-->
|
||||
<div class="w-full overflow-hidden">
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<table class="table table-sm table-compact w-full mb-5 mt-5">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Name</th>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Value</th>
|
||||
<th class="text-white text-sm font-medium bg-[#0F0F0F] shadow-md">Signal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each (showFullStats ? signalList : signalList?.slice(0, 3)) as item,index}
|
||||
<tr class="border-b border-[#202020] sm:border-[#0F0F0F] {index === 2 && !showFullStats && signalList?.length > 2 ? 'opacity-[0.3]' : '' }">
|
||||
<td class="text-white text-sm w-1/2 sm:w-full">
|
||||
{item?.name}
|
||||
</td>
|
||||
|
||||
<td class="text-white">
|
||||
<span class="text-white text-sm">
|
||||
{item?.value}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="text-white">
|
||||
<SignalBar signal = {item?.signal} />
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<label on:click={() => showFullStats = !showFullStats} class="{signalList?.length < 4 ? 'hidden' : ''} cursor-pointer m-auto flex justify-center items-center mt-5">
|
||||
<svg class="w-10 h-10 transform {showFullStats ? 'rotate-180' : ''} " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2A323C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 13.5L7.5 11l1.42-1.41L12 12.67l3.08-3.08L16.5 11L12 15.5z"/></svg>
|
||||
</label>
|
||||
|
||||
</Lazy>
|
||||
</div>
|
||||
<!--End Momentum Indicators-->
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
565
src/lib/components/TagSearchbar.svelte
Normal file
565
src/lib/components/TagSearchbar.svelte
Normal file
@ -0,0 +1,565 @@
|
||||
<script lang="ts">
|
||||
|
||||
import { userRegion, searchBarData, stockTicker, etfTicker, screenWidth} from '$lib/store';
|
||||
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
let apiURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
if (usRegion.includes(value)) {
|
||||
apiURL = import.meta.env.VITE_USEAST_API_URL;
|
||||
} else {
|
||||
apiURL = import.meta.env.VITE_EU_API_URL;
|
||||
}
|
||||
});
|
||||
|
||||
let assetType = '';
|
||||
|
||||
let showSuggestions = false;
|
||||
let notFoundTicker = false;
|
||||
let searchQuery = '';
|
||||
|
||||
let searchOpen = false;
|
||||
let searchBarModalChecked = false; // Initialize it to false
|
||||
let inputElement;
|
||||
|
||||
|
||||
async function loadSearchData() {
|
||||
|
||||
if($searchBarData?.length !== 0)
|
||||
{
|
||||
return
|
||||
}
|
||||
else {
|
||||
|
||||
// make the GET request to the endpoint
|
||||
const response = await fetch(apiURL+'/searchbar-data', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
|
||||
$searchBarData = await response.json();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function popularTicker(state) {
|
||||
searchOpen = false;
|
||||
stockTicker.update(value => state.toUpperCase());
|
||||
|
||||
const closePopup = document.getElementById("tagSearchBarModal");
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
|
||||
}
|
||||
|
||||
function searchBarTicker(state, assetType)
|
||||
{
|
||||
|
||||
|
||||
showSuggestions = false;
|
||||
if(state !== '' && $searchBarData.find(item => item?.symbol === state.toUpperCase()))
|
||||
{
|
||||
|
||||
stockTicker.update( value => state?.toUpperCase() );
|
||||
//In this case we dont care if stockTicker or etfTicker
|
||||
|
||||
|
||||
searchOpen = false
|
||||
notFoundTicker = false;
|
||||
//searchQuery = state.toUpperCase();
|
||||
const closePopup = document.getElementById("tagSearchBarModal");
|
||||
closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
else
|
||||
{
|
||||
notFoundTicker = true;
|
||||
//searchQuery ="";
|
||||
}
|
||||
|
||||
searchQuery ="";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
let searchResults = [];
|
||||
|
||||
async function search() {
|
||||
const normalizedSearchQuery = searchQuery?.toLowerCase();
|
||||
|
||||
// Define names for which symbols without dots should be prioritized
|
||||
const prioritizeWithoutDotsForNames = ["apple", /* Add more names as needed */];
|
||||
|
||||
const filteredList = $searchBarData?.map(item => ({
|
||||
...item,
|
||||
nameLower: item?.name?.toLowerCase(),
|
||||
symbolLower: item?.symbol?.toLowerCase(),
|
||||
}))?.filter(({ nameLower, symbolLower }) =>
|
||||
nameLower?.includes(normalizedSearchQuery) ||
|
||||
symbolLower?.includes(normalizedSearchQuery)
|
||||
);
|
||||
|
||||
filteredList?.sort((a, b) => {
|
||||
const aSymbolLower = a?.symbolLower;
|
||||
const bSymbolLower = b?.symbolLower;
|
||||
const aNameLower = a?.nameLower;
|
||||
const bNameLower = b?.nameLower;
|
||||
|
||||
// Check for exact symbol matches
|
||||
const isExactMatchA = aSymbolLower === normalizedSearchQuery;
|
||||
const isExactMatchB = bSymbolLower === normalizedSearchQuery;
|
||||
|
||||
if (isExactMatchA && !isExactMatchB) {
|
||||
return -1; // Prioritize exact symbol match for A
|
||||
} else if (!isExactMatchA && isExactMatchB) {
|
||||
return 1; // Prioritize exact symbol match for B
|
||||
}
|
||||
|
||||
const aSymbolIndex = aSymbolLower?.indexOf(normalizedSearchQuery);
|
||||
const bSymbolIndex = bSymbolLower?.indexOf(normalizedSearchQuery);
|
||||
|
||||
const aNameIndex = aNameLower?.indexOf(normalizedSearchQuery);
|
||||
const bNameIndex = bNameLower?.indexOf(normalizedSearchQuery);
|
||||
|
||||
// If no exact symbol match, prioritize based on the combined position in name and symbol
|
||||
const positionComparison = aSymbolIndex + aNameIndex - (bSymbolIndex + bNameIndex);
|
||||
|
||||
// Additional condition for prioritizing symbols without dots for specific names
|
||||
if (prioritizeWithoutDotsForNames.includes(normalizedSearchQuery)) {
|
||||
const aHasDot = aSymbolLower?.includes(".") || false;
|
||||
const bHasDot = bSymbolLower?.includes(".") || false;
|
||||
|
||||
// Prioritize results without dots for the specified names
|
||||
return aHasDot - bHasDot || positionComparison;
|
||||
}
|
||||
|
||||
return positionComparison;
|
||||
});
|
||||
|
||||
searchResults = filteredList?.slice(0, 5);
|
||||
showSuggestions = normalizedSearchQuery !== "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
const onKeyPress = e => {
|
||||
if (e.charCode === 13) {
|
||||
searchBarTicker(searchQuery, assetType);
|
||||
focusedSuggestion = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let focusedSuggestion = null;
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "ArrowDown" && showSuggestions) {
|
||||
// Move down in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = searchResults.findIndex(item => item.symbol === searchQuery);
|
||||
if (currentIndex < searchResults.length - 1) {
|
||||
searchQuery = searchResults[currentIndex + 1].symbol;
|
||||
assetType = searchResults[currentIndex + 1].type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
} else if (event.key === "ArrowUp" && showSuggestions) {
|
||||
// Move up in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = searchResults.findIndex(item => item.symbol === searchQuery);
|
||||
if (currentIndex > 0) {
|
||||
searchQuery = searchResults[currentIndex - 1].symbol;
|
||||
assetType = searchResults[currentIndex - 1].type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
}
|
||||
|
||||
else if (event.key === "ArrowDown" && !showSuggestions) {
|
||||
// Move down in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = popularList.findIndex(item => item.symbol === searchQuery);
|
||||
if (currentIndex < popularList.length - 1) {
|
||||
searchQuery = popularList[currentIndex + 1].symbol;
|
||||
assetType = popularList[currentIndex + 1].type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
}
|
||||
else if (event.key === "ArrowUp" && !showSuggestions) {
|
||||
// Move up in the suggestions
|
||||
event.preventDefault(); // Prevent scrolling
|
||||
const currentIndex = popularList.findIndex(item => item.symbol === searchQuery);
|
||||
if (currentIndex > 0) {
|
||||
searchQuery = popularList[currentIndex - 1].symbol;
|
||||
assetType = popularList[currentIndex - 1].type;
|
||||
focusedSuggestion = searchQuery; // Update the focused suggestion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let charNumber = 20;
|
||||
|
||||
|
||||
$: {
|
||||
if ($screenWidth < 640)
|
||||
{
|
||||
charNumber = 20;
|
||||
}
|
||||
else {
|
||||
charNumber = 20;
|
||||
}
|
||||
}
|
||||
|
||||
let popularList = [];
|
||||
const popularSymbols = ['ADBE','SHOP','CRM','UBER','RDHL','TSM','INTC','NIO','DBX','HOOD','AMZN', 'TSLA', 'AMD','MCD','NVDA','PYPL','AAPL','BYND','KO'];
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if ($searchBarData && popularList?.length === 0) {
|
||||
popularList = $searchBarData.filter(({ symbol }) =>
|
||||
popularSymbols?.includes(symbol)
|
||||
);
|
||||
|
||||
// Fisher-Yates (Knuth) Shuffle Algorithm
|
||||
for (let i = popularList.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[popularList[i], popularList[j]] = [popularList[j], popularList[i]];
|
||||
}
|
||||
|
||||
popularList = popularList?.slice(0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if( searchBarModalChecked === true && typeof window !== 'undefined')
|
||||
{
|
||||
if($screenWidth > 640)
|
||||
{
|
||||
inputElement.focus();
|
||||
}
|
||||
//Page is not scrollable now
|
||||
document.body.classList.add("overflow-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if( searchBarModalChecked === false && typeof window !== 'undefined')
|
||||
{
|
||||
showSuggestions = searchQuery = "";
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if(searchQuery?.length !== 0)
|
||||
{
|
||||
notFoundTicker = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<label on:click={loadSearchData} for="tagSearchBarModal" class="border border-slate-500 mb-2 flex flex-wrap pl-4 pr-4 py-2 m-1 mr-3 justify-between items-center text-sm font-medium rounded-xl cursor-pointer text-gray-200 hover:text-gray-100">
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="fill-current text-white" d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
|
||||
<path class="fill-current text-white" d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
|
||||
</svg>
|
||||
<span class="text-gray-300">Search a ticker...</span>
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Searchbar Modal-->
|
||||
|
||||
{#if $screenWidth > 640}
|
||||
<input type="checkbox" id="tagSearchBarModal" class="modal-toggle" bind:checked={searchBarModalChecked} />
|
||||
|
||||
<dialog id="tagSearchBarModal" class="modal modal-top ">
|
||||
|
||||
|
||||
<label for="tagSearchBarModal" class="cursor-pointer modal-backdrop"></label>
|
||||
|
||||
|
||||
|
||||
<div class="modal-box overflow-hidden rounded-xl bg-[#09090B] sm:my-8 sm:m-auto sm:h-auto w-full sm:w-1/2 2xl:w-1/3 " >
|
||||
|
||||
|
||||
<!-- Search layout -->
|
||||
<div class="mt-5 sm:mt-0">
|
||||
<div class="relative">
|
||||
<label for="modal-search" class="sr-only">Search</label>
|
||||
<input
|
||||
id="modal-search"
|
||||
class="rounded-lg w-full text-white bg-[#09090B] border border-slate-800 focus:ring-transparent placeholder-gray-200 py-3 pl-10 pr-4"
|
||||
type="search"
|
||||
placeholder="Search Anything…"
|
||||
bind:value={searchQuery}
|
||||
bind:this={inputElement}
|
||||
on:input={search}
|
||||
on:keydown={handleKeyDown}
|
||||
on:keypress={onKeyPress}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button on:click={() => (searchBarTicker(searchQuery, assetType))} class="absolute inset-0 right-auto group" type="submit" aria-label="Search">
|
||||
<svg class="w-4 h-4 shrink-0 fill-current text-white ml-4 mr-2 text-slate-400" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
|
||||
<path d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $searchBarData?.length !== 0}
|
||||
<div class="py-4">
|
||||
<!-- Popular searches -->
|
||||
<div class="mb-3 last:mb-0 mt-3">
|
||||
{#if notFoundTicker}
|
||||
<p class="text-xs font-semibold text-[#FB6A67] px-2 mb-4">Oh snapp, ticker does not exist in our database</p>
|
||||
{/if}
|
||||
{#if !showSuggestions}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Popular
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="text-sm" >
|
||||
{#if !showSuggestions }
|
||||
{#each popularList as item}
|
||||
<li>
|
||||
<a data-sveltekit-preload-data="false" on:click={() => popularTicker(item?.symbol) } class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'shake-ticker cursor-pointer bg-[#202020] rounded-lg flex justify-start items-center p-2 text-white group'} w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#000] flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6" src={`https://financialmodelingprep.com/image-stock/${item?.symbol}.png`} loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col ml-2">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name.length > 150 ? item?.name?.slice(0,150) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
|
||||
{:else if showSuggestions && searchResults?.length > 0}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Suggestions
|
||||
</div>
|
||||
{#each searchResults as item}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label data-sveltekit-preload-data="false" on:click={() => (searchBarTicker(item?.symbol, item?.type))} class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'cursor-pointer mb-2 bg-[#202020] rounded-lg flex justify-start items-center p-2 text-white group'}">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name?.length > 150 ? item?.name?.slice(0,150) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
{:else if showSuggestions && searchResults?.length === 0}
|
||||
<li>
|
||||
<label class="flex items-center p-2 text-white hover:text-white hover:bg-[#404040] bg-opacity-[0.25] rounded group" >
|
||||
<svg class="w-3 h-3 fill-slate-400 shrink-0 mr-3 dark:fill-slate-500" width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.953 4.29a.5.5 0 0 0-.454-.292H6.14L6.984.62A.5.5 0 0 0 6.12.173l-6 7a.5.5 0 0 0 .379.825h5.359l-.844 3.38a.5.5 0 0 0 .864.445l6-7a.5.5 0 0 0 .075-.534Z" />
|
||||
</svg>
|
||||
<span>No results found</span>
|
||||
</label>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center m-auto mt-4 py-20">
|
||||
<span class="loading loading-lg loading-spinner text-success"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<label for="searchBarModal" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
<span class="text-white text-md font-medium">
|
||||
Return
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<!--Start Drawer Sidewise for mobile-->
|
||||
|
||||
|
||||
<div class="drawer drawer-end overflow-hidden" style="z-index: 9999">
|
||||
<input id="tagSearchBarModal" type="checkbox" class="drawer-toggle" bind:checked={searchBarModalChecked} />
|
||||
|
||||
<div class="drawer-side overflow-hidden">
|
||||
|
||||
|
||||
|
||||
<div class="modal-box overflow-hidden rounded-xl bg-[#09090B] min-h-screen w-screen pt-10" >
|
||||
|
||||
|
||||
<!-- Search layout -->
|
||||
<div class="mt-5 sm:mt-0">
|
||||
<div class="relative">
|
||||
<label for="modal-search" class="sr-only">Search</label>
|
||||
<input
|
||||
id="modal-search"
|
||||
class="rounded-lg w-full text-white bg-[#09090B] border border-slate-800 focus:ring-transparent placeholder-gray-200 py-3 pl-10 pr-4"
|
||||
type="search"
|
||||
placeholder="Search Anything…"
|
||||
bind:value={searchQuery}
|
||||
bind:this={inputElement}
|
||||
on:input={search}
|
||||
on:keydown={handleKeyDown}
|
||||
on:keypress={onKeyPress}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button on:click={() => (searchBarTicker(searchQuery, assetType))} class="absolute inset-0 right-auto group" type="submit" aria-label="Search">
|
||||
<svg class="w-4 h-4 shrink-0 fill-current text-white ml-4 mr-2 text-slate-400" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <path d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
|
||||
<path d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $searchBarData?.length !== 0}
|
||||
<div class="py-4">
|
||||
<!-- Popular searches -->
|
||||
<div class="mb-3 last:mb-0 mt-3">
|
||||
{#if notFoundTicker}
|
||||
<p class="text-xs font-semibold text-[#FB6A67] px-2 mb-4">Oh snapp, ticker does not exist in our database</p>
|
||||
{/if}
|
||||
{#if !showSuggestions}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Popular
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="text-sm" >
|
||||
{#if !showSuggestions }
|
||||
{#each popularList as item}
|
||||
<li>
|
||||
<a data-sveltekit-preload-data="false" on:click={() => popularTicker(item?.symbol, item?.type) } class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'cursor-pointer bg-[#202020] rounded-lg flex justify-start items-center p-2 text-white group'} w-full">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#000] flex items-center justify-center">
|
||||
<img style="clip-path: circle(50%);" class="w-6 h-6" src={`https://financialmodelingprep.com/image-stock/${item?.symbol}.png`} loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col ml-2">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name.length > charNumber ? item?.name.slice(0,charNumber) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto mr-2">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
|
||||
{:else if showSuggestions && searchResults?.length > 0}
|
||||
<div class="text-start text-sm font-semibold text-slate-400 mb-2">
|
||||
Suggestions
|
||||
</div>
|
||||
{#each searchResults as item}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label data-sveltekit-preload-data="false" on:click={() => (searchBarTicker(item?.symbol, item?.type))} class="mb-2 {item?.symbol === focusedSuggestion ? 'shake-ticker cursor-pointer flex justify-start items-center p-2 text-white bg-[#404040] bg-opacity-[0.25] rounded group' : 'cursor-pointer mb-2 bg-[#202020] rounded-lg flex justify-start items-center p-2 text-white group'}">
|
||||
<div class="flex flex-row items-center w-full">
|
||||
|
||||
<div class="flex flex-col ml-1">
|
||||
<span class="text-blue-400">{item?.symbol}</span>
|
||||
<span class="text-white">{item?.name?.length > charNumber ? item?.name?.slice(0,charNumber) + "..." : item?.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-white font-medium ml-auto mr-2">
|
||||
{item?.type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
{:else if showSuggestions && searchResults?.length === 0}
|
||||
<li>
|
||||
<label class="flex items-center p-2 text-white hover:text-white hover:bg-[#404040] bg-opacity-[0.25] rounded group" >
|
||||
<svg class="w-3 h-3 fill-slate-400 shrink-0 mr-3 dark:fill-slate-500" width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.953 4.29a.5.5 0 0 0-.454-.292H6.14L6.984.62A.5.5 0 0 0 6.12.173l-6 7a.5.5 0 0 0 .379.825h5.359l-.844 3.38a.5.5 0 0 0 .864.445l6-7a.5.5 0 0 0 .075-.534Z" />
|
||||
</svg>
|
||||
<span>No results found</span>
|
||||
</label>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center m-auto mt-4 py-20">
|
||||
<span class="loading loading-lg loading-spinner text-success"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<label for="tagSearchBarModal" class="absolute left-6 top-4 sm:hidden">
|
||||
<svg class="w-6 h-6 inline-block mb-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--End Drawer Sidewise for mobile-->
|
||||
{/if}
|
||||
<!--End Searchbar Modal-->
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user