├── src ├── components │ ├── ThemeDebug.jsx │ ├── images │ │ └── bg.png │ ├── Layout.jsx │ ├── AuthContext.js │ ├── ThemeToggle.jsx │ ├── data │ │ └── stockData.json │ ├── ErrorBoundary.js │ ├── StockMetricsCard.jsx │ ├── Watchlist.jsx │ ├── firebase.js │ ├── ThemeToggle.css │ ├── ThemeContext.js │ ├── Watchlist.module.css │ ├── about-professional.css │ ├── Login.js │ ├── Login.css │ ├── BackToTopBtn.jsx │ ├── Signup.css │ ├── Footer.css │ ├── ContactForm.module.css │ ├── Prediction.jsx │ ├── StockList.jsx │ ├── About.jsx │ ├── Signup.js │ ├── ContactForm.jsx │ ├── Header.jsx │ ├── Header.css │ ├── StockList.module.css │ ├── SentimentChart.jsx │ └── Footer.jsx ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── utils │ ├── firebaseWatchlist.js │ └── watchlistManager.js ├── styles │ ├── theme-toggle.css │ └── global.css └── App.js ├── backend ├── __init__.py ├── utils │ ├── __init__.py │ └── cache.py ├── routes │ ├── __init__.py │ └── stock_routes.py ├── services │ ├── __init__.py │ ├── stock_predict.py │ └── stock_service.py ├── tf.keras ├── config.py ├── tests │ ├── conftest.py │ ├── test_cache_utils.py │ └── test_integration_cache.py ├── airflow_dags │ ├── airflow │ │ ├── .env.example │ │ └── README.md │ └── fetch_stock_data_dag.py ├── generate_csvs.py ├── app.py └── requirements.txt ├── Images ├── bg.png ├── home.png ├── logo.png ├── main.png ├── dataline.png └── prediction.png ├── public ├── icon.png ├── robots.txt ├── manifest.json └── index.html ├── .github ├── ISSUE_TEMPLATE │ ├── enhancement.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── auto-comment.yml └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── package.json ├── start-dev.js ├── CONTRIBUTION.md ├── suppress-webpack-warnings.js ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── README.md ├── SETUP.md └── .gitignore /src/components/ThemeDebug.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes the app directory a Python package 2 | -------------------------------------------------------------------------------- /backend/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility package for backend helpers (cache, etc.) 2 | -------------------------------------------------------------------------------- /Images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/Images/bg.png -------------------------------------------------------------------------------- /backend/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes the routes directory a Python package 2 | -------------------------------------------------------------------------------- /Images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/Images/home.png -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/Images/logo.png -------------------------------------------------------------------------------- /Images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/Images/main.png -------------------------------------------------------------------------------- /backend/services/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes the services directory a Python package 2 | -------------------------------------------------------------------------------- /backend/tf.keras: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/backend/tf.keras -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/public/icon.png -------------------------------------------------------------------------------- /Images/dataline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/Images/dataline.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /Images/prediction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/Images/prediction.png -------------------------------------------------------------------------------- /src/components/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SrigadaAkshayKumar/stock/HEAD/src/components/images/bg.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration module for environment-specific settings used across the application. 3 | """ 4 | 5 | import os 6 | 7 | class Config: 8 | # Directory where stock CSV data is stored (can be overridden by environment variable) 9 | DATA_DIR = os.getenv("DATA_DIR", "data") -------------------------------------------------------------------------------- /backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # Ensure 'backend' package root is on sys.path for tests 5 | THIS_DIR = os.path.dirname(__file__) 6 | BACKEND_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..')) 7 | if BACKEND_ROOT not in sys.path: 8 | sys.path.insert(0, BACKEND_ROOT) 9 | -------------------------------------------------------------------------------- /backend/airflow_dags/airflow/.env.example: -------------------------------------------------------------------------------- 1 | # Existing airflow envs here 2 | 3 | # Backend cache configuration (used by Flask app; copy to backend .env) 4 | CACHE_ENABLED=true 5 | REDIS_URL=redis://localhost:6379/0 6 | CACHE_TTL_STOCK=900 7 | CACHE_TTL_PRED=3600 8 | DATA_DIR=./data 9 | CATALOG_DB=./data/catalog.sqlite 10 | MARKET_TZ=Asia/Kolkata -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "StockAnalyzer", 3 | "name": "Stock Analyzer", 4 | "icons": [ 5 | { 6 | "src": "icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "icon.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /backend/airflow_dags/airflow/README.md: -------------------------------------------------------------------------------- 1 | # Airflow (Local Dev) 2 | 3 | - DAGs live in `../airflow_dags/`. 4 | - The `fetch_stock_data` DAG is **paused by default**; enable only after 18 Aug EOD per maintainer. 5 | - No remote API calls; only works with local `./data/*.csv`. 6 | 7 | ## Local Run (optional) 8 | python -m venv .venv && source .venv/bin/activate 9 | pip install "apache-airflow==2.9.3" --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-2.9.3/constraints-3.10.txt" -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /backend/generate_csvs.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import yfinance as yf 4 | import os 5 | 6 | # Make sure the data folder exists 7 | os.makedirs("data", exist_ok=True) 8 | 9 | tickers = ['TCS.NS', 'INFY.NS', 'RELIANCE.NS', 'TCS.BO', 'INFY.BO', 'RELIANCE.BO', 'SBIN.BO', 'HDFCBANK.BO', 'ITC.BO', 'LT.BO', 'BHARTIARTL.BO', 'KOTAKBANK.BO', 'AXISBANK.BO', 'HINDUNILVR.BO', 'BAJFINANCE.BO', 'MARUTI.BO', 'SUNPHARMA.BO', 'WIPRO.BO'] 10 | 11 | for ticker in tickers: 12 | print(f"Downloading {ticker}...") 13 | data = yf.download(ticker, period='5y', interval='1d') 14 | csv_path = f'data/{ticker}.csv' 15 | data.to_csv(csv_path) 16 | print(f"Saved: {csv_path}") -------------------------------------------------------------------------------- /backend/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | import os 4 | from routes.stock_routes import stock_routes 5 | from dotenv import load_dotenv 6 | 7 | # Load env 8 | load_dotenv() 9 | 10 | app = Flask(__name__) 11 | CORS(app, resources={r"/*": {"origins": ["http://localhost:3000", "https://aistockanalyzer.onrender.com"]}}) 12 | 13 | # Home route: 14 | @app.route('/') 15 | def home(): 16 | return "Welcome to the Stock Analysis API" 17 | 18 | # Register blueprint 19 | app.register_blueprint(stock_routes, url_prefix="/api") 20 | 21 | if __name__ == '__main__': 22 | port = int(os.environ.get('PORT', 10000)) 23 | app.run(host='0.0.0.0', port=port) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code/Design Enhancement 3 | about: Suggest improvements to existing code or UI/UX 4 | title: "[ENHANCEMENT] " 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Enhancement Details 10 | 11 | "Describe the current behavior and how you think it can be improved." 12 | 13 | ## Reason 14 | 15 | Why is this change needed? What’s the benefit? 16 | 17 | ## Select Enhancement part 18 | 19 | select Frontend or backend 20 | 21 | - [ ] Frontend 22 | - [ ] Backend 23 | 24 | ## Visual Comparison (Optional) 25 | 26 | Before / After screenshots, if applicable. 27 | 28 | ## Additional Context 29 | 30 | Any other information you want to provide. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new idea or functionality 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Describe the Feature 10 | 11 | "A clear and concise description of the feature you'd like to see." 12 | 13 | ## Use Case / Motivation 14 | 15 | Why is this feature useful? What problem does it solve? 16 | 17 | ## Where you want to add this feature 18 | 19 | select Frontend or backend 20 | 21 | - [ ] Frontend 22 | - [ ] Backend 23 | 24 | ## Screenshots or Examples 25 | 26 | (Optional) Include visuals or references for inspiration. 27 | 28 | ## Demo Video 29 | 30 | (Optional but helpful) Link to a short video describing the feature. 31 | -------------------------------------------------------------------------------- /src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import { ThemeProvider } from "./ThemeContext"; 4 | import Header from "./Header"; 5 | import Footer from "./Footer"; 6 | import "../styles/global.css"; // global theme tokens 7 | 8 | 9 | export default function Layout() { 10 | /* Layout owns ThemeProvider so theme is wired for all pages inside the layout */ 11 | return ( 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | ); 22 | } -------------------------------------------------------------------------------- /src/components/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | import { onAuthStateChanged } from "firebase/auth"; 4 | import { auth } from "./firebase"; 5 | 6 | const AuthContext = createContext(); 7 | 8 | export const AuthProvider = ({ children }) => { 9 | const [user, setUser] = useState(null); 10 | const [loading, setLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | const unsubscribe = onAuthStateChanged(auth, (user) => { 14 | setUser(user); 15 | setLoading(false); 16 | }); 17 | 18 | return () => unsubscribe(); 19 | }, []); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export const useAuth = () => useContext(AuthContext); 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report something that's not working 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ## Description 10 | 11 | "Mention a clear and concise description of what the bug is." 12 | 13 | ## Where is the issue? 14 | 15 | select Frontend or backend 16 | 17 | - [ ] Frontend 18 | - [ ] Backend 19 | 20 | ## Steps to Reproduce 21 | 22 | Example: 23 | 24 | 1. Go to '...' 25 | 2. Click on '...' 26 | 3. Scroll down to '...' 27 | 4. See error 28 | 29 | ## Screenshots 30 | 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | ## Environment 34 | 35 | - OS: [e.g., Windows 11, macOS Monterey] 36 | - Browser: [e.g., Chrome, Firefox] 37 | - Device: [e.g., Desktop, Mobile] 38 | 39 | ## Additional Notes 40 | 41 | Any other context about the problem. 42 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.13.4 2 | blinker==1.9.0 3 | certifi==2025.8.3 4 | cffi==1.17.1 5 | charset-normalizer==3.4.2 6 | click==8.2.1 7 | curl_cffi==0.12.0 8 | dotenv 9 | Flask==3.1.1 10 | flask-cors==6.0.1 11 | frozendict==2.4.6 12 | idna==3.10 13 | itsdangerous==2.2.0 14 | Jinja2==3.1.6 15 | joblib==1.5.1 16 | MarkupSafe==3.0.2 17 | multitasking==0.0.12 18 | narwhals==2.0.1 19 | numpy==2.3.2 20 | packaging==25.0 21 | pandas==2.3.1 22 | peewee==3.18.2 23 | platformdirs==4.3.8 24 | plotly==6.2.0 25 | protobuf==6.31.1 26 | pycparser==2.22 27 | python-dateutil==2.9.0.post0 28 | pytz==2025.2 29 | requests==2.32.4 30 | scikit-learn==1.7.1 31 | scipy==1.16.1 32 | six==1.17.0 33 | soupsieve==2.7 34 | threadpoolctl==3.6.0 35 | typing_extensions==4.14.1 36 | tzdata==2025.2 37 | urllib3==2.5.0 38 | websockets==15.0.1 39 | Werkzeug==3.1.3 40 | yfinance==0.2.65 41 | textblob -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Srigada Akshay Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/firebaseWatchlist.js: -------------------------------------------------------------------------------- 1 | import { getFirestore, doc, getDoc, setDoc, updateDoc } from "../components/firebase"; 2 | 3 | const db = getFirestore(); 4 | 5 | export const addToWatchlist = async (userId, stock) => { 6 | const docRef = doc(db, 'watchlists', userId); 7 | const docSnap = await getDoc(docRef); 8 | let currentList = []; 9 | 10 | if (docSnap.exists()) { 11 | currentList = docSnap.data().stocks || []; 12 | } 13 | 14 | const alreadyAdded = currentList.find((s) => s.symbol === stock.symbol); 15 | if (!alreadyAdded) { 16 | currentList.push(stock); 17 | await setDoc(docRef, { stocks: currentList }); 18 | } 19 | }; 20 | 21 | export const removeFromWatchlist = async (userId, symbol) => { 22 | const docRef = doc(db, 'watchlists', userId); 23 | const docSnap = await getDoc(docRef); 24 | if (!docSnap.exists()) return; 25 | 26 | const currentList = docSnap.data().stocks || []; 27 | const updatedList = currentList.filter((s) => s.symbol !== symbol); 28 | await updateDoc(docRef, { stocks: updatedList }); 29 | }; 30 | 31 | export const getUserWatchlist = async (userId) => { 32 | const docRef = doc(db, 'watchlists', userId); 33 | const docSnap = await getDoc(docRef); 34 | return docSnap.exists() ? docSnap.data().stocks : []; 35 | }; 36 | -------------------------------------------------------------------------------- /backend/airflow_dags/fetch_stock_data_dag.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import os 3 | from pathlib import Path 4 | from airflow import DAG 5 | from airflow.operators.empty import EmptyOperator 6 | from airflow.operators.python import PythonOperator 7 | 8 | DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) 9 | 10 | def validate_seed_csvs(): 11 | DATA_DIR.mkdir(parents=True, exist_ok=True) 12 | print(f"[validate_seed_csvs] OK. Data dir: {DATA_DIR.resolve()}") 13 | 14 | def ingest_to_catalog(): 15 | print("[ingest_to_catalog] Stub. CSV -> curated output.") 16 | 17 | DEFAULT_ARGS = { 18 | "owner": "asr", 19 | "depends_on_past": False, 20 | "retries": 1, 21 | "retry_delay": timedelta(minutes=5), 22 | } 23 | 24 | with DAG( 25 | dag_id="fetch_stock_data", 26 | description="Validate & curate local stock CSVs (no yfinance)", 27 | default_args=DEFAULT_ARGS, 28 | start_date=datetime(2025, 8, 1), 29 | schedule="0 18 * * 1-5", # 6PM Mon–Fri 30 | catchup=False, 31 | is_paused_upon_creation=True, # stays paused until 18th 32 | ) as dag: 33 | 34 | start = EmptyOperator(task_id="start") 35 | validate = PythonOperator(task_id="validate_seed_csvs", python_callable=validate_seed_csvs) 36 | ingest = PythonOperator(task_id="ingest_to_catalog", python_callable=ingest_to_catalog) 37 | end = EmptyOperator(task_id="end") 38 | 39 | start >> validate >> ingest >> end -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stock", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/core": "^7.26.8", 7 | "@babel/preset-env": "^7.26.8", 8 | "@testing-library/jest-dom": "^5.17.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.7.9", 11 | "babel-loader": "^9.2.1", 12 | "firebase": "^11.10.0", 13 | "framer-motion": "^12.23.9", 14 | "plotly.js": "^3.1.0", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "react-firebase-hooks": "^5.1.1", 18 | "react-icons": "^5.5.0", 19 | "react-plotly.js": "^2.6.0", 20 | "react-router-dom": "^7.1.5", 21 | "react-spinners": "^0.15.0", 22 | "react-toastify": "^11.0.5", 23 | "web-vitals": "^2.1.4" 24 | }, 25 | "scripts": { 26 | "start": "node start-dev.js", 27 | "start:original": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 52 | "react-scripts": "^5.0.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /start-dev.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const path = require('path'); 3 | 4 | // Override console.warn to filter out specific warnings 5 | const originalWarn = console.warn; 6 | console.warn = function(...args) { 7 | const message = args.join(' '); 8 | if ( 9 | message.includes('onAfterSetupMiddleware') || 10 | message.includes('onBeforeSetupMiddleware') || 11 | message.includes("'onAfterSetupMiddleware' option is deprecated") || 12 | message.includes("'onBeforeSetupMiddleware' option is deprecated") 13 | ) { 14 | return; // Suppress these specific warnings 15 | } 16 | return originalWarn.apply(console, arguments); 17 | }; 18 | 19 | // Override process.emit to catch deprecation warnings 20 | const originalEmit = process.emit; 21 | process.emit = function(name, data, ...args) { 22 | if ( 23 | name === 'warning' && 24 | typeof data === 'object' && 25 | data.name === 'DeprecationWarning' && 26 | (data.message.includes('onAfterSetupMiddleware') || 27 | data.message.includes('onBeforeSetupMiddleware')) 28 | ) { 29 | return false; // Suppress the warning 30 | } 31 | return originalEmit.apply(process, arguments); 32 | }; 33 | 34 | // Start react-scripts - Use the actual JS file on Windows 35 | const reactScriptsPath = path.join(__dirname, 'node_modules', 'react-scripts', 'bin', 'react-scripts.js'); 36 | const child = spawn('node', [reactScriptsPath, 'start'], { 37 | stdio: 'inherit', 38 | env: { 39 | ...process.env, 40 | GENERATE_SOURCEMAP: 'false' 41 | } 42 | }); 43 | 44 | child.on('exit', (code) => { 45 | process.exit(code); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useTheme } from './ThemeContext'; 3 | import { LuMoon, LuSun } from 'react-icons/lu'; 4 | import './ThemeToggle.css'; 5 | 6 | const ThemeToggle = ({ className = '' }) => { 7 | const { theme, toggleTheme } = useTheme(); 8 | const [mounted, setMounted] = useState(false); 9 | const isDark = theme === 'dark'; 10 | 11 | // Only render on client-side to avoid hydration mismatch 12 | useEffect(() => { 13 | setMounted(true); 14 | }, []); 15 | 16 | const handleToggle = () => { 17 | console.log('Theme toggle clicked. Current theme:', theme); 18 | toggleTheme(); 19 | }; 20 | 21 | if (!mounted) { 22 | return ( 23 | 28 | ); 29 | } 30 | 31 | return ( 32 | 47 | ); 48 | }; 49 | 50 | export default ThemeToggle; 51 | -------------------------------------------------------------------------------- /.github/workflows/auto-comment.yml: -------------------------------------------------------------------------------- 1 | name: Auto Comment Bot 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request_target: # <-- Changed from pull_request for correct permissions on forked PRs 7 | types: [opened] 8 | 9 | permissions: # <-- Added recommended permissions for commenting 10 | issues: write 11 | pull-requests: write 12 | contents: read 13 | 14 | jobs: 15 | comment: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Add Comment 19 | uses: actions/github-script@v7 20 | with: 21 | script: | 22 | const issueOrPR = context.payload.issue || context.payload.pull_request; 23 | const isPR = !!context.payload.pull_request; 24 | 25 | const message = isPR 26 | ? `Thanks for opening this Pull Request! A maintainer will review it shortly. Please make sure you've followed all [contribution guidelines](https://github.com/SrigadaAkshayKumar/stock/blob/main/CONTRIBUTION.md).\n\n Join our [Discord Community](https://discord.gg/ypQSaPbsDv) for discussions, questions, and real-time collaboration!` 27 | : `Thanks for addressing the issue! We’ll review it and get back to you shortly. If it’s a feature request, consider adding screenshots or a demo video for clarity.\n\n Join our [Discord Community](https://discord.gg/ypQSaPbsDv) for discussions, questions, and real-time collaboration!`; 28 | 29 | github.rest.issues.createComment({ 30 | issue_number: issueOrPR.number, 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | body: message 34 | }); 35 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines – Stock Analyzer 2 | 3 | Thank you for showing interest in contributing to **Stock Analyzer** 4 | We're building a powerful stock prediction and visualization tool, and your help is appreciated. 5 | 6 | ## Getting Started with Contributions 7 | 8 | Before you begin coding, please follow these steps: 9 | 10 | 1. **Check the [Issues Section](https://github.com/SrigadaAkshayKumar/stock/issues)** 11 | 12 | - Look for issues labeled `good first issue`, `bug`, or `enhancement`. 13 | - Try solving one that interests you. 14 | 15 | 2. **Want to Suggest a New Feature or Improvement?** 16 | - Please raise a new issue with: 17 | - A **clear description** of what the feature or enhancement is. 18 | - Add **screenshots** if applicable. 19 | - Include a **demo video** (optional but helpful for understanding). 20 | - Wait for approval/comment from the maintainers before creating a pull request (PR). 21 | 22 | ## How to Contribute 23 | 24 | 1. **Set Up the Project Locally** 25 | 26 | Follow the guide in [`SETUP.md`](./SETUP.md) to run the backend and frontend on your system. 27 | 28 | 2. **Create a New Branch** 29 | 30 | ```bash 31 | git checkout -b feature/your-feature-name 32 | ``` 33 | 34 | 3. **Make Your Changes** 35 | 36 | - Ensure your code is fully functional and tested locally. 37 | 38 | - Maintain UI/UX consistency if working on the frontend. 39 | 40 | - Add comments where necessary and follow naming conventions. 41 | 42 | 4. **Commit Your Changes** 43 | 44 | ```bash 45 | git commit -m "Add: your feature description" 46 | ``` 47 | 48 | 5. **Push and Create a Pull Request** 49 | 50 | ```bash 51 | git push origin feature/your-feature-name 52 | ``` 53 | 54 | Then go to GitHub and open a Pull Request (PR) targeting the main branch. 55 | -------------------------------------------------------------------------------- /backend/tests/test_cache_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from utils.cache import cache_key_from, set_cache, get_cache, is_cache_enabled 4 | 5 | 6 | def test_cache_key_from_stable(): 7 | key1 = cache_key_from('/api/stock/RELIANCE', {"chart_period": "1mo", "table_period": "1mo"}) 8 | key2 = cache_key_from('/api/stock/RELIANCE', {"table_period": "1mo", "chart_period": "1mo"}) 9 | assert key1 == key2 10 | assert key1.startswith("resp:/api/stock/RELIANCE?") 11 | 12 | 13 | def test_memory_cache_fallback(monkeypatch): 14 | monkeypatch.setenv("CACHE_ENABLED", "true") 15 | # Ensure redis is not used by setting invalid URL so code falls back 16 | monkeypatch.setenv("REDIS_URL", "redis://invalid:0/0") 17 | 18 | # Re-import to apply env; in real suite, design for DI. Here keep it simple. 19 | import importlib 20 | cache_mod = importlib.import_module('utils.cache') 21 | importlib.reload(cache_mod) 22 | 23 | key = cache_mod.cache_key_from('/api/stock/TCS', {"chart_period": "1d", "table_period": "1d"}) 24 | payload = json.dumps({"ok": True}) 25 | # Use a TTL that comfortably exceeds potential fallback delays 26 | assert cache_mod.set_cache(key, payload, ttl=5) is True 27 | assert cache_mod.get_cache(key) == payload 28 | 29 | 30 | def test_cache_miss_then_hit(monkeypatch): 31 | import importlib 32 | monkeypatch.setenv("CACHE_ENABLED", "true") 33 | monkeypatch.setenv("REDIS_URL", "redis://invalid:0/0") 34 | cache_mod = importlib.import_module('utils.cache') 35 | importlib.reload(cache_mod) 36 | 37 | key = cache_mod.cache_key_from('/api/stock/INFY', {"chart_period": "1mo", "table_period": "1mo"}) 38 | assert cache_mod.get_cache(key) is None # miss 39 | payload = json.dumps({"ok": 1}) 40 | cache_mod.set_cache(key, payload, ttl=10) 41 | assert cache_mod.get_cache(key) == payload # hit -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Stock Analyzer 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/data/stockData.json: -------------------------------------------------------------------------------- 1 | { 2 | "BSE": [ 3 | { "symbol": "RELIANCE.BO", "name": "Reliance Industries" }, 4 | { "symbol": "TCS.BO", "name": "Tata Consultancy Services" }, 5 | { "symbol": "INFY.BO", "name": "Infosys" }, 6 | { "symbol": "HDFCBANK.BO", "name": "HDFC Bank" }, 7 | { "symbol": "ITC.BO", "name": "ITC Limited" }, 8 | { "symbol": "SBIN.BO", "name": "State Bank of India" }, 9 | { "symbol": "LT.BO", "name": "Larsen & Toubro" }, 10 | { "symbol": "BHARTIARTL.BO", "name": "Bharti Airtel" }, 11 | { "symbol": "KOTAKBANK.BO", "name": "Kotak Mahindra Bank" }, 12 | { "symbol": "AXISBANK.BO", "name": "Axis Bank" }, 13 | { "symbol": "HINDUNILVR.BO", "name": "Hindustan Unilever" }, 14 | { "symbol": "BAJFINANCE.BO", "name": "Bajaj Finance" }, 15 | { "symbol": "MARUTI.BO", "name": "Maruti Suzuki" }, 16 | { "symbol": "SUNPHARMA.BO", "name": "Sun Pharma" }, 17 | { "symbol": "WIPRO.BO", "name": "Wipro" } 18 | ], 19 | "NSE": [ 20 | { "symbol": "RELIANCE.NS", "name": "Reliance Industries" }, 21 | { "symbol": "TCS.NS", "name": "Tata Consultancy Services" }, 22 | { "symbol": "INFY.NS", "name": "Infosys" }, 23 | { "symbol": "HDFCBANK.NS", "name": "HDFC Bank" }, 24 | { "symbol": "ITC.NS", "name": "ITC Limited" }, 25 | { "symbol": "SBIN.NS", "name": "State Bank of India" }, 26 | { "symbol": "LT.NS", "name": "Larsen & Toubro" }, 27 | { "symbol": "BHARTIARTL.NS", "name": "Bharti Airtel" }, 28 | { "symbol": "KOTAKBANK.NS", "name": "Kotak Mahindra Bank" }, 29 | { "symbol": "AXISBANK.NS", "name": "Axis Bank" }, 30 | { "symbol": "HINDUNILVR.NS", "name": "Hindustan Unilever" }, 31 | { "symbol": "BAJFINANCE.NS", "name": "Bajaj Finance" }, 32 | { "symbol": "MARUTI.NS", "name": "Maruti Suzuki" }, 33 | { "symbol": "SUNPHARMA.NS", "name": "Sun Pharma" }, 34 | { "symbol": "WIPRO.NS", "name": "Wipro" } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false, error: null, errorInfo: null }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | return { hasError: true }; 11 | } 12 | 13 | componentDidCatch(error, errorInfo) { 14 | this.setState({ 15 | error: error, 16 | errorInfo: errorInfo 17 | }); 18 | console.error('ErrorBoundary caught an error:', error, errorInfo); 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | return ( 24 |
33 |

🚨 Something went wrong!

34 |
35 | Error Details (Click to expand) 36 |

Error: {this.state.error && this.state.error.toString()}

37 |

Error Info:

38 |
{this.state.errorInfo.componentStack}
39 |
40 | 54 |
55 | ); 56 | } 57 | 58 | return this.props.children; 59 | } 60 | } 61 | 62 | export default ErrorBoundary; 63 | -------------------------------------------------------------------------------- /src/components/StockMetricsCard.jsx: -------------------------------------------------------------------------------- 1 | // StockMetricsCard component to display stock metrics 2 | export const StockMetricsCard = ({ 3 | open, 4 | close, 5 | high, 6 | low, 7 | previousClose 8 | }) => { 9 | 10 | const PrevClose = previousClose ?? "N/A"; 11 | const Open = open ?? "N/A"; 12 | const Close = close ?? "N/A"; 13 | const Low = low ?? "N/A"; 14 | const High = high ?? "N/A"; 15 | 16 | // Formatting the value 17 | const formatValue = (value) => `$${Number(value).toFixed(2)}`; 18 | 19 | // Arrow indicator to check trends 20 | const getTrend = (current, compareWith) => { 21 | if (current > compareWith) { 22 | return ; 23 | } else if (current < compareWith) { 24 | return ; 25 | } 26 | return ; 27 | }; 28 | 29 | return ( 30 |
31 |
32 | {["OPEN", "CLOSE", "LOW", "HIGH"].map((label) => ( 33 |
34 | {label} 35 |
36 | ))} 37 |
38 | 39 |
40 | 41 | {/* OPEN */} 42 |
43 | {formatValue(Open)} 44 | {getTrend(Open, PrevClose)} 45 |
46 | 47 | {/* CLOSE */} 48 |
49 | {formatValue(Close)} 50 | {getTrend(Close, Open)} 51 |
52 | 53 | {/* LOW */} 54 |
55 | {formatValue(Low)} 56 |
57 | 58 | {/* HIGH */} 59 |
60 | {formatValue(High)} 61 |
62 |
63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /suppress-webpack-warnings.js: -------------------------------------------------------------------------------- 1 | // suppress-webpack-warnings.js 2 | // This script suppresses the webpack-dev-server deprecation warnings 3 | // until Create React App updates to use the new setupMiddlewares option 4 | 5 | // Patch the process.emitWarning method 6 | const originalEmitWarning = process.emitWarning; 7 | process.emitWarning = function(warning, type, code, ctor) { 8 | if ( 9 | type === 'DeprecationWarning' && 10 | (warning.includes('onAfterSetupMiddleware') || 11 | warning.includes('onBeforeSetupMiddleware') || 12 | warning.includes("'onAfterSetupMiddleware' option is deprecated") || 13 | warning.includes("'onBeforeSetupMiddleware' option is deprecated")) 14 | ) { 15 | // Suppress these specific deprecation warnings 16 | return; 17 | } 18 | return originalEmitWarning.call(this, warning, type, code, ctor); 19 | }; 20 | 21 | // Also override process.emit 22 | const originalEmit = process.emit; 23 | process.emit = function (name, data, ...args) { 24 | // Suppress specific webpack-dev-server deprecation warnings 25 | if ( 26 | name === 'warning' && 27 | typeof data === 'object' && 28 | data.name === 'DeprecationWarning' && 29 | (data.message.includes('onAfterSetupMiddleware') || 30 | data.message.includes('onBeforeSetupMiddleware') || 31 | data.message.includes("'onAfterSetupMiddleware' option is deprecated") || 32 | data.message.includes("'onBeforeSetupMiddleware' option is deprecated")) 33 | ) { 34 | // Suppress the webpack-dev-server middleware warnings 35 | return false; 36 | } 37 | 38 | return originalEmit.apply(process, arguments); 39 | }; 40 | 41 | // Also suppress console warnings 42 | const originalConsoleWarn = console.warn; 43 | console.warn = function(...args) { 44 | const message = args.join(' '); 45 | if ( 46 | message.includes('onAfterSetupMiddleware') || 47 | message.includes('onBeforeSetupMiddleware') || 48 | message.includes("'onAfterSetupMiddleware' option is deprecated") || 49 | message.includes("'onBeforeSetupMiddleware' option is deprecated") 50 | ) { 51 | return; 52 | } 53 | return originalConsoleWarn.apply(console, arguments); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/Watchlist.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { getWatchlist, removeStockFromWatchlist } from "../utils/watchlistManager"; 3 | import { auth } from "./firebase"; 4 | import { onAuthStateChanged } from "firebase/auth"; 5 | import BackToTopBtn from "./BackToTopBtn"; 6 | import styles from "./Watchlist.module.css"; 7 | 8 | const Watchlist = () => { 9 | const [watchlist, setWatchlist] = useState([]); 10 | const [loading, setLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | const unsubscribe = onAuthStateChanged(auth, (currentUser) => { 14 | getWatchlist().then((data) => { 15 | setWatchlist(data); 16 | setLoading(false); 17 | }); 18 | }); 19 | 20 | return () => unsubscribe(); 21 | }, []); 22 | 23 | const handleRemove = async (symbol) => { 24 | const confirm = window.confirm(`Remove ${symbol} from your watchlist?`); 25 | if (!confirm) return; 26 | 27 | await removeStockFromWatchlist(symbol); 28 | setWatchlist((prev) => prev.filter((s) => s.symbol !== symbol)); 29 | }; 30 | 31 | if (loading) { 32 | return ( 33 |
34 |

Loading your watchlist...

35 |
36 | ); 37 | } 38 | 39 | return ( 40 |
41 |

📈 My Watchlist

42 | {watchlist.length === 0 ? ( 43 |

44 | 📋 45 | No stocks added yet. 46 |

47 | ) : ( 48 |
49 | {watchlist.map((stock) => ( 50 |
51 |

{stock.symbol}

52 |

{stock.name}

53 | 59 |
60 | ))} 61 |
62 | )} 63 | 64 |
65 | ); 66 | }; 67 | 68 | 69 | export default Watchlist; 70 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thank you for your interest in keeping the `stock` project secure. This document explains how to report security vulnerabilities, what to expect after reporting, and how we handle disclosures. 4 | 5 | --- 6 | 7 | ## Supported Versions 8 | 9 | We aim to keep `stock` up to date and secure. Please see below for the versions we currently support with security updates. 10 | 11 | | Version | Supported | 12 | |---------|--------------------| 13 | | Latest | Yes | 14 | | Older | No | 15 | 16 | --- 17 | 18 | ## Reporting a Vulnerability 19 | 20 | If you discover a security vulnerability, **please do not open an issue** on GitHub. 21 | 22 | Instead, follow these steps: 23 | 24 | 1. **Email the maintainer directly** 25 | 2. Include the following details: 26 | - Description of the vulnerability 27 | - Steps to reproduce (if possible) 28 | - Potential impact 29 | - Any mitigation or workaround suggestions 30 | 31 | We aim to respond to security reports **within 72 hours**.. 32 | 33 | --- 34 | 35 | ## Responsible Disclosure Guidelines 36 | 37 | We ask that you: 38 | - Do not publicly disclose the issue until it has been resolved. 39 | - Avoid testing vulnerabilities in a way that could disrupt services. 40 | - Act in good faith and with respect for user data and privacy. 41 | 42 | --- 43 | 44 | ## Disclosure Policy 45 | 46 | - We follow a **coordinated disclosure** approach. 47 | - We appreciate responsible reporting and will publicly disclose the issue only **after a fix has been released**. 48 | --- 49 | 50 | ## Security Fixes & Releases 51 | 52 | Security fixes will be merged into `main` and any supported release branches. We will publish release notes describing the fix and migration steps when required. 53 | 54 | --- 55 | 56 | ## Acknowledgments 57 | 58 | We value the contributions from the community and encourage responsible disclosure to help keep `stock` safe and secure for all users. 59 | 60 | --- 61 | 62 | ## Resources 63 | 64 | - [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories) 65 | - [OpenSSF Best Practices](https://bestpractices.dev/) 66 | - [OWASP Top 10](https://owasp.org/www-project-top-ten/) 67 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request for Stock Analyzer 2 | 3 | ### Thank you for contributing to the **Stock Analyzer** open-source project! 4 | 5 | #### Issue Title 6 | 7 | **Please enter the title of the issue related to your pull request.** 8 | _Example: Add Linear Regression model for stock prediction_ 9 | 10 | #### Contributor Details 11 | 12 | - **Name:** _Enter your name here_ 13 | - **GitHub ID:** _Enter your GitHub username here_ 14 | - **LinkedIn Username (for appreciation):** _Enter your LinkedIn username here_ 15 | - **LinkedIn Profile URL (for verification):** _Enter your LinkedIn Profile Url here_ 16 | - **Email ID (for communication):** _Enter your email address here_ 17 | - **Participation Program (if any):** _e.g., GSSOC, OSCI, Hacktoberfest, None_ 18 | 19 | #### Closes 20 | 21 | **Mention the issue number that your PR fixes.** 22 | _Example: Closes #15_ 23 | 24 | #### Describe Your Changes 25 | 26 | **What have you added or modified in this pull request?** 27 | _Be clear and specific. Example: "Integrated Decision Tree model in backend to support stock predictions."_ 28 | 29 | #### Screenshots 30 | 31 | provide screenshots if applicable, demo video for easy to understand(optional) 32 | 33 | #### what are the files you have changed 34 | 35 | - Eg: app.py, stocklist.py etc.. 36 | 37 | #### How Has This Been Tested? 38 | 39 | **Describe your testing process.** 40 | 41 | describe how to test this update 42 | 43 | #### Type of Change 44 | 45 | _Select all that apply:_ 46 | 47 | - [ ] Bug fix (fixes an existing issue) 48 | - [ ] New feature (adds new functionality) 49 | - [ ] Code style update (formatting, refactoring) 50 | - [ ] Breaking change (may affect existing features) 51 | - [ ] Documentation update 52 | 53 | --- 54 | 55 | #### Checklist 56 | 57 | - [ ] My code follows the project’s guidelines and best practices. 58 | - [ ] I have performed a self-review of my code. 59 | - [ ] I have added meaningful comments where necessary. 60 | - [ ] I have updated documentation wherever applicable. 61 | - [ ] My changes do not generate any new warnings or errors. 62 | - [ ] I have included screenshots, graphs, or test logs (if applicable). 63 | - [ ] Any dependent changes have been merged and documented. 64 | 65 | --- 66 | 67 | Thank you for helping improve **Stock Analyzer**! 68 | -------------------------------------------------------------------------------- /src/styles/theme-toggle.css: -------------------------------------------------------------------------------- 1 | .theme-toggle { 2 | display: flex; 3 | align-items: center; 4 | gap: 0.5rem; 5 | background: transparent; 6 | border: none; 7 | color: var(--color-text); 8 | cursor: pointer; 9 | padding: 0.5rem 0.75rem; 10 | border-radius: var(--radius-full); 11 | font-size: 0.9375rem; 12 | font-weight: 500; 13 | transition: all 0.2s ease; 14 | margin: 0 0.5rem; 15 | } 16 | 17 | .theme-toggle:hover { 18 | background-color: var(--color-bg-secondary); 19 | } 20 | 21 | .theme-toggle:focus { 22 | outline: 2px solid var(--color-primary); 23 | outline-offset: 2px; 24 | } 25 | 26 | .theme-toggle-track { 27 | display: inline-flex; 28 | align-items: center; 29 | justify-content: center; 30 | width: 2.5rem; 31 | height: 1.5rem; 32 | border-radius: 0.75rem; 33 | background-color: var(--color-bg-secondary); 34 | position: relative; 35 | transition: background-color 0.2s ease; 36 | } 37 | 38 | .theme-toggle-thumb { 39 | position: absolute; 40 | left: 0.25rem; 41 | width: 1.25rem; 42 | height: 1.25rem; 43 | border-radius: 50%; 44 | background-color: var(--color-text); 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | transition: transform 0.3s cubic-bezier(0.4, 0.03, 0.25, 1); 49 | transform: translateX(0); 50 | color: var(--color-bg); 51 | } 52 | 53 | [data-theme="dark"] .theme-toggle-thumb { 54 | transform: translateX(1rem); 55 | background-color: var(--color-primary); 56 | color: var(--color-bg); 57 | } 58 | 59 | .theme-icon { 60 | font-size: 0.75rem; 61 | display: flex; 62 | align-items: center; 63 | justify-content: center; 64 | transition: opacity 0.2s ease; 65 | } 66 | 67 | .theme-toggle-label { 68 | font-size: 0.875rem; 69 | white-space: nowrap; 70 | } 71 | 72 | /* Reduce motion for users who prefer it */ 73 | @media (prefers-reduced-motion: reduce) { 74 | .theme-toggle, 75 | .theme-toggle-thumb, 76 | .theme-toggle-track { 77 | transition: none; 78 | } 79 | } 80 | 81 | /* Responsive adjustments */ 82 | @media (max-width: 768px) { 83 | .theme-toggle { 84 | width: 100%; 85 | justify-content: space-between; 86 | padding: 0.75rem 1rem; 87 | margin: 0.25rem 0; 88 | border-radius: var(--radius-md); 89 | } 90 | 91 | .theme-toggle:hover { 92 | background-color: var(--color-bg-secondary); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/firebase.js: -------------------------------------------------------------------------------- 1 | // firebase.js 2 | import { initializeApp, getApps, getApp } from "firebase/app"; 3 | import { getAuth } from "firebase/auth"; 4 | import { getDatabase } from "firebase/database"; 5 | import { getFirestore } from "firebase/firestore"; 6 | 7 | // ✅ Use .env variables (set these in your .env file) 8 | // If no Firebase config is provided, use dummy config for development 9 | const firebaseConfig = { 10 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY || "demo-api-key", 11 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN || "demo-project.firebaseapp.com", 12 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID || "demo-project", 13 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET || "demo-project.appspot.com", 14 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID || "123456789", 15 | appId: process.env.REACT_APP_FIREBASE_APP_ID || "1:123456789:web:demo", 16 | // 👇 Only include if you need analytics 17 | measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID || "G-DEMO123", 18 | // 👇 Only include if you enable Realtime Database 19 | databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL || "https://demo-project.firebaseio.com", 20 | }; 21 | 22 | // Avoid initializing twice 23 | let app; 24 | let auth; 25 | let realtimeDb; 26 | let firestoreDb; 27 | 28 | try { 29 | app = !getApps().length ? initializeApp(firebaseConfig) : getApp(); 30 | auth = getAuth(app); 31 | realtimeDb = getDatabase(app); 32 | firestoreDb = getFirestore(app); 33 | } catch (error) { 34 | console.warn("Firebase initialization failed:", error.message); 35 | console.warn("Firebase features will be disabled. Please configure your .env file with valid Firebase credentials."); 36 | 37 | // Create mock objects to prevent app crashes 38 | auth = { 39 | currentUser: null, 40 | onAuthStateChanged: () => () => {}, 41 | signInWithEmailAndPassword: () => Promise.reject(new Error("Firebase not configured")), 42 | signInWithPopup: () => Promise.reject(new Error("Firebase not configured")), 43 | createUserWithEmailAndPassword: () => Promise.reject(new Error("Firebase not configured")), 44 | signOut: () => Promise.resolve(), 45 | }; 46 | 47 | realtimeDb = { 48 | ref: () => ({ 49 | set: () => Promise.resolve(), 50 | get: () => Promise.resolve({ val: () => null }), 51 | }), 52 | }; 53 | 54 | firestoreDb = { 55 | collection: () => ({ 56 | doc: () => ({ 57 | set: () => Promise.resolve(), 58 | get: () => Promise.resolve({ data: () => null }), 59 | }), 60 | }), 61 | }; 62 | } 63 | 64 | export { auth, realtimeDb, firestoreDb }; 65 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.css: -------------------------------------------------------------------------------- 1 | /* Theme Toggle Switch */ 2 | .theme-toggle { 3 | --toggle-size: 2.5rem; 4 | --toggle-padding: 0.25rem; 5 | --thumb-size: calc(var(--toggle-size) - (var(--toggle-padding) * 2)); 6 | 7 | background: none; 8 | border: none; 9 | padding: 0; 10 | cursor: pointer; 11 | width: var(--toggle-size); 12 | height: var(--toggle-size); 13 | border-radius: 50%; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | position: relative; 18 | transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease; 19 | background-color: var(--color-bg-secondary); 20 | color: var(--color-text); 21 | border: 2px solid var(--color-border); 22 | } 23 | 24 | .theme-toggle:hover { 25 | transform: scale(1.1); 26 | background-color: var(--color-bg-tertiary); 27 | border-color: var(--color-primary); 28 | } 29 | 30 | .theme-toggle:focus { 31 | outline: none; 32 | box-shadow: 0 0 0 3px var(--color-primary-transparent); 33 | } 34 | 35 | .theme-toggle.dark { 36 | background-color: var(--color-bg-secondary); 37 | color: var(--color-primary); 38 | } 39 | 40 | .theme-toggle.light { 41 | background-color: var(--color-bg-secondary); 42 | color: var(--color-warning); 43 | } 44 | 45 | .theme-icon { 46 | width: 1.25rem; 47 | height: 1.25rem; 48 | transition: all 0.3s ease; 49 | color: currentColor; 50 | } 51 | 52 | .sun-icon { 53 | color: #fbbf24; 54 | } 55 | 56 | .moon-icon { 57 | color: #60a5fa; 58 | } 59 | 60 | .theme-toggle-track { 61 | width: 100%; 62 | height: 100%; 63 | position: relative; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | border-radius: 50%; 68 | overflow: hidden; 69 | } 70 | 71 | .theme-toggle-thumb { 72 | width: 100%; 73 | height: 100%; 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); 78 | } 79 | 80 | .theme-icon { 81 | width: 1.25rem; 82 | height: 1.25rem; 83 | transition: all 0.3s ease; 84 | color: currentColor; 85 | } 86 | 87 | /* For reduced motion preferences */ 88 | @media (prefers-reduced-motion: reduce) { 89 | .theme-toggle, 90 | .theme-toggle-thumb, 91 | .theme-icon { 92 | transition: none; 93 | } 94 | } 95 | 96 | /* Screen reader only text */ 97 | .sr-only { 98 | position: absolute; 99 | width: 1px; 100 | height: 1px; 101 | padding: 0; 102 | margin: -1px; 103 | overflow: hidden; 104 | clip: rect(0, 0, 0, 0); 105 | white-space: nowrap; 106 | border: 0; 107 | } 108 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | // App.js 2 | import React, { useEffect } from "react"; 3 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 4 | import "./App.css"; 5 | import "./styles/global.css"; 6 | 7 | // Components 8 | import Header from "./components/Header"; 9 | import Footer from "./components/Footer"; 10 | import StocksList from "./components/StockList"; 11 | import Stockdata from "./components/Stockdata"; 12 | import AboutComponent from "./components/About"; 13 | import ContactForm from "./components/ContactForm"; 14 | import Login from "./components/Login"; 15 | import Signup from "./components/Signup"; 16 | import Watchlist from "./components/Watchlist"; 17 | import { AuthProvider } from "./components/AuthContext"; 18 | import ErrorBoundary from "./components/ErrorBoundary"; 19 | 20 | // Theme 21 | import { ThemeProvider, useTheme } from "./components/ThemeContext"; 22 | 23 | // Global styles for smooth transitions 24 | const GlobalStyles = () => { 25 | const { theme } = useTheme(); 26 | 27 | useEffect(() => { 28 | // Remove the no-js class if JavaScript is enabled 29 | document.documentElement.classList.remove('no-js'); 30 | document.documentElement.classList.add('js'); 31 | 32 | // Set theme class on body for easier targeting 33 | document.body.className = `theme-${theme}`; 34 | 35 | // Add transition class after initial render 36 | const timer = setTimeout(() => { 37 | document.documentElement.classList.add('theme-transition-ready'); 38 | }, 100); 39 | 40 | return () => clearTimeout(timer); 41 | }, [theme]); 42 | 43 | return null; 44 | }; 45 | 46 | const App = () => { 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 | 57 | } 60 | /> 61 | } /> 62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | } /> 67 | } /> 68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | export default App; 80 | -------------------------------------------------------------------------------- /backend/services/stock_predict.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.linear_model import LinearRegression 3 | from datetime import datetime, timedelta 4 | from flask import jsonify 5 | import pandas as pd 6 | import os 7 | 8 | DATA_FOLDER = os.path.join(os.path.dirname(__file__), "data") 9 | 10 | def normalize_symbol(symbol: str) -> str: 11 | """Remove exchange suffix like .NS or .BO to match CSV filenames.""" 12 | return symbol.split(".")[0].upper() 13 | 14 | def predict_stock_handler(symbol): 15 | """ 16 | Handles prediction request based on historical stock data. 17 | """ 18 | try: 19 | # Normalize symbol (remove .NS or .BO) 20 | symbol = normalize_symbol(symbol) 21 | 22 | # Load CSV 23 | file_path = os.path.join(DATA_FOLDER, f"{symbol}.csv") 24 | if not os.path.exists(file_path): 25 | return jsonify({'error': f'CSV data not found for {symbol}'}), 404 26 | 27 | data = pd.read_csv(file_path) 28 | 29 | # Expecting standard headers 30 | if 'Date' not in data.columns or 'Close' not in data.columns: 31 | return jsonify({'error': 'CSV must contain Date and Close columns'}), 400 32 | 33 | # Preprocess 34 | data['Date'] = pd.to_datetime(data['Date']) 35 | data = data.sort_values('Date') 36 | data.set_index('Date', inplace=True) 37 | 38 | X = data.index.map(datetime.toordinal).values.reshape(-1, 1) 39 | y = data['Close'].values.flatten() 40 | 41 | # Train model 42 | model = LinearRegression() 43 | model.fit(X, y) 44 | 45 | # Predict next 10 years 46 | future_dates = [datetime.now() + timedelta(days=365 * i) for i in range(1, 11)] 47 | future_ordinals = [d.toordinal() for d in future_dates] 48 | predictions = model.predict(np.array(future_ordinals).reshape(-1, 1)) 49 | predicted_dates = [d.strftime('%Y-%m-%d') for d in future_dates] 50 | 51 | # Project returns 52 | stocks = [10, 20, 50, 100] 53 | current_price = y[-1] 54 | returns = [ 55 | { 56 | 'stocks_bought': stock, 57 | 'current_price': round(current_price * stock, 2), 58 | 'after_1_year': round(predictions[0] * stock, 2), 59 | 'after_5_years': round(predictions[4] * stock, 2), 60 | 'after_10_years': round(predictions[9] * stock, 2), 61 | } 62 | for stock in stocks 63 | ] 64 | 65 | return jsonify({ 66 | 'predictions': predictions.tolist(), 67 | 'predicted_dates': predicted_dates, 68 | 'actual': y.tolist(), 69 | 'actual_dates': data.index.strftime('%Y-%m-%d').tolist(), 70 | 'returns': returns 71 | }) 72 | 73 | except Exception as e: 74 | return jsonify({'error': f'Internal Server Error: {str(e)}'}), 500 75 | -------------------------------------------------------------------------------- /src/components/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react'; 2 | 3 | const ThemeContext = createContext(); 4 | 5 | export function ThemeProvider({ children }) { 6 | const [theme, setTheme] = useState('light'); 7 | const [isInitialized, setIsInitialized] = useState(false); 8 | 9 | // Initialize theme from localStorage or system preference 10 | useEffect(() => { 11 | try { 12 | const savedTheme = localStorage.getItem('theme'); 13 | const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 14 | 15 | const initialTheme = savedTheme || (systemPrefersDark ? 'dark' : 'light'); 16 | setTheme(initialTheme); 17 | 18 | // Apply theme immediately 19 | applyTheme(initialTheme); 20 | setIsInitialized(true); 21 | 22 | } catch (error) { 23 | console.warn('Could not initialize theme:', error); 24 | setTheme('light'); 25 | applyTheme('light'); 26 | setIsInitialized(true); 27 | } 28 | }, []); 29 | 30 | // Apply theme class and save preference 31 | useEffect(() => { 32 | if (!isInitialized) return; 33 | 34 | applyTheme(theme); 35 | 36 | try { 37 | localStorage.setItem('theme', theme); 38 | } catch (e) { 39 | console.warn('Could not save theme preference:', e); 40 | } 41 | }, [theme, isInitialized]); 42 | 43 | const applyTheme = (themeName) => { 44 | try { 45 | const root = document.documentElement; 46 | const body = document.body; 47 | 48 | // Remove existing theme classes 49 | root.classList.remove('light', 'dark', 'theme-light', 'theme-dark'); 50 | body.classList.remove('light', 'dark', 'theme-light', 'theme-dark'); 51 | 52 | // Add new theme classes 53 | root.classList.add(themeName, `theme-${themeName}`); 54 | body.classList.add(themeName, `theme-${themeName}`); 55 | 56 | // Set data-theme attribute for CSS selectors 57 | root.setAttribute('data-theme', themeName); 58 | body.setAttribute('data-theme', themeName); 59 | 60 | // Force re-computation of CSS variables 61 | root.style.setProperty('--current-theme', themeName); 62 | 63 | } catch (error) { 64 | console.warn('Could not apply theme:', error); 65 | } 66 | }; 67 | 68 | const toggleTheme = () => { 69 | const newTheme = theme === 'light' ? 'dark' : 'light'; 70 | setTheme(newTheme); 71 | }; 72 | 73 | return ( 74 | 75 | {children} 76 | 77 | ); 78 | } 79 | 80 | export const useTheme = () => { 81 | const context = useContext(ThemeContext); 82 | if (!context) { 83 | throw new Error('useTheme must be used within a ThemeProvider'); 84 | } 85 | return context; 86 | }; 87 | 88 | export default ThemeContext; 89 | -------------------------------------------------------------------------------- /src/components/Watchlist.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 2rem 1rem; 3 | max-width: 1200px; 4 | margin: 0 auto; 5 | color: var(--color-text); 6 | min-height: calc(100vh - 200px); 7 | background-color: var(--color-bg); 8 | transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease; 9 | } 10 | 11 | .heading { 12 | text-align: center; 13 | margin-bottom: 2rem; 14 | color: var(--color-text); 15 | font-size: 2rem; 16 | font-weight: 600; 17 | position: relative; 18 | padding-bottom: 0.5rem; 19 | } 20 | 21 | .heading::after { 22 | content: ''; 23 | position: absolute; 24 | bottom: 0; 25 | left: 50%; 26 | transform: translateX(-50%); 27 | width: 80px; 28 | height: 3px; 29 | background-color: var(--color-primary); 30 | border-radius: 2px; 31 | } 32 | 33 | .empty { 34 | text-align: center; 35 | color: var(--color-text-secondary); 36 | padding: 3rem 1rem; 37 | background-color: var(--color-bg-tertiary); 38 | border-radius: var(--form-border-radius); 39 | border: 1px dashed var(--color-border); 40 | } 41 | 42 | .empty::before { 43 | content: '📋'; 44 | font-size: 2rem; 45 | display: block; 46 | margin-bottom: 1rem; 47 | } 48 | 49 | .grid { 50 | display: grid; 51 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 52 | gap: 1.5rem; 53 | padding: 0.5rem 0; 54 | } 55 | 56 | .card { 57 | background-color: var(--color-bg-secondary); 58 | color: var(--color-text); 59 | padding: 1.5rem; 60 | border-radius: var(--form-border-radius); 61 | border: 1px solid var(--color-border); 62 | box-shadow: 0 2px 10px var(--color-shadow); 63 | transition: all var(--transition-speed) ease; 64 | position: relative; 65 | overflow: hidden; 66 | } 67 | 68 | .card:hover { 69 | transform: translateY(-3px); 70 | box-shadow: 0 6px 15px var(--color-shadow-hover); 71 | background-color: var(--color-card-hover); 72 | } 73 | 74 | .card h4 { 75 | margin: 0 0 0.5rem 0; 76 | color: var(--color-primary); 77 | font-size: 1.1rem; 78 | font-weight: 600; 79 | } 80 | 81 | .card p { 82 | margin: 0 0 1rem 0; 83 | color: var(--color-text-secondary); 84 | font-size: 0.95rem; 85 | } 86 | 87 | .removeBtn { 88 | margin-top: 10px; 89 | background-color: var(--color-danger); 90 | color: white; 91 | border: none; 92 | padding: 8px 16px; 93 | border-radius: var(--form-border-radius); 94 | cursor: pointer; 95 | transition: all var(--transition-speed) ease; 96 | display: inline-flex; 97 | align-items: center; 98 | gap: 6px; 99 | font-size: 0.9rem; 100 | font-weight: 500; 101 | } 102 | 103 | .removeBtn:hover { 104 | background-color: var(--color-danger-hover); 105 | transform: translateY(-1px); 106 | box-shadow: 0 2px 8px rgba(220, 53, 69, 0.2); 107 | } 108 | 109 | .removeBtn:active { 110 | transform: translateY(0); 111 | box-shadow: none; 112 | } 113 | -------------------------------------------------------------------------------- /src/components/about-professional.css: -------------------------------------------------------------------------------- 1 | /* --- Offers BG Section --- */ 2 | .offers-bg-section { 3 | position: relative; 4 | left: 50%; 5 | width: 100vw; 6 | min-width: 100vw; 7 | margin-left: -50vw; 8 | margin-right: 0; 9 | margin-top: 44px; 10 | margin-bottom: 48px; 11 | padding: 72px 0 80px 0; 12 | border-radius: 0; /* Edge to edge */ 13 | overflow: hidden; 14 | min-height: 440px; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | background: #132750; 20 | box-sizing: border-box; 21 | } 22 | 23 | /* Blur effect using an overlay with the local image */ 24 | .offers-bg-section::before { 25 | content: ""; 26 | position: absolute; 27 | inset: 0; 28 | z-index: 1; 29 | background: 30 | linear-gradient(to bottom, rgba(15,35,75,0.56), rgba(33,84,214,0.12)), 31 | url("../Images/stock.png"); 32 | background-size: cover; 33 | background-position: center; 34 | background-repeat: no-repeat; 35 | filter: blur(12px) brightness(0.90) saturate(0.8); 36 | opacity: 0.83; 37 | pointer-events: none; 38 | } 39 | 40 | /* Ensure foreground content sits above blur */ 41 | .offers-bg-section > * { 42 | position: relative; 43 | z-index: 2; 44 | } 45 | 46 | .offers-bg-section h2 { 47 | color: #fff !important; 48 | text-align: center; 49 | font-weight: 700; 50 | letter-spacing: -0.5px; 51 | margin-bottom: 32px; 52 | font-size: 2.2rem; 53 | text-shadow: 0 2px 16px #014aaa44, 0 1px 0 #00121a; 54 | } 55 | 56 | .offers-bg-content { 57 | display: flex; 58 | flex-wrap: wrap; 59 | justify-content: center; 60 | gap: 48px 64px; 61 | padding: 0 18px; 62 | width: 100%; 63 | max-width: 1120px; 64 | margin: 0 auto; 65 | box-sizing: border-box; 66 | } 67 | 68 | .offers-emoji-title { 69 | display: flex; 70 | flex-direction: column; 71 | align-items: center; 72 | min-width: 152px; 73 | min-height: 110px; 74 | padding: 27px 16px; 75 | background: rgba(255,255,255,0.15); 76 | border-radius: 18px; 77 | box-shadow: 0 2.5px 12px rgba(32,84,214,0.19); 78 | margin: 0 5px; 79 | transition: background .23s, transform .17s; 80 | } 81 | .offers-emoji-title:hover { 82 | background: rgba(255,255,255,0.28); 83 | transform: translateY(-2px) scale(1.04); 84 | } 85 | 86 | .offer-emoji { 87 | font-size: 2.65rem; 88 | margin-bottom: 13px; 89 | line-height: 1.1; 90 | } 91 | .offer-title { 92 | color: #fff; 93 | font-size: 1.18rem; 94 | font-weight: 700; 95 | text-align: center; 96 | letter-spacing: .01em; 97 | text-shadow: 0 2px 7px #014aaa33, 0 1px 0 #00121a; 98 | } 99 | 100 | /* Responsive adjustments */ 101 | @media (max-width: 1200px) { 102 | .offers-bg-content { 103 | max-width: 98vw; 104 | gap: 36px 20px; 105 | } 106 | } 107 | @media (max-width: 950px) { 108 | .offers-bg-content { 109 | gap: 24px 14px; 110 | max-width: 99vw; 111 | } 112 | .offers-emoji-title { 113 | min-width: 120px; 114 | min-height: 70px; 115 | padding: 14px 7px; 116 | } 117 | .offers-bg-section { 118 | padding: 46px 0 45px 0; 119 | min-height: 250px; 120 | } 121 | } 122 | @media (max-width: 650px) { 123 | .offers-bg-section { 124 | padding: 32px 0 38px 0; 125 | min-height: 180px; 126 | } 127 | .offers-emoji-title { 128 | font-size: 0.98rem; 129 | } 130 | .offer-title { 131 | font-size: 0.99rem; 132 | } 133 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | **✨Documentation Code of Conduct — Stock App ✨** 2 | 3 | **🌟 Our Commitment** 4 | 5 | As part of **GSSoC’25, the Stock** project is dedicated to fostering a respectful, inclusive, and collaborative environment for all—whether you're a first-time contributor, experienced developer, mentor, or project admin. This is a space for learning, sharing, and building impactful financial tools together. 6 | 7 | --- 8 | 9 | **🤝 Community Values** 10 | 11 | We believe our community thrives when we: 12 | - 📊 Recognize every contribution—small or significant 13 | - 🌍 Embrace diversity in backgrounds, ideas, and expertise 14 | - 💬 Encourage questions and respond with kindness and patience 15 | - 📚 Offer constructive feedback and celebrate improvement 16 | - 🤝 Support one another—contributors, mentors, and admins alike 17 | - 🛠️ Collaborate transparently, respectfully, and with empathy 18 | 19 | --- 20 | 21 | **🚫 Not Tolerated** 22 | 23 | To maintain a safe and productive space for all contributors, mentors, and project admins, we strictly prohibit: 24 | - ❌ Disrespectful, harmful, or dismissive behavior 25 | - ❌ Harassment, personal attacks, or unsolicited messages 26 | - ❌ Gatekeeping or exclusionary behavior 27 | - ❌ Spam, off-topic promotions, or disruptive content 28 | - ❌ Any form of hate speech, discrimination, or toxicity 29 | 30 | --- 31 | 32 | **🧭 Where This Applies** 33 | 34 | This Code of Conduct applies to all areas related to Stock during GSSoC’25: 35 | - 🖥️ GitHub Repository and Issue Discussions 36 | - 💬 Discord or Community Communication Platforms 37 | - 📅 Virtual events, workshops, and hack sessions 38 | - 📢 Social media conversations and promotions 39 | 40 | --- 41 | 42 | If you experience or observe a violation of this Code of Conduct, please **reach out** to: 43 | 44 | - A **Project Admin** 45 | - A **GSSoC’25 Mentor** assigned to **Stock** 46 | 47 | All reports will be treated with confidentiality and seriousness. The GSSoC’25 team is committed to ensuring a safe experience for everyone involved. 48 | 49 | --- 50 | 51 | **🧩 Possible Actions** 52 | 53 | In response to violations, the following actions may be taken: 54 | - **🟡 Friendly reminder of community guidelines** 55 | - **🟠 Formal warning issued by a Project Admin or Mentor** 56 | - **🔴 Temporary suspension from the project** 57 | - **⚫ Permanent removal from participation in the Stock community** 58 | 59 | --- 60 | 61 | **🎯 Contributor Expectations** 62 | 63 | All contributors to Stock App during GSSoC’25 are expected to: 64 | - ✅ Be kind, respectful, and professional in all interactions 65 | - ✅ Submit original, ethical, and helpful contributions 66 | - ✅ Be open to feedback and willing to iterate 67 | - ✅ Credit sources, collaborators, and inspirations 68 | - ✅ Help create a beginner-friendly and inclusive space 69 | 70 | --- 71 | 72 | **🌸 Our Vision** 73 | 74 | Stock is not just a project—it’s a collaborative effort to build a smarter, more accessible financial future. Together with our contributors, mentors, and project admins under GSSoC’25, we aim to create tools that empower users and foster lifelong learning. 💹📈 75 | 76 | --- 77 | 78 | **📄 Attribution** 79 | 80 | This Code of Conduct has been created as part of **GSSoC’25** to reflect our shared values of inclusivity, mentorship, open source collaboration and adapted for Stock under GSSoC 2025 guidelines. Original work licensed under CC BY 4.0. 81 | 82 | Inspired by the [Contributor Covenant (v3.0)](https://www.contributor-covenant.org/version/3/0/code_of_conduct/) 83 | 84 | Modifications include stock-specific community standards, contributor expectations, and mentorship responsibilities. 85 | 86 | Let’s build a respectful, welcoming, and impactful open-source community. 🌱💻 -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | signInWithEmailAndPassword, 4 | signInWithPopup, 5 | GoogleAuthProvider, 6 | } from "firebase/auth"; 7 | import { auth } from "./firebase"; 8 | import { useNavigate, Link } from "react-router-dom"; 9 | import { syncLocalToFirebase } from "../utils/watchlistManager"; 10 | import "./Login.css"; 11 | 12 | const Login = () => { 13 | const [email, setEmail] = useState(""); 14 | const [password, setPassword] = useState(""); 15 | const [error, setError] = useState(""); 16 | const [loading, setLoading] = useState(false); 17 | const navigate = useNavigate(); 18 | 19 | const handleLogin = async (e) => { 20 | e.preventDefault(); 21 | setError(""); 22 | setLoading(true); 23 | 24 | try { 25 | // Firebase authentication 26 | const userCredential = await signInWithEmailAndPassword( 27 | auth, 28 | email, 29 | password 30 | ); 31 | 32 | // Sync local data to Firebase after login 33 | await syncLocalToFirebase(userCredential.user); 34 | 35 | navigate("/"); // redirect to home page 36 | } catch (err) { 37 | setError(err.message); 38 | } finally { 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | const handleGoogleLogin = async () => { 44 | setError(""); 45 | setLoading(true); 46 | 47 | try { 48 | const provider = new GoogleAuthProvider(); 49 | const result = await signInWithPopup(auth, provider); 50 | // Sync local data to Firebase after Google login 51 | await syncLocalToFirebase(result.user); 52 | navigate("/"); // redirect to home page 53 | } catch (err) { 54 | const errorCode = err.code; 55 | 56 | if (errorCode === "auth/popup-closed-by-user") { 57 | setError("Sign-in cancelled by user"); 58 | } else if (errorCode === "auth/popup-blocked") { 59 | setError( 60 | "Popup blocked by browser. Please allow popups and try again." 61 | ); 62 | } else { 63 | setError(err.message); 64 | } 65 | } finally { 66 | setLoading(false); 67 | } 68 | }; 69 | 70 | return ( 71 |
72 |
73 |

Login

74 | 75 | {/* Google Sign-In Button */} 76 | 84 | 85 |
86 | or 87 |
88 | 89 |
90 | setEmail(e.target.value)} 95 | required 96 | disabled={loading} 97 | /> 98 | setPassword(e.target.value)} 103 | required 104 | disabled={loading} 105 | /> 106 | {error &&

{error}

} 107 | 110 |
111 | 112 |

113 | Don't have an account? Signup 114 |

115 |
116 |
117 | ); 118 | }; 119 | 120 | export default Login; 121 | -------------------------------------------------------------------------------- /src/components/Login.css: -------------------------------------------------------------------------------- 1 | /* Login message for route protection */ 2 | .login-message { 3 | color: #1976d2; 4 | background: #e3f2fd; 5 | border-radius: 6px; 6 | padding: 10px; 7 | margin-bottom: 16px; 8 | font-size: 15px; 9 | text-align: center; 10 | } 11 | /* Background and container styling */ 12 | .login-page { 13 | background: url("/Images/bg.png") no-repeat center center fixed; 14 | background-size: cover; 15 | height: 100vh; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | /* Card container */ 22 | .login-container { 23 | background-color: rgba(255, 255, 255, 0.9); 24 | padding: 40px; 25 | border-radius: 16px; 26 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); 27 | width: 90%; 28 | max-width: 400px; 29 | text-align: center; 30 | } 31 | 32 | /* Form styling */ 33 | .login-form input { 34 | width: 100%; 35 | padding: 12px; 36 | margin: 10px 0; 37 | border: 1px solid #ccc; 38 | border-radius: 8px; 39 | font-size: 16px; 40 | } 41 | 42 | .login-form button { 43 | width: 100%; 44 | padding: 12px; 45 | background-color: #007bff; 46 | color: white; 47 | font-size: 16px; 48 | border: none; 49 | border-radius: 8px; 50 | cursor: pointer; 51 | margin-top: 10px; 52 | } 53 | 54 | .login-form button:hover { 55 | background-color: #0056b3; 56 | } 57 | 58 | /* Error message */ 59 | .login-error { 60 | color: red; 61 | font-size: 14px; 62 | margin-top: 5px; 63 | } 64 | 65 | /* Add these styles to your existing Login.css file */ 66 | 67 | .google-login-btn { 68 | width: 100%; 69 | padding: 12px; 70 | border: 1px solid #dadce0; 71 | border-radius: 4px; 72 | background-color: #fff; 73 | color: #3c4043; 74 | font-size: 14px; 75 | font-weight: 500; 76 | cursor: pointer; 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | transition: background-color 0.2s ease, box-shadow 0.2s ease; 81 | margin-bottom: 20px; 82 | } 83 | 84 | .google-login-btn:hover:not(:disabled) { 85 | background-color: #f8f9fa; 86 | box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3); 87 | } 88 | 89 | .google-login-btn:active:not(:disabled) { 90 | background-color: #f1f3f4; 91 | } 92 | 93 | .google-login-btn:disabled { 94 | opacity: 0.6; 95 | cursor: not-allowed; 96 | } 97 | 98 | /* Add Google logo if desired */ 99 | .google-login-btn::before { 100 | content: ""; 101 | width: 18px; 102 | height: 18px; 103 | margin-right: 8px; 104 | background-image: url("data:image/svg+xml,%3csvg width='18' height='18' viewBox='0 0 18 18' xmlns='http://www.w3.org/2000/svg'%3e%3cg fill='none' fill-rule='evenodd'%3e%3cpath d='M17.64 9.205c0-.639-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615Z' fill='%234285F4'/%3e%3cpath d='M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18Z' fill='%2334A853'/%3e%3cpath d='M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332Z' fill='%23FBBC05'/%3e%3cpath d='M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58Z' fill='%23EA4335'/%3e%3c/g%3e%3c/svg%3e"); 105 | background-size: 18px 18px; 106 | background-repeat: no-repeat; 107 | } 108 | 109 | .divider { 110 | display: flex; 111 | align-items: center; 112 | text-align: center; 113 | margin: 20px 0; 114 | } 115 | 116 | .divider::before, 117 | .divider::after { 118 | content: ""; 119 | flex: 1; 120 | height: 1px; 121 | background: #dadce0; 122 | } 123 | 124 | .divider span { 125 | padding: 0 16px; 126 | color: #5f6368; 127 | font-size: 14px; 128 | } 129 | -------------------------------------------------------------------------------- /backend/tests/test_integration_cache.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from types import ModuleType 3 | import sys 4 | 5 | 6 | def test_stock_cache_miss_then_hit_and_bypass(monkeypatch): 7 | # Stub service modules BEFORE importing app to avoid heavy deps 8 | mod = ModuleType('services.stock_service') 9 | modp = ModuleType('services.stock_predict') 10 | calls = {"n": 0} 11 | 12 | def stub_stock_handler(symbol, chart_period, table_period): 13 | calls["n"] += 1 14 | return jsonify({ 15 | "symbol": symbol, 16 | "chart_period": chart_period, 17 | "table_period": table_period, 18 | "ok": True 19 | }) 20 | 21 | mod.get_stock_data_handler = stub_stock_handler # type: ignore[attr-defined] 22 | # minimal predict stub to satisfy import 23 | def _stub_predict(symbol): 24 | return jsonify({"predictions": [42], "symbol": symbol}) 25 | modp.predict_stock_handler = _stub_predict # type: ignore[attr-defined] 26 | sys.modules['services.stock_service'] = mod 27 | sys.modules['services.stock_predict'] = modp 28 | 29 | # Import app 30 | from app import app 31 | # Force caching on and force in-memory (no redis attempts) 32 | import utils.cache as cache 33 | monkeypatch.setattr(cache, "CACHE_ENABLED", True, raising=False) 34 | monkeypatch.setattr(cache, "_get_redis_client", lambda: None, raising=False) 35 | # reset memory cache to clean state 36 | monkeypatch.setattr(cache, "_memory_cache", {}, raising=False) 37 | 38 | # Handlers are already stubbed by module injection above 39 | 40 | client = app.test_client() 41 | url = "/api/stock/TEST?chart_period=1d&table_period=1d" 42 | 43 | # 1) MISS 44 | r1 = client.get(url) 45 | assert r1.status_code == 200 46 | assert r1.headers.get("X-Cache") == "MISS" 47 | assert calls["n"] == 1 48 | 49 | # 2) HIT 50 | r2 = client.get(url) 51 | assert r2.status_code == 200 52 | assert r2.headers.get("X-Cache") == "HIT" 53 | assert calls["n"] == 1 # no recompute 54 | 55 | # 3) BYPASS with refresh=true 56 | r3 = client.get(url + "&refresh=true") 57 | assert r3.status_code == 200 58 | assert r3.headers.get("X-Cache") == "BYPASS" 59 | assert calls["n"] == 2 60 | 61 | 62 | def test_predict_cache_miss_then_hit(monkeypatch): 63 | # Stub both service modules BEFORE importing app 64 | mod = ModuleType('services.stock_service') 65 | modp = ModuleType('services.stock_predict') 66 | mod.get_stock_data_handler = lambda *args, **kwargs: jsonify({"ok": True}) # type: ignore[attr-defined] 67 | calls = {"n": 0} 68 | def stub_predict_handler(symbol): 69 | calls["n"] += 1 70 | return jsonify({ 71 | "predictions": [1, 2, 3], 72 | "symbol": symbol 73 | }) 74 | modp.predict_stock_handler = stub_predict_handler # type: ignore[attr-defined] 75 | sys.modules['services.stock_service'] = mod 76 | sys.modules['services.stock_predict'] = modp 77 | 78 | from app import app 79 | import routes.stock_routes as routes 80 | import utils.cache as cache 81 | monkeypatch.setattr(cache, "CACHE_ENABLED", True, raising=False) 82 | monkeypatch.setattr(cache, "_get_redis_client", lambda: None, raising=False) 83 | monkeypatch.setattr(cache, "_memory_cache", {}, raising=False) 84 | # Ensure routes use our counting stub for this test run 85 | monkeypatch.setattr(routes, "predict_stock_handler", stub_predict_handler, raising=True) 86 | 87 | client = app.test_client() 88 | url = "/api/stock/FOO/predict" 89 | 90 | # MISS 91 | r1 = client.get(url) 92 | assert r1.status_code == 200 93 | assert r1.headers.get("X-Cache") == "MISS" 94 | assert calls["n"] == 1 95 | 96 | # HIT 97 | r2 = client.get(url) 98 | assert r2.status_code == 200 99 | assert r2.headers.get("X-Cache") == "HIT" 100 | assert calls["n"] == 1 101 | -------------------------------------------------------------------------------- /src/utils/watchlistManager.js: -------------------------------------------------------------------------------- 1 | import { auth, firestoreDb } from "../components/firebase"; 2 | import { 3 | collection, 4 | addDoc, 5 | deleteDoc, 6 | query, 7 | where, 8 | getDocs, 9 | doc, 10 | } from "firebase/firestore"; 11 | 12 | export const toggleWatchlist = async (stock) => { 13 | const user = auth.currentUser; 14 | 15 | if (user) { 16 | const q = query( 17 | collection(firestoreDb, "watchlist"), 18 | where("uid", "==", user.uid), 19 | where("symbol", "==", stock.symbol) 20 | ); 21 | const snapshot = await getDocs(q); 22 | 23 | if (!snapshot.empty) { 24 | await Promise.all( 25 | snapshot.docs.map((docSnap) => 26 | deleteDoc(doc(firestoreDb, "watchlist", docSnap.id)) 27 | ) 28 | ); 29 | } else { 30 | await addDoc(collection(firestoreDb, "watchlist"), { 31 | uid: user.uid, 32 | symbol: stock.symbol, 33 | name: stock.name, 34 | }); 35 | } 36 | } else { 37 | const stored = JSON.parse(localStorage.getItem("watchlist")) || []; 38 | const exists = stored.find((s) => s.symbol === stock.symbol); 39 | 40 | if (exists) { 41 | const updated = stored.filter((s) => s.symbol !== stock.symbol); 42 | localStorage.setItem("watchlist", JSON.stringify(updated)); 43 | } else { 44 | localStorage.setItem("watchlist", JSON.stringify([...stored, stock])); 45 | } 46 | } 47 | }; 48 | 49 | export const getWatchlistSymbols = async () => { 50 | const user = auth.currentUser; 51 | 52 | if (user) { 53 | const q = query(collection(firestoreDb, "watchlist"), where("uid", "==", user.uid)); 54 | const snapshot = await getDocs(q); 55 | return snapshot.docs.map((doc) => doc.data().symbol); 56 | } else { 57 | const stored = JSON.parse(localStorage.getItem("watchlist")) || []; 58 | return stored.map((stock) => stock.symbol); 59 | } 60 | }; 61 | 62 | export const syncLocalToFirebase = async () => { 63 | const user = auth.currentUser; 64 | 65 | if (!user) return; 66 | 67 | const localWatchlist = JSON.parse(localStorage.getItem("watchlist")) || []; 68 | 69 | const q = query( 70 | collection(firestoreDb, "watchlist"), 71 | where("uid", "==", user.uid) 72 | ); 73 | const snapshot = await getDocs(q); 74 | const firebaseSymbols = snapshot.docs.map((doc) => doc.data().symbol); 75 | 76 | const newItems = localWatchlist.filter( 77 | (item) => !firebaseSymbols.includes(item.symbol) 78 | ); 79 | 80 | for (const stock of newItems) { 81 | await addDoc(collection(firestoreDb, "watchlist"), { 82 | uid: user.uid, 83 | symbol: stock.symbol, 84 | name: stock.name, 85 | }); 86 | } 87 | 88 | localStorage.removeItem("watchlist"); 89 | }; 90 | 91 | export const getWatchlist = async () => { 92 | const user = auth.currentUser; 93 | 94 | if (user) { 95 | const q = query(collection(firestoreDb, "watchlist"), where("uid", "==", user.uid)); 96 | const snapshot = await getDocs(q); 97 | return snapshot.docs.map((doc) => doc.data()); 98 | } else { 99 | const stored = JSON.parse(localStorage.getItem("watchlist")) || []; 100 | return stored; 101 | } 102 | }; 103 | 104 | export const removeStockFromWatchlist = async (symbol) => { 105 | const user = auth.currentUser; 106 | 107 | if (user) { 108 | const q = query( 109 | collection(firestoreDb, "watchlist"), 110 | where("uid", "==", user.uid), 111 | where("symbol", "==", symbol) 112 | ); 113 | const snapshot = await getDocs(q); 114 | 115 | await Promise.all( 116 | snapshot.docs.map((docSnap) => 117 | deleteDoc(doc(firestoreDb, "watchlist", docSnap.id)) 118 | ) 119 | ); 120 | } else { 121 | const stored = JSON.parse(localStorage.getItem("watchlist")) || []; 122 | const updated = stored.filter((s) => s.symbol !== symbol); 123 | localStorage.setItem("watchlist", JSON.stringify(updated)); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/BackToTopBtn.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { FaAnglesUp } from "react-icons/fa6"; 3 | 4 | // Simple throttle utility 5 | const throttle = (func, delay) => { 6 | let timeoutId; 7 | let lastExecTime = 0; 8 | return function (...args) { 9 | const currentTime = Date.now(); 10 | 11 | if (currentTime - lastExecTime > delay) { 12 | func.apply(this, args); 13 | lastExecTime = currentTime; 14 | } else { 15 | clearTimeout(timeoutId); 16 | timeoutId = setTimeout(() => { 17 | func.apply(this, args); 18 | lastExecTime = Date.now(); 19 | }, delay - (currentTime - lastExecTime)); 20 | } 21 | }; 22 | }; 23 | 24 | const BackToTopBtn = ({ 25 | threshold = 300, 26 | className = "", 27 | showTooltip = true 28 | }) => { 29 | const [isVisible, setIsVisible] = useState(false); 30 | const [isAnimating, setIsAnimating] = useState(false); 31 | 32 | // Throttled scroll handler for better performance 33 | const toggleVisibility = throttle(() => { 34 | setIsVisible(window.scrollY > threshold); 35 | }, 100); 36 | 37 | useEffect(() => { 38 | window.addEventListener('scroll', toggleVisibility); 39 | return () => window.removeEventListener('scroll', toggleVisibility); 40 | }, [toggleVisibility]); 41 | 42 | // Smooth scroll to top with animation state 43 | const scrollToTop = () => { 44 | setIsAnimating(true); 45 | window.scrollTo({ 46 | top: 0, 47 | behavior: 'smooth' 48 | }); 49 | 50 | // Reset animation state after scroll completes 51 | setTimeout(() => setIsAnimating(false), 1000); 52 | }; 53 | 54 | // Handle keyboard navigation 55 | const handleKeyDown = (e) => { 56 | if (e.key === 'Enter' || e.key === ' ') { 57 | e.preventDefault(); 58 | scrollToTop(); 59 | } 60 | }; 61 | 62 | if (!isVisible) return null; 63 | 64 | return ( 65 |
74 | 104 | 105 | {showTooltip && ( 106 |
124 | Back to top 125 |
126 | )} 127 |
128 | ); 129 | }; 130 | 131 | export default BackToTopBtn; -------------------------------------------------------------------------------- /src/components/Signup.css: -------------------------------------------------------------------------------- 1 | /* Signup.css */ 2 | 3 | .login-page { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | height: 100vh; 8 | background-image: url("/Images/bg.png"); 9 | background-size: cover; 10 | background-position: center; 11 | background-repeat: no-repeat; 12 | padding: 20px; 13 | } 14 | 15 | .login-container { 16 | background-color: #ffffff; 17 | padding: 40px; 18 | border-radius: 12px; 19 | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); 20 | width: 100%; 21 | max-width: 400px; 22 | box-sizing: border-box; 23 | text-align: center; 24 | } 25 | 26 | .login-container h2 { 27 | margin-bottom: 25px; 28 | color: #333; 29 | } 30 | 31 | .login-form input { 32 | width: 100%; 33 | padding: 12px 15px; 34 | margin: 10px 0; 35 | border: 1px solid #ccc; 36 | border-radius: 8px; 37 | font-size: 16px; 38 | transition: border 0.3s ease; 39 | } 40 | 41 | .login-form input:focus { 42 | border-color: #007bff; 43 | outline: none; 44 | } 45 | 46 | .login-error { 47 | color: #ff4d4d; 48 | margin-top: 5px; 49 | font-size: 14px; 50 | } 51 | 52 | .login-form button { 53 | width: 100%; 54 | padding: 12px; 55 | margin-top: 15px; 56 | background-color: #007bff; 57 | border: none; 58 | color: white; 59 | font-size: 16px; 60 | border-radius: 8px; 61 | cursor: pointer; 62 | transition: background-color 0.3s ease; 63 | } 64 | 65 | .login-form button:hover { 66 | background-color: #0056b3; 67 | } 68 | 69 | .login-container p { 70 | margin-top: 20px; 71 | font-size: 14px; 72 | } 73 | 74 | .login-container a { 75 | color: #007bff; 76 | text-decoration: none; 77 | font-weight: 500; 78 | } 79 | 80 | .login-container a:hover { 81 | text-decoration: underline; 82 | } 83 | 84 | .google-login-btn { 85 | width: 100%; 86 | padding: 12px; 87 | border: 1px solid #dadce0; 88 | border-radius: 4px; 89 | background-color: #fff; 90 | color: #3c4043; 91 | font-size: 14px; 92 | font-weight: 500; 93 | cursor: pointer; 94 | display: flex; 95 | align-items: center; 96 | justify-content: center; 97 | transition: background-color 0.2s ease, box-shadow 0.2s ease; 98 | margin-bottom: 20px; 99 | } 100 | 101 | .google-login-btn:hover:not(:disabled) { 102 | background-color: #f8f9fa; 103 | box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3); 104 | } 105 | 106 | .google-login-btn:active:not(:disabled) { 107 | background-color: #f1f3f4; 108 | } 109 | 110 | .google-login-btn:disabled { 111 | opacity: 0.6; 112 | cursor: not-allowed; 113 | } 114 | 115 | /* Add Google logo if desired */ 116 | .google-login-btn::before { 117 | content: ""; 118 | width: 18px; 119 | height: 18px; 120 | margin-right: 8px; 121 | background-image: url("data:image/svg+xml,%3csvg width='18' height='18' viewBox='0 0 18 18' xmlns='http://www.w3.org/2000/svg'%3e%3cg fill='none' fill-rule='evenodd'%3e%3cpath d='M17.64 9.205c0-.639-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615Z' fill='%234285F4'/%3e%3cpath d='M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18Z' fill='%2334A853'/%3e%3cpath d='M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332Z' fill='%23FBBC05'/%3e%3cpath d='M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58Z' fill='%23EA4335'/%3e%3c/g%3e%3c/svg%3e"); 122 | background-size: 18px 18px; 123 | background-repeat: no-repeat; 124 | } 125 | 126 | .divider { 127 | display: flex; 128 | align-items: center; 129 | text-align: center; 130 | margin: 20px 0; 131 | } 132 | 133 | .divider::before, 134 | .divider::after { 135 | content: ""; 136 | flex: 1; 137 | height: 1px; 138 | background: #dadce0; 139 | } 140 | 141 | .divider span { 142 | padding: 0 16px; 143 | color: #5f6368; 144 | font-size: 14px; 145 | } 146 | -------------------------------------------------------------------------------- /backend/services/stock_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | import json 4 | import plotly.express as px 5 | import plotly 6 | import logging 7 | from flask import jsonify 8 | from .sentiment_service import fetch_stock_news_with_sentiment 9 | 10 | DATA_FOLDER = os.path.join(os.path.dirname(__file__), "data") 11 | 12 | def normalize_symbol(symbol: str) -> str: 13 | """ 14 | Removes NSE (.NS) and BSE (.BO) suffixes to match CSV filenames. 15 | """ 16 | return symbol.replace(".NS", "").replace(".BO", "") 17 | 18 | def get_stock_data_handler(symbol, chart_period="1mo", table_period="1mo"): 19 | """ 20 | Handles GET request for stock data from local CSV files. 21 | Returns price chart, table, news with sentiment, and stock info in JSON format. 22 | """ 23 | try: 24 | # Normalize symbol to match CSV filename 25 | clean_symbol = normalize_symbol(symbol) 26 | 27 | # Path to stock CSV file 28 | csv_path = os.path.join(DATA_FOLDER, f"{clean_symbol}.csv") 29 | if not os.path.exists(csv_path): 30 | logging.error(f"CSV not found: {csv_path}") 31 | return jsonify({"error": f"No data found for ticker {clean_symbol}"}), 404 32 | 33 | # Load data 34 | df = pd.read_csv(csv_path) 35 | 36 | # Ensure proper datetime (dayfirst for DD-MM-YYYY format) 37 | if "Date" in df.columns: 38 | df["Date"] = pd.to_datetime(df["Date"], dayfirst=True, errors="coerce") 39 | else: 40 | return jsonify({"error": "CSV missing 'Date' column"}), 500 41 | 42 | # Drop invalid dates if any 43 | df = df.dropna(subset=["Date"]).sort_values("Date", ascending=True).reset_index(drop=True) 44 | 45 | if df.empty: 46 | return jsonify({"error": f"No valid rows found in {clean_symbol}.csv"}), 500 47 | 48 | # Extract latest row for stock info 49 | latest = df.iloc[-1] 50 | stock_basic_info = { 51 | "name": clean_symbol, 52 | "exchange": "Local CSV", 53 | "open": float(latest["Open"]) if "Open" in df.columns else 0, 54 | "close": float(latest["Close"]) if "Close" in df.columns else 0, 55 | "high": float(latest["High"]) if "High" in df.columns else 0, 56 | "low": float(latest["Low"]) if "Low" in df.columns else 0, 57 | "volume": int(latest["Volume"]) if "Volume" in df.columns else 0, 58 | "market_cap": 0, 59 | "pe_ratio": 0, 60 | "dividend_yield": 0 61 | } 62 | 63 | # For now, using same data for chart & table 64 | chart_data = df.copy() 65 | table_data = df.copy() 66 | 67 | # Format date for frontend 68 | chart_data["Date"] = chart_data["Date"].dt.strftime("%d-%m-%Y") 69 | table_data["Date"] = table_data["Date"].dt.strftime("%d-%m-%Y") 70 | 71 | # Generate line chart 72 | fig1 = px.line(chart_data, x="Date", y="Close", 73 | title=f"{clean_symbol} Stock Price Over Time", markers=True) 74 | fig1.update_layout(width=1200, height=600) 75 | fig1.update_xaxes(autorange="reversed") 76 | graphJSON1 = json.dumps(fig1, cls=plotly.utils.PlotlyJSONEncoder) 77 | 78 | # Generate area chart 79 | fig2 = px.area(chart_data, x="Date", y="Close", 80 | title=f"{clean_symbol} Stock Price Area Chart", markers=True) 81 | fig2.update_layout(width=1200, height=600) 82 | fig2.update_xaxes(autorange="reversed") 83 | graphJSON2 = json.dumps(fig2, cls=plotly.utils.PlotlyJSONEncoder) 84 | 85 | # Fetch news with sentiment analysis 86 | news_data = fetch_stock_news_with_sentiment(clean_symbol) 87 | 88 | return jsonify({ 89 | "stock_data": table_data.to_dict(orient="records"), 90 | "graph_data1": graphJSON1, 91 | "graph_data2": graphJSON2, 92 | "stock_info": stock_basic_info, 93 | "stock_news": news_data.get("articles", []), 94 | "sentiment_summary": news_data.get("sentiment_summary", {}), 95 | "chart_period": chart_period, 96 | "table_period": table_period 97 | }) 98 | 99 | except Exception as e: 100 | logging.exception(f"Error in get_stock_data_handler for {symbol}") 101 | return jsonify({"error": str(e)}), 500 102 | -------------------------------------------------------------------------------- /src/components/Footer.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | .footer { 9 | width: 100%; 10 | background: var(--color-bg-footer); 11 | color: var(--color-text); 12 | padding: 50px 20px 20px; 13 | font-family: Arial, sans-serif; 14 | border-top: 1px solid var(--color-border-footer); 15 | margin-top: auto; /* Push footer to bottom */ 16 | position: relative; 17 | z-index: 10; /* Ensure footer stays on top */ 18 | flex-shrink: 0; /* Prevent footer from shrinking */ 19 | } 20 | 21 | .footer-container { 22 | width: 90%; 23 | margin: auto; 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: flex-start; 27 | flex-wrap: wrap; 28 | position: relative; 29 | } 30 | 31 | .footer-main { 32 | display: flex; 33 | justify-content: space-between; 34 | flex-wrap: wrap; 35 | width: 100%; 36 | } 37 | 38 | /* Column generic */ 39 | .footer-column { 40 | flex-basis: 22%; 41 | padding: 10px; 42 | margin-bottom: 20px; 43 | } 44 | 45 | .footer-heading { 46 | font-size: 20px; 47 | margin-bottom: 15px; 48 | position: relative; 49 | color: var(--color-text); 50 | } 51 | 52 | .footer-heading::after { 53 | content: ""; 54 | display: block; 55 | height: 2px; 56 | width: 50px; 57 | background: var(--color-text); 58 | margin-top: 8px; 59 | } 60 | 61 | /* About section */ 62 | .footer-about .footer-logo { 63 | display: flex; 64 | align-items: center; 65 | gap: 8px; 66 | font-size: 20px; 67 | color: var(--color-text); 68 | font-weight: bold; 69 | text-decoration: none; 70 | margin-bottom: 10px; 71 | } 72 | 73 | .footer-description { 74 | font-size: 14px; 75 | line-height: 1.5; 76 | color: var(--color-text-secondary); 77 | } 78 | 79 | /* Links */ 80 | .footer-links { 81 | list-style: none; 82 | padding: 0; 83 | } 84 | 85 | .footer-links li { 86 | margin-bottom: 10px; 87 | } 88 | 89 | .footer-link { 90 | color: var(--color-text); 91 | text-decoration: none; 92 | font-size: 14px; 93 | } 94 | 95 | .footer-link:hover { 96 | color: var(--color-primary); 97 | transition: all 0.3s ease; 98 | } 99 | 100 | /* Social */ 101 | .social-links { 102 | display: flex; 103 | gap: 12px; 104 | margin-top: 10px; 105 | } 106 | 107 | .social-link { 108 | display: flex; 109 | align-items: center; 110 | justify-content: center; 111 | width: 38px; 112 | height: 38px; 113 | border-radius: 50%; 114 | background: var(--color-bg-secondary); 115 | color: var(--color-text); 116 | font-size: 18px; 117 | transition: all 0.3s ease; 118 | text-decoration: none; 119 | } 120 | 121 | .social-link:hover { 122 | background: var(--color-primary); 123 | color: var(--color-bg); 124 | } 125 | 126 | /* Contact info */ 127 | .footer-text { 128 | font-size: 14px; 129 | color: var(--color-text-secondary); 130 | margin-bottom: 6px; 131 | } 132 | 133 | /* Bottom section */ 134 | .footer-bottom { 135 | width: 100%; 136 | border-top: 1px solid var(--color-border-footer); 137 | text-align: center; 138 | padding-top: 15px; 139 | margin-top: 20px; 140 | font-size: 13px; 141 | color: var(--color-muted); 142 | } 143 | 144 | .footer-legal { 145 | margin-top: 8px; 146 | } 147 | 148 | .footer-legal-link { 149 | color: var(--color-muted); 150 | text-decoration: none; 151 | margin: 0 6px; 152 | font-size: 13px; 153 | } 154 | 155 | .footer-legal-link:hover { 156 | color: var(--color-primary); 157 | } 158 | 159 | .footer-legal-separator { 160 | margin: 0 4px; 161 | color: var(--color-border-footer); 162 | } 163 | 164 | /* Responsive Design */ 165 | @media (max-width: 768px) { 166 | .footer { 167 | padding: 30px 15px 15px; 168 | } 169 | 170 | .footer-container { 171 | width: 95%; 172 | } 173 | 174 | .footer-main { 175 | flex-direction: column; 176 | gap: 20px; 177 | } 178 | 179 | .footer-column { 180 | flex-basis: 100%; 181 | margin-bottom: 15px; 182 | } 183 | 184 | .footer-bottom { 185 | text-align: center; 186 | } 187 | 188 | .footer-legal { 189 | margin-top: 10px; 190 | } 191 | } 192 | 193 | @media (max-width: 480px) { 194 | .social-links { 195 | justify-content: center; 196 | } 197 | 198 | .footer-legal { 199 | flex-direction: column; 200 | gap: 5px; 201 | } 202 | 203 | .footer-legal-separator { 204 | display: none; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /backend/utils/cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Caching utilities with Redis backend and safe in-memory fallback. 3 | 4 | Env vars: 5 | - CACHE_ENABLED (default: true) 6 | - REDIS_URL (default: redis://localhost:6379/0) 7 | - CACHE_TTL_STOCK (default: 900 seconds) 8 | - CACHE_TTL_PRED (default: 3600 seconds) 9 | 10 | Functions: 11 | - get_cache(key) -> Optional[str] 12 | - set_cache(key, value, ttl=None) -> bool 13 | - cache_key_from(path: str, params: dict) -> str 14 | - is_cache_enabled() -> bool 15 | 16 | Adds X-Cache header handling at call sites; this module only manages storage. 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | import os 22 | import time 23 | import json 24 | import logging 25 | from typing import Optional, Dict, Any 26 | 27 | try: 28 | import redis # type: ignore 29 | except Exception: # pragma: no cover - redis optional at runtime 30 | redis = None # type: ignore 31 | 32 | _logger = logging.getLogger(__name__) 33 | 34 | 35 | def _str2bool(v: Optional[str], default: bool = True) -> bool: 36 | if v is None: 37 | return default 38 | return str(v).strip().lower() in {"1", "true", "yes", "on"} 39 | 40 | 41 | CACHE_ENABLED: bool = _str2bool(os.getenv("CACHE_ENABLED"), True) 42 | REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0") 43 | DEFAULT_TTL_STOCK: int = int(os.getenv("CACHE_TTL_STOCK", "900")) 44 | DEFAULT_TTL_PRED: int = int(os.getenv("CACHE_TTL_PRED", "3600")) 45 | 46 | _redis_client = None 47 | _memory_cache: Dict[str, Any] = {} 48 | 49 | 50 | def is_cache_enabled() -> bool: 51 | return CACHE_ENABLED 52 | 53 | 54 | def _get_redis_client(): 55 | global _redis_client 56 | if not CACHE_ENABLED: 57 | return None 58 | if redis is None: 59 | return None 60 | if _redis_client is not None: 61 | return _redis_client 62 | try: 63 | client = redis.Redis.from_url(REDIS_URL, decode_responses=True) 64 | # Validate connectivity early 65 | client.ping() 66 | _redis_client = client 67 | _logger.info("Redis cache connected") 68 | return _redis_client 69 | except Exception as e: # pragma: no cover - depends on env 70 | _logger.warning(f"Redis unavailable ({e}); falling back to in-memory cache") 71 | return None 72 | 73 | 74 | def get_cache(key: str) -> Optional[str]: 75 | """Return cached string payload if present and not expired.""" 76 | if not CACHE_ENABLED: 77 | return None 78 | 79 | client = _get_redis_client() 80 | if client is not None: 81 | try: 82 | result = client.get(key) 83 | return str(result) if result is not None else None 84 | except Exception as e: # pragma: no cover 85 | _logger.warning(f"Redis GET failed: {e}") 86 | return None 87 | 88 | # Fallback: in-memory cache with TTL 89 | entry = _memory_cache.get(key) 90 | if not entry: 91 | return None 92 | value, expires_at = entry 93 | if expires_at is not None and time.time() > expires_at: 94 | # Expired; remove 95 | _memory_cache.pop(key, None) 96 | return None 97 | return value 98 | 99 | 100 | def set_cache(key: str, value: str, ttl: Optional[int] = None) -> bool: 101 | """Set cache value with optional TTL (seconds).""" 102 | if not CACHE_ENABLED: 103 | return False 104 | 105 | client = _get_redis_client() 106 | if client is not None: 107 | try: 108 | if ttl: 109 | client.setex(key, ttl, value) 110 | else: 111 | client.set(key, value) 112 | return True 113 | except Exception as e: # pragma: no cover 114 | _logger.warning(f"Redis SET failed: {e}") 115 | 116 | # Fallback: in-memory 117 | expires_at = (time.time() + ttl) if ttl else None 118 | _memory_cache[key] = (value, expires_at) 119 | return True 120 | 121 | 122 | def cache_key_from(path: str, params: Dict[str, Any]) -> str: 123 | """Stable composite key using sorted params.""" 124 | items = [] 125 | for k in sorted(params.keys()): 126 | v = params[k] 127 | if isinstance(v, (dict, list)): 128 | v = json.dumps(v, sort_keys=True) 129 | items.append(f"{k}={v}") 130 | param_str = "&".join(items) 131 | return f"resp:{path}?{param_str}" 132 | 133 | 134 | __all__ = [ 135 | "get_cache", 136 | "set_cache", 137 | "cache_key_from", 138 | "is_cache_enabled", 139 | "DEFAULT_TTL_STOCK", 140 | "DEFAULT_TTL_PRED", 141 | ] 142 | -------------------------------------------------------------------------------- /src/components/ContactForm.module.css: -------------------------------------------------------------------------------- 1 | .contact-container { 2 | width: 100%; 3 | min-height: 100vh; 4 | padding: 40px 20px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | background: var(--background-color); 9 | color: var(--text-color); 10 | transition: background 0.3s ease, color 0.3s ease; 11 | } 12 | 13 | :root { 14 | --background-color: #f9f9f9; 15 | --text-color: #1a1a1a; 16 | --card-bg: #ffffff; 17 | --input-bg: #ffffff; 18 | --input-border: #ccc; 19 | --error-color: #e63946; 20 | --success-color: #2a9d8f; 21 | --button-bg: #007bff; 22 | --button-hover: #0056b3; 23 | } 24 | 25 | [data-theme="dark"] { 26 | --background-color: #121212; 27 | --text-color: #f1f1f1; 28 | --card-bg: #1e1e1e; 29 | --input-bg: #2c2c2c; 30 | --input-border: #555; 31 | --error-color: #ff6b6b; 32 | --success-color: #4ade80; 33 | --button-bg: #2563eb; 34 | --button-hover: #1d4ed8; 35 | } 36 | 37 | .contact-main { 38 | display: flex; 39 | flex-wrap: wrap; 40 | max-width: 1100px; 41 | width: 100%; 42 | background: var(--card-bg); 43 | border-radius: 12px; 44 | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); 45 | overflow: hidden; 46 | } 47 | 48 | .contact-left { 49 | flex: 1; 50 | padding: 40px; 51 | background: linear-gradient(135deg, var(--button-bg), var(--button-hover)); 52 | color: #fff; 53 | } 54 | 55 | .contact-left h2, 56 | .contact-left p, 57 | .contact-left li { 58 | color: #fff; 59 | } 60 | 61 | .contact-title { 62 | font-size: 26px; 63 | margin-bottom: 15px; 64 | font-weight: 700; 65 | } 66 | 67 | .contact-description { 68 | font-size: 15px; 69 | margin-bottom: 20px; 70 | line-height: 1.6; 71 | } 72 | 73 | .contact-info { 74 | list-style: none; 75 | padding: 0; 76 | margin: 0; 77 | } 78 | 79 | .contact-info li { 80 | margin-bottom: 12px; 81 | font-size: 14px; 82 | display: flex; 83 | align-items: center; 84 | } 85 | 86 | .contact-info li span { 87 | margin-right: 10px; 88 | font-size: 16px; 89 | } 90 | 91 | .contact-right { 92 | flex: 1.2; 93 | padding: 40px; 94 | } 95 | 96 | .contact-form-title { 97 | font-size: 26px; 98 | margin-bottom: 15px; 99 | font-weight: 700; 100 | color: var(--text-color); 101 | } 102 | 103 | .contact-form { 104 | display: flex; 105 | flex-direction: column; 106 | } 107 | 108 | .contact-row { 109 | display: flex; 110 | gap: 15px; 111 | margin-bottom: 15px; 112 | } 113 | 114 | .form-group { 115 | flex: 1; 116 | display: flex; 117 | flex-direction: column; 118 | } 119 | 120 | .contact-form input, 121 | .contact-form textarea { 122 | padding: 12px; 123 | border-radius: 8px; 124 | border: 1px solid var(--input-border); 125 | background: var(--input-bg); 126 | color: var(--text-color); 127 | font-size: 14px; 128 | transition: all 0.3s ease; 129 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); 130 | } 131 | 132 | .contact-form input:focus, 133 | .contact-form textarea:focus { 134 | border-color: var(--button-bg); 135 | outline: none; 136 | background: var(--card-bg); 137 | box-shadow: 0 0 8px rgba(37, 99, 235, 0.4); 138 | } 139 | 140 | textarea { 141 | resize: none; 142 | margin-top: 30px; 143 | } 144 | 145 | .contact-error { 146 | color: var(--error-color); 147 | font-size: 12px; 148 | margin-top: 4px; 149 | } 150 | 151 | button { 152 | margin-top: 15px; 153 | padding: 14px; 154 | background: linear-gradient(90deg, #ff7e5f, #feb47b); 155 | color: #fff; 156 | border: none; 157 | border-radius: 30px; 158 | font-size: 15px; 159 | font-weight: 600; 160 | cursor: pointer; 161 | transition: all 0.3s ease; 162 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 163 | } 164 | 165 | button:hover { 166 | transform: translateY(-2px); 167 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.3); 168 | } 169 | 170 | button:disabled { 171 | opacity: 0.6; 172 | cursor: not-allowed; 173 | } 174 | 175 | .contact-status { 176 | margin-top: 15px; 177 | font-size: 14px; 178 | font-weight: 500; 179 | } 180 | 181 | .success { 182 | color: var(--success-color); 183 | } 184 | 185 | .error { 186 | color: var(--error-color); 187 | } 188 | 189 | @media (max-width: 900px) { 190 | .contact-main { 191 | flex-direction: column; 192 | } 193 | 194 | .contact-left, 195 | .contact-right { 196 | padding: 25px; 197 | } 198 | 199 | .contact-row { 200 | flex-direction: column; 201 | } 202 | } 203 | 204 | @media (max-width: 600px) { 205 | .contact-left, 206 | .contact-right { 207 | padding: 20px; 208 | } 209 | 210 | .contact-form input, 211 | .contact-form textarea { 212 | font-size: 13px; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/components/Prediction.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import axios from "axios"; 3 | import Plot from "react-plotly.js"; 4 | import { ClipLoader } from "react-spinners"; // Import ClipLoader 5 | 6 | function Prediction({ ticker }) { 7 | const [predictedData, setPredictedData] = useState([]); 8 | const [predictedDates, setPredictedDates] = useState([]); 9 | const [actualData, setActualData] = useState([]); 10 | const [actualDates, setActualDates] = useState([]); 11 | const [returns, setReturns] = useState([]); 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const fetchPredictionData = useCallback(async () => { 15 | setIsLoading(true); // Start loading 16 | try { 17 | const res = await axios.get( 18 | `${process.env.REACT_APP_API_URL}/api/stock/${ticker}/predict` //${process.env.REACT_APP_API_URL} 19 | ); 20 | setPredictedData(res.data.predictions || []); 21 | setPredictedDates(res.data.predicted_dates || []); // Updated 22 | setActualData(res.data.actual || []); 23 | setActualDates(res.data.actual_dates || []); // Updated 24 | setReturns(res.data.returns || []); 25 | } catch (error) { 26 | console.error("Error fetching prediction data:", error); 27 | } finally { 28 | setIsLoading(false); // Stop loading 29 | } 30 | }, [ticker]); 31 | 32 | useEffect(() => { 33 | if (ticker) { 34 | fetchPredictionData(); 35 | } 36 | }, [ticker, fetchPredictionData]); 37 | 38 | return ( 39 |
40 |
41 | {isLoading ? ( 42 |
43 | {/* Loading animation */} 44 |

Loading prediction data...

45 |
46 | ) : ( 47 | <> 48 |
49 |

Predicted vs Actual Stock Prices for {ticker}

50 | 97 |
98 |
99 |

Investment Returns

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {returns.map((item, index) => ( 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | ))} 120 | 121 |
Stocks BoughtCurrent PriceAfter 1 YearAfter 5 YearsAfter 10 Years
{item.stocks_bought}{item.current_price}{item.after_1_year}{item.after_5_years}{item.after_10_years}
122 |
123 | 124 | )} 125 |
126 |
127 | ); 128 | } 129 | 130 | export default Prediction; 131 | -------------------------------------------------------------------------------- /src/components/StockList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { ClipLoader } from "react-spinners"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { toggleWatchlist } from "../utils/watchlistManager"; 6 | import { auth } from "./firebase"; 7 | import stockData from "./data/stockData.json"; 8 | import BackToTopBtn from "./BackToTopBtn"; 9 | import styles from "./StockList.module.css"; 10 | import stockBg2 from "./images/bg.png"; 11 | 12 | const StocksList = () => { 13 | const [stocks, setStocks] = useState([]); 14 | const [exchange, setExchange] = useState("BSE"); 15 | const [searchTicker, setSearchTicker] = useState(""); 16 | const [isLoading, setIsLoading] = useState(false); 17 | const navigate = useNavigate(); 18 | 19 | useEffect(() => { 20 | setIsLoading(true); 21 | setTimeout(() => { 22 | setStocks(stockData[exchange] || []); 23 | setIsLoading(false); 24 | }, 500); 25 | }, [exchange]); 26 | 27 | const handleSearch = () => { 28 | if (searchTicker.trim()) { 29 | navigate(`/stock/${searchTicker.trim()}`); 30 | } 31 | }; 32 | 33 | const handleAddToWatchlist = async (stock) => { 34 | const user = auth.currentUser; 35 | if (user) { 36 | try { 37 | await toggleWatchlist(stock); 38 | alert(`${stock.symbol} added to your Firebase watchlist!`); 39 | } catch (err) { 40 | alert("Failed to add to watchlist."); 41 | console.error(err); 42 | } 43 | } else { 44 | navigate("/login", { 45 | state: { message: "Please log in to use the watchlist." }, 46 | }); 47 | } 48 | }; 49 | 50 | return ( 51 | 56 | {/* 🚀 Hero Section */} 57 |
66 |

Welcome to Stock Analyzer!

67 |

Track, analyze, and manage your favorite stocks with ease.

68 |
69 | 70 | {/* 🔍 Modern Search Bar */} 71 |
72 | setSearchTicker(e.target.value)} 77 | className={styles.searchInput} 78 | /> 79 | 82 |
83 | 84 | {/* 🏦 Exchange Toggle */} 85 |
86 | {["BSE", "NSE"].map((exchangeName) => ( 87 | 96 | ))} 97 |
98 | 99 | {/* 📊 Stocks as Cards */} 100 | 101 | {isLoading ? ( 102 |
103 | 104 |

Loading stocks...

105 |
106 | ) : ( 107 |
108 | {stocks.map((stock, index) => ( 109 | navigate(`/stock/${stock.symbol}`)} 115 | > 116 |
📈
117 |

{stock.symbol}

118 |

{stock.name}

119 | 128 |
129 | ))} 130 |
131 | )} 132 |
133 | 134 | 135 |
136 | ); 137 | }; 138 | 139 | export default StocksList; 140 | -------------------------------------------------------------------------------- /backend/routes/stock_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, make_response, Response 2 | from services.stock_service import get_stock_data_handler 3 | from services.stock_predict import predict_stock_handler 4 | from utils.cache import ( 5 | get_cache, 6 | set_cache, 7 | cache_key_from, 8 | is_cache_enabled, 9 | DEFAULT_TTL_STOCK, 10 | DEFAULT_TTL_PRED, 11 | ) 12 | 13 | stock_routes = Blueprint('stock_routes', __name__) 14 | 15 | # GET /api/stock/ 16 | @stock_routes.route('/stock/', methods=['GET']) 17 | def get_stock_data(symbol): 18 | chart_period = request.args.get("chart_period", "1mo") 19 | table_period = request.args.get("table_period", "1mo") 20 | refresh = request.args.get("refresh", "false").lower() in ("1", "true", "yes", "on") 21 | 22 | # Build cache key 23 | key = cache_key_from( 24 | f"/api/stock/{symbol}", 25 | {"chart_period": chart_period, "table_period": table_period} 26 | ) 27 | 28 | # Attempt cache HIT unless refresh requested 29 | if is_cache_enabled() and not refresh: 30 | cached = get_cache(key) 31 | if cached: 32 | resp = make_response(cached) 33 | resp.headers["Content-Type"] = "application/json" 34 | resp.headers["X-Cache"] = "HIT" 35 | return resp 36 | 37 | # Miss or bypass -> compute 38 | result = get_stock_data_handler(symbol, chart_period, table_period) 39 | 40 | # Normalize to Response object (supports Response or tuple variants) 41 | resp: Response 42 | if isinstance(result, Response): 43 | resp = result 44 | elif isinstance(result, tuple): 45 | body = result[0] 46 | status = result[1] if len(result) > 1 else 200 47 | headers = result[2] if len(result) > 2 else {} 48 | if isinstance(body, Response): 49 | resp = body 50 | resp.status_code = status 51 | for k, v in (headers or {}).items(): 52 | try: 53 | resp.headers[k] = v 54 | except Exception: 55 | pass 56 | else: 57 | resp = make_response(body, status) 58 | for k, v in (headers or {}).items(): 59 | try: 60 | resp.headers[k] = v 61 | except Exception: 62 | pass 63 | else: 64 | # Fallback 65 | resp = make_response(result) 66 | 67 | # Cache successful responses 68 | if is_cache_enabled() and getattr(resp, "status_code", 200) == 200: 69 | try: 70 | payload = resp.get_data(as_text=True) 71 | set_cache(key, payload, ttl=DEFAULT_TTL_STOCK) 72 | except Exception: 73 | pass 74 | 75 | try: 76 | if hasattr(resp, 'headers'): 77 | resp.headers["X-Cache"] = "MISS" if not refresh else "BYPASS" 78 | except Exception: 79 | pass 80 | return resp 81 | 82 | # GET /api/stock//predict 83 | @stock_routes.route('/stock//predict', methods=['GET']) 84 | def predict(symbol): 85 | refresh = request.args.get("refresh", "false").lower() in ("1", "true", "yes", "on") 86 | 87 | key = cache_key_from(f"/api/stock/{symbol}/predict", {}) 88 | 89 | if is_cache_enabled() and not refresh: 90 | cached = get_cache(key) 91 | if cached: 92 | resp = make_response(cached) 93 | resp.headers["Content-Type"] = "application/json" 94 | resp.headers["X-Cache"] = "HIT" 95 | return resp 96 | 97 | result = predict_stock_handler(symbol) 98 | 99 | # Normalize to Response 100 | if isinstance(result, Response): 101 | resp = result 102 | elif isinstance(result, tuple): 103 | body = result[0] 104 | status = result[1] if len(result) > 1 else 200 105 | headers = result[2] if len(result) > 2 else {} 106 | if isinstance(body, Response): 107 | resp = body 108 | resp.status_code = status 109 | for k, v in (headers or {}).items(): 110 | try: 111 | resp.headers[k] = v 112 | except Exception: 113 | pass 114 | else: 115 | resp = make_response(body, status) 116 | for k, v in (headers or {}).items(): 117 | try: 118 | resp.headers[k] = v 119 | except Exception: 120 | pass 121 | else: 122 | resp = make_response(result) 123 | 124 | if is_cache_enabled() and getattr(resp, "status_code", 200) == 200: 125 | try: 126 | payload = resp.get_data(as_text=True) 127 | set_cache(key, payload, ttl=DEFAULT_TTL_PRED) 128 | except Exception: 129 | pass 130 | 131 | try: 132 | resp.headers["X-Cache"] = "MISS" if not refresh else "BYPASS" 133 | except Exception: 134 | pass 135 | return resp 136 | -------------------------------------------------------------------------------- /src/components/About.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FaChartLine, FaClock, FaRobot } from "react-icons/fa"; 3 | import { MdOutlineDesignServices } from "react-icons/md"; 4 | import { FiRefreshCcw } from "react-icons/fi"; 5 | import { Link } from "react-router-dom"; 6 | import "../App.css"; 7 | import BackToTopBtn from "./BackToTopBtn"; 8 | 9 | const AboutComponent = () => { 10 | const iconColor = "var(--color-primary)"; 11 | const headingStyle = { color: "var(--color-primary)" }; 12 | 13 | return ( 14 |
15 |
16 |

About Us

17 |

Welcome to our AI Stock Analyzer

18 |

Your personal companion for real-time stock insights.

19 |

20 | Our app offers real-time stock data, user-friendly interfaces, and 21 | secure authentication to ensure a seamless trading experience. 22 |

23 |

24 | Whether you are a beginner or an experienced trader, our platform is 25 | designed to help you make informed decisions and manage your 26 | investments effectively. 27 |

28 | 29 | {/* mission & vision */} 30 |
31 |
32 |

Our Mission

33 |

34 | To deliver reliable and data-driven stock analysis through innovative tools and insights, we are committed to empowering investors, learners and financial professionals with transparent , accurate and user-friendly solutions that support smarter investment decisions. 35 |

36 |
37 |
38 |

Our Vision

39 |

40 | To be recognized as a leading global platform for stock market analysis - where innovation, trust, and accessibility come together to help evry individual and organization achieve long-term financial success. 41 |

42 |
43 |
44 | 45 | {/* what we offer */} 46 |
47 |

What we offer

48 |
49 |
50 | 51 |

Real-Time Data

52 |

53 | Access live stock prices and market trends to stay ahead in your 54 | trading journey. 55 |

56 |
57 |
58 | 59 |

User-Friendly Interface

60 |

61 | Navigate through our intuitive design that makes trading easy and 62 | efficient. 63 |

64 |
65 |
66 | 67 |

AI-Powered Predictions

68 |

Accurate forecasts using Machine Learning and AI models.

69 |
70 |
71 | 72 |

Market Trend Analysis

73 |

Clear visuals to show current and future trends.

74 |
75 |
76 | 77 |

Continuous Updates

78 |

79 | We are constantly improving our app to provide you with the best 80 | trading experience. 81 |

82 |
83 |
84 | 85 |

24/7 Data Monitoring

86 |

Constant tracking for updated insights.

87 |
88 |
89 |
90 | 91 | {/* why choose stock analyzer */} 92 |
93 |

Why Choose Stock Analyzer?

94 |
    95 |
  • ✅ Trusted by over 500,000 users worldwide
  • 96 |
  • ✅ Backed by a team of expert analysts and engineers
  • 97 |
  • ✅ Secure, fast, and reliable platform
  • 98 |
  • ✅ Continuous innovation and feature updates
  • 99 |
100 |
101 | 102 |
103 | 104 | 105 | 106 |
107 |
108 | 109 |
110 | ); 111 | }; 112 | 113 | export default AboutComponent; 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Stock Analyzer 2 | 3 | The **Stock Analyzer** project is a complete stock market analysis tool utilizing ML models. It allows users to input stock symbols, select date ranges, view historical stock trends, and see future stock price predictions via interactive charts. 4 | 5 |
8 | Forks 9 | Issues 10 | Issues Closed 11 | Open Pull Requests 12 | Close Pull Requests 13 |
14 | 15 | ## Live Demo 16 | 17 | [View Deployed App on Render](https://aistockanalyzer.onrender.com) 18 | 19 | --- 20 | 21 | ![Open Source Connect India](Images/osconnect.png) 22 | #OSCI #OpenSourceConnect 23 | 24 | ## Overview 25 | 26 | ### Home Page 27 | 28 | ![Home Page](Images/home.png) 29 | 30 | ### Stock Analysis View 31 | 32 | ![Stock Analysis](Images/main.png) 33 | 34 | ### Stock Predictions 35 | 36 | ## ![Stock prediction](Images/prediction.png) 37 | 38 | ## Technologies Used 39 | 40 | - **React:** User Interface 41 | - **Plotly.js:** Interactive visualizations 42 | - **Axios:** API calls 43 | - **Flask:** Backend Framework 44 | - **yfinance:** Stock data extraction 45 | - **Pandas:** Data manipulation 46 | 47 | --- 48 | 49 | ## Directory Structure 50 | 51 | ``` 52 | Directory structure: 53 | └── srigadaakshaykumar-stock/ 54 | ├── LICENSE 55 | ├── README.md 56 | ├── SETUP.md 57 | ├── CONTRIBUTION.md 58 | ├── CODE_OF_CONDUCT.md 59 | ├── SECURITY.md 60 | ├── package-lock.json 61 | ├── package.json 62 | ├── static.json 63 | ├── backend/ 64 | ├──app/ 65 | ├──data/ 66 | │ ├── app.py 67 | | ├── generate_csvs.py 68 | │ ├── requirements.txt 69 | │ ├── stock-prediction.ipynb 70 | │ └── tf.keras 71 | ├── public/ 72 | | ├── icon.png 73 | │ ├── index.html 74 | │ ├── manifest.json 75 | │ └── robots.txt 76 | └── src/ 77 | ├── App.css 78 | ├── App.js 79 | ├── App.test.js 80 | ├── index.css 81 | ├── index.js 82 | ├── reportWebVitals.js 83 | ├── setupTests.js 84 | └── components/ 85 | ├── About.jsx 86 | ├── AuthContext.jsx 87 | ├── BackToTopBtn.jsx 88 | ├── ContactForm.jsx 89 | ├── firebase.js 90 | ├── Footer.css 91 | ├── Footer.jsx 92 | ├── Header.jsx 93 | ├── Login.css 94 | ├── Login.js 95 | ├── Prediction.jsx 96 | ├── SentimentChart.jsx 97 | ├── SignUp.css 98 | ├── Signup.jsx 99 | ├── Stockdata.jsx 100 | ├── StockMetricCard.jsx 101 | ├── StockList.jsx 102 | └── data/ 103 | └── stockData.json 104 | ``` 105 | 106 | --- 107 | 108 | ## API Endpoints 109 | 110 | | **Endpoint** | **Method** | **Description** | 111 | | ----------------------- | ---------- | --------------------------- | 112 | | `/api/stocks/` | GET | Fetch historical stock data | 113 | | `/api/predict/` | GET | Predict future stock prices | 114 | 115 | --- 116 | 117 | ## Data Pipeline Architecture 118 | 119 | ![Home Page](Images/dataline.png) 120 | 121 | --- 122 | 123 | ## Project Status 124 | 125 | **Stock Analyzer** is currently in the **development stage** and hosted on a free hosting service for testing purposes. 126 | 127 | ## The latest pulls are merged every Saturday. 128 | 129 | ## Future Enhancements 130 | 131 | We have a clear roadmap for improvements: 132 | 133 | - Allow more API calls per day 134 | - Reduce response time for end users 135 | - Add International stock exchanges 136 | - Enhance the user interface for better experience 137 | - Improve machine learning model accuracy 138 | - Provide more insightful and interactive visualizations 139 | - Migrate deployment from Render to Google Cloud Platform (GCP) 140 | 141 | ## Contributions 142 | 143 | We welcome all forms of open-source contributions — whether it's a: 144 | 145 | - Bug fix 146 | - New feature 147 | - Enhancement or optimization 148 | 149 | Please make sure to: 150 | 151 | - Review our [Contribution Guidelines](./CONTRIBUTION.md) 152 | - Follow the [Setup Instructions](./SETUP.md) to run the project locally 153 | - Join the [Discord](https://discord.gg/ypQSaPbsDv) 154 | 155 | ![Open Source Connect India](Images/osconnect.png) 156 | 157 | ## License 158 | 159 | This project is licensed under the [MIT License](LICENSE). 160 | You’re free to use, modify, and share this software under the license terms. 161 | -------------------------------------------------------------------------------- /src/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | createUserWithEmailAndPassword, 4 | signInWithPopup, 5 | GoogleAuthProvider, 6 | } from "firebase/auth"; 7 | import { auth, realtimeDb as database } from "./firebase"; 8 | import { ref, set } from "firebase/database"; 9 | import { useNavigate, Link } from "react-router-dom"; 10 | import { syncLocalToFirebase } from "../utils/watchlistManager"; 11 | import { toast } from "react-toastify"; 12 | import "react-toastify/dist/ReactToastify.css"; 13 | import { useAuth } from "./AuthContext"; 14 | import "./Signup.css"; 15 | 16 | const Signup = () => { 17 | const [email, setEmail] = useState(""); 18 | const [password, setPassword] = useState(""); 19 | const [confirmPassword, setConfirmPassword] = useState(""); 20 | const [error, setError] = useState(""); 21 | const [loading, setLoading] = useState(false); 22 | 23 | const { user } = useAuth(); // अगर AuthContext यूज कर रहे हैं तो 24 | const navigate = useNavigate(); 25 | 26 | // अगर user पहले से logged in है तो redirect कर दें 27 | useEffect(() => { 28 | if (user) { 29 | navigate("/"); 30 | } 31 | }, [user, navigate]); 32 | 33 | const handleSignup = async (e) => { 34 | e.preventDefault(); 35 | setError(""); 36 | 37 | if (password !== confirmPassword) { 38 | setError("Passwords do not match"); 39 | return; 40 | } 41 | 42 | setLoading(true); 43 | 44 | try { 45 | const userCredential = await createUserWithEmailAndPassword(auth, email, password); 46 | const user = userCredential.user; 47 | 48 | // Firebase Realtime Database में user data सेव करें 49 | await set(ref(database, `users/${user.uid}`), { 50 | email: user.email, 51 | createdAt: new Date().toISOString(), 52 | }); 53 | 54 | // Local watchlist Firebase में sync करें 55 | await syncLocalToFirebase(user); 56 | 57 | toast.success("Signup successful!"); 58 | navigate("/"); // Signup के बाद home page पर redirect करें 59 | } catch (err) { 60 | setError(err.message); 61 | console.error("Signup error:", err); 62 | } finally { 63 | setLoading(false); 64 | } 65 | }; 66 | 67 | const handleGoogleSignup = async () => { 68 | setError(""); 69 | setLoading(true); 70 | 71 | try { 72 | const provider = new GoogleAuthProvider(); 73 | const result = await signInWithPopup(auth, provider); 74 | const user = result.user; 75 | 76 | await set(ref(database, `users/${user.uid}`), { 77 | email: user.email, 78 | displayName: user.displayName, 79 | photoURL: user.photoURL, 80 | createdAt: new Date().toISOString(), 81 | }); 82 | 83 | await syncLocalToFirebase(user); 84 | 85 | toast.success("Signup successful with Google!"); 86 | navigate("/"); // Google signup के बाद redirect 87 | } catch (err) { 88 | const errorCode = err.code; 89 | 90 | if (errorCode === "auth/popup-closed-by-user") { 91 | setError("Sign-up cancelled by user"); 92 | } else if (errorCode === "auth/popup-blocked") { 93 | setError("Popup blocked by browser. Please allow popups and try again."); 94 | } else if (errorCode === "auth/account-exists-with-different-credential") { 95 | setError("An account already exists with this email. Try signing in instead."); 96 | } else { 97 | setError(err.message); 98 | } 99 | console.error("Google Signup error:", err); 100 | } finally { 101 | setLoading(false); 102 | } 103 | }; 104 | 105 | return ( 106 |
107 |
108 |

Sign Up

109 | 110 | 118 | 119 |
120 | or 121 |
122 | 123 |
124 | setEmail(e.target.value)} 129 | required 130 | disabled={loading} 131 | /> 132 | setPassword(e.target.value)} 137 | onPaste={(e) => e.preventDefault()} 138 | required 139 | disabled={loading} 140 | minLength={6} 141 | /> 142 | setConfirmPassword(e.target.value)} 147 | onPaste={(e) => e.preventDefault()} 148 | required 149 | disabled={loading} 150 | minLength={6} 151 | /> 152 | {error &&

{error}

} 153 | 156 |
157 | 158 |

159 | Already have an account? Login 160 |

161 |
162 |
163 | ); 164 | }; 165 | 166 | export default Signup; 167 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Stock Analyzer – Project Setup Guide 2 | 3 | Welcome to the Stock Analyzer project! Follow the steps below to set up the project locally and start contributing or testing. 4 | 5 | ## Prerequisites 6 | 7 | Make sure you have the following installed on your system: 8 | 9 | - *Python 3.11.+* 10 | - *Node.js and npm* 11 | - *Git* 12 | 13 | ## Project Structure 14 | 15 | 16 | Directory structure: 17 | └── srigadaakshaykumar-stock/ 18 | ├── LICENSE 19 | ├── README.md 20 | ├── SETUP.md 21 | ├── CONTRIBUTION.md 22 | ├── CODE_OF_CONDUCT.md 23 | ├── SECURITY.md 24 | ├── package-lock.json 25 | ├── package.json 26 | ├── static.json 27 | ├── backend/ 28 | ├──app/ 29 | ├──data/ 30 | │ ├── app.py 31 | | ├── generate_csvs.py 32 | │ ├── requirements.txt 33 | │ ├── stock-prediction.ipynb 34 | │ └── tf.keras 35 | ├── public/ 36 | | ├── icon.png 37 | │ ├── index.html 38 | │ ├── manifest.json 39 | │ └── robots.txt 40 | └── src/ 41 | ├── App.css 42 | ├── App.js 43 | ├── App.test.js 44 | ├── index.css 45 | ├── index.js 46 | ├── reportWebVitals.js 47 | ├── setupTests.js 48 | └── components/ 49 | ├── About.jsx 50 | ├── AuthContext.jsx 51 | ├── BackToTopBtn.jsx 52 | ├── ContactForm.jsx 53 | ├── firebase.js 54 | ├── Footer.css 55 | ├── Footer.jsx 56 | ├── Header.jsx 57 | ├── Login.css 58 | ├── Login.js 59 | ├── Prediction.jsx 60 | ├── SentimentChart.jsx 61 | ├── SignUp.css 62 | ├── Signup.jsx 63 | ├── Stockdata.jsx 64 | ├── StockMetricCard.jsx 65 | ├── StockList.jsx 66 | └── data/ 67 | └── stockData.json 68 | 69 | 70 | 1. *Fork the repository* 71 | Fork the project to your github account 72 | 73 | 2. *Clone the repository* 74 | 75 | bash 76 | git clone https://github.com/yourusername/stock.git 77 | 78 | 79 | ## Backend Setup (Flask) 80 | 81 | Navigate to backend folder 82 | 83 | bash 84 | cd stock/backend 85 | 86 | 87 | 1. *Create a Virtual Environemnt* 88 | 89 | bash 90 | python -m venv venv 91 | source venv/bin/activate # Linux / macOS 92 | venv\Scripts\activate # Windows 93 | 94 | Note: run python --version before creating a virtual environment. The modules in requirement.txt are compatible with <=3.11 version of python. 95 | 96 | If you're using higher versions, consider creating the virtual environement in the following way: 97 | bash 98 | python -3.11 -m venv venv 99 | 100 | Visit official website of python to download version 3.11.* 101 | 102 | 2. *Install dependencies* 103 | 104 | bash 105 | pip install -r requirements.txt 106 | 107 | 3. *Start the backend server* 108 | 109 | bash 110 | python app.py 111 | 112 | ### Optional: Enable API response caching (Redis or in-memory) 113 | 114 | Set the following environment variables (e.g., in a `.env` next to `backend/app.py`): 115 | 116 | ``` 117 | CACHE_ENABLED=true 118 | REDIS_URL=redis://localhost:6379/0 119 | CACHE_TTL_STOCK=900 # seconds for stock data 120 | CACHE_TTL_PRED=3600 # seconds for prediction endpoint 121 | ``` 122 | 123 | - If Redis is reachable, it will be used. 124 | - If Redis is not reachable, the app falls back to a safe in-memory cache. 125 | - Force refresh any endpoint with `?refresh=true`. 126 | - Cache headers: the API adds `X-Cache: HIT|MISS|BYPASS` to responses. 127 | 128 | 129 | The app will be available at http://x.x.x.x:10000. (you will find the correct url in the server console) 130 | copy the server url to use in frontend 131 | make sure the app in the testing during the code editing 132 | 133 | Change the following: 134 | 135 | bash 136 | app = Flask(__name__) 137 | CORS(app, ....) 138 | 139 | 140 | to 141 | 142 | bash 143 | app = Flask(__name__) 144 | CORS(app) 145 | 146 | 147 | for testing environment 148 | 149 | ## Frontend Setup (React) 150 | 151 | *Install dependencies* 152 | 153 | bash 154 | npm install 155 | 156 | 157 | Add server url to frontend Stockdata.jsx and Predict.jsx page 158 | 159 | from 160 | 161 | bash 162 | ${process.env.REACT_APP_API_URL} 163 | 164 | 165 | to 166 | 167 | bash 168 | http://x.x.x.x:10000 169 | 170 | 171 | ## Create a .env and add the following: 172 | 173 | bash 174 | REACT_APP_FIREBASE_API_KEY="xxxx(you credentials)" 175 | REACT_APP_FIREBASE_AUTH_DOMAIN="xxxx(you credentials)" 176 | REACT_APP_FIREBASE_DATABASE_URL="xxxx(you credentials)" 177 | REACT_APP_FIREBASE_PROJECT_ID="xxxx(you credentials)" 178 | REACT_APP_FIREBASE_STORAGE_BUCKET="xxxx(you credentials)" 179 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID="xxxx(you credentials)" 180 | REACT_APP_FIREBASE_APP_ID="xxxx(you credentials)" 181 | REACT_APP_FIREBASE_MEASUREMENT_ID="xxxx(you credentials)" 182 | 183 | ## How to get Firebase Credentials? 184 | 185 | 1. Go to [Firebase](https://console.firebase.google.com) Console 186 | 187 | 2. Create a Project (or use existing) 188 | 189 | - Click Add Project → enter a name (e.g., Stock Analyzer). 190 | - Configure Google Analytics (optional). 191 | - Project will take a few seconds to be ready. 192 | 193 | 3. Register a Web App 194 | 195 | - Inside your project → go to Project Overview (top-left) → Add app → choose Web ( icon). 196 | - Give your app a nickname (e.g., stock-analyzer-web). 197 | - Click Register App. 198 | 199 | 4. Get Firebase Config Object 200 | 201 | - After registering, Firebase shows you code like this: 202 | js 203 | const firebaseConfig = { 204 | apiKey: "AIzaSyDxxxxxx", 205 | authDomain: "your-project-id.firebaseapp.com", 206 | databaseURL: "https://your-project-id.firebaseio.com", 207 | projectId: "your-project-id", 208 | storageBucket: "your-project-id.appspot.com", 209 | messagingSenderId: "123456789", 210 | appId: "1:123456789:web:abcdef123456", 211 | measurementId: "G-ABC123XYZ" 212 | }; 213 | 214 | 215 | These values map 1:1 to the .env variables. 216 | 217 | 5. Copy them into .env 218 | 219 | ## Start the project 220 | 221 | bash 222 | npm start 223 | 224 | 225 | The app will be available at http://localhost:3000. -------------------------------------------------------------------------------- /src/components/ContactForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styles from "./ContactForm.module.css"; 3 | 4 | const ContactForm = () => { 5 | const [formData, setFormData] = useState({ 6 | firstName: "", 7 | lastName: "", 8 | email: "", 9 | message: "", 10 | }); 11 | 12 | const [status, setStatus] = useState(""); 13 | const [errors, setErrors] = useState({}); 14 | 15 | const nameRegex = /^[a-zA-Z\s]*$/; 16 | const emailRegex = /^[a-zA-Z][a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 17 | 18 | const validateField = (name, value) => { 19 | let error = ""; 20 | if ( 21 | (name === "firstName" || name === "lastName") && 22 | !nameRegex.test(value) 23 | ) { 24 | error = "Only letters and spaces are allowed."; 25 | } 26 | if (name === "email" && !emailRegex.test(value)) { 27 | error = "Please enter a valid email address."; 28 | } 29 | setErrors((prev) => ({ ...prev, [name]: error })); 30 | return error === ""; 31 | }; 32 | 33 | const handleChange = (e) => { 34 | const { name, value } = e.target; 35 | setFormData((prev) => ({ ...prev, [name]: value })); 36 | validateField(name, value); 37 | }; 38 | 39 | const validateForm = () => { 40 | let isValid = true; 41 | ["firstName", "lastName", "email"].forEach((field) => { 42 | if (!validateField(field, formData[field])) isValid = false; 43 | }); 44 | return isValid && Object.values(errors).every((e) => e === ""); 45 | }; 46 | 47 | const handleSubmit = async (e) => { 48 | e.preventDefault(); 49 | if (!validateForm()) { 50 | setStatus("Please correct the errors in the form."); 51 | return; 52 | } 53 | 54 | setStatus("Sending message..."); 55 | const payload = { 56 | access_key: process.env.REACT_APP_ACCESS_KEY, 57 | ...formData, 58 | }; 59 | 60 | try { 61 | const res = await fetch("https://api.web3forms.com/submit", { 62 | method: "POST", 63 | headers: { "Content-Type": "application/json" }, 64 | body: JSON.stringify(payload), 65 | }); 66 | 67 | const result = await res.json(); 68 | if (result.success) { 69 | setStatus("Message sent successfully!"); 70 | setFormData({ firstName: "", lastName: "", email: "", message: "" }); 71 | setErrors({}); 72 | } else { 73 | setStatus("Failed to send message. Please try again."); 74 | } 75 | } catch (error) { 76 | console.error("Error:", error); 77 | setStatus("An error occurred. Please try again."); 78 | } 79 | }; 80 | 81 | return ( 82 |
83 |
84 | {/* Left Section */} 85 |
86 |

87 | Let's talk about smart investments 88 |

89 |

90 | Stock Analyzer helps you track, analyze, and understand market 91 | trends with ease. Get insights that empower smarter investment 92 | decisions. 93 |

94 |
    95 |
  • 96 | ✉️ support@stockanalyzer.com 97 |
  • 98 |
  • 99 | 📞 +1800-457-5834 100 |
  • 101 |
  • 102 | 📍 Bhubaneswar, Odisha 103 |
  • 104 |
105 |
106 | 107 | {/* Right Section (Form) */} 108 |
109 |

Request a Callback

110 |
111 |
112 |
113 | 121 | {errors.firstName && ( 122 | 123 | {errors.firstName} 124 | 125 | )} 126 |
127 |
128 | 136 | {errors.lastName && ( 137 | 138 | {errors.lastName} 139 | 140 | )} 141 |
142 |
143 | 144 |
145 | 153 | {errors.email && ( 154 | {errors.email} 155 | )} 156 |
157 | 158 |
159 |