AronWolverine commited on
Commit
56bd117
·
1 Parent(s): e70a41e
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. .gitattributes +3 -0
  3. .gitignore +9 -0
  4. Dockerfile +35 -0
  5. __pycache__/app.cpython-312.pyc +0 -0
  6. __pycache__/lstm_n_pipeline.cpython-312.pyc +0 -0
  7. __pycache__/lstm_pipeline.cpython-312.pyc +0 -0
  8. app.py +397 -0
  9. requirements.txt +13 -0
  10. static/css/fundamentals.css +1160 -0
  11. static/css/legal.css +140 -0
  12. static/css/login.css +322 -0
  13. static/css/movers.css +964 -0
  14. static/css/news.css +480 -0
  15. static/css/predict.css +466 -0
  16. static/css/styles.css +652 -0
  17. static/images/bg.png +3 -0
  18. static/js/auth.js +44 -0
  19. static/js/firebase-config.js +92 -0
  20. static/js/fundamentals.js +695 -0
  21. static/js/home.js +235 -0
  22. static/js/legal.js +22 -0
  23. static/js/login.js +295 -0
  24. static/js/movers.js +477 -0
  25. static/js/news.js +139 -0
  26. static/js/newsletter.js +34 -0
  27. static/js/predict.js +574 -0
  28. static/models/coins.glb +3 -0
  29. static/models/finbert_sentiment089/config.json +35 -0
  30. static/models/finbert_sentiment089/model.safetensors +3 -0
  31. static/models/finbert_sentiment089/special_tokens_map.json +37 -0
  32. static/models/finbert_sentiment089/tokenizer_config.json +59 -0
  33. static/models/finbert_sentiment089/vocab.txt +0 -0
  34. static/pipelines/__pycache__/lstm_n_pipeline.cpython-312.pyc +0 -0
  35. static/pipelines/__pycache__/lstm_pipeline.cpython-312.pyc +0 -0
  36. static/pipelines/lstm_n_pipeline.py +239 -0
  37. static/pipelines/lstm_pipeline.py +219 -0
  38. static/video/bg video.mp4 +3 -0
  39. t.txt +242 -0
  40. templates/404.html +150 -0
  41. templates/base.html +23 -0
  42. templates/disclaimer.html +44 -0
  43. templates/fundamentals.html +251 -0
  44. templates/home.html +155 -0
  45. templates/login.html +81 -0
  46. templates/movers.html +158 -0
  47. templates/news.html +105 -0
  48. templates/predict.html +179 -0
  49. templates/privacy.html +60 -0
  50. 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

  • SHA256: 514c4178b3c4027d5f0fa2c3cd1a5af8dbc2d51912fd34fdefb9b5c6593177f6
  • Pointer size: 131 Bytes
  • Size of remote file: 523 kB
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 = '&#9790;'; // Moon
19
+ toggleIcon.style.color = "#4a8fdf";
20
+ } else {
21
+ toggleIcon.innerHTML = '&#9728;'; // 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 = '&#9790;'; // Moon
11
+ } else {
12
+ toggleIcon.innerHTML = '&#9728;'; // 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 = '&#9790;'; // Moon
13
+ } else {
14
+ toggleIcon.innerHTML = '&#9728;'; // 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 = '&#9790;'; // Moon
19
+ } else {
20
+ toggleIcon.innerHTML = '&#9728;'; // 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 += ' &nbsp; ';
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 = '&#9790;'; // Moon
8
+ } else {
9
+ toggleIcon.innerHTML = '&#9728;'; // 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 = '&#9790;'; // Moon
67
+ toggleIcon.style.color = "#4a8fdf";
68
+ } else {
69
+ toggleIcon.innerHTML = '&#9728;'; // 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 = '&#9790;'; // Moon
174
+ } else {
175
+ toggleIcon.innerHTML = '&#9728;'; // 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">&#9790;</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">&#9790;</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">&#9790;</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">&#9790;</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">&#9790;</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
+ &#x21bb; 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">&times;</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">&#9728;</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">&#9790;</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">&#9790;</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>