Spaces:
Sleeping
Sleeping
Commit ·
56bd117
1
Parent(s): e70a41e
initial
Browse files- .DS_Store +0 -0
- .gitattributes +3 -0
- .gitignore +9 -0
- Dockerfile +35 -0
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/lstm_n_pipeline.cpython-312.pyc +0 -0
- __pycache__/lstm_pipeline.cpython-312.pyc +0 -0
- app.py +397 -0
- requirements.txt +13 -0
- static/css/fundamentals.css +1160 -0
- static/css/legal.css +140 -0
- static/css/login.css +322 -0
- static/css/movers.css +964 -0
- static/css/news.css +480 -0
- static/css/predict.css +466 -0
- static/css/styles.css +652 -0
- static/images/bg.png +3 -0
- static/js/auth.js +44 -0
- static/js/firebase-config.js +92 -0
- static/js/fundamentals.js +695 -0
- static/js/home.js +235 -0
- static/js/legal.js +22 -0
- static/js/login.js +295 -0
- static/js/movers.js +477 -0
- static/js/news.js +139 -0
- static/js/newsletter.js +34 -0
- static/js/predict.js +574 -0
- static/models/coins.glb +3 -0
- static/models/finbert_sentiment089/config.json +35 -0
- static/models/finbert_sentiment089/model.safetensors +3 -0
- static/models/finbert_sentiment089/special_tokens_map.json +37 -0
- static/models/finbert_sentiment089/tokenizer_config.json +59 -0
- static/models/finbert_sentiment089/vocab.txt +0 -0
- static/pipelines/__pycache__/lstm_n_pipeline.cpython-312.pyc +0 -0
- static/pipelines/__pycache__/lstm_pipeline.cpython-312.pyc +0 -0
- static/pipelines/lstm_n_pipeline.py +239 -0
- static/pipelines/lstm_pipeline.py +219 -0
- static/video/bg video.mp4 +3 -0
- t.txt +242 -0
- templates/404.html +150 -0
- templates/base.html +23 -0
- templates/disclaimer.html +44 -0
- templates/fundamentals.html +251 -0
- templates/home.html +155 -0
- templates/login.html +81 -0
- templates/movers.html +158 -0
- templates/news.html +105 -0
- templates/predict.html +179 -0
- templates/privacy.html +60 -0
- templates/terms.html +54 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.glb filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mongo_uri_secret
|
| 2 |
+
firebase_api_key_secret
|
| 3 |
+
firebase_api_key_secret
|
| 4 |
+
firebase_auth_domain_secret
|
| 5 |
+
firebase_project_secret
|
| 6 |
+
firebase_storage_bucket_secret
|
| 7 |
+
firebase_messaging_sender_id_secret
|
| 8 |
+
firebase_app_id_secret
|
| 9 |
+
firebase_measurement_id_secret
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python image
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
# Set environment variables
|
| 5 |
+
ENV PYTHONDONTWRITEBYTECODE 1
|
| 6 |
+
ENV PYTHONUNBUFFERED 1
|
| 7 |
+
|
| 8 |
+
# Set work directory
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Install system dependencies
|
| 12 |
+
RUN apt-get update && apt-get install -y \
|
| 13 |
+
build-essential \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
# Copy requirements and install Python dependencies
|
| 17 |
+
COPY requirements.txt .
|
| 18 |
+
RUN pip install --upgrade pip
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# Copy project files
|
| 22 |
+
COPY . .
|
| 23 |
+
|
| 24 |
+
# Expose port (Flask default is 5000)
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
# Set environment variable for Flask
|
| 28 |
+
ENV FLASK_APP=app.py
|
| 29 |
+
ENV OMP_NUM_THREADS=1
|
| 30 |
+
ENV TF_NUM_INTRAOP_THREADS=1
|
| 31 |
+
ENV TF_NUM_INTEROP_THREADS=1
|
| 32 |
+
ENV TF_ENABLE_ONEDNN_OPTS=0
|
| 33 |
+
ENV GUNICORN_CMD_ARGS="--timeout 6000"
|
| 34 |
+
# Run the app with Gunicorn for production
|
| 35 |
+
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "app:app"]
|
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (19.2 kB). View file
|
|
|
__pycache__/lstm_n_pipeline.cpython-312.pyc
ADDED
|
Binary file (17.5 kB). View file
|
|
|
__pycache__/lstm_pipeline.cpython-312.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, jsonify, request, redirect, url_for, session, send_from_directory
|
| 2 |
+
import requests
|
| 3 |
+
import os
|
| 4 |
+
from functools import wraps
|
| 5 |
+
import firebase_admin
|
| 6 |
+
from firebase_admin import credentials, auth
|
| 7 |
+
from flask import Flask, jsonify, render_template
|
| 8 |
+
from pymongo import MongoClient
|
| 9 |
+
import json
|
| 10 |
+
from static.pipelines.lstm_pipeline import run_lstm_prediction
|
| 11 |
+
from static.pipelines.lstm_n_pipeline import run_lstm_sentiment_prediction
|
| 12 |
+
import tensorflow as tf
|
| 13 |
+
import yfinance as yf
|
| 14 |
+
import requests
|
| 15 |
+
from flask import Response
|
| 16 |
+
import numpy as np
|
| 17 |
+
import pandas as pd
|
| 18 |
+
import traceback
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
|
| 21 |
+
app = Flask(__name__)
|
| 22 |
+
app.secret_key = os.urandom(24)
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
# Firebase configuration
|
| 26 |
+
firebase_api_key = os.getenv("FIREBASE_API_KEY")
|
| 27 |
+
firebase_auth_domain = os.getenv("FIREBASE_AUTH_DOMAIN")
|
| 28 |
+
firebase_project = os.getenv("FIREBASE_PROJECT")
|
| 29 |
+
firebase_storage_bucket = os.getenv("FIREBASE_STORAGE_BUCKET")
|
| 30 |
+
firebase_messaging_sender_id = os.getenv("FIREBASE_MESSAGING_SENDER_ID")
|
| 31 |
+
firebase_app_id = os.getenv("FIREBASE_APP_ID")
|
| 32 |
+
firebase_measurement_id = os.getenv("FIREBASE_MEASUREMENT_ID")
|
| 33 |
+
|
| 34 |
+
# Example usage
|
| 35 |
+
# Use the MongoDB URI from the .env file
|
| 36 |
+
mongo_uri = os.getenv("MONGO_URI")
|
| 37 |
+
client = MongoClient(mongo_uri)
|
| 38 |
+
db = client["stock_news"]
|
| 39 |
+
companies_collection = db["nse50_companies"]
|
| 40 |
+
news_collection = db["moneyworks_company_news"]
|
| 41 |
+
|
| 42 |
+
# Firebase credentials
|
| 43 |
+
firebase_credentials_json = os.getenv("FIREBASE_CREDENTIALS_JSON")
|
| 44 |
+
if firebase_credentials_json:
|
| 45 |
+
firebase_credentials = json.loads(firebase_credentials_json)
|
| 46 |
+
cred = credentials.Certificate(firebase_credentials)
|
| 47 |
+
if not firebase_admin._apps: # Check if no app is already initialized
|
| 48 |
+
firebase_admin.initialize_app(cred)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def verify_firebase_token(token):
|
| 52 |
+
try:
|
| 53 |
+
decoded_token = auth.verify_id_token(token)
|
| 54 |
+
return decoded_token
|
| 55 |
+
except Exception as e:
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def login_required(f):
|
| 60 |
+
@wraps(f)
|
| 61 |
+
def decorated_function(*args, **kwargs):
|
| 62 |
+
# Check if user is logged in (client-side auth check)
|
| 63 |
+
# For server-side verification, you would validate the Firebase token here
|
| 64 |
+
if 'user_logged_in' not in session:
|
| 65 |
+
return redirect(url_for('login'))
|
| 66 |
+
return f(*args, **kwargs)
|
| 67 |
+
return decorated_function
|
| 68 |
+
|
| 69 |
+
@app.route('/')
|
| 70 |
+
def home():
|
| 71 |
+
modelpath = url_for('static', filename='models/coins.glb')
|
| 72 |
+
return render_template("home.html", modelpath=modelpath)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@app.route('/login')
|
| 76 |
+
def login():
|
| 77 |
+
return render_template("login.html")
|
| 78 |
+
|
| 79 |
+
@app.route('/fundamentals')
|
| 80 |
+
def fundamentals():
|
| 81 |
+
return render_template("fundamentals.html")
|
| 82 |
+
|
| 83 |
+
@app.route('/movers')
|
| 84 |
+
def movers():
|
| 85 |
+
return render_template("movers.html")
|
| 86 |
+
|
| 87 |
+
@app.route('/news')
|
| 88 |
+
def news():
|
| 89 |
+
return render_template("news.html")
|
| 90 |
+
|
| 91 |
+
@app.route('/firebase-config')
|
| 92 |
+
def firebase_config():
|
| 93 |
+
return jsonify({
|
| 94 |
+
"apiKey": firebase_api_key,
|
| 95 |
+
"authDomain": firebase_auth_domain,
|
| 96 |
+
"projectId": firebase_project,
|
| 97 |
+
"storageBucket": firebase_storage_bucket,
|
| 98 |
+
"messagingSenderId": firebase_messaging_sender_id,
|
| 99 |
+
"appId": firebase_app_id,
|
| 100 |
+
"measurementId": firebase_measurement_id
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@app.route("/get-companies")
|
| 105 |
+
def get_companies():
|
| 106 |
+
filtered = list(companies_collection.find({}, {"_id": 0, "Company Name": 1, "Yahoo Finance Ticker": 1}))
|
| 107 |
+
# Remove companies with duplicate tickers or ticker in ["NIFTY", "SENSEX"]
|
| 108 |
+
seen = set()
|
| 109 |
+
companies = []
|
| 110 |
+
for c in filtered:
|
| 111 |
+
ticker = c.get("Yahoo Finance Ticker", "").upper()
|
| 112 |
+
if not ticker or ticker in seen or ticker in {"^NSEI", "^BSESN"}:
|
| 113 |
+
continue
|
| 114 |
+
seen.add(ticker)
|
| 115 |
+
companies.append(c)
|
| 116 |
+
return jsonify(companies)
|
| 117 |
+
|
| 118 |
+
@app.route('/api/news-sentiment')
|
| 119 |
+
def api_news_sentiment():
|
| 120 |
+
ticker = request.args.get('ticker')
|
| 121 |
+
if not ticker:
|
| 122 |
+
return jsonify({'error': 'No ticker provided'}), 400
|
| 123 |
+
try:
|
| 124 |
+
# Fetch the latest sentiment from your database or sentiment analysis pipeline
|
| 125 |
+
news = news_collection.find_one({"yahoo_ticker": ticker}, sort=[("date", -1)])
|
| 126 |
+
if news and "sentiment" in news:
|
| 127 |
+
return jsonify({'sentiment': news["sentiment"], 'score': news.get("score", "N/A")})
|
| 128 |
+
else:
|
| 129 |
+
return jsonify({'sentiment': 'Neutral', 'score': 'N/A'})
|
| 130 |
+
except Exception as e:
|
| 131 |
+
return jsonify({'error': str(e)}), 500
|
| 132 |
+
|
| 133 |
+
@app.route('/predict')
|
| 134 |
+
def predict():
|
| 135 |
+
return render_template("predict.html")
|
| 136 |
+
|
| 137 |
+
@app.route('/privacy')
|
| 138 |
+
def privacy():
|
| 139 |
+
return render_template("privacy.html")
|
| 140 |
+
|
| 141 |
+
@app.route('/terms')
|
| 142 |
+
def terms():
|
| 143 |
+
return render_template("terms.html")
|
| 144 |
+
|
| 145 |
+
@app.route('/disclaimer')
|
| 146 |
+
def disclaimer():
|
| 147 |
+
return render_template("disclaimer.html")
|
| 148 |
+
|
| 149 |
+
@app.route('/predict-result', methods=['POST'])
|
| 150 |
+
def predict_result():
|
| 151 |
+
import tensorflow as tf
|
| 152 |
+
data = request.get_json()
|
| 153 |
+
prediction_type = data.get('type')
|
| 154 |
+
prediction_date = data.get('prediction_date')
|
| 155 |
+
epochs = int(data.get('epochs', 100))
|
| 156 |
+
|
| 157 |
+
try:
|
| 158 |
+
if prediction_type == 'historical-only' and prediction_date:
|
| 159 |
+
data['epochs'] = epochs
|
| 160 |
+
response = run_lstm_prediction(data)
|
| 161 |
+
elif prediction_type == 'news-sentiment' and prediction_date:
|
| 162 |
+
data['epochs'] = epochs
|
| 163 |
+
response = run_lstm_sentiment_prediction(data)
|
| 164 |
+
elif prediction_type == 'both' and prediction_date:
|
| 165 |
+
data['epochs'] = epochs
|
| 166 |
+
hist_result = run_lstm_prediction(data)
|
| 167 |
+
tf.keras.backend.clear_session()
|
| 168 |
+
sent_result = run_lstm_sentiment_prediction(data)
|
| 169 |
+
tf.keras.backend.clear_session()
|
| 170 |
+
if hasattr(hist_result, 'get_json'):
|
| 171 |
+
hist_result = hist_result.get_json()
|
| 172 |
+
if hasattr(sent_result, 'get_json'):
|
| 173 |
+
sent_result = sent_result.get_json()
|
| 174 |
+
return jsonify({
|
| 175 |
+
'historical': hist_result,
|
| 176 |
+
'sentiment': sent_result
|
| 177 |
+
})
|
| 178 |
+
else:
|
| 179 |
+
return jsonify({'error': 'Invalid prediction type'}), 400
|
| 180 |
+
except Exception as e:
|
| 181 |
+
print(traceback.format_exc())
|
| 182 |
+
return jsonify({'error': str(e)}), 500
|
| 183 |
+
finally:
|
| 184 |
+
tf.keras.backend.clear_session()
|
| 185 |
+
return response
|
| 186 |
+
@app.route('/api/lookup-symbol')
|
| 187 |
+
def lookup_symbol():
|
| 188 |
+
query = request.args.get('query', '').strip()
|
| 189 |
+
if not query:
|
| 190 |
+
return jsonify({'error': 'No query provided'}), 400
|
| 191 |
+
# Search by company name, symbol, company searched, or ticker (case-insensitive)
|
| 192 |
+
company = companies_collection.find_one({
|
| 193 |
+
"$or": [
|
| 194 |
+
{"Company Name": {"$regex": f"^{query}$", "$options": "i"}},
|
| 195 |
+
{"Yahoo Finance Ticker": {"$regex": f"^{query}$", "$options": "i"}},
|
| 196 |
+
{"Symbol": {"$regex": f"^{query}$", "$options": "i"}},
|
| 197 |
+
{"Company Searched": {"$regex": f"^{query}$", "$options": "i"}}
|
| 198 |
+
]
|
| 199 |
+
}, {"_id": 0, "Yahoo Finance Ticker": 1})
|
| 200 |
+
if not company:
|
| 201 |
+
return jsonify({'error': 'Company not found'}), 404
|
| 202 |
+
return jsonify({'symbol': company["Yahoo Finance Ticker"]})
|
| 203 |
+
|
| 204 |
+
@app.route('/api/historical')
|
| 205 |
+
def api_historical():
|
| 206 |
+
symbol = request.args.get('symbol')
|
| 207 |
+
start = request.args.get('start')
|
| 208 |
+
end = request.args.get('end')
|
| 209 |
+
if not symbol:
|
| 210 |
+
return jsonify({'error': 'No symbol provided'}), 400
|
| 211 |
+
try:
|
| 212 |
+
ticker = yf.Ticker(symbol)
|
| 213 |
+
if start and end:
|
| 214 |
+
history = ticker.history(start=start, end=end)
|
| 215 |
+
else:
|
| 216 |
+
history = ticker.history(period="1y")
|
| 217 |
+
if history.empty:
|
| 218 |
+
return jsonify({'error': f'No data found for symbol: {symbol}'}), 404
|
| 219 |
+
data = {
|
| 220 |
+
'history': history.reset_index().to_dict(orient='records')
|
| 221 |
+
}
|
| 222 |
+
return jsonify(data)
|
| 223 |
+
except Exception as e:
|
| 224 |
+
return jsonify({'error': str(e)}), 500
|
| 225 |
+
|
| 226 |
+
@app.route('/api/fundamentals')
|
| 227 |
+
def api_fundamentals():
|
| 228 |
+
symbol = request.args.get('symbol')
|
| 229 |
+
if not symbol:
|
| 230 |
+
return jsonify({'error': 'No symbol provided'}), 400
|
| 231 |
+
try:
|
| 232 |
+
ticker = yf.Ticker(symbol)
|
| 233 |
+
# Always fetch 1y for risk metrics
|
| 234 |
+
risk_history = ticker.history(period="1y")
|
| 235 |
+
# Always fetch max for chart/history
|
| 236 |
+
full_history = ticker.history(period="max")
|
| 237 |
+
info = ticker.info
|
| 238 |
+
|
| 239 |
+
# Risk metrics from 1y history
|
| 240 |
+
if not risk_history.empty:
|
| 241 |
+
risk_history['Return'] = risk_history['Close'].pct_change()
|
| 242 |
+
volatility = float(risk_history['Return'].std() * np.sqrt(252))
|
| 243 |
+
var_95 = float(np.percentile(risk_history['Return'].dropna(), 5))
|
| 244 |
+
else:
|
| 245 |
+
volatility = None
|
| 246 |
+
var_95 = None
|
| 247 |
+
|
| 248 |
+
beta = info.get('beta')
|
| 249 |
+
|
| 250 |
+
def pct(val):
|
| 251 |
+
return round(val * 100, 2) if val is not None else None
|
| 252 |
+
|
| 253 |
+
data = {
|
| 254 |
+
'pe': info.get('trailingPE'),
|
| 255 |
+
'pb': info.get('priceToBook'),
|
| 256 |
+
'ps': info.get('priceToSalesTrailing12Months'),
|
| 257 |
+
'divYield': pct(info.get('dividendYield')),
|
| 258 |
+
'roe': pct(info.get('returnOnEquity')),
|
| 259 |
+
'roa': pct(info.get('returnOnAssets')),
|
| 260 |
+
'grossMargin': pct(info.get('grossMargins')),
|
| 261 |
+
'opMargin': pct(info.get('operatingMargins')),
|
| 262 |
+
'currentRatio': info.get('currentRatio'),
|
| 263 |
+
'quickRatio': info.get('quickRatio'),
|
| 264 |
+
'debtEquity': info.get('debtToEquity'),
|
| 265 |
+
'ebitdaMargin': info.get('ebitdaMargins'),
|
| 266 |
+
'volatility': volatility,
|
| 267 |
+
'beta': beta,
|
| 268 |
+
'var95': var_95,
|
| 269 |
+
}
|
| 270 |
+
# Always include full history for charting
|
| 271 |
+
if not full_history.empty:
|
| 272 |
+
data['history'] = full_history.reset_index().to_dict(orient='records')
|
| 273 |
+
return jsonify(data)
|
| 274 |
+
except Exception as e:
|
| 275 |
+
return jsonify({'error': str(e)}), 500
|
| 276 |
+
@app.route('/api/news')
|
| 277 |
+
def api_news():
|
| 278 |
+
query = request.args.get('query', 'Indian stock market OR NSE OR Sensex OR Nifty')
|
| 279 |
+
rss_url = f"https://news.google.com/rss/search?q=Indian+finance+OR+economy+OR+RBI+OR+inflation+when:7d&hl=en-IN&gl=IN&ceid=IN:en"
|
| 280 |
+
r = requests.get(rss_url)
|
| 281 |
+
return Response(r.content, mimetype='application/xml')
|
| 282 |
+
|
| 283 |
+
@app.route('/api/market-movers')
|
| 284 |
+
def api_market_movers():
|
| 285 |
+
try:
|
| 286 |
+
# Get unique tickers from MongoDB
|
| 287 |
+
mongo_docs = list(companies_collection.find(
|
| 288 |
+
{"Yahoo Finance Ticker": {"$ne": None}},
|
| 289 |
+
{"Yahoo Finance Ticker": 1, "_id": 0}
|
| 290 |
+
))
|
| 291 |
+
tickers = list({doc["Yahoo Finance Ticker"] for doc in mongo_docs if "Yahoo Finance Ticker" in doc})
|
| 292 |
+
if not tickers:
|
| 293 |
+
return jsonify({"gainers": [], "losers": [], "error": "No tickers found in DB"}), 200
|
| 294 |
+
|
| 295 |
+
# Download last 3 days to buffer for non-trading days
|
| 296 |
+
data = yf.download(tickers, period="3d", interval="1d", group_by='ticker', progress=False, threads=True)
|
| 297 |
+
|
| 298 |
+
results = []
|
| 299 |
+
for ticker in tickers:
|
| 300 |
+
try:
|
| 301 |
+
df = data[ticker].dropna()
|
| 302 |
+
if df.shape[0] < 2:
|
| 303 |
+
continue
|
| 304 |
+
prev_close = df['Close'].iloc[-2]
|
| 305 |
+
last_close = df['Close'].iloc[-1]
|
| 306 |
+
pct_change = ((last_close - prev_close) / prev_close) * 100
|
| 307 |
+
|
| 308 |
+
# Only append if all values are valid numbers
|
| 309 |
+
if all(x is not None for x in [prev_close, last_close, pct_change]):
|
| 310 |
+
results.append({
|
| 311 |
+
'symbol': ticker,
|
| 312 |
+
'prev_close': round(prev_close, 2),
|
| 313 |
+
'last_close': round(last_close, 2),
|
| 314 |
+
'pct_change': round(pct_change, 2)
|
| 315 |
+
})
|
| 316 |
+
except Exception as e:
|
| 317 |
+
print(f"⚠️ Error processing {ticker}: {e}")
|
| 318 |
+
|
| 319 |
+
if not results:
|
| 320 |
+
return jsonify({"gainers": [], "losers": [], "error": "No price data available"}), 200
|
| 321 |
+
|
| 322 |
+
results_df = pd.DataFrame(results)
|
| 323 |
+
top_gainers = results_df.sort_values(by='pct_change', ascending=False).head(10).to_dict(orient='records')
|
| 324 |
+
top_losers = results_df.sort_values(by='pct_change', ascending=True).head(10).to_dict(orient='records')
|
| 325 |
+
|
| 326 |
+
return jsonify({"gainers": top_gainers, "losers": top_losers})
|
| 327 |
+
except Exception as e:
|
| 328 |
+
print("Market Movers API ERROR:", e)
|
| 329 |
+
return jsonify({"error": str(e)}), 500
|
| 330 |
+
|
| 331 |
+
results_df = pd.DataFrame(results)
|
| 332 |
+
top_gainers = results_df.sort_values(by='pct_change', ascending=False).head(5).to_dict(orient='records')
|
| 333 |
+
top_losers = results_df.sort_values(by='pct_change', ascending=True).head(5).to_dict(orient='records')
|
| 334 |
+
|
| 335 |
+
return jsonify({"gainers": top_gainers, "losers": top_losers})
|
| 336 |
+
except Exception as e:
|
| 337 |
+
print("Market Movers API ERROR:", e)
|
| 338 |
+
return jsonify({"error": str(e)}), 500
|
| 339 |
+
|
| 340 |
+
@app.route('/api/price')
|
| 341 |
+
def get_price():
|
| 342 |
+
ticker = request.args.get('ticker')
|
| 343 |
+
if not ticker:
|
| 344 |
+
return jsonify({'error': 'No ticker provided'}), 400
|
| 345 |
+
try:
|
| 346 |
+
stock = yf.Ticker(ticker)
|
| 347 |
+
price = stock.history(period="1d")['Close'][-1]
|
| 348 |
+
return jsonify({'price': round(float(price), 2)})
|
| 349 |
+
except Exception as e:
|
| 350 |
+
return jsonify({'error': f'Could not fetch price for {ticker}'}), 500
|
| 351 |
+
|
| 352 |
+
@app.errorhandler(404)
|
| 353 |
+
def page_not_found(e):
|
| 354 |
+
return render_template('404.html'), 404
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
@app.route('/api/auth/login', methods=['POST'])
|
| 358 |
+
def auth_login():
|
| 359 |
+
data = request.get_json()
|
| 360 |
+
|
| 361 |
+
# In a real app, you'd verify the Firebase token here
|
| 362 |
+
# token = data.get('token')
|
| 363 |
+
# user_data = verify_firebase_token(token)
|
| 364 |
+
# if not user_data:
|
| 365 |
+
# return jsonify({'success': False, 'message': 'Invalid token'}), 401
|
| 366 |
+
|
| 367 |
+
# Set session variable to mark user as logged in
|
| 368 |
+
session['user_logged_in'] = True
|
| 369 |
+
# You can store additional user info in session if needed
|
| 370 |
+
# session['user_id'] = data.get('uid')
|
| 371 |
+
# session['email'] = data.get('email')
|
| 372 |
+
|
| 373 |
+
return jsonify({'success': True})
|
| 374 |
+
|
| 375 |
+
# API route for logout
|
| 376 |
+
@app.route('/api/auth/logout', methods=['POST'])
|
| 377 |
+
def auth_logout():
|
| 378 |
+
# Clear session
|
| 379 |
+
session.pop('user_id', None)
|
| 380 |
+
session.clear()
|
| 381 |
+
redirect(url_for('home'))
|
| 382 |
+
return jsonify({'success': True})
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# Error handlers
|
| 387 |
+
@app.errorhandler(404)
|
| 388 |
+
def page_not_found(e):
|
| 389 |
+
return render_template('404.html'), 404
|
| 390 |
+
|
| 391 |
+
@app.errorhandler(500)
|
| 392 |
+
def server_error(e):
|
| 393 |
+
return render_template('500.html'), 500
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
if __name__ == '__main__':
|
| 397 |
+
app.run(debug=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask
|
| 2 |
+
requests
|
| 3 |
+
pymongo
|
| 4 |
+
firebase-admin
|
| 5 |
+
python-dotenv
|
| 6 |
+
yfinance
|
| 7 |
+
numpy
|
| 8 |
+
pandas
|
| 9 |
+
scikit-learn
|
| 10 |
+
tensorflow
|
| 11 |
+
gunicorn
|
| 12 |
+
transformers
|
| 13 |
+
torch
|
static/css/fundamentals.css
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
nav ul {
|
| 2 |
+
list-style: none;
|
| 3 |
+
display: flex;
|
| 4 |
+
background-color: #333;
|
| 5 |
+
padding: 0;
|
| 6 |
+
}
|
| 7 |
+
nav ul li {
|
| 8 |
+
margin: 0 10px;
|
| 9 |
+
}
|
| 10 |
+
nav ul li a {
|
| 11 |
+
color: white;
|
| 12 |
+
text-decoration: none;
|
| 13 |
+
padding: 10px 15px;
|
| 14 |
+
display: block;
|
| 15 |
+
}
|
| 16 |
+
nav ul li a:hover {
|
| 17 |
+
background-color: #555;
|
| 18 |
+
}
|
| 19 |
+
/* Example: Start of your CSS */
|
| 20 |
+
* {
|
| 21 |
+
margin: 0;
|
| 22 |
+
padding: 0;
|
| 23 |
+
box-sizing: border-box;
|
| 24 |
+
}
|
| 25 |
+
body {
|
| 26 |
+
margin: 0;
|
| 27 |
+
font-family: Arial, sans-serif;
|
| 28 |
+
background-color: #f4f2ec;
|
| 29 |
+
color: #000;
|
| 30 |
+
transition: background-color 0.2s, color 0.2s;
|
| 31 |
+
opacity: 0;
|
| 32 |
+
transform: scale(1.05);
|
| 33 |
+
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out;
|
| 34 |
+
|
| 35 |
+
}
|
| 36 |
+
body.loaded {
|
| 37 |
+
opacity: 1;
|
| 38 |
+
transform: scale(1);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* Transition Overlay (Expanding Circle Effect) */
|
| 42 |
+
.transition-overlay {
|
| 43 |
+
position: fixed;
|
| 44 |
+
top: 50%;
|
| 45 |
+
left: 50%;
|
| 46 |
+
width: 100px;
|
| 47 |
+
height: 100px;
|
| 48 |
+
background: radial-gradient(circle, #4a8fdf, #000); /* Green fading into black */
|
| 49 |
+
border-radius: 50%;
|
| 50 |
+
transform: translate(-50%, -50%) scale(0);
|
| 51 |
+
transition: transform 0.7s ease-in-out;
|
| 52 |
+
z-index: 9999;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* Expand circle effect when navigating */
|
| 56 |
+
.transition-overlay.active {
|
| 57 |
+
transform: translate(-50%, -50%) scale(30);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* Smooth transition for links */
|
| 61 |
+
a {
|
| 62 |
+
text-decoration: none;
|
| 63 |
+
color: #4a8fdf;
|
| 64 |
+
transition: color 0.3s ease-in-out;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
a:hover {
|
| 68 |
+
color: #fff;
|
| 69 |
+
}
|
| 70 |
+
.dark-mode {
|
| 71 |
+
background-color: #131b2b;
|
| 72 |
+
color: #fff;
|
| 73 |
+
}
|
| 74 |
+
.navbar {
|
| 75 |
+
display: flex;
|
| 76 |
+
justify-content: space-between;
|
| 77 |
+
align-items: center;
|
| 78 |
+
padding: 20px 30px;
|
| 79 |
+
background: #f4f2ec;
|
| 80 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 81 |
+
font-size: 18px;
|
| 82 |
+
font-weight: bold;
|
| 83 |
+
position: sticky;
|
| 84 |
+
top: 0;
|
| 85 |
+
z-index: 1000;
|
| 86 |
+
}
|
| 87 |
+
.dark-mode .navbar {
|
| 88 |
+
background: #2c3e50;
|
| 89 |
+
}
|
| 90 |
+
.navbar a {
|
| 91 |
+
text-decoration: none;
|
| 92 |
+
color: #000;
|
| 93 |
+
margin: 0 20px;
|
| 94 |
+
font-size: 18px;
|
| 95 |
+
font-weight: bold;
|
| 96 |
+
}
|
| 97 |
+
.dark-mode .navbar a {
|
| 98 |
+
color: #fff;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.dropdown {
|
| 102 |
+
position: relative;
|
| 103 |
+
display: inline-block;
|
| 104 |
+
}
|
| 105 |
+
.dropdown-content {
|
| 106 |
+
display: none;
|
| 107 |
+
position: absolute;
|
| 108 |
+
background-color: #f9f9f9;
|
| 109 |
+
min-width: 160px;
|
| 110 |
+
box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
|
| 111 |
+
z-index: 1;
|
| 112 |
+
}
|
| 113 |
+
.dropdown-content a {
|
| 114 |
+
color: black;
|
| 115 |
+
padding: 10px 16px;
|
| 116 |
+
display: block;
|
| 117 |
+
text-decoration: none;
|
| 118 |
+
}
|
| 119 |
+
.dropdown-content a:hover {
|
| 120 |
+
background-color: #ddd;
|
| 121 |
+
}
|
| 122 |
+
.dropdown:hover .dropdown-content {
|
| 123 |
+
display: block;
|
| 124 |
+
}
|
| 125 |
+
.dark-mode .dropdown-content {
|
| 126 |
+
background-color: #2c3e50;
|
| 127 |
+
}
|
| 128 |
+
.dark-mode .dropdown-content a {
|
| 129 |
+
color: white;
|
| 130 |
+
}
|
| 131 |
+
.dark-mode .dropdown-content a:hover {
|
| 132 |
+
background-color: #374f66;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
.toggle-switch {
|
| 137 |
+
position: relative;
|
| 138 |
+
display: inline-block;
|
| 139 |
+
width: 50px;
|
| 140 |
+
height: 25px;
|
| 141 |
+
}
|
| 142 |
+
.toggle-switch input {
|
| 143 |
+
opacity: 0;
|
| 144 |
+
width: 0;
|
| 145 |
+
height: 0;
|
| 146 |
+
}
|
| 147 |
+
.slider {
|
| 148 |
+
position: absolute;
|
| 149 |
+
cursor: pointer;
|
| 150 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 151 |
+
background-color: #ccc;
|
| 152 |
+
transition: .4s;
|
| 153 |
+
border-radius: 25px;
|
| 154 |
+
}
|
| 155 |
+
.toggle-circle {
|
| 156 |
+
position: absolute;
|
| 157 |
+
left: 4px;
|
| 158 |
+
bottom: 3px;
|
| 159 |
+
width: 18px;
|
| 160 |
+
height: 18px;
|
| 161 |
+
background: #fff;
|
| 162 |
+
border-radius: 50%;
|
| 163 |
+
display: flex;
|
| 164 |
+
align-items: center;
|
| 165 |
+
justify-content: center;
|
| 166 |
+
transition: transform 0.4s;
|
| 167 |
+
z-index: 2;
|
| 168 |
+
font-size: 14px;
|
| 169 |
+
}
|
| 170 |
+
.toggle-icon {
|
| 171 |
+
transition: color 0.4s, content 0.4s;
|
| 172 |
+
color: #FFD600; /* Sun color */
|
| 173 |
+
}
|
| 174 |
+
input:checked + .slider {
|
| 175 |
+
background-color: #4a8fdf;
|
| 176 |
+
}
|
| 177 |
+
input:checked + .slider .toggle-circle {
|
| 178 |
+
transform: translateX(24px);
|
| 179 |
+
}
|
| 180 |
+
input:checked + .slider .toggle-icon {
|
| 181 |
+
color: #4a8fdf; /* Moon color */
|
| 182 |
+
/* Use content swap via JS for moon icon */
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/* Footer Styles */
|
| 186 |
+
|
| 187 |
+
footer {
|
| 188 |
+
background-color: #2c3e50;
|
| 189 |
+
color: #ecf0f1;
|
| 190 |
+
padding: 40px 20px;
|
| 191 |
+
font-family: Arial, sans-serif;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.footer-content {
|
| 195 |
+
display: flex;
|
| 196 |
+
justify-content: space-between;
|
| 197 |
+
flex-wrap: wrap;
|
| 198 |
+
gap: 20px;
|
| 199 |
+
max-width: 1200px;
|
| 200 |
+
margin: 0 auto;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.footer-section {
|
| 204 |
+
flex: 1;
|
| 205 |
+
min-width: 200px;
|
| 206 |
+
margin-bottom: 20px;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.footer-section h3 {
|
| 210 |
+
font-size: 18px;
|
| 211 |
+
margin-bottom: 15px;
|
| 212 |
+
color: #4a8fdf;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.footer-section ul {
|
| 216 |
+
list-style: none;
|
| 217 |
+
padding: 0;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.footer-section ul li {
|
| 221 |
+
margin-bottom: 10px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.footer-section ul li a {
|
| 225 |
+
color: #ecf0f1;
|
| 226 |
+
text-decoration: none;
|
| 227 |
+
transition: color 0.3s ease;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.footer-section ul li a:hover {
|
| 231 |
+
color: #4a8fdf;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.footer-section form {
|
| 235 |
+
display: flex;
|
| 236 |
+
gap: 10px;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.footer-section input[type="email"] {
|
| 240 |
+
padding: 10px;
|
| 241 |
+
border: none;
|
| 242 |
+
border-radius: 5px;
|
| 243 |
+
width: 70%;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.footer-section button {
|
| 247 |
+
padding: 10px 20px;
|
| 248 |
+
background-color: #4a8fdf;
|
| 249 |
+
color: #fff;
|
| 250 |
+
border: none;
|
| 251 |
+
border-radius: 5px;
|
| 252 |
+
cursor: pointer;
|
| 253 |
+
transition: background-color 0.3s ease;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.footer-section button:hover {
|
| 257 |
+
background-color: #4a8fdf;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.footer-bottom {
|
| 261 |
+
text-align: center;
|
| 262 |
+
margin-top: 20px;
|
| 263 |
+
padding-top: 20px;
|
| 264 |
+
border-top: 1px solid #34495e;
|
| 265 |
+
font-size: 14px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
html{
|
| 269 |
+
scroll-behavior: smooth;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.news-section {
|
| 273 |
+
padding: 60px 20px;
|
| 274 |
+
background-color: #fff;
|
| 275 |
+
margin: 40px auto;
|
| 276 |
+
max-width: 1200px;
|
| 277 |
+
border-radius: 15px;
|
| 278 |
+
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.1);
|
| 279 |
+
background: linear-gradient(135deg, #f9f9f9, #ffffff);
|
| 280 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 281 |
+
animation: glowing 1.5s infinite alternate;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.news-section h2 {
|
| 285 |
+
text-align: center;
|
| 286 |
+
margin-bottom: 40px;
|
| 287 |
+
font-size: 50px;
|
| 288 |
+
font-weight: 800;
|
| 289 |
+
text-transform: uppercase;
|
| 290 |
+
letter-spacing: 2px;
|
| 291 |
+
background: linear-gradient(90deg, #4a8fdf, #ff0000);
|
| 292 |
+
background-clip: text;
|
| 293 |
+
-webkit-background-clip: text;
|
| 294 |
+
-webkit-text-fill-color: transparent;
|
| 295 |
+
position: relative;
|
| 296 |
+
font-family: 'Montserrat', sans-serif
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.news-section h2::after {
|
| 300 |
+
content: "";
|
| 301 |
+
position: absolute;
|
| 302 |
+
bottom: -10px;
|
| 303 |
+
left: 50%;
|
| 304 |
+
transform: translateX(-50%);
|
| 305 |
+
width: 100px;
|
| 306 |
+
height: 4px;
|
| 307 |
+
background-color: #ff0000;
|
| 308 |
+
border-radius: 2px;
|
| 309 |
+
animation: underline 2s infinite;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
@keyframes underline {
|
| 313 |
+
0% {
|
| 314 |
+
width: 0;
|
| 315 |
+
}
|
| 316 |
+
50% {
|
| 317 |
+
width: 100px;
|
| 318 |
+
}
|
| 319 |
+
100% {
|
| 320 |
+
width: 0;
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
.news-container {
|
| 326 |
+
display: grid;
|
| 327 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 328 |
+
gap: 30px;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.news-article {
|
| 332 |
+
background-color: #fff;
|
| 333 |
+
padding: 25px;
|
| 334 |
+
border-radius: 15px;
|
| 335 |
+
box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.1);
|
| 336 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 337 |
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.news-article:hover {
|
| 341 |
+
transform: translateY(-10px);
|
| 342 |
+
box-shadow: 0px 12px 30px rgba(0, 0, 0, 0.15);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.news-article h3 {
|
| 346 |
+
margin: 0;
|
| 347 |
+
font-size: 22px;
|
| 348 |
+
color: #333;
|
| 349 |
+
font-weight: 600;
|
| 350 |
+
margin-bottom: 15px;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.news-article p {
|
| 354 |
+
margin: 10px 0;
|
| 355 |
+
color: #666;
|
| 356 |
+
font-size: 15px;
|
| 357 |
+
line-height: 1.6;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.news-article a {
|
| 361 |
+
color: #4a8fdf;
|
| 362 |
+
text-decoration: none;
|
| 363 |
+
font-weight: bold;
|
| 364 |
+
display: inline-block;
|
| 365 |
+
margin-top: 15px;
|
| 366 |
+
transition: color 0.3s ease;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.news-article a:hover {
|
| 370 |
+
color: #4a8fdf;
|
| 371 |
+
text-decoration: underline;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Dark Mode Styles */
|
| 375 |
+
.dark-mode .news-section {
|
| 376 |
+
background: linear-gradient(135deg, #2c3e50, #374d64);
|
| 377 |
+
border-color: rgba(44, 62, 80, 0.1);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.dark-mode .news-article {
|
| 381 |
+
background-color: #2c3e50;
|
| 382 |
+
border-color: rgba(255, 255, 255, 0.05);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.dark-mode .news-article h3 {
|
| 386 |
+
color: #ddd;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.dark-mode .news-article p {
|
| 390 |
+
color: #bbb;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.dark-mode .news-article a {
|
| 394 |
+
color: #4a8fdf;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.dark-mode .news-article a:hover {
|
| 398 |
+
color: #34495e;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
@keyframes glowing {
|
| 402 |
+
0% {
|
| 403 |
+
box-shadow: 0 0 15px rgba(1, 90, 255,0.6);
|
| 404 |
+
}
|
| 405 |
+
100% {
|
| 406 |
+
box-shadow: 0 0 30px rgb(1, 90, 255), 0 0 50px rgba(74,143,223,0.8);
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/* Loading Spinner */
|
| 411 |
+
.loading-spinner {
|
| 412 |
+
display: flex;
|
| 413 |
+
justify-content: center;
|
| 414 |
+
align-items: center;
|
| 415 |
+
height: 100px;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.loading-spinner::after {
|
| 419 |
+
content: "";
|
| 420 |
+
width: 40px;
|
| 421 |
+
height: 40px;
|
| 422 |
+
border: 4px solid #4a8fdf;
|
| 423 |
+
border-top-color: transparent;
|
| 424 |
+
border-radius: 50%;
|
| 425 |
+
animation: spin 1s linear infinite;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
@keyframes spin {
|
| 429 |
+
to {
|
| 430 |
+
transform: rotate(360deg);
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
/* Container for the entire stock data section */
|
| 435 |
+
.stock-data-container {
|
| 436 |
+
width: 90%;
|
| 437 |
+
max-width: 1200px;
|
| 438 |
+
margin: 40px auto;
|
| 439 |
+
padding: 20px;
|
| 440 |
+
background-color: #fff;
|
| 441 |
+
border-radius: 10px;
|
| 442 |
+
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
| 443 |
+
overflow: hidden;
|
| 444 |
+
border: 2px solid #4a8fdf; /* Add a solid border */
|
| 445 |
+
animation: glowBorder 2s infinite alternate; /* Apply glow animation */
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Center the date range filter */
|
| 449 |
+
.date-range-filter {
|
| 450 |
+
display: flex;
|
| 451 |
+
justify-content: center;
|
| 452 |
+
align-items: center;
|
| 453 |
+
gap: 10px;
|
| 454 |
+
margin-bottom: 20px;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.date-range-filter label {
|
| 458 |
+
font-weight: bold;
|
| 459 |
+
color: #333;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.date-range-filter input {
|
| 463 |
+
padding: 8px;
|
| 464 |
+
border: 1px solid #ddd;
|
| 465 |
+
border-radius: 5px;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.date-range-filter button {
|
| 469 |
+
padding: 8px 16px;
|
| 470 |
+
background-color: #4a8fdf;
|
| 471 |
+
color: white;
|
| 472 |
+
border: none;
|
| 473 |
+
border-radius: 5px;
|
| 474 |
+
cursor: pointer;
|
| 475 |
+
transition: background-color 0.3s ease;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.date-range-filter button:hover {
|
| 479 |
+
background-color: #4a8fdf;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
/* Search bar styling */
|
| 483 |
+
#search-bar {
|
| 484 |
+
padding: 8px;
|
| 485 |
+
border: 1px solid #ddd;
|
| 486 |
+
border-radius: 5px;
|
| 487 |
+
width: 200px;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
/* Animation for table */
|
| 491 |
+
@keyframes slideInFromTop {
|
| 492 |
+
0% {
|
| 493 |
+
transform: translateY(-100%);
|
| 494 |
+
opacity: 0;
|
| 495 |
+
}
|
| 496 |
+
100% {
|
| 497 |
+
transform: translateY(0);
|
| 498 |
+
opacity: 1;
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
/* Table styling */
|
| 503 |
+
table {
|
| 504 |
+
width: 100%;
|
| 505 |
+
border-collapse: collapse;
|
| 506 |
+
margin-top: 20px;
|
| 507 |
+
animation: slideInFromTop 0.8s ease-out forwards;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
th, td {
|
| 511 |
+
padding: 10px;
|
| 512 |
+
text-align: center;
|
| 513 |
+
border: 1px solid #ddd;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
th {
|
| 517 |
+
background-color: #4a8fdf;
|
| 518 |
+
color: white;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
tr:nth-child(even) {
|
| 522 |
+
background-color: #f9f9f9;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
tr:hover {
|
| 526 |
+
background-color: #f1f1f1;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
/* Dark mode adjustments */
|
| 530 |
+
.dark-mode .stock-data-container {
|
| 531 |
+
background-color: #131b2b;
|
| 532 |
+
color: #fff;
|
| 533 |
+
border-color: #4a8fdf; /* Keep the border color consistent */
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.dark-mode .date-range-filter label {
|
| 537 |
+
color: #ddd;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.dark-mode .date-range-filter input {
|
| 541 |
+
background-color: #243342;
|
| 542 |
+
color: #fff;
|
| 543 |
+
border-color: #555;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.dark-mode .date-range-filter button {
|
| 547 |
+
background-color: #4a8fdf;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.dark-mode .date-range-filter button:hover {
|
| 551 |
+
background-color: #4a8fdf;
|
| 552 |
+
}
|
| 553 |
+
.date-range-filter input[type="date"] {
|
| 554 |
+
padding: 12px 15px;
|
| 555 |
+
border: 2px solid #ddd;
|
| 556 |
+
border-radius: 8px;
|
| 557 |
+
font-size: 16px;
|
| 558 |
+
background-color: #f4f2ec;
|
| 559 |
+
color: #000;
|
| 560 |
+
width: 250px;
|
| 561 |
+
height: 44px; /* Optional: for consistent height with dropdown */
|
| 562 |
+
margin-right: 10px;
|
| 563 |
+
box-sizing: border-box;
|
| 564 |
+
transition: border 0.3s, background-color 0.3s;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.date-range-filter input[type="date"]:focus {
|
| 568 |
+
border-color: #4a8fdf;
|
| 569 |
+
outline: none;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.dark-mode .date-range-filter input[type="date"] {
|
| 573 |
+
background-color: #243342;
|
| 574 |
+
color: #fff;
|
| 575 |
+
border-color: #555;
|
| 576 |
+
}
|
| 577 |
+
.dark-mode #search-bar {
|
| 578 |
+
background-color: #243342;
|
| 579 |
+
color: #fff;
|
| 580 |
+
border-color: #555;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.dark-mode table {
|
| 584 |
+
border-color: #243342;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.dark-mode th {
|
| 588 |
+
background-color: #2c3e50;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
.dark-mode tr:nth-child(even) {
|
| 592 |
+
background-color: #243342;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.dark-mode tr:hover {
|
| 596 |
+
background-color: #243342;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
/* Glow effect for the container */
|
| 600 |
+
@keyframes glowBorder {
|
| 601 |
+
0% {
|
| 602 |
+
box-shadow: 0 0 10px rgba(74,143,223,0.6);
|
| 603 |
+
}
|
| 604 |
+
50% {
|
| 605 |
+
box-shadow: 0 0 20px rgba(74,143,223,0.8), 0 0 30px rgba(74,143,223,0.6);
|
| 606 |
+
}
|
| 607 |
+
100% {
|
| 608 |
+
box-shadow: 0 0 10px rgba(74,143,223,0.6);
|
| 609 |
+
}
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
.stock-container {
|
| 613 |
+
max-width: 600px;
|
| 614 |
+
margin: 0 auto;
|
| 615 |
+
padding: 20px;
|
| 616 |
+
border: 1px solid #ddd;
|
| 617 |
+
border-radius: 8px;
|
| 618 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 619 |
+
}
|
| 620 |
+
h1 {
|
| 621 |
+
text-align: center;
|
| 622 |
+
color: #333;
|
| 623 |
+
}
|
| 624 |
+
.ticker-input {
|
| 625 |
+
width: 100%;
|
| 626 |
+
padding: 10px;
|
| 627 |
+
margin-bottom: 20px;
|
| 628 |
+
border: 1px solid #ccc;
|
| 629 |
+
border-radius: 4px;
|
| 630 |
+
}
|
| 631 |
+
.fetch-btn {
|
| 632 |
+
background-color: #4a8fdf;
|
| 633 |
+
color: rgb(0, 0, 0);
|
| 634 |
+
padding: 10px 15px;
|
| 635 |
+
border: none;
|
| 636 |
+
border-radius: 4px;
|
| 637 |
+
cursor: pointer;
|
| 638 |
+
font-size: 16px;
|
| 639 |
+
}
|
| 640 |
+
.fetch-btn:hover {
|
| 641 |
+
background-color: #4a8fdf;
|
| 642 |
+
}
|
| 643 |
+
.ratios-table {
|
| 644 |
+
width: 100%;
|
| 645 |
+
border-collapse: collapse;
|
| 646 |
+
margin-top: 20px;
|
| 647 |
+
}
|
| 648 |
+
.ratios-table th, .ratios-table td {
|
| 649 |
+
border: 1px solid #ddd;
|
| 650 |
+
padding: 8px;
|
| 651 |
+
text-align: left;
|
| 652 |
+
}
|
| 653 |
+
.ratios-table th {
|
| 654 |
+
background-color: #000000;
|
| 655 |
+
}
|
| 656 |
+
.ratios-table tr:nth-child(even) {
|
| 657 |
+
background-color: #000000;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
.loading-spinner {
|
| 661 |
+
display: inline-block;
|
| 662 |
+
width: 20px;
|
| 663 |
+
height: 20px;
|
| 664 |
+
border: 3px solid rgba(76, 175, 80, 0.3);
|
| 665 |
+
border-radius: 50%;
|
| 666 |
+
border-top-color: #4a8fdf;
|
| 667 |
+
animation: spin 1s ease-in-out infinite;
|
| 668 |
+
margin-right: 10px;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
@keyframes spin {
|
| 672 |
+
to { transform: rotate(360deg); }
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.ratios-table td {
|
| 676 |
+
vertical-align: middle;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.chart-container {
|
| 680 |
+
width: 100%;
|
| 681 |
+
height: 400px;
|
| 682 |
+
max-width: 100%;
|
| 683 |
+
min-width: 200px;
|
| 684 |
+
margin: 0 auto 30px auto;
|
| 685 |
+
padding: 0;
|
| 686 |
+
box-sizing: border-box;
|
| 687 |
+
display: flex;
|
| 688 |
+
align-items: center;
|
| 689 |
+
justify-content: center;
|
| 690 |
+
background: transparent;
|
| 691 |
+
}
|
| 692 |
+
#stockChart {
|
| 693 |
+
width: 100% !important;
|
| 694 |
+
height: 100% !important;
|
| 695 |
+
max-width: 100%;
|
| 696 |
+
margin: 0 auto;
|
| 697 |
+
}
|
| 698 |
+
.chart-title {
|
| 699 |
+
text-align: center;
|
| 700 |
+
margin: 20px 0;
|
| 701 |
+
font-size: 1.5rem;
|
| 702 |
+
color: #333;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.dark-mode .chart-title {
|
| 706 |
+
color: #3a4b5c;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.tab-container {
|
| 710 |
+
display: flex;
|
| 711 |
+
margin-bottom: 20px;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.tab {
|
| 715 |
+
padding: 10px 20px;
|
| 716 |
+
cursor: pointer;
|
| 717 |
+
background-color: #f1f1f1;
|
| 718 |
+
border: none;
|
| 719 |
+
margin-right: 5px;
|
| 720 |
+
border-radius: 5px 5px 0 0;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
.tab.active {
|
| 724 |
+
background-color: #4a8fdf;
|
| 725 |
+
color: white;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
.dark-mode .tab {
|
| 729 |
+
background-color: #2c3e50;
|
| 730 |
+
color: #fff;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.dark-mode .tab.active {
|
| 734 |
+
background-color: #4a8fdf;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
/* Fundamentals Section Styles */
|
| 738 |
+
.fundamentals-container {
|
| 739 |
+
max-width: 1200px;
|
| 740 |
+
margin: 40px auto;
|
| 741 |
+
padding: 30px;
|
| 742 |
+
background-color: #fff;
|
| 743 |
+
border-radius: 15px;
|
| 744 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
| 745 |
+
border: 2px solid #4a8fdf;
|
| 746 |
+
animation: glowBorder 2s infinite alternate;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.fundamentals-container h2 {
|
| 750 |
+
text-align: center;
|
| 751 |
+
margin-bottom: 30px;
|
| 752 |
+
color: #333;
|
| 753 |
+
font-size: 28px;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.fundamentals-tabs {
|
| 757 |
+
display: flex;
|
| 758 |
+
justify-content: center;
|
| 759 |
+
margin-bottom: 30px;
|
| 760 |
+
border-bottom: 1px solid #ddd;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
.tab-button {
|
| 764 |
+
padding: 12px 24px;
|
| 765 |
+
background: none;
|
| 766 |
+
border: none;
|
| 767 |
+
cursor: pointer;
|
| 768 |
+
font-size: 16px;
|
| 769 |
+
font-weight: bold;
|
| 770 |
+
color: #666;
|
| 771 |
+
position: relative;
|
| 772 |
+
transition: all 0.3s ease;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.tab-button.active {
|
| 776 |
+
color: #4a8fdf;
|
| 777 |
+
}@media (max-width: 700px) {
|
| 778 |
+
.chart-container {
|
| 779 |
+
height: 250px;
|
| 780 |
+
}
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
.tab-button.active::after {
|
| 784 |
+
content: '';
|
| 785 |
+
position: absolute;
|
| 786 |
+
bottom: -1px;
|
| 787 |
+
left: 0;
|
| 788 |
+
width: 100%;
|
| 789 |
+
height: 3px;
|
| 790 |
+
background-color: #4a8fdf;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.tab-content {
|
| 794 |
+
display: none;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.fundamentals-grid {
|
| 798 |
+
display: grid;
|
| 799 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 800 |
+
gap: 20px;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
.fundamental-card {
|
| 804 |
+
background-color: #f9f9f9;
|
| 805 |
+
padding: 20px;
|
| 806 |
+
border-radius: 10px;
|
| 807 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.fundamental-card h3 {
|
| 811 |
+
margin-top: 0;
|
| 812 |
+
margin-bottom: 15px;
|
| 813 |
+
color: #333;
|
| 814 |
+
font-size: 18px;
|
| 815 |
+
border-bottom: 1px solid #ddd;
|
| 816 |
+
padding-bottom: 10px;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
.ratio-item {
|
| 820 |
+
display: flex;
|
| 821 |
+
justify-content: space-between;
|
| 822 |
+
margin-bottom: 10px;
|
| 823 |
+
padding: 8px 0;
|
| 824 |
+
border-bottom: 1px dashed #eee;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
.ratio-name {
|
| 828 |
+
font-weight: 600;
|
| 829 |
+
color: #555;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.ratio-value {
|
| 833 |
+
font-weight: bold;
|
| 834 |
+
color: #333;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.chart-container, .comparison-chart-container {
|
| 838 |
+
height: 400px;
|
| 839 |
+
margin-top: 20px;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
.comparison-controls {
|
| 843 |
+
display: flex;
|
| 844 |
+
gap: 10px;
|
| 845 |
+
margin-bottom: 20px;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
.comparison-controls input {
|
| 849 |
+
padding: 8px 12px;
|
| 850 |
+
border: 1px solid #ddd;
|
| 851 |
+
border-radius: 4px;
|
| 852 |
+
flex-grow: 1;
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.comparison-controls button {
|
| 856 |
+
padding: 8px 16px;
|
| 857 |
+
background-color: #4a8fdf;
|
| 858 |
+
color: white;
|
| 859 |
+
border: none;
|
| 860 |
+
border-radius: 4px;
|
| 861 |
+
cursor: pointer;
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
#peer-list {
|
| 865 |
+
margin-bottom: 20px;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.peer-item {
|
| 869 |
+
display: inline-block;
|
| 870 |
+
background-color: #f0f0f0;
|
| 871 |
+
padding: 5px 10px;
|
| 872 |
+
margin-right: 10px;
|
| 873 |
+
margin-bottom: 10px;
|
| 874 |
+
border-radius: 4px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
/* Dark mode styles */
|
| 878 |
+
.dark-mode .fundamentals-container {
|
| 879 |
+
background-color: #131b2b;
|
| 880 |
+
color: #fff;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
.dark-mode .fundamentals-container h2 {
|
| 884 |
+
color: #fff;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
.dark-mode .fundamental-card {
|
| 888 |
+
background-color: #243342;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
.dark-mode .fundamental-card h3 {
|
| 892 |
+
color: #fff;
|
| 893 |
+
border-bottom-color: #2c3e50;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
.dark-mode .ratio-name {
|
| 897 |
+
color: #bbb;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
.dark-mode .ratio-value {
|
| 901 |
+
color: #fff;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
.dark-mode .tab-button {
|
| 905 |
+
color: #bbb;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
.dark-mode .tab-button.active {
|
| 909 |
+
color: #4a8fdf;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.dark-mode .comparison-controls input {
|
| 913 |
+
background-color: #243342;
|
| 914 |
+
color: #fff;
|
| 915 |
+
border-color: #555;
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
/* White Glowing Curve Effect */
|
| 919 |
+
.shiny-curve {
|
| 920 |
+
position: fixed;
|
| 921 |
+
bottom: 0;
|
| 922 |
+
left: 0;
|
| 923 |
+
width: 100%;
|
| 924 |
+
height: 80px; /* Reduced height for subtlety */
|
| 925 |
+
pointer-events: none;
|
| 926 |
+
z-index: 100;
|
| 927 |
+
display: none;
|
| 928 |
+
overflow: hidden;
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
.dark-mode .shiny-curve {
|
| 932 |
+
display: block;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
.shiny-curve::before {
|
| 936 |
+
content: '';
|
| 937 |
+
position: absolute;
|
| 938 |
+
bottom: 0;
|
| 939 |
+
left: 0;
|
| 940 |
+
width: 100%;
|
| 941 |
+
height: 100%;
|
| 942 |
+
background: linear-gradient(to top,
|
| 943 |
+
rgba(10, 10, 10, 0.7) 0%,
|
| 944 |
+
rgba(255, 255, 255, 0.5) 50%,
|
| 945 |
+
rgba(255, 255, 255, 0) 100%);
|
| 946 |
+
-webkit-mask-image: radial-gradient(ellipse 80% 60px at 50% 100%, black 60%, transparent 65%);
|
| 947 |
+
mask-image: radial-gradient(ellipse 80% 60px at 50% 100%, black 60%, transparent 65%);
|
| 948 |
+
animation: white-glow 3s infinite alternate;
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
.shiny-curve::after {
|
| 952 |
+
content: '';
|
| 953 |
+
position: absolute;
|
| 954 |
+
bottom: 15px;
|
| 955 |
+
left: 0;
|
| 956 |
+
width: 100%;
|
| 957 |
+
height: 1px;
|
| 958 |
+
background: rgba(255, 255, 255, 0.8);
|
| 959 |
+
border-radius: 100%;
|
| 960 |
+
filter: blur(15px);
|
| 961 |
+
animation: white-line-glow 3s infinite alternate;
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
@keyframes white-glow {
|
| 965 |
+
0% {
|
| 966 |
+
opacity: 0.7;
|
| 967 |
+
background: linear-gradient(to top,
|
| 968 |
+
rgba(10, 10, 10, 0.7) 0%,
|
| 969 |
+
rgba(255, 255, 255, 0.4) 50%,
|
| 970 |
+
rgba(255, 255, 255, 0) 100%);
|
| 971 |
+
}
|
| 972 |
+
100% {
|
| 973 |
+
opacity: 1;
|
| 974 |
+
background: linear-gradient(to top,
|
| 975 |
+
rgba(10, 10, 10, 0.7) 0%,
|
| 976 |
+
rgba(255, 255, 255, 0.6) 60%,
|
| 977 |
+
rgba(255, 255, 255, 0) 100%);
|
| 978 |
+
}
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
@keyframes white-line-glow {
|
| 982 |
+
0% {
|
| 983 |
+
opacity: 0.6;
|
| 984 |
+
filter: blur(12px);
|
| 985 |
+
}
|
| 986 |
+
100% {
|
| 987 |
+
opacity: 0.9;
|
| 988 |
+
filter: blur(18px);
|
| 989 |
+
}
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
.fundamentals-search {
|
| 993 |
+
display: flex;
|
| 994 |
+
justify-content: center;
|
| 995 |
+
gap: 10px;
|
| 996 |
+
margin-bottom: 30px;
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
.fundamentals-search input {
|
| 1000 |
+
padding: 10px 15px;
|
| 1001 |
+
border: 1px solid #ddd;
|
| 1002 |
+
border-radius: 5px;
|
| 1003 |
+
width: 300px;
|
| 1004 |
+
font-size: 16px;
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
.fundamentals-search button {
|
| 1008 |
+
padding: 10px 20px;
|
| 1009 |
+
background-color: #4a8fdf;
|
| 1010 |
+
color: white;
|
| 1011 |
+
border: none;
|
| 1012 |
+
border-radius: 5px;
|
| 1013 |
+
cursor: pointer;
|
| 1014 |
+
transition: background-color 0.3s ease;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
.fundamentals-search button:hover {
|
| 1018 |
+
background-color: #4a8fdf;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.dark-mode .fundamentals-search input {
|
| 1022 |
+
background-color: #243342;
|
| 1023 |
+
color: #fff;
|
| 1024 |
+
border-color: #555;
|
| 1025 |
+
}
|
| 1026 |
+
#fundamentals-symbol,
|
| 1027 |
+
#fundamentals-search-bar.styled-dropdown {
|
| 1028 |
+
padding: 12px 15px;
|
| 1029 |
+
border: 2px solid #ddd;
|
| 1030 |
+
border-radius: 8px;
|
| 1031 |
+
font-size: 16px;
|
| 1032 |
+
background-color: #fff; /* <-- Make it pure white in light mode */
|
| 1033 |
+
color: #000;
|
| 1034 |
+
transition: border 0.3s, background-color 0.3s;
|
| 1035 |
+
width: 250px;
|
| 1036 |
+
margin-right: 10px;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
#fundamentals-symbol,
|
| 1040 |
+
#fundamentals-search-bar.styled-dropdown:focus {
|
| 1041 |
+
border-color: #4a8fdf;
|
| 1042 |
+
outline: none;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
.dark-mode #fundamentals-symbol,
|
| 1046 |
+
.dark-mode #fundamentals-search-bar.styled-dropdown {
|
| 1047 |
+
background-color: #243342;
|
| 1048 |
+
color: #fff;
|
| 1049 |
+
border-color: #555;
|
| 1050 |
+
}
|
| 1051 |
+
#fetch {
|
| 1052 |
+
padding: 10px 20px;
|
| 1053 |
+
background-color: #4a8fdf;
|
| 1054 |
+
color: white;
|
| 1055 |
+
border: none;
|
| 1056 |
+
border-radius: 5px;
|
| 1057 |
+
cursor: pointer;
|
| 1058 |
+
font-size: 16px;
|
| 1059 |
+
transition: background-color 0.3s ease;
|
| 1060 |
+
margin-left: 10px; /* Optional: space between inputs and button */
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
#fetch:hover {
|
| 1064 |
+
background-color: #357abd;
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
.dark-mode #fetch {
|
| 1068 |
+
background-color: #4a8fdf;
|
| 1069 |
+
color: #fff;
|
| 1070 |
+
border-color: #555;
|
| 1071 |
+
}
|
| 1072 |
+
.chart-container > div,
|
| 1073 |
+
#stockChart,
|
| 1074 |
+
#fundamentals-chart,
|
| 1075 |
+
#risk-metrics-chart,
|
| 1076 |
+
#rolling-vol-chart,
|
| 1077 |
+
#beta-gauge-chart {
|
| 1078 |
+
width: 100% !important;
|
| 1079 |
+
height: 100% !important;
|
| 1080 |
+
min-height: 200px;
|
| 1081 |
+
max-width: 100%;
|
| 1082 |
+
margin: 0;
|
| 1083 |
+
padding: 0;
|
| 1084 |
+
box-sizing: border-box;
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
@media (max-width: 700px) {
|
| 1088 |
+
.chart-container {
|
| 1089 |
+
height: 250px;
|
| 1090 |
+
}
|
| 1091 |
+
}
|
| 1092 |
+
/* Make the stock data table scrollable after 400px */
|
| 1093 |
+
#stock-data-table {
|
| 1094 |
+
max-height: 400px;
|
| 1095 |
+
overflow-y: auto;
|
| 1096 |
+
display: block;
|
| 1097 |
+
}
|
| 1098 |
+
/* Light mode scrollbar */
|
| 1099 |
+
#stock-data-table {
|
| 1100 |
+
scrollbar-width: thin;
|
| 1101 |
+
scrollbar-color: #4a8fdf #e0e7ef;
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
/* Chrome, Edge, Safari */
|
| 1105 |
+
#stock-data-table::-webkit-scrollbar {
|
| 1106 |
+
width: 10px;
|
| 1107 |
+
background: #e0e7ef;
|
| 1108 |
+
}
|
| 1109 |
+
#stock-data-table::-webkit-scrollbar-thumb {
|
| 1110 |
+
background: #4a8fdf;
|
| 1111 |
+
border-radius: 8px;
|
| 1112 |
+
}
|
| 1113 |
+
#stock-data-table::-webkit-scrollbar-thumb:hover {
|
| 1114 |
+
background: #357abd;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
/* Dark mode scrollbar */
|
| 1118 |
+
.dark-mode #stock-data-table {
|
| 1119 |
+
scrollbar-color: #4a8fdf #243342;
|
| 1120 |
+
}
|
| 1121 |
+
.dark-mode #stock-data-table::-webkit-scrollbar {
|
| 1122 |
+
background: #243342;
|
| 1123 |
+
}
|
| 1124 |
+
.dark-mode #stock-data-table::-webkit-scrollbar-thumb {
|
| 1125 |
+
background: #4a8fdf;
|
| 1126 |
+
}
|
| 1127 |
+
.dark-mode #stock-data-table::-webkit-scrollbar-thumb:hover {
|
| 1128 |
+
background: #357abd;
|
| 1129 |
+
}
|
| 1130 |
+
/* Light mode date input */
|
| 1131 |
+
input[type="date"] {
|
| 1132 |
+
background-color: #fff;
|
| 1133 |
+
color: #000;
|
| 1134 |
+
border: 2px solid #4a8fdf;
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
/* Dark mode date input */
|
| 1138 |
+
.dark-mode input[type="date"] {
|
| 1139 |
+
background-color: #243342;
|
| 1140 |
+
color: #fff;
|
| 1141 |
+
border: 2px solid #4a8fdf;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
/* Calendar icon color for Chrome */
|
| 1145 |
+
input[type="date"]::-webkit-calendar-picker-indicator {
|
| 1146 |
+
filter: invert(36%) sepia(91%) saturate(749%) hue-rotate(183deg) brightness(95%) contrast(92%);
|
| 1147 |
+
/* This makes the icon blue-ish to match #4a8fdf */
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
/* Calendar icon color for dark mode */
|
| 1151 |
+
.dark-mode input[type="date"]::-webkit-calendar-picker-indicator {
|
| 1152 |
+
filter: invert(70%) sepia(13%) saturate(749%) hue-rotate(183deg) brightness(95%) contrast(92%);
|
| 1153 |
+
}
|
| 1154 |
+
.custom-introjs-tooltip {
|
| 1155 |
+
font-size: 1.05rem;
|
| 1156 |
+
color: #222;
|
| 1157 |
+
}
|
| 1158 |
+
.introjs-tooltip {
|
| 1159 |
+
z-index: 11000 !important;
|
| 1160 |
+
}
|
static/css/legal.css
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: Arial, sans-serif;
|
| 3 |
+
background-color: #f4f2ec;
|
| 4 |
+
color: #222;
|
| 5 |
+
margin: 0;
|
| 6 |
+
transition: background 0.3s, color 0.3s;
|
| 7 |
+
}
|
| 8 |
+
.dark-mode {
|
| 9 |
+
background-color: #131b2b;
|
| 10 |
+
color: #fff;
|
| 11 |
+
}
|
| 12 |
+
.navbar {
|
| 13 |
+
display: flex;
|
| 14 |
+
justify-content: space-between;
|
| 15 |
+
align-items: center;
|
| 16 |
+
padding: 20px 30px;
|
| 17 |
+
background: #f4f2ec;
|
| 18 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 19 |
+
font-size: 18px;
|
| 20 |
+
font-weight: bold;
|
| 21 |
+
position: sticky;
|
| 22 |
+
top: 0;
|
| 23 |
+
z-index: 1000;
|
| 24 |
+
}
|
| 25 |
+
.dark-mode .navbar {
|
| 26 |
+
background: #243342;
|
| 27 |
+
}
|
| 28 |
+
.navbar a {
|
| 29 |
+
text-decoration: none;
|
| 30 |
+
color: #000;
|
| 31 |
+
margin: 0 20px;
|
| 32 |
+
font-size: 18px;
|
| 33 |
+
font-weight: bold;
|
| 34 |
+
transition: color 0.3s;
|
| 35 |
+
}
|
| 36 |
+
.dark-mode .navbar a {
|
| 37 |
+
color: #fff;
|
| 38 |
+
}
|
| 39 |
+
.legal-container {
|
| 40 |
+
max-width: 800px;
|
| 41 |
+
margin: 60px auto 40px auto;
|
| 42 |
+
background: #fff;
|
| 43 |
+
border-radius: 12px;
|
| 44 |
+
box-shadow: 0 0 20px #4a8fdf33;
|
| 45 |
+
padding: 40px 30px;
|
| 46 |
+
animation: fadeIn 1s;
|
| 47 |
+
}
|
| 48 |
+
.dark-mode .legal-container {
|
| 49 |
+
background: #1a2436;
|
| 50 |
+
color: #fff;
|
| 51 |
+
box-shadow: 0 0 30px #4a8fdf, 0 0 60px #4a8fdf33;
|
| 52 |
+
}
|
| 53 |
+
.legal-container h1 {
|
| 54 |
+
color: #4a8fdf;
|
| 55 |
+
margin-bottom: 20px;
|
| 56 |
+
}
|
| 57 |
+
.legal-container h2 {
|
| 58 |
+
color: #007bff;
|
| 59 |
+
margin-top: 30px;
|
| 60 |
+
}
|
| 61 |
+
.legal-container ul {
|
| 62 |
+
margin-left: 20px;
|
| 63 |
+
margin-bottom: 20px;
|
| 64 |
+
}
|
| 65 |
+
.legal-container a {
|
| 66 |
+
color: #4a8fdf;
|
| 67 |
+
text-decoration: underline;
|
| 68 |
+
}
|
| 69 |
+
.legal-container a:hover {
|
| 70 |
+
color: #007bff;
|
| 71 |
+
}
|
| 72 |
+
footer {
|
| 73 |
+
background-color: #2c3e50;
|
| 74 |
+
color: #ecf0f1;
|
| 75 |
+
padding: 30px 10px 10px 10px;
|
| 76 |
+
text-align: center;
|
| 77 |
+
font-size: 14px;
|
| 78 |
+
margin-top: 40px;
|
| 79 |
+
}
|
| 80 |
+
.toggle-switch {
|
| 81 |
+
position: relative;
|
| 82 |
+
display: inline-block;
|
| 83 |
+
width: 50px;
|
| 84 |
+
height: 25px;
|
| 85 |
+
margin-left: 20px;
|
| 86 |
+
}
|
| 87 |
+
.toggle-switch input {
|
| 88 |
+
opacity: 0;
|
| 89 |
+
width: 0;
|
| 90 |
+
height: 0;
|
| 91 |
+
}
|
| 92 |
+
.slider {
|
| 93 |
+
position: absolute;
|
| 94 |
+
cursor: pointer;
|
| 95 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 96 |
+
background-color: #ccc;
|
| 97 |
+
transition: .4s;
|
| 98 |
+
border-radius: 25px;
|
| 99 |
+
}
|
| 100 |
+
.toggle-circle {
|
| 101 |
+
position: absolute;
|
| 102 |
+
left: 4px;
|
| 103 |
+
bottom: 3px;
|
| 104 |
+
width: 18px;
|
| 105 |
+
height: 18px;
|
| 106 |
+
background: #fff;
|
| 107 |
+
border-radius: 50%;
|
| 108 |
+
display: flex;
|
| 109 |
+
align-items: center;
|
| 110 |
+
justify-content: center;
|
| 111 |
+
transition: transform 0.4s;
|
| 112 |
+
z-index: 2;
|
| 113 |
+
font-size: 14px;
|
| 114 |
+
}
|
| 115 |
+
.toggle-icon {
|
| 116 |
+
transition: color 0.4s, content 0.4s;
|
| 117 |
+
color: #FFD600;
|
| 118 |
+
}
|
| 119 |
+
input:checked + .slider {
|
| 120 |
+
background-color: #4a8fdf;
|
| 121 |
+
}
|
| 122 |
+
input:checked + .slider .toggle-circle {
|
| 123 |
+
transform: translateX(24px);
|
| 124 |
+
}
|
| 125 |
+
input:checked + .slider .toggle-icon {
|
| 126 |
+
color: #4a8fdf;
|
| 127 |
+
}
|
| 128 |
+
@keyframes fadeIn {
|
| 129 |
+
from { opacity: 0; transform: translateY(20px);}
|
| 130 |
+
to { opacity: 1; transform: translateY(0);}
|
| 131 |
+
}
|
| 132 |
+
@media (max-width: 900px) {
|
| 133 |
+
.legal-container {
|
| 134 |
+
padding: 20px 5vw;
|
| 135 |
+
}
|
| 136 |
+
.navbar {
|
| 137 |
+
flex-direction: column;
|
| 138 |
+
align-items: flex-start;
|
| 139 |
+
}
|
| 140 |
+
}
|
static/css/login.css
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Global Styles */
|
| 2 |
+
body {
|
| 3 |
+
font-family: Arial, sans-serif;
|
| 4 |
+
margin: 0;
|
| 5 |
+
padding: 0;
|
| 6 |
+
background-color: #f4f2ec;
|
| 7 |
+
color: #000;
|
| 8 |
+
display: flex;
|
| 9 |
+
justify-content: center;
|
| 10 |
+
align-items: center;
|
| 11 |
+
height: 100vh;
|
| 12 |
+
transition: background-color 0.3s, color 0.3s;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.dark-mode {
|
| 16 |
+
background-color: #1c2733;
|
| 17 |
+
color: #fff;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* Container */
|
| 21 |
+
.container {
|
| 22 |
+
display: flex;
|
| 23 |
+
width: 80%;
|
| 24 |
+
max-width: 1000px;
|
| 25 |
+
background: #fff;
|
| 26 |
+
border-radius: 10px;
|
| 27 |
+
overflow: hidden;
|
| 28 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.dark-mode .container {
|
| 32 |
+
background: #243342;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Left Section with Animation */
|
| 36 |
+
.left-section {
|
| 37 |
+
width: 50%;
|
| 38 |
+
background: url('../images/bg.png') no-repeat center center/cover;
|
| 39 |
+
display: flex;
|
| 40 |
+
flex-direction: column;
|
| 41 |
+
justify-content: space-between;
|
| 42 |
+
padding: 40px;
|
| 43 |
+
text-align: center;
|
| 44 |
+
animation: dropDown 1s ease-out;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Drop Down Animation */
|
| 48 |
+
@keyframes dropDown {
|
| 49 |
+
from {
|
| 50 |
+
transform: translateY(-100%);
|
| 51 |
+
opacity: 0;
|
| 52 |
+
}
|
| 53 |
+
to {
|
| 54 |
+
transform: translateY(0);
|
| 55 |
+
opacity: 1;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* Right Section */
|
| 60 |
+
.right-section {
|
| 61 |
+
width: 50%;
|
| 62 |
+
padding: 40px;
|
| 63 |
+
display: flex;
|
| 64 |
+
flex-direction: column;
|
| 65 |
+
align-items: center;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Title */
|
| 69 |
+
.title {
|
| 70 |
+
font-size: 2rem;
|
| 71 |
+
font-weight: bold;
|
| 72 |
+
margin-bottom: 10px;
|
| 73 |
+
color: #4a8fdf;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Subtitle */
|
| 77 |
+
.subtitle a {
|
| 78 |
+
color: #4a8fdf;
|
| 79 |
+
text-decoration: none;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Form Styles */
|
| 83 |
+
form {
|
| 84 |
+
width: 100%;
|
| 85 |
+
display: flex;
|
| 86 |
+
flex-direction: column;
|
| 87 |
+
gap: 10px;
|
| 88 |
+
margin-top: 20px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.name-fields {
|
| 92 |
+
display: flex;
|
| 93 |
+
gap: 10px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* Input Fields */
|
| 97 |
+
input {
|
| 98 |
+
width: 100%;
|
| 99 |
+
padding: 10px;
|
| 100 |
+
border-radius: 5px;
|
| 101 |
+
border: 2px solid #ddd;
|
| 102 |
+
background: #fff;
|
| 103 |
+
color: #000;
|
| 104 |
+
transition: border 0.3s, background 0.3s;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
input:focus {
|
| 108 |
+
border-color: #4a8fdf;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.dark-mode input {
|
| 112 |
+
background: #2c3e50;
|
| 113 |
+
color: #fff;
|
| 114 |
+
border: 2px solid #4a8fdf;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.dark-mode input:focus {
|
| 118 |
+
background: #2c3e50; /* Keep dark background when focused in dark mode */
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
/* Submit Button */
|
| 123 |
+
.submit-btn {
|
| 124 |
+
background: #4a8fdf;
|
| 125 |
+
color: #377ddf;
|
| 126 |
+
padding: 12px;
|
| 127 |
+
border: none;
|
| 128 |
+
border-radius: 5px;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
font-weight: bold;
|
| 131 |
+
transition: background 0.3s;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.submit-btn:hover {
|
| 135 |
+
background: #377ddf;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/* Separator */
|
| 139 |
+
.separator {
|
| 140 |
+
display: flex;
|
| 141 |
+
align-items: center;
|
| 142 |
+
margin: 20px 0;
|
| 143 |
+
width: 100%;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.separator .line {
|
| 147 |
+
flex: 1;
|
| 148 |
+
height: 1px;
|
| 149 |
+
background: #aaa;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.separator span {
|
| 153 |
+
padding: 0 10px;
|
| 154 |
+
color: #888;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* Social Box */
|
| 158 |
+
.social-box {
|
| 159 |
+
display: flex;
|
| 160 |
+
flex-direction: column;
|
| 161 |
+
align-items: center;
|
| 162 |
+
background: #f0f0f0;
|
| 163 |
+
padding: 15px;
|
| 164 |
+
border-radius: 10px;
|
| 165 |
+
width: 100%;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.dark-mode .social-box {
|
| 169 |
+
background: #2c3e50;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Social Buttons */
|
| 173 |
+
.social-buttons {
|
| 174 |
+
display: flex;
|
| 175 |
+
gap: 10px;
|
| 176 |
+
width: 100%;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.social-buttons button {
|
| 180 |
+
background: transparent;
|
| 181 |
+
border: 1px solid #4a8fdf;
|
| 182 |
+
padding: 10px;
|
| 183 |
+
cursor: pointer;
|
| 184 |
+
border-radius: 5px;
|
| 185 |
+
display: flex;
|
| 186 |
+
align-items: center;
|
| 187 |
+
justify-content: center;
|
| 188 |
+
gap: 10px;
|
| 189 |
+
color: #4a8fdf;
|
| 190 |
+
font-size: 14px;
|
| 191 |
+
flex: 1;
|
| 192 |
+
transition: background 0.3s;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.social-buttons button:hover {
|
| 196 |
+
background: rgba(74, 143, 223, 0.1);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.dark-mode .social-buttons button {
|
| 200 |
+
color: #fff;
|
| 201 |
+
border-color: #4a8fdf;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.dark-mode .social-buttons button:hover {
|
| 205 |
+
background: rgba(74, 143, 223, 0.2);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* Message Styling */
|
| 209 |
+
.error-message {
|
| 210 |
+
color: #e74c3c;
|
| 211 |
+
font-size: 14px;
|
| 212 |
+
margin-top: 10px;
|
| 213 |
+
text-align: center;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.success-message {
|
| 217 |
+
color: #0FFF50;
|
| 218 |
+
font-size: 14px;
|
| 219 |
+
margin-top: 10px;
|
| 220 |
+
text-align: center;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/* Toggle Link */
|
| 224 |
+
.login-toggle {
|
| 225 |
+
margin-top: 20px;
|
| 226 |
+
color: #4a8fdf;
|
| 227 |
+
cursor: pointer;
|
| 228 |
+
text-decoration: underline;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/* Spinner */
|
| 232 |
+
.spinner {
|
| 233 |
+
border: 3px solid rgba(0, 0, 0, 0.1);
|
| 234 |
+
border-radius: 50%;
|
| 235 |
+
border-top: 3px solid #4a8fdf;
|
| 236 |
+
width: 20px;
|
| 237 |
+
height: 20px;
|
| 238 |
+
animation: spin 1s linear infinite;
|
| 239 |
+
display: inline-block;
|
| 240 |
+
margin-left: 10px;
|
| 241 |
+
vertical-align: middle;
|
| 242 |
+
display: none;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
@keyframes spin {
|
| 246 |
+
0% { transform: rotate(0deg); }
|
| 247 |
+
100% { transform: rotate(360deg); }
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* Responsive Design */
|
| 251 |
+
@media (max-width: 768px) {
|
| 252 |
+
.container {
|
| 253 |
+
flex-direction: column;
|
| 254 |
+
width: 90%;
|
| 255 |
+
}
|
| 256 |
+
.left-section {
|
| 257 |
+
display: none;
|
| 258 |
+
}
|
| 259 |
+
.right-section {
|
| 260 |
+
width: 100%;
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* Toggle Switch */
|
| 265 |
+
.toggle-switch {
|
| 266 |
+
position: relative;
|
| 267 |
+
display: inline-block;
|
| 268 |
+
width: 50px;
|
| 269 |
+
height: 25px;
|
| 270 |
+
}
|
| 271 |
+
.toggle-switch input {
|
| 272 |
+
opacity: 0;
|
| 273 |
+
width: 0;
|
| 274 |
+
height: 0;
|
| 275 |
+
}
|
| 276 |
+
.slider {
|
| 277 |
+
position: absolute;
|
| 278 |
+
cursor: pointer;
|
| 279 |
+
top: 0;
|
| 280 |
+
left: 0;
|
| 281 |
+
right: 0;
|
| 282 |
+
bottom: 0;
|
| 283 |
+
background-color: #ccc;
|
| 284 |
+
transition: 0.4s;
|
| 285 |
+
border-radius: 25px;
|
| 286 |
+
}
|
| 287 |
+
.slider:before {
|
| 288 |
+
position: absolute;
|
| 289 |
+
content: "";
|
| 290 |
+
height: 18px;
|
| 291 |
+
width: 18px;
|
| 292 |
+
left: 4px;
|
| 293 |
+
bottom: 3px;
|
| 294 |
+
background-color: white;
|
| 295 |
+
transition: 0.4s;
|
| 296 |
+
border-radius: 50%;
|
| 297 |
+
}
|
| 298 |
+
input:checked + .slider {
|
| 299 |
+
background-color: #4a8fdf;
|
| 300 |
+
}
|
| 301 |
+
input:checked + .slider:before {
|
| 302 |
+
transform: translateX(24px);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
input:-webkit-autofill,
|
| 306 |
+
input:-webkit-autofill:focus,
|
| 307 |
+
input:-webkit-autofill:hover,
|
| 308 |
+
input:-webkit-autofill:active {
|
| 309 |
+
transition: background-color 5000s ease-in-out 0s;
|
| 310 |
+
color: inherit !important;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
input:-webkit-autofill {
|
| 314 |
+
-webkit-text-fill-color: #000000 !important;
|
| 315 |
+
caret-color: #fff;
|
| 316 |
+
box-shadow: 0 0 0px 1000px #fcfeff inset !important;
|
| 317 |
+
}
|
| 318 |
+
.dark-mode input:-webkit-autofill {
|
| 319 |
+
-webkit-text-fill-color: #fff !important;
|
| 320 |
+
caret-color: #fff;
|
| 321 |
+
box-shadow: 0 0 0px 1000px #4c6c8b inset !important;
|
| 322 |
+
}
|
static/css/movers.css
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&family=Montserrat:wght@400;700&display=swap');
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
|
| 3 |
+
|
| 4 |
+
nav ul {
|
| 5 |
+
list-style: none;
|
| 6 |
+
display: flex;
|
| 7 |
+
background-color: #333;
|
| 8 |
+
padding: 0;
|
| 9 |
+
}
|
| 10 |
+
nav ul li {
|
| 11 |
+
margin: 0 10px;
|
| 12 |
+
}
|
| 13 |
+
nav ul li a {
|
| 14 |
+
color: white;
|
| 15 |
+
text-decoration: none;
|
| 16 |
+
padding: 10px 15px;
|
| 17 |
+
display: block;
|
| 18 |
+
}
|
| 19 |
+
nav ul li a:hover {
|
| 20 |
+
background-color: #555;
|
| 21 |
+
}
|
| 22 |
+
/* Example: Start of your CSS */
|
| 23 |
+
* {
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
box-sizing: border-box;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
margin: 0;
|
| 31 |
+
font-family: Arial, sans-serif;
|
| 32 |
+
background-color: #f4f2ec;
|
| 33 |
+
color: #000;
|
| 34 |
+
transition: background-color 0.3s, color 0.3s;
|
| 35 |
+
opacity: 0;
|
| 36 |
+
transform: scale(1.05);
|
| 37 |
+
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out;
|
| 38 |
+
}
|
| 39 |
+
body.loaded {
|
| 40 |
+
opacity: 1;
|
| 41 |
+
transform: scale(1);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* Transition Overlay (Expanding Circle Effect) */
|
| 45 |
+
.transition-overlay {
|
| 46 |
+
position: fixed;
|
| 47 |
+
top: 50%;
|
| 48 |
+
left: 50%;
|
| 49 |
+
width: 100px;
|
| 50 |
+
height: 100px;
|
| 51 |
+
background: radial-gradient(circle, #4a8fdf, #000); /* Green fading into black */
|
| 52 |
+
border-radius: 50%;
|
| 53 |
+
transform: translate(-50%, -50%) scale(0);
|
| 54 |
+
transition: transform 0.7s ease-in-out;
|
| 55 |
+
z-index: 9999;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* Expand circle effect when navigating */
|
| 59 |
+
.transition-overlay.active {
|
| 60 |
+
transform: translate(-50%, -50%) scale(30);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
/* Smooth transition for links */
|
| 65 |
+
a {
|
| 66 |
+
text-decoration: none;
|
| 67 |
+
color: #4a8fdf;
|
| 68 |
+
transition: color 0.3s ease-in-out;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
a:hover {
|
| 72 |
+
color: #fff;
|
| 73 |
+
}
|
| 74 |
+
.dark-mode {
|
| 75 |
+
background-color: #131b2b;
|
| 76 |
+
color: #fff;
|
| 77 |
+
}
|
| 78 |
+
.navbar {
|
| 79 |
+
display: flex;
|
| 80 |
+
justify-content: space-between;
|
| 81 |
+
align-items: center;
|
| 82 |
+
padding: 20px 30px;
|
| 83 |
+
background: #f4f2ec;
|
| 84 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 85 |
+
font-size: 18px;
|
| 86 |
+
font-weight: bold;
|
| 87 |
+
position: sticky;
|
| 88 |
+
top: 0;
|
| 89 |
+
z-index: 1000;
|
| 90 |
+
}
|
| 91 |
+
.dark-mode .navbar {
|
| 92 |
+
background: #2c3e50;
|
| 93 |
+
}
|
| 94 |
+
.navbar a {
|
| 95 |
+
text-decoration: none;
|
| 96 |
+
color: #000;
|
| 97 |
+
margin: 0 20px;
|
| 98 |
+
font-size: 18px;
|
| 99 |
+
font-weight: bold;
|
| 100 |
+
}
|
| 101 |
+
.dark-mode .navbar a {
|
| 102 |
+
color: #fff;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.dropdown {
|
| 106 |
+
position: relative;
|
| 107 |
+
display: inline-block;
|
| 108 |
+
}
|
| 109 |
+
.dropdown-content {
|
| 110 |
+
display: none;
|
| 111 |
+
position: absolute;
|
| 112 |
+
background-color: #f9f9f9;
|
| 113 |
+
min-width: 160px;
|
| 114 |
+
box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
|
| 115 |
+
z-index: 1;
|
| 116 |
+
}
|
| 117 |
+
.dropdown-content a {
|
| 118 |
+
color: black;
|
| 119 |
+
padding: 10px 16px;
|
| 120 |
+
display: block;
|
| 121 |
+
text-decoration: none;
|
| 122 |
+
}
|
| 123 |
+
.dropdown-content a:hover {
|
| 124 |
+
background-color: #ddd;
|
| 125 |
+
}
|
| 126 |
+
.dropdown:hover .dropdown-content {
|
| 127 |
+
display: block;
|
| 128 |
+
}
|
| 129 |
+
.dark-mode .dropdown-content {
|
| 130 |
+
background-color: #2c3e50;
|
| 131 |
+
}
|
| 132 |
+
.dark-mode .dropdown-content a {
|
| 133 |
+
color: white;
|
| 134 |
+
}
|
| 135 |
+
.dark-mode .dropdown-content a:hover {
|
| 136 |
+
background-color: #374f66;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
.toggle-switch {
|
| 141 |
+
position: relative;
|
| 142 |
+
display: inline-block;
|
| 143 |
+
width: 50px;
|
| 144 |
+
height: 25px;
|
| 145 |
+
}
|
| 146 |
+
.toggle-switch input {
|
| 147 |
+
opacity: 0;
|
| 148 |
+
width: 0;
|
| 149 |
+
height: 0;
|
| 150 |
+
}
|
| 151 |
+
.slider {
|
| 152 |
+
position: absolute;
|
| 153 |
+
cursor: pointer;
|
| 154 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 155 |
+
background-color: #ccc;
|
| 156 |
+
transition: .4s;
|
| 157 |
+
border-radius: 25px;
|
| 158 |
+
}
|
| 159 |
+
.toggle-circle {
|
| 160 |
+
position: absolute;
|
| 161 |
+
left: 4px;
|
| 162 |
+
bottom: 3px;
|
| 163 |
+
width: 18px;
|
| 164 |
+
height: 18px;
|
| 165 |
+
background: #fff;
|
| 166 |
+
border-radius: 50%;
|
| 167 |
+
display: flex;
|
| 168 |
+
align-items: center;
|
| 169 |
+
justify-content: center;
|
| 170 |
+
transition: transform 0.4s;
|
| 171 |
+
z-index: 2;
|
| 172 |
+
font-size: 14px;
|
| 173 |
+
}
|
| 174 |
+
.toggle-icon {
|
| 175 |
+
transition: color 0.4s, content 0.4s;
|
| 176 |
+
color: #FFD600; /* Sun color */
|
| 177 |
+
}
|
| 178 |
+
input:checked + .slider {
|
| 179 |
+
background-color: #4a8fdf;
|
| 180 |
+
}
|
| 181 |
+
input:checked + .slider .toggle-circle {
|
| 182 |
+
transform: translateX(24px);
|
| 183 |
+
}
|
| 184 |
+
input:checked + .slider .toggle-icon {
|
| 185 |
+
color: #4a8fdf; /* Moon color */
|
| 186 |
+
/* Use content swap via JS for moon icon */
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.stock-ticker-indian {
|
| 190 |
+
background: #f8f9fa;
|
| 191 |
+
color: #222;
|
| 192 |
+
padding: 10px 0;
|
| 193 |
+
height: 45px;
|
| 194 |
+
overflow: hidden;
|
| 195 |
+
white-space: nowrap;
|
| 196 |
+
font-size: 1.05rem;
|
| 197 |
+
border-radius: 6px;
|
| 198 |
+
margin-bottom: 12px;
|
| 199 |
+
border-top: 1px solid #ddd;
|
| 200 |
+
border-bottom: 1px solid #ddd;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.ticker-content-indian {
|
| 204 |
+
display: flex;
|
| 205 |
+
align-items: center;
|
| 206 |
+
gap: 5px;
|
| 207 |
+
animation: scroll-left 25s linear infinite;
|
| 208 |
+
}
|
| 209 |
+
.ticker-label {
|
| 210 |
+
font-weight: bold;
|
| 211 |
+
margin-right: 8px;
|
| 212 |
+
}
|
| 213 |
+
.gainers-label { color: #22c55e; }
|
| 214 |
+
.losers-label { color: #ef4444; }
|
| 215 |
+
.ticker-item { margin-right: 18px; }
|
| 216 |
+
.ticker-sep { color: #888; margin: 0 8px; }
|
| 217 |
+
.up {
|
| 218 |
+
color: #22c55e !important;
|
| 219 |
+
font-weight: bold;
|
| 220 |
+
margin-left: 2px;
|
| 221 |
+
}
|
| 222 |
+
.gainer-price {
|
| 223 |
+
color: #22c55e !important;
|
| 224 |
+
font-weight: bold;
|
| 225 |
+
margin-left: 2px;
|
| 226 |
+
}
|
| 227 |
+
.down {
|
| 228 |
+
color: #ef4444 !important;
|
| 229 |
+
font-weight: bold;
|
| 230 |
+
margin-left: 2px;
|
| 231 |
+
}
|
| 232 |
+
.loser-price {
|
| 233 |
+
color: #ef4444 !important;
|
| 234 |
+
font-weight: bold;
|
| 235 |
+
margin-left: 2px;
|
| 236 |
+
}
|
| 237 |
+
/* Dark mode styles */
|
| 238 |
+
body.dark-mode .stock-ticker-indian {
|
| 239 |
+
background: #2c3e50;
|
| 240 |
+
color: #e0e7ef;
|
| 241 |
+
border-top: 0.3px solid #5c7a99;
|
| 242 |
+
border-bottom: 0.3px solid #5c7a99;
|
| 243 |
+
}
|
| 244 |
+
body.dark-mode .ticker-content-indian span {
|
| 245 |
+
color: #e0e7ef;
|
| 246 |
+
}
|
| 247 |
+
body.dark-mode .ticker-label { color: #fff; }
|
| 248 |
+
body.dark-mode .gainers-label { color: #22c55e; }
|
| 249 |
+
body.dark-mode .losers-label { color: #ef4444; }
|
| 250 |
+
body.dark-mode .ticker-sep { color: #aaa; }
|
| 251 |
+
|
| 252 |
+
.gainers-label {
|
| 253 |
+
color: #22c55e !important; /* Bright green */
|
| 254 |
+
font-weight: bold;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.losers-label {
|
| 258 |
+
color: #ef4444 !important; /* Bright red */
|
| 259 |
+
font-weight: bold;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* For dark mode, keep the same colors for labels */
|
| 263 |
+
body.dark-mode .gainers-label {
|
| 264 |
+
color: #22c55e !important;
|
| 265 |
+
}
|
| 266 |
+
body.dark-mode .losers-label {
|
| 267 |
+
color: #ef4444 !important;
|
| 268 |
+
}
|
| 269 |
+
/* Smooth scrolling effect */
|
| 270 |
+
@keyframes scroll-left {
|
| 271 |
+
from {
|
| 272 |
+
transform: translateX(100%);
|
| 273 |
+
}
|
| 274 |
+
to {
|
| 275 |
+
transform: translateX(-100%);
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Main Content */
|
| 280 |
+
.video-container {
|
| 281 |
+
position: relative;
|
| 282 |
+
width: 100%;
|
| 283 |
+
height: 50vh;
|
| 284 |
+
overflow: hidden;
|
| 285 |
+
display: flex;
|
| 286 |
+
justify-content: center;
|
| 287 |
+
align-items: center;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
#bgVideo {
|
| 291 |
+
position: absolute;
|
| 292 |
+
top: 50%;
|
| 293 |
+
left: 50%;
|
| 294 |
+
transform: translate(-50%, -50%);
|
| 295 |
+
width: 100%;
|
| 296 |
+
height: 100%;
|
| 297 |
+
object-fit: cover;
|
| 298 |
+
z-index: -1;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.content {
|
| 302 |
+
position: absolute;
|
| 303 |
+
top: 50%;
|
| 304 |
+
left: 50%;
|
| 305 |
+
transform: translate(-50%, -50%);
|
| 306 |
+
z-index: 2;
|
| 307 |
+
background: rgba(0, 0, 0, 0.5);
|
| 308 |
+
padding: 50px 20px;
|
| 309 |
+
color: #fff;
|
| 310 |
+
border-radius: 12px;
|
| 311 |
+
font-family: 'Poppins', sans-serif;
|
| 312 |
+
text-align: center;
|
| 313 |
+
width: 80%;
|
| 314 |
+
display: flex;
|
| 315 |
+
justify-content: center;
|
| 316 |
+
align-items: center;
|
| 317 |
+
height: auto;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* Initially, set h1 position at the center */
|
| 321 |
+
.content h1 {
|
| 322 |
+
font-size: 3em;
|
| 323 |
+
font-weight: 700;
|
| 324 |
+
letter-spacing: 2px;
|
| 325 |
+
text-transform: uppercase;
|
| 326 |
+
text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.8);
|
| 327 |
+
opacity: 0;
|
| 328 |
+
transform: translateY(0); /* Keep centered, no off-center movement */
|
| 329 |
+
animation: fadeInUp 1.5s ease-out forwards;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* Smooth fade-in effect */
|
| 333 |
+
@keyframes fadeInUp {
|
| 334 |
+
0% {
|
| 335 |
+
opacity: 0;
|
| 336 |
+
transform: scale(0.8); /* Slightly smaller size */
|
| 337 |
+
}
|
| 338 |
+
100% {
|
| 339 |
+
opacity: 1;
|
| 340 |
+
transform: scale(1); /* Normal size */
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
/* Glowing effect for the heading */
|
| 346 |
+
@keyframes glowText {
|
| 347 |
+
0% {
|
| 348 |
+
text-shadow: 0px 0px 10px rgba(74, 143, 223, 0.6);
|
| 349 |
+
}
|
| 350 |
+
100% {
|
| 351 |
+
text-shadow: 0px 0px 25px rgba(74, 143, 223, 1), 0px 0px 35px rgba(74, 143, 223, 0.8);
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/* Fade-in animation */
|
| 356 |
+
@keyframes fadeIn {
|
| 357 |
+
from {
|
| 358 |
+
opacity: 0;
|
| 359 |
+
transform: translateY(-20px);
|
| 360 |
+
}
|
| 361 |
+
to {
|
| 362 |
+
opacity: 1;
|
| 363 |
+
transform: translateY(0);
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/* Dark Mode */
|
| 368 |
+
.dark-mode .content {
|
| 369 |
+
background: rgba(0, 0, 0, 0.5); /* Semi-transparent background for readability */
|
| 370 |
+
color: #ffffff;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/* Market Movers Section */
|
| 374 |
+
.market-movers {
|
| 375 |
+
width: 80%;
|
| 376 |
+
margin: 50px auto;
|
| 377 |
+
text-align: center;
|
| 378 |
+
padding: 40px;
|
| 379 |
+
background: #fff;
|
| 380 |
+
border: 2px solid #4a8fdf; /* Add a solid border */
|
| 381 |
+
border-radius: 8px;
|
| 382 |
+
box-shadow: 0 0 15px rgba(74, 143, 223, 0.6); /* Green glow */
|
| 383 |
+
animation: glowBorder 1.5s infinite alternate;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.movers-container {
|
| 387 |
+
display: flex;
|
| 388 |
+
justify-content: space-around;
|
| 389 |
+
gap: 20px;
|
| 390 |
+
margin-top: 20px;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.movers-section {
|
| 394 |
+
width: 45%;
|
| 395 |
+
background: #f8f9fa;
|
| 396 |
+
padding: 20px;
|
| 397 |
+
border-radius: 8px;
|
| 398 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.movers-section h3 {
|
| 402 |
+
font-size: 24px;
|
| 403 |
+
margin-bottom: 15px;
|
| 404 |
+
color: #4a8fdf;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.movers-section ul {
|
| 408 |
+
list-style-type: none;
|
| 409 |
+
padding: 0;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.movers-section ul li {
|
| 413 |
+
font-size: 16px;
|
| 414 |
+
padding: 8px 0;
|
| 415 |
+
border-bottom: 1px solid #ddd;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.movers-section ul li:last-child {
|
| 419 |
+
border-bottom: none;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.up {
|
| 423 |
+
color: #0FFF50;
|
| 424 |
+
font-weight: bold;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.down {
|
| 428 |
+
color: red;
|
| 429 |
+
font-weight: bold;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
/* Dark Mode Adjustments */
|
| 433 |
+
.dark-mode .market-movers {
|
| 434 |
+
background: #131b2b;
|
| 435 |
+
color: #fff;
|
| 436 |
+
box-shadow: 0 0 15px rgba(74, 143, 223, 0.6); /* Neon green glow */
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.dark-mode .movers-section {
|
| 440 |
+
background: #2c3e50;
|
| 441 |
+
color: #fff;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.dark-mode .movers-section ul li {
|
| 445 |
+
border-bottom: 1px solid #555;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.dark-mode .stock-ticker-indian {
|
| 449 |
+
overflow: hidden;
|
| 450 |
+
white-space: nowrap;
|
| 451 |
+
background: #2c3e50;
|
| 452 |
+
padding: 10px 0;
|
| 453 |
+
position: relative;
|
| 454 |
+
width: 100%;
|
| 455 |
+
margin-top: 60px; /* Space below fixed navbar */
|
| 456 |
+
border-top: 0.3px solid #5c7a99;
|
| 457 |
+
border-bottom: 0.3px solid #5c7a99;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
/* Footer Styles */
|
| 461 |
+
footer {
|
| 462 |
+
background-color: #2c3e50;
|
| 463 |
+
color: #ecf0f1;
|
| 464 |
+
padding: 40px 20px;
|
| 465 |
+
font-family: Arial, sans-serif;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.footer-content {
|
| 469 |
+
display: flex;
|
| 470 |
+
justify-content: space-between;
|
| 471 |
+
flex-wrap: wrap;
|
| 472 |
+
gap: 20px;
|
| 473 |
+
max-width: 1200px;
|
| 474 |
+
margin: 0 auto;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.footer-section {
|
| 478 |
+
flex: 1;
|
| 479 |
+
min-width: 200px;
|
| 480 |
+
margin-bottom: 20px;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.footer-section h3 {
|
| 484 |
+
font-size: 18px;
|
| 485 |
+
margin-bottom: 15px;
|
| 486 |
+
color: #4a8fdf;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.footer-section ul {
|
| 490 |
+
list-style: none;
|
| 491 |
+
padding: 0;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.footer-section ul li {
|
| 495 |
+
margin-bottom: 10px;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.footer-section ul li a {
|
| 499 |
+
color: #ecf0f1;
|
| 500 |
+
text-decoration: none;
|
| 501 |
+
transition: color 0.3s ease;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.footer-section ul li a:hover {
|
| 505 |
+
color: #4a8fdf;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.footer-section form {
|
| 509 |
+
display: flex;
|
| 510 |
+
gap: 10px;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.footer-section input[type="email"] {
|
| 514 |
+
padding: 10px;
|
| 515 |
+
border: none;
|
| 516 |
+
border-radius: 5px;
|
| 517 |
+
width: 70%;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.footer-section button {
|
| 521 |
+
padding: 10px 20px;
|
| 522 |
+
background-color: #4a8fdf;
|
| 523 |
+
color: #fff;
|
| 524 |
+
border: none;
|
| 525 |
+
border-radius: 5px;
|
| 526 |
+
cursor: pointer;
|
| 527 |
+
transition: background-color 0.3s ease;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.footer-section button:hover {
|
| 531 |
+
background-color: #4a8fdf;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.footer-bottom {
|
| 535 |
+
text-align: center;
|
| 536 |
+
margin-top: 20px;
|
| 537 |
+
padding-top: 20px;
|
| 538 |
+
border-top: 1px solid #34495e;
|
| 539 |
+
font-size: 14px;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
html{
|
| 543 |
+
scroll-behavior: smooth;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
/* Container for the entire stock data section */
|
| 547 |
+
.stock-data-container {
|
| 548 |
+
width: 90%;
|
| 549 |
+
max-width: 1200px;
|
| 550 |
+
margin: 40px auto;
|
| 551 |
+
padding: 20px;
|
| 552 |
+
background-color: #fff;
|
| 553 |
+
border-radius: 10px;
|
| 554 |
+
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
| 555 |
+
overflow: hidden;
|
| 556 |
+
border: 2px solid #4a8fdf; /* Add a solid border */
|
| 557 |
+
animation: glowBorder 2s infinite alternate; /* Apply glow animation */
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
/* Center the date range filter */
|
| 561 |
+
.date-range-filter {
|
| 562 |
+
display: flex;
|
| 563 |
+
justify-content: center;
|
| 564 |
+
align-items: center;
|
| 565 |
+
gap: 10px;
|
| 566 |
+
margin-bottom: 20px;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.date-range-filter label {
|
| 570 |
+
font-weight: bold;
|
| 571 |
+
color: #333;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.date-range-filter input {
|
| 575 |
+
padding: 8px;
|
| 576 |
+
border: 1px solid #ddd;
|
| 577 |
+
border-radius: 5px;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.date-range-filter button {
|
| 581 |
+
padding: 8px 16px;
|
| 582 |
+
background-color: #4a8fdf;
|
| 583 |
+
color: white;
|
| 584 |
+
border: none;
|
| 585 |
+
border-radius: 5px;
|
| 586 |
+
cursor: pointer;
|
| 587 |
+
transition: background-color 0.3s ease;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
.date-range-filter button:hover {
|
| 591 |
+
background-color: #4a8fdf;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
/* Search bar styling */
|
| 595 |
+
#search-bar {
|
| 596 |
+
padding: 8px;
|
| 597 |
+
border: 1px solid #ddd;
|
| 598 |
+
border-radius: 5px;
|
| 599 |
+
width: 200px;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
/* Animation for table */
|
| 603 |
+
@keyframes slideInFromTop {
|
| 604 |
+
0% {
|
| 605 |
+
transform: translateY(-100%);
|
| 606 |
+
opacity: 0;
|
| 607 |
+
}
|
| 608 |
+
100% {
|
| 609 |
+
transform: translateY(0);
|
| 610 |
+
opacity: 1;
|
| 611 |
+
}
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
/* Table styling */
|
| 615 |
+
table {
|
| 616 |
+
width: 100%;
|
| 617 |
+
border-collapse: collapse;
|
| 618 |
+
margin-top: 20px;
|
| 619 |
+
animation: slideInFromTop 0.8s ease-out forwards;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
th, td {
|
| 623 |
+
padding: 10px;
|
| 624 |
+
text-align: center;
|
| 625 |
+
border: 1px solid #ddd;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
th {
|
| 629 |
+
background-color: #4a8fdf;
|
| 630 |
+
color: white;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
tr:nth-child(even) {
|
| 634 |
+
background-color: #f9f9f9;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
tr:hover {
|
| 638 |
+
background-color: #f1f1f1;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
/* Dark mode adjustments */
|
| 642 |
+
.dark-mode .stock-data-container {
|
| 643 |
+
background-color: #131b2b;
|
| 644 |
+
color: #fff;
|
| 645 |
+
border-color: #4a8fdf; /* Keep the border color consistent */
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.dark-mode .date-range-filter label {
|
| 649 |
+
color: #ddd;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
.dark-mode .date-range-filter input {
|
| 653 |
+
background-color: #243342;
|
| 654 |
+
color: #fff;
|
| 655 |
+
border-color: #555;
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
.dark-mode .date-range-filter button {
|
| 659 |
+
background-color: #4a8fdf;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
.dark-mode .date-range-filter button:hover {
|
| 663 |
+
background-color: #4a8fdf;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.dark-mode #search-bar {
|
| 667 |
+
background-color: #243342;
|
| 668 |
+
color: #fff;
|
| 669 |
+
border-color: #555;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.dark-mode table {
|
| 673 |
+
border-color: #555;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.dark-mode th {
|
| 677 |
+
background-color: #243342;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.dark-mode tr:nth-child(even) {
|
| 681 |
+
background-color: #243342;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.dark-mode tr:hover {
|
| 685 |
+
background-color: #555;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
/* Glow effect for the container */
|
| 689 |
+
@keyframes glowBorder {
|
| 690 |
+
0% {
|
| 691 |
+
box-shadow: 0 0 10px rgba(74, 143, 223, 0.6);
|
| 692 |
+
}
|
| 693 |
+
50% {
|
| 694 |
+
box-shadow: 0 0 20px rgba(74, 143, 223, 0.8), 0 0 30px rgba(74, 143, 223, 0.6);
|
| 695 |
+
}
|
| 696 |
+
100% {
|
| 697 |
+
box-shadow: 0 0 10px rgba(74, 143, 223, 0.6);
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
/* Apply the same animation to both stock-data-container and market-movers */
|
| 701 |
+
.stock-data-container,
|
| 702 |
+
.market-movers {
|
| 703 |
+
animation: glowBorder 2s infinite alternate; /* Apply the same glow animation */
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.indian-head {
|
| 707 |
+
text-align: center;
|
| 708 |
+
font-size: 28px;
|
| 709 |
+
font-weight: 700;
|
| 710 |
+
color: #4a8fdf;
|
| 711 |
+
margin-bottom: 20px;
|
| 712 |
+
text-transform: uppercase;
|
| 713 |
+
letter-spacing: 2px;
|
| 714 |
+
position: relative;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.indian-head::after {
|
| 718 |
+
content: "";
|
| 719 |
+
position: absolute;
|
| 720 |
+
bottom: -10px;
|
| 721 |
+
left: 50%;
|
| 722 |
+
transform: translateX(-50%);
|
| 723 |
+
width: 60px;
|
| 724 |
+
height: 4px;
|
| 725 |
+
background-color: #4a8fdf;
|
| 726 |
+
border-radius: 2px;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
/* Toggle Buttons */
|
| 730 |
+
.toggle-buttons {
|
| 731 |
+
display: flex;
|
| 732 |
+
justify-content: center;
|
| 733 |
+
gap: 10px;
|
| 734 |
+
margin-bottom: 20px;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.toggle-buttons button {
|
| 738 |
+
padding: 10px 20px;
|
| 739 |
+
font-size: 16px;
|
| 740 |
+
border: none;
|
| 741 |
+
border-radius: 5px;
|
| 742 |
+
cursor: pointer;
|
| 743 |
+
background-color: #4a8fdf;
|
| 744 |
+
color: white;
|
| 745 |
+
transition: background-color 0.3s ease;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
.toggle-buttons button:hover {
|
| 749 |
+
background-color: #4a8fdf;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.toggle-buttons button.active {
|
| 753 |
+
background-color: #4a8fdf;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
/* Container styling */
|
| 757 |
+
.stock-data-container {
|
| 758 |
+
max-width: 700px;
|
| 759 |
+
margin: 40px auto;
|
| 760 |
+
background: #fff;
|
| 761 |
+
border-radius: 12px;
|
| 762 |
+
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
|
| 763 |
+
padding: 32px 24px 24px 24px;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.stock-data-container h2 {
|
| 767 |
+
margin-bottom: 24px;
|
| 768 |
+
color: #1976d2;
|
| 769 |
+
font-weight: 600;
|
| 770 |
+
text-align: center;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.date-range-filter {
|
| 774 |
+
display: flex;
|
| 775 |
+
flex-wrap: wrap;
|
| 776 |
+
gap: 16px;
|
| 777 |
+
align-items: center;
|
| 778 |
+
margin-bottom: 24px;
|
| 779 |
+
justify-content: center;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
.date-range-filter label {
|
| 783 |
+
font-weight: 500;
|
| 784 |
+
color: #333;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.date-range-filter input[type="date"],
|
| 788 |
+
.date-range-filter select {
|
| 789 |
+
padding: 6px 12px;
|
| 790 |
+
border: 1px solid #bdbdbd;
|
| 791 |
+
border-radius: 6px;
|
| 792 |
+
font-size: 1rem;
|
| 793 |
+
background: #fafafa;
|
| 794 |
+
color: #333;
|
| 795 |
+
min-width: 160px;
|
| 796 |
+
transition: border-color 0.2s;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
.date-range-filter input[type="date"]:focus,
|
| 800 |
+
.date-range-filter select:focus {
|
| 801 |
+
border-color: #1976d2;
|
| 802 |
+
outline: none;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
.date-range-filter button {
|
| 806 |
+
padding: 8px 18px;
|
| 807 |
+
background: #1976d2;
|
| 808 |
+
color: #fff;
|
| 809 |
+
border: none;
|
| 810 |
+
border-radius: 6px;
|
| 811 |
+
font-size: 1rem;
|
| 812 |
+
font-weight: 500;
|
| 813 |
+
cursor: pointer;
|
| 814 |
+
transition: background 0.2s;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.date-range-filter button:hover {
|
| 818 |
+
background: #1565c0;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
#message-container {
|
| 822 |
+
margin: 16px 0;
|
| 823 |
+
font-size: 1rem;
|
| 824 |
+
text-align: center;
|
| 825 |
+
border-radius: 6px;
|
| 826 |
+
padding: 10px;
|
| 827 |
+
display: none;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.chart-container {
|
| 831 |
+
margin-top: 24px;
|
| 832 |
+
background: #f5f7fa;
|
| 833 |
+
border-radius: 10px;
|
| 834 |
+
padding: 18px;
|
| 835 |
+
box-shadow: 0 1px 8px rgba(0,0,0,0.04);
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
@media (max-width: 600px) {
|
| 839 |
+
.stock-data-container {
|
| 840 |
+
padding: 16px 4px;
|
| 841 |
+
}
|
| 842 |
+
.date-range-filter {
|
| 843 |
+
flex-direction: column;
|
| 844 |
+
gap: 10px;
|
| 845 |
+
}
|
| 846 |
+
.chart-container {
|
| 847 |
+
padding: 6px;
|
| 848 |
+
}
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
.favorites-container {
|
| 852 |
+
max-width: 700px;
|
| 853 |
+
margin: 40px auto;
|
| 854 |
+
background: #fff;
|
| 855 |
+
border-radius: 12px;
|
| 856 |
+
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
|
| 857 |
+
animation: tableGlow 2s infinite alternate;
|
| 858 |
+
padding: 32px 24px 24px 24px;
|
| 859 |
+
}
|
| 860 |
+
.dark-mode .favorites-container {
|
| 861 |
+
background: #131b2b;
|
| 862 |
+
color: #fff;
|
| 863 |
+
}
|
| 864 |
+
#favorites-table {
|
| 865 |
+
width: 100%;
|
| 866 |
+
border-collapse: collapse;
|
| 867 |
+
margin-top: 10px;
|
| 868 |
+
}
|
| 869 |
+
#favorites-table th, #favorites-table td {
|
| 870 |
+
padding: 10px;
|
| 871 |
+
text-align: center;
|
| 872 |
+
border: 1px solid #ddd;
|
| 873 |
+
}
|
| 874 |
+
#favorites-table th {
|
| 875 |
+
background-color: #4a8fdf;
|
| 876 |
+
color: white;
|
| 877 |
+
}
|
| 878 |
+
#favorites-table tr:nth-child(even) {
|
| 879 |
+
background-color: #f9f9f9;
|
| 880 |
+
}
|
| 881 |
+
.dark-mode #favorites-table th {
|
| 882 |
+
background-color: #243342;
|
| 883 |
+
}
|
| 884 |
+
.dark-mode #favorites-table tr:nth-child(even) {
|
| 885 |
+
background-color: #243342;
|
| 886 |
+
}
|
| 887 |
+
.bookmark-btn {
|
| 888 |
+
background: none;
|
| 889 |
+
border: none;
|
| 890 |
+
color: #fbbf24;
|
| 891 |
+
font-size: 1.3em;
|
| 892 |
+
cursor: pointer;
|
| 893 |
+
transition: color 0.2s;
|
| 894 |
+
}
|
| 895 |
+
.bookmark-btn.bookmarked {
|
| 896 |
+
color: #f59e42;
|
| 897 |
+
font-weight: bold;
|
| 898 |
+
}
|
| 899 |
+
.bookmark-btn:hover {
|
| 900 |
+
color: #f59e42;
|
| 901 |
+
}
|
| 902 |
+
/* Glowing animation for the favorites table */
|
| 903 |
+
@keyframes tableGlow {
|
| 904 |
+
0% {
|
| 905 |
+
box-shadow: 0 0 10px 0 #4a8fdf, 0 0 0px 0 #4a8fdf;
|
| 906 |
+
}
|
| 907 |
+
50% {
|
| 908 |
+
box-shadow: 0 0 24px 4px #4a8fdf, 0 0 12px 2px #1976d2;
|
| 909 |
+
}
|
| 910 |
+
100% {
|
| 911 |
+
box-shadow: 0 0 10px 0 #4a8fdf, 0 0 0px 0 #4a8fdf;
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.favorites-table-wrapper {
|
| 916 |
+
max-height: 500px;
|
| 917 |
+
overflow-y: auto;
|
| 918 |
+
border-radius: 15px;
|
| 919 |
+
margin-bottom: 24px;
|
| 920 |
+
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
+
/* Custom scrollbar for light mode */
|
| 924 |
+
.favorites-table-wrapper::-webkit-scrollbar {
|
| 925 |
+
width: 10px;
|
| 926 |
+
}
|
| 927 |
+
.favorites-table-wrapper::-webkit-scrollbar-thumb {
|
| 928 |
+
background: #4a8fdf;
|
| 929 |
+
border-radius: 8px;
|
| 930 |
+
}
|
| 931 |
+
.favorites-table-wrapper::-webkit-scrollbar-track {
|
| 932 |
+
background: #e0e7ef;
|
| 933 |
+
border-radius: 8px;
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
/* Custom scrollbar for dark mode */
|
| 937 |
+
body.dark-mode .favorites-table-wrapper::-webkit-scrollbar {
|
| 938 |
+
width: 10px;
|
| 939 |
+
}
|
| 940 |
+
body.dark-mode .favorites-table-wrapper::-webkit-scrollbar-thumb {
|
| 941 |
+
background: #243342;
|
| 942 |
+
border-radius: 8px;
|
| 943 |
+
box-shadow: 0 0 8px #4a8fdf;
|
| 944 |
+
}
|
| 945 |
+
body.dark-mode .favorites-table-wrapper::-webkit-scrollbar-track {
|
| 946 |
+
background: #1e2a38;
|
| 947 |
+
border-radius: 8px;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
/* Optional: Firefox scrollbar styling */
|
| 951 |
+
.favorites-table-wrapper {
|
| 952 |
+
scrollbar-width: thin;
|
| 953 |
+
scrollbar-color: #4a8fdf #e0e7ef;
|
| 954 |
+
}
|
| 955 |
+
body.dark-mode .favorites-table-wrapper {
|
| 956 |
+
scrollbar-color: #243342 #1e2a38;
|
| 957 |
+
}
|
| 958 |
+
.custom-introjs-tooltip {
|
| 959 |
+
font-size: 1.05rem;
|
| 960 |
+
color: #222;
|
| 961 |
+
}
|
| 962 |
+
.introjs-tooltip {
|
| 963 |
+
z-index: 11000 !important;
|
| 964 |
+
}
|
static/css/news.css
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
nav ul {
|
| 2 |
+
list-style: none;
|
| 3 |
+
display: flex;
|
| 4 |
+
background-color: #333;
|
| 5 |
+
padding: 0;
|
| 6 |
+
}
|
| 7 |
+
nav ul li {
|
| 8 |
+
margin: 0 10px;
|
| 9 |
+
}
|
| 10 |
+
nav ul li a {
|
| 11 |
+
color: white;
|
| 12 |
+
text-decoration: none;
|
| 13 |
+
padding: 10px 15px;
|
| 14 |
+
display: block;
|
| 15 |
+
}
|
| 16 |
+
nav ul li a:hover {
|
| 17 |
+
background-color: #555;
|
| 18 |
+
}
|
| 19 |
+
/* Example: Start of your CSS */
|
| 20 |
+
* {
|
| 21 |
+
margin: 0;
|
| 22 |
+
padding: 0;
|
| 23 |
+
box-sizing: border-box;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body {
|
| 27 |
+
margin: 0;
|
| 28 |
+
font-family: Arial, sans-serif;
|
| 29 |
+
background-color: #f4f2ec;
|
| 30 |
+
color: #000;
|
| 31 |
+
transition: background-color 0.2s, color 0.2s;
|
| 32 |
+
opacity: 0;
|
| 33 |
+
transform: scale(1.05);
|
| 34 |
+
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out;
|
| 35 |
+
|
| 36 |
+
}
|
| 37 |
+
body.loaded {
|
| 38 |
+
opacity: 1;
|
| 39 |
+
transform: scale(1);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Transition Overlay (Expanding Circle Effect) */
|
| 43 |
+
.transition-overlay {
|
| 44 |
+
position: fixed;
|
| 45 |
+
top: 50%;
|
| 46 |
+
left: 50%;
|
| 47 |
+
width: 100px;
|
| 48 |
+
height: 100px;
|
| 49 |
+
background: radial-gradient(circle, #4a8fdf, #000); /* Green fading into black */
|
| 50 |
+
border-radius: 50%;
|
| 51 |
+
transform: translate(-50%, -50%) scale(0);
|
| 52 |
+
transition: transform 0.7s ease-in-out;
|
| 53 |
+
z-index: 9999;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Expand circle effect when navigating */
|
| 57 |
+
.transition-overlay.active {
|
| 58 |
+
transform: translate(-50%, -50%) scale(30);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Smooth transition for links */
|
| 62 |
+
a {
|
| 63 |
+
text-decoration: none;
|
| 64 |
+
color: #4a8fdf;
|
| 65 |
+
transition: color 0.3s ease-in-out;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
a:hover {
|
| 69 |
+
color: #fff;
|
| 70 |
+
}
|
| 71 |
+
.dark-mode {
|
| 72 |
+
background-color: #131b2b;
|
| 73 |
+
color: #fff;
|
| 74 |
+
}
|
| 75 |
+
.navbar {
|
| 76 |
+
display: flex;
|
| 77 |
+
justify-content: space-between;
|
| 78 |
+
align-items: center;
|
| 79 |
+
padding: 20px 30px;
|
| 80 |
+
background: #f4f2ec;
|
| 81 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 82 |
+
font-size: 18px;
|
| 83 |
+
font-weight: bold;
|
| 84 |
+
position: sticky;
|
| 85 |
+
top: 0;
|
| 86 |
+
z-index: 1000;
|
| 87 |
+
}
|
| 88 |
+
.dark-mode .navbar {
|
| 89 |
+
background: #2c3e50;
|
| 90 |
+
}
|
| 91 |
+
.navbar a {
|
| 92 |
+
text-decoration: none;
|
| 93 |
+
color: #000;
|
| 94 |
+
margin: 0 20px;
|
| 95 |
+
font-size: 18px;
|
| 96 |
+
font-weight: bold;
|
| 97 |
+
}
|
| 98 |
+
.dark-mode .navbar a {
|
| 99 |
+
color: #fff;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.dropdown {
|
| 103 |
+
position: relative;
|
| 104 |
+
display: inline-block;
|
| 105 |
+
}
|
| 106 |
+
.dropdown-content {
|
| 107 |
+
display: none;
|
| 108 |
+
position: absolute;
|
| 109 |
+
background-color: #f9f9f9;
|
| 110 |
+
min-width: 160px;
|
| 111 |
+
box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
|
| 112 |
+
z-index: 1;
|
| 113 |
+
}
|
| 114 |
+
.dropdown-content a {
|
| 115 |
+
color: black;
|
| 116 |
+
padding: 10px 16px;
|
| 117 |
+
display: block;
|
| 118 |
+
text-decoration: none;
|
| 119 |
+
}
|
| 120 |
+
.dropdown-content a:hover {
|
| 121 |
+
background-color: #ddd;
|
| 122 |
+
}
|
| 123 |
+
.dropdown:hover .dropdown-content {
|
| 124 |
+
display: block;
|
| 125 |
+
}
|
| 126 |
+
.dark-mode .dropdown-content {
|
| 127 |
+
background-color: #2c3e50;
|
| 128 |
+
}
|
| 129 |
+
.dark-mode .dropdown-content a {
|
| 130 |
+
color: white;
|
| 131 |
+
}
|
| 132 |
+
.dark-mode .dropdown-content a:hover {
|
| 133 |
+
background-color: #374f66;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
.toggle-switch {
|
| 138 |
+
position: relative;
|
| 139 |
+
display: inline-block;
|
| 140 |
+
width: 50px;
|
| 141 |
+
height: 25px;
|
| 142 |
+
}
|
| 143 |
+
.toggle-switch input {
|
| 144 |
+
opacity: 0;
|
| 145 |
+
width: 0;
|
| 146 |
+
height: 0;
|
| 147 |
+
}
|
| 148 |
+
.slider {
|
| 149 |
+
position: absolute;
|
| 150 |
+
cursor: pointer;
|
| 151 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 152 |
+
background-color: #ccc;
|
| 153 |
+
transition: .4s;
|
| 154 |
+
border-radius: 25px;
|
| 155 |
+
}
|
| 156 |
+
.toggle-circle {
|
| 157 |
+
position: absolute;
|
| 158 |
+
left: 4px;
|
| 159 |
+
bottom: 3px;
|
| 160 |
+
width: 18px;
|
| 161 |
+
height: 18px;
|
| 162 |
+
background: #fff;
|
| 163 |
+
border-radius: 50%;
|
| 164 |
+
display: flex;
|
| 165 |
+
align-items: center;
|
| 166 |
+
justify-content: center;
|
| 167 |
+
transition: transform 0.4s;
|
| 168 |
+
z-index: 2;
|
| 169 |
+
font-size: 14px;
|
| 170 |
+
}
|
| 171 |
+
.toggle-icon {
|
| 172 |
+
transition: color 0.4s, content 0.4s;
|
| 173 |
+
color: #FFD600; /* Sun color */
|
| 174 |
+
}
|
| 175 |
+
input:checked + .slider {
|
| 176 |
+
background-color: #4a8fdf;
|
| 177 |
+
}
|
| 178 |
+
input:checked + .slider .toggle-circle {
|
| 179 |
+
transform: translateX(24px);
|
| 180 |
+
}
|
| 181 |
+
input:checked + .slider .toggle-icon {
|
| 182 |
+
color: #4a8fdf; /* Moon color */
|
| 183 |
+
/* Use content swap via JS for moon icon */
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* Footer Styles */
|
| 187 |
+
|
| 188 |
+
footer {
|
| 189 |
+
background-color: #2c3e50;
|
| 190 |
+
color: #ecf0f1;
|
| 191 |
+
padding: 40px 20px;
|
| 192 |
+
font-family: Arial, sans-serif;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.footer-content {
|
| 196 |
+
display: flex;
|
| 197 |
+
justify-content: space-between;
|
| 198 |
+
flex-wrap: wrap;
|
| 199 |
+
gap: 20px;
|
| 200 |
+
max-width: 1200px;
|
| 201 |
+
margin: 0 auto;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.footer-section {
|
| 205 |
+
flex: 1;
|
| 206 |
+
min-width: 200px;
|
| 207 |
+
margin-bottom: 20px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.footer-section h3 {
|
| 211 |
+
font-size: 18px;
|
| 212 |
+
margin-bottom: 15px;
|
| 213 |
+
color: #4a8fdf;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.footer-section ul {
|
| 217 |
+
list-style: none;
|
| 218 |
+
padding: 0;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.footer-section ul li {
|
| 222 |
+
margin-bottom: 10px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.footer-section ul li a {
|
| 226 |
+
color: #ecf0f1;
|
| 227 |
+
text-decoration: none;
|
| 228 |
+
transition: color 0.3s ease;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.footer-section ul li a:hover {
|
| 232 |
+
color: #4a8fdf;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.footer-section form {
|
| 236 |
+
display: flex;
|
| 237 |
+
gap: 10px;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.footer-section input[type="email"] {
|
| 241 |
+
padding: 10px;
|
| 242 |
+
border: none;
|
| 243 |
+
border-radius: 5px;
|
| 244 |
+
width: 70%;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.footer-section button {
|
| 248 |
+
padding: 10px 20px;
|
| 249 |
+
background-color: #4a8fdf;
|
| 250 |
+
color: #fff;
|
| 251 |
+
border: none;
|
| 252 |
+
border-radius: 5px;
|
| 253 |
+
cursor: pointer;
|
| 254 |
+
transition: background-color 0.3s ease;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.footer-section button:hover {
|
| 258 |
+
background-color: #34495e;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.footer-bottom {
|
| 262 |
+
text-align: center;
|
| 263 |
+
margin-top: 20px;
|
| 264 |
+
padding-top: 20px;
|
| 265 |
+
border-top: 1px solid #34495e;
|
| 266 |
+
font-size: 14px;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
html{
|
| 270 |
+
scroll-behavior: smooth;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.news-section {
|
| 274 |
+
padding: 60px 20px;
|
| 275 |
+
background-color: #fff;
|
| 276 |
+
margin: 40px auto;
|
| 277 |
+
max-width: 1200px;
|
| 278 |
+
border-radius: 15px;
|
| 279 |
+
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); /* Green glow */
|
| 280 |
+
background: linear-gradient(135deg, #f9f9f9, #ffffff);
|
| 281 |
+
overflow: hidden;
|
| 282 |
+
border: 2px solid #4a8fdf; /* Add a solid border */
|
| 283 |
+
animation: glowing 2s infinite alternate;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.news-section h2 {
|
| 287 |
+
text-align: center;
|
| 288 |
+
margin-bottom: 40px;
|
| 289 |
+
font-size: 50px;
|
| 290 |
+
font-weight: 800;
|
| 291 |
+
text-transform: uppercase;
|
| 292 |
+
letter-spacing: 2px;
|
| 293 |
+
background: linear-gradient(90deg, #4a8fdf, #ff0000);
|
| 294 |
+
background-clip: text;
|
| 295 |
+
-webkit-background-clip: text;
|
| 296 |
+
-webkit-text-fill-color: transparent;
|
| 297 |
+
position: relative;
|
| 298 |
+
font-family: 'Montserrat', sans-serif
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.news-section h2::after {
|
| 302 |
+
content: "";
|
| 303 |
+
position: absolute;
|
| 304 |
+
bottom: -10px;
|
| 305 |
+
left: 50%;
|
| 306 |
+
transform: translateX(-50%);
|
| 307 |
+
width: 100px;
|
| 308 |
+
height: 4px;
|
| 309 |
+
background-color: #ff0000;
|
| 310 |
+
border-radius: 2px;
|
| 311 |
+
animation: underline 2s infinite;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
@keyframes underline {
|
| 315 |
+
0% {
|
| 316 |
+
width: 0;
|
| 317 |
+
}
|
| 318 |
+
50% {
|
| 319 |
+
width: 100px;
|
| 320 |
+
}
|
| 321 |
+
100% {
|
| 322 |
+
width: 0;
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
.news-container {
|
| 328 |
+
display: grid;
|
| 329 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 330 |
+
gap: 30px;
|
| 331 |
+
}
|
| 332 |
+
#news-container {
|
| 333 |
+
max-height: 1500px;
|
| 334 |
+
overflow-y: auto;
|
| 335 |
+
padding-right: 8px; /* optional, for scrollbar space */
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.news-article {
|
| 339 |
+
background-color: #fff;
|
| 340 |
+
padding: 25px;
|
| 341 |
+
border-radius: 15px;
|
| 342 |
+
box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.1);
|
| 343 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 344 |
+
border: 2px solid #4a8fdf;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.news-article:hover {
|
| 348 |
+
transform: translateY(-10px);
|
| 349 |
+
box-shadow: 0px 12px 30px rgba(0, 0, 0, 0.15);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.news-article h3 {
|
| 353 |
+
margin: 0;
|
| 354 |
+
font-size: 22px;
|
| 355 |
+
color: #333;
|
| 356 |
+
font-weight: 600;
|
| 357 |
+
margin-bottom: 15px;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.news-article p {
|
| 361 |
+
margin: 10px 0;
|
| 362 |
+
color: #666;
|
| 363 |
+
font-size: 15px;
|
| 364 |
+
line-height: 1.6;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.news-article a {
|
| 368 |
+
color: #4a8fdf;
|
| 369 |
+
text-decoration: none;
|
| 370 |
+
font-weight: bold;
|
| 371 |
+
display: inline-block;
|
| 372 |
+
margin-top: 15px;
|
| 373 |
+
transition: color 0.3s ease;
|
| 374 |
+
border-color: #4a8fdf;
|
| 375 |
+
border-radius: 2px;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.news-article a:hover {
|
| 379 |
+
color: #34495e;
|
| 380 |
+
text-decoration: underline;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* Dark Mode Styles */
|
| 384 |
+
.dark-mode .news-section {
|
| 385 |
+
background: linear-gradient(135deg, #233250, #131b2b);
|
| 386 |
+
border-color: #4a8fdf;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.dark-mode .news-article {
|
| 390 |
+
background-color: #131b2b;
|
| 391 |
+
border-color: #4a8fdf;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.dark-mode .news-article h3 {
|
| 395 |
+
color: #ddd;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.dark-mode .news-article p {
|
| 399 |
+
color: #bbb;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.dark-mode .news-article a {
|
| 403 |
+
color: #4a8fdf;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.dark-mode .news-article a:hover {
|
| 407 |
+
color: #34495e;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
@keyframes glowing {
|
| 411 |
+
0% {
|
| 412 |
+
box-shadow: 0 0 15px rgba(74, 143, 223, 0.6);
|
| 413 |
+
}
|
| 414 |
+
100% {
|
| 415 |
+
box-shadow: 0 0 30px rgba(74, 143, 223, 1), 0 0 50px rgba(74,143,223,0.8);
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
/* Loading Spinner */
|
| 420 |
+
.loading-spinner {
|
| 421 |
+
display: flex;
|
| 422 |
+
justify-content: center;
|
| 423 |
+
align-items: center;
|
| 424 |
+
height: 100px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.loading-spinner::after {
|
| 428 |
+
content: "";
|
| 429 |
+
width: 40px;
|
| 430 |
+
height: 40px;
|
| 431 |
+
border: 4px solid #4a8fdf;
|
| 432 |
+
border-top-color: transparent;
|
| 433 |
+
border-radius: 50%;
|
| 434 |
+
animation: spin 1s linear infinite;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
@keyframes spin {
|
| 438 |
+
to {
|
| 439 |
+
transform: rotate(360deg);
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
/* Light mode scrollbar for news container */
|
| 445 |
+
#news-container {
|
| 446 |
+
scrollbar-width: thin;
|
| 447 |
+
scrollbar-color: #4a8fdf #e0e7ef;
|
| 448 |
+
}
|
| 449 |
+
#news-container::-webkit-scrollbar {
|
| 450 |
+
width: 10px;
|
| 451 |
+
background: #e0e7ef;
|
| 452 |
+
}
|
| 453 |
+
#news-container::-webkit-scrollbar-thumb {
|
| 454 |
+
background: #4a8fdf;
|
| 455 |
+
border-radius: 8px;
|
| 456 |
+
}
|
| 457 |
+
#news-container::-webkit-scrollbar-thumb:hover {
|
| 458 |
+
background: #357abd;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
/* Dark mode scrollbar for news container */
|
| 462 |
+
.dark-mode #news-container {
|
| 463 |
+
scrollbar-color: #4a8fdf #243342;
|
| 464 |
+
}
|
| 465 |
+
.dark-mode #news-container::-webkit-scrollbar {
|
| 466 |
+
background: #243342;
|
| 467 |
+
}
|
| 468 |
+
.dark-mode #news-container::-webkit-scrollbar-thumb {
|
| 469 |
+
background: #4a8fdf;
|
| 470 |
+
}
|
| 471 |
+
.dark-mode #news-container::-webkit-scrollbar-thumb:hover {
|
| 472 |
+
background: #357abd;
|
| 473 |
+
}
|
| 474 |
+
.custom-introjs-tooltip {
|
| 475 |
+
font-size: 1.05rem;
|
| 476 |
+
color: #222;
|
| 477 |
+
}
|
| 478 |
+
.introjs-tooltip {
|
| 479 |
+
z-index: 11000 !important;
|
| 480 |
+
}
|
static/css/predict.css
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.predictor-container {
|
| 2 |
+
width: 90%;
|
| 3 |
+
max-width: 1100px;
|
| 4 |
+
margin: 40px auto 0 auto;
|
| 5 |
+
padding: 24px 24px 40px 24px;
|
| 6 |
+
background: #fff;
|
| 7 |
+
border-radius: 18px;
|
| 8 |
+
box-shadow: 0 4px 32px rgba(74,143,223,0.08);
|
| 9 |
+
display: flex;
|
| 10 |
+
flex-direction: column;
|
| 11 |
+
align-items: stretch;
|
| 12 |
+
min-height: 300px;
|
| 13 |
+
transition: min-height 0.3s, height 0.3s;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.dark-mode .predictor-container {
|
| 17 |
+
background-color: #131b2b;
|
| 18 |
+
color: #ffffff;
|
| 19 |
+
box-shadow: 0 0 20px rgba(255, 255, 255, 0.05);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.predictor-container h1 {
|
| 23 |
+
font-size: 2.2rem;
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #4a8fdf;
|
| 26 |
+
margin-bottom: 30px;
|
| 27 |
+
text-transform: uppercase;
|
| 28 |
+
letter-spacing: 1px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.predictor-form {
|
| 32 |
+
display: flex;
|
| 33 |
+
flex-direction: column;
|
| 34 |
+
gap: 20px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.form-group {
|
| 38 |
+
display: flex;
|
| 39 |
+
flex-direction: column;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.form-group label {
|
| 43 |
+
margin-bottom: 8px;
|
| 44 |
+
font-weight: bold;
|
| 45 |
+
color: #131b2b;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.dark-mode .form-group label {
|
| 49 |
+
color: #ddd;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.predictor-form select,
|
| 53 |
+
.predictor-form input[type="date"] {
|
| 54 |
+
padding: 12px 15px;
|
| 55 |
+
border: 2px solid #ddd;
|
| 56 |
+
border-radius: 8px;
|
| 57 |
+
font-size: 16px;
|
| 58 |
+
background-color: #f4f2ec;
|
| 59 |
+
color: #000;
|
| 60 |
+
transition: border 0.3s, background-color 0.3s;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.predictor-form select:focus,
|
| 64 |
+
.predictor-form input[type="date"]:focus {
|
| 65 |
+
border-color: #4a8fdf;
|
| 66 |
+
outline: none;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.dark-mode .predictor-form select,
|
| 70 |
+
.dark-mode .predictor-form input[type="date"] {
|
| 71 |
+
background-color: #18233a;
|
| 72 |
+
color: #fff;
|
| 73 |
+
border-color: #555;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.radio-group {
|
| 77 |
+
display: flex;
|
| 78 |
+
flex-wrap: wrap;
|
| 79 |
+
gap: 20px;
|
| 80 |
+
margin-top: 10px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.radio-option {
|
| 84 |
+
display: flex;
|
| 85 |
+
align-items: center;
|
| 86 |
+
gap: 8px;
|
| 87 |
+
font-size: 15px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.radio-option input {
|
| 91 |
+
margin: 0;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.predict-btn {
|
| 95 |
+
width: 100%;
|
| 96 |
+
padding: 14px 0;
|
| 97 |
+
background-color: #4a8fdf;
|
| 98 |
+
color: white;
|
| 99 |
+
border: none;
|
| 100 |
+
border-radius: 8px;
|
| 101 |
+
font-size: 16px;
|
| 102 |
+
font-weight: bold;
|
| 103 |
+
cursor: pointer;
|
| 104 |
+
transition: background-color 0.3s;
|
| 105 |
+
box-shadow: 0 4px 10px rgba(74, 143, 223, 0.3);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.predict-btn:hover {
|
| 109 |
+
background-color: #377ddf;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.result-section {
|
| 113 |
+
margin-top: 50px;
|
| 114 |
+
animation: fadeIn 0.6s ease-in-out;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.result-section {
|
| 118 |
+
margin-top: 32px;
|
| 119 |
+
background: #4a8fdf;
|
| 120 |
+
border-radius: 12px;
|
| 121 |
+
padding: 24px;
|
| 122 |
+
box-shadow: 0 2px 16px rgba(74,143,223,0.06);
|
| 123 |
+
width: 100%;
|
| 124 |
+
display: flex;
|
| 125 |
+
flex-direction: column;
|
| 126 |
+
align-items: stretch;
|
| 127 |
+
transition: min-height 0.3s, height 0.3s;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.prediction-result {
|
| 131 |
+
background-color: #f8f9fa;
|
| 132 |
+
border-left: 5px solid #4a8fdf;
|
| 133 |
+
padding: 20px 30px;
|
| 134 |
+
border-radius: 10px;
|
| 135 |
+
color: #2c3e50;
|
| 136 |
+
font-size: 16px;
|
| 137 |
+
margin-top: 20px;
|
| 138 |
+
}
|
| 139 |
+
.predictor-container,
|
| 140 |
+
.result-section {
|
| 141 |
+
overflow: hidden;
|
| 142 |
+
position: relative;
|
| 143 |
+
/* margin-bottom already present for footer gap */
|
| 144 |
+
}
|
| 145 |
+
.dark-mode .prediction-result {
|
| 146 |
+
background-color: #2c3e50;
|
| 147 |
+
color: #fff;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.prediction-value {
|
| 151 |
+
font-size: 28px;
|
| 152 |
+
font-weight: bold;
|
| 153 |
+
color: #4a8fdf;
|
| 154 |
+
margin: 10px 0;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.confidence {
|
| 158 |
+
color: #666;
|
| 159 |
+
font-style: italic;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.dark-mode .confidence {
|
| 163 |
+
color: #bbb;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.chart-container {
|
| 167 |
+
width: 100%;
|
| 168 |
+
max-width: 100%;
|
| 169 |
+
min-height: 450px;
|
| 170 |
+
position: relative;
|
| 171 |
+
margin-top: 30px;
|
| 172 |
+
display: flex;
|
| 173 |
+
flex-direction: column;
|
| 174 |
+
gap: 32px;
|
| 175 |
+
background-color: #fff;
|
| 176 |
+
box-sizing: border-box;
|
| 177 |
+
border-radius: 10px;
|
| 178 |
+
padding: 20px;
|
| 179 |
+
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
|
| 180 |
+
overflow-x: visible; /* Remove horizontal scroll */
|
| 181 |
+
}
|
| 182 |
+
/* Each graph will expand as needed */
|
| 183 |
+
#prediction-chart,
|
| 184 |
+
.extra-graph,
|
| 185 |
+
#prediction-chart-historical,
|
| 186 |
+
#prediction-chart-sentiment {
|
| 187 |
+
width: 100% !important;
|
| 188 |
+
max-width: 100% !important;
|
| 189 |
+
min-width: 0 !important;
|
| 190 |
+
box-sizing: border-box;
|
| 191 |
+
min-height: 400px;
|
| 192 |
+
height: auto !important;
|
| 193 |
+
margin-bottom: 0;
|
| 194 |
+
display: block;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.dark-mode .chart-container {
|
| 198 |
+
background-color: #1e2a38;
|
| 199 |
+
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.05);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
@keyframes fadeIn {
|
| 203 |
+
0% {
|
| 204 |
+
opacity: 0;
|
| 205 |
+
transform: translateY(15px);
|
| 206 |
+
}
|
| 207 |
+
100% {
|
| 208 |
+
opacity: 1;
|
| 209 |
+
transform: translateY(0);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* Responsive */
|
| 214 |
+
@media (max-width: 768px) {
|
| 215 |
+
.predictor-container {
|
| 216 |
+
padding: 20px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.radio-group {
|
| 220 |
+
flex-direction: column;
|
| 221 |
+
gap: 10px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.prediction-result,
|
| 225 |
+
.chart-container {
|
| 226 |
+
padding: 15px;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* Glowing effect */
|
| 231 |
+
@keyframes glowBorder {
|
| 232 |
+
0% {
|
| 233 |
+
box-shadow: 0 0 10px rgba(74,143,223,0.6);
|
| 234 |
+
}
|
| 235 |
+
50% {
|
| 236 |
+
box-shadow: 0 0 20px rgba(74,143,223,0.8), 0 0 30px rgba(74,143,223,0.6);
|
| 237 |
+
}
|
| 238 |
+
100% {
|
| 239 |
+
box-shadow: 0 0 10px rgba(74,143,223,0.6);
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
/* Apply glow to predictor container */
|
| 243 |
+
.predictor-container {
|
| 244 |
+
animation: glowBorder 2.5s infinite alternate;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.dark-mode .predictor-container {
|
| 248 |
+
animation: glowBorder 2s infinite alternate;
|
| 249 |
+
}
|
| 250 |
+
/* Style for the epochs dropdown and custom input */
|
| 251 |
+
#epochs, #custom-epochs {
|
| 252 |
+
padding: 12px 15px;
|
| 253 |
+
border: 2px solid #ddd;
|
| 254 |
+
border-radius: 8px;
|
| 255 |
+
font-size: 16px;
|
| 256 |
+
background-color: #f4f2ec;
|
| 257 |
+
color: #000;
|
| 258 |
+
margin-top: 8px;
|
| 259 |
+
transition: border 0.3s, background-color 0.3s;
|
| 260 |
+
width: 100%;
|
| 261 |
+
box-sizing: border-box;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
#epochs:focus, #custom-epochs:focus {
|
| 265 |
+
border-color: #4a8fdf;
|
| 266 |
+
outline: none;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.dark-mode #epochs,
|
| 270 |
+
.dark-mode #custom-epochs {
|
| 271 |
+
background-color: #18233a;
|
| 272 |
+
color: #fff;
|
| 273 |
+
border-color: #555;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
/* Optional: Add spacing below the custom input */
|
| 277 |
+
#custom-epochs {
|
| 278 |
+
margin-bottom: 8px;
|
| 279 |
+
}
|
| 280 |
+
#loading-overlay {
|
| 281 |
+
position: fixed;
|
| 282 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 283 |
+
background: rgba(255,255,255,0.1);
|
| 284 |
+
z-index: 9999;
|
| 285 |
+
display: flex;
|
| 286 |
+
align-items: center;
|
| 287 |
+
justify-content: center;
|
| 288 |
+
}
|
| 289 |
+
#message-box {
|
| 290 |
+
z-index: 9999;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.loader-container {
|
| 294 |
+
display: flex;
|
| 295 |
+
flex-direction: column;
|
| 296 |
+
align-items: center;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.spinner {
|
| 300 |
+
border: 6px solid #e0e0e0;
|
| 301 |
+
border-top: 6px solid #0077ff;
|
| 302 |
+
border-radius: 50%;
|
| 303 |
+
width: 54px;
|
| 304 |
+
height: 54px;
|
| 305 |
+
animation: spin 1s linear infinite;
|
| 306 |
+
margin-bottom: 18px;
|
| 307 |
+
box-shadow: 0 0 0 2px #fff; /* White border around the spinner */
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
@keyframes spin {
|
| 311 |
+
0% { transform: rotate(0deg);}
|
| 312 |
+
100% { transform: rotate(360deg);}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/* Add a white border to the loading text */
|
| 316 |
+
#progress-text {
|
| 317 |
+
font-size: 18px;
|
| 318 |
+
color: #0077ff;
|
| 319 |
+
font-weight: bold;
|
| 320 |
+
text-shadow: 0 0 2px #fff; /* White border effect for text */
|
| 321 |
+
padding: 5px;
|
| 322 |
+
background-color: rgba(255, 255, 255, 0.8); /* Optional: Add a semi-transparent background */
|
| 323 |
+
border-radius: 5px; /* Rounded corners for the text background */
|
| 324 |
+
display: inline-block; /* Ensure the background wraps around the text */
|
| 325 |
+
}
|
| 326 |
+
.graph-area {
|
| 327 |
+
background: #fff;
|
| 328 |
+
border-radius: 10px;
|
| 329 |
+
box-shadow: 0 2px 12px rgba(74,143,223,0.07);
|
| 330 |
+
padding: 18px 12px 12px 12px;
|
| 331 |
+
margin-bottom: 24px;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.dark-mode .graph-area {
|
| 335 |
+
background: #1e2a38;
|
| 336 |
+
box-shadow: 0 2px 12px rgba(255,255,255,0.04);
|
| 337 |
+
}
|
| 338 |
+
.toggle-switch {
|
| 339 |
+
position: relative;
|
| 340 |
+
display: inline-block;
|
| 341 |
+
width: 50px;
|
| 342 |
+
height: 25px;
|
| 343 |
+
}
|
| 344 |
+
.toggle-switch input {
|
| 345 |
+
opacity: 0;
|
| 346 |
+
width: 0;
|
| 347 |
+
height: 0;
|
| 348 |
+
}
|
| 349 |
+
.slider {
|
| 350 |
+
position: absolute;
|
| 351 |
+
cursor: pointer;
|
| 352 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 353 |
+
background-color: #ccc;
|
| 354 |
+
transition: .4s;
|
| 355 |
+
border-radius: 25px;
|
| 356 |
+
}
|
| 357 |
+
.toggle-circle {
|
| 358 |
+
position: absolute;
|
| 359 |
+
left: 4px;
|
| 360 |
+
bottom: 3px;
|
| 361 |
+
width: 18px;
|
| 362 |
+
height: 18px;
|
| 363 |
+
background: #fff;
|
| 364 |
+
border-radius: 50%;
|
| 365 |
+
display: flex;
|
| 366 |
+
align-items: center;
|
| 367 |
+
justify-content: center;
|
| 368 |
+
transition: transform 0.4s;
|
| 369 |
+
z-index: 2;
|
| 370 |
+
font-size: 14px;
|
| 371 |
+
}
|
| 372 |
+
.toggle-icon {
|
| 373 |
+
transition: color 0.4s, content 0.4s;
|
| 374 |
+
color: #FFD600; /* Sun color */
|
| 375 |
+
}
|
| 376 |
+
input:checked + .slider .toggle-circle {
|
| 377 |
+
transform: translateX(24px);
|
| 378 |
+
}
|
| 379 |
+
input:checked + .slider .toggle-icon {
|
| 380 |
+
color: #4a8fdf; /* Moon color */
|
| 381 |
+
}
|
| 382 |
+
/* Disclaimer Overlay Styles */
|
| 383 |
+
#disclaimer-overlay {
|
| 384 |
+
position: fixed;
|
| 385 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 386 |
+
background: rgba(30, 42, 56, 0.85);
|
| 387 |
+
z-index: 10000;
|
| 388 |
+
display: flex;
|
| 389 |
+
align-items: center;
|
| 390 |
+
justify-content: center;
|
| 391 |
+
}
|
| 392 |
+
.disclaimer-box {
|
| 393 |
+
background: #fff;
|
| 394 |
+
color: #222;
|
| 395 |
+
max-width: 420px;
|
| 396 |
+
width: 90vw;
|
| 397 |
+
padding: 32px 28px 24px 28px;
|
| 398 |
+
border-radius: 14px;
|
| 399 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
| 400 |
+
text-align: left;
|
| 401 |
+
position: relative;
|
| 402 |
+
font-size: 1rem;
|
| 403 |
+
line-height: 1.6;
|
| 404 |
+
animation: fadeIn 0.5s;
|
| 405 |
+
}
|
| 406 |
+
.disclaimer-box h2 {
|
| 407 |
+
margin-top: 0;
|
| 408 |
+
color: #4a8fdf;
|
| 409 |
+
font-size: 1.3rem;
|
| 410 |
+
}
|
| 411 |
+
.disclaimer-box ul {
|
| 412 |
+
padding-left: 18px;
|
| 413 |
+
margin-bottom: 18px;
|
| 414 |
+
}
|
| 415 |
+
.disclaimer-risk {
|
| 416 |
+
font-size: 0.98rem;
|
| 417 |
+
color: #c62828;
|
| 418 |
+
}
|
| 419 |
+
#close-disclaimer {
|
| 420 |
+
position: absolute;
|
| 421 |
+
top: 12px;
|
| 422 |
+
right: 14px;
|
| 423 |
+
background: #4a8fdf;
|
| 424 |
+
color: #fff;
|
| 425 |
+
border: none;
|
| 426 |
+
border-radius: 4px;
|
| 427 |
+
font-size: 1.1rem;
|
| 428 |
+
padding: 2px 12px;
|
| 429 |
+
cursor: pointer;
|
| 430 |
+
transition: background 0.2s;
|
| 431 |
+
}
|
| 432 |
+
#close-disclaimer:hover {
|
| 433 |
+
background: #357abd;
|
| 434 |
+
}
|
| 435 |
+
@media (max-width: 600px) {
|
| 436 |
+
.disclaimer-box {
|
| 437 |
+
padding: 18px 8px 16px 8px;
|
| 438 |
+
font-size: 0.98rem;
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
.custom-introjs-tooltip {
|
| 442 |
+
font-size: 1.05rem;
|
| 443 |
+
color: #222;
|
| 444 |
+
}
|
| 445 |
+
.introjs-tooltip {
|
| 446 |
+
z-index: 11000 !important;
|
| 447 |
+
}
|
| 448 |
+
.news-sentiment {
|
| 449 |
+
margin-top: 20px;
|
| 450 |
+
padding: 15px;
|
| 451 |
+
background-color: #f8f9fa;
|
| 452 |
+
border-left: 5px solid #4a8fdf;
|
| 453 |
+
border-radius: 10px;
|
| 454 |
+
color: #2c3e50;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.dark-mode .news-sentiment {
|
| 458 |
+
background-color: #2c3e50;
|
| 459 |
+
color: #fff;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.news-sentiment h3 {
|
| 463 |
+
margin: 0 0 10px;
|
| 464 |
+
font-size: 1.2rem;
|
| 465 |
+
color: #4a8fdf;
|
| 466 |
+
}
|
static/css/styles.css
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
nav ul {
|
| 2 |
+
list-style: none;
|
| 3 |
+
display: flex;
|
| 4 |
+
background-color: #333;
|
| 5 |
+
padding: 0;
|
| 6 |
+
}
|
| 7 |
+
nav ul li {
|
| 8 |
+
margin: 0 10px;
|
| 9 |
+
}
|
| 10 |
+
nav ul li a {
|
| 11 |
+
color: white;
|
| 12 |
+
text-decoration: none;
|
| 13 |
+
padding: 10px 15px;
|
| 14 |
+
display: block;
|
| 15 |
+
}
|
| 16 |
+
nav ul li a:hover {
|
| 17 |
+
background-color: #555;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* Example: Start of your CSS */
|
| 21 |
+
* {
|
| 22 |
+
margin: 0;
|
| 23 |
+
padding: 0;
|
| 24 |
+
box-sizing: border-box;
|
| 25 |
+
}
|
| 26 |
+
body {
|
| 27 |
+
font-family: Arial, sans-serif;
|
| 28 |
+
background-color: #f4f2ec;
|
| 29 |
+
}
|
| 30 |
+
* {
|
| 31 |
+
margin: 0;
|
| 32 |
+
padding: 0;
|
| 33 |
+
box-sizing: border-box;
|
| 34 |
+
}
|
| 35 |
+
body {
|
| 36 |
+
margin: 0;
|
| 37 |
+
font-family: Arial, sans-serif;
|
| 38 |
+
background-color: #f4f2ec;
|
| 39 |
+
color: #000;
|
| 40 |
+
transition: background-color 0.3s, color 0.3s;
|
| 41 |
+
opacity: 0;
|
| 42 |
+
transform: scale(1.05);
|
| 43 |
+
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out;
|
| 44 |
+
}
|
| 45 |
+
body.loaded {
|
| 46 |
+
opacity: 1;
|
| 47 |
+
transform: scale(1);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* Transition Overlay (Expanding Circle Effect) */
|
| 51 |
+
.transition-overlay {
|
| 52 |
+
position: fixed;
|
| 53 |
+
top: 50%;
|
| 54 |
+
left: 50%;
|
| 55 |
+
width: 100px;
|
| 56 |
+
height: 100px;
|
| 57 |
+
background: radial-gradient(circle, #4a8fdf, #000); /* Green fading into black */
|
| 58 |
+
border-radius: 50%;
|
| 59 |
+
transform: translate(-50%, -50%) scale(0);
|
| 60 |
+
transition: transform 0.9s ease-in-out;
|
| 61 |
+
z-index: 9999;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* Expand circle effect when navigating */
|
| 65 |
+
/* Expand */
|
| 66 |
+
.transition-overlay.expand {
|
| 67 |
+
transform: translate(-50%, -50%) scale(30);
|
| 68 |
+
}
|
| 69 |
+
/* Contract */
|
| 70 |
+
.transition-overlay.contract {
|
| 71 |
+
transform: translate(-50%, -50%) scale(0);
|
| 72 |
+
}
|
| 73 |
+
/* Smooth transition for links */
|
| 74 |
+
a {
|
| 75 |
+
text-decoration: none;
|
| 76 |
+
color: #4a8fdf;
|
| 77 |
+
transition: color 0.3s ease-in-out;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
a:hover {
|
| 81 |
+
color: #fff;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.dark-mode {
|
| 85 |
+
background-color: #131b2b;
|
| 86 |
+
color: #fff;
|
| 87 |
+
}
|
| 88 |
+
.navbar {
|
| 89 |
+
display: flex;
|
| 90 |
+
justify-content: space-between;
|
| 91 |
+
align-items: center;
|
| 92 |
+
padding: 20px 30px;
|
| 93 |
+
background: #f4f2ec;
|
| 94 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 95 |
+
font-size: 18px;
|
| 96 |
+
font-weight: bold;
|
| 97 |
+
position: sticky;
|
| 98 |
+
top: 0;
|
| 99 |
+
z-index: 1000;
|
| 100 |
+
}
|
| 101 |
+
.dark-mode .navbar {
|
| 102 |
+
background: #243342;
|
| 103 |
+
}
|
| 104 |
+
.navbar a {
|
| 105 |
+
text-decoration: none;
|
| 106 |
+
color: #000;
|
| 107 |
+
margin: 0 20px;
|
| 108 |
+
font-size: 18px;
|
| 109 |
+
font-weight: bold;
|
| 110 |
+
}
|
| 111 |
+
.dark-mode .navbar a {
|
| 112 |
+
color: #fff;
|
| 113 |
+
}
|
| 114 |
+
/* Highlighted Section */
|
| 115 |
+
.highlight-container {
|
| 116 |
+
position: relative; /* Changed from absolute */
|
| 117 |
+
text-align: center;
|
| 118 |
+
padding: 50px;
|
| 119 |
+
background: rgba(255, 255, 255, 0.9);
|
| 120 |
+
box-shadow: 0px 0px 30px rgba(74,143,223, 0.7);
|
| 121 |
+
border-radius: 15px;
|
| 122 |
+
animation: fadeIn 1s ease-in-out;
|
| 123 |
+
margin: 100px auto; /* Center and add margin */
|
| 124 |
+
max-width: 800px; /* Limit width for better readability */
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.dark-mode .highlight-container {
|
| 128 |
+
background: #131b2b;
|
| 129 |
+
box-shadow: 0px 0px 30px rgba(74,143,223, 0.7);
|
| 130 |
+
}
|
| 131 |
+
/* Animated Glow Effect */
|
| 132 |
+
@keyframes glow {
|
| 133 |
+
0% { box-shadow: 0px 0px 10px rgba(74,143,223, 0.5); }
|
| 134 |
+
50% { box-shadow: 0px 0px 20px rgba(74,143,223, 1); }
|
| 135 |
+
100% { box-shadow: 0px 0px 10px rgba(74,143,223, 0.5); }
|
| 136 |
+
}
|
| 137 |
+
@keyframes fadeIn {
|
| 138 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 139 |
+
to { opacity: 1; transform: translateY(0px); }
|
| 140 |
+
}
|
| 141 |
+
@keyframes fadeBounce {
|
| 142 |
+
0% { opacity: 0; transform: translateY(-30px); }
|
| 143 |
+
50% { opacity: 1; transform: translateY(5px); }
|
| 144 |
+
100% { transform: translateY(0); }
|
| 145 |
+
}
|
| 146 |
+
.animated-title {
|
| 147 |
+
font-size: 48px;
|
| 148 |
+
font-weight: bold;
|
| 149 |
+
color: #4a8fdf;
|
| 150 |
+
text-align: center;
|
| 151 |
+
margin-bottom: 20px;
|
| 152 |
+
animation: fadeBounce 1.5s ease-in-out;
|
| 153 |
+
}
|
| 154 |
+
.dark-mode .animated-title {
|
| 155 |
+
color: #4a8fdf;
|
| 156 |
+
}
|
| 157 |
+
.description {
|
| 158 |
+
font-size: 26px;
|
| 159 |
+
font-weight: bold;
|
| 160 |
+
color: #4a8fdf;
|
| 161 |
+
margin-bottom: 20px;
|
| 162 |
+
animation: glow 1.5s infinite alternate;
|
| 163 |
+
}
|
| 164 |
+
.dark-mode .description {
|
| 165 |
+
color: #4a8fdf;
|
| 166 |
+
}
|
| 167 |
+
/* Centered Search Bar */
|
| 168 |
+
.search-container {
|
| 169 |
+
width: 400px;
|
| 170 |
+
position: relative;
|
| 171 |
+
margin: 0 auto;
|
| 172 |
+
}
|
| 173 |
+
.search-box {
|
| 174 |
+
width: 100%;
|
| 175 |
+
padding: 14px 50px 14px 16px;
|
| 176 |
+
font-size: 18px;
|
| 177 |
+
border: 3px solid #4a8fdf;
|
| 178 |
+
border-radius: 30px;
|
| 179 |
+
outline: none;
|
| 180 |
+
transition: 0.3s;
|
| 181 |
+
animation: glow 1.5s infinite alternate;
|
| 182 |
+
}
|
| 183 |
+
.search-box:focus {
|
| 184 |
+
border-color: #ff9800;
|
| 185 |
+
box-shadow: 0 0 15px rgba(255, 152, 0, 0.7);
|
| 186 |
+
}
|
| 187 |
+
.search-container i {
|
| 188 |
+
position: absolute;
|
| 189 |
+
right: 15px;
|
| 190 |
+
top: 50%;
|
| 191 |
+
transform: translateY(-50%);
|
| 192 |
+
font-size: 20px;
|
| 193 |
+
color: #777;
|
| 194 |
+
cursor: pointer;
|
| 195 |
+
}
|
| 196 |
+
.stock-suggestions {
|
| 197 |
+
margin-top: 15px;
|
| 198 |
+
font-size: 16px;
|
| 199 |
+
color: #333;
|
| 200 |
+
}
|
| 201 |
+
.dark-mode .stock-suggestions {
|
| 202 |
+
color: #ddd;
|
| 203 |
+
}
|
| 204 |
+
.suggestions-list {
|
| 205 |
+
display: flex;
|
| 206 |
+
justify-content: center;
|
| 207 |
+
gap: 10px;
|
| 208 |
+
flex-wrap: wrap;
|
| 209 |
+
}
|
| 210 |
+
.suggestion {
|
| 211 |
+
background-color: #f0f0f0;
|
| 212 |
+
padding: 8px 12px;
|
| 213 |
+
border-radius: 5px;
|
| 214 |
+
cursor: pointer;
|
| 215 |
+
transition: 0.3s;
|
| 216 |
+
}
|
| 217 |
+
.suggestion:hover {
|
| 218 |
+
background-color: #4a8fdf;
|
| 219 |
+
color: white;
|
| 220 |
+
}
|
| 221 |
+
.dark-mode .suggestion {
|
| 222 |
+
background-color: #243342;
|
| 223 |
+
color: white;
|
| 224 |
+
}
|
| 225 |
+
.dark-mode .suggestion:hover {
|
| 226 |
+
background-color: #4a8fdf;
|
| 227 |
+
}
|
| 228 |
+
.dark-mode .search-box {
|
| 229 |
+
background-color: #131b2b;
|
| 230 |
+
color: white;
|
| 231 |
+
border-color: #4a8fdf;
|
| 232 |
+
}
|
| 233 |
+
.dark-mode .search-box:focus {
|
| 234 |
+
border-color: #ff9800;
|
| 235 |
+
}
|
| 236 |
+
.dark-mode .search-container i {
|
| 237 |
+
color: #aaa;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.dropdown {
|
| 241 |
+
position: relative;
|
| 242 |
+
display: inline-block;
|
| 243 |
+
}
|
| 244 |
+
.dropdown-content {
|
| 245 |
+
display: none;
|
| 246 |
+
position: absolute;
|
| 247 |
+
background-color: #f9f9f9;
|
| 248 |
+
min-width: 160px;
|
| 249 |
+
box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
|
| 250 |
+
z-index: 1;
|
| 251 |
+
}
|
| 252 |
+
.dropdown-content a {
|
| 253 |
+
color: black;
|
| 254 |
+
padding: 10px 16px;
|
| 255 |
+
display: block;
|
| 256 |
+
text-decoration: none;
|
| 257 |
+
}
|
| 258 |
+
.dropdown-content a:hover {
|
| 259 |
+
background-color: #ddd;
|
| 260 |
+
}
|
| 261 |
+
.dropdown:hover .dropdown-content {
|
| 262 |
+
display: block;
|
| 263 |
+
}
|
| 264 |
+
.dark-mode .dropdown-content {
|
| 265 |
+
background-color: #243342;
|
| 266 |
+
}
|
| 267 |
+
.dark-mode .dropdown-content a {
|
| 268 |
+
color: white;
|
| 269 |
+
}
|
| 270 |
+
.dark-mode .dropdown-content a:hover {
|
| 271 |
+
background-color: #374f66;
|
| 272 |
+
}
|
| 273 |
+
.toggle-switch {
|
| 274 |
+
position: relative;
|
| 275 |
+
display: inline-block;
|
| 276 |
+
width: 50px;
|
| 277 |
+
height: 25px;
|
| 278 |
+
}
|
| 279 |
+
.toggle-switch input {
|
| 280 |
+
opacity: 0;
|
| 281 |
+
width: 0;
|
| 282 |
+
height: 0;
|
| 283 |
+
}
|
| 284 |
+
.slider {
|
| 285 |
+
position: absolute;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 288 |
+
background-color: #ccc;
|
| 289 |
+
transition: .4s;
|
| 290 |
+
border-radius: 25px;
|
| 291 |
+
}
|
| 292 |
+
.toggle-circle {
|
| 293 |
+
position: absolute;
|
| 294 |
+
left: 4px;
|
| 295 |
+
bottom: 3px;
|
| 296 |
+
width: 18px;
|
| 297 |
+
height: 18px;
|
| 298 |
+
background: #fff;
|
| 299 |
+
border-radius: 50%;
|
| 300 |
+
display: flex;
|
| 301 |
+
align-items: center;
|
| 302 |
+
justify-content: center;
|
| 303 |
+
transition: transform 0.4s;
|
| 304 |
+
z-index: 2;
|
| 305 |
+
font-size: 14px;
|
| 306 |
+
}
|
| 307 |
+
.toggle-icon {
|
| 308 |
+
transition: color 0.4s, content 0.4s;
|
| 309 |
+
color: #FFD600; /* Sun color */
|
| 310 |
+
}
|
| 311 |
+
input:checked + .slider {
|
| 312 |
+
background-color: #4a8fdf;
|
| 313 |
+
}
|
| 314 |
+
input:checked + .slider .toggle-circle {
|
| 315 |
+
transform: translateX(24px);
|
| 316 |
+
}
|
| 317 |
+
input:checked + .slider .toggle-icon {
|
| 318 |
+
color: #4a8fdf; /* Moon color */
|
| 319 |
+
/* Use content swap via JS for moon icon */
|
| 320 |
+
}
|
| 321 |
+
.about-us {
|
| 322 |
+
text-align: center;
|
| 323 |
+
padding: 40px;
|
| 324 |
+
background-color: #fff;
|
| 325 |
+
border-radius: 10px;
|
| 326 |
+
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
| 327 |
+
width: 80%;
|
| 328 |
+
margin: 40px auto; /* Center and add margin */
|
| 329 |
+
margin-top: 200px; /* Add space between search bar and about-us */
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.dark-mode .about-us {
|
| 333 |
+
background-color: #131b2b;
|
| 334 |
+
color: #ddd;
|
| 335 |
+
}
|
| 336 |
+
#three-container {
|
| 337 |
+
width: 100%;
|
| 338 |
+
height: 400px;
|
| 339 |
+
position: relative;
|
| 340 |
+
margin: 40px auto; /* Add margin */
|
| 341 |
+
background-color: #000;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.container {
|
| 345 |
+
position: relative;
|
| 346 |
+
font-size: 7vw;
|
| 347 |
+
font-weight: bold;
|
| 348 |
+
text-transform: uppercase;
|
| 349 |
+
color: transparent;
|
| 350 |
+
-webkit-text-stroke: 2px #4a8fdf;
|
| 351 |
+
display: flex;
|
| 352 |
+
justify-content: center;
|
| 353 |
+
align-items: center;
|
| 354 |
+
margin-top: 10%;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.glow::before {
|
| 358 |
+
content: attr(data-text);
|
| 359 |
+
position: absolute;
|
| 360 |
+
color: #4a8fdf;
|
| 361 |
+
width: 0;
|
| 362 |
+
overflow: hidden;
|
| 363 |
+
white-space: nowrap;
|
| 364 |
+
animation: revealText 4s ease-in-out infinite alternate;
|
| 365 |
+
display: flex;
|
| 366 |
+
justify-content: center;
|
| 367 |
+
align-items: center;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
@keyframes revealText {
|
| 371 |
+
0% {
|
| 372 |
+
width: 0;
|
| 373 |
+
filter: blur(5px);
|
| 374 |
+
opacity: 0;
|
| 375 |
+
}
|
| 376 |
+
20% {
|
| 377 |
+
opacity: 1;
|
| 378 |
+
filter: blur(3px);
|
| 379 |
+
}
|
| 380 |
+
50% {
|
| 381 |
+
width: 100%;
|
| 382 |
+
filter: blur(0px);
|
| 383 |
+
}
|
| 384 |
+
70% {
|
| 385 |
+
width: 100%;
|
| 386 |
+
}
|
| 387 |
+
100% {
|
| 388 |
+
width: 0;
|
| 389 |
+
filter: blur(5px);
|
| 390 |
+
opacity: 0;
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
.cards-container {
|
| 394 |
+
display: flex;
|
| 395 |
+
gap: 20px;
|
| 396 |
+
justify-content: center;
|
| 397 |
+
align-items: stretch;
|
| 398 |
+
align-items: center;/
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.card {
|
| 402 |
+
background: #fff;
|
| 403 |
+
padding: 40px;
|
| 404 |
+
width: 220px;
|
| 405 |
+
height: 550px;
|
| 406 |
+
text-align: center;
|
| 407 |
+
border-radius: 10px;
|
| 408 |
+
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
| 409 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 410 |
+
margin-top: 200px;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.card:hover {
|
| 414 |
+
transform: translateY(-5px);
|
| 415 |
+
box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.2);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.card h3 {
|
| 419 |
+
margin: 0;
|
| 420 |
+
padding: 10px;
|
| 421 |
+
background: #007bff;
|
| 422 |
+
color: white;
|
| 423 |
+
border-radius: 5px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.card p {
|
| 427 |
+
margin-top: 15px;
|
| 428 |
+
color: #333;
|
| 429 |
+
font-size: 14px;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
/* Footer Styles */
|
| 433 |
+
footer {
|
| 434 |
+
background-color: #2c3e50;
|
| 435 |
+
color: #ecf0f1;
|
| 436 |
+
padding: 40px 20px;
|
| 437 |
+
font-family: Arial, sans-serif;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.footer-content {
|
| 441 |
+
display: flex;
|
| 442 |
+
justify-content: space-between;
|
| 443 |
+
flex-wrap: wrap;
|
| 444 |
+
gap: 20px;
|
| 445 |
+
max-width: 1200px;
|
| 446 |
+
margin: 0 auto;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.footer-section {
|
| 450 |
+
flex: 1;
|
| 451 |
+
min-width: 200px;
|
| 452 |
+
margin-bottom: 20px;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.footer-section h3 {
|
| 456 |
+
font-size: 18px;
|
| 457 |
+
margin-bottom: 15px;
|
| 458 |
+
color: #4a8fdf;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.footer-section ul {
|
| 462 |
+
list-style: none;
|
| 463 |
+
padding: 0;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.footer-section ul li {
|
| 467 |
+
margin-bottom: 10px;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.footer-section ul li a {
|
| 471 |
+
color: #ecf0f1;
|
| 472 |
+
text-decoration: none;
|
| 473 |
+
transition: color 0.3s ease;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.footer-section ul li a:hover {
|
| 477 |
+
color: #4a8fdf;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.footer-section form {
|
| 481 |
+
display: flex;
|
| 482 |
+
gap: 10px;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.footer-section input[type="email"] {
|
| 486 |
+
padding: 10px;
|
| 487 |
+
border: none;
|
| 488 |
+
border-radius: 5px;
|
| 489 |
+
width: 70%;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.footer-section button {
|
| 493 |
+
padding: 10px 20px;
|
| 494 |
+
background-color: #4a8fdf;
|
| 495 |
+
color: #fff;
|
| 496 |
+
border: none;
|
| 497 |
+
border-radius: 5px;
|
| 498 |
+
cursor: pointer;
|
| 499 |
+
transition: background-color 0.3s ease;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.footer-section button:hover {
|
| 503 |
+
background-color: #4a8fdf;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.footer-bottom {
|
| 507 |
+
text-align: center;
|
| 508 |
+
margin-top: 20px;
|
| 509 |
+
padding-top: 20px;
|
| 510 |
+
border-top: 1px solid #34495e;
|
| 511 |
+
font-size: 14px;
|
| 512 |
+
}
|
| 513 |
+
html {
|
| 514 |
+
scroll-behavior: smooth;
|
| 515 |
+
}
|
| 516 |
+
.fade-in {
|
| 517 |
+
opacity: 0;
|
| 518 |
+
transform: translateY(20px);
|
| 519 |
+
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.fade-in.visible {
|
| 523 |
+
opacity: 1;
|
| 524 |
+
transform: translateY(0);
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* Cards Container */
|
| 528 |
+
.cards-container {
|
| 529 |
+
display: flex;
|
| 530 |
+
gap: 30px;
|
| 531 |
+
justify-content: center;
|
| 532 |
+
align-items: center;
|
| 533 |
+
flex-wrap: wrap;
|
| 534 |
+
padding: 20px;
|
| 535 |
+
margin-top: 150px;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
/* Card Styling */
|
| 539 |
+
.card {
|
| 540 |
+
background: #fff;
|
| 541 |
+
padding: 30px;
|
| 542 |
+
width: 280px;
|
| 543 |
+
text-align: center;
|
| 544 |
+
border-radius: 15px;
|
| 545 |
+
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
|
| 546 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
|
| 547 |
+
position: relative;
|
| 548 |
+
overflow: hidden;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.card::before {
|
| 552 |
+
content: "";
|
| 553 |
+
position: absolute;
|
| 554 |
+
top: 0;
|
| 555 |
+
left: 0;
|
| 556 |
+
width: 100%;
|
| 557 |
+
height: 100%;
|
| 558 |
+
background: linear-gradient(45deg, #4a8fdf, #007bff);
|
| 559 |
+
opacity: 0;
|
| 560 |
+
transition: opacity 0.3s ease;
|
| 561 |
+
z-index: 1;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.card:hover::before {
|
| 565 |
+
opacity: 0.1;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.card:hover {
|
| 569 |
+
transform: translateY(-10px);
|
| 570 |
+
box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.2);
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.card h3 {
|
| 574 |
+
margin: 0;
|
| 575 |
+
padding: 15px;
|
| 576 |
+
background: linear-gradient(45deg, #4a8fdf, #007bff);
|
| 577 |
+
color: white;
|
| 578 |
+
border-radius: 10px;
|
| 579 |
+
font-size: 20px;
|
| 580 |
+
position: relative;
|
| 581 |
+
z-index: 2;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
.card p {
|
| 585 |
+
margin-top: 20px;
|
| 586 |
+
color: #555;
|
| 587 |
+
font-size: 15px;
|
| 588 |
+
line-height: 1.6;
|
| 589 |
+
position: relative;
|
| 590 |
+
z-index: 2;
|
| 591 |
+
}
|
| 592 |
+
.dark-mode .card {
|
| 593 |
+
background-color: #131b2b;
|
| 594 |
+
color: #ddd;
|
| 595 |
+
box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.05);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.dark-mode .card p {
|
| 599 |
+
margin-top: 20px;
|
| 600 |
+
color: #c9d1dd;
|
| 601 |
+
font-size: 15px;
|
| 602 |
+
line-height: 1.6;
|
| 603 |
+
position: relative;
|
| 604 |
+
z-index: 2;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Fade-in Animation */
|
| 608 |
+
.fade-in {
|
| 609 |
+
opacity: 0;
|
| 610 |
+
animation: fadeIn 1s ease-in-out forwards;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
@keyframes fadeIn {
|
| 614 |
+
from {
|
| 615 |
+
opacity: 0;
|
| 616 |
+
transform: translateY(20px);
|
| 617 |
+
}
|
| 618 |
+
to {
|
| 619 |
+
opacity: 1;
|
| 620 |
+
transform: translateY(0);
|
| 621 |
+
}
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
/* Responsive Design */
|
| 625 |
+
@media (max-width: 768px) {
|
| 626 |
+
.cards-container {
|
| 627 |
+
flex-direction: column;
|
| 628 |
+
gap: 20px;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.card {
|
| 632 |
+
width: 90%;
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
/* Include the rest from the style block */
|
| 636 |
+
/* Glow effect for card and about-us containers */
|
| 637 |
+
.card, .about-us {
|
| 638 |
+
animation: glow 2s infinite alternate;
|
| 639 |
+
box-shadow: 0 0 20px #4a8fdf, 0 0 40px #4a8fdf33;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.dark-mode .card, .dark-mode .about-us {
|
| 643 |
+
animation: glow 2s infinite alternate;
|
| 644 |
+
box-shadow: 0 0 30px #4a8fdf, 0 0 60px #4a8fdf33;
|
| 645 |
+
}
|
| 646 |
+
.custom-introjs-tooltip {
|
| 647 |
+
font-size: 1.05rem;
|
| 648 |
+
color: #222;
|
| 649 |
+
}
|
| 650 |
+
.introjs-tooltip {
|
| 651 |
+
z-index: 11000 !important;
|
| 652 |
+
}
|
static/images/bg.png
ADDED
|
Git LFS Details
|
static/js/auth.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeFirebase, auth } from './firebase-config.js';
|
| 2 |
+
import {
|
| 3 |
+
onAuthStateChanged,
|
| 4 |
+
signOut
|
| 5 |
+
} from "https://www.gstatic.com/firebasejs/10.11.1/firebase-auth.js";
|
| 6 |
+
|
| 7 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
| 8 |
+
// Initialize Firebase
|
| 9 |
+
await initializeFirebase();
|
| 10 |
+
|
| 11 |
+
const logoutLink = document.getElementById("logout");
|
| 12 |
+
|
| 13 |
+
// Monitor auth state
|
| 14 |
+
onAuthStateChanged(auth, (user) => {
|
| 15 |
+
if (user) {
|
| 16 |
+
logoutLink.textContent = "Logout";
|
| 17 |
+
logoutLink.href = ""; // Prevent navigation
|
| 18 |
+
logoutLink.addEventListener("click", handleLogout);
|
| 19 |
+
} else {
|
| 20 |
+
logoutLink.textContent = "Login";
|
| 21 |
+
logoutLink.href = "/login"; // Use your actual login route
|
| 22 |
+
logoutLink.removeEventListener("click", handleLogout);
|
| 23 |
+
}
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
function handleLogout(e) {
|
| 27 |
+
e.preventDefault();
|
| 28 |
+
signOut(auth).then(() => {
|
| 29 |
+
window.location.href = "/"; // Redirect after logout
|
| 30 |
+
}).catch((error) => {
|
| 31 |
+
console.error("Logout Error:", error);
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// ✅ Function to protect page access
|
| 37 |
+
export async function requireLogin() {
|
| 38 |
+
await initializeFirebase();
|
| 39 |
+
onAuthStateChanged(auth, (user) => {
|
| 40 |
+
if (!user) {
|
| 41 |
+
window.location.href = "/login"; // 🔒 Redirect if not logged in
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
}
|
static/js/firebase-config.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.11.1/firebase-app.js';
|
| 2 |
+
import { getFirestore, collection } from 'https://www.gstatic.com/firebasejs/10.11.1/firebase-firestore.js';
|
| 3 |
+
import {
|
| 4 |
+
getAuth,
|
| 5 |
+
createUserWithEmailAndPassword,
|
| 6 |
+
signInWithEmailAndPassword,
|
| 7 |
+
signInWithPopup,
|
| 8 |
+
GoogleAuthProvider,
|
| 9 |
+
OAuthProvider,
|
| 10 |
+
onAuthStateChanged,
|
| 11 |
+
signOut
|
| 12 |
+
} from 'https://www.gstatic.com/firebasejs/10.11.1/firebase-auth.js';
|
| 13 |
+
|
| 14 |
+
// Fetch Firebase configuration from the backend
|
| 15 |
+
export async function getFirebaseConfig() {
|
| 16 |
+
try {
|
| 17 |
+
const res = await fetch('/firebase-config');
|
| 18 |
+
if (!res.ok) {
|
| 19 |
+
throw new Error(`Failed to fetch Firebase config: ${res.statusText}`);
|
| 20 |
+
}
|
| 21 |
+
return await res.json();
|
| 22 |
+
} catch (error) {
|
| 23 |
+
console.error("Error fetching Firebase configuration:", error);
|
| 24 |
+
throw error;
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Initialize Firebase app, auth, and Firestore
|
| 29 |
+
let app, auth, db;
|
| 30 |
+
|
| 31 |
+
export async function initializeFirebase() {
|
| 32 |
+
try {
|
| 33 |
+
const res = await fetch('/firebase-config');
|
| 34 |
+
const firebaseConfig = await res.json();
|
| 35 |
+
if (!firebaseConfig.apiKey) {
|
| 36 |
+
throw new Error("Invalid Firebase configuration: Missing API key.");
|
| 37 |
+
}
|
| 38 |
+
app = initializeApp(firebaseConfig);
|
| 39 |
+
auth = getAuth(app);
|
| 40 |
+
db = getFirestore(app);
|
| 41 |
+
console.log("Firebase initialized successfully.");
|
| 42 |
+
} catch (error) {
|
| 43 |
+
console.error("Error initializing Firebase:", error);
|
| 44 |
+
throw error;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Firebase authentication helper functions
|
| 49 |
+
const firebaseAuth = {
|
| 50 |
+
signUp: (email, password) =>
|
| 51 |
+
createUserWithEmailAndPassword(auth, email, password),
|
| 52 |
+
|
| 53 |
+
signIn: (email, password) =>
|
| 54 |
+
signInWithEmailAndPassword(auth, email, password),
|
| 55 |
+
|
| 56 |
+
signInWithGoogle: () => {
|
| 57 |
+
const provider = new GoogleAuthProvider();
|
| 58 |
+
return signInWithPopup(auth, provider);
|
| 59 |
+
},
|
| 60 |
+
|
| 61 |
+
signInWithApple: () => {
|
| 62 |
+
const provider = new OAuthProvider('apple.com');
|
| 63 |
+
return signInWithPopup(auth, provider);
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
signOut: () =>
|
| 67 |
+
signOut(auth),
|
| 68 |
+
|
| 69 |
+
onAuthStateChanged: (callback) =>
|
| 70 |
+
onAuthStateChanged(auth, callback),
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
// Helper function to get the user ID (or generate a guest ID)
|
| 74 |
+
export function getUserIdAsync() {
|
| 75 |
+
return new Promise((resolve) => {
|
| 76 |
+
onAuthStateChanged(auth, (user) => {
|
| 77 |
+
if (user) {
|
| 78 |
+
resolve(user.uid);
|
| 79 |
+
} else {
|
| 80 |
+
let guestId = localStorage.getItem('guestUserId');
|
| 81 |
+
if (!guestId) {
|
| 82 |
+
guestId = 'guest_' + Math.random().toString(36).substr(2, 9);
|
| 83 |
+
localStorage.setItem('guestUserId', guestId);
|
| 84 |
+
}
|
| 85 |
+
resolve(guestId);
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Export initialized Firebase services and helper functions
|
| 92 |
+
export { auth, db, collection, firebaseAuth };
|
static/js/fundamentals.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeNewsletterForm } from './newsletter.js';
|
| 2 |
+
// --- Variables ---
|
| 3 |
+
let comparisonChart = null;
|
| 4 |
+
const peers = [];
|
| 5 |
+
let currentView = 'table';
|
| 6 |
+
|
| 7 |
+
// --- Page transition and dark mode ---
|
| 8 |
+
document.addEventListener("DOMContentLoaded", async function () {
|
| 9 |
+
document.body.classList.add("loaded");
|
| 10 |
+
|
| 11 |
+
// Dark mode toggle with sun/moon icon in toggle circle
|
| 12 |
+
const darkModeToggle = document.getElementById("darkModeToggle");
|
| 13 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 14 |
+
|
| 15 |
+
function updateToggleIcon() {
|
| 16 |
+
if (darkModeToggle && toggleIcon) {
|
| 17 |
+
if (darkModeToggle.checked) {
|
| 18 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 19 |
+
toggleIcon.style.color = "#4a8fdf";
|
| 20 |
+
} else {
|
| 21 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 22 |
+
toggleIcon.style.color = "#FFD600";
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
if (darkModeToggle) {
|
| 28 |
+
// Set the toggle to checked (on) by default
|
| 29 |
+
darkModeToggle.checked = true;
|
| 30 |
+
// Apply dark mode on page load
|
| 31 |
+
document.body.classList.add("dark-mode");
|
| 32 |
+
// Initial icon state
|
| 33 |
+
updateToggleIcon();
|
| 34 |
+
|
| 35 |
+
darkModeToggle.addEventListener("change", function () {
|
| 36 |
+
document.body.classList.toggle("dark-mode");
|
| 37 |
+
updateToggleIcon();
|
| 38 |
+
updateAllPlotlyChartColors();
|
| 39 |
+
if (window.lastChartData) showChart(window.lastChartData.history, window.lastChartData.symbol);
|
| 40 |
+
if (window.lastFundamentalsHistory) updateFundamentalsHistoryChart(window.lastFundamentalsHistory.history, window.lastFundamentalsHistory.symbol);
|
| 41 |
+
if (typeof updateComparisonChart === "function") updateComparisonChart();
|
| 42 |
+
});
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Page transition for links
|
| 46 |
+
document.querySelectorAll("a").forEach(link => {
|
| 47 |
+
link.addEventListener("click", function (event) {
|
| 48 |
+
if (this.getAttribute("target") === "_blank") return;
|
| 49 |
+
event.preventDefault();
|
| 50 |
+
let href = this.href;
|
| 51 |
+
let overlay = document.createElement("div");
|
| 52 |
+
overlay.classList.add("transition-overlay");
|
| 53 |
+
document.body.appendChild(overlay);
|
| 54 |
+
setTimeout(() => { overlay.classList.add("expand"); }, 90);
|
| 55 |
+
setTimeout(() => { overlay.remove(); }, 1000);
|
| 56 |
+
setTimeout(() => { window.location.href = href; }, 890);
|
| 57 |
+
});
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
// Populate dropdown
|
| 61 |
+
fetch("/get-companies")
|
| 62 |
+
.then(res => res.json())
|
| 63 |
+
.then(companies => {
|
| 64 |
+
const selects = [
|
| 65 |
+
document.getElementById("fundamentals-symbol"),
|
| 66 |
+
document.getElementById("fundamentals-search-bar")
|
| 67 |
+
];
|
| 68 |
+
selects.forEach(select => {
|
| 69 |
+
if (!select) return;
|
| 70 |
+
select.innerHTML = '<option value="">Select a company...</option>';
|
| 71 |
+
companies.forEach(company => {
|
| 72 |
+
const option = document.createElement("option");
|
| 73 |
+
option.value = company["Yahoo Finance Ticker"];
|
| 74 |
+
option.textContent = `${company["Company Name"]} (${company["Yahoo Finance Ticker"]})`;
|
| 75 |
+
select.appendChild(option);
|
| 76 |
+
});
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// Pre-select if a company was chosen on home
|
| 80 |
+
const selectedCompany = localStorage.getItem('selectedCompany');
|
| 81 |
+
if (selectedCompany) {
|
| 82 |
+
companies.forEach(company => {
|
| 83 |
+
const displayName = company.name || company["Company Name"] || company.ticker || company["Yahoo Finance Ticker"];
|
| 84 |
+
if (
|
| 85 |
+
displayName === selectedCompany ||
|
| 86 |
+
`${company["Company Name"]} (${company["Yahoo Finance Ticker"]})` === selectedCompany
|
| 87 |
+
) {
|
| 88 |
+
selects.forEach(select => {
|
| 89 |
+
if (select) select.value = company["Yahoo Finance Ticker"];
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
});
|
| 93 |
+
// Optionally clear the selection after use
|
| 94 |
+
// localStorage.removeItem('selectedCompany');
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
// Load default stock if present
|
| 99 |
+
const params = new URLSearchParams(window.location.search);
|
| 100 |
+
const userInput = params.get("symbol");
|
| 101 |
+
const searchBar = document.getElementById("fundamentals-search-bar");
|
| 102 |
+
if (searchBar && userInput) searchBar.value = userInput;
|
| 103 |
+
if (userInput) {
|
| 104 |
+
try {
|
| 105 |
+
const res = await fetch(`/api/lookup-symbol?query=${encodeURIComponent(userInput)}`);
|
| 106 |
+
const data = await res.json();
|
| 107 |
+
if (data.symbol) fetchFundamentals(data.symbol);
|
| 108 |
+
else showMessage("Company not found", true);
|
| 109 |
+
} catch {
|
| 110 |
+
showMessage("Error looking up symbol", true);
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Attach fetch button event for historical data (if present)
|
| 115 |
+
const fetchBtn = document.getElementById('fetch');
|
| 116 |
+
const symbolSelect = document.getElementById('fundamentals-symbol');
|
| 117 |
+
const startInput = document.getElementById('start-date');
|
| 118 |
+
const endInput = document.getElementById('end-date');
|
| 119 |
+
if (fetchBtn && symbolSelect && startInput && endInput) {
|
| 120 |
+
fetchBtn.addEventListener('click', async function () {
|
| 121 |
+
const symbol = symbolSelect.value;
|
| 122 |
+
const start = startInput.value;
|
| 123 |
+
const end = endInput.value;
|
| 124 |
+
if (!symbol) {
|
| 125 |
+
showMessage("Please select a company from the dropdown.", true);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
let url = `/api/historical?symbol=${encodeURIComponent(symbol)}`;
|
| 129 |
+
if (start && end) url += `&start=${start}&end=${end}`;
|
| 130 |
+
try {
|
| 131 |
+
const res = await fetch(url);
|
| 132 |
+
const data = await res.json();
|
| 133 |
+
if (data.error) {
|
| 134 |
+
showMessage(data.error, true);
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
showTable(data.history);
|
| 138 |
+
showChart(data.history, symbol);
|
| 139 |
+
showView(currentView);
|
| 140 |
+
} catch (e) {
|
| 141 |
+
showMessage("Failed to fetch stock data.", true);
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
}
|
| 145 |
+
});
|
| 146 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 147 |
+
document.body.classList.add("loaded"); // Fade-in page on load
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 151 |
+
fetch("/get-companies")
|
| 152 |
+
.then(res => res.json())
|
| 153 |
+
.then(companies => {
|
| 154 |
+
const selects = [
|
| 155 |
+
document.getElementById("fundamentals-symbol"),
|
| 156 |
+
document.getElementById("fundamentals-search-bar")
|
| 157 |
+
];
|
| 158 |
+
selects.forEach(select => {
|
| 159 |
+
if (!select) return;
|
| 160 |
+
select.innerHTML = '<option value="">Select a company...</option>';
|
| 161 |
+
companies.forEach(company => {
|
| 162 |
+
const option = document.createElement("option");
|
| 163 |
+
option.value = company["Yahoo Finance Ticker"];
|
| 164 |
+
option.textContent = `${company["Company Name"]} (${company["Yahoo Finance Ticker"]})`;
|
| 165 |
+
select.appendChild(option);
|
| 166 |
+
});
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
// Pre-select if a company was chosen on home
|
| 170 |
+
const selectedCompany = localStorage.getItem('selectedCompany');
|
| 171 |
+
if (selectedCompany) {
|
| 172 |
+
companies.forEach(company => {
|
| 173 |
+
const displayName = company.name || company["Company Name"] || company.ticker || company["Yahoo Finance Ticker"];
|
| 174 |
+
if (
|
| 175 |
+
displayName === selectedCompany ||
|
| 176 |
+
`${company["Company Name"]} (${company["Yahoo Finance Ticker"]})` === selectedCompany
|
| 177 |
+
) {
|
| 178 |
+
selects.forEach(select => {
|
| 179 |
+
if (select) select.value = company["Yahoo Finance Ticker"];
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
});
|
| 183 |
+
// Optionally clear the selection after use
|
| 184 |
+
// localStorage.removeItem('selectedCompany');
|
| 185 |
+
}
|
| 186 |
+
});
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
// --- Show messages ---
|
| 190 |
+
function showMessage(message, isError = false) {
|
| 191 |
+
const messageContainer = document.getElementById("message-container");
|
| 192 |
+
if (!messageContainer) return;
|
| 193 |
+
messageContainer.textContent = message;
|
| 194 |
+
messageContainer.style.display = "block";
|
| 195 |
+
messageContainer.style.backgroundColor = isError ? "#ffebee" : "#e8f5e9";
|
| 196 |
+
messageContainer.style.color = isError ? "#c62828" : "#2e7d32";
|
| 197 |
+
messageContainer.style.border = isError ? "1px solid #c62828" : "1px solid #2e7d32";
|
| 198 |
+
setTimeout(() => { messageContainer.style.display = "none"; }, 5000);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// --- Fetch and display fundamentals ---
|
| 202 |
+
async function fetchFundamentals(symbol) {
|
| 203 |
+
if (!symbol || symbol.trim() === '') {
|
| 204 |
+
showMessage("Please select a stock symbol", true);
|
| 205 |
+
return;
|
| 206 |
+
}
|
| 207 |
+
try {
|
| 208 |
+
document.querySelectorAll('.ratio-value').forEach(el => {
|
| 209 |
+
el.textContent = '...';
|
| 210 |
+
el.classList.add('loading');
|
| 211 |
+
});
|
| 212 |
+
const response = await fetch(`/api/fundamentals?symbol=${encodeURIComponent(symbol)}`);
|
| 213 |
+
const data = await response.json();
|
| 214 |
+
if (data.error) {
|
| 215 |
+
showMessage(data.error, true);
|
| 216 |
+
return;
|
| 217 |
+
}
|
| 218 |
+
updateFundamentalsUI(data);
|
| 219 |
+
showRiskMetricsChart(data);
|
| 220 |
+
showBetaGauge(data.beta);
|
| 221 |
+
if (data.history) {
|
| 222 |
+
updateFundamentalsHistoryChart(data.history, symbol);
|
| 223 |
+
showRollingVolatilityChart(data.history);
|
| 224 |
+
} else {
|
| 225 |
+
Plotly.purge('fundamentals-chart');
|
| 226 |
+
}
|
| 227 |
+
} catch (error) {
|
| 228 |
+
showMessage("Failed to fetch fundamentals data.", true);
|
| 229 |
+
}
|
| 230 |
+
}window.fetchFundamentals = fetchFundamentals; // Expose function globally
|
| 231 |
+
|
| 232 |
+
// --- Update UI with ratios and risk metrics ---
|
| 233 |
+
function updateFundamentalsUI(data) {
|
| 234 |
+
document.getElementById('pe-ratio').textContent = data.pe ? data.pe.toFixed(2) : 'N/A';
|
| 235 |
+
document.getElementById('pb-ratio').textContent = data.pb ? data.pb.toFixed(2) : 'N/A';
|
| 236 |
+
document.getElementById('ps-ratio').textContent = data.ps ? data.ps.toFixed(2) : 'N/A';
|
| 237 |
+
document.getElementById('div-yield').textContent = data.divYield ? (data.divYield.toFixed(2) + '%') : 'N/A';
|
| 238 |
+
document.getElementById('roe').textContent = data.roe ? (data.roe.toFixed(2) + '%') : 'N/A';
|
| 239 |
+
document.getElementById('roa').textContent = data.roa ? (data.roa.toFixed(2) + '%') : 'N/A';
|
| 240 |
+
document.getElementById('gross-margin').textContent = data.grossMargin ? (data.grossMargin.toFixed(2) + '%') : 'N/A';
|
| 241 |
+
document.getElementById('op-margin').textContent = data.opMargin ? (data.opMargin.toFixed(2) + '%') : 'N/A';
|
| 242 |
+
document.getElementById('current-ratio').textContent = data.currentRatio ? data.currentRatio.toFixed(2) : 'N/A';
|
| 243 |
+
document.getElementById('quick-ratio').textContent = data.quickRatio ? data.quickRatio.toFixed(2) : 'N/A';
|
| 244 |
+
document.getElementById('debt-equity').textContent = data.debtEquity ? data.debtEquity.toFixed(2) : 'N/A';
|
| 245 |
+
document.getElementById('ebitda-margin').textContent = data.ebitdaMargin ? (data.ebitdaMargin.toFixed(2) + '%') : 'N/A';
|
| 246 |
+
document.getElementById('volatility').textContent =
|
| 247 |
+
data.volatility !== null && data.volatility !== undefined ? (data.volatility * 100).toFixed(2) + '%' : 'N/A';
|
| 248 |
+
document.getElementById('beta').textContent =
|
| 249 |
+
data.beta !== null && data.beta !== undefined ? data.beta.toFixed(2) : 'N/A';
|
| 250 |
+
document.getElementById('var95').textContent =
|
| 251 |
+
data.var95 !== null && data.var95 !== undefined ? (data.var95 * 100).toFixed(2) + '%' : 'N/A';
|
| 252 |
+
document.querySelectorAll('.ratio-value').forEach(el => el.classList.remove('loading'));
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// --- Plotly chart for price/volume history ---
|
| 256 |
+
function updateFundamentalsHistoryChart(history, symbol) {
|
| 257 |
+
window.lastFundamentalsHistory = { history, symbol };
|
| 258 |
+
const chartDiv = document.getElementById('fundamentals-chart');
|
| 259 |
+
if (!history || !history.length) {
|
| 260 |
+
Plotly.purge(chartDiv);
|
| 261 |
+
return;
|
| 262 |
+
}
|
| 263 |
+
const { years, avgCloses, totalVolumes } = groupByYear(history);
|
| 264 |
+
const plotBg = getBgColor();
|
| 265 |
+
const paperBg = getBgColor();
|
| 266 |
+
const gridColor = getGridColor();
|
| 267 |
+
const fontColor = getTextColor();
|
| 268 |
+
// Calculate 3-year and 5-year moving averages for yearly data
|
| 269 |
+
const dma3 = movingAverage(avgCloses, 3);
|
| 270 |
+
const dma5 = movingAverage(avgCloses, 5);
|
| 271 |
+
const priceTrace = {
|
| 272 |
+
x: years,
|
| 273 |
+
y: avgCloses,
|
| 274 |
+
type: 'scatter',
|
| 275 |
+
mode: 'lines+markers',
|
| 276 |
+
name: 'Avg Closing Price (₹)',
|
| 277 |
+
line: { color: '#2196F3' },
|
| 278 |
+
yaxis: 'y1',
|
| 279 |
+
fill: 'tozeroy',
|
| 280 |
+
fillcolor: 'rgba(33,150,243,0.15)'
|
| 281 |
+
};
|
| 282 |
+
const volumeTrace = {
|
| 283 |
+
x: years,
|
| 284 |
+
y: totalVolumes,
|
| 285 |
+
type: 'bar',
|
| 286 |
+
name: 'Total Volume',
|
| 287 |
+
marker: { color: '#4caf50', opacity: 0.4 },
|
| 288 |
+
yaxis: 'y2'
|
| 289 |
+
};
|
| 290 |
+
const dma3Trace = {
|
| 291 |
+
x: years,
|
| 292 |
+
y: dma3,
|
| 293 |
+
type: 'scatter',
|
| 294 |
+
mode: 'lines',
|
| 295 |
+
name: '3-Year MA',
|
| 296 |
+
line: { color: '#ff9800', width: 2, dash: 'dot' },
|
| 297 |
+
yaxis: 'y1'
|
| 298 |
+
};
|
| 299 |
+
const dma5Trace = {
|
| 300 |
+
x: years,
|
| 301 |
+
y: dma5,
|
| 302 |
+
type: 'scatter',
|
| 303 |
+
mode: 'lines',
|
| 304 |
+
name: '5-Year MA',
|
| 305 |
+
line: { color: '#FF00FF', width: 2, dash: 'dash' },
|
| 306 |
+
yaxis: 'y1'
|
| 307 |
+
};
|
| 308 |
+
const layout = {
|
| 309 |
+
title: `${symbol} Yearly Stock Performance`,
|
| 310 |
+
xaxis: { title: 'Year', gridcolor: gridColor },
|
| 311 |
+
yaxis: { title: 'Avg Price (₹)', side: 'left', gridcolor: gridColor },
|
| 312 |
+
yaxis2: {
|
| 313 |
+
title: 'Total Volume',
|
| 314 |
+
overlaying: 'y',
|
| 315 |
+
side: 'right',
|
| 316 |
+
showgrid: false
|
| 317 |
+
},
|
| 318 |
+
legend: { x: 0, y: 1.1, orientation: 'h' },
|
| 319 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 320 |
+
plot_bgcolor: plotBg,
|
| 321 |
+
paper_bgcolor: paperBg,
|
| 322 |
+
font: { color: fontColor }
|
| 323 |
+
};
|
| 324 |
+
Plotly.newPlot(chartDiv, [priceTrace, dma3Trace, dma5Trace, volumeTrace], layout, {responsive: true});
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// --- Risk Metrics Bar Chart ---
|
| 328 |
+
function showRiskMetricsChart(data) {
|
| 329 |
+
window.lastRiskMetricsData = data;
|
| 330 |
+
const chartDiv = document.getElementById('risk-metrics-chart');
|
| 331 |
+
if (!chartDiv) return;
|
| 332 |
+
const plotBg = getBgColor();
|
| 333 |
+
const paperBg = getBgColor();
|
| 334 |
+
const gridColor = getGridColor();
|
| 335 |
+
const fontColor = getTextColor();
|
| 336 |
+
const labels = ['Volatility (%)', 'Beta', 'VaR 95% (%)'];
|
| 337 |
+
const values = [
|
| 338 |
+
data.volatility !== null && data.volatility !== undefined ? data.volatility * 100 : null,
|
| 339 |
+
data.beta !== null && data.beta !== undefined ? data.beta : null,
|
| 340 |
+
data.var95 !== null && data.var95 !== undefined ? data.var95 * 100 : null
|
| 341 |
+
];
|
| 342 |
+
const trace = {
|
| 343 |
+
x: labels,
|
| 344 |
+
y: values,
|
| 345 |
+
type: 'bar',
|
| 346 |
+
marker: {
|
| 347 |
+
color: ['rgba(54,162,235,0.6)', 'rgba(255,99,132,0.6)', 'rgba(255,167,38,0.6)'], // semi-transparent fill
|
| 348 |
+
line: {
|
| 349 |
+
color: ['#36a2eb', '#ff6384', '#ffa726'], // opaque border
|
| 350 |
+
width: 2
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
};
|
| 354 |
+
const layout = {
|
| 355 |
+
title: 'Risk Metrics',
|
| 356 |
+
xaxis: { title: '', gridcolor: gridColor },
|
| 357 |
+
yaxis: { title: '', rangemode: 'tozero', gridcolor: gridColor },
|
| 358 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 359 |
+
plot_bgcolor: plotBg,
|
| 360 |
+
paper_bgcolor: paperBg,
|
| 361 |
+
font: { color: fontColor }
|
| 362 |
+
};
|
| 363 |
+
Plotly.newPlot(chartDiv, [trace], layout, {responsive: true});
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// --- Utility functions ---
|
| 367 |
+
function groupByYear(history) {
|
| 368 |
+
const yearly = {};
|
| 369 |
+
history.forEach(d => {
|
| 370 |
+
const dateStr = d.Date || d.date;
|
| 371 |
+
const year = new Date(dateStr).getFullYear();
|
| 372 |
+
if (!yearly[year]) yearly[year] = { closeSum: 0, volumeSum: 0, count: 0 };
|
| 373 |
+
yearly[year].closeSum += Number(d.Close ?? d.close);
|
| 374 |
+
yearly[year].volumeSum += Number(d.Volume ?? d.volume);
|
| 375 |
+
yearly[year].count += 1;
|
| 376 |
+
});
|
| 377 |
+
const years = Object.keys(yearly).sort();
|
| 378 |
+
const avgCloses = years.map(y => yearly[y].closeSum / yearly[y].count);
|
| 379 |
+
const totalVolumes = years.map(y => yearly[y].volumeSum);
|
| 380 |
+
return { years, avgCloses, totalVolumes };
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
function getGridColor() {
|
| 384 |
+
return document.body.classList.contains("dark-mode") ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)';
|
| 385 |
+
}
|
| 386 |
+
function getBgColor() {
|
| 387 |
+
return document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff';
|
| 388 |
+
}
|
| 389 |
+
function getTextColor() {
|
| 390 |
+
return document.body.classList.contains("dark-mode") ? "#fff" : "#333";
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
function updateAllPlotlyChartColors() {
|
| 394 |
+
const layoutUpdate = {
|
| 395 |
+
paper_bgcolor: getBgColor(),
|
| 396 |
+
plot_bgcolor: getBgColor(),
|
| 397 |
+
font: { color: getTextColor() },
|
| 398 |
+
xaxis: { color: getTextColor(), gridcolor: getGridColor() },
|
| 399 |
+
yaxis: { color: getTextColor(), gridcolor: getGridColor() }
|
| 400 |
+
};
|
| 401 |
+
[
|
| 402 |
+
'stockChart',
|
| 403 |
+
'fundamentals-chart',
|
| 404 |
+
'comparison-chart',
|
| 405 |
+
'prediction-chart',
|
| 406 |
+
'beta-gauge-chart'
|
| 407 |
+
].forEach(id => {
|
| 408 |
+
const chartDiv = document.getElementById(id);
|
| 409 |
+
if (chartDiv && chartDiv.data) {
|
| 410 |
+
Plotly.relayout(chartDiv, layoutUpdate);
|
| 411 |
+
}
|
| 412 |
+
// For risk-metrics and rolling-vol, redraw with latest data
|
| 413 |
+
if (window.lastRiskMetricsData) showRiskMetricsChart(window.lastRiskMetricsData);
|
| 414 |
+
if (window.lastFundamentalsHistory) showRollingVolatilityChart(window.lastFundamentalsHistory.history);
|
| 415 |
+
});
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// --- Tab switching ---
|
| 419 |
+
function openTab(tabName) {
|
| 420 |
+
// Hide all tab contents
|
| 421 |
+
document.querySelectorAll('.tab-content').forEach(tab => tab.style.display = 'none');
|
| 422 |
+
// Remove 'active' from all tab buttons
|
| 423 |
+
document.querySelectorAll('.fundamentals-tabs .tab-button').forEach(btn => btn.classList.remove('active'));
|
| 424 |
+
// Show the selected tab content
|
| 425 |
+
document.getElementById(tabName).style.display = 'block';
|
| 426 |
+
// Add 'active' to the clicked tab button
|
| 427 |
+
document.querySelectorAll('.fundamentals-tabs .tab-button').forEach(btn => {
|
| 428 |
+
if (btn.getAttribute('onclick') === `openTab('${tabName}')`) {
|
| 429 |
+
btn.classList.add('active');
|
| 430 |
+
}
|
| 431 |
+
});
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
// --- Force Plotly to resize charts in the now-visible tab ---
|
| 435 |
+
setTimeout(() => {
|
| 436 |
+
if (tabName === 'chart') {
|
| 437 |
+
const chartDiv = document.getElementById('fundamentals-chart');
|
| 438 |
+
if (chartDiv) Plotly.Plots.resize(chartDiv);
|
| 439 |
+
}
|
| 440 |
+
if (tabName === 'compare') {
|
| 441 |
+
['risk-metrics-chart', 'rolling-vol-chart', 'beta-gauge-chart'].forEach(id => {
|
| 442 |
+
const chartDiv = document.getElementById(id);
|
| 443 |
+
if (chartDiv) Plotly.Plots.resize(chartDiv);
|
| 444 |
+
});
|
| 445 |
+
}
|
| 446 |
+
}, 100); // Delay ensures the tab is visible before resizing
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
window.openTab = openTab;
|
| 450 |
+
function showRollingVolatilityChart(history) {
|
| 451 |
+
if (!history || !history.length) return;
|
| 452 |
+
const plotBg = getBgColor();
|
| 453 |
+
const paperBg = getBgColor();
|
| 454 |
+
const gridColor = getGridColor();
|
| 455 |
+
const fontColor = getTextColor();
|
| 456 |
+
const returns = history.map((d, i, arr) => i > 0 ? (d.Close - arr[i-1].Close) / arr[i-1].Close : null).slice(1);
|
| 457 |
+
const windowSize = 30;
|
| 458 |
+
const rollingVol = [];
|
| 459 |
+
for (let i = windowSize; i < returns.length; i++) {
|
| 460 |
+
const window = returns.slice(i - windowSize, i);
|
| 461 |
+
const std = Math.sqrt(window.reduce((sum, r) => sum + Math.pow(r - (window.reduce((a, b) => a + b, 0) / window.length), 2), 0) / window.length);
|
| 462 |
+
rollingVol.push(std * Math.sqrt(252) * 100); // annualized %
|
| 463 |
+
}
|
| 464 |
+
const dates = history.slice(windowSize + 1).map(d => d.Date || d.date);
|
| 465 |
+
const trace = {
|
| 466 |
+
x: dates,
|
| 467 |
+
y: rollingVol,
|
| 468 |
+
type: 'scatter',
|
| 469 |
+
mode: 'lines',
|
| 470 |
+
name: '30-day Rolling Volatility (%)',
|
| 471 |
+
fill: 'tozeroy',
|
| 472 |
+
line: { color: '#36a2eb' }
|
| 473 |
+
};
|
| 474 |
+
const layout = {
|
| 475 |
+
title: 'Rolling Volatility (30-day, Annualized)',
|
| 476 |
+
xaxis: { title: 'Date', gridcolor: gridColor },
|
| 477 |
+
yaxis: { title: 'Volatility (%)', rangemode: 'tozero', gridcolor: gridColor },
|
| 478 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 479 |
+
plot_bgcolor: plotBg,
|
| 480 |
+
paper_bgcolor: paperBg,
|
| 481 |
+
font: { color: fontColor }
|
| 482 |
+
};
|
| 483 |
+
Plotly.newPlot('rolling-vol-chart', [trace], layout, {responsive: true});
|
| 484 |
+
}
|
| 485 |
+
// --- Beta Gauge Chart ---
|
| 486 |
+
function showBetaGauge(beta) {
|
| 487 |
+
window.lastBeta = beta; // <-- Add this line
|
| 488 |
+
const chartDiv = document.getElementById('beta-gauge-chart');
|
| 489 |
+
if (!chartDiv || beta === null || beta === undefined) return;
|
| 490 |
+
const plotBg = getBgColor();
|
| 491 |
+
const paperBg = getBgColor();
|
| 492 |
+
const fontColor = getTextColor();
|
| 493 |
+
// Get the actual width of the container
|
| 494 |
+
const containerWidth = chartDiv.parentElement ? chartDiv.parentElement.offsetWidth : 400;
|
| 495 |
+
Plotly.newPlot(chartDiv, [{
|
| 496 |
+
type: "indicator",
|
| 497 |
+
mode: "gauge+number",
|
| 498 |
+
value: beta,
|
| 499 |
+
title: { text: "Beta", font: { color: fontColor } },
|
| 500 |
+
gauge: {
|
| 501 |
+
axis: {
|
| 502 |
+
range: [0, 2],
|
| 503 |
+
tickcolor: fontColor,
|
| 504 |
+
linecolor: "#ff6384", // Opaque axis line
|
| 505 |
+
linewidth: 3
|
| 506 |
+
},
|
| 507 |
+
bar: {
|
| 508 |
+
color: "rgba(255,99,132,0.6)", // semi-transparent fill
|
| 509 |
+
line: { color: "#ff6384", width: 3 } // opaque border
|
| 510 |
+
},
|
| 511 |
+
steps: [
|
| 512 |
+
{ range: [0, 1], color: "rgba(174,229,113,0.5)", line: {color: "rgba(175, 229, 113,1)", width: 4} }, // semi-transparent green with opaque edge
|
| 513 |
+
{ range: [1, 2], color: "rgba(255,224,130,0.5)", line: {color: "rgba(255, 224, 130,1)", width: 4} } // semi-transparent yellow with opaque edge
|
| 514 |
+
],
|
| 515 |
+
bgcolor: plotBg,
|
| 516 |
+
bordercolor: paperBg
|
| 517 |
+
}
|
| 518 |
+
}], {
|
| 519 |
+
width: containerWidth, // <-- Force width to match container
|
| 520 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 521 |
+
paper_bgcolor: paperBg,
|
| 522 |
+
plot_bgcolor: plotBg,
|
| 523 |
+
font: { color: fontColor }
|
| 524 |
+
});
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 528 |
+
const fetchBtn = document.getElementById('fetch');
|
| 529 |
+
const symbolSelect = document.getElementById('fundamentals-symbol');
|
| 530 |
+
const startInput = document.getElementById('start-date');
|
| 531 |
+
const endInput = document.getElementById('end-date');
|
| 532 |
+
|
| 533 |
+
fetchBtn.addEventListener('click', async function () {
|
| 534 |
+
const symbol = symbolSelect.value;
|
| 535 |
+
const start = startInput.value;
|
| 536 |
+
const end = endInput.value;
|
| 537 |
+
if (!symbol) {
|
| 538 |
+
showMessage("Please select a company from the dropdown.", true);
|
| 539 |
+
return;
|
| 540 |
+
}
|
| 541 |
+
let url = `/api/historical?symbol=${encodeURIComponent(symbol)}`;
|
| 542 |
+
if (start && end) url += `&start=${start}&end=${end}`;
|
| 543 |
+
try {
|
| 544 |
+
const res = await fetch(url);
|
| 545 |
+
const data = await res.json();
|
| 546 |
+
if (data.error) {
|
| 547 |
+
showMessage(data.error, true);
|
| 548 |
+
return;
|
| 549 |
+
}
|
| 550 |
+
// You need to implement showTable and showChart functions for your table/chart
|
| 551 |
+
showTable(data.history);
|
| 552 |
+
showChart(data.history, symbol);
|
| 553 |
+
showView(currentView); // If you have tab switching for table/chart/both
|
| 554 |
+
} catch (e) {
|
| 555 |
+
showMessage("Failed to fetch stock data.", true);
|
| 556 |
+
}
|
| 557 |
+
});
|
| 558 |
+
});
|
| 559 |
+
|
| 560 |
+
function showTable(history) {
|
| 561 |
+
// Simple table rendering for demonstration
|
| 562 |
+
const tableDiv = document.getElementById('stock-data-table');
|
| 563 |
+
if (!history || !history.length) {
|
| 564 |
+
tableDiv.innerHTML = "<p>No data available.</p>";
|
| 565 |
+
return;
|
| 566 |
+
}
|
| 567 |
+
let html = "<table><thead><tr>";
|
| 568 |
+
Object.keys(history[0]).forEach(key => {
|
| 569 |
+
html += `<th>${key}</th>`;
|
| 570 |
+
});
|
| 571 |
+
html += "</tr></thead><tbody>";
|
| 572 |
+
history.forEach(row => {
|
| 573 |
+
html += "<tr>";
|
| 574 |
+
Object.values(row).forEach(val => {
|
| 575 |
+
html += `<td>${val !== undefined ? val : ''}</td>`;
|
| 576 |
+
});
|
| 577 |
+
html += "</tr>";
|
| 578 |
+
});
|
| 579 |
+
html += "</tbody></table>";
|
| 580 |
+
tableDiv.innerHTML = html;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
// --- Stock Price Chart for /api/historical ---
|
| 584 |
+
function showChart(history, symbol) {
|
| 585 |
+
const chartDiv = document.getElementById('stockChart');
|
| 586 |
+
if (!history || !history.length) {
|
| 587 |
+
Plotly.purge(chartDiv);
|
| 588 |
+
return;
|
| 589 |
+
}
|
| 590 |
+
const plotBg = getBgColor();
|
| 591 |
+
const paperBg = getBgColor();
|
| 592 |
+
const gridColor = getGridColor();
|
| 593 |
+
const fontColor = getTextColor();
|
| 594 |
+
const dates = history.map(d => d.Date || d.date);
|
| 595 |
+
const closes = history.map(d => d.Close ?? d.close);
|
| 596 |
+
const trace = {
|
| 597 |
+
x: dates,
|
| 598 |
+
y: closes,
|
| 599 |
+
type: 'scatter',
|
| 600 |
+
mode: 'lines+markers',
|
| 601 |
+
name: 'Close Price',
|
| 602 |
+
fill: 'tozeroy',
|
| 603 |
+
line: { color: '#2196F3' }
|
| 604 |
+
};
|
| 605 |
+
const layout = {
|
| 606 |
+
title: `${symbol} Price Chart`,
|
| 607 |
+
xaxis: { title: 'Date', gridcolor: gridColor },
|
| 608 |
+
yaxis: { title: 'Close Price', gridcolor: gridColor },
|
| 609 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 610 |
+
plot_bgcolor: plotBg,
|
| 611 |
+
paper_bgcolor: paperBg,
|
| 612 |
+
font: { color: fontColor }
|
| 613 |
+
};
|
| 614 |
+
Plotly.newPlot(chartDiv, [trace], layout, {responsive: true});
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
function showView(view) {
|
| 618 |
+
// Remove 'active' from all tab buttons
|
| 619 |
+
document.querySelectorAll('.tab-container .tab-button').forEach(btn => btn.classList.remove('active'));
|
| 620 |
+
// Add 'active' to the clicked tab button
|
| 621 |
+
document.querySelectorAll('.tab-container .tab-button').forEach(btn => {
|
| 622 |
+
if (btn.getAttribute('onclick') === `showView('${view}')`) {
|
| 623 |
+
btn.classList.add('active');
|
| 624 |
+
}
|
| 625 |
+
});
|
| 626 |
+
window.showView = showView;
|
| 627 |
+
// Show/hide table/chart views as needed
|
| 628 |
+
document.getElementById('stock-data-table').style.display = (view === 'table' || view === 'both') ? 'block' : 'none';
|
| 629 |
+
document.getElementById('chart-view').style.display = (view === 'chart' || view === 'both') ? 'block' : 'none';
|
| 630 |
+
|
| 631 |
+
// Force Plotly to resize if chart is shown
|
| 632 |
+
if (view === 'chart' || view === 'both') {
|
| 633 |
+
setTimeout(() => {
|
| 634 |
+
const chartDiv = document.getElementById('stockChart');
|
| 635 |
+
if (chartDiv) Plotly.Plots.resize(chartDiv);
|
| 636 |
+
}, 100);
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
window.addEventListener('resize', () => {
|
| 640 |
+
if (window.lastBeta !== undefined) {
|
| 641 |
+
showBetaGauge(window.lastBeta);
|
| 642 |
+
}
|
| 643 |
+
});
|
| 644 |
+
document.querySelectorAll("a").forEach(link => {
|
| 645 |
+
link.addEventListener("click", function (event) {
|
| 646 |
+
if (this.getAttribute("target") === "_blank") return;
|
| 647 |
+
event.preventDefault();
|
| 648 |
+
let href = this.href;
|
| 649 |
+
let overlay = document.createElement("div");
|
| 650 |
+
overlay.classList.add("transition-overlay");
|
| 651 |
+
document.body.appendChild(overlay);
|
| 652 |
+
setTimeout(() => { overlay.classList.add("active"); }, 90);
|
| 653 |
+
setTimeout(() => { overlay.remove(); }, 1000);
|
| 654 |
+
setTimeout(() => { window.location.href = href; }, 890);
|
| 655 |
+
});
|
| 656 |
+
});
|
| 657 |
+
|
| 658 |
+
function movingAverage(arr, windowSize) {
|
| 659 |
+
let result = [];
|
| 660 |
+
for (let i = 0; i < arr.length; i++) {
|
| 661 |
+
if (i < windowSize - 1) {
|
| 662 |
+
result.push(null);
|
| 663 |
+
} else {
|
| 664 |
+
let sum = 0;
|
| 665 |
+
for (let j = i - windowSize + 1; j <= i; j++) {
|
| 666 |
+
sum += arr[j];
|
| 667 |
+
}
|
| 668 |
+
result.push(sum / windowSize);
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
return result;
|
| 672 |
+
}
|
| 673 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 674 |
+
if (!localStorage.getItem('fundamentals_tutorial_shown')) {
|
| 675 |
+
introJs().setOptions({
|
| 676 |
+
nextLabel: 'Next',
|
| 677 |
+
prevLabel: 'Back',
|
| 678 |
+
doneLabel: 'Done',
|
| 679 |
+
skipLabel: 'Skip',
|
| 680 |
+
showProgress: true,
|
| 681 |
+
showBullets: false,
|
| 682 |
+
exitOnOverlayClick: false,
|
| 683 |
+
tooltipClass: 'custom-introjs-tooltip',
|
| 684 |
+
}).oncomplete(function() {
|
| 685 |
+
localStorage.setItem('fundamentals_tutorial_shown', 'yes');
|
| 686 |
+
}).onexit(function() {
|
| 687 |
+
localStorage.setItem('fundamentals_tutorial_shown', 'yes');
|
| 688 |
+
}).start();
|
| 689 |
+
}
|
| 690 |
+
});
|
| 691 |
+
|
| 692 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 693 |
+
initializeNewsletterForm();
|
| 694 |
+
// Other page-specific code...
|
| 695 |
+
});
|
static/js/home.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeNewsletterForm } from './newsletter.js';
|
| 2 |
+
// Dark Mode Toggle
|
| 3 |
+
// Update dark mode toggle to be on by default
|
| 4 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 5 |
+
const darkModeToggle = document.getElementById("darkModeToggle");
|
| 6 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 7 |
+
|
| 8 |
+
function updateToggleIcon() {
|
| 9 |
+
if (darkModeToggle.checked) {
|
| 10 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 11 |
+
} else {
|
| 12 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Initial state
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
// Set the toggle to checked (on) by default
|
| 20 |
+
darkModeToggle.checked = true;
|
| 21 |
+
updateToggleIcon();
|
| 22 |
+
// Apply dark mode on page load
|
| 23 |
+
document.body.classList.add("dark-mode");
|
| 24 |
+
|
| 25 |
+
darkModeToggle.addEventListener("change", function() {
|
| 26 |
+
document.body.classList.toggle("dark-mode");
|
| 27 |
+
if (typeof updateChartColors === "function") updateChartColors(this.checked);
|
| 28 |
+
updateToggleIcon();
|
| 29 |
+
});
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Logout Functionality
|
| 33 |
+
document.getElementById("logout").addEventListener("click", function(event) {
|
| 34 |
+
event.preventDefault(); // Prevent default link behavior
|
| 35 |
+
// Clear session storage or local storage if needed
|
| 36 |
+
sessionStorage.clear();
|
| 37 |
+
localStorage.clear();
|
| 38 |
+
// Redirect to login page
|
| 39 |
+
window.location.href = "login";
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
// Ensure Three.js scene initializes properly
|
| 44 |
+
const container = document.getElementById("three-container");
|
| 45 |
+
const scene = new THREE.Scene();
|
| 46 |
+
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
| 47 |
+
|
| 48 |
+
// Zoom in slightly by moving camera closer
|
| 49 |
+
camera.position.z = 3; // default is usually 5
|
| 50 |
+
|
| 51 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
| 52 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 53 |
+
renderer.outputEncoding = THREE.sRGBEncoding;
|
| 54 |
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
| 55 |
+
renderer.toneMappingExposure = 0.5; // lower exposure for less brightness
|
| 56 |
+
container.appendChild(renderer.domElement);
|
| 57 |
+
|
| 58 |
+
// Add Ambient & Directional Lighting (low intensity)
|
| 59 |
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
|
| 60 |
+
scene.add(ambientLight);
|
| 61 |
+
|
| 62 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
|
| 63 |
+
directionalLight.position.set(2, 2, 5);
|
| 64 |
+
scene.add(directionalLight);
|
| 65 |
+
|
| 66 |
+
// Load GLTF Model
|
| 67 |
+
const loader = new THREE.GLTFLoader();
|
| 68 |
+
loader.load(modelpath, function(gltf) {
|
| 69 |
+
const model = gltf.scene;
|
| 70 |
+
model.position.set(0, -1, 0);
|
| 71 |
+
model.scale.set(1.9, 1.5, 0.8);
|
| 72 |
+
scene.add(model);
|
| 73 |
+
}, undefined, function(error) {
|
| 74 |
+
console.error('Error loading model:', error);
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
// Set Camera Position
|
| 78 |
+
camera.position.set(0, 4.2, 4.9);
|
| 79 |
+
camera.lookAt(0, 0, 0);
|
| 80 |
+
|
| 81 |
+
// Animation Loop
|
| 82 |
+
function animate() {
|
| 83 |
+
requestAnimationFrame(animate);
|
| 84 |
+
renderer.render(scene, camera);
|
| 85 |
+
}
|
| 86 |
+
animate();
|
| 87 |
+
|
| 88 |
+
// Resize Event Listener
|
| 89 |
+
window.addEventListener('resize', () => {
|
| 90 |
+
const width = container.clientWidth;
|
| 91 |
+
const height = container.clientHeight;
|
| 92 |
+
renderer.setSize(width, height);
|
| 93 |
+
camera.aspect = width / height;
|
| 94 |
+
camera.updateProjectionMatrix();
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
// Page Transition
|
| 98 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 99 |
+
document.body.classList.add("loaded"); // Fade-in page on load
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
// Handle link clicks for page transitions
|
| 103 |
+
document.querySelectorAll("a").forEach(link => {
|
| 104 |
+
link.addEventListener("click", function (event) {
|
| 105 |
+
if (this.getAttribute("target") === "_blank") return;
|
| 106 |
+
|
| 107 |
+
event.preventDefault(); // Stop instant navigation
|
| 108 |
+
let href = this.href;
|
| 109 |
+
|
| 110 |
+
// Create transition element
|
| 111 |
+
let overlay = document.createElement("div");
|
| 112 |
+
overlay.classList.add("transition-overlay");
|
| 113 |
+
document.body.appendChild(overlay);
|
| 114 |
+
|
| 115 |
+
// Trigger expanding circle effect
|
| 116 |
+
setTimeout(() => {
|
| 117 |
+
overlay.classList.add("expand");
|
| 118 |
+
}, 90);
|
| 119 |
+
// Optional: Clean up the overlay after the animation duration
|
| 120 |
+
setTimeout(() => {
|
| 121 |
+
overlay.remove();
|
| 122 |
+
}, 1000);
|
| 123 |
+
|
| 124 |
+
// Navigate after transition
|
| 125 |
+
setTimeout(() => {
|
| 126 |
+
window.location.href = href;
|
| 127 |
+
}, 890);
|
| 128 |
+
});
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
// Fade-In Animation on Scroll
|
| 134 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 135 |
+
const fadeElements = document.querySelectorAll(".fade-in");
|
| 136 |
+
|
| 137 |
+
const checkVisibility = () => {
|
| 138 |
+
fadeElements.forEach((element) => {
|
| 139 |
+
const elementTop = element.getBoundingClientRect().top;
|
| 140 |
+
const elementBottom = element.getBoundingClientRect().bottom;
|
| 141 |
+
const isVisible = elementTop < window.innerHeight && elementBottom >= 0;
|
| 142 |
+
|
| 143 |
+
if (isVisible) {
|
| 144 |
+
element.classList.add("visible");
|
| 145 |
+
}
|
| 146 |
+
});
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
window.addEventListener("scroll", checkVisibility);
|
| 150 |
+
checkVisibility(); // Check on page load
|
| 151 |
+
});
|
| 152 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 153 |
+
let companies = [];
|
| 154 |
+
// Fetch company list for autocomplete and suggestions
|
| 155 |
+
fetch("/get-companies")
|
| 156 |
+
.then(res => res.json())
|
| 157 |
+
.then(data => {
|
| 158 |
+
companies = data;
|
| 159 |
+
|
| 160 |
+
// Show 8 random suggestions from the company list
|
| 161 |
+
const suggestionsDiv = document.querySelector('.suggestions-list');
|
| 162 |
+
if (suggestionsDiv) {
|
| 163 |
+
suggestionsDiv.innerHTML = '';
|
| 164 |
+
const shuffled = companies.sort(() => 0.5 - Math.random());
|
| 165 |
+
const randomSuggestions = shuffled.slice(0, 8);
|
| 166 |
+
randomSuggestions.forEach(company => {
|
| 167 |
+
const span = document.createElement('span');
|
| 168 |
+
span.className = 'suggestion';
|
| 169 |
+
// Use company name or ticker as needed
|
| 170 |
+
span.textContent = company.name || company["Company Name"] || company.ticker || company["Yahoo Finance Ticker"];
|
| 171 |
+
span.addEventListener('click', function() {
|
| 172 |
+
const searchBox = document.getElementById("home-search-box");
|
| 173 |
+
if (searchBox) {
|
| 174 |
+
searchBox.value = this.textContent;
|
| 175 |
+
searchBox.focus();
|
| 176 |
+
}
|
| 177 |
+
});
|
| 178 |
+
suggestionsDiv.appendChild(span);
|
| 179 |
+
});
|
| 180 |
+
}
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
const searchBox = document.getElementById("home-search-box");
|
| 184 |
+
if (searchBox) {
|
| 185 |
+
searchBox.addEventListener("keydown", async function(e) {
|
| 186 |
+
if (e.key === "Enter") {
|
| 187 |
+
const query = searchBox.value.trim();
|
| 188 |
+
localStorage.setItem('selectedCompany', query);
|
| 189 |
+
try {
|
| 190 |
+
const res = await fetch(`/api/lookup-symbol?query=${encodeURIComponent(query)}`);
|
| 191 |
+
const data = await res.json();
|
| 192 |
+
if (data.symbol) {
|
| 193 |
+
window.location.href = `/fundamentals?symbol=${encodeURIComponent(data.symbol)}`;
|
| 194 |
+
} else {
|
| 195 |
+
showMessage("Company not found", true);
|
| 196 |
+
}
|
| 197 |
+
} catch {
|
| 198 |
+
showMessage("Error looking up symbol", true);
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
});
|
| 202 |
+
}
|
| 203 |
+
});
|
| 204 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 205 |
+
if (!localStorage.getItem('marketmind_tutorial_shown')) {
|
| 206 |
+
introJs().setOptions({
|
| 207 |
+
nextLabel: 'Next',
|
| 208 |
+
prevLabel: 'Back',
|
| 209 |
+
doneLabel: 'Done',
|
| 210 |
+
skipLabel: 'Skip',
|
| 211 |
+
showProgress: true,
|
| 212 |
+
showBullets: false,
|
| 213 |
+
exitOnOverlayClick: false,
|
| 214 |
+
tooltipClass: 'custom-introjs-tooltip',
|
| 215 |
+
}).oncomplete(function() {
|
| 216 |
+
localStorage.setItem('marketmind_tutorial_shown', 'yes');
|
| 217 |
+
}).onexit(function() {
|
| 218 |
+
localStorage.setItem('marketmind_tutorial_shown', 'yes');
|
| 219 |
+
}).start();
|
| 220 |
+
}
|
| 221 |
+
});
|
| 222 |
+
|
| 223 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 224 |
+
initializeNewsletterForm();
|
| 225 |
+
// Other page-specific code...
|
| 226 |
+
});
|
| 227 |
+
span.addEventListener('click', function() {
|
| 228 |
+
const searchBox = document.getElementById("home-search-box");
|
| 229 |
+
if (searchBox) {
|
| 230 |
+
searchBox.value = this.textContent;
|
| 231 |
+
searchBox.focus();
|
| 232 |
+
// Store the selected company in localStorage
|
| 233 |
+
localStorage.setItem('selectedCompany', this.textContent);
|
| 234 |
+
}
|
| 235 |
+
});
|
static/js/legal.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 2 |
+
const darkModeToggle = document.getElementById("darkModeToggle");
|
| 3 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 4 |
+
|
| 5 |
+
// Set dark mode on by default
|
| 6 |
+
darkModeToggle.checked = true;
|
| 7 |
+
document.body.classList.add("dark-mode");
|
| 8 |
+
updateToggleIcon();
|
| 9 |
+
|
| 10 |
+
function updateToggleIcon() {
|
| 11 |
+
if (darkModeToggle.checked) {
|
| 12 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 13 |
+
} else {
|
| 14 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
darkModeToggle.addEventListener("change", function() {
|
| 19 |
+
document.body.classList.toggle("dark-mode");
|
| 20 |
+
updateToggleIcon();
|
| 21 |
+
});
|
| 22 |
+
});
|
static/js/login.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { firebaseAuth , initializeFirebase } from './firebase-config.js';
|
| 2 |
+
import { onAuthStateChanged } from "https://www.gstatic.com/firebasejs/10.11.1/firebase-auth.js";
|
| 3 |
+
|
| 4 |
+
document.addEventListener("DOMContentLoaded", async function () {
|
| 5 |
+
// Always initialize Firebase first!
|
| 6 |
+
await initializeFirebase();
|
| 7 |
+
const emailInput = document.getElementById("email");
|
| 8 |
+
const passwordInput = document.getElementById("password");
|
| 9 |
+
|
| 10 |
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
| 11 |
+
|
| 12 |
+
// Create feedback containers
|
| 13 |
+
const emailFeedback = document.createElement("div");
|
| 14 |
+
const passwordFeedback = document.createElement("div");
|
| 15 |
+
emailFeedback.style.color = "red";
|
| 16 |
+
passwordFeedback.style.color = "red";
|
| 17 |
+
emailInput.insertAdjacentElement("afterend", emailFeedback);
|
| 18 |
+
passwordInput.insertAdjacentElement("afterend", passwordFeedback);
|
| 19 |
+
|
| 20 |
+
// Email validation
|
| 21 |
+
emailInput.addEventListener("input", () => {
|
| 22 |
+
const emailValue = emailInput.value;
|
| 23 |
+
if (!emailRegex.test(emailValue)) {
|
| 24 |
+
emailFeedback.textContent = "Invalid email format";
|
| 25 |
+
emailInput.style.border = "2px solid red";
|
| 26 |
+
} else if (emailValue.length < 5 || emailValue.length > 50) {
|
| 27 |
+
emailFeedback.textContent = "Email must be between 5 and 50 characters";
|
| 28 |
+
emailInput.style.border = "2px solid orange";
|
| 29 |
+
} else {
|
| 30 |
+
emailFeedback.textContent = "";
|
| 31 |
+
emailInput.style.border = "2px solid #0FFF50";
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Password strength checker
|
| 36 |
+
passwordInput.addEventListener("input", () => {
|
| 37 |
+
const pwd = passwordInput.value;
|
| 38 |
+
let strength = 0;
|
| 39 |
+
if (pwd.length >= 8) strength++;
|
| 40 |
+
if (/[A-Z]/.test(pwd)) strength++;
|
| 41 |
+
if (/[a-z]/.test(pwd)) strength++;
|
| 42 |
+
if (/\d/.test(pwd)) strength++;
|
| 43 |
+
if (/[@$!%*?&]/.test(pwd)) strength++;
|
| 44 |
+
|
| 45 |
+
if (pwd.length < 8 || pwd.length > 20) {
|
| 46 |
+
passwordFeedback.textContent = "Password must be 8–20 characters";
|
| 47 |
+
passwordInput.style.border = "2px solid red";
|
| 48 |
+
passwordFeedback.style.color = "red";
|
| 49 |
+
} else if (strength <= 2) {
|
| 50 |
+
passwordFeedback.textContent = "Weak password";
|
| 51 |
+
passwordInput.style.border = "2px solid red";
|
| 52 |
+
passwordFeedback.style.color = "red";
|
| 53 |
+
} else if (strength === 3 || strength === 4) {
|
| 54 |
+
passwordFeedback.textContent = "Moderate password";
|
| 55 |
+
passwordInput.style.border = "2px solid orange";
|
| 56 |
+
passwordFeedback.style.color = "orange";
|
| 57 |
+
} else {
|
| 58 |
+
passwordFeedback.textContent = "Strong password";
|
| 59 |
+
passwordInput.style.border = "2px solid #0FFF50";
|
| 60 |
+
passwordFeedback.style.color = "#0FFF50";
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 66 |
+
const darkModeToggle = document.getElementById("theme-toggle-checkbox");
|
| 67 |
+
|
| 68 |
+
// Set the toggle to checked (on) by default
|
| 69 |
+
darkModeToggle.checked = true;
|
| 70 |
+
|
| 71 |
+
// Apply dark mode on page load
|
| 72 |
+
document.body.classList.add("dark-mode");
|
| 73 |
+
|
| 74 |
+
// Add the existing event listener for toggling
|
| 75 |
+
darkModeToggle.addEventListener("change", function () {
|
| 76 |
+
document.body.classList.toggle("dark-mode");
|
| 77 |
+
|
| 78 |
+
// Update chart colors if charts exist
|
| 79 |
+
updateChartColors(this.checked);
|
| 80 |
+
});
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 84 |
+
// DOM Elements
|
| 85 |
+
const authForm = document.getElementById('auth-form');
|
| 86 |
+
const nameFields = document.getElementById('name-fields');
|
| 87 |
+
const firstNameInput = document.getElementById('first-name');
|
| 88 |
+
const lastNameInput = document.getElementById('last-name');
|
| 89 |
+
const emailInput = document.getElementById('email');
|
| 90 |
+
const passwordInput = document.getElementById('password');
|
| 91 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 92 |
+
const messageDiv = document.getElementById('message');
|
| 93 |
+
const formTitle = document.getElementById('form-title');
|
| 94 |
+
const formSubtitle = document.getElementById('form-subtitle');
|
| 95 |
+
const toggleFormLink = document.getElementById('toggle-form');
|
| 96 |
+
const separatorText = document.getElementById('separator-text');
|
| 97 |
+
// const googleBtn = document.getElementById('google-btn');
|
| 98 |
+
// const appleBtn = document.getElementById('apple-btn');
|
| 99 |
+
const loadingSpinner = document.getElementById('loading-spinner');
|
| 100 |
+
|
| 101 |
+
// Flag to track current form mode (signup or login)
|
| 102 |
+
let isSignupMode = true;
|
| 103 |
+
|
| 104 |
+
// Toggle between signup and login forms
|
| 105 |
+
function toggleForm(e) {
|
| 106 |
+
if (e) e.preventDefault();
|
| 107 |
+
isSignupMode = !isSignupMode;
|
| 108 |
+
|
| 109 |
+
if (isSignupMode) {
|
| 110 |
+
formTitle.textContent = 'Create an account';
|
| 111 |
+
formSubtitle.innerHTML = 'Already have an account? <a href="#" id="toggle-form">Log in</a>';
|
| 112 |
+
submitBtn.textContent = 'Create account';
|
| 113 |
+
nameFields.style.display = 'flex';
|
| 114 |
+
separatorText.textContent = 'Or Register With';
|
| 115 |
+
} else {
|
| 116 |
+
formTitle.textContent = 'Welcome back';
|
| 117 |
+
formSubtitle.innerHTML = 'New user? <a href="#" id="toggle-form">Sign up</a>';
|
| 118 |
+
submitBtn.textContent = 'Login';
|
| 119 |
+
nameFields.style.display = 'none';
|
| 120 |
+
separatorText.textContent = 'Or Login With';
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Reattach event listener to the new toggle link
|
| 124 |
+
document.getElementById('toggle-form').addEventListener('click', toggleForm);
|
| 125 |
+
|
| 126 |
+
// Clear form and messages
|
| 127 |
+
authForm.reset();
|
| 128 |
+
messageDiv.textContent = '';
|
| 129 |
+
messageDiv.className = 'error-message';
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// Initial toggle link setup
|
| 133 |
+
toggleFormLink.addEventListener('click', toggleForm);
|
| 134 |
+
|
| 135 |
+
// Show loading spinner
|
| 136 |
+
function showLoading() {
|
| 137 |
+
loadingSpinner.style.display = 'inline-block';
|
| 138 |
+
submitBtn.disabled = true;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Hide loading spinner
|
| 142 |
+
function hideLoading() {
|
| 143 |
+
loadingSpinner.style.display = 'none';
|
| 144 |
+
submitBtn.disabled = false;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Display error message
|
| 148 |
+
function showError(message) {
|
| 149 |
+
messageDiv.textContent = message;
|
| 150 |
+
messageDiv.className = 'error-message';
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// Display success message
|
| 154 |
+
function showSuccess(message) {
|
| 155 |
+
messageDiv.textContent = message;
|
| 156 |
+
messageDiv.className = 'success-message';
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// Handle redirection after successful authentication
|
| 160 |
+
function handleSuccessfulAuth() {
|
| 161 |
+
showSuccess('Authentication successful! Redirecting...');
|
| 162 |
+
setTimeout(() => {
|
| 163 |
+
// Redirect to home page
|
| 164 |
+
window.location.href = '/';
|
| 165 |
+
}, 2000);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Form submission handler
|
| 169 |
+
authForm.addEventListener('submit', function (e) {
|
| 170 |
+
e.preventDefault();
|
| 171 |
+
|
| 172 |
+
const email = emailInput.value.trim();
|
| 173 |
+
const password = passwordInput.value.trim();
|
| 174 |
+
|
| 175 |
+
// Validate form
|
| 176 |
+
if (!email || !password) {
|
| 177 |
+
showError('Please fill in all required fields');
|
| 178 |
+
return;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
if (isSignupMode && (!firstNameInput.value.trim() || !lastNameInput.value.trim())) {
|
| 182 |
+
showError('Please provide both first and last name');
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
showLoading();
|
| 187 |
+
|
| 188 |
+
if (isSignupMode) {
|
| 189 |
+
// Signup with email and password
|
| 190 |
+
const firstName = firstNameInput.value.trim();
|
| 191 |
+
const lastName = lastNameInput.value.trim();
|
| 192 |
+
|
| 193 |
+
firebaseAuth.signUp(email, password, firstName, lastName)
|
| 194 |
+
.then(() => {
|
| 195 |
+
hideLoading();
|
| 196 |
+
handleSuccessfulAuth();
|
| 197 |
+
})
|
| 198 |
+
.catch((error) => {
|
| 199 |
+
hideLoading();
|
| 200 |
+
|
| 201 |
+
switch (error.code) {
|
| 202 |
+
case 'auth/email-already-in-use':
|
| 203 |
+
showError('Email is already in use. Try logging in instead.');
|
| 204 |
+
break;
|
| 205 |
+
case 'auth/invalid-email':
|
| 206 |
+
showError('Please enter a valid email address.');
|
| 207 |
+
break;
|
| 208 |
+
case 'auth/weak-password':
|
| 209 |
+
showError('Password is too weak. Use at least 6 characters.');
|
| 210 |
+
break;
|
| 211 |
+
default:
|
| 212 |
+
showError('An error occurred during signup. Please try again.');
|
| 213 |
+
}
|
| 214 |
+
});
|
| 215 |
+
} else {
|
| 216 |
+
// Login with email and password
|
| 217 |
+
firebaseAuth.signIn(email, password)
|
| 218 |
+
.then(() => {
|
| 219 |
+
hideLoading();
|
| 220 |
+
handleSuccessfulAuth();
|
| 221 |
+
})
|
| 222 |
+
.catch((error) => {
|
| 223 |
+
hideLoading();
|
| 224 |
+
|
| 225 |
+
switch (error.code) {
|
| 226 |
+
case 'auth/user-not-found':
|
| 227 |
+
case 'auth/wrong-password':
|
| 228 |
+
showError('Invalid email or password.');
|
| 229 |
+
break;
|
| 230 |
+
case 'auth/too-many-requests':
|
| 231 |
+
showError('Too many failed login attempts. Try again later.');
|
| 232 |
+
break;
|
| 233 |
+
default:
|
| 234 |
+
showError('Login failed. Please try again.');
|
| 235 |
+
}
|
| 236 |
+
});
|
| 237 |
+
}
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// // Google Sign-In
|
| 241 |
+
// googleBtn.addEventListener('click', function () {
|
| 242 |
+
// showLoading();
|
| 243 |
+
|
| 244 |
+
// firebaseAuth.signInWithGoogle()
|
| 245 |
+
// .then(() => {
|
| 246 |
+
// hideLoading();
|
| 247 |
+
// handleSuccessfulAuth();
|
| 248 |
+
// })
|
| 249 |
+
// .catch((error) => {
|
| 250 |
+
// hideLoading();
|
| 251 |
+
|
| 252 |
+
// if (error.code === 'auth/popup-closed-by-user') {
|
| 253 |
+
// showError('Sign-in popup was closed. Please try again.');
|
| 254 |
+
// } else if (error.code === 'auth/account-exists-with-different-credential') {
|
| 255 |
+
// showError('An account already exists with the same email address but different sign-in credentials.');
|
| 256 |
+
// } else if (error.code === 'auth/unauthorized-domain') {
|
| 257 |
+
// showError('Unauthorized domain. Please check your Firebase redirect URI settings.');
|
| 258 |
+
// } else {
|
| 259 |
+
// showError('An error occurred during Google sign-in. Please try again.');
|
| 260 |
+
// }
|
| 261 |
+
// });
|
| 262 |
+
// });
|
| 263 |
+
|
| 264 |
+
// // Apple Sign-In
|
| 265 |
+
// appleBtn.addEventListener('click', function () {
|
| 266 |
+
// showLoading();
|
| 267 |
+
|
| 268 |
+
// firebaseAuth.signInWithApple()
|
| 269 |
+
// .then(() => {
|
| 270 |
+
// hideLoading();
|
| 271 |
+
// handleSuccessfulAuth();
|
| 272 |
+
// })
|
| 273 |
+
// .catch((error) => {
|
| 274 |
+
// hideLoading();
|
| 275 |
+
|
| 276 |
+
// if (error.code === 'auth/popup-closed-by-user') {
|
| 277 |
+
// showError('Sign-in popup was closed. Please try again.');
|
| 278 |
+
// } else {
|
| 279 |
+
// showError('An error occurred during Apple sign-in. Please try again.');
|
| 280 |
+
// }
|
| 281 |
+
// });
|
| 282 |
+
// });
|
| 283 |
+
|
| 284 |
+
// Monitor auth state
|
| 285 |
+
onAuthStateChanged(firebaseAuth.auth, (user) => {
|
| 286 |
+
const messageDiv = document.getElementById('loginMessage');
|
| 287 |
+
if (user) {
|
| 288 |
+
if (window.location.pathname === '/login') {
|
| 289 |
+
messageDiv.textContent = 'You are already logged in. Please return to the home page.';
|
| 290 |
+
}
|
| 291 |
+
} else {
|
| 292 |
+
messageDiv.textContent = '';
|
| 293 |
+
}
|
| 294 |
+
});
|
| 295 |
+
});
|
static/js/movers.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { doc, getDoc, setDoc, collection } from "https://www.gstatic.com/firebasejs/10.11.1/firebase-firestore.js";
|
| 2 |
+
import { db, getUserIdAsync, initializeFirebase } from './firebase-config.js';
|
| 3 |
+
|
| 4 |
+
// To get a collection reference:
|
| 5 |
+
let favoritesCollection;
|
| 6 |
+
document.addEventListener("DOMContentLoaded", async function() {
|
| 7 |
+
await initializeFirebase(); // Ensure Firebase is initialized
|
| 8 |
+
favoritesCollection = collection(db, "Bookmarks"); // Now db is valid
|
| 9 |
+
|
| 10 |
+
});
|
| 11 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 12 |
+
// Dark mode toggle
|
| 13 |
+
const darkModeToggle = document.getElementById("darkModeToggle");
|
| 14 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 15 |
+
|
| 16 |
+
function updateToggleIcon() {
|
| 17 |
+
if (darkModeToggle.checked) {
|
| 18 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 19 |
+
} else {
|
| 20 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
// Initial state
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
// Set the toggle to checked (on) by default
|
| 27 |
+
darkModeToggle.checked = true;
|
| 28 |
+
updateToggleIcon();
|
| 29 |
+
// Apply dark mode on page load
|
| 30 |
+
document.body.classList.add("dark-mode");
|
| 31 |
+
|
| 32 |
+
darkModeToggle.addEventListener("change", function() {
|
| 33 |
+
document.body.classList.toggle("dark-mode");
|
| 34 |
+
if (typeof updateChartColors === "function") updateChartColors(this.checked);
|
| 35 |
+
updateToggleIcon();
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
// Populate the dropdown with company options
|
| 39 |
+
fetch("/get-companies")
|
| 40 |
+
.then(res => res.json())
|
| 41 |
+
.then(companies => {
|
| 42 |
+
const select = document.getElementById("fundamentals-symbol");
|
| 43 |
+
if (!select) return;
|
| 44 |
+
companies.forEach(company => {
|
| 45 |
+
const option = document.createElement("option");
|
| 46 |
+
option.value = company["Yahoo Finance Ticker"];
|
| 47 |
+
option.textContent = `${company["Company Name"]} (${company["Yahoo Finance Ticker"]})`;
|
| 48 |
+
select.appendChild(option);
|
| 49 |
+
});
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// Fade-in pge on load
|
| 53 |
+
document.body.classList.add("loaded");
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
// Smooth page transitions for internal links
|
| 57 |
+
document.querySelectorAll("a").forEach(link => {
|
| 58 |
+
link.addEventListener("click", function (event) {
|
| 59 |
+
if (this.getAttribute("target") === "_blank") return;
|
| 60 |
+
event.preventDefault();
|
| 61 |
+
let href = this.href;
|
| 62 |
+
let overlay = document.createElement("div");
|
| 63 |
+
overlay.classList.add("transition-overlay");
|
| 64 |
+
document.body.appendChild(overlay);
|
| 65 |
+
setTimeout(() => { overlay.classList.add("active"); }, 90);
|
| 66 |
+
setTimeout(() => { overlay.remove(); }, 1000);
|
| 67 |
+
setTimeout(() => { window.location.href = href; }, 890);
|
| 68 |
+
});
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
// --- GRAPH MODE ONLY ---
|
| 72 |
+
|
| 73 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 74 |
+
const fetchBtn = document.getElementById('fetch');
|
| 75 |
+
const symbolSelect = document.getElementById('fundamentals-symbol');
|
| 76 |
+
const startInput = document.getElementById('start-date');
|
| 77 |
+
const endInput = document.getElementById('end-date');
|
| 78 |
+
const chartDiv = document.getElementById('stockChart');
|
| 79 |
+
const messageContainer = document.getElementById('message-container');
|
| 80 |
+
|
| 81 |
+
if (fetchBtn) {
|
| 82 |
+
fetchBtn.addEventListener('click', async function () {
|
| 83 |
+
const symbol = symbolSelect.value;
|
| 84 |
+
const start = startInput.value;
|
| 85 |
+
const end = endInput.value;
|
| 86 |
+
if (!symbol) {
|
| 87 |
+
showMessage("Please select a company from the dropdown.", true);
|
| 88 |
+
return;
|
| 89 |
+
}
|
| 90 |
+
if (!start || !end) {
|
| 91 |
+
showMessage("Please select both start and end dates.", true);
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
+
messageContainer.style.display = "none";
|
| 95 |
+
if (chartDiv) chartDiv.innerHTML = "<div class='loading-spinner'></div>";
|
| 96 |
+
|
| 97 |
+
let url = `/api/historical?symbol=${encodeURIComponent(symbol)}`;
|
| 98 |
+
if (start && end) url += `&start=${start}&end=${end}`;
|
| 99 |
+
try {
|
| 100 |
+
const res = await fetch(url);
|
| 101 |
+
const data = await res.json();
|
| 102 |
+
if (data.error) {
|
| 103 |
+
showMessage(data.error, true);
|
| 104 |
+
return;
|
| 105 |
+
}
|
| 106 |
+
if (!data.history || !Array.isArray(data.history) || !data.history.length) {
|
| 107 |
+
if (chartDiv) chartDiv.innerHTML = "<p>No data available for the selected range.</p>";
|
| 108 |
+
return;
|
| 109 |
+
}
|
| 110 |
+
renderStockChart(data.history, symbol);
|
| 111 |
+
renderStockChart(currentView);
|
| 112 |
+
} catch (e) {
|
| 113 |
+
showMessage("Failed to fetch stock data.", true);
|
| 114 |
+
}
|
| 115 |
+
});
|
| 116 |
+
}
|
| 117 |
+
fetch('/api/market-movers')
|
| 118 |
+
.then(res => res.json())
|
| 119 |
+
.then(data => {
|
| 120 |
+
if (data.error) {
|
| 121 |
+
document.getElementById('top-gainers').innerHTML = "<li>Error loading gainers</li>";
|
| 122 |
+
document.getElementById('top-losers').innerHTML = "<li>Error loading losers</li>";
|
| 123 |
+
return;
|
| 124 |
+
}
|
| 125 |
+
// Gainers
|
| 126 |
+
const gainersList = document.getElementById('top-gainers');
|
| 127 |
+
gainersList.innerHTML = "";
|
| 128 |
+
data.gainers.forEach(g => {
|
| 129 |
+
const pct = (g.pct_change !== undefined && g.pct_change !== null) ? g.pct_change + "%" : "N/A";
|
| 130 |
+
const price = (g.last_close !== undefined && g.last_close !== null) ? "₹" + g.last_close : "N/A";
|
| 131 |
+
gainersList.innerHTML += `<li>
|
| 132 |
+
<span>${g.symbol}</span>
|
| 133 |
+
<span class="up">▲ ${pct}</span>
|
| 134 |
+
<span>${price}</span>
|
| 135 |
+
</li>`;
|
| 136 |
+
});
|
| 137 |
+
// Losers
|
| 138 |
+
const losersList = document.getElementById('top-losers');
|
| 139 |
+
losersList.innerHTML = "";
|
| 140 |
+
data.losers.forEach(l => {
|
| 141 |
+
const pct = (l.pct_change !== undefined && l.pct_change !== null) ? l.pct_change + "%" : "N/A";
|
| 142 |
+
const price = (l.last_close !== undefined && l.last_close !== null) ? "₹" + l.last_close : "N/A";
|
| 143 |
+
losersList.innerHTML += `<li>
|
| 144 |
+
<span>${l.symbol}</span>
|
| 145 |
+
<span class="down">▼ ${pct}</span>
|
| 146 |
+
<span>${price}</span>
|
| 147 |
+
</li>`;
|
| 148 |
+
});
|
| 149 |
+
})
|
| 150 |
+
.catch(() => {
|
| 151 |
+
document.getElementById('top-gainers').innerHTML = "<li>Error loading gainers</li>";
|
| 152 |
+
document.getElementById('top-losers').innerHTML = "<li>Error loading losers</li>";
|
| 153 |
+
});
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
function renderStockChart(history, symbol) {
|
| 157 |
+
const chartDiv = document.getElementById('stockChart');
|
| 158 |
+
if (!chartDiv || !Array.isArray(history) || !history.length) return;
|
| 159 |
+
const plotBg = document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff';
|
| 160 |
+
const paperBg = plotBg;
|
| 161 |
+
const gridColor = document.body.classList.contains("dark-mode") ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)';
|
| 162 |
+
const fontColor = document.body.classList.contains("dark-mode") ? "#fff" : "#333";
|
| 163 |
+
const dates = history.map(d => d.Date || d.date);
|
| 164 |
+
const closes = history.map(d => d.Close ?? d.close);
|
| 165 |
+
const trace = {
|
| 166 |
+
x: dates,
|
| 167 |
+
y: closes,
|
| 168 |
+
type: 'scatter',
|
| 169 |
+
mode: 'lines+markers',
|
| 170 |
+
name: 'Close Price',
|
| 171 |
+
fill: 'tozeroy',
|
| 172 |
+
line: { color: '#2196F3' }
|
| 173 |
+
};
|
| 174 |
+
const layout = {
|
| 175 |
+
title: `${symbol} Price Chart`,
|
| 176 |
+
xaxis: { title: 'Date', gridcolor: gridColor },
|
| 177 |
+
yaxis: { title: 'Close Price', gridcolor: gridColor },
|
| 178 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 179 |
+
plot_bgcolor: plotBg,
|
| 180 |
+
paper_bgcolor: paperBg,
|
| 181 |
+
font: { color: fontColor }
|
| 182 |
+
};
|
| 183 |
+
Plotly.newPlot(chartDiv, [trace], layout, {responsive: true});
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
function showMessage(message, isError = false) {
|
| 188 |
+
const messageContainer = document.getElementById("message-container");
|
| 189 |
+
if (!messageContainer) return;
|
| 190 |
+
messageContainer.textContent = message;
|
| 191 |
+
messageContainer.style.display = "block";
|
| 192 |
+
messageContainer.style.backgroundColor = isError ? "#ffebee" : "#e8f5e9";
|
| 193 |
+
messageContainer.style.color = isError ? "#c62828" : "#2e7d32";
|
| 194 |
+
messageContainer.style.border = isError ? "1px solid #c62828" : "1px solid #2e7d32";
|
| 195 |
+
setTimeout(() => {
|
| 196 |
+
messageContainer.style.display = "none";
|
| 197 |
+
}, 5000);
|
| 198 |
+
}
|
| 199 |
+
let gainersChart, losersChart;
|
| 200 |
+
|
| 201 |
+
function showGainersPlotly(gainers) {
|
| 202 |
+
const symbols = gainers.map(g => g.symbol);
|
| 203 |
+
const changes = gainers.map(g => g.pct_change);
|
| 204 |
+
const trace = {
|
| 205 |
+
x: symbols,
|
| 206 |
+
y: changes,
|
| 207 |
+
type: 'bar',
|
| 208 |
+
marker: { color: 'rgba(34,197,94,0.7)' }
|
| 209 |
+
};
|
| 210 |
+
const layout = {
|
| 211 |
+
title: 'Top Gainers (% Change)',
|
| 212 |
+
xaxis: { title: 'Symbol' },
|
| 213 |
+
yaxis: { title: '% Change' },
|
| 214 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 215 |
+
plot_bgcolor: document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff',
|
| 216 |
+
paper_bgcolor: document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff',
|
| 217 |
+
font: { color: document.body.classList.contains("dark-mode") ? "#fff" : "#333" }
|
| 218 |
+
};
|
| 219 |
+
Plotly.newPlot('gainers-plotly', [trace], layout, {responsive: true});
|
| 220 |
+
}
|
| 221 |
+
window.showGainersPlotly = showGainersPlotly;
|
| 222 |
+
function showLosersPlotly(losers) {
|
| 223 |
+
const symbols = losers.map(l => l.symbol);
|
| 224 |
+
const changes = losers.map(l => l.pct_change);
|
| 225 |
+
|
| 226 |
+
const trace = {
|
| 227 |
+
x: symbols,
|
| 228 |
+
y: changes,
|
| 229 |
+
type: 'bar',
|
| 230 |
+
marker: { color: 'rgba(239,68,68,0.7)' }
|
| 231 |
+
};
|
| 232 |
+
const layout = {
|
| 233 |
+
title: 'Top Losers (% Change)',
|
| 234 |
+
xaxis: { title: 'Symbol' },
|
| 235 |
+
yaxis: { title: '% Change' },
|
| 236 |
+
margin: { t: 40, l: 40, r: 40, b: 40 },
|
| 237 |
+
plot_bgcolor: document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff',
|
| 238 |
+
paper_bgcolor: document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff',
|
| 239 |
+
font: { color: document.body.classList.contains("dark-mode") ? "#fff" : "#333" }
|
| 240 |
+
};
|
| 241 |
+
Plotly.newPlot('losers-plotly', [trace], layout, {responsive: true});
|
| 242 |
+
}
|
| 243 |
+
window.showLosersPlotly = showLosersPlotly;
|
| 244 |
+
|
| 245 |
+
// Toggle logic for list/graph view
|
| 246 |
+
document.getElementById('list-view-btn').addEventListener('click', function() {
|
| 247 |
+
document.getElementById('top-gainers').style.display = '';
|
| 248 |
+
document.getElementById('top-losers').style.display = '';
|
| 249 |
+
document.getElementById('gainers-plotly').style.display = 'none';
|
| 250 |
+
document.getElementById('losers-plotly').style.display = 'none';
|
| 251 |
+
this.classList.add('active');
|
| 252 |
+
document.getElementById('graph-view-btn').classList.remove('active');
|
| 253 |
+
});
|
| 254 |
+
document.getElementById('graph-view-btn').addEventListener('click', function() {
|
| 255 |
+
document.getElementById('top-gainers').style.display = 'none';
|
| 256 |
+
document.getElementById('top-losers').style.display = 'none';
|
| 257 |
+
document.getElementById('gainers-plotly').style.display = '';
|
| 258 |
+
document.getElementById('losers-plotly').style.display = '';
|
| 259 |
+
this.classList.add('active');
|
| 260 |
+
document.getElementById('list-view-btn').classList.remove('active');
|
| 261 |
+
// Draw charts with latest data
|
| 262 |
+
if (window.latestGainers && window.latestLosers) {
|
| 263 |
+
showGainersPlotly(window.latestGainers);
|
| 264 |
+
showLosersPlotly(window.latestLosers);
|
| 265 |
+
}
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
// After fetching market movers, store for chart use
|
| 269 |
+
fetch('/api/market-movers')
|
| 270 |
+
.then(res => res.json())
|
| 271 |
+
.then(data => {
|
| 272 |
+
if (data.error) {
|
| 273 |
+
document.getElementById('top-gainers').innerHTML = "<li>Error loading gainers</li>";
|
| 274 |
+
document.getElementById('top-losers').innerHTML = "<li>Error loading losers</li>";
|
| 275 |
+
return;
|
| 276 |
+
}
|
| 277 |
+
window.latestGainers = data.gainers;
|
| 278 |
+
window.latestLosers = data.losers;
|
| 279 |
+
// ...existing list rendering code...
|
| 280 |
+
const gainersList = document.getElementById('top-gainers');
|
| 281 |
+
gainersList.innerHTML = "";
|
| 282 |
+
data.gainers.forEach(g => {
|
| 283 |
+
const pct = (g.pct_change !== undefined && g.pct_change !== null) ? g.pct_change + "%" : "N/A";
|
| 284 |
+
const price = (g.last_close !== undefined && g.last_close !== null) ? "₹" + g.last_close : "N/A";
|
| 285 |
+
gainersList.innerHTML += `<li>
|
| 286 |
+
<span>${g.symbol}</span>
|
| 287 |
+
<span class="up">▲ ${pct}</span>
|
| 288 |
+
<span>${price}</span>
|
| 289 |
+
</li>`;
|
| 290 |
+
});
|
| 291 |
+
const losersList = document.getElementById('top-losers');
|
| 292 |
+
losersList.innerHTML = "";
|
| 293 |
+
data.losers.forEach(l => {
|
| 294 |
+
const pct = (l.pct_change !== undefined && l.pct_change !== null) ? l.pct_change + "%" : "N/A";
|
| 295 |
+
const price = (l.last_close !== undefined && l.last_close !== null) ? "₹" + l.last_close : "N/A";
|
| 296 |
+
losersList.innerHTML += `<li>
|
| 297 |
+
<span>${l.symbol}</span>
|
| 298 |
+
<span class="down">▼ ${pct}</span>
|
| 299 |
+
<span>${price}</span>
|
| 300 |
+
</li>`;
|
| 301 |
+
});
|
| 302 |
+
})
|
| 303 |
+
.catch(() => {
|
| 304 |
+
document.getElementById('top-gainers').innerHTML = "<li>Error loading gainers</li>";
|
| 305 |
+
document.getElementById('top-losers').innerHTML = "<li>Error loading losers</li>";
|
| 306 |
+
});
|
| 307 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 308 |
+
fetch('/api/market-movers')
|
| 309 |
+
.then(res => res.json())
|
| 310 |
+
.then(data => {
|
| 311 |
+
const tickerDiv = document.getElementById('ticker-content-indian');
|
| 312 |
+
if (!tickerDiv) return;
|
| 313 |
+
let tickerHTML = '';
|
| 314 |
+
|
| 315 |
+
if (data.gainers && data.gainers.length) {
|
| 316 |
+
tickerHTML += `<span class="ticker-label gainers-label">Top Gainers:</span> `;
|
| 317 |
+
tickerHTML += data.gainers.map(g =>
|
| 318 |
+
`<span class="ticker-item">
|
| 319 |
+
${g.symbol}
|
| 320 |
+
<span class="up">▲${g.pct_change}%</span>
|
| 321 |
+
<span class="gainer-price">₹${g.last_close}</span>
|
| 322 |
+
</span>`
|
| 323 |
+
).join(' <span class="ticker-sep">|</span>');
|
| 324 |
+
tickerHTML += ' ';
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
if (data.losers && data.losers.length) {
|
| 328 |
+
tickerHTML += `<span class="ticker-label losers-label">Top Losers:</span> `;
|
| 329 |
+
tickerHTML += data.losers.map(l =>
|
| 330 |
+
`<span class="ticker-item">
|
| 331 |
+
${l.symbol}
|
| 332 |
+
<span class="down">▼${l.pct_change}%</span>
|
| 333 |
+
<span class="loser-price">₹${l.last_close}</span>
|
| 334 |
+
</span>`
|
| 335 |
+
).join(' <span class="ticker-sep">|</span> ');
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
tickerDiv.innerHTML = tickerHTML;
|
| 339 |
+
})
|
| 340 |
+
.catch(() => {
|
| 341 |
+
const tickerDiv = document.getElementById('ticker-content-indian');
|
| 342 |
+
if (tickerDiv) tickerDiv.textContent = "Unable to load market movers.";
|
| 343 |
+
});
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
document.addEventListener("DOMContentLoaded", async function () {
|
| 347 |
+
const tableBody = document.getElementById('favorites-tbody');
|
| 348 |
+
let bookmarks = [];
|
| 349 |
+
await initializeFirebase();
|
| 350 |
+
// Get user ID
|
| 351 |
+
const userId = await getUserIdAsync();
|
| 352 |
+
|
| 353 |
+
// Fetch bookmarks from Firebase
|
| 354 |
+
async function fetchBookmarks() {
|
| 355 |
+
const docRef = doc(db, "Bookmarks", userId);
|
| 356 |
+
const docSnap = await getDoc(docRef);
|
| 357 |
+
if (docSnap.exists()) {
|
| 358 |
+
bookmarks = docSnap.data().symbols || [];
|
| 359 |
+
} else {
|
| 360 |
+
bookmarks = [];
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Save bookmarks to Firebase
|
| 365 |
+
async function saveBookmarks() {
|
| 366 |
+
const docRef = doc(db, "Bookmarks", userId);
|
| 367 |
+
await setDoc(docRef, { symbols: bookmarks }, { merge: true });
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// Fetch companies from MongoDB
|
| 371 |
+
async function fetchCompanies() {
|
| 372 |
+
const res = await fetch("/get-companies");
|
| 373 |
+
return await res.json();
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
// Fetch current price for a ticker (implement this endpoint in your backend)
|
| 377 |
+
async function fetchCurrentPrice(ticker) {
|
| 378 |
+
try {
|
| 379 |
+
const res = await fetch(`/api/price?ticker=${encodeURIComponent(ticker)}`);
|
| 380 |
+
const data = await res.json();
|
| 381 |
+
return data.price ?? "N/A";
|
| 382 |
+
} catch {
|
| 383 |
+
return "N/A";
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
// Render the table
|
| 388 |
+
async function renderTable() {
|
| 389 |
+
console.log("renderTable called");
|
| 390 |
+
tableBody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>';
|
| 391 |
+
const companies = await fetchCompanies();
|
| 392 |
+
|
| 393 |
+
console.log("Companies fetched:", companies);
|
| 394 |
+
if (!companies || !companies.length) {
|
| 395 |
+
tableBody.innerHTML = '<tr><td colspan="4">No companies found.</td></tr>';
|
| 396 |
+
return;
|
| 397 |
+
}
|
| 398 |
+
// Fetch all prices in parallel
|
| 399 |
+
const prices = await Promise.all(companies.map(c =>
|
| 400 |
+
fetchCurrentPrice(c["Yahoo Finance Ticker"])
|
| 401 |
+
));
|
| 402 |
+
await fetchBookmarks();
|
| 403 |
+
console.log("Prices fetched:", prices);
|
| 404 |
+
|
| 405 |
+
// Combine companies and prices for easier sorting
|
| 406 |
+
const companiesWithPrices = companies.map((company, idx) => ({
|
| 407 |
+
...company,
|
| 408 |
+
price: prices[idx],
|
| 409 |
+
isBookmarked: bookmarks.includes(company["Yahoo Finance Ticker"])
|
| 410 |
+
}));
|
| 411 |
+
|
| 412 |
+
// Sort: bookmarked first, then others
|
| 413 |
+
companiesWithPrices.sort((a, b) => {
|
| 414 |
+
if (a.isBookmarked === b.isBookmarked) return 0;
|
| 415 |
+
return a.isBookmarked ? -1 : 1;
|
| 416 |
+
});
|
| 417 |
+
|
| 418 |
+
tableBody.innerHTML = '';
|
| 419 |
+
companiesWithPrices.forEach((company) => {
|
| 420 |
+
const ticker = company["Yahoo Finance Ticker"];
|
| 421 |
+
const isBookmarked = company.isBookmarked;
|
| 422 |
+
const price = company.price;
|
| 423 |
+
const row = document.createElement('tr');
|
| 424 |
+
row.innerHTML = `
|
| 425 |
+
<td>
|
| 426 |
+
<button class="bookmark-btn${isBookmarked ? ' bookmarked' : ''}" data-ticker="${ticker}" title="Bookmark">
|
| 427 |
+
${isBookmarked ? '★' : '☆'}
|
| 428 |
+
</button>
|
| 429 |
+
</td>
|
| 430 |
+
<td>${company["Company Name"]}</td>
|
| 431 |
+
<td>${ticker}</td>
|
| 432 |
+
<td>${price !== undefined ? '₹' + price : 'N/A'}</td>
|
| 433 |
+
`;
|
| 434 |
+
tableBody.appendChild(row);
|
| 435 |
+
});
|
| 436 |
+
} window.renderTable = renderTable;
|
| 437 |
+
|
| 438 |
+
// Handle bookmark click
|
| 439 |
+
tableBody.addEventListener('click', async function (e) {
|
| 440 |
+
if (e.target.classList.contains('bookmark-btn')) {
|
| 441 |
+
const ticker = e.target.getAttribute('data-ticker');
|
| 442 |
+
if (bookmarks.includes(ticker)) {
|
| 443 |
+
bookmarks = bookmarks.filter(t => t !== ticker);
|
| 444 |
+
} else {
|
| 445 |
+
bookmarks.push(ticker);
|
| 446 |
+
}
|
| 447 |
+
await saveBookmarks();
|
| 448 |
+
renderTable();
|
| 449 |
+
}
|
| 450 |
+
});
|
| 451 |
+
|
| 452 |
+
renderTable();
|
| 453 |
+
});
|
| 454 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 455 |
+
if (!localStorage.getItem('movers_tutorial_shown')) {
|
| 456 |
+
introJs().setOptions({
|
| 457 |
+
nextLabel: 'Next',
|
| 458 |
+
prevLabel: 'Back',
|
| 459 |
+
doneLabel: 'Done',
|
| 460 |
+
skipLabel: 'Skip',
|
| 461 |
+
showProgress: true,
|
| 462 |
+
showBullets: false,
|
| 463 |
+
exitOnOverlayClick: false,
|
| 464 |
+
tooltipClass: 'custom-introjs-tooltip',
|
| 465 |
+
}).oncomplete(function() {
|
| 466 |
+
localStorage.setItem('movers_tutorial_shown', 'yes');
|
| 467 |
+
}).onexit(function() {
|
| 468 |
+
localStorage.setItem('movers_tutorial_shown', 'yes');
|
| 469 |
+
}).start();
|
| 470 |
+
}
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
import { initializeNewsletterForm } from './newsletter.js';
|
| 474 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 475 |
+
initializeNewsletterForm();
|
| 476 |
+
// Other page-specific code...
|
| 477 |
+
});
|
static/js/news.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 2 |
+
const darkModeToggle = document.getElementById("darkModeToggle");
|
| 3 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 4 |
+
|
| 5 |
+
function updateToggleIcon() {
|
| 6 |
+
if (darkModeToggle.checked) {
|
| 7 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 8 |
+
} else {
|
| 9 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// Initial state
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
// Set the toggle to checked (on) by default
|
| 17 |
+
darkModeToggle.checked = true;
|
| 18 |
+
updateToggleIcon();
|
| 19 |
+
// Apply dark mode on page load
|
| 20 |
+
document.body.classList.add("dark-mode");
|
| 21 |
+
|
| 22 |
+
darkModeToggle.addEventListener("change", function() {
|
| 23 |
+
document.body.classList.toggle("dark-mode");
|
| 24 |
+
if (typeof updateChartColors === "function") updateChartColors(this.checked);
|
| 25 |
+
updateToggleIcon();
|
| 26 |
+
});
|
| 27 |
+
});
|
| 28 |
+
// Page transition
|
| 29 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 30 |
+
document.body.classList.add("loaded"); // Fade-in page on load
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
document.querySelectorAll("a").forEach(link => {
|
| 34 |
+
link.addEventListener("click", function (event) {
|
| 35 |
+
if (this.getAttribute("target") === "_blank") return;
|
| 36 |
+
|
| 37 |
+
event.preventDefault(); // Stop instant navigation
|
| 38 |
+
let href = this.href;
|
| 39 |
+
|
| 40 |
+
// Create transition element
|
| 41 |
+
let overlay = document.createElement("div");
|
| 42 |
+
overlay.classList.add("transition-overlay");
|
| 43 |
+
document.body.appendChild(overlay);
|
| 44 |
+
|
| 45 |
+
// Trigger expanding circle effect
|
| 46 |
+
setTimeout(() => {
|
| 47 |
+
overlay.classList.add("active");
|
| 48 |
+
}, 90);
|
| 49 |
+
setTimeout(() => {
|
| 50 |
+
overlay.remove();
|
| 51 |
+
}, 1000);
|
| 52 |
+
|
| 53 |
+
// Navigate after transition
|
| 54 |
+
setTimeout(() => {
|
| 55 |
+
window.location.href = href;
|
| 56 |
+
}, 890);
|
| 57 |
+
});
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 61 |
+
const apiUrl = `/api/news?query`;
|
| 62 |
+
const newsContainer = document.getElementById("news-container");
|
| 63 |
+
const refreshBtn = document.getElementById("refresh-news-btn");
|
| 64 |
+
|
| 65 |
+
function loadNews() {
|
| 66 |
+
// Clear previous news
|
| 67 |
+
newsContainer.innerHTML = "";
|
| 68 |
+
// Add loading spinner
|
| 69 |
+
const loadingSpinner = document.createElement("div");
|
| 70 |
+
loadingSpinner.classList.add("loading-spinner");
|
| 71 |
+
newsContainer.appendChild(loadingSpinner);
|
| 72 |
+
|
| 73 |
+
fetch(apiUrl)
|
| 74 |
+
.then(response => response.text())
|
| 75 |
+
.then(str => {
|
| 76 |
+
newsContainer.removeChild(loadingSpinner);
|
| 77 |
+
const parser = new window.DOMParser();
|
| 78 |
+
const xml = parser.parseFromString(str, "text/xml");
|
| 79 |
+
const items = xml.querySelectorAll("item");
|
| 80 |
+
if (items.length === 0) {
|
| 81 |
+
newsContainer.innerHTML = "<p>No news found for this query.</p>";
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
items.forEach(item => {
|
| 85 |
+
const title = item.querySelector("title")?.textContent || "No title";
|
| 86 |
+
const link = item.querySelector("link")?.textContent || "#";
|
| 87 |
+
const description = item.querySelector("description")?.textContent || "No description available.";
|
| 88 |
+
const pubDate = item.querySelector("pubDate")?.textContent || "";
|
| 89 |
+
|
| 90 |
+
const newsArticle = document.createElement("div");
|
| 91 |
+
newsArticle.classList.add("news-article");
|
| 92 |
+
newsArticle.innerHTML = `
|
| 93 |
+
<h3>${title}</h3>
|
| 94 |
+
<p>${description}</p>
|
| 95 |
+
<small>${pubDate}</small><br>
|
| 96 |
+
<a href="${link}" target="_blank">Read more</a>
|
| 97 |
+
`;
|
| 98 |
+
newsContainer.appendChild(newsArticle);
|
| 99 |
+
});
|
| 100 |
+
})
|
| 101 |
+
.catch((error) => {
|
| 102 |
+
console.error("Error fetching news:", error);
|
| 103 |
+
newsContainer.innerHTML = "<p>Failed to load news. Please try again later.</p>";
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Initial load
|
| 108 |
+
loadNews();
|
| 109 |
+
|
| 110 |
+
// Refresh button handler
|
| 111 |
+
if (refreshBtn) {
|
| 112 |
+
refreshBtn.addEventListener("click", function () {
|
| 113 |
+
loadNews();
|
| 114 |
+
});
|
| 115 |
+
}
|
| 116 |
+
});
|
| 117 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 118 |
+
if (!localStorage.getItem('news_tutorial_shown')) {
|
| 119 |
+
introJs().setOptions({
|
| 120 |
+
nextLabel: 'Next',
|
| 121 |
+
prevLabel: 'Back',
|
| 122 |
+
doneLabel: 'Done',
|
| 123 |
+
skipLabel: 'Skip',
|
| 124 |
+
showProgress: true,
|
| 125 |
+
showBullets: false,
|
| 126 |
+
exitOnOverlayClick: false,
|
| 127 |
+
tooltipClass: 'custom-introjs-tooltip'
|
| 128 |
+
}).oncomplete(function() {
|
| 129 |
+
localStorage.setItem('news_tutorial_shown', 'yes');
|
| 130 |
+
}).onexit(function() {
|
| 131 |
+
localStorage.setItem('news_tutorial_shown', 'yes');
|
| 132 |
+
}).start();
|
| 133 |
+
}
|
| 134 |
+
});
|
| 135 |
+
import { initializeNewsletterForm } from './newsletter.js';
|
| 136 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 137 |
+
initializeNewsletterForm();
|
| 138 |
+
// Other page-specific code...
|
| 139 |
+
});
|
static/js/newsletter.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { doc, setDoc } from "https://www.gstatic.com/firebasejs/10.11.1/firebase-firestore.js";
|
| 2 |
+
import { db, getUserIdAsync } from './firebase-config.js';
|
| 3 |
+
|
| 4 |
+
export function initializeNewsletterForm() {
|
| 5 |
+
const newsletterForm = document.getElementById("newsletter-form");
|
| 6 |
+
const newsletterEmail = document.getElementById("newsletter-email");
|
| 7 |
+
|
| 8 |
+
if (newsletterForm) {
|
| 9 |
+
newsletterForm.addEventListener("submit", async function (e) {
|
| 10 |
+
e.preventDefault(); // Prevent form from refreshing the page
|
| 11 |
+
|
| 12 |
+
const email = newsletterEmail.value.trim();
|
| 13 |
+
if (!email) {
|
| 14 |
+
alert("Please enter a valid email address.");
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
// Get the user ID
|
| 20 |
+
const userId = await getUserIdAsync();
|
| 21 |
+
|
| 22 |
+
// Save the email to Firebase under the user's document
|
| 23 |
+
const docRef = doc(db, "NewsletterSubscriptions", userId);
|
| 24 |
+
await setDoc(docRef, { email: email }, { merge: true });
|
| 25 |
+
|
| 26 |
+
alert("Thank you for subscribing to our newsletter!");
|
| 27 |
+
newsletterEmail.value = ""; // Clear the input field
|
| 28 |
+
} catch (error) {
|
| 29 |
+
console.error("Error saving email to Firebase:", error);
|
| 30 |
+
alert("Failed to subscribe. Please try again later.");
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
}
|
static/js/predict.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Page Transition
|
| 2 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 3 |
+
document.body.classList.add("loaded"); // Fade-in page on load
|
| 4 |
+
});
|
| 5 |
+
|
| 6 |
+
// Handle link clicks for page transitions
|
| 7 |
+
document.querySelectorAll("a").forEach(link => {
|
| 8 |
+
link.addEventListener("click", function (event) {
|
| 9 |
+
if (this.getAttribute("target") === "_blank") return;
|
| 10 |
+
|
| 11 |
+
event.preventDefault(); // Stop instant navigation
|
| 12 |
+
let href = this.href;
|
| 13 |
+
|
| 14 |
+
// Create transition element
|
| 15 |
+
let overlay = document.createElement("div");
|
| 16 |
+
overlay.classList.add("transition-overlay");
|
| 17 |
+
document.body.appendChild(overlay);
|
| 18 |
+
|
| 19 |
+
// Trigger expanding circle effect
|
| 20 |
+
setTimeout(() => {
|
| 21 |
+
overlay.classList.add("expand");
|
| 22 |
+
}, 90);
|
| 23 |
+
// Optional: Clean up the overlay after the animation duration
|
| 24 |
+
setTimeout(() => {
|
| 25 |
+
overlay.remove();
|
| 26 |
+
}, 1000);
|
| 27 |
+
|
| 28 |
+
// Navigate after transition
|
| 29 |
+
setTimeout(() => {
|
| 30 |
+
window.location.href = href;
|
| 31 |
+
}, 890);
|
| 32 |
+
});
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Fade-In Animation on Scroll
|
| 36 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 37 |
+
const fadeElements = document.querySelectorAll(".fade-in");
|
| 38 |
+
|
| 39 |
+
const checkVisibility = () => {
|
| 40 |
+
fadeElements.forEach((element) => {
|
| 41 |
+
const elementTop = element.getBoundingClientRect().top;
|
| 42 |
+
const elementBottom = element.getBoundingClientRect().bottom;
|
| 43 |
+
const isVisible = elementTop < window.innerHeight && elementBottom >= 0;
|
| 44 |
+
|
| 45 |
+
if (isVisible) {
|
| 46 |
+
element.classList.add("visible");
|
| 47 |
+
}
|
| 48 |
+
});
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
window.addEventListener("scroll", checkVisibility);
|
| 52 |
+
checkVisibility(); // Check on page load
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
// Page transition & dark mode toggle
|
| 57 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 58 |
+
document.body.classList.add("loaded");
|
| 59 |
+
|
| 60 |
+
const darkToggle = document.getElementById("darkModeToggle");
|
| 61 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 62 |
+
|
| 63 |
+
function updateToggleIcon() {
|
| 64 |
+
if (darkToggle && toggleIcon) {
|
| 65 |
+
if (darkToggle.checked) {
|
| 66 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 67 |
+
toggleIcon.style.color = "#4a8fdf";
|
| 68 |
+
} else {
|
| 69 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 70 |
+
toggleIcon.style.color = "#FFD600";
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if (darkToggle) {
|
| 76 |
+
darkToggle.checked = true;
|
| 77 |
+
document.body.classList.add("dark-mode");
|
| 78 |
+
updateToggleIcon();
|
| 79 |
+
|
| 80 |
+
darkToggle.addEventListener("change", () => {
|
| 81 |
+
document.body.classList.toggle("dark-mode");
|
| 82 |
+
updateToggleIcon();
|
| 83 |
+
updatePredictionChartColors();
|
| 84 |
+
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
document.querySelectorAll("a").forEach(link => {
|
| 89 |
+
if (link.getAttribute("target") === "_blank") return;
|
| 90 |
+
|
| 91 |
+
link.addEventListener("click", function (event) {
|
| 92 |
+
event.preventDefault();
|
| 93 |
+
const overlay = document.createElement("div");
|
| 94 |
+
overlay.classList.add("transition-overlay");
|
| 95 |
+
document.body.appendChild(overlay);
|
| 96 |
+
|
| 97 |
+
setTimeout(() => overlay.classList.add("active"), 90);
|
| 98 |
+
setTimeout(() => overlay.remove(), 1000);
|
| 99 |
+
setTimeout(() => window.location.href = this.href, 890);
|
| 100 |
+
});
|
| 101 |
+
});
|
| 102 |
+
// Set min date for prediction input
|
| 103 |
+
const today = new Date();
|
| 104 |
+
const minDate = new Date(today);
|
| 105 |
+
minDate.setDate(today.getDate() + 6);
|
| 106 |
+
const maxDate = new Date(today);
|
| 107 |
+
maxDate.setMonth(today.getMonth() + 1);
|
| 108 |
+
|
| 109 |
+
function formatDate(d) {
|
| 110 |
+
const yyyy = d.getFullYear();
|
| 111 |
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
| 112 |
+
const dd = String(d.getDate()).padStart(2, '0');
|
| 113 |
+
return `${yyyy}-${mm}-${dd}`;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
const predictionDateInput = document.getElementById('prediction-date');
|
| 117 |
+
predictionDateInput.min = formatDate(minDate);
|
| 118 |
+
predictionDateInput.max = formatDate(maxDate);
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 122 |
+
fetch("/get-companies")
|
| 123 |
+
.then(response => response.json())
|
| 124 |
+
.then(data => {
|
| 125 |
+
const select = document.getElementById("stock-select");
|
| 126 |
+
const seen = new Set(); // Track unique tickers
|
| 127 |
+
data.forEach(company => {
|
| 128 |
+
const ticker = company["Yahoo Finance Ticker"];
|
| 129 |
+
const name = company["Company Name"];
|
| 130 |
+
// Exclude NIFTY and SENSEX tickers and duplicates
|
| 131 |
+
if (ticker !== "^NSEI" && ticker !== "^BSESN" && !seen.has(ticker)) {
|
| 132 |
+
const option = document.createElement("option");
|
| 133 |
+
option.value = ticker;
|
| 134 |
+
option.textContent = name;
|
| 135 |
+
select.appendChild(option);
|
| 136 |
+
seen.add(ticker);
|
| 137 |
+
}
|
| 138 |
+
});
|
| 139 |
+
})
|
| 140 |
+
.catch(error => console.error("Error fetching companies:", error));
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
let predictionChart = null;
|
| 144 |
+
function handleEpochsChange(select) {
|
| 145 |
+
const customInput = document.getElementById('custom-epochs');
|
| 146 |
+
if (select.value === 'custom') {
|
| 147 |
+
customInput.style.display = 'block';
|
| 148 |
+
customInput.focus();
|
| 149 |
+
} else {
|
| 150 |
+
customInput.style.display = 'none';
|
| 151 |
+
}
|
| 152 |
+
}window.handleEpochsChange = handleEpochsChange;
|
| 153 |
+
|
| 154 |
+
// Listen for the "Predict" button click and route prediction request based on radio button
|
| 155 |
+
document.getElementById('predict-btn').addEventListener('click', async function (e) {
|
| 156 |
+
e.preventDefault();
|
| 157 |
+
const stockSymbol = document.getElementById('stock-select').value;
|
| 158 |
+
const predictionType = document.querySelector('input[name="data-source"]:checked').value;
|
| 159 |
+
const predictionDate = document.getElementById('prediction-date').value;
|
| 160 |
+
|
| 161 |
+
if (!stockSymbol) {
|
| 162 |
+
showMessage("Please select a stock", true);
|
| 163 |
+
return;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if (!predictionDate) {
|
| 167 |
+
showMessage("Please select a prediction date", true);
|
| 168 |
+
return;
|
| 169 |
+
}
|
| 170 |
+
// Date validation
|
| 171 |
+
const selected = new Date(predictionDate);
|
| 172 |
+
const today = new Date();
|
| 173 |
+
const min = new Date(today);
|
| 174 |
+
min.setDate(today.getDate() + 6); // 6 days minimum
|
| 175 |
+
const max = new Date(today);
|
| 176 |
+
max.setMonth(today.getMonth() + 1);
|
| 177 |
+
|
| 178 |
+
selected.setHours(0,0,0,0);
|
| 179 |
+
min.setHours(0,0,0,0);
|
| 180 |
+
max.setHours(0,0,0,0);
|
| 181 |
+
|
| 182 |
+
if (selected < min || selected > max) {
|
| 183 |
+
showMessage("Prediction date must be at least 6 days from today and at most 1 month from today.", true);
|
| 184 |
+
return;
|
| 185 |
+
}
|
| 186 |
+
let epochs;
|
| 187 |
+
const epochsSelect = document.getElementById('epochs');
|
| 188 |
+
if (epochsSelect.value === 'custom') {
|
| 189 |
+
epochs = document.getElementById('custom-epochs').value || 100;
|
| 190 |
+
} else {
|
| 191 |
+
epochs = epochsSelect.value;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
// Show loading overlay and start progress
|
| 195 |
+
document.getElementById('loading-overlay').style.display = "flex";
|
| 196 |
+
updateProgress(0);
|
| 197 |
+
|
| 198 |
+
this.disabled = true;
|
| 199 |
+
this.textContent = "Predicting...";
|
| 200 |
+
|
| 201 |
+
let payload = {
|
| 202 |
+
ticker: stockSymbol,
|
| 203 |
+
type: predictionType,
|
| 204 |
+
prediction_date: predictionDate,
|
| 205 |
+
epochs: epochs
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
// Set progress duration based on prediction type
|
| 209 |
+
let progress = 0;
|
| 210 |
+
let intervalMs;
|
| 211 |
+
let maxProgress = 95;
|
| 212 |
+
if (predictionType === "both") {
|
| 213 |
+
// Double the time for both pipelines (~130 seconds)
|
| 214 |
+
intervalMs = 51000; // 100% / 130s = ~1% per 1.3s
|
| 215 |
+
} else if (predictionType === "news-sentiment") {
|
| 216 |
+
// About 79 seconds for sentiment pipeline
|
| 217 |
+
intervalMs = 27000; // 100% / 79s = ~1% per 0.79s
|
| 218 |
+
} else {
|
| 219 |
+
intervalMs = 18000; // ~65 seconds for single pipeline
|
| 220 |
+
}
|
| 221 |
+
let progressInterval = setInterval(() => {
|
| 222 |
+
if (progress < maxProgress) {
|
| 223 |
+
progress += 1;
|
| 224 |
+
updateProgress(progress);
|
| 225 |
+
}
|
| 226 |
+
}, intervalMs);
|
| 227 |
+
|
| 228 |
+
try {
|
| 229 |
+
const response = await fetch('/predict-result', {
|
| 230 |
+
method: 'POST',
|
| 231 |
+
headers: { 'Content-Type': 'application/json' },
|
| 232 |
+
body: JSON.stringify(payload)
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
clearInterval(progressInterval);
|
| 236 |
+
updateProgress(100);
|
| 237 |
+
|
| 238 |
+
const result = await response.json();
|
| 239 |
+
|
| 240 |
+
setTimeout(() => {
|
| 241 |
+
document.getElementById('loading-overlay').style.display = "none";
|
| 242 |
+
}, 400);
|
| 243 |
+
|
| 244 |
+
if (response.ok) {
|
| 245 |
+
displayPredictionResults(result);
|
| 246 |
+
} else {
|
| 247 |
+
showMessage(result.error || "Prediction failed", true);
|
| 248 |
+
}
|
| 249 |
+
} catch (error) {
|
| 250 |
+
clearInterval(progressInterval);
|
| 251 |
+
document.getElementById('loading-overlay').style.display = "none";
|
| 252 |
+
showMessage("An error occurred", true);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
this.disabled = false;
|
| 256 |
+
this.textContent = "Predict Stock Price";
|
| 257 |
+
});
|
| 258 |
+
|
| 259 |
+
function updateProgress(percent) {
|
| 260 |
+
document.getElementById('progress-text').textContent = `Loading... ${percent}%`;
|
| 261 |
+
}
|
| 262 |
+
function displayPredictionResults(prediction) {
|
| 263 |
+
// If both, render two graphs in separate areas with space
|
| 264 |
+
if (prediction.historical && prediction.sentiment) {
|
| 265 |
+
document.getElementById('result-section').style.display = "block";
|
| 266 |
+
document.getElementById('result-stock-name').textContent = prediction.historical.stock;
|
| 267 |
+
document.getElementById('predicted-price').textContent = prediction.historical.predictedPrice;
|
| 268 |
+
|
| 269 |
+
// Clear and create two separate chart areas with spacing
|
| 270 |
+
const chartContainer = document.querySelector('.chart-container');
|
| 271 |
+
chartContainer.innerHTML = `
|
| 272 |
+
<div class="graph-area" id="graph-area-historical">
|
| 273 |
+
<h4 style="margin-bottom:12px;">Historical Only</h4>
|
| 274 |
+
<div id="prediction-chart-historical" style="height: 450px;"></div>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="graph-area" id="graph-area-sentiment">
|
| 277 |
+
<h4 style="margin-bottom:12px;">With Sentiment</h4>
|
| 278 |
+
<div id="prediction-chart-sentiment" style="height: 450px;"></div>
|
| 279 |
+
</div>
|
| 280 |
+
`;
|
| 281 |
+
|
| 282 |
+
renderPredictionChart(prediction.historical, 'prediction-chart-historical', 'Historical Only');
|
| 283 |
+
renderPredictionChart(prediction.sentiment, 'prediction-chart-sentiment', 'With Sentiment');
|
| 284 |
+
} else {
|
| 285 |
+
// Single pipeline as before
|
| 286 |
+
document.getElementById('result-stock-name').textContent = prediction.stock;
|
| 287 |
+
document.getElementById('predicted-price').textContent = prediction.predictedPrice;
|
| 288 |
+
document.getElementById('result-section').style.display = "block";
|
| 289 |
+
renderPredictionChart(prediction, 'prediction-chart');
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
function renderPredictionChart(prediction, chartId = 'prediction-chart', customTitle = null) {
|
| 294 |
+
const data = [];
|
| 295 |
+
|
| 296 |
+
// Train data (from historicalData)
|
| 297 |
+
const trainLabels = prediction.historicalData.map(d => d.date);
|
| 298 |
+
const trainValues = prediction.historicalData.map(d => d.price);
|
| 299 |
+
data.push({
|
| 300 |
+
x: trainLabels,
|
| 301 |
+
y: trainValues,
|
| 302 |
+
mode: 'lines',
|
| 303 |
+
name: 'Train Data',
|
| 304 |
+
fill: 'tozeroy',
|
| 305 |
+
line: { color: 'rgba(74,143,223,1)' }
|
| 306 |
+
});
|
| 307 |
+
|
| 308 |
+
// Actual price (validation/test) from actualVsPredicted
|
| 309 |
+
const validLabels = prediction.actualVsPredicted.map(d => d.date);
|
| 310 |
+
const actualValues = prediction.actualVsPredicted.map(d => d.actual);
|
| 311 |
+
data.push({
|
| 312 |
+
x: validLabels,
|
| 313 |
+
y: actualValues,
|
| 314 |
+
mode: 'lines',
|
| 315 |
+
name: 'Actual Price',
|
| 316 |
+
fill: 'tozeroy',
|
| 317 |
+
line: { color: 'green', width: 2 }
|
| 318 |
+
});
|
| 319 |
+
|
| 320 |
+
// Predicted price (validation/test) from actualVsPredicted
|
| 321 |
+
const predictedValues = prediction.actualVsPredicted.map(d => d.predicted);
|
| 322 |
+
data.push({
|
| 323 |
+
x: validLabels,
|
| 324 |
+
y: predictedValues,
|
| 325 |
+
mode: 'lines',
|
| 326 |
+
name: 'Predicted Price',
|
| 327 |
+
fill: 'tozeroy',
|
| 328 |
+
line: { color: 'red', width: 3, dash: 'dot' }
|
| 329 |
+
});
|
| 330 |
+
|
| 331 |
+
// Future predictions
|
| 332 |
+
if (prediction.futureDates && prediction.futurePrices) {
|
| 333 |
+
data.push({
|
| 334 |
+
x: prediction.futureDates,
|
| 335 |
+
y: prediction.futurePrices,
|
| 336 |
+
mode: 'lines+markers',
|
| 337 |
+
name: 'Future Predictions',
|
| 338 |
+
fill: 'tozeroy',
|
| 339 |
+
line: { color: 'orange', width: 2, dash: 'dash' },
|
| 340 |
+
marker: { size: 8, color: 'orange' }
|
| 341 |
+
});
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Buy signals
|
| 345 |
+
if (prediction.buySignals && prediction.buySignals.points) {
|
| 346 |
+
const buyPoints = prediction.buySignals.points;
|
| 347 |
+
const buyReasons = prediction.buySignals.reasons || [];
|
| 348 |
+
const buyIndices = prediction.buyIndices || Array.from({length: buyPoints.length}, (_, i) => i);
|
| 349 |
+
const buyDates = buyIndices.map(idx => prediction.futureDates[idx] || '');
|
| 350 |
+
|
| 351 |
+
data.push({
|
| 352 |
+
x: buyDates,
|
| 353 |
+
y: buyPoints,
|
| 354 |
+
mode: 'markers',
|
| 355 |
+
name: 'Buy Signals',
|
| 356 |
+
marker: {
|
| 357 |
+
color: 'cyan',
|
| 358 |
+
size: 12,
|
| 359 |
+
symbol: 'star',
|
| 360 |
+
line: { width: 2, color: 'black' }
|
| 361 |
+
},
|
| 362 |
+
text: buyReasons,
|
| 363 |
+
hovertemplate: '<b>Buy Signal</b><br>Date: %{x}<br>Price: ₹%{y:,.2f}<br>Reason: %{text}<extra></extra>'
|
| 364 |
+
});
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// Layout (unchanged)
|
| 368 |
+
const getTextColor = () => '#A0AEC0';
|
| 369 |
+
const getGridColor = () => 'rgba(105,123,147,0.4)';
|
| 370 |
+
const getBgColor = () => 'rgba(19,27,43,0.0)';
|
| 371 |
+
const layout = {
|
| 372 |
+
title: customTitle || 'Stock Price Prediction with Future Forecast & Buy Signals',
|
| 373 |
+
xaxis: {
|
| 374 |
+
title: 'Date',
|
| 375 |
+
color: getTextColor(),
|
| 376 |
+
gridcolor: getGridColor(),
|
| 377 |
+
spikecolor: "rgba(105,123,147,255)",
|
| 378 |
+
spikedash: "solid",
|
| 379 |
+
spikethickness: -2
|
| 380 |
+
},
|
| 381 |
+
yaxis: {
|
| 382 |
+
title: 'Stock Price',
|
| 383 |
+
color: getTextColor(),
|
| 384 |
+
gridcolor: getGridColor(),
|
| 385 |
+
tickprefix: '₹',
|
| 386 |
+
tickformat: ',',
|
| 387 |
+
spikecolor: "rgba(105,123,149,255)",
|
| 388 |
+
spikedash: "solid",
|
| 389 |
+
spikethickness: -2,
|
| 390 |
+
zeroline: false
|
| 391 |
+
},
|
| 392 |
+
paper_bgcolor: getBgColor(),
|
| 393 |
+
plot_bgcolor: getBgColor(),
|
| 394 |
+
font: { color: getTextColor() },
|
| 395 |
+
margin: { t: 50, b: 50, l: 50, r: 20 },
|
| 396 |
+
legend: { x: 0, y: 1 },
|
| 397 |
+
hovermode: "closest"
|
| 398 |
+
};
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
Plotly.newPlot(chartId, data, layout, { responsive: true });
|
| 402 |
+
}
|
| 403 |
+
window.renderPredictionChart = renderPredictionChart;
|
| 404 |
+
function updatePredictionChartColors() {
|
| 405 |
+
const layoutUpdate = {
|
| 406 |
+
paper_bgcolor: getBgColor(),
|
| 407 |
+
plot_bgcolor: getBgColor(),
|
| 408 |
+
font: { color: getTextColor() },
|
| 409 |
+
xaxis: { color: getTextColor(), gridcolor: getGridColor() },
|
| 410 |
+
yaxis: { color: getTextColor(), gridcolor: getGridColor() }
|
| 411 |
+
};
|
| 412 |
+
Plotly.relayout('prediction-chart', layoutUpdate);
|
| 413 |
+
}
|
| 414 |
+
function getTextColor() {
|
| 415 |
+
return document.body.classList.contains("dark-mode") ? '#fff' : '#333';
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
function getGridColor() {
|
| 419 |
+
return document.body.classList.contains("dark-mode") ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)';
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
function getBgColor() {
|
| 423 |
+
return document.body.classList.contains("dark-mode") ? '#1e2a38' : '#fff';
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function showMessage(message, isError = false) {
|
| 427 |
+
let box = document.getElementById("message-box");
|
| 428 |
+
if (!box) {
|
| 429 |
+
box = document.createElement("div");
|
| 430 |
+
box.id = "message-box";
|
| 431 |
+
// Place above the Predict button
|
| 432 |
+
const btn = document.getElementById('predict-btn');
|
| 433 |
+
btn.parentNode.insertBefore(box, btn);
|
| 434 |
+
}
|
| 435 |
+
box.style.position = "static";
|
| 436 |
+
box.style.marginBottom = "16px";
|
| 437 |
+
box.style.padding = "10px 18px";
|
| 438 |
+
box.style.borderRadius = "6px";
|
| 439 |
+
box.style.zIndex = "1";
|
| 440 |
+
box.style.fontSize = "15px";
|
| 441 |
+
box.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
|
| 442 |
+
box.style.backgroundColor = isError ? "#ffe6e6" : "#e0f7fa";
|
| 443 |
+
box.style.color = isError ? "#c62828" : "#00796b";
|
| 444 |
+
box.style.border = `1px solid ${isError ? "#c62828" : "#00796b"}`;
|
| 445 |
+
box.style.display = "inline-block";
|
| 446 |
+
box.style.maxWidth = "320px";
|
| 447 |
+
box.style.textAlign = "center";
|
| 448 |
+
box.style.whiteSpace = "normal";
|
| 449 |
+
box.style.wordBreak = "break-word";
|
| 450 |
+
box.style.marginLeft = "auto";
|
| 451 |
+
box.style.marginRight = "auto";
|
| 452 |
+
box.textContent = message;
|
| 453 |
+
|
| 454 |
+
setTimeout(() => { box.style.display = "none"; }, 4000);
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
// Disclaimer close logic
|
| 459 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 460 |
+
const closeBtn = document.getElementById("close-disclaimer");
|
| 461 |
+
if (closeBtn) {
|
| 462 |
+
closeBtn.onclick = function() {
|
| 463 |
+
document.getElementById("disclaimer-overlay").style.display = "none";
|
| 464 |
+
};
|
| 465 |
+
}
|
| 466 |
+
});
|
| 467 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 468 |
+
// Hide disclaimer by default
|
| 469 |
+
document.getElementById("disclaimer-overlay").style.display = "none";
|
| 470 |
+
|
| 471 |
+
function showDisclaimer() {
|
| 472 |
+
document.getElementById("disclaimer-overlay").style.display = "flex";
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// Only show tutorial if not seen before
|
| 476 |
+
if (!localStorage.getItem("predictorTutorialSeen")) {
|
| 477 |
+
setTimeout(() => {
|
| 478 |
+
introJs().setOptions({
|
| 479 |
+
nextLabel: 'Next',
|
| 480 |
+
prevLabel: 'Back',
|
| 481 |
+
skipLabel: 'Skip',
|
| 482 |
+
doneLabel: 'Done',
|
| 483 |
+
overlayOpacity: 0.7,
|
| 484 |
+
showProgress: true,
|
| 485 |
+
tooltipClass: 'custom-introjs-tooltip'
|
| 486 |
+
}).oncomplete(function() {
|
| 487 |
+
localStorage.setItem("predictorTutorialSeen", "yes");
|
| 488 |
+
showDisclaimer();
|
| 489 |
+
}).onexit(function() {
|
| 490 |
+
localStorage.setItem("predictorTutorialSeen", "yes");
|
| 491 |
+
showDisclaimer();
|
| 492 |
+
}).start();
|
| 493 |
+
}, 600); // Wait for page to load
|
| 494 |
+
} else {
|
| 495 |
+
// If tutorial already seen, show disclaimer immediately
|
| 496 |
+
showDisclaimer();
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// Disclaimer close logic
|
| 500 |
+
const closeBtn = document.getElementById("close-disclaimer");
|
| 501 |
+
if (closeBtn) {
|
| 502 |
+
closeBtn.onclick = function() {
|
| 503 |
+
document.getElementById("disclaimer-overlay").style.display = "none";
|
| 504 |
+
};
|
| 505 |
+
}
|
| 506 |
+
});
|
| 507 |
+
|
| 508 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 509 |
+
const sentimentContainer = document.getElementById('news-sentiment');
|
| 510 |
+
const sentimentText = document.getElementById('sentiment-text');
|
| 511 |
+
const radioButtons = document.querySelectorAll('input[name="data-source"]');
|
| 512 |
+
|
| 513 |
+
// Function to map sentiment values to text
|
| 514 |
+
function mapSentimentValueToText(value) {
|
| 515 |
+
switch (value) {
|
| 516 |
+
case 0:
|
| 517 |
+
return "Negative";
|
| 518 |
+
case 1:
|
| 519 |
+
return "Neutral";
|
| 520 |
+
case 2:
|
| 521 |
+
return "Positive";
|
| 522 |
+
default:
|
| 523 |
+
return "Unknown";
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
// Function to fetch and display news sentiment
|
| 528 |
+
async function fetchNewsSentiment(stockSymbol) {
|
| 529 |
+
try {
|
| 530 |
+
const response = await fetch(`/api/news-sentiment?ticker=${stockSymbol}`);
|
| 531 |
+
const data = await response.json();
|
| 532 |
+
if (response.ok && data.sentiment !== undefined) {
|
| 533 |
+
const sentimentTextValue = mapSentimentValueToText(data.sentiment);
|
| 534 |
+
sentimentText.textContent = `Sentiment: ${sentimentTextValue}`;
|
| 535 |
+
} else {
|
| 536 |
+
sentimentText.textContent = 'No sentiment data available.';
|
| 537 |
+
}
|
| 538 |
+
} catch (error) {
|
| 539 |
+
sentimentText.textContent = 'Error fetching sentiment data.';
|
| 540 |
+
console.error('Error fetching sentiment:', error);
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
// Event listener for radio button changes
|
| 545 |
+
radioButtons.forEach(radio => {
|
| 546 |
+
radio.addEventListener('change', function () {
|
| 547 |
+
const stockSymbol = document.getElementById('stock-select').value;
|
| 548 |
+
if ((this.value === 'news-sentiment' || this.value === 'both') && stockSymbol) {
|
| 549 |
+
sentimentContainer.style.display = 'block';
|
| 550 |
+
fetchNewsSentiment(stockSymbol);
|
| 551 |
+
} else {
|
| 552 |
+
sentimentContainer.style.display = 'none';
|
| 553 |
+
}
|
| 554 |
+
});
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Trigger sentiment fetch after prediction
|
| 558 |
+
document.getElementById('predict-btn').addEventListener('click', function () {
|
| 559 |
+
const selectedRadio = document.querySelector('input[name="data-source"]:checked');
|
| 560 |
+
const stockSymbol = document.getElementById('stock-select').value;
|
| 561 |
+
if (selectedRadio && (selectedRadio.value === 'news-sentiment' || selectedRadio.value === 'both')) {
|
| 562 |
+
sentimentContainer.style.display = 'block';
|
| 563 |
+
fetchNewsSentiment(stockSymbol);
|
| 564 |
+
} else {
|
| 565 |
+
sentimentContainer.style.display = 'none';
|
| 566 |
+
}
|
| 567 |
+
});
|
| 568 |
+
});
|
| 569 |
+
|
| 570 |
+
import { initializeNewsletterForm } from './newsletter.js';
|
| 571 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 572 |
+
initializeNewsletterForm();
|
| 573 |
+
// Other page-specific code...
|
| 574 |
+
});
|
static/models/coins.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ec40d873933a5e1c7b644f37f0a1fba0003727a3f2fcb8c1c3ef48d6b2290c68
|
| 3 |
+
size 13616660
|
static/models/finbert_sentiment089/config.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"BertForSequenceClassification"
|
| 4 |
+
],
|
| 5 |
+
"attention_probs_dropout_prob": 0.1,
|
| 6 |
+
"classifier_dropout": null,
|
| 7 |
+
"hidden_act": "gelu",
|
| 8 |
+
"hidden_dropout_prob": 0.1,
|
| 9 |
+
"hidden_size": 768,
|
| 10 |
+
"id2label": {
|
| 11 |
+
"0": "negative",
|
| 12 |
+
"1": "neutral",
|
| 13 |
+
"2": "positive"
|
| 14 |
+
},
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"intermediate_size": 3072,
|
| 17 |
+
"label2id": {
|
| 18 |
+
"negative": 0,
|
| 19 |
+
"neutral": 1,
|
| 20 |
+
"positive": 2
|
| 21 |
+
},
|
| 22 |
+
"layer_norm_eps": 1e-12,
|
| 23 |
+
"max_position_embeddings": 512,
|
| 24 |
+
"model_type": "bert",
|
| 25 |
+
"num_attention_heads": 12,
|
| 26 |
+
"num_hidden_layers": 12,
|
| 27 |
+
"pad_token_id": 0,
|
| 28 |
+
"position_embedding_type": "absolute",
|
| 29 |
+
"problem_type": "single_label_classification",
|
| 30 |
+
"torch_dtype": "float32",
|
| 31 |
+
"transformers_version": "4.51.3",
|
| 32 |
+
"type_vocab_size": 2,
|
| 33 |
+
"use_cache": true,
|
| 34 |
+
"vocab_size": 30873
|
| 35 |
+
}
|
static/models/finbert_sentiment089/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cc3cc6e3e3b049d7d0a532b03afd27206f09d30f7175dc9ef61d0b742859abad
|
| 3 |
+
size 439039996
|
static/models/finbert_sentiment089/special_tokens_map.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cls_token": {
|
| 3 |
+
"content": "[CLS]",
|
| 4 |
+
"lstrip": false,
|
| 5 |
+
"normalized": false,
|
| 6 |
+
"rstrip": false,
|
| 7 |
+
"single_word": false
|
| 8 |
+
},
|
| 9 |
+
"mask_token": {
|
| 10 |
+
"content": "[MASK]",
|
| 11 |
+
"lstrip": false,
|
| 12 |
+
"normalized": false,
|
| 13 |
+
"rstrip": false,
|
| 14 |
+
"single_word": false
|
| 15 |
+
},
|
| 16 |
+
"pad_token": {
|
| 17 |
+
"content": "[PAD]",
|
| 18 |
+
"lstrip": false,
|
| 19 |
+
"normalized": false,
|
| 20 |
+
"rstrip": false,
|
| 21 |
+
"single_word": false
|
| 22 |
+
},
|
| 23 |
+
"sep_token": {
|
| 24 |
+
"content": "[SEP]",
|
| 25 |
+
"lstrip": false,
|
| 26 |
+
"normalized": false,
|
| 27 |
+
"rstrip": false,
|
| 28 |
+
"single_word": false
|
| 29 |
+
},
|
| 30 |
+
"unk_token": {
|
| 31 |
+
"content": "[UNK]",
|
| 32 |
+
"lstrip": false,
|
| 33 |
+
"normalized": false,
|
| 34 |
+
"rstrip": false,
|
| 35 |
+
"single_word": false
|
| 36 |
+
}
|
| 37 |
+
}
|
static/models/finbert_sentiment089/tokenizer_config.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"added_tokens_decoder": {
|
| 3 |
+
"0": {
|
| 4 |
+
"content": "[PAD]",
|
| 5 |
+
"lstrip": false,
|
| 6 |
+
"normalized": false,
|
| 7 |
+
"rstrip": false,
|
| 8 |
+
"single_word": false,
|
| 9 |
+
"special": true
|
| 10 |
+
},
|
| 11 |
+
"2": {
|
| 12 |
+
"content": "[UNK]",
|
| 13 |
+
"lstrip": false,
|
| 14 |
+
"normalized": false,
|
| 15 |
+
"rstrip": false,
|
| 16 |
+
"single_word": false,
|
| 17 |
+
"special": true
|
| 18 |
+
},
|
| 19 |
+
"3": {
|
| 20 |
+
"content": "[CLS]",
|
| 21 |
+
"lstrip": false,
|
| 22 |
+
"normalized": false,
|
| 23 |
+
"rstrip": false,
|
| 24 |
+
"single_word": false,
|
| 25 |
+
"special": true
|
| 26 |
+
},
|
| 27 |
+
"4": {
|
| 28 |
+
"content": "[SEP]",
|
| 29 |
+
"lstrip": false,
|
| 30 |
+
"normalized": false,
|
| 31 |
+
"rstrip": false,
|
| 32 |
+
"single_word": false,
|
| 33 |
+
"special": true
|
| 34 |
+
},
|
| 35 |
+
"5": {
|
| 36 |
+
"content": "[MASK]",
|
| 37 |
+
"lstrip": false,
|
| 38 |
+
"normalized": false,
|
| 39 |
+
"rstrip": false,
|
| 40 |
+
"single_word": false,
|
| 41 |
+
"special": true
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"clean_up_tokenization_spaces": true,
|
| 45 |
+
"cls_token": "[CLS]",
|
| 46 |
+
"do_basic_tokenize": true,
|
| 47 |
+
"do_lower_case": false,
|
| 48 |
+
"extra_special_tokens": {},
|
| 49 |
+
"mask_token": "[MASK]",
|
| 50 |
+
"max_len": 512,
|
| 51 |
+
"model_max_length": 512,
|
| 52 |
+
"never_split": null,
|
| 53 |
+
"pad_token": "[PAD]",
|
| 54 |
+
"sep_token": "[SEP]",
|
| 55 |
+
"strip_accents": null,
|
| 56 |
+
"tokenize_chinese_chars": true,
|
| 57 |
+
"tokenizer_class": "BertTokenizer",
|
| 58 |
+
"unk_token": "[UNK]"
|
| 59 |
+
}
|
static/models/finbert_sentiment089/vocab.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/pipelines/__pycache__/lstm_n_pipeline.cpython-312.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
static/pipelines/__pycache__/lstm_pipeline.cpython-312.pyc
ADDED
|
Binary file (12 kB). View file
|
|
|
static/pipelines/lstm_n_pipeline.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 6 |
+
from tensorflow.keras.models import Sequential
|
| 7 |
+
from tensorflow.keras.layers import LSTM, Dense, BatchNormalization
|
| 8 |
+
from flask import jsonify
|
| 9 |
+
from tensorflow.keras import regularizers
|
| 10 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
| 11 |
+
from sklearn.model_selection import train_test_split
|
| 12 |
+
import pymongo
|
| 13 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 14 |
+
import torch
|
| 15 |
+
import os
|
| 16 |
+
from dotenv import load_dotenv
|
| 17 |
+
|
| 18 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
model_path = os.path.abspath(os.path.join(BASE_DIR, "..", "models", "finbert_sentiment089"))
|
| 20 |
+
|
| 21 |
+
def get_sentiment_model(model_path=model_path):
|
| 22 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
| 23 |
+
sentiment_model = AutoModelForSequenceClassification.from_pretrained(model_path)
|
| 24 |
+
return tokenizer, sentiment_model
|
| 25 |
+
|
| 26 |
+
def get_sentiment(text, tokenizer, sentiment_model):
|
| 27 |
+
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128)
|
| 28 |
+
with torch.no_grad():
|
| 29 |
+
outputs = sentiment_model(**inputs)
|
| 30 |
+
scores = torch.softmax(outputs.logits, dim=1).cpu().numpy()[0]
|
| 31 |
+
sentiment = int(scores.argmax())
|
| 32 |
+
return sentiment # 0=negative, 1=neutral, 2=positive
|
| 33 |
+
|
| 34 |
+
def fetch_and_merge_sentiment(df, ticker, start_date, end_date, tokenizer, sentiment_model):
|
| 35 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 36 |
+
df.columns = ['_'.join([str(i) for i in col if i]) for col in df.columns.values]
|
| 37 |
+
load_dotenv()
|
| 38 |
+
mongo_uri = os.getenv("MONGO_URI")
|
| 39 |
+
client = pymongo.MongoClient(mongo_uri)
|
| 40 |
+
db = client["stock_news"]
|
| 41 |
+
collection = db["moneyworks_company_news"]
|
| 42 |
+
tickers_to_fetch = [ticker, "^NSEI", "^BSESN"]
|
| 43 |
+
news_sentiment_daily_dict = {}
|
| 44 |
+
for tkr in tickers_to_fetch:
|
| 45 |
+
news_cursor = collection.find({"yahoo_ticker": tkr})
|
| 46 |
+
for doc in news_cursor:
|
| 47 |
+
if "sentiment" not in doc or doc["sentiment"] in ("", None):
|
| 48 |
+
title = doc.get("title", "")
|
| 49 |
+
if title:
|
| 50 |
+
sentiment = get_sentiment(title, tokenizer, sentiment_model)
|
| 51 |
+
collection.update_one({"_id": doc["_id"]}, {"$set": {"sentiment": sentiment}})
|
| 52 |
+
news_cursor = collection.find({"yahoo_ticker": tkr, "sentiment": {"$exists": True}})
|
| 53 |
+
news_df = pd.DataFrame(list(news_cursor))
|
| 54 |
+
if not news_df.empty:
|
| 55 |
+
news_df["date"] = pd.to_datetime(news_df["date"]).dt.date
|
| 56 |
+
news_sentiment_daily = news_df.groupby("date")["sentiment"].mean()
|
| 57 |
+
else:
|
| 58 |
+
news_sentiment_daily = pd.Series(dtype=float)
|
| 59 |
+
news_sentiment_daily_dict[tkr] = news_sentiment_daily
|
| 60 |
+
df["date"] = df.index.date
|
| 61 |
+
df = df.merge(news_sentiment_daily_dict.get(ticker, pd.Series(dtype=float)).rename("news_sentiment_company"), left_on="date", right_index=True, how="left")
|
| 62 |
+
df = df.merge(news_sentiment_daily_dict.get("^NSEI", pd.Series(dtype=float)).rename("news_sentiment_nifty"), left_on="date", right_index=True, how="left")
|
| 63 |
+
df = df.merge(news_sentiment_daily_dict.get("^BSESN", pd.Series(dtype=float)).rename("news_sentiment_sensex"), left_on="date", right_index=True, how="left")
|
| 64 |
+
df["news_sentiment_company"] = df["news_sentiment_company"].fillna(0)
|
| 65 |
+
df["news_sentiment_nifty"] = df["news_sentiment_nifty"].fillna(0)
|
| 66 |
+
df["news_sentiment_sensex"] = df["news_sentiment_sensex"].fillna(0)
|
| 67 |
+
# After merging sentiment columns, check if news_df is empty for each ticker
|
| 68 |
+
for tkr in tickers_to_fetch:
|
| 69 |
+
news_cursor = collection.find({"yahoo_ticker": tkr, "sentiment": {"$exists": True}})
|
| 70 |
+
news_df = pd.DataFrame(list(news_cursor))
|
| 71 |
+
if news_df.empty:
|
| 72 |
+
print(f"Warning: No sentiment data found for {tkr}. All sentiment values will be zero.")
|
| 73 |
+
else:
|
| 74 |
+
print(f"Sentiment data found for {tkr}: {news_df.shape[0]} records.")
|
| 75 |
+
|
| 76 |
+
# Rename columns and drop 'date' column
|
| 77 |
+
df = df.rename(columns={
|
| 78 |
+
f'Close_{ticker}': 'Close',
|
| 79 |
+
f'Open_{ticker}': 'Open',
|
| 80 |
+
f'High_{ticker}': 'High',
|
| 81 |
+
f'Low_{ticker}': 'Low',
|
| 82 |
+
f'Volume_{ticker}': 'Volume',
|
| 83 |
+
'NIFTY_Close_^NSEI': 'NIFTY_Close',
|
| 84 |
+
'SENSEX_Close_^BSESN': 'SENSEX_Close'
|
| 85 |
+
})
|
| 86 |
+
if 'date' in df.columns:
|
| 87 |
+
df = df.drop(columns=['date'])
|
| 88 |
+
return df
|
| 89 |
+
|
| 90 |
+
def create_sequences(data, seq_length=20):
|
| 91 |
+
sequences = []
|
| 92 |
+
for i in range(len(data) - seq_length):
|
| 93 |
+
sequences.append(data[i: i + seq_length])
|
| 94 |
+
return np.array(sequences)
|
| 95 |
+
|
| 96 |
+
def split_data(sequence):
|
| 97 |
+
train_data, test_data = train_test_split(sequence, test_size=0.3, shuffle=False)
|
| 98 |
+
val_data, test_data = train_test_split(test_data, test_size=0.5, shuffle=False)
|
| 99 |
+
return train_data, val_data, test_data
|
| 100 |
+
|
| 101 |
+
def predict_future(model, last_sequence, scaler, n_steps=1, n_features=10):
|
| 102 |
+
last_sequence_reshaped = last_sequence.reshape((1, last_sequence.shape[0], last_sequence.shape[1]))
|
| 103 |
+
future_predictions = []
|
| 104 |
+
current_input = last_sequence_reshaped
|
| 105 |
+
for _ in range(n_steps):
|
| 106 |
+
predicted_price = model.predict(current_input)[0, 0]
|
| 107 |
+
future_predictions.append(predicted_price)
|
| 108 |
+
current_input = np.roll(current_input, -1, axis=1)
|
| 109 |
+
current_input[0, -1, 0] = predicted_price
|
| 110 |
+
if current_input.shape[2] > 1:
|
| 111 |
+
current_input[0, -1, 1:] = current_input[0, -2, 1:]
|
| 112 |
+
future_predictions = scaler.inverse_transform(
|
| 113 |
+
np.hstack((np.array(future_predictions).reshape(-1, 1), np.zeros((len(future_predictions), n_features - 1))))
|
| 114 |
+
)[:, 0]
|
| 115 |
+
return future_predictions
|
| 116 |
+
|
| 117 |
+
def find_optimal_buy_points(prices, window=3, threshold=0.01):
|
| 118 |
+
buy_indices = []
|
| 119 |
+
buy_signals = {"points": [], "reasons": []}
|
| 120 |
+
if len(prices) < 2 * window + 1:
|
| 121 |
+
return buy_indices, buy_signals
|
| 122 |
+
for i in range(window, len(prices) - window):
|
| 123 |
+
window_slice = prices[i - window:i + window + 1]
|
| 124 |
+
if prices[i] == min(window_slice):
|
| 125 |
+
before_diff = prices[i] - min(prices[i - window:i])
|
| 126 |
+
after_diff = prices[i] - min(prices[i + 1:i + window + 1])
|
| 127 |
+
if before_diff > threshold and after_diff > threshold:
|
| 128 |
+
buy_indices.append(i)
|
| 129 |
+
buy_signals["points"].append(prices[i])
|
| 130 |
+
buy_signals["reasons"].append("Local minimum with significant change before and after")
|
| 131 |
+
if not buy_indices and len(prices) > 0:
|
| 132 |
+
min_idx = np.argmin(prices)
|
| 133 |
+
buy_indices.append(min_idx)
|
| 134 |
+
buy_signals["points"].append(prices[min_idx])
|
| 135 |
+
buy_signals["reasons"].append("Lowest price point")
|
| 136 |
+
return buy_indices, buy_signals
|
| 137 |
+
|
| 138 |
+
def run_lstm_sentiment_prediction(data):
|
| 139 |
+
ticker = data['ticker']
|
| 140 |
+
prediction_date = data['prediction_date']
|
| 141 |
+
epochs = int(data.get('epochs', 100))
|
| 142 |
+
start_date = '2019-01-01'
|
| 143 |
+
end_date = datetime.today().strftime('%Y-%m-%d')
|
| 144 |
+
df = yf.download(ticker, start=start_date, end=end_date)
|
| 145 |
+
if df.empty:
|
| 146 |
+
return jsonify({'error': 'Failed to fetch stock data.'}), 400
|
| 147 |
+
nifty = yf.download("^NSEI", start=start_date, end=end_date)[['Close']].rename(columns={'Close': 'NIFTY_Close'})
|
| 148 |
+
sensex = yf.download("^BSESN", start=start_date, end=end_date)[['Close']].rename(columns={'Close': 'SENSEX_Close'})
|
| 149 |
+
df = df.merge(nifty, left_index=True, right_index=True, how='inner')
|
| 150 |
+
df = df.merge(sensex, left_index=True, right_index=True, how='inner')
|
| 151 |
+
# Sentiment
|
| 152 |
+
tokenizer, sentiment_model = get_sentiment_model()
|
| 153 |
+
df = fetch_and_merge_sentiment(df, ticker, start_date, end_date, tokenizer, sentiment_model)
|
| 154 |
+
df = df[['Close', 'Open', 'High', 'Low', 'Volume', 'NIFTY_Close', 'SENSEX_Close',
|
| 155 |
+
'news_sentiment_company', 'news_sentiment_nifty', 'news_sentiment_sensex']]
|
| 156 |
+
scaler = MinMaxScaler(feature_range=(-1, 1))
|
| 157 |
+
df_scaled = scaler.fit_transform(df)
|
| 158 |
+
df_array = df_scaled.astype(np.float32)
|
| 159 |
+
seq_length = 30
|
| 160 |
+
sequences = create_sequences(df_array, seq_length + 1)
|
| 161 |
+
train_data, val_data, test_data = split_data(sequences)
|
| 162 |
+
X_train, y_train = train_data[:, :-1, :], train_data[:, -1, 0]
|
| 163 |
+
X_valid, y_valid = val_data[:, :-1, :], val_data[:, -1, 0]
|
| 164 |
+
X_test, y_test = test_data[:, :-1, :], test_data[:, -1, 0]
|
| 165 |
+
model = Sequential()
|
| 166 |
+
model.add(LSTM(130, return_sequences=True, activation='relu', input_shape=(X_train.shape[1], X_train.shape[2])))
|
| 167 |
+
model.add(LSTM(69, return_sequences=False, activation='relu',kernel_regularizer=regularizers.l1(0.000001)))
|
| 168 |
+
model.add(Dense(63, activation='relu'))
|
| 169 |
+
model.add(BatchNormalization())
|
| 170 |
+
model.add(Dense(1))
|
| 171 |
+
model.compile(optimizer='adam', loss='mse', metrics=['mean_squared_error'])
|
| 172 |
+
model.fit(X_train, y_train, epochs=epochs, batch_size=32, verbose=1, shuffle=False, validation_data=(X_valid, y_valid))
|
| 173 |
+
predictions = model.predict(X_test)
|
| 174 |
+
predictions = predictions.reshape(-1, 1)
|
| 175 |
+
predictions = scaler.inverse_transform(np.hstack((predictions, np.zeros((len(predictions), df.shape[1] - 1)))))[:, 0]
|
| 176 |
+
y_test_unscaled = scaler.inverse_transform(np.hstack((y_test.reshape(-1, 1), np.zeros((len(y_test), df.shape[1] - 1)))))[:, 0]
|
| 177 |
+
predictions = predictions[-len(y_test_unscaled):]
|
| 178 |
+
rmse = np.sqrt(np.mean((predictions - y_test_unscaled) ** 2))
|
| 179 |
+
mae = mean_absolute_error(y_test_unscaled, predictions)
|
| 180 |
+
mse = mean_squared_error(y_test_unscaled, predictions)
|
| 181 |
+
r2 = r2_score(y_test_unscaled, predictions)
|
| 182 |
+
print(f"Test RMSE: {rmse}")
|
| 183 |
+
print(f"Test MAE: {mae}")
|
| 184 |
+
print(f"Test MSE: {mse}")
|
| 185 |
+
print(f"Test R2: {r2}")
|
| 186 |
+
# Prepare train and valid DataFrames for plotting
|
| 187 |
+
train_size = int(len(df) * 0.9)
|
| 188 |
+
train = df.iloc[:train_size].copy()
|
| 189 |
+
valid = df.iloc[train_size:].copy()
|
| 190 |
+
|
| 191 |
+
# Ensure predictions and valid have the same length
|
| 192 |
+
if len(predictions) > len(valid):
|
| 193 |
+
predictions = predictions[-len(valid):]
|
| 194 |
+
elif len(predictions) < len(valid):
|
| 195 |
+
valid = valid.iloc[-len(predictions):].copy()
|
| 196 |
+
|
| 197 |
+
valid['Predictions'] = predictions
|
| 198 |
+
valid.index = df.index[train_size:][-len(valid):]
|
| 199 |
+
train.index = df.index[:train_size]
|
| 200 |
+
valid['Predictions'] = valid['Predictions'].interpolate() # Smooth transition
|
| 201 |
+
train.index = df.index[:train_size]
|
| 202 |
+
historical_data = [
|
| 203 |
+
{
|
| 204 |
+
'date': str(train.index[i].date()) if hasattr(train.index[i], 'date') else str(train.index[i]),
|
| 205 |
+
'price': float(train['Close'].iloc[i])
|
| 206 |
+
}
|
| 207 |
+
for i in range(len(train))
|
| 208 |
+
]
|
| 209 |
+
last_sequence = X_test[-1]
|
| 210 |
+
last_date = df.index[-1]
|
| 211 |
+
future_start_date = datetime.strptime(prediction_date, '%Y-%m-%d')
|
| 212 |
+
future_days = (future_start_date - last_date).days
|
| 213 |
+
if future_days <= 0:
|
| 214 |
+
return jsonify({'error': 'Prediction date must be in the future.'}), 400
|
| 215 |
+
future_prices = predict_future(model, last_sequence, scaler, n_steps=future_days, n_features=df.shape[1])
|
| 216 |
+
future_dates = pd.date_range(start=last_date + timedelta(days=1), periods=future_days, freq='B')
|
| 217 |
+
buy_indices, buy_signals = find_optimal_buy_points(future_prices)
|
| 218 |
+
actual_vs_predicted = [
|
| 219 |
+
{
|
| 220 |
+
'date': str(valid.index[i].date()),
|
| 221 |
+
'actual': float(valid['Close'].iloc[i]),
|
| 222 |
+
'predicted': float(valid['Predictions'].iloc[i])
|
| 223 |
+
}
|
| 224 |
+
for i in range(len(valid))
|
| 225 |
+
]
|
| 226 |
+
result = {
|
| 227 |
+
'stock': str(ticker),
|
| 228 |
+
'predictedPrice': float(round(predictions[-1], 2)),
|
| 229 |
+
'historicalData': historical_data,
|
| 230 |
+
'futurePrices': [float(x) for x in future_prices.tolist()],
|
| 231 |
+
'futureDates': [str(x) for x in future_dates.strftime('%Y-%m-%d').tolist()],
|
| 232 |
+
'buySignals': {
|
| 233 |
+
"points": [float(x) for x in buy_signals["points"]],
|
| 234 |
+
"reasons": [str(r) for r in buy_signals["reasons"]]
|
| 235 |
+
},
|
| 236 |
+
'buyIndices': [int(x) for x in buy_indices],
|
| 237 |
+
'actualVsPredicted': actual_vs_predicted
|
| 238 |
+
}
|
| 239 |
+
return jsonify(result)
|
static/pipelines/lstm_pipeline.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 6 |
+
from tensorflow.keras.models import Sequential
|
| 7 |
+
from tensorflow.keras.layers import LSTM, Dense, BatchNormalization
|
| 8 |
+
from tensorflow.keras.optimizers import Adam
|
| 9 |
+
from tensorflow.keras.losses import Huber
|
| 10 |
+
from flask import jsonify
|
| 11 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
| 12 |
+
from sklearn.model_selection import train_test_split
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Function to predict future stock prices
|
| 16 |
+
def predict_future(model, last_sequence, scaler, n_steps=1):
|
| 17 |
+
last_sequence_reshaped = last_sequence.reshape((1, last_sequence.shape[0], last_sequence.shape[1]))
|
| 18 |
+
future_predictions = []
|
| 19 |
+
current_input = last_sequence_reshaped
|
| 20 |
+
|
| 21 |
+
for _ in range(n_steps):
|
| 22 |
+
predicted_price = model.predict(current_input)[0, 0]
|
| 23 |
+
future_predictions.append(predicted_price)
|
| 24 |
+
current_input = np.roll(current_input, -1, axis=1)
|
| 25 |
+
current_input[0, -1, 0] = predicted_price
|
| 26 |
+
|
| 27 |
+
future_predictions = scaler.inverse_transform(
|
| 28 |
+
np.hstack((np.array(future_predictions).reshape(-1, 1), np.zeros((len(future_predictions), 6))))
|
| 29 |
+
)[:, 0]
|
| 30 |
+
|
| 31 |
+
return future_predictions
|
| 32 |
+
|
| 33 |
+
# Function to find optimal buy points
|
| 34 |
+
def find_optimal_buy_points(prices, window=3, threshold=0.01):
|
| 35 |
+
buy_indices = []
|
| 36 |
+
buy_signals = {"points": [], "reasons": []}
|
| 37 |
+
|
| 38 |
+
if len(prices) < 2 * window + 1:
|
| 39 |
+
return buy_indices, buy_signals
|
| 40 |
+
|
| 41 |
+
for i in range(window, len(prices) - window):
|
| 42 |
+
window_slice = prices[i - window:i + window + 1]
|
| 43 |
+
if prices[i] == min(window_slice):
|
| 44 |
+
before_diff = prices[i] - min(prices[i - window:i])
|
| 45 |
+
after_diff = prices[i] - min(prices[i + 1:i + window + 1])
|
| 46 |
+
if before_diff > threshold and after_diff > threshold:
|
| 47 |
+
buy_indices.append(i)
|
| 48 |
+
buy_signals["points"].append(prices[i])
|
| 49 |
+
buy_signals["reasons"].append("Local minimum with significant change before and after")
|
| 50 |
+
|
| 51 |
+
if not buy_indices and len(prices) > 0:
|
| 52 |
+
min_idx = np.argmin(prices)
|
| 53 |
+
buy_indices.append(min_idx)
|
| 54 |
+
buy_signals["points"].append(prices[min_idx])
|
| 55 |
+
buy_signals["reasons"].append("Lowest price point")
|
| 56 |
+
|
| 57 |
+
return buy_indices, buy_signals
|
| 58 |
+
|
| 59 |
+
# 6. Create Sequences for Time Series Forecasting
|
| 60 |
+
def create_sequences(data, seq_length=20):
|
| 61 |
+
sequences = []
|
| 62 |
+
for i in range(len(data) - seq_length):
|
| 63 |
+
sequences.append(data[i: i + seq_length])
|
| 64 |
+
return np.array(sequences)
|
| 65 |
+
|
| 66 |
+
# Main pipeline function to train LSTM model and return predictions
|
| 67 |
+
def run_lstm_prediction(data):
|
| 68 |
+
ticker = data['ticker']
|
| 69 |
+
prediction_date = data['prediction_date']
|
| 70 |
+
epochs = int(data.get('epochs', 100)) # default to 100 if not provided
|
| 71 |
+
|
| 72 |
+
# Download stock data
|
| 73 |
+
df = yf.download(ticker, start='2019-01-01', end=datetime.today().strftime('%Y-%m-%d'))
|
| 74 |
+
if df.empty:
|
| 75 |
+
return jsonify({'error': 'Failed to fetch stock data.'}), 400
|
| 76 |
+
|
| 77 |
+
# Download market indices
|
| 78 |
+
nifty = yf.download("^NSEI", start='2019-01-01', end=datetime.today().strftime('%Y-%m-%d'))[['Close']].rename(columns={'Close': 'NIFTY_Close'})
|
| 79 |
+
sensex = yf.download("^BSESN", start='2019-01-01', end=datetime.today().strftime('%Y-%m-%d'))[['Close']].rename(columns={'Close': 'SENSEX_Close'})
|
| 80 |
+
|
| 81 |
+
# Merge indices with stock data
|
| 82 |
+
df = df.merge(nifty, left_index=True, right_index=True)
|
| 83 |
+
df = df.merge(sensex, left_index=True, right_index=True)
|
| 84 |
+
|
| 85 |
+
df = df[['Close', 'Open', 'High', 'Low', 'Volume', 'NIFTY_Close', 'SENSEX_Close']]
|
| 86 |
+
|
| 87 |
+
# Scaling
|
| 88 |
+
scaler = MinMaxScaler(feature_range=(-1, 1))
|
| 89 |
+
df_scaled = scaler.fit_transform(df)
|
| 90 |
+
# 5. Convert to NumPy Array
|
| 91 |
+
df_array = df_scaled.astype(np.float32)
|
| 92 |
+
|
| 93 |
+
seq_length = 20 # Use past 20 days to predict next day
|
| 94 |
+
sequences = create_sequences(df_array, seq_length+1)
|
| 95 |
+
|
| 96 |
+
# 7. Split Data into Train (80%), Validation (10%), Test (10%)
|
| 97 |
+
def split_data(sequence):
|
| 98 |
+
train_data, test_data = train_test_split(sequence, test_size=0.2, shuffle=False)
|
| 99 |
+
val_data, test_data = train_test_split(test_data, test_size=0.5, shuffle=False)
|
| 100 |
+
return train_data, val_data, test_data
|
| 101 |
+
|
| 102 |
+
train_data, val_data, test_data = split_data(sequences)
|
| 103 |
+
|
| 104 |
+
# 8. Separate Features (X) and Target (y)
|
| 105 |
+
X_train, y_train = train_data[:, :-1, :], train_data[:, -1, 0]
|
| 106 |
+
X_valid, y_valid = val_data[:, :-1, :], val_data[:, -1, 0]
|
| 107 |
+
X_test, y_test = test_data[:, :-1, :], test_data[:, -1, 0]
|
| 108 |
+
|
| 109 |
+
print(f"x_train shape: {X_train.shape}, y_train shape: {y_train.shape}")
|
| 110 |
+
print(f"x_valid shape: {X_valid.shape}, y_valid shape: {y_valid.shape}")
|
| 111 |
+
print(f"x_test shape: {X_test.shape}, y_test shape: {y_test.shape}")
|
| 112 |
+
|
| 113 |
+
# Build LSTM model
|
| 114 |
+
model = Sequential()
|
| 115 |
+
model.add(LSTM(128, return_sequences=True, activation='relu', input_shape=(X_test.shape[1], X_test.shape[2])))
|
| 116 |
+
model.add(LSTM(64, return_sequences=False,activation='relu'))
|
| 117 |
+
model.add(Dense(39, activation='relu'))
|
| 118 |
+
model.add(BatchNormalization())
|
| 119 |
+
model.add(Dense(1))
|
| 120 |
+
model.compile(optimizer='adam', loss='mse',metrics=['mean_squared_error'])
|
| 121 |
+
model.fit(X_train, y_train, epochs=epochs, batch_size=32, verbose=1,shuffle=False,validation_data=(X_valid,y_valid))
|
| 122 |
+
|
| 123 |
+
# Predict and Reshape Results
|
| 124 |
+
predictions = model.predict(X_test)
|
| 125 |
+
predictions = predictions.reshape(-1, 1)
|
| 126 |
+
|
| 127 |
+
# Inverse Transform Predictions (Only Close Price)
|
| 128 |
+
predictions = scaler.inverse_transform(np.hstack((predictions, np.zeros((len(predictions), 6)))))[:, 0]
|
| 129 |
+
|
| 130 |
+
# Inverse Transform y_test (Only Close Price)
|
| 131 |
+
y_test_unscaled = scaler.inverse_transform(np.hstack((y_test.reshape(-1, 1), np.zeros((len(y_test), 6)))))[:, 0]
|
| 132 |
+
|
| 133 |
+
# Align predictions and actuals
|
| 134 |
+
predictions = predictions[-len(y_test_unscaled):]
|
| 135 |
+
rmse= np.sqrt(np.mean((predictions-y_test_unscaled)**2))
|
| 136 |
+
mae = mean_absolute_error(y_test_unscaled, predictions)
|
| 137 |
+
mse= mean_squared_error(y_test_unscaled, predictions)
|
| 138 |
+
r2= r2_score(y_test_unscaled, predictions)
|
| 139 |
+
print(f"Test RMSE: {rmse}")
|
| 140 |
+
print(f"Test MAE: {mae}")
|
| 141 |
+
print(f"Test MSE: {mse}")
|
| 142 |
+
print(f"Test R2: {r2}")
|
| 143 |
+
|
| 144 |
+
# Split into Train and Validation/Test Data
|
| 145 |
+
train_size = int(len(df) * 0.9)
|
| 146 |
+
train = df.iloc[:train_size].copy()
|
| 147 |
+
valid = df.iloc[train_size:].copy()
|
| 148 |
+
|
| 149 |
+
# Ensure valid has the same length as predictions
|
| 150 |
+
valid = valid.iloc[-len(predictions):].copy()
|
| 151 |
+
valid['Predictions'] = predictions
|
| 152 |
+
valid.index = df.index[train_size:][-len(valid):] # Align timestamps
|
| 153 |
+
|
| 154 |
+
# Optional: Fill missing data (if any)
|
| 155 |
+
valid['Predictions'] = valid['Predictions'].interpolate()
|
| 156 |
+
|
| 157 |
+
# Fix Training Data Index (if needed)
|
| 158 |
+
train.index = df.index[:train_size]
|
| 159 |
+
|
| 160 |
+
# Historical data for response
|
| 161 |
+
#test_indices = df.index[-len(X_test):]
|
| 162 |
+
|
| 163 |
+
# Historical data for response (use test_indices)
|
| 164 |
+
historical_data = [
|
| 165 |
+
{
|
| 166 |
+
'date': str(train.index[i].date()) if hasattr(train.index[i], 'date') else str(train.index[i]),
|
| 167 |
+
'price': float(train['Close'].iloc[i])
|
| 168 |
+
}
|
| 169 |
+
for i in range(len(train))
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
# Future prediction logic
|
| 173 |
+
last_sequence = X_test[-1]
|
| 174 |
+
last_date = df.index[-1]
|
| 175 |
+
future_start_date = datetime.strptime(prediction_date, '%Y-%m-%d')
|
| 176 |
+
future_days = (future_start_date - last_date).days
|
| 177 |
+
|
| 178 |
+
if future_days <= 0:
|
| 179 |
+
return jsonify({'error': 'Prediction date must be in the future.'}), 400
|
| 180 |
+
|
| 181 |
+
# Predict future prices
|
| 182 |
+
future_prices = predict_future(model, last_sequence, scaler, n_steps=future_days)
|
| 183 |
+
future_dates = pd.date_range(start=last_date + timedelta(days=1), periods=future_days, freq='B')
|
| 184 |
+
|
| 185 |
+
# Find buy points
|
| 186 |
+
buy_indices, buy_signals = find_optimal_buy_points(future_prices)
|
| 187 |
+
|
| 188 |
+
# # Validation DataFrame for actual vs predicted
|
| 189 |
+
# valid = pd.DataFrame({
|
| 190 |
+
# 'Close': y_test_unscaled,
|
| 191 |
+
# 'Predictions': predictions
|
| 192 |
+
# }, index=test_indices)
|
| 193 |
+
|
| 194 |
+
# Prepare actual vs predicted for validation/test period
|
| 195 |
+
actual_vs_predicted = [
|
| 196 |
+
{
|
| 197 |
+
'date': str(valid.index[i].date()),
|
| 198 |
+
'actual': float(valid['Close'].iloc[i]),
|
| 199 |
+
'predicted': float(valid['Predictions'].iloc[i])
|
| 200 |
+
}
|
| 201 |
+
for i in range(len(valid))
|
| 202 |
+
]
|
| 203 |
+
|
| 204 |
+
# Final result JSON
|
| 205 |
+
result = {
|
| 206 |
+
'stock': str(ticker),
|
| 207 |
+
'predictedPrice': float(round(predictions[-1], 2)),
|
| 208 |
+
'historicalData': historical_data,
|
| 209 |
+
'futurePrices': [float(x) for x in future_prices.tolist()],
|
| 210 |
+
'futureDates': [str(x) for x in future_dates.strftime('%Y-%m-%d').tolist()],
|
| 211 |
+
'buySignals': {
|
| 212 |
+
"points": [float(x) for x in buy_signals["points"]],
|
| 213 |
+
"reasons": [str(r) for r in buy_signals["reasons"]]
|
| 214 |
+
},
|
| 215 |
+
'buyIndices': [int(x) for x in buy_indices],
|
| 216 |
+
'actualVsPredicted': actual_vs_predicted
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
return jsonify(result)
|
static/video/bg video.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f7655d1b3e395206b7bad9777d18bb0d201c88482eca8fa30001cefd8cc8e143
|
| 3 |
+
size 4454739
|
t.txt
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@app.route("/predict-stock", methods=['POST'])
|
| 2 |
+
def predict_stock():
|
| 3 |
+
data = request.get_json()
|
| 4 |
+
ticker = data['ticker']
|
| 5 |
+
|
| 6 |
+
df = yf.download(ticker, start='2019-01-01', end=datetime.today().strftime('%Y-%m-%d'))
|
| 7 |
+
if df.empty:
|
| 8 |
+
return jsonify({'error': 'Failed to fetch stock data.'}), 400
|
| 9 |
+
|
| 10 |
+
nifty = yf.download("^NSEI", start='2019-01-01', end=datetime.today().strftime('%Y-%m-%d'))[['Close']].rename(columns={'Close': 'NIFTY_Close'})
|
| 11 |
+
sensex = yf.download("^BSESN", start='2019-01-01', end=datetime.today().strftime('%Y-%m-%d'))[['Close']].rename(columns={'Close': 'SENSEX_Close'})
|
| 12 |
+
|
| 13 |
+
df = df.merge(nifty, left_index=True, right_index=True)
|
| 14 |
+
df = df.merge(sensex, left_index=True, right_index=True)
|
| 15 |
+
|
| 16 |
+
df = df[['Close', 'Open', 'High', 'Low', 'Volume', 'NIFTY_Close', 'SENSEX_Close']]
|
| 17 |
+
|
| 18 |
+
scaler = MinMaxScaler(feature_range=(-1, 1))
|
| 19 |
+
df_scaled = scaler.fit_transform(df)
|
| 20 |
+
|
| 21 |
+
def create_sequences(data, seq_length=20):
|
| 22 |
+
sequences = []
|
| 23 |
+
for i in range(len(data) - seq_length):
|
| 24 |
+
sequences.append(data[i: i + seq_length + 1])
|
| 25 |
+
return np.array(sequences)
|
| 26 |
+
|
| 27 |
+
sequences = create_sequences(df_scaled, 20)
|
| 28 |
+
|
| 29 |
+
X_test = sequences[:, :-1, :]
|
| 30 |
+
y_test = sequences[:, -1, 0]
|
| 31 |
+
|
| 32 |
+
model = Sequential()
|
| 33 |
+
model.add(LSTM(128, return_sequences=True, activation='relu', input_shape=(X_test.shape[1], X_test.shape[2])))
|
| 34 |
+
model.add(LSTM(64, activation='relu'))
|
| 35 |
+
model.add(Dense(39, activation='relu'))
|
| 36 |
+
model.add(BatchNormalization())
|
| 37 |
+
model.add(Dense(1))
|
| 38 |
+
|
| 39 |
+
model.compile(optimizer=Adam(1e-6), loss=Huber(), metrics=['mse'])
|
| 40 |
+
|
| 41 |
+
model.fit(X_test, y_test, epochs=5, batch_size=32, verbose=0)
|
| 42 |
+
|
| 43 |
+
predictions = model.predict(X_test).reshape(-1, 1)
|
| 44 |
+
predictions_unscaled = scaler.inverse_transform(np.hstack((predictions, np.zeros((len(predictions), 6)))))[:, 0]
|
| 45 |
+
|
| 46 |
+
historical_data = [{
|
| 47 |
+
'date': str(df.index[-len(predictions_unscaled)+i].date()),
|
| 48 |
+
'price': float(pred)
|
| 49 |
+
} for i, pred in enumerate(predictions_unscaled)]
|
| 50 |
+
|
| 51 |
+
result = {
|
| 52 |
+
'stock': ticker,
|
| 53 |
+
'predictedPrice': round(predictions_unscaled[-1], 2),
|
| 54 |
+
'historicalData': historical_data
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return jsonify(result)
|
| 58 |
+
|
| 59 |
+
1. Update your API response in run_lstm_prediction
|
| 60 |
+
Add these lines after you compute valid['Predictions'] and before returning the result:
|
| 61 |
+
|
| 62 |
+
# ...existing code...
|
| 63 |
+
|
| 64 |
+
# Prepare actual vs predicted for validation/test period
|
| 65 |
+
actual_vs_predicted = [
|
| 66 |
+
{
|
| 67 |
+
'date': str(valid.index[i].date()),
|
| 68 |
+
'actual': float(valid['Close'].iloc[i]),
|
| 69 |
+
'predicted': float(valid['Predictions'].iloc[i])
|
| 70 |
+
}
|
| 71 |
+
for i in range(len(valid))
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
# Final result JSON
|
| 75 |
+
result = {
|
| 76 |
+
'stock': str(ticker),
|
| 77 |
+
'predictedPrice': float(round(predictions_unscaled[-1], 2)),
|
| 78 |
+
'historicalData': [
|
| 79 |
+
{
|
| 80 |
+
'date': str(df.index[-len(predictions_unscaled) + i].date()),
|
| 81 |
+
'price': float(pred)
|
| 82 |
+
} for i, pred in enumerate(predictions_unscaled)
|
| 83 |
+
],
|
| 84 |
+
'futurePrices': [float(x) for x in future_prices.tolist()],
|
| 85 |
+
'futureDates': [str(x) for x in future_dates.strftime('%Y-%m-%d').tolist()],
|
| 86 |
+
'buySignals': {
|
| 87 |
+
"points": [float(x) for x in buy_signals["points"]],
|
| 88 |
+
"reasons": [str(r) for r in buy_signals["reasons"]]
|
| 89 |
+
},
|
| 90 |
+
'buyIndices': [int(x) for x in buy_indices],
|
| 91 |
+
'actualVsPredicted': actual_vs_predicted # <-- add this line
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
return jsonify(result)
|
| 95 |
+
# ...existing code...
|
| 96 |
+
|
| 97 |
+
2. Update your frontend (JS) to plot both lines
|
| 98 |
+
In your predict.js, update the chart rendering logic to use the new actualVsPredicted array:
|
| 99 |
+
function renderPredictionChart(prediction) {
|
| 100 |
+
// ...existing code...
|
| 101 |
+
|
| 102 |
+
// Plot actual vs predicted for validation/test period
|
| 103 |
+
if (prediction.actualVsPredicted && Array.isArray(prediction.actualVsPredicted)) {
|
| 104 |
+
const validDates = prediction.actualVsPredicted.map(d => d.date);
|
| 105 |
+
const actualVals = prediction.actualVsPredicted.map(d => d.actual);
|
| 106 |
+
const predictedVals = prediction.actualVsPredicted.map(d => d.predicted);
|
| 107 |
+
|
| 108 |
+
const traceActual = {
|
| 109 |
+
x: validDates,
|
| 110 |
+
y: actualVals,
|
| 111 |
+
mode: 'lines',
|
| 112 |
+
name: 'Actual Price',
|
| 113 |
+
line: { color: 'green', width: 2 }
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const tracePredicted = {
|
| 117 |
+
x: validDates,
|
| 118 |
+
y: predictedVals,
|
| 119 |
+
mode: 'lines',
|
| 120 |
+
name: 'Predicted Price',
|
| 121 |
+
line: { color: 'red', width: 2, dash: 'dot' }
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
// Add these traces to your Plotly data array
|
| 125 |
+
data.push(traceActual, tracePredicted);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// ...existing code...
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@app.route('/api/fundamentals')
|
| 133 |
+
def api_fundamentals():
|
| 134 |
+
symbol = request.args.get('symbol')
|
| 135 |
+
if not symbol:
|
| 136 |
+
return jsonify({'error': 'No symbol provided'}), 400
|
| 137 |
+
try:
|
| 138 |
+
ticker = yf.Ticker(symbol)
|
| 139 |
+
info = ticker.info
|
| 140 |
+
history = ticker.history(period="1y")
|
| 141 |
+
if history.empty:
|
| 142 |
+
return jsonify({'error': f'No data found for symbol: {symbol}'}), 404
|
| 143 |
+
# ... rest of your code ...
|
| 144 |
+
# You can adjust which fields you want to send
|
| 145 |
+
data = {
|
| 146 |
+
'pe': info.get('trailingPE'),
|
| 147 |
+
'pb': info.get('priceToBook'),
|
| 148 |
+
'ps': info.get('priceToSalesTrailing12Months'),
|
| 149 |
+
'divYield': (info.get('dividendYield') or 0) * 100,
|
| 150 |
+
'roe': info.get('returnOnEquity') * 100 if info.get('returnOnEquity') is not None else None,
|
| 151 |
+
'roa': info.get('returnOnAssets') * 100 if info.get('returnOnAssets') is not None else None,
|
| 152 |
+
'grossMargin': info.get('grossMargins') * 100 if info.get('grossMargins') is not None else None,
|
| 153 |
+
'opMargin': info.get('operatingMargins') * 100 if info.get('operatingMargins') is not None else None,
|
| 154 |
+
'currentRatio': info.get('currentRatio'),
|
| 155 |
+
'quickRatio': info.get('quickRatio'),
|
| 156 |
+
'debtEquity': info.get('debtToEquity'),
|
| 157 |
+
'intCoverage': info.get('ebitda') / info.get('interestExpense') if info.get('ebitda') and info.get('interestExpense') else None,
|
| 158 |
+
'history': ticker.history(period="1y").reset_index().to_dict(orient='records')
|
| 159 |
+
}
|
| 160 |
+
return jsonify(data)
|
| 161 |
+
except Exception as e:
|
| 162 |
+
return jsonify({'error': str(e)}), 500
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 168 |
+
const darkModeToggle = document.getElementById("darkModeToggle");
|
| 169 |
+
const toggleIcon = document.querySelector('.toggle-circle .toggle-icon');
|
| 170 |
+
|
| 171 |
+
function updateToggleIcon() {
|
| 172 |
+
if (darkModeToggle.checked) {
|
| 173 |
+
toggleIcon.innerHTML = '☾'; // Moon
|
| 174 |
+
} else {
|
| 175 |
+
toggleIcon.innerHTML = '☀'; // Sun
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Initial state
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
// Set the toggle to checked (on) by default
|
| 183 |
+
darkModeToggle.checked = true;
|
| 184 |
+
updateToggleIcon();
|
| 185 |
+
// Apply dark mode on page load
|
| 186 |
+
document.body.classList.add("dark-mode");
|
| 187 |
+
|
| 188 |
+
darkModeToggle.addEventListener("change", function() {
|
| 189 |
+
document.body.classList.toggle("dark-mode");
|
| 190 |
+
if (typeof updateChartColors === "function") updateChartColors(this.checked);
|
| 191 |
+
updateToggleIcon();
|
| 192 |
+
});
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
.toggle-switch {
|
| 196 |
+
position: relative;
|
| 197 |
+
display: inline-block;
|
| 198 |
+
width: 50px;
|
| 199 |
+
height: 25px;
|
| 200 |
+
}
|
| 201 |
+
.toggle-switch input {
|
| 202 |
+
opacity: 0;
|
| 203 |
+
width: 0;
|
| 204 |
+
height: 0;
|
| 205 |
+
}
|
| 206 |
+
.slider {
|
| 207 |
+
position: absolute;
|
| 208 |
+
cursor: pointer;
|
| 209 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 210 |
+
background-color: #ccc;
|
| 211 |
+
transition: .4s;
|
| 212 |
+
border-radius: 25px;
|
| 213 |
+
}
|
| 214 |
+
.toggle-circle {
|
| 215 |
+
position: absolute;
|
| 216 |
+
left: 4px;
|
| 217 |
+
bottom: 3px;
|
| 218 |
+
width: 18px;
|
| 219 |
+
height: 18px;
|
| 220 |
+
background: #fff;
|
| 221 |
+
border-radius: 50%;
|
| 222 |
+
display: flex;
|
| 223 |
+
align-items: center;
|
| 224 |
+
justify-content: center;
|
| 225 |
+
transition: transform 0.4s;
|
| 226 |
+
z-index: 2;
|
| 227 |
+
font-size: 14px;
|
| 228 |
+
}
|
| 229 |
+
.toggle-icon {
|
| 230 |
+
transition: color 0.4s, content 0.4s;
|
| 231 |
+
color: #FFD600; /* Sun color */
|
| 232 |
+
}
|
| 233 |
+
input:checked + .slider {
|
| 234 |
+
background-color: #4a8fdf;
|
| 235 |
+
}
|
| 236 |
+
input:checked + .slider .toggle-circle {
|
| 237 |
+
transform: translateX(24px);
|
| 238 |
+
}
|
| 239 |
+
input:checked + .slider .toggle-icon {
|
| 240 |
+
color: #4a8fdf; /* Moon color */
|
| 241 |
+
/* Use content swap via JS for moon icon */
|
| 242 |
+
}
|
templates/404.html
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Page Not Found</title>
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--light-bg: #f4f2ec;
|
| 11 |
+
--light-text: #000;
|
| 12 |
+
--dark-bg: #1c2733;
|
| 13 |
+
--dark-text: #fff;
|
| 14 |
+
--primary-color: #4a8fdf;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
body {
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
font-family: Arial, sans-serif;
|
| 21 |
+
background-color: var(--light-bg);
|
| 22 |
+
color: var(--light-text);
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
justify-content: center;
|
| 26 |
+
align-items: center;
|
| 27 |
+
height: 100vh;
|
| 28 |
+
transition: background-color 0.3s, color 0.3s;
|
| 29 |
+
text-align: center;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body.dark-mode {
|
| 33 |
+
background-color: var(--dark-bg);
|
| 34 |
+
color: var(--dark-text);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h1 {
|
| 38 |
+
font-size: 3rem;
|
| 39 |
+
margin-bottom: 10px;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
p {
|
| 43 |
+
font-size: 1.2rem;
|
| 44 |
+
margin-bottom: 30px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
a {
|
| 48 |
+
display: inline-block;
|
| 49 |
+
padding: 10px 20px;
|
| 50 |
+
background-color: var(--primary-color);
|
| 51 |
+
color: #fff;
|
| 52 |
+
text-decoration: none;
|
| 53 |
+
border-radius: 5px;
|
| 54 |
+
transition: background 0.3s;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
a:hover {
|
| 58 |
+
background-color: #377ddf;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Toggle Switch */
|
| 62 |
+
.theme-toggle {
|
| 63 |
+
position: absolute;
|
| 64 |
+
top: 20px;
|
| 65 |
+
right: 30px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.toggle-switch {
|
| 69 |
+
position: relative;
|
| 70 |
+
display: inline-block;
|
| 71 |
+
width: 50px;
|
| 72 |
+
height: 25px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.toggle-switch input {
|
| 76 |
+
opacity: 0;
|
| 77 |
+
width: 0;
|
| 78 |
+
height: 0;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.slider {
|
| 82 |
+
position: absolute;
|
| 83 |
+
cursor: pointer;
|
| 84 |
+
top: 0;
|
| 85 |
+
left: 0;
|
| 86 |
+
right: 0;
|
| 87 |
+
bottom: 0;
|
| 88 |
+
background-color: #ccc;
|
| 89 |
+
transition: 0.4s;
|
| 90 |
+
border-radius: 25px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.slider:before {
|
| 94 |
+
position: absolute;
|
| 95 |
+
content: "";
|
| 96 |
+
height: 18px;
|
| 97 |
+
width: 18px;
|
| 98 |
+
left: 4px;
|
| 99 |
+
bottom: 3px;
|
| 100 |
+
background-color: white;
|
| 101 |
+
transition: 0.4s;
|
| 102 |
+
border-radius: 50%;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
input:checked + .slider {
|
| 106 |
+
background-color: var(--primary-color);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
input:checked + .slider:before {
|
| 110 |
+
transform: translateX(24px);
|
| 111 |
+
}
|
| 112 |
+
</style>
|
| 113 |
+
</head>
|
| 114 |
+
<body>
|
| 115 |
+
|
| 116 |
+
<!-- Toggle -->
|
| 117 |
+
<div class="theme-toggle">
|
| 118 |
+
<label class="toggle-switch">
|
| 119 |
+
<input type="checkbox" id="theme-toggle-checkbox">
|
| 120 |
+
<span class="slider"></span>
|
| 121 |
+
</label>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<h1>404 - Page Not Found</h1>
|
| 125 |
+
<p>Sorry, the page you’re looking for doesn’t exist.</p>
|
| 126 |
+
<a href="/">Go to Homepage</a>
|
| 127 |
+
|
| 128 |
+
<script>
|
| 129 |
+
// Theme toggle logic
|
| 130 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 131 |
+
const toggle = document.getElementById('theme-toggle-checkbox');
|
| 132 |
+
const currentTheme = localStorage.getItem('theme');
|
| 133 |
+
|
| 134 |
+
if (currentTheme === 'dark') {
|
| 135 |
+
document.body.classList.add('dark-mode');
|
| 136 |
+
toggle.checked = true;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
toggle.addEventListener('change', () => {
|
| 140 |
+
document.body.classList.toggle('dark-mode');
|
| 141 |
+
if (document.body.classList.contains('dark-mode')) {
|
| 142 |
+
localStorage.setItem('theme', 'dark');
|
| 143 |
+
} else {
|
| 144 |
+
localStorage.setItem('theme', 'light');
|
| 145 |
+
}
|
| 146 |
+
});
|
| 147 |
+
});
|
| 148 |
+
</script>
|
| 149 |
+
</body>
|
| 150 |
+
</html>
|
templates/base.html
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>{% block title %}Stock APP AR - MarketMind{% endblock %}</title>
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<nav>
|
| 10 |
+
<ul>
|
| 11 |
+
<li><a href="{{ url_for('home') }}">Home</a></li>
|
| 12 |
+
<li><a href="{{ url_for('login') }}">Login</a></li>
|
| 13 |
+
<li><a href="{{ url_for('fundamentals') }}">Fundamentals</a></li>
|
| 14 |
+
<li><a href="{{ url_for('movers') }}">Market Movers</a></li>
|
| 15 |
+
<li><a href="{{ url_for('news') }}">News</a></li>
|
| 16 |
+
<li><a href="{{ url_for('predict') }}">Predict</a></li>
|
| 17 |
+
</ul>
|
| 18 |
+
</nav>
|
| 19 |
+
|
| 20 |
+
{% block content %}{% endblock %}
|
| 21 |
+
|
| 22 |
+
</body>
|
| 23 |
+
</html>
|
templates/disclaimer.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Disclaimer - MarketMind</title>
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/legal.css') }}">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<nav class="navbar">
|
| 11 |
+
<div><strong>MarketMind</strong></div>
|
| 12 |
+
<div>
|
| 13 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 14 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 15 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 16 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 17 |
+
<a href="{{ url_for('predict') }}">Predictor</a>
|
| 18 |
+
<label class="toggle-switch">
|
| 19 |
+
<input type="checkbox" id="darkModeToggle">
|
| 20 |
+
<span class="slider">
|
| 21 |
+
<span class="toggle-circle">
|
| 22 |
+
<span class="toggle-icon">☾</span>
|
| 23 |
+
</span>
|
| 24 |
+
</span>
|
| 25 |
+
</label>
|
| 26 |
+
</div>
|
| 27 |
+
</nav>
|
| 28 |
+
<main class="legal-container">
|
| 29 |
+
<h1>Disclaimer</h1>
|
| 30 |
+
<p>
|
| 31 |
+
All information provided by MarketMind is for informational and educational purposes only. We do not guarantee the accuracy, completeness, or timeliness of the data. MarketMind does not provide investment advice, and users should consult a qualified financial advisor before making investment decisions.
|
| 32 |
+
</p>
|
| 33 |
+
<p>
|
| 34 |
+
MarketMind is not responsible for any losses or damages resulting from the use of this site.
|
| 35 |
+
</p>
|
| 36 |
+
</main>
|
| 37 |
+
<footer>
|
| 38 |
+
<div class="footer-bottom">
|
| 39 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 40 |
+
</div>
|
| 41 |
+
</footer>
|
| 42 |
+
<script type="module" src="{{ url_for('static', filename='js/legal.js') }}"></script>
|
| 43 |
+
</body>
|
| 44 |
+
</html>
|
templates/fundamentals.html
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
|
| 8 |
+
<script type="module" src="{{ url_for('static', filename='js/firebase-config.js') }}"></script>
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/fundamentals.css') }}">
|
| 10 |
+
<!-- Intro.js CSS -->
|
| 11 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css">
|
| 12 |
+
<!-- Intro.js JS -->
|
| 13 |
+
<script src="https://cdn.jsdelivr.net/npm/intro.js/minified/intro.min.js"></script>
|
| 14 |
+
<script type="module"> import { requireLogin } from "{{ url_for('static', filename='js/auth.js') }}"; requireLogin(); </script>
|
| 15 |
+
<!-- Add this to your head section -->
|
| 16 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 17 |
+
<title>MarketMind - Fundamentals</title>
|
| 18 |
+
|
| 19 |
+
</head>
|
| 20 |
+
<body>
|
| 21 |
+
<div class="transition-overlay"></div>
|
| 22 |
+
|
| 23 |
+
<nav class="navbar">
|
| 24 |
+
<div><strong>MarketMind</strong></div>
|
| 25 |
+
<div>
|
| 26 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 27 |
+
<div class="dropdown">
|
| 28 |
+
<h4>Services</h4>
|
| 29 |
+
<div class="dropdown-content">
|
| 30 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 31 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 32 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 33 |
+
<a href="{{url_for('predict')}}">Predictor</a>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
<a href="{{ url_for('login') }}" id="logout">Login</a>
|
| 37 |
+
<label class="toggle-switch">
|
| 38 |
+
<input type="checkbox" id="darkModeToggle">
|
| 39 |
+
<span class="slider">
|
| 40 |
+
<span class="toggle-circle">
|
| 41 |
+
<span class="toggle-icon">☾</span> <!-- Sun by default -->
|
| 42 |
+
</span>
|
| 43 |
+
</span>
|
| 44 |
+
</label>
|
| 45 |
+
</div>
|
| 46 |
+
</nav>
|
| 47 |
+
|
| 48 |
+
<div class="stock-data-container" data-intro="Here you can view and filter historical stock data for Indian companies." data-step="1">
|
| 49 |
+
<h2>Indian Stock Market Data</h2>
|
| 50 |
+
<div class="date-range-filter">
|
| 51 |
+
<label for="start-date">Start Date:</label>
|
| 52 |
+
<input type="date" id="start-date" name="start-date">
|
| 53 |
+
|
| 54 |
+
<label for="end-date">End Date:</label>
|
| 55 |
+
<input type="date" id="end-date" name="end-date">
|
| 56 |
+
|
| 57 |
+
<select id="fundamentals-symbol" style="width: 250px;">
|
| 58 |
+
<option value="">Select a company...</option>
|
| 59 |
+
</select>
|
| 60 |
+
|
| 61 |
+
<button id="fetch">Fetch Data</button>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div id="message-container" style="display: none; padding: 10px; margin: 10px 0; border-radius: 5px;"></div>
|
| 65 |
+
|
| 66 |
+
<!-- Add tabs for different views -->
|
| 67 |
+
<div class="tab-container" data-intro="Switch between Table, Chart, or Both views for your selected stock data." data-step="2">
|
| 68 |
+
<button class="tab-button active" onclick="showView('table')">Table View</button>
|
| 69 |
+
<button class="tab-button" onclick="showView('chart')">Chart View</button>
|
| 70 |
+
<button class="tab-button" onclick="showView('both')">Both</button>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div id="stock-data-table"></div>
|
| 74 |
+
|
| 75 |
+
<!-- Add chart container -->
|
| 76 |
+
<div id="chart-view" style="display: none;">
|
| 77 |
+
<h3 class="chart-title">Price Movement</h3>
|
| 78 |
+
<div class="chart-container">
|
| 79 |
+
<div id="stockChart"></div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
<!-- Add this section after your stock data container -->
|
| 86 |
+
<div class="fundamentals-container">
|
| 87 |
+
<h2>Stock Fundamentals Analysis</h2>
|
| 88 |
+
|
| 89 |
+
<div class="fundamentals-search" data-intro="Analyze key fundamentals, ratios, and risk metrics for any company here." data-step="3">
|
| 90 |
+
<select id="fundamentals-search-bar" class="styled-dropdown">
|
| 91 |
+
<option value="">Select a company...</option>
|
| 92 |
+
<!-- Options will be populated by JS -->
|
| 93 |
+
</select>
|
| 94 |
+
<button onclick="fetchFundamentals(document.getElementById('fundamentals-search-bar').value)">Analyze</button>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div class="fundamentals-tabs">
|
| 98 |
+
<button class="tab-button active" onclick="openTab('ratios')">Key Ratios</button>
|
| 99 |
+
<button class="tab-button" onclick="openTab('chart')">Historical Trends</button>
|
| 100 |
+
<button class="tab-button" onclick="openTab('compare')">Risk Metrics</button>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div id="ratios" class="tab-content" style="display: block;">
|
| 104 |
+
<div class="fundamentals-grid">
|
| 105 |
+
<div class="fundamental-card">
|
| 106 |
+
<h3>Valuation Ratios</h3>
|
| 107 |
+
<div class="ratio-item">
|
| 108 |
+
<span class="ratio-name">P/E Ratio</span>
|
| 109 |
+
<span class="ratio-value" id="pe-ratio">-</span>
|
| 110 |
+
</div>
|
| 111 |
+
<div class="ratio-item">
|
| 112 |
+
<span class="ratio-name">P/B Ratio</span>
|
| 113 |
+
<span class="ratio-value" id="pb-ratio">-</span>
|
| 114 |
+
</div>
|
| 115 |
+
<div class="ratio-item">
|
| 116 |
+
<span class="ratio-name">P/S Ratio</span>
|
| 117 |
+
<span class="ratio-value" id="ps-ratio">-</span>
|
| 118 |
+
</div>
|
| 119 |
+
<div class="ratio-item">
|
| 120 |
+
<span class="ratio-name">Dividend Yield</span>
|
| 121 |
+
<span class="ratio-value" id="div-yield">-</span>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div class="fundamental-card">
|
| 126 |
+
<h3>Profitability Ratios</h3>
|
| 127 |
+
<div class="ratio-item">
|
| 128 |
+
<span class="ratio-name">ROE</span>
|
| 129 |
+
<span class="ratio-value" id="roe">-</span>
|
| 130 |
+
</div>
|
| 131 |
+
<div class="ratio-item">
|
| 132 |
+
<span class="ratio-name">ROA</span>
|
| 133 |
+
<span class="ratio-value" id="roa">-</span>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="ratio-item">
|
| 136 |
+
<span class="ratio-name">Gross Margin</span>
|
| 137 |
+
<span class="ratio-value" id="gross-margin">-</span>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="ratio-item">
|
| 140 |
+
<span class="ratio-name">Operating Margin</span>
|
| 141 |
+
<span class="ratio-value" id="op-margin">-</span>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div class="fundamental-card">
|
| 146 |
+
<h3>Liquidity Ratios</h3>
|
| 147 |
+
<div class="ratio-item">
|
| 148 |
+
<span class="ratio-name">Current Ratio</span>
|
| 149 |
+
<span class="ratio-value" id="current-ratio">-</span>
|
| 150 |
+
</div>
|
| 151 |
+
<div class="ratio-item">
|
| 152 |
+
<span class="ratio-name">Quick Ratio</span>
|
| 153 |
+
<span class="ratio-value" id="quick-ratio">-</span>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div class="fundamental-card">
|
| 158 |
+
<h3>Leverage Ratios</h3>
|
| 159 |
+
<div class="ratio-item">
|
| 160 |
+
<span class="ratio-name">Debt/Equity</span>
|
| 161 |
+
<span class="ratio-value" id="debt-equity">-</span>
|
| 162 |
+
</div>
|
| 163 |
+
<div class="ratio-item">
|
| 164 |
+
<span class="ratio-name">EBITDA Margin</span>
|
| 165 |
+
<span class="ratio-value" id="ebitda-margin">-</span>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<!-- Inside the fundamentals-container, in the chart tab -->
|
| 172 |
+
<div id="chart" class="tab-content" style="display: none;">
|
| 173 |
+
<div class="chart-container">
|
| 174 |
+
<div id="fundamentals-chart"></div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<div id="compare" class="tab-content">
|
| 179 |
+
<div class="fundamental-card">
|
| 180 |
+
<h3>Risk Metrics</h3>
|
| 181 |
+
<div class="ratio-item">
|
| 182 |
+
<span class="ratio-name">Volatility (Annualized)</span>
|
| 183 |
+
<span class="ratio-value" id="volatility">-</span>
|
| 184 |
+
</div>
|
| 185 |
+
<div class="ratio-item">
|
| 186 |
+
<span class="ratio-name">Beta</span>
|
| 187 |
+
<span class="ratio-value" id="beta">-</span>
|
| 188 |
+
</div>
|
| 189 |
+
<div class="ratio-item">
|
| 190 |
+
<span class="ratio-name">VaR (95%)</span>
|
| 191 |
+
<span class="ratio-value" id="var95">-</span>
|
| 192 |
+
</div>
|
| 193 |
+
<div class="chart-container">
|
| 194 |
+
<div id="risk-metrics-chart"></div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="chart-container">
|
| 197 |
+
<div id="rolling-vol-chart"></div>
|
| 198 |
+
</div>
|
| 199 |
+
<div class="chart-container" style="height:450px;">
|
| 200 |
+
<div id="beta-gauge-chart"></div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
<script type="module" src="{{ url_for('static', filename='js/fundamentals.js') }}"></script>
|
| 206 |
+
<script type="module" src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
| 207 |
+
<div class="shiny-curve"></div>
|
| 208 |
+
<div style="margin-top:500px"></div>
|
| 209 |
+
|
| 210 |
+
<footer>
|
| 211 |
+
<div class="footer-content">
|
| 212 |
+
<div class="footer-section">
|
| 213 |
+
<h3>Quick Links</h3>
|
| 214 |
+
<ul>
|
| 215 |
+
<li><a href="#">Home</a></li>
|
| 216 |
+
<li><a href="{{ url_for('fundamentals') }}">Fundamentals</a></li>
|
| 217 |
+
<li><a href="{{ url_for('movers') }}">Market Movers</a></li>
|
| 218 |
+
<li><a href="{{ url_for('news') }}">News</a></li>
|
| 219 |
+
<li><a href="{{ url_for('login') }}">Login</a></li>
|
| 220 |
+
<li><a href="{{url_for('predict')}}">Predictor</a></li>
|
| 221 |
+
</ul>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="footer-section">
|
| 224 |
+
<h3>Legal</h3>
|
| 225 |
+
<ul>
|
| 226 |
+
<li><a href="{{ url_for('privacy') }}">Privacy Policy</a></li>
|
| 227 |
+
<li><a href="{{ url_for('terms') }}">Terms of Service</a></li>
|
| 228 |
+
<li><a href="{{ url_for('disclaimer') }}">Disclaimer</a></li>
|
| 229 |
+
</ul>
|
| 230 |
+
</div>
|
| 231 |
+
<div class="footer-section">
|
| 232 |
+
<h3>Contact</h3>
|
| 233 |
+
<ul>
|
| 234 |
+
<li><a href="mailto:support@stockai.com">support@stockai.com</a></li>
|
| 235 |
+
<li><a href="tel:+15551234567">+1 (555) 123-4567</a></li>
|
| 236 |
+
</ul>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="footer-section">
|
| 239 |
+
<h3>Newsletter</h3>
|
| 240 |
+
<form id="newsletter-form">
|
| 241 |
+
<input type="email" id="newsletter-email" placeholder="Enter your email" required>
|
| 242 |
+
<button type="submit">Subscribe</button>
|
| 243 |
+
</form>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="footer-bottom">
|
| 247 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 248 |
+
</div>
|
| 249 |
+
</footer>
|
| 250 |
+
</body>
|
| 251 |
+
</html>
|
templates/home.html
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Stocker - Homepage</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
| 8 |
+
<script> const modelpath = "{{ modelpath }}"; </script>
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
|
| 11 |
+
<!-- Intro.js CSS -->
|
| 12 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css">
|
| 13 |
+
<!-- Intro.js JS -->
|
| 14 |
+
<script src="https://cdn.jsdelivr.net/npm/intro.js/minified/intro.min.js"></script>
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
|
| 18 |
+
<div class="transition-overlay"></div>
|
| 19 |
+
|
| 20 |
+
<nav class="navbar" data-intro="This is the main navigation bar. Use it to access all features!" data-step="2">
|
| 21 |
+
<div><strong>MarketMind</strong></div>
|
| 22 |
+
<div>
|
| 23 |
+
<a href="#">Home</a>
|
| 24 |
+
<div class="dropdown">
|
| 25 |
+
<h4>Services</h4>
|
| 26 |
+
<div class="dropdown-content">
|
| 27 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 28 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 29 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 30 |
+
<a href="{{url_for('predict')}}">Predictor</a>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<a href="{{ url_for('login') }}" id="logout">Login</a>
|
| 34 |
+
<label class="toggle-switch">
|
| 35 |
+
<input type="checkbox" id="darkModeToggle">
|
| 36 |
+
<span class="slider">
|
| 37 |
+
<span class="toggle-circle">
|
| 38 |
+
<span class="toggle-icon">☾</span> <!-- Sun by default -->
|
| 39 |
+
</span>
|
| 40 |
+
</span>
|
| 41 |
+
</label>
|
| 42 |
+
</div>
|
| 43 |
+
</nav>
|
| 44 |
+
|
| 45 |
+
<div class="container glow" data-text="MarketMind" data-intro="Welcome to MarketMind! This is your dashboard." data-step="1">MarketMind</div>
|
| 46 |
+
|
| 47 |
+
<!-- Highlighted Search Section -->
|
| 48 |
+
<div class="highlight-container fade-in" data-intro="Use the search bar to find companies and get instant suggestions." data-step="3">
|
| 49 |
+
<div class="description">
|
| 50 |
+
Stock analysis and screening tool for investors in India.
|
| 51 |
+
</div>
|
| 52 |
+
<div class="search-container">
|
| 53 |
+
<input type="text" class="search-box" id="home-search-box" placeholder="Search for a company">
|
| 54 |
+
<i class="fas fa-search"></i>
|
| 55 |
+
</div>
|
| 56 |
+
<div class="stock-suggestions">
|
| 57 |
+
<p>Trending Stocks:</p>
|
| 58 |
+
<div class="suggestions-list">
|
| 59 |
+
<span class="suggestion">Reliance Industries</span>
|
| 60 |
+
<span class="suggestion">Tata Consultancy Services (TCS)</span>
|
| 61 |
+
<span class="suggestion">HDFC Bank</span>
|
| 62 |
+
<span class="suggestion">Infosys</span>
|
| 63 |
+
<span class="suggestion">ICICI Bank</span>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
<!-- About Us Section -->
|
| 68 |
+
<div class="about-us fade-in">
|
| 69 |
+
<h2>About Us</h2>
|
| 70 |
+
<p>The stock market has a rich history, evolving from small trading groups to global financial powerhouses. Today, major players such as Reliance Industries, Tata Consultancy Services, and HDFC Bank dominate the Indian stock market. With technological advancements, stock analysis and prediction have become crucial tools for investors, helping them navigate the ever-changing market trends.</p>
|
| 71 |
+
<div id="three-container"></div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div class="cards-container fade-in" data-intro="Explore our services: Market Movers, Fundamentals, News, and Predictor." data-step="4">
|
| 75 |
+
<div class="card">
|
| 76 |
+
<h3>movers</h3>
|
| 77 |
+
<p>Explore real-time insights into the National Stock Exchange (NSE) with our interactive dashboard.
|
| 78 |
+
Track top gainers and losers, switch between list and graph views,
|
| 79 |
+
nd fetch historical stock data by selecting dates and searching for specific stocks. Stay informed and make data-driven decisions with ease.</p>
|
| 80 |
+
</div>
|
| 81 |
+
<div class="card">
|
| 82 |
+
<h3>Fundamentals</h3>
|
| 83 |
+
<p>The MarketMind's Fundamental page analyzes Indian Stock Market Data with options to input dates, search stocks, and view data in Table, Chart, or Both formats.
|
| 84 |
+
It features a Stock Fundamentals Analysis section with Key Ratios, Historical Trends, and Peer Comparison, displaying Valuation, Profitability, Liquidity, and Leverage ratios.
|
| 85 |
+
The page includes navigation links for Home, Services, Logout, and a theme toggle.</p>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="card">
|
| 88 |
+
<h3>News</h3>
|
| 89 |
+
<p>Latest News on Indian Markets:
|
| 90 |
+
Stay updated with the most recent developments in the Indian stock market.
|
| 91 |
+
Our news section provides insights on market trends, banking performance, policy updates, and sector-specific movements.
|
| 92 |
+
From rising indices to regulatory meetings and emerging investment opportunities, explore key stories shaping the financial landscape.</p>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="card">
|
| 95 |
+
<h3>Predictor</h3>
|
| 96 |
+
<p>Leverage the power of AI and machine learning to forecast stock price movements with our Predictor tool.
|
| 97 |
+
Input a stock and receive short-term price predictions based on historical trends, technical indicators, and sentiment analysis.
|
| 98 |
+
Designed for educational and research purposes, this tool aids in understanding market behavior.
|
| 99 |
+
Please note: predictions may vary significantly and should not be considered financial advice.
|
| 100 |
+
Use it to enhance your market intuition and strategy planning.
|
| 101 |
+
More features coming soon!</p>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<!-- Include Firebase and other necessary JS files -->
|
| 106 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.0/firebase-app.js"></script>
|
| 107 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.0/firebase-auth.js"></script>
|
| 108 |
+
<script type="module" src="{{ url_for('static', filename='js/home.js') }}"></script>
|
| 109 |
+
<script type="module" src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
| 110 |
+
|
| 111 |
+
<div style="margin-top:500px"></div>
|
| 112 |
+
|
| 113 |
+
<footer>
|
| 114 |
+
<div class="footer-content" data-intro="Find quick links, legal info, and contact details here." data-step="5">
|
| 115 |
+
<div class="footer-section">
|
| 116 |
+
<h3>Quick Links</h3>
|
| 117 |
+
<ul>
|
| 118 |
+
<li><a href="#">Home</a></li>
|
| 119 |
+
<li><a href="{{ url_for('fundamentals') }}">Fundamentals</a></li>
|
| 120 |
+
<li><a href="{{ url_for('movers') }}">movers</a></li>
|
| 121 |
+
<li><a href="{{ url_for('news') }}">News</a></li>
|
| 122 |
+
<li><a href="{{ url_for('login') }}">Login</a></li>
|
| 123 |
+
<li><a href="{{url_for('predict')}}">Predictor</a></li>
|
| 124 |
+
</ul>
|
| 125 |
+
</div>
|
| 126 |
+
<div class="footer-section">
|
| 127 |
+
<h3>Legal</h3>
|
| 128 |
+
<ul>
|
| 129 |
+
<li><a href="{{ url_for('privacy') }}">Privacy Policy</a></li>
|
| 130 |
+
<li><a href="{{ url_for('terms') }}">Terms of Service</a></li>
|
| 131 |
+
<li><a href="{{ url_for('disclaimer') }}">Disclaimer</a></li>
|
| 132 |
+
</ul>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="footer-section">
|
| 135 |
+
<h3>Contact</h3>
|
| 136 |
+
<ul>
|
| 137 |
+
<li><a href="mailto:support@stockai.com">support@stockai.com</a></li>
|
| 138 |
+
<li><a href="tel:+15551234567">+1 (555) 123-4567</a></li>
|
| 139 |
+
</ul>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="footer-section">
|
| 142 |
+
<h3>Newsletter</h3>
|
| 143 |
+
<form id="newsletter-form">
|
| 144 |
+
<input type="email" id="newsletter-email" placeholder="Enter your email" required>
|
| 145 |
+
<button type="submit">Subscribe</button>
|
| 146 |
+
</form>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="footer-bottom">
|
| 150 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 151 |
+
</div>
|
| 152 |
+
</footer>
|
| 153 |
+
|
| 154 |
+
</body>
|
| 155 |
+
</html>
|
templates/login.html
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Login Page</title>
|
| 7 |
+
<!-- Font Awesome -->
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
| 9 |
+
<!-- Custom CSS -->
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<!-- Theme Toggle Switch -->
|
| 14 |
+
<div class="theme-toggle" style="position: absolute; top: 20px; right: 30px;">
|
| 15 |
+
<label class="toggle-switch">
|
| 16 |
+
<input type="checkbox" id="theme-toggle-checkbox">
|
| 17 |
+
<span class="slider"></span>
|
| 18 |
+
</label>
|
| 19 |
+
</div>
|
| 20 |
+
<div class="container">
|
| 21 |
+
<!-- Left Section -->
|
| 22 |
+
<div class="left-section"></div>
|
| 23 |
+
|
| 24 |
+
<!-- Right Section -->
|
| 25 |
+
<div class="right-section">
|
| 26 |
+
<h1 class="title" id="form-title">Create an account</h1>
|
| 27 |
+
<p class="subtitle" id="form-subtitle">Already have an account? <a href="#" id="toggle-form">Log in</a></p>
|
| 28 |
+
|
| 29 |
+
<form id="auth-form">
|
| 30 |
+
<div class="name-fields" id="name-fields">
|
| 31 |
+
<input type="text" id="first-name" placeholder="First Name">
|
| 32 |
+
<input type="text" id="last-name" placeholder="Last Name">
|
| 33 |
+
</div>
|
| 34 |
+
<input type="email" id="email" placeholder="Email" required
|
| 35 |
+
pattern="^[a-zA-Z0-9._%+-]{2,}@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$"
|
| 36 |
+
minlength="6"
|
| 37 |
+
maxlength="40"
|
| 38 |
+
title="Enter a valid email (e.g., name@example.com)" >
|
| 39 |
+
<input type="password"
|
| 40 |
+
id="password"
|
| 41 |
+
placeholder="Enter your password"
|
| 42 |
+
required
|
| 43 |
+
minlength="8"
|
| 44 |
+
maxlength="20"
|
| 45 |
+
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,20}$"
|
| 46 |
+
title="Password must be 8-20 characters and include uppercase, lowercase, number, and special character">
|
| 47 |
+
<button type="submit" class="submit-btn" id="submit-btn">
|
| 48 |
+
Create account
|
| 49 |
+
<span class="spinner" id="loading-spinner"></span>
|
| 50 |
+
</button>
|
| 51 |
+
</form>
|
| 52 |
+
|
| 53 |
+
<div id="message" class="error-message"></div>
|
| 54 |
+
|
| 55 |
+
<div class="separator">
|
| 56 |
+
<div class="line"></div>
|
| 57 |
+
<span id="separator-text">Or Register With</span>
|
| 58 |
+
<div class="line"></div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<!-- <div class="social-box">
|
| 62 |
+
<div class="social-buttons">
|
| 63 |
+
<button type="button" id="google-btn" class="google-btn"><i class="fab fa-google"></i> Google</button>
|
| 64 |
+
<button type="button" id="apple-btn" class="apple-btn"><i class="fab fa-apple"></i> Apple</button>
|
| 65 |
+
</div>
|
| 66 |
+
</div> -->
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<!-- Firebase App (the core Firebase SDK) -->
|
| 71 |
+
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
|
| 72 |
+
<!-- Firebase Auth SDK -->
|
| 73 |
+
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js"></script>
|
| 74 |
+
|
| 75 |
+
<!-- Firebase Configuration -->
|
| 76 |
+
<script type="module" src="{{ url_for('static', filename='js/firebase-config.js') }}"></script>
|
| 77 |
+
|
| 78 |
+
<!-- Login Page JavaScript -->
|
| 79 |
+
<script type="module" src="{{ url_for('static', filename='js/login.js') }}"></script>
|
| 80 |
+
</body>
|
| 81 |
+
</html>
|
templates/movers.html
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
|
| 8 |
+
<!-- Intro.js CSS -->
|
| 9 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css">
|
| 10 |
+
<!-- Intro.js JS -->
|
| 11 |
+
<script src="https://cdn.jsdelivr.net/npm/intro.js/minified/intro.min.js"></script>
|
| 12 |
+
<script type="module"> import { requireLogin } from "{{ url_for('static', filename='js/auth.js') }}"; requireLogin(); </script>
|
| 13 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 14 |
+
<title>MarketMind - movers-Page</title>
|
| 15 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/movers.css') }}">
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
<div class="transition-overlay"></div>
|
| 19 |
+
|
| 20 |
+
<nav class="navbar">
|
| 21 |
+
<div><strong>MarketMind</strong></div>
|
| 22 |
+
<div>
|
| 23 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 24 |
+
<div class="dropdown">
|
| 25 |
+
<h4>Services</h4>
|
| 26 |
+
<div class="dropdown-content">
|
| 27 |
+
<a href="{{ url_for('movers') }}">market Movers</a>
|
| 28 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 29 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 30 |
+
<a href="{{url_for('predict')}}">Predictor</a>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<a href="{{ url_for('login') }}" id="logout">Logout</a>
|
| 34 |
+
<label class="toggle-switch">
|
| 35 |
+
<input type="checkbox" id="darkModeToggle">
|
| 36 |
+
<span class="slider">
|
| 37 |
+
<span class="toggle-circle">
|
| 38 |
+
<span class="toggle-icon">☾</span> <!-- Sun by default -->
|
| 39 |
+
</span>
|
| 40 |
+
</span>
|
| 41 |
+
</label>
|
| 42 |
+
</div>
|
| 43 |
+
</nav>
|
| 44 |
+
|
| 45 |
+
<!-- Rotating Stock Ticker -->
|
| 46 |
+
<div class="stock-ticker-indian">
|
| 47 |
+
<div class="ticker-content-indian" id="ticker-content-indian">
|
| 48 |
+
<!-- Stock data will be dynamically inserted here -->
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<!-- Main Content -->
|
| 53 |
+
<div class="video-container ">
|
| 54 |
+
<video autoplay muted loop id="bgVideo">
|
| 55 |
+
<source src="{{ url_for('static', filename='video/bg video.mp4') }}" type="video/mp4">
|
| 56 |
+
Your browser does not support the video tag.
|
| 57 |
+
</video>
|
| 58 |
+
<div class="content ">
|
| 59 |
+
<h1>Welcome to NSE Market Dashboard</h1>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<!-- Market Movers Section -->
|
| 64 |
+
<div class="market-movers" data-intro="See the top gainers and losers in the market. Switch between list and graph view." data-step="1">
|
| 65 |
+
<h2 class="indian-head">Market Movers</h2>
|
| 66 |
+
<!-- Toggle Buttons -->
|
| 67 |
+
<div class="toggle-buttons">
|
| 68 |
+
<button id="list-view-btn" class="active">List View</button>
|
| 69 |
+
<button id="graph-view-btn">Graph View</button>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="movers-container">
|
| 72 |
+
<!-- Top Gainers -->
|
| 73 |
+
<div class="movers-section">
|
| 74 |
+
<h3>Top Gainers</h3>
|
| 75 |
+
<ul id="top-gainers"></ul>
|
| 76 |
+
<div id="gainers-plotly" style="display: none; height:350px;"></div>
|
| 77 |
+
</div>
|
| 78 |
+
<!-- Top Losers -->
|
| 79 |
+
<div class="movers-section">
|
| 80 |
+
<h3>Top Losers</h3>
|
| 81 |
+
<ul id="top-losers"></ul>
|
| 82 |
+
<div id="losers-plotly" style="display: none; height:350px;"></div>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
<div class="favorites-container" data-intro="Bookmark your favorite companies and track their prices here." data-step="2">
|
| 89 |
+
<h2>My Favorite Companies</h2>
|
| 90 |
+
<div id="favorites-message" style="display:none; margin-bottom:10px;"></div>
|
| 91 |
+
<div class="favorites-table-wrapper">
|
| 92 |
+
<table id="favorites-table">
|
| 93 |
+
<thead>
|
| 94 |
+
<tr>
|
| 95 |
+
<th>Bookmark</th>
|
| 96 |
+
<th>Company</th>
|
| 97 |
+
<th>Ticker</th>
|
| 98 |
+
<th>Current Price</th>
|
| 99 |
+
</tr>
|
| 100 |
+
</thead>
|
| 101 |
+
<tbody id="favorites-tbody">
|
| 102 |
+
<!-- Rows will be inserted here -->
|
| 103 |
+
</tbody>
|
| 104 |
+
</table>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<div id="message-container" style="display: none; padding: 10px; margin: 10px 0; border-radius: 5px;"></div>
|
| 109 |
+
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<script type="module" src="{{ url_for('static', filename='js/movers.js') }}"></script>
|
| 113 |
+
<script type="module" src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
<footer>
|
| 117 |
+
<div class="footer-content">
|
| 118 |
+
<div class="footer-section">
|
| 119 |
+
<h3>Quick Links</h3>
|
| 120 |
+
<ul>
|
| 121 |
+
<li><a href="#">Home</a></li>
|
| 122 |
+
<li><a href="{{ url_for('fundamentals') }}">Fundamentals</a></li>
|
| 123 |
+
<li><a href="{{ url_for('movers') }}">Market Movers</a></li>
|
| 124 |
+
<li><a href="{{ url_for('news') }}">News</a></li>
|
| 125 |
+
<li><a href="{{ url_for('login') }}">Login</a></li>
|
| 126 |
+
<li><a href="{{url_for('predict')}}">Predictor</a></li>
|
| 127 |
+
</ul>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="footer-section">
|
| 130 |
+
<h3>Legal</h3>
|
| 131 |
+
<ul>
|
| 132 |
+
<li><a href="{{ url_for('privacy') }}">Privacy Policy</a></li>
|
| 133 |
+
<li><a href="{{ url_for('terms') }}">Terms of Service</a></li>
|
| 134 |
+
<li><a href="{{ url_for('disclaimer') }}">Disclaimer</a></li>
|
| 135 |
+
</ul>
|
| 136 |
+
</div>
|
| 137 |
+
<div class="footer-section">
|
| 138 |
+
<h3>Contact</h3>
|
| 139 |
+
<ul>
|
| 140 |
+
<li><a href="mailto:support@stockai.com">support@stockai.com</a></li>
|
| 141 |
+
<li><a href="tel:+15551234567">+1 (555) 123-4567</a></li>
|
| 142 |
+
</ul>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="footer-section">
|
| 145 |
+
<h3>Newsletter</h3>
|
| 146 |
+
<form id="newsletter-form">
|
| 147 |
+
<input type="email" id="newsletter-email" placeholder="Enter your email" required>
|
| 148 |
+
<button type="submit">Subscribe</button>
|
| 149 |
+
</form>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="footer-bottom">
|
| 153 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 154 |
+
</div>
|
| 155 |
+
</footer>
|
| 156 |
+
|
| 157 |
+
</body>
|
| 158 |
+
</html>
|
templates/news.html
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/news.css') }}">
|
| 9 |
+
<!-- Intro.js CSS -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css">
|
| 11 |
+
<!-- Intro.js JS -->
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/intro.js/minified/intro.min.js"></script>
|
| 13 |
+
<script type="module"> import { requireLogin } from "{{ url_for('static', filename='js/auth.js') }}"; requireLogin(); </script>
|
| 14 |
+
<title>MarketMind - Fundamentals</title>
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<div class="transition-overlay"></div>
|
| 18 |
+
|
| 19 |
+
<nav class="navbar">
|
| 20 |
+
<div><strong>MarketMind</strong></div>
|
| 21 |
+
<div>
|
| 22 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 23 |
+
<div class="dropdown">
|
| 24 |
+
<h4>Services</h4>
|
| 25 |
+
<div class="dropdown-content">
|
| 26 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 27 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 28 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 29 |
+
<a href="{{url_for('predict')}}">Predictor</a>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
<a href="{{ url_for('login') }}" id="logout">Logout</a>
|
| 33 |
+
<label class="toggle-switch">
|
| 34 |
+
<input type="checkbox" id="darkModeToggle">
|
| 35 |
+
<span class="slider">
|
| 36 |
+
<span class="toggle-circle">
|
| 37 |
+
<span class="toggle-icon">☾</span> <!-- Sun by default -->
|
| 38 |
+
</span>
|
| 39 |
+
</span>
|
| 40 |
+
</label>
|
| 41 |
+
</div>
|
| 42 |
+
</nav>
|
| 43 |
+
<!-- News Section -->
|
| 44 |
+
<section class="news-section" data-intro="This section displays the latest news on Indian markets." data-step="1">
|
| 45 |
+
<h2>Latest News on Indian Markets</h2>
|
| 46 |
+
<div style="text-align: center; margin-bottom: 20px;" data-intro="Click this button to refresh the news feed."
|
| 47 |
+
data-step="2">
|
| 48 |
+
<button id="refresh-news-btn" style="padding: 8px 18px; background: #4a8fdf; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
|
| 49 |
+
↻ Refresh News
|
| 50 |
+
</button>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="news-container" id="news-container" data-intro="Here you can view the latest news articles. Scroll to explore more."
|
| 53 |
+
data-step="3">
|
| 54 |
+
<!-- News articles will be dynamically inserted here -->
|
| 55 |
+
</div>
|
| 56 |
+
</section>
|
| 57 |
+
|
| 58 |
+
<script type="module" src="{{ url_for('static', filename='js/news.js') }}"></script>
|
| 59 |
+
<script type="module" src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
| 60 |
+
|
| 61 |
+
<div style="margin-top:100px"></div>
|
| 62 |
+
|
| 63 |
+
<footer>
|
| 64 |
+
<div class="footer-content">
|
| 65 |
+
<div class="footer-section">
|
| 66 |
+
<h3>Quick Links</h3>
|
| 67 |
+
<ul>
|
| 68 |
+
<li><a href="#">Home</a></li>
|
| 69 |
+
<li><a href="{{ url_for('fundamentals') }}">Fundamentals</a></li>
|
| 70 |
+
<li><a href="{{ url_for('movers') }}">Market Movers</a></li>
|
| 71 |
+
<li><a href="{{ url_for('news') }}">News</a></li>
|
| 72 |
+
<li><a href="{{ url_for('login') }}">Login</a></li>
|
| 73 |
+
<li><a href="{{url_for('predict')}}">Predictor</a></li>
|
| 74 |
+
</ul>
|
| 75 |
+
</div>
|
| 76 |
+
<div class="footer-section">
|
| 77 |
+
<h3>Legal</h3>
|
| 78 |
+
<ul>
|
| 79 |
+
<li><a href="{{ url_for('privacy') }}">Privacy Policy</a></li>
|
| 80 |
+
<li><a href="{{ url_for('terms') }}">Terms of Service</a></li>
|
| 81 |
+
<li><a href="{{ url_for('disclaimer') }}">Disclaimer</a></li>
|
| 82 |
+
</ul>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="footer-section">
|
| 85 |
+
<h3>Contact</h3>
|
| 86 |
+
<ul>
|
| 87 |
+
<li><a href="mailto:support@stockai.com">support@stockai.com</a></li>
|
| 88 |
+
<li><a href="tel:+15551234567">+1 (555) 123-4567</a></li>
|
| 89 |
+
</ul>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="footer-section">
|
| 92 |
+
<h3>Newsletter</h3>
|
| 93 |
+
<form id="newsletter-form">
|
| 94 |
+
<input type="email" id="newsletter-email" placeholder="Enter your email" required>
|
| 95 |
+
<button type="submit">Subscribe</button>
|
| 96 |
+
</form>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="footer-bottom">
|
| 100 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 101 |
+
</div>
|
| 102 |
+
</footer>
|
| 103 |
+
|
| 104 |
+
</body>
|
| 105 |
+
</html>
|
templates/predict.html
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>MarketMind - Stock Predictor</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/predict.css') }}">
|
| 9 |
+
<script type="module"> import { requireLogin } from "{{ url_for('static', filename='js/auth.js') }}"; requireLogin(); </script>
|
| 10 |
+
<!-- Intro.js CSS & JS -->
|
| 11 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css">
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/intro.js/minified/intro.min.js"></script>
|
| 13 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<div class="transition-overlay"></div>
|
| 17 |
+
<!-- Disclaimer Overlay -->
|
| 18 |
+
<div id="disclaimer-overlay" style="display:none;">
|
| 19 |
+
<div class="disclaimer-box">
|
| 20 |
+
<button id="close-disclaimer" aria-label="Close disclaimer">×</button>
|
| 21 |
+
<h2>Disclaimer</h2>
|
| 22 |
+
<ul>
|
| 23 |
+
<li>Stock market predictions shown here are based on historical data and statistical models.</li>
|
| 24 |
+
<li>They do not guarantee future results and should not be treated as financial advice.</li>
|
| 25 |
+
<li>Price differences between 100 to 600 points may occur due to market volatility.</li>
|
| 26 |
+
<li>Always consult a certified financial advisor before making investment decisions.</li>
|
| 27 |
+
</ul>
|
| 28 |
+
<div class="disclaimer-risk">
|
| 29 |
+
By using this app, you acknowledge and accept these risks.
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<!-- Navbar -->
|
| 35 |
+
<nav class="navbar">
|
| 36 |
+
<div><strong>MarketMind</strong></div>
|
| 37 |
+
<div>
|
| 38 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 39 |
+
<div class="dropdown">
|
| 40 |
+
<h4>Services</h4>
|
| 41 |
+
<div class="dropdown-content">
|
| 42 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 43 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 44 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 45 |
+
<a href="{{ url_for('predict') }}">Predictor</a>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<a href="{{ url_for('login') }}" id="logout">Logout</a>
|
| 49 |
+
<label class="toggle-switch">
|
| 50 |
+
<input type="checkbox" id="darkModeToggle">
|
| 51 |
+
<span class="slider">
|
| 52 |
+
<span class="toggle-circle">
|
| 53 |
+
<span class="toggle-icon">☀</span>
|
| 54 |
+
</span>
|
| 55 |
+
</span>
|
| 56 |
+
</label>
|
| 57 |
+
</div>
|
| 58 |
+
</nav>
|
| 59 |
+
|
| 60 |
+
<!-- Main Container -->
|
| 61 |
+
<div class="predictor-container">
|
| 62 |
+
<h1>Stock Market Predictor</h1>
|
| 63 |
+
<form class="predictor-form">
|
| 64 |
+
<div class="form-group">
|
| 65 |
+
<label for="stock-select">Choose Stock</label>
|
| 66 |
+
<select id="stock-select" data-intro="Select the stock you want to predict." data-step="1">
|
| 67 |
+
<option value="">-- Select a stock --</option>
|
| 68 |
+
</select>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="form-group">
|
| 72 |
+
<label for="prediction-date">Prediction Date</label>
|
| 73 |
+
<input type="date" id="prediction-date" name="predictionDate" min="" data-intro="Pick a prediction date (6 days to 1 month from today)." data-step="2"/>
|
| 74 |
+
</div>
|
| 75 |
+
<!-- Epochs selection (dropdown with custom input) -->
|
| 76 |
+
<div class="form-group">
|
| 77 |
+
<label for="epochs">Epochs</label>
|
| 78 |
+
<select id="epochs" name="epochs" onchange="handleEpochsChange(this)" data-intro="Choose the number of training epochs (higher = more accurate, but slower)." data-step="3">
|
| 79 |
+
<option value="100" selected>100 (default)</option>
|
| 80 |
+
<option value="120">120</option>
|
| 81 |
+
<option value="150">150</option>
|
| 82 |
+
<option value="custom">Custom...</option>
|
| 83 |
+
</select>
|
| 84 |
+
<input type="number" id="custom-epochs" min="1" max="500" style="display:none; margin-top:8px;" placeholder="Enter epochs" />
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="form-group">
|
| 88 |
+
<label>Data Source</label>
|
| 89 |
+
<div class="radio-group" data-intro="Choose the data source for prediction." data-step="4">
|
| 90 |
+
<div class="radio-option">
|
| 91 |
+
<input type="radio" id="historical-only" name="data-source" value="historical-only" checked/>
|
| 92 |
+
<label for="historical-only">Historical Data Only</label>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="radio-option">
|
| 96 |
+
<input type="radio" id="sentiment-only" name="data-source" value="news-sentiment" />
|
| 97 |
+
<label for="sentiment-only">Sentiment Analysis Only</label>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="radio-option">
|
| 100 |
+
<input type="radio" id="both" name="data-source" value="both" />
|
| 101 |
+
<label for="both">Both Historical and Sentiment</label>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
<div id="message-box" style="display:none;"></div>
|
| 106 |
+
<button type="button" id="predict-btn" class="predict-btn" data-intro="Click here to predict the stock price!" data-step="5">Predict Stock Price</button>
|
| 107 |
+
</form>
|
| 108 |
+
|
| 109 |
+
<div class="result-section" id="result-section" style="display: none;">
|
| 110 |
+
<h2>Prediction Results</h2>
|
| 111 |
+
<div class="prediction-result">
|
| 112 |
+
<h3>Predicted Price for <span id="result-stock-name"></span></h3>
|
| 113 |
+
<div class="prediction-value">₹<span id="predicted-price"></span></div>
|
| 114 |
+
<div class="news-sentiment" id="news-sentiment" style="display: none;">
|
| 115 |
+
<h3>Latest News Sentiment</h3>
|
| 116 |
+
<p id="sentiment-text">Fetching sentiment...</p>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="prediction-date" id="prediction-date-display"></div>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="chart-container" id="chart-container">
|
| 121 |
+
<div id="prediction-chart" style="height: 450px;"></div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
<div style="height: 50px;"></div>
|
| 126 |
+
<div id="loading-overlay" style="display:none;">
|
| 127 |
+
<div class="loader-container">
|
| 128 |
+
<div class="spinner"></div>
|
| 129 |
+
<div id="progress-text">Loading... 0%</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<script type="module" src="{{ url_for('static', filename='js/predict.js') }}"></script>
|
| 133 |
+
<script type="module" src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
| 134 |
+
<!-- Footer -->
|
| 135 |
+
<footer>
|
| 136 |
+
<div class="footer-content">
|
| 137 |
+
<div class="footer-section">
|
| 138 |
+
<h3>Quick Links</h3>
|
| 139 |
+
<ul>
|
| 140 |
+
<li><a href="{{ url_for('home') }}">Home</a></li>
|
| 141 |
+
<li><a href="{{ url_for('fundamentals') }}">Fundamentals</a></li>
|
| 142 |
+
<li><a href="{{ url_for('movers') }}">Market Movers</a></li>
|
| 143 |
+
<li><a href="{{ url_for('news') }}">News</a></li>
|
| 144 |
+
<li><a href="{{ url_for('login') }}">Login</a></li>
|
| 145 |
+
<li><a href="{{ url_for('predict') }}">Predictor</a></li>
|
| 146 |
+
</ul>
|
| 147 |
+
</div>
|
| 148 |
+
<div class="footer-section">
|
| 149 |
+
<h3>Legal</h3>
|
| 150 |
+
<ul>
|
| 151 |
+
<li><a href="{{ url_for('privacy') }}">Privacy Policy</a></li>
|
| 152 |
+
<li><a href="{{ url_for('terms') }}">Terms of Service</a></li>
|
| 153 |
+
<li><a href="{{ url_for('disclaimer') }}">Disclaimer</a></li>
|
| 154 |
+
</ul>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="footer-section">
|
| 157 |
+
<h3>Contact</h3>
|
| 158 |
+
<ul>
|
| 159 |
+
<li><a href="mailto:support@stockai.com">support@stockai.com</a></li>
|
| 160 |
+
<li><a href="tel:+15551234567">+1 (555) 123-4567</a></li>
|
| 161 |
+
</ul>
|
| 162 |
+
</div>
|
| 163 |
+
<div class="footer-section">
|
| 164 |
+
<h3>Newsletter</h3>
|
| 165 |
+
<form id="newsletter-form">
|
| 166 |
+
<input type="email" id="newsletter-email" placeholder="Enter your email" required>
|
| 167 |
+
<button type="submit">Subscribe</button>
|
| 168 |
+
</form>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="footer-bottom">
|
| 172 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 173 |
+
</div>
|
| 174 |
+
</footer>
|
| 175 |
+
|
| 176 |
+
<!-- Scripts -->
|
| 177 |
+
|
| 178 |
+
</body>
|
| 179 |
+
</html>
|
templates/privacy.html
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Privacy Policy - MarketMind</title>
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/legal.css') }}">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<nav class="navbar">
|
| 11 |
+
<div><strong>MarketMind</strong></div>
|
| 12 |
+
<div>
|
| 13 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 14 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 15 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 16 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 17 |
+
<a href="{{ url_for('predict') }}">Predictor</a>
|
| 18 |
+
<label class="toggle-switch">
|
| 19 |
+
<input type="checkbox" id="darkModeToggle">
|
| 20 |
+
<span class="slider">
|
| 21 |
+
<span class="toggle-circle">
|
| 22 |
+
<span class="toggle-icon">☾</span>
|
| 23 |
+
</span>
|
| 24 |
+
</span>
|
| 25 |
+
</label>
|
| 26 |
+
</div>
|
| 27 |
+
</nav>
|
| 28 |
+
<main class="legal-container">
|
| 29 |
+
<h1>Privacy Policy</h1>
|
| 30 |
+
<p>
|
| 31 |
+
At MarketMind, your privacy is important to us. We collect only the information necessary to provide our services, such as your email for newsletter subscriptions and bookmarks for your personalized experience. We do not sell or share your data with third parties except as required by law.
|
| 32 |
+
</p>
|
| 33 |
+
<h2>Information We Collect</h2>
|
| 34 |
+
<ul>
|
| 35 |
+
<li>Email address (for newsletter and authentication)</li>
|
| 36 |
+
<li>Bookmarks and preferences</li>
|
| 37 |
+
<li>Usage analytics (anonymized)</li>
|
| 38 |
+
</ul>
|
| 39 |
+
<h2>How We Use Information</h2>
|
| 40 |
+
<ul>
|
| 41 |
+
<li>To provide and improve our services</li>
|
| 42 |
+
<li>To send newsletters if you subscribe</li>
|
| 43 |
+
<li>To personalize your experience</li>
|
| 44 |
+
</ul>
|
| 45 |
+
<h2>Your Rights</h2>
|
| 46 |
+
<p>
|
| 47 |
+
You can request deletion of your data at any time by contacting us at <a href="mailto:support@stockai.com">support@stockai.com</a>.
|
| 48 |
+
</p>
|
| 49 |
+
<p>
|
| 50 |
+
For more details, please contact us.
|
| 51 |
+
</p>
|
| 52 |
+
</main>
|
| 53 |
+
<footer>
|
| 54 |
+
<div class="footer-bottom">
|
| 55 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 56 |
+
</div>
|
| 57 |
+
</footer>
|
| 58 |
+
<script type="module" src="{{ url_for('static', filename='js/legal.js') }}"></script>
|
| 59 |
+
</body>
|
| 60 |
+
</html>
|
templates/terms.html
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Terms of Service - MarketMind</title>
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/legal.css') }}">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<nav class="navbar">
|
| 11 |
+
<div><strong>MarketMind</strong></div>
|
| 12 |
+
<div>
|
| 13 |
+
<a href="{{ url_for('home') }}">Home</a>
|
| 14 |
+
<a href="{{ url_for('fundamentals') }}">Fundamentals</a>
|
| 15 |
+
<a href="{{ url_for('movers') }}">Market Movers</a>
|
| 16 |
+
<a href="{{ url_for('news') }}">News</a>
|
| 17 |
+
<a href="{{ url_for('predict') }}">Predictor</a>
|
| 18 |
+
<label class="toggle-switch">
|
| 19 |
+
<input type="checkbox" id="darkModeToggle">
|
| 20 |
+
<span class="slider">
|
| 21 |
+
<span class="toggle-circle">
|
| 22 |
+
<span class="toggle-icon">☾</span>
|
| 23 |
+
</span>
|
| 24 |
+
</span>
|
| 25 |
+
</label>
|
| 26 |
+
</div>
|
| 27 |
+
</nav>
|
| 28 |
+
<main class="legal-container">
|
| 29 |
+
<h1>Terms of Service</h1>
|
| 30 |
+
<p>
|
| 31 |
+
By using MarketMind, you agree to these terms. Our services are for informational and educational purposes only and do not constitute financial advice. You are responsible for your investment decisions.
|
| 32 |
+
</p>
|
| 33 |
+
<h2>Use of Service</h2>
|
| 34 |
+
<ul>
|
| 35 |
+
<li>Do not misuse or attempt to disrupt our services.</li>
|
| 36 |
+
<li>Do not use our content for unlawful purposes.</li>
|
| 37 |
+
</ul>
|
| 38 |
+
<h2>Limitation of Liability</h2>
|
| 39 |
+
<p>
|
| 40 |
+
MarketMind is not liable for any losses or damages arising from the use of our platform.
|
| 41 |
+
</p>
|
| 42 |
+
<h2>Changes to Terms</h2>
|
| 43 |
+
<p>
|
| 44 |
+
We may update these terms from time to time. Continued use of the service constitutes acceptance of the new terms.
|
| 45 |
+
</p>
|
| 46 |
+
</main>
|
| 47 |
+
<footer>
|
| 48 |
+
<div class="footer-bottom">
|
| 49 |
+
<p>© 2025 MarketMind. All rights reserved.</p>
|
| 50 |
+
</div>
|
| 51 |
+
</footer>
|
| 52 |
+
<script type="module" src="{{ url_for('static', filename='js/legal.js') }}"></script>
|
| 53 |
+
</body>
|
| 54 |
+
</html>
|