4 Commits

Author SHA1 Message Date
f33011b219 Research View 2026-08-08 08:03:46 +05:30
e4ad2233b8 commit - mock buy 2026-08-07 22:38:27 +05:30
53ad1abb78 Customize - Stock map list, watch list, home page 2026-08-05 23:51:18 +05:30
076c4ff68f User Auth Done 2026-08-05 22:00:49 +05:30
51 changed files with 2803 additions and 651 deletions

21
Dockerfile.frontend Normal file
View File

@@ -0,0 +1,21 @@
# Build stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built assets
COPY --from=build /app/build /usr/share/nginx/html
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,14 @@
FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y git cmake g++ && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

116
backend/app/api/auth.py Normal file
View File

@@ -0,0 +1,116 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from pydantic import BaseModel
from jose import JWTError, jwt
from app.db.database import get_db
from app.db.models import User
from app.core.security import (
public_key, decrypt_password, verify_password,
get_password_hash, create_access_token
)
from app.core.config import SECRET_KEY, ALGORITHM
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
class AuthRequest(BaseModel):
username: str
password: str # Base64 encoded RSA-encrypted password
class ProfileUpdateRequest(BaseModel):
display_name: str = None
email_id: str = None
mobile_no: str = None
gender: str = None
password: str = None # Base64 encoded RSA-encrypted new password
class SectorMapUpdateRequest(BaseModel):
sector_map: dict
@router.get("/public-key")
def get_public_key():
return {"public_key": public_key.decode("utf-8")}
@router.post("/signup")
def signup(req: AuthRequest, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.username == req.username).first()
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
# Decrypt password
decrypted_password = decrypt_password(req.password)
hashed_password = get_password_hash(decrypted_password)
new_user = User(username=req.username, hashed_password=hashed_password)
db.add(new_user)
db.commit()
return {"message": "User created successfully"}
@router.post("/login")
def login(req: AuthRequest, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.username == req.username).first()
if not db_user:
raise HTTPException(status_code=400, detail="Invalid credentials")
decrypted_password = decrypt_password(req.password)
if not verify_password(decrypted_password, db_user.hashed_password):
raise HTTPException(status_code=400, detail="Invalid credentials")
access_token = create_access_token(data={"sub": db_user.username})
return {"access_token": access_token, "token_type": "bearer"}
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.username == username).first()
if user is None:
raise credentials_exception
return user
@router.get("/me")
def get_me(current_user: User = Depends(get_current_user)):
return {
"username": current_user.username,
"display_name": current_user.display_name,
"email_id": current_user.email_id,
"mobile_no": current_user.mobile_no,
"gender": current_user.gender,
"sector_map": current_user.sector_map
}
@router.put("/update-sectors")
def update_sectors(req: SectorMapUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
current_user.sector_map = req.sector_map
db.commit()
return {"message": "Sectors updated successfully"}
@router.put("/update-profile")
def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
if req.display_name is not None:
current_user.display_name = req.display_name
if req.email_id is not None:
current_user.email_id = req.email_id
if req.mobile_no is not None:
current_user.mobile_no = req.mobile_no
if req.gender is not None:
current_user.gender = req.gender
if req.password:
decrypted_password = decrypt_password(req.password)
hashed_password = get_password_hash(decrypted_password)
current_user.hashed_password = hashed_password
db.commit()
return {"message": "Profile updated successfully"}

View File

@@ -0,0 +1,144 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel
import yfinance as yf
from app.db.database import get_db
from app.db.models import User, PortfolioItem
from app.api.auth import get_current_user
router = APIRouter()
class BuyRequest(BaseModel):
symbol: str
quantity: int
buy_price: float
class SellRequest(BaseModel):
portfolio_item_id: int
@router.post("/buy")
def buy_stock(req: BuyRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
final_buy_price = req.buy_price
# If price wasn't provided (e.g. from watchlist), fetch it live
if final_buy_price <= 0:
try:
ticker = yf.Ticker(req.symbol)
final_buy_price = ticker.fast_info.last_price
except Exception as e:
print(f"Error fetching live price on buy for {req.symbol}: {e}")
raise HTTPException(status_code=400, detail="Could not fetch live price, please try again.")
new_item = PortfolioItem(
user_id=current_user.id,
symbol=req.symbol,
quantity=req.quantity,
buy_price=final_buy_price
)
db.add(new_item)
db.commit()
db.refresh(new_item)
return {"message": "Stock purchased successfully", "item": new_item}
@router.get("/")
def get_portfolio(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
items = db.query(PortfolioItem).filter(PortfolioItem.user_id == current_user.id).all()
portfolio = []
total_invested = 0
total_current_value = 0
for item in items:
# Fetch live price
live_price = item.buy_price # Fallback
try:
ticker = yf.Ticker(item.symbol)
live_price = ticker.fast_info.last_price
except Exception as e:
print(f"Error fetching price for {item.symbol}: {e}")
invested = item.quantity * item.buy_price
current_value = item.quantity * live_price
pnl = current_value - invested
pnl_percent = (pnl / invested) * 100 if invested > 0 else 0
total_invested += invested
total_current_value += current_value
portfolio.append({
"id": item.id,
"symbol": item.symbol,
"quantity": item.quantity,
"buy_price": item.buy_price,
"current_price": round(live_price, 2),
"invested": round(invested, 2),
"current_value": round(current_value, 2),
"pnl": round(pnl, 2),
"pnl_percent": round(pnl_percent, 2),
"purchase_date": item.purchase_date
})
total_pnl = total_current_value - total_invested
total_pnl_percent = (total_pnl / total_invested) * 100 if total_invested > 0 else 0
return {
"items": portfolio,
"summary": {
"total_invested": round(total_invested, 2),
"total_current_value": round(total_current_value, 2),
"total_pnl": round(total_pnl, 2),
"total_pnl_percent": round(total_pnl_percent, 2)
}
}
@router.delete("/{item_id}")
def delete_portfolio_item(item_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
item = db.query(PortfolioItem).filter(PortfolioItem.id == item_id, PortfolioItem.user_id == current_user.id).first()
if not item:
raise HTTPException(status_code=404, detail="Item not found")
db.delete(item)
db.commit()
return {"message": "Stock sold/removed successfully"}
@router.get("/recommendations/{symbol}")
def get_recommendations(symbol: str, current_user: User = Depends(get_current_user)):
try:
ticker = yf.Ticker(symbol)
recs = ticker.recommendations
if recs is None or recs.empty:
return {"error": "No recommendation data available", "confidence": 0}
# Get the most recent month's data (period '0m' is usually index 0)
latest = recs.iloc[0]
strong_buy = int(latest.get("strongBuy", 0))
buy = int(latest.get("buy", 0))
hold = int(latest.get("hold", 0))
sell = int(latest.get("sell", 0))
strong_sell = int(latest.get("strongSell", 0))
total = strong_buy + buy + hold + sell + strong_sell
if total == 0:
return {"error": "No recommendations found", "confidence": 0}
# Calculate confidence score
# Strong Buy = 100, Buy = 75, Hold = 50, Sell = 25, Strong Sell = 0
score = (strong_buy * 100 + buy * 75 + hold * 50 + sell * 25) / total
return {
"symbol": symbol,
"period": str(latest.get("period", "0m")),
"strongBuy": strong_buy,
"buy": buy,
"hold": hold,
"sell": sell,
"strongSell": strong_sell,
"total": total,
"confidence": round(score, 1)
}
except Exception as e:
print(f"Error fetching recommendations for {symbol}: {e}")
return {"error": str(e), "confidence": 0}

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends
from app.api.auth import get_current_user
from app.services.stock_engine import (
get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel
)
import requests
router = APIRouter()
@router.get("/scan")
def scan(current_user = Depends(get_current_user)):
return get_scan(current_user.sector_map or {})
@router.get("/ribbon")
def ribbon(current_user = Depends(get_current_user)):
return get_ribbon(current_user.sector_map or {})
@router.get("/smartmoney")
def smartmoney(current_user = Depends(get_current_user)):
return get_smartmoney(current_user.sector_map or {})
@router.get("/trend")
def trend(current_user = Depends(get_current_user)):
return get_trend(current_user.sector_map or {})
@router.get("/marketintel")
def marketintel(current_user = Depends(get_current_user)):
return get_marketintel()
@router.get("/search")
def search(q: str, current_user = Depends(get_current_user)):
headers = {'User-Agent': 'Mozilla/5.0'}
url = f"https://query2.finance.yahoo.com/v1/finance/search?q={q}&quotesCount=10&newsCount=0"
res = requests.get(url, headers=headers)
if res.status_code == 200:
data = res.json()
quotes = data.get("quotes", [])
# Filter for Indian stocks (.NS or .BO)
indian_stocks = [q for q in quotes if q.get("symbol", "").endswith((".NS", ".BO"))][:10]
if indian_stocks:
import yfinance as yf
symbols = [q["symbol"] for q in indian_stocks]
tickers = yf.Tickers(" ".join(symbols))
for stock in indian_stocks:
try:
info = tickers.tickers[stock["symbol"]].info
stock["currentPrice"] = info.get("currentPrice") or info.get("regularMarketPrice")
stock["marketCap"] = info.get("marketCap")
except Exception as e:
print(f"Error fetching {stock['symbol']}: {e}")
stock["currentPrice"] = None
stock["marketCap"] = None
return indian_stocks
return []

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
import os
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:M%40triXPostgr3s%406202@192.168.0.111:5432/stock_scanner")
SECRET_KEY = "stock-scanner-super-secret-key-12345"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60

View File

@@ -0,0 +1,61 @@
import os
import base64
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
import bcrypt
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from fastapi import HTTPException
from app.core.config import SECRET_KEY, ALGORITHM
RSA_PRIVATE_KEY_PATH = "private.pem"
RSA_PUBLIC_KEY_PATH = "public.pem"
if not os.path.exists(RSA_PRIVATE_KEY_PATH) or not os.path.exists(RSA_PUBLIC_KEY_PATH):
key = RSA.generate(2048)
private_key = key.export_key()
with open(RSA_PRIVATE_KEY_PATH, "wb") as file_out:
file_out.write(private_key)
public_key = key.publickey().export_key()
with open(RSA_PUBLIC_KEY_PATH, "wb") as file_out:
file_out.write(public_key)
else:
with open(RSA_PRIVATE_KEY_PATH, "rb") as f:
private_key = f.read()
with open(RSA_PUBLIC_KEY_PATH, "rb") as f:
public_key = f.read()
rsa_private_key = RSA.import_key(private_key)
cipher_rsa = PKCS1_OAEP.new(rsa_private_key)
def decrypt_password(encrypted_b64_password: str) -> str:
try:
encrypted_bytes = base64.b64decode(encrypted_b64_password)
decrypted = cipher_rsa.decrypt(encrypted_bytes)
return decrypted.decode("utf-8")
except Exception as e:
raise HTTPException(status_code=400, detail="Invalid RSA payload or decryption failed")
def verify_password(plain_password, hashed_password):
if isinstance(plain_password, str):
plain_password = plain_password.encode('utf-8')
if isinstance(hashed_password, str):
hashed_password = hashed_password.encode('utf-8')
return bcrypt.checkpw(plain_password, hashed_password)
def get_password_hash(password):
if isinstance(password, str):
password = password.encode('utf-8')
return bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8')
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,15 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import DATABASE_URL
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

25
backend/app/db/models.py Normal file
View File

@@ -0,0 +1,25 @@
from sqlalchemy import Column, Integer, String, JSON, Float, ForeignKey, DateTime
from app.db.database import Base
import datetime
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
display_name = Column(String, nullable=True)
email_id = Column(String, nullable=True)
mobile_no = Column(String, nullable=True)
gender = Column(String, nullable=True)
sector_map = Column(JSON, nullable=True)
class PortfolioItem(Base):
__tablename__ = "portfolio_items"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
symbol = Column(String, index=True)
quantity = Column(Integer, default=1)
buy_price = Column(Float)
purchase_date = Column(DateTime, default=datetime.datetime.utcnow)

27
backend/app/main.py Normal file
View File

@@ -0,0 +1,27 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api import auth, scanner, portfolio
from app.db.database import engine, Base
# Initialize the database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Stock Scanner Pro API")
# Configure CORS for frontend access
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include Routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(scanner.router, prefix="/api/scanner", tags=["scanner"])
app.include_router(portfolio.router, prefix="/api/portfolio", tags=["portfolio"])
@app.get("/")
def read_root():
return {"message": "Stock Scanner Backend is running."}

View File

@@ -1,20 +1,8 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import yfinance as yf
import pandas as pd
import numpy as np
from concurrent.futures import ThreadPoolExecutor
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ✅ PERSONAL
# ✅ PERSONAL
personal_stocks = [
@@ -437,13 +425,12 @@ def analyze(stock):
# =========================================
# ✅ API - FAST SCAN
# =========================================
@app.get("/scan")
def scan():
def get_scan(user_sector_map):
sector_output = {}
all_stocks = []
for sector, stocks in sector_map.items():
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x]
@@ -549,28 +536,28 @@ def analyze_ribbon(stock):
return None
@app.get("/ribbon")
def ribbon():
def get_ribbon(user_sector_map):
results = []
for stocks in sector_map.values():
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks))
data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data)
return results
@app.get("/smartmoney")
def smartmoney():
def get_smartmoney(user_sector_map):
results = []
for sector, stocks in sector_map.items():
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data)
@@ -590,16 +577,15 @@ def smartmoney():
"all": results
}
@app.get("/trend")
def trend():
def get_trend(user_sector_map):
results = []
for sector, stocks in sector_map.items():
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data)
results.sort(
@@ -623,8 +609,7 @@ def trend():
# - Full sector_map (too large)
# - Deep MACD + Supertrend
# - 6 month data calls
@app.get("/marketintel")
def marketintel():
def get_marketintel():
try:
nifty_news = yf.Ticker("^NSEI").news or []
reliance_news = yf.Ticker("RELIANCE.NS").news or []

27
backend/private.pem Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtqxw/s/eCLosxX61Zvd0PYicYUoKjbn6gsxe+jb4p0hMNK36
YsOqYqRPKIh7OosX9CaKDDn8ws6LUt5MAOjwTkNBF8P3bPJ+R+qVXeiqFBIAkRNX
VqxU8eoWNaoNDcNQWSxOIJJanv6UZHbxlv04WLtw5WUtYUbLwO2TcAYghmzk/UTw
p0YjQ0kw6O2E7iQ3i/H7aNqKlEoZoXHcsnDaUsIa3qy6LG7UkGz0LQ3RKu9RqoVO
I8DOV8i1lO3Z5SERiSulfhqzoaD2vWU/sXBRC+yJ3UxZvqIUpR6taToZxPL0aZG9
XoxFcS2ywogI1XGKWJAYxosgbA8KxKilQFs+EQIDAQABAoIBAE2cThW0sxz6HHN8
Ng4dLGHIXMho8LruPSS9N80O9e38pYPsNuknQIjQTmFmOxTQa8jrZtNy/5S0tDTt
BVWNoiteH5W9SK4dCNH9NKDFbR1B2MPKd9z3Ms/lKLJ8ITert8NrM3ZbN+09NEbK
0jHYO8xXNsv/hJxDu+WoCnAZ6k+x1IFDXRfV4MrTBX27t4xtfA5KeXQyPzotUeBn
VjVIoRqkCSIJLr+f6ATa+OHlfBjyTbBQscG1/0JNfDDQFmvOYyyc4zpVn0bAINw7
klvJaXP++DRaZsvNnM1SGpsJ+U86O56ODkX4v2a+H3czhpOA+q0yePHQ/nWr73Nf
7WQYxdECgYEA1OVJZoNPxqVefooY9Tm5mDRXeRVdOormEI8GZkrIAG85w+MD9MMr
2s8FjXDniN4khSClpF9YunB8e77ufu0TgpqhHhXSnn/5JEQ4UO5wiO1EqBvhkFUJ
CXitjYehgwDc5WKvUfPxeMCMvF+rYoHhN1aYIczuN36fhDAXA6pxGyMCgYEA26iy
2RHgu0XNa37Z9KeJCDGNtpj2dYttRAua4UVudxPYn+Q2IdKZ9HPjgWPKgdUjAEup
uuIDhbk1zmQiquvyNMP0HX+p2yG8DX77cdajyvBG5p3BxUX1MxXx4Q6ICVAr4K51
cZg3YWsIsCsCCO7DkgW1kFRlVptUMMmbJjNoXzsCgYAE32qaqg69YTOUedywYC3b
SfdmkhKcMGmrn1pqJPQG7oTH8v44L+9lBq/92MOz4kG7uk+QP45sVf7DZk9XIF39
80QUyDMV5Z/yMI2JbKuutp+HqXu0Lf4S9WwjfSM5OF/V8DhLC+ZO+Tk/ZoEptAdP
mO/KdkJNitxjziX4s4H7OQKBgB77oJ51oxlHMz5iWiPkLbP2KWMEGF9kFzlt2Z7E
yFwLdJa4/dmvdv/ACOsLRFkj0xgLlBlEH/MQuMIv5aPuO++tZBV1GGRMUdYlfxoD
iH7rfVSyE87bm0ZlZgS0pAOMR2Qdt3saWVVoX4VZy6Ou6e8C1yVQgirBJhLrnPK6
dZJZAoGBAK/Nob7Lrxi2kXuJbUOp+Jcir/sPeWE0ZPH9vnAdDkDLrUFbyNWJHHgI
tn1rrPCJeF3+gGdbKcrubHGxJMZGDh3qALs35L6195ZaGys6l+Imp1FxsSFROf/S
Hy2wbjf7fz/gb0U7dfD7ZtVPDvqdFax7NrEdvdcslA0IUiVPqGkC
-----END RSA PRIVATE KEY-----

9
backend/public.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtqxw/s/eCLosxX61Zvd0
PYicYUoKjbn6gsxe+jb4p0hMNK36YsOqYqRPKIh7OosX9CaKDDn8ws6LUt5MAOjw
TkNBF8P3bPJ+R+qVXeiqFBIAkRNXVqxU8eoWNaoNDcNQWSxOIJJanv6UZHbxlv04
WLtw5WUtYUbLwO2TcAYghmzk/UTwp0YjQ0kw6O2E7iQ3i/H7aNqKlEoZoXHcsnDa
UsIa3qy6LG7UkGz0LQ3RKu9RqoVOI8DOV8i1lO3Z5SERiSulfhqzoaD2vWU/sXBR
C+yJ3UxZvqIUpR6taToZxPL0aZG9XoxFcS2ywogI1XGKWJAYxosgbA8KxKilQFs+
EQIDAQAB
-----END PUBLIC KEY-----

114
backend/requirements.txt Normal file
View File

@@ -0,0 +1,114 @@
alembic==1.18.5
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.1
APScheduler==3.11.2
argon2-cffi==25.1.0
argon2-cffi-bindings==25.1.0
bcrypt==5.0.0
beautifulsoup4==4.15.0
cachetools==7.0.5
certifi==2026.1.4
cffi==2.0.0
charset-normalizer==3.4.7
click==8.3.1
cryptography==48.0.0
curl_cffi==0.16.0
defusedxml==0.7.1
dnspython==2.8.0
-e git+https://github.com/maddy23285/docvision.git@b0a6bea7ad1db9f515c9128964370c6bfdaad9e1#egg=docfingerprint
-e git+https://github.com/maddy23285/docvision.git@b0a6bea7ad1db9f515c9128964370c6bfdaad9e1#egg=docfingerprint_embedding&subdirectory=embedding_service
ecdsa==0.19.2
email-validator==2.3.0
fastapi==0.128.0
filelock==3.17.0
fonttools==4.62.1
fpdf2==2.8.7
fsspec==2025.2.0
h11==0.16.0
hf-xet==1.5.2
httpcore==1.0.9
httptools==0.7.1
httpx==0.28.1
huggingface_hub==0.36.2
idna==3.11
ImageHash==4.3.2
imap-tools==1.13.0
iniconfig==2.3.0
Jinja2==3.1.4
lxml==6.1.1
Mako==1.3.12
MarkupSafe==3.0.2
mpmath==1.3.0
multitasking==0.0.13
networkx==3.4.2
numpy==2.2.3
ollama==0.6.2
opencv-python==4.12.0.88
opencv-python-headless==4.13.0.92
packaging==26.2
pandas==3.0.5
passlib==1.7.4
pdf2image==1.17.0
pdfminer.six==20251230
pdfplumber==0.11.9
peewee==4.3.0
pgvector==0.5.0
pillow==11.1.0
platformdirs==4.11.0
pluggy==1.6.0
protobuf==7.35.1
psycopg==3.3.4
psycopg-binary==3.3.4
psycopg2-binary==2.9.11
pyasn1==0.6.4
pycparser==3.0
pycryptodome==3.23.0
pycryptodomex==3.21.0
pydantic==2.12.5
pydantic-settings==2.14.2
pydantic_core==2.41.5
Pygments==2.20.0
PyJWT==2.11.0
pypdf==6.13.0
pypdfium2==5.9.0
pytesseract==0.3.13
pytest==8.4.2
pytest-asyncio==0.26.0
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-dotenv==1.2.1
python-jose==3.5.0
python-multipart==0.0.22
pytz==2026.3.post1
PyWavelets==1.9.0
PyYAML==6.0.3
redis==5.2.0
regex==2026.7.19
requests==2.34.2
rsa==4.9.1
safetensors==0.8.0
scipy==1.18.0
setuptools==75.8.0
six==1.17.0
soupsieve==2.9.1
SQLAlchemy==2.0.46
starlette==0.50.0
structlog==25.5.0
sympy==1.13.1
tokenizers==0.22.2
torch==2.6.0
torchaudio==2.6.0
torchvision==0.21.0
tqdm==4.69.0
transformers==4.57.6
typing-inspection==0.4.2
typing_extensions==4.15.0
tzlocal==5.3.1
urllib3==2.7.0
uvicorn==0.40.0
uvloop==0.22.1
watchfiles==1.1.1
websockets==16.0
yfinance==1.5.2
zxing-cpp==2.3.0

50
build_and_push.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Configuration
REGISTRY="hub.technobeesolutions.in"
USERNAME="technobee_admin"
PASSWORD='M@tr!x#149@dm!N'
BACKEND_IMAGE="${REGISTRY}/stock-scanner-backend:latest"
FRONTEND_IMAGE="${REGISTRY}/stock-scanner-frontend:latest"
PLATFORM="linux/amd64"
echo "=========================================="
echo " Stock Scanner - Docker Build & Push "
echo "=========================================="
# 1. Login to Docker Registry
echo "[1/4] Logging into Docker Registry ($REGISTRY)..."
# Using --password-stdin is the secure way to pass passwords to docker login
echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin
if [ $? -ne 0 ]; then
echo "❌ Docker login failed! Please check your credentials and ensure Docker is running."
exit 1
fi
echo "✅ Login successful."
echo ""
# 2. Build and push backend
echo "[2/4] Building and pushing Backend Image for $PLATFORM..."
docker buildx build --platform "$PLATFORM" -t "$BACKEND_IMAGE" -f backend/Dockerfile.backend ./backend --push
if [ $? -ne 0 ]; then
echo "❌ Backend build/push failed!"
exit 1
fi
echo "✅ Backend image built and pushed successfully."
echo ""
# 3. Build and push frontend
echo "[3/4] Building and pushing Frontend Image for $PLATFORM..."
docker buildx build --platform "$PLATFORM" -t "$FRONTEND_IMAGE" -f Dockerfile.frontend . --push
if [ $? -ne 0 ]; then
echo "❌ Frontend build/push failed!"
exit 1
fi
echo "✅ Frontend image built and pushed successfully."
echo ""
echo "=========================================="
echo "🎉 Build and Push Complete!"
echo "You can now run 'docker compose pull && docker compose up -d' on your target server."
echo "=========================================="

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
services:
frontend:
image: hub.technobeesolutions.in/stock-scanner-frontend:latest
restart: always
ports:
- "3055:80"
depends_on:
- backend
backend:
image: hub.technobeesolutions.in/stock-scanner-backend:latest
restart: always
ports:
- "8055:8000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- DATABASE_URL=postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal:5432/stock_scanner

10
nginx.conf Normal file
View File

@@ -0,0 +1,10 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

17
package-lock.json generated
View File

@@ -14,9 +14,10 @@
"@testing-library/user-event": "^13.5.0",
"axios": "^1.18.1",
"lightweight-charts": "^5.2.0",
"node-forge": "^1.4.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0",
"react-router-dom": "^7.18.2",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
@@ -13958,9 +13959,9 @@
}
},
"node_modules/react-router": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -13980,12 +13981,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT",
"dependencies": {
"react-router": "7.18.0"
"react-router": "7.18.2"
},
"engines": {
"node": ">=20.0.0"

View File

@@ -9,9 +9,10 @@
"@testing-library/user-event": "^13.5.0",
"axios": "^1.18.1",
"lightweight-charts": "^5.2.0",
"node-forge": "^1.4.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0",
"react-router-dom": "^7.18.2",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
@@ -21,6 +22,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"proxy": "http://127.0.0.1:8000",
"eslintConfig": {
"extends": [
"react-app",

27
private.pem Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuUUB5HsRK6mvKyjAClLckNxDK69MZTW2Yn9qOJZvw3j0R4sl
IhoyBRAm1fxFYthj+klxqw0XKUR7F6DcDuK+3z+a+Ds/uP3tDYbnfT4cr9VEhgq9
FoLi7AgfH98mqp4VkqDCuYubt4XRtfhpN0TuShT/xImmqLfxkmvZSP/syPoFytEq
X66QZ2QyT4gqzz9/ZMo7EFMeuKA44IBv6qeQzFxSBt0xpeyMXHChn8AeocaOySC3
kyZBefzmUF2lkOyZKlaPC+0/mUb6ndT1OBfK+EZXzWEbugBWOg/NSy9pnzPRGjAY
qknWs1912+H7t2FEA8BCRZCOK85B05BYeZM+SwIDAQABAoIBAAdhNs6Vkl0FYMtc
z615mHySsYBDds0PQykQPzhq0Z/dKQnSgpOlrHlkJ8/dQRxLV0woccUo+9OyfR63
UxyqroSxAWjC32psfu1LgtwLxdPBMH4QXnDPnN9fAXJhDqa8LkrNaYQ/id/ZDj3r
27EOZ1l/FaMMAp302cQF1L1iKFnm/ksN9s4G2+8UwrtUWum5LOeD6x22H8443uRf
Lg4IvMC4nLYCQRUAeaKJoTZnw8KeOmKpF2opfu8L1vQ8ubYoZqBX+gNP6GflFG6j
zgoNWfZvRQnDfofH+ZOPcB7vbVKcXKFZ0kHzfw40Nq49GxZndsK30xCUtdDiW7eE
Mgfv9EUCgYEA1q/HUTKBFpvKGu1BwCVTIdvqI0qScWpYst525CbeI36MCkP6ReCX
kvi7259saMRdAaEhIj8EN3BuDu5k8jjNRqkAnkq0pNHkYD3lzXBc4H3RC0LdmEmn
cUUS16sLSkcciI3+SnGjZUbCjso2r2Sy1d/iPhR7L2QpmlzqtPNZxr0CgYEA3OwK
NJMNN1ARv+aOka+mxn6+MsbbWWB0CLSbhg5WRRnmsefDMBjtWsRr+/ufQO7rC+q1
rv83jiCgJl3u5idS4Hrm/k77iZZzrvJruuR5W5GgvqMTcdut7l4pdqSlvV96D4z7
29/sze3vVoZv4zIeWxj5QDeT7xLwvjoZpUbHDacCgYAsCjsVCQs6HBNFms4WIJIB
LB/HxZBs+6feaYxyGRcQqPEJWhCJLR1q5OOElhujEkUSBH/LiqnOxZ2OKpFCryxN
BnY+Ao00EmqK46e0kQw8cRLlAH58sv9KWSUYYNocDqJn0NkNZGpkaDOZHxpAuKOH
BDphCcqLWjy+kbkEDbeo8QKBgQCD9m7GJsyvLKHdmi+xMFYTnWOpWwVtZuMIzDFW
Kzw2/JjDzifWlB07qbbDBvOCyvQV4zZxeLvLpwtiv5tTWUv1ERTn9W/lKLyjVOUq
9wzSuLNnDGwyB8Hmb9KerwzdiKmVnmZXWXPPMoBTk+xDrw1Y5xsD0+8G0K6DQptN
EXEXYwKBgQDS5DR5IDGvTycguDhU1QGSW0iEzMfBlZftKAucB/KYcQj8A4+AECKM
9SqcTI2Aneou+7PubpCFDSEckSKZkxb6ibw/ynA1EE55wyjuctyHTpqeHJjoK7jS
R5hyVL5iFBwGxv6oiWeKld+ZbDSB+i89km+kARaXjIB2gZmkepAIyA==
-----END RSA PRIVATE KEY-----

9
public.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuUUB5HsRK6mvKyjAClLc
kNxDK69MZTW2Yn9qOJZvw3j0R4slIhoyBRAm1fxFYthj+klxqw0XKUR7F6DcDuK+
3z+a+Ds/uP3tDYbnfT4cr9VEhgq9FoLi7AgfH98mqp4VkqDCuYubt4XRtfhpN0Tu
ShT/xImmqLfxkmvZSP/syPoFytEqX66QZ2QyT4gqzz9/ZMo7EFMeuKA44IBv6qeQ
zFxSBt0xpeyMXHChn8AeocaOySC3kyZBefzmUF2lkOyZKlaPC+0/mUb6ndT1OBfK
+EZXzWEbugBWOg/NSy9pnzPRGjAYqknWs1912+H7t2FEA8BCRZCOK85B05BYeZM+
SwIDAQAB
-----END PUBLIC KEY-----

18
run_backend.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
echo "Starting Stock Scanner Backend..."
# Navigate to the backend directory
cd backend || exit 1
# Activate virtual environment if it exists (optional, helps if user uses venv)
if [ -d "venv" ]; then
echo "Activating virtual environment..."
source venv/bin/activate
elif [ -d "../venv" ]; then
echo "Activating virtual environment..."
source ../venv/bin/activate
fi
# Run the uvicorn server with hot reload
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

View File

@@ -1,4 +1,6 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom";
import axios from "axios";
import Chart from "./components/Chart";
import Scanner from "./components/Scanner";
import Ribbon from "./components/Ribbon";
@@ -7,59 +9,141 @@ import Popup from "./components/Popup";
import AboutScanner from "./components/AboutScanner";
import MarketIntel from "./components/MarketIntel";
import TrendScanner from "./components/TrendScanner";
import WatchlistManager from "./components/WatchlistManager";
import Login from "./components/Login";
import Signup from "./components/Signup";
import ProtectedRoute from "./components/ProtectedRoute";
import Profile from "./components/Profile";
import MainDashboard from "./components/MainDashboard";
import "./index.css";
function App() {
function Dashboard() {
const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE");
const [signal, setSignal] = useState(null);
const [view, setView] = useState("scanner");
const [view, setView] = useState("dashboard");
const [userProfile, setUserProfile] = useState(null);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [scannerLoaded, setScannerLoaded] = useState(false);
const [buyModalData, setBuyModalData] = useState(null);
const [researchModalData, setResearchModalData] = useState(null);
const [buyModalRecs, setBuyModalRecs] = useState(null);
const [buyModalRecsLoading, setBuyModalRecsLoading] = useState(false);
const [buyQuantity, setBuyQuantity] = useState(1);
const navigate = useNavigate();
const fetchProfile = () => {
axios.get("/api/auth/me")
.then(res => setUserProfile(res.data))
.catch(err => {
if (err.response?.status === 401) handleLogout();
});
};
useEffect(() => {
fetchProfile();
}, []);
const handleLogout = () => {
localStorage.removeItem("token");
navigate("/login");
};
useEffect(() => {
if (researchModalData) {
setBuyModalRecs(null);
setBuyModalRecsLoading(true);
axios.get(`/api/portfolio/recommendations/${researchModalData.symbol}`)
.then(res => {
setBuyModalRecs(res.data);
setBuyModalRecsLoading(false);
})
.catch(err => {
console.error("Failed to fetch recs", err);
setBuyModalRecsLoading(false);
});
}
}, [researchModalData]);
const handleBuySubmit = async () => {
if (!buyModalData || buyQuantity < 1) return;
try {
await axios.post("/api/portfolio/buy", {
symbol: buyModalData.symbol,
quantity: buyQuantity,
buy_price: buyModalData.price
});
setBuyModalData(null);
setBuyQuantity(1);
} catch (err) {
console.error("Error buying stock:", err);
alert("Failed to buy stock");
}
};
const onBuyClick = (symbol, price) => {
setBuyModalData({ symbol, price });
setBuyQuantity(1);
};
const onResearchClick = (symbol) => {
setResearchModalData({ symbol });
};
return (
<div className="app-container">
<h1>📈 Stock Scanner Pro</h1>
{/* HEADER */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Logo only, no text */}
<span style={{ fontSize: '2.5rem' }}>📊</span>
</div>
<div className="nav-container">
<button
className={`nav-btn ${view === "scanner" ? "active" : ""}`}
onClick={() => setView("scanner")}
{userProfile && (
<div style={{ position: 'relative' }}>
<div
style={{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', background: 'var(--glass-bg)', padding: '6px 12px', borderRadius: '30px', border: '1px solid var(--border-color)', backdropFilter: 'blur(10px)' }}
onClick={() => setDropdownOpen(!dropdownOpen)}
>
📊 Scanner
</button>
<div style={{ width: '30px', height: '30px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontWeight: 'bold', fontSize: '1rem', color: 'white' }}>
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div>
<span style={{ fontWeight: 600, fontSize: '0.9rem' }}>{userProfile.display_name || userProfile.username}</span>
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}></span>
</div>
<button
className={`nav-btn ${view === "ribbon" ? "active" : ""}`}
onClick={() => setView("ribbon")}
>
📈 Ribbon Strategy
{dropdownOpen && (
<div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '50px', right: '0', width: '200px', zIndex: 100, padding: '8px' }}>
<button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap', padding: '8px' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
Profile Settings
</button>
<button className="nav-btn" style={{ width: '100%', border: 'none', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', whiteSpace: 'nowrap', padding: '8px' }} onClick={handleLogout}>
🚪 Logout
</button>
</div>
)}
</div>
)}
</div>
<button
className={`nav-btn ${view === "smartmoney" ? "active" : ""}`}
onClick={() => setView("smartmoney")}
>
🏦 Smart Money
</button>
<button
className={`nav-btn ${view === "trend" ? "active" : ""}`}
onClick={() => setView("trend")}
>
Trend Analysis
</button>
<button
className={`nav-btn ${view === "marketintel" ? "active" : ""}`}
onClick={() => setView("marketintel")}
>
📰 Market Intel
</button>
<div className="nav-container" style={{ gap: '8px' }}>
<button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '6px 12px' }}>🏠</button>
<button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📊 Scanner</button>
<button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📈 Ribbon</button>
<button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>🏦 Smart Money</button>
<button className={`nav-btn ${view === "trend" ? "active" : ""}`} onClick={() => setView("trend")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}> Trend</button>
<button className={`nav-btn ${view === "marketintel" ? "active" : ""}`} onClick={() => setView("marketintel")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📰 Intel</button>
<button className={`nav-btn ${view === "watchlists" ? "active" : ""}`} onClick={() => setView("watchlists")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📋 Watchlists</button>
</div>
<div className="animate-fade-in">
{view === "scanner" && (
<>
<AboutScanner />
<Scanner onSelectStock={setSelectedStock} onSignal={setSignal} />
<Scanner
onSelectStock={setSelectedStock}
onSignal={setSignal}
onBuyClick={onBuyClick}
onResearchClick={onResearchClick}
/>
<div className="glass-card" style={{ marginTop: "20px" }}>
<h2>TradingView Chart</h2>
<Chart symbol={selectedStock} />
@@ -67,17 +151,146 @@ function App() {
{signal && <Popup signal={signal} />}
</>
)}
{view === "ribbon" && <Ribbon />}
{view === "smartmoney" && <SmartMoney />}
{view === "marketintel" && <MarketIntel />}
{view === "trend" && <TrendScanner />}
{view === "dashboard" && <MainDashboard userProfile={userProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "ribbon" && <Ribbon onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "smartmoney" && <SmartMoney onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "marketintel" && <MarketIntel onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "trend" && <TrendScanner onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
</div>
{/* Buy Modal */}
{buyModalData && (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)',
display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999
}}>
<div style={{
background: 'var(--glass-bg)', padding: '30px', borderRadius: '15px',
border: '1px solid var(--border-color)', width: 'min(350px, 90vw)', textAlign: 'center',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)'
}}>
<h2 style={{ marginBottom: '10px' }}>Mock Buy</h2>
<h3 style={{ color: 'var(--accent-green)', margin: '10px 0', fontSize: '1.8rem' }}>{buyModalData.symbol}</h3>
<p style={{ color: 'var(--text-secondary)' }}>Current Price: <strong style={{ color: 'var(--text-color)' }}>{buyModalData.price}</strong></p>
<div style={{ margin: '25px 0', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '15px' }}>
<label style={{ fontWeight: '500' }}>Quantity: </label>
<input
type="number"
value={buyQuantity}
onChange={(e) => setBuyQuantity(parseInt(e.target.value) || 1)}
min="1"
style={{
background: 'var(--search-bg)', border: '1px solid var(--border-color)',
color: 'var(--text-color)', padding: '10px', borderRadius: '8px', width: '100px',
fontSize: '1.1rem', textAlign: 'center'
}}
/>
</div>
<div style={{ margin: '20px 0', padding: '15px', background: 'rgba(0,0,0,0.2)', borderRadius: '10px' }}>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Total Investment</span>
<p style={{ fontWeight: 'bold', fontSize: '1.4rem', margin: '5px 0 0 0', color: 'var(--accent-blue)' }}>
{(buyModalData.price * buyQuantity).toFixed(2)}
</p>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '15px', marginTop: '25px' }}>
<button className="nav-btn" style={{ flex: 1, padding: '12px' }} onClick={() => setBuyModalData(null)}>Cancel</button>
<button className="action-btn" onClick={handleBuySubmit} style={{ flex: 1, padding: '12px', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', color: '#1a1a2e', border: 'none', fontWeight: 'bold' }}>Confirm Buy</button>
</div>
</div>
</div>
)}
{/* Research Modal */}
{researchModalData && (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)',
display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999
}}>
<div style={{
background: 'var(--glass-bg)', padding: '30px', borderRadius: '15px',
border: '1px solid var(--border-color)', width: 'min(400px, 90vw)', textAlign: 'center',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)'
}}>
<h2 style={{ marginBottom: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
<span>🔍</span> Analyst Research
</h2>
<h3 style={{ color: 'var(--accent-blue)', margin: '10px 0', fontSize: '1.8rem' }}>{researchModalData.symbol}</h3>
{/* Analyst Recommendations */}
<div style={{ margin: '20px 0', padding: '15px', background: 'var(--bg-secondary)', borderRadius: '10px', border: '1px solid var(--border-color)', textAlign: 'left' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<span style={{ fontSize: '0.95rem', fontWeight: 'bold', color: 'var(--text-primary)' }}>Analyst Consensus</span>
{buyModalRecsLoading && <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Analyzing...</span>}
{buyModalRecs && !buyModalRecs.error && (
<span className={`badge ${buyModalRecs.confidence >= 70 ? 'badge-success' : buyModalRecs.confidence >= 40 ? 'badge-warning' : 'badge-danger'}`} style={{ fontSize: '0.9rem' }}>
{buyModalRecs.confidence}% Confidence
</span>
)}
</div>
{buyModalRecs && buyModalRecs.error && (
<div style={{ fontSize: '0.9rem', color: 'var(--text-secondary)', textAlign: 'center', padding: '10px 0' }}>
No sufficient analyst coverage found.
</div>
)}
{buyModalRecs && !buyModalRecs.error && (
<div style={{ display: 'flex', gap: '8px', fontSize: '0.85rem', textAlign: 'center' }}>
{(buyModalRecs.strongBuy + buyModalRecs.buy) > 0 && (
<div style={{ flex: buyModalRecs.strongBuy + buyModalRecs.buy, background: 'var(--accent-green-bg)', color: 'var(--accent-green)', padding: '8px 4px', borderRadius: '6px', fontWeight: 'bold' }}>
Buy<br/>({buyModalRecs.strongBuy + buyModalRecs.buy})
</div>
)}
{buyModalRecs.hold > 0 && (
<div style={{ flex: buyModalRecs.hold, background: 'var(--accent-orange-bg)', color: 'var(--accent-orange)', padding: '8px 4px', borderRadius: '6px', fontWeight: 'bold' }}>
Hold<br/>({buyModalRecs.hold})
</div>
)}
{(buyModalRecs.sell + buyModalRecs.strongSell) > 0 && (
<div style={{ flex: buyModalRecs.sell + buyModalRecs.strongSell, background: 'var(--accent-red-bg)', color: 'var(--accent-red)', padding: '8px 4px', borderRadius: '6px', fontWeight: 'bold' }}>
Sell<br/>({buyModalRecs.sell + buyModalRecs.strongSell})
</div>
)}
</div>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', marginTop: '25px' }}>
<a
href={`https://trendlyne.com/research-reports/stock/${researchModalData.symbol.replace('.NS', '')}`}
target="_blank"
rel="noreferrer"
className="action-btn"
style={{ padding: '12px', background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border-color)', fontWeight: 'bold', textDecoration: 'none' }}
>
Read Detailed Trendlyne Reports
</a>
<button className="nav-btn" style={{ padding: '12px', margin: 0, justifyContent: 'center' }} onClick={() => setResearchModalData(null)}>Close</button>
</div>
</div>
</div>
)}
</div>
);
}
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
</Routes>
</Router>
);
}
export default App;

View File

@@ -1,6 +1,14 @@
import React from "react";
import React, { useState, useEffect } from "react";
function AboutScanner({ isLoaded }) {
const [isCollapsed, setIsCollapsed] = useState(false);
useEffect(() => {
if (isLoaded) {
setIsCollapsed(true);
}
}, [isLoaded]);
function AboutScanner() {
return (
<div
style={{
@@ -11,7 +19,16 @@ function AboutScanner() {
color: "white"
}}
>
<h2>🧠 How The Scanner Works</h2>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer" }}
onClick={() => setIsCollapsed(!isCollapsed)}
>
<h2 style={{ margin: 0 }}>🧠 How The Scanner Works</h2>
<span style={{ fontSize: "1.2rem", padding: "5px" }}>{isCollapsed ? "▼" : "▲"}</span>
</div>
{!isCollapsed && (
<div className="animate-fade-in" style={{ marginTop: "15px" }}>
<ul>
<li>
@@ -65,6 +82,8 @@ function AboutScanner() {
<li>D (&lt;60) = Avoid</li>
</ul>
</div>
)}
</div>
);
}

View File

@@ -7,7 +7,7 @@ function Backtest() {
// ✅ Fetch backtest data
const loadBacktest = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/backtest");
const res = await axios.get("/api/backtest");
setData(res.data);
} catch (err) {
console.error("Backtest Error:", err);

70
src/components/Login.js Normal file
View File

@@ -0,0 +1,70 @@
import React, { useState } from "react";
import axios from "axios";
import { useNavigate, Link } from "react-router-dom";
import forge from "node-forge";
function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const resKey = await axios.get("/api/auth/public-key");
const publicKeyPem = resKey.data.public_key;
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
const encryptedBase64 = forge.util.encode64(encrypted);
const res = await axios.post("/api/auth/login", {
username,
password: encryptedBase64
});
localStorage.setItem("token", res.data.access_token);
navigate("/");
} catch (err) {
setError(err.response?.data?.detail || "Login failed");
} finally {
setLoading(false);
}
};
return (
<div className="app-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div className="glass-card" style={{ width: '400px', textAlign: 'center' }}>
<h2 className="text-blue">📈 Login</h2>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
{error && <p className="text-red">{error}</p>}
<button type="submit" className="nav-btn active" style={{ justifyContent: 'center' }} disabled={loading}>
{loading ? "Logging in..." : "Login"}
</button>
</form>
<p style={{ marginTop: '20px' }}>Don't have an account? <Link to="/signup" className="text-blue">Sign up</Link></p>
</div>
</div>
);
}
export default Login;

View File

@@ -6,7 +6,7 @@ function LongTerm() {
const loadData = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/longterm");
const res = await axios.get("/api/longterm");
setData(res.data);
} catch (err) {
console.error(err);

View File

@@ -0,0 +1,282 @@
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
function MainDashboard({ userProfile, onBuyClick, onResearchClick }) {
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [searching, setSearching] = useState(false);
const searchTimeout = useRef(null);
const [portfolio, setPortfolio] = useState([]);
const [portfolioSummary, setPortfolioSummary] = useState(null);
const [loadingPortfolio, setLoadingPortfolio] = useState(true);
useEffect(() => {
fetchPortfolio();
}, []);
const fetchPortfolio = async () => {
try {
const res = await axios.get("/api/portfolio/");
setPortfolio(res.data.items);
setPortfolioSummary(res.data.summary);
} catch (err) {
console.error("Error fetching portfolio:", err);
}
setLoadingPortfolio(false);
};
const sellStock = async (id) => {
try {
await axios.delete(`/api/portfolio/${id}`);
fetchPortfolio(); // refresh
} catch (err) {
console.error("Error selling stock:", err);
}
};
const formatMarketCap = (cap) => {
if (!cap) return "N/A";
if (cap >= 1e12) return "₹" + (cap / 1e12).toFixed(2) + "T";
if (cap >= 1e9) return "₹" + (cap / 1e9).toFixed(2) + "B";
if (cap >= 1e7) return "₹" + (cap / 1e7).toFixed(2) + "Cr";
return "₹" + cap.toLocaleString();
};
const getMarketCapCategory = (cap) => {
if (!cap) return null;
const cr = cap / 1e7;
if (cr >= 20000) return "Large Cap";
if (cr >= 5000) return "Mid Cap";
return "Small Cap";
};
const formatPrice = (price) => {
if (!price) return "N/A";
return "₹" + price.toFixed(2);
};
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
if (searchTimeout.current) clearTimeout(searchTimeout.current);
if (query.trim().length < 2) {
setSearchResults([]);
return;
}
searchTimeout.current = setTimeout(async () => {
setSearching(true);
try {
const res = await axios.get(`/api/scanner/search?q=${query}`);
setSearchResults(res.data);
} catch (err) {
console.error(err);
}
setSearching(false);
}, 500);
};
const openChart = (symbol) => {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
};
return (
<div className="animate-fade-in" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '30px' }}>
<div style={{ width: '100%', position: 'relative' }}>
<div style={{ position: 'relative' }}>
<span style={{ position: 'absolute', left: '20px', top: '50%', transform: 'translateY(-50%)', fontSize: '1.5rem', color: 'var(--text-muted)' }}>🔍</span>
<input
type="text"
value={searchQuery}
onChange={handleSearch}
placeholder="Search Indian stocks (e.g. TATAMOTORS, INFY)..."
style={{
width: '100%',
padding: '20px 20px 20px 60px',
borderRadius: '30px',
border: '2px solid var(--accent-blue)',
background: 'rgba(0,0,0,0.4)',
color: 'white',
fontSize: '1.2rem',
boxShadow: '0 8px 32px rgba(59, 130, 246, 0.2)',
outline: 'none',
transition: 'all 0.3s'
}}
/>
{searching && <span style={{ position: 'absolute', right: '25px', top: '50%', transform: 'translateY(-50%)', color: 'var(--accent-blue)' }}>Searching...</span>}
</div>
{/* Search Results Dropdown */}
{searchResults.length > 0 && searchQuery.length >= 2 && (
<div className="glass-card" style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 100, marginTop: '10px', maxHeight: '400px', overflowY: 'auto', padding: '10px', borderRadius: '20px' }}>
{searchResults.map((result, i) => (
<div
key={i}
onClick={() => openChart(result.symbol)}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '15px',
borderBottom: '1px solid var(--border-color)',
background: 'var(--bg-tertiary)',
marginBottom: '8px',
borderRadius: '12px',
cursor: 'pointer',
transition: 'background 0.2s'
}}
onMouseOver={(e) => e.currentTarget.style.background = 'rgba(59, 130, 246, 0.1)'}
onMouseOut={(e) => e.currentTarget.style.background = 'var(--bg-tertiary)'}
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<strong style={{ color: 'var(--accent-blue)', fontSize: '1.2rem' }}>{result.symbol}</strong>
{result.sector && (
<span style={{ fontSize: '0.8rem', background: 'rgba(59, 130, 246, 0.2)', color: 'var(--accent-blue)', padding: '4px 8px', borderRadius: '6px', border: '1px solid var(--accent-blue)' }}>
{result.sector}
</span>
)}
{result.marketCap && (
<span style={{ fontSize: '0.8rem', background: 'rgba(16, 185, 129, 0.2)', color: 'var(--accent-green)', padding: '4px 8px', borderRadius: '6px', border: '1px solid var(--accent-green)' }}>
{getMarketCapCategory(result.marketCap)}
</span>
)}
</div>
<div className="text-small text-muted" style={{ display: 'flex', gap: '10px', marginTop: '6px', fontSize: '0.9rem' }}>
<span>{result.longname || result.shortname}</span>
{result.currentPrice && (
<>
<span></span>
<span style={{ color: 'var(--text-primary)' }}>Price: {formatPrice(result.currentPrice)}</span>
</>
)}
{result.marketCap && (
<>
<span></span>
<span>{formatMarketCap(result.marketCap)}</span>
</>
)}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<button
className="action-btn"
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 10px', fontSize: '0.8rem', fontWeight: 'bold', borderRadius: '8px', border: 'none', cursor: 'pointer' }}
onClick={(e) => { e.stopPropagation(); onBuyClick(result.symbol, result.currentPrice); }}
>
Buy
</button>
<button
className="action-btn"
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', padding: '4px 10px', fontSize: '0.8rem', fontWeight: 'bold', borderRadius: '8px', cursor: 'pointer' }}
onClick={(e) => { e.stopPropagation(); onResearchClick(result.symbol); }}
>
Research
</button>
<div style={{ color: 'var(--accent-green)', display: 'flex', alignItems: 'center', gap: '5px' }}>
<span>Chart</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="13" x2="18" y2="19"></line>
<polyline points="18 5 18 11 12 11"></polyline>
<polyline points="12 19 12 5 6 5"></polyline>
</svg>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Portfolio Section */}
<div style={{ width: '100%', marginTop: '50px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ display: 'flex', alignItems: 'center', gap: '10px' }}><span style={{ fontSize: '1.8rem' }}>💼</span> Mock Portfolio</h2>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>As of: {new Date().toLocaleDateString()}</span>
</div>
{loadingPortfolio ? (
<p>Loading portfolio...</p>
) : portfolio.length === 0 ? (
<div className="glass-card" style={{ textAlign: 'center', padding: '40px' }}>
<p style={{ color: 'var(--text-muted)' }}>Your mock portfolio is empty.</p>
<p style={{ fontSize: '0.9rem', color: 'var(--text-secondary)' }}>Search for a stock or run a scanner to simulate buying.</p>
</div>
) : (
<>
{/* Summary Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px', marginBottom: '30px' }}>
<div className="glass-card" style={{ textAlign: 'center' }}>
<p style={{ color: 'var(--text-secondary)', margin: '0 0 10px 0' }}>Total Invested</p>
<h3 style={{ margin: 0 }}>{portfolioSummary.total_invested.toLocaleString()}</h3>
</div>
<div className="glass-card" style={{ textAlign: 'center' }}>
<p style={{ color: 'var(--text-secondary)', margin: '0 0 10px 0' }}>Current Value</p>
<h3 style={{ margin: 0, color: 'var(--accent-blue)' }}>{portfolioSummary.total_current_value.toLocaleString()}</h3>
</div>
<div className="glass-card" style={{ textAlign: 'center', border: `1px solid ${portfolioSummary.total_pnl >= 0 ? 'var(--accent-green)' : 'var(--accent-red)'}` }}>
<p style={{ color: 'var(--text-secondary)', margin: '0 0 10px 0' }}>Overall P&L</p>
<h3 style={{ margin: 0, color: portfolioSummary.total_pnl >= 0 ? 'var(--accent-green)' : 'var(--accent-red)' }}>
{portfolioSummary.total_pnl >= 0 ? '+' : ''}{portfolioSummary.total_pnl.toLocaleString()}
<span style={{ fontSize: '1rem', marginLeft: '8px' }}>({portfolioSummary.total_pnl_percent.toFixed(2)}%)</span>
</h3>
</div>
</div>
{/* Holdings Table */}
<div className="glass-card" style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr>
<th>Symbol</th>
<th>Qty</th>
<th>DOP</th>
<th>Buy Price</th>
<th>Invested</th>
<th>LTP</th>
<th>Current Value</th>
<th>P&L</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{portfolio.map(item => (
<tr key={item.id}>
<td style={{ fontWeight: 'bold', color: 'var(--accent-blue)', cursor: 'pointer' }} onClick={() => openChart(item.symbol)}>
{item.symbol}
</td>
<td>{item.quantity}</td>
<td style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
<div>{new Date(item.purchase_date).toLocaleDateString()}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--accent-blue)', marginTop: '2px' }}>
{Math.max(0, Math.floor((new Date() - new Date(item.purchase_date)) / (1000 * 60 * 60 * 24)))} days
</div>
</td>
<td>{item.buy_price.toFixed(2)}</td>
<td>{item.invested.toLocaleString()}</td>
<td>{item.current_price.toFixed(2)}</td>
<td>{item.current_value.toLocaleString()}</td>
<td style={{ color: item.pnl >= 0 ? 'var(--accent-green)' : 'var(--accent-red)', fontWeight: 'bold' }}>
{item.pnl >= 0 ? '+' : ''}{item.pnl.toLocaleString()} ({item.pnl_percent.toFixed(2)}%)
</td>
<td>
<button className="delete-btn" onClick={() => sellStock(item.id)} title="Sell / Remove">Sell</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
</div>
);
}
export default MainDashboard;

View File

@@ -10,7 +10,7 @@ function MarketIntel() {
const loadData = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/marketintel");
const res = await axios.get("/api/scanner/marketintel");
setData(res.data);
} catch (err) {
console.error(err);

173
src/components/Profile.js Normal file
View File

@@ -0,0 +1,173 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import forge from "node-forge";
function Profile({ userProfile, onProfileUpdated }) {
const [formData, setFormData] = useState({
display_name: "",
email_id: "",
mobile_no: "",
gender: "",
password: ""
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
if (userProfile) {
setFormData({
display_name: userProfile.display_name || "",
email_id: userProfile.email_id || "",
mobile_no: userProfile.mobile_no || "",
gender: userProfile.gender || "",
password: "" // password field is blank initially
});
}
}, [userProfile]);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSave = async (e) => {
e.preventDefault();
setLoading(true);
setMessage("");
setError("");
try {
const payload = { ...formData };
if (payload.password) {
// Fetch Public Key to encrypt new password
const resKey = await axios.get("/api/auth/public-key");
const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key);
const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP');
payload.password = forge.util.encode64(encrypted);
} else {
delete payload.password;
}
await axios.put("/api/auth/update-profile", payload);
setMessage("Profile updated successfully! ✅");
if (onProfileUpdated) onProfileUpdated();
setFormData(prev => ({ ...prev, password: "" })); // Clear password field
} catch (err) {
setError(err.response?.data?.detail || "Failed to update profile");
} finally {
setLoading(false);
}
};
if (!userProfile) return <div className="glass-card text-center">Loading Profile...</div>;
return (
<div className="animate-fade-in" style={{ display: 'flex', justifyContent: 'center', marginTop: '20px' }}>
<div className="glass-card" style={{ width: '100%', maxWidth: '600px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '30px' }}>
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '2.5rem', fontWeight: 'bold', color: 'white', boxShadow: 'var(--shadow-glow)' }}>
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div>
<div>
<h2 style={{ margin: 0, fontSize: '1.8rem' }} className="text-blue">Profile Settings</h2>
<p className="text-muted" style={{ margin: '5px 0 0 0' }}>Manage your personal details and security</p>
</div>
</div>
<form onSubmit={handleSave} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Username (Read Only)</label>
<input
type="text"
value={userProfile.username}
disabled
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'rgba(0,0,0,0.2)', color: 'var(--text-muted)', cursor: 'not-allowed' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Display Name</label>
<input
type="text" name="display_name" value={formData.display_name} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="Enter your name"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Email Address</label>
<input
type="email" name="email_id" value={formData.email_id} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="your.email@example.com"
autoComplete="off"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Mobile Number</label>
<input
type="text" name="mobile_no" value={formData.mobile_no} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="+91 9999999999"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Gender</label>
<select
name="gender" value={formData.gender} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
>
<option value="">Select Gender</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div style={{ borderTop: '1px solid var(--border-color)', margin: '10px 0' }}></div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-orange" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Change Password (Optional)</label>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleChange}
style={{ width: '100%', padding: '12px', paddingRight: '45px', borderRadius: '8px', border: '1px solid rgba(245, 158, 11, 0.3)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="Leave blank to keep current password"
autoComplete="new-password"
/>
<span
onClick={() => setShowPassword(!showPassword)}
style={{ position: 'absolute', right: '15px', cursor: 'pointer', fontSize: '1.2rem', userSelect: 'none' }}
title={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? "🙈" : "👁️"}
</span>
</div>
</div>
{message && <div style={{ padding: '10px', background: 'var(--accent-green-bg)', color: 'var(--accent-green)', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}>{message}</div>}
{error && <div style={{ padding: '10px', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', borderRadius: '8px', border: '1px solid rgba(239, 68, 68, 0.2)' }}>{error}</div>}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '10px' }}>
<button type="submit" className="nav-btn active" style={{ padding: '12px 30px', fontSize: '1rem' }} disabled={loading}>
{loading ? "Saving..." : "Save Changes"}
</button>
</div>
</form>
</div>
</div>
);
}
export default Profile;

View File

@@ -0,0 +1,12 @@
import React from "react";
import { Navigate } from "react-router-dom";
function ProtectedRoute({ children }) {
const token = localStorage.getItem("token");
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}
export default ProtectedRoute;

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
function Ribbon() {
function Ribbon({ onBuyClick, onResearchClick }) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const loadData = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/ribbon");
const res = await axios.get("/api/scanner/ribbon");
setData(res.data || []);
setLoading(false);
} catch (err) {
@@ -26,7 +26,18 @@ function Ribbon() {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
};
if (loading) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Ribbon...</div>;
const removeStockFromSector = async (symbol, sector) => {
try {
await axios.delete("/api/scanner/sectors", {
data: { sector, symbol }
});
setData(prev => prev.filter(s => s.symbol !== symbol));
} catch (err) {
console.error("Error removing stock", err);
}
};
if (loading) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Ribbon (this may take up to 60 seconds for large watchlists)...</div>;
return (
<div className="glass-card animate-fade-in">
@@ -65,9 +76,35 @@ function Ribbon() {
</span>
</td>
<td>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={() => onBuyClick(s.symbol, s.price)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
<button className="action-btn" onClick={() => openChart(s.symbol)}>
Chart
</button>
{s.sector && (
<button
onClick={() => removeStockFromSector(s.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td>
</tr>
))}

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
import AboutScanner from "./AboutScanner";
function Scanner({ onSelectStock, onSignal }) {
function Scanner({ onSelectStock, onSignal, onBuyClick, onResearchClick }) {
const [data, setData] = useState(null);
const [lastBest, setLastBest] = useState(null);
const loadData = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/scan");
const res = await axios.get("/api/scanner/scan");
const newData = res.data;
const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null;
@@ -33,14 +34,41 @@ function Scanner({ onSelectStock, onSignal }) {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
};
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Scanner Data...</div>;
const removeStockFromSector = async (symbol, sector) => {
try {
await axios.delete("/api/scanner/sectors", {
data: { sector, symbol }
});
setData(prev => {
const newData = JSON.parse(JSON.stringify(prev));
if (newData.sectors && newData.sectors[sector]) {
newData.sectors[sector].stocks = newData.sectors[sector].stocks.filter(s => s.symbol !== symbol);
}
if (newData.best_5) {
newData.best_5 = newData.best_5.filter(s => s.symbol !== symbol);
}
return newData;
});
} catch (err) {
console.error("Error removing stock", err);
}
};
if (!data) return (
<>
<AboutScanner isLoaded={false} />
<div className="glass-card" style={{ textAlign: "center" }}>Loading Scanner Data (this may take up to 60 seconds for large watchlists)...</div>
</>
);
return (
<div>
<AboutScanner isLoaded={true} />
{/* Top 5 Trades */}
{(data.best_5 || []).length > 0 && (
<div className="glass-card animate-fade-in" style={{ marginBottom: "30px", borderLeft: "4px solid var(--accent-green)" }}>
<h2 className="text-green">🔥 Top 5 Momentum Picks</h2>
<h2 className="text-green">🔥 Top 10 Momentum Picks</h2>
<div className="data-table-container">
<table className="data-table">
<thead>
@@ -72,9 +100,35 @@ function Scanner({ onSelectStock, onSignal }) {
<td>{s.grade}</td>
<td>{s.institutional ? <span className="text-orange">🔥 Yes</span> : <span className="text-muted">No</span>}</td>
<td>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
View Chart
</button>
<button
onClick={() => onBuyClick(s?.symbol, s?.price || 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s?.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
{s?.sector && (
<button
onClick={() => removeStockFromSector(s?.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td>
</tr>
))}
@@ -125,9 +179,28 @@ function Scanner({ onSelectStock, onSignal }) {
<td className="text-green">{s?.target}</td>
<td>{s?.ai_score}</td>
<td>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
className="action-btn"
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 8px', fontSize: '0.8rem' }}
onClick={() => onBuyClick(s?.symbol, s?.price)}
>
Buy
</button>
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
Chart
</button>
<button
onClick={() => removeStockFromSector(s?.symbol, sector)}
title={`Remove from ${sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</td>
</tr>
))}

69
src/components/Signup.js Normal file
View File

@@ -0,0 +1,69 @@
import React, { useState } from "react";
import axios from "axios";
import { useNavigate, Link } from "react-router-dom";
import forge from "node-forge";
function Signup() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleSignup = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const resKey = await axios.get("/api/auth/public-key");
const publicKeyPem = resKey.data.public_key;
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
const encryptedBase64 = forge.util.encode64(encrypted);
await axios.post("/api/auth/signup", {
username,
password: encryptedBase64
});
navigate("/login");
} catch (err) {
setError(err.response?.data?.detail || "Signup failed");
} finally {
setLoading(false);
}
};
return (
<div className="app-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div className="glass-card" style={{ width: '400px', textAlign: 'center' }}>
<h2 className="text-green"> Sign Up</h2>
<form onSubmit={handleSignup} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
{error && <p className="text-red">{error}</p>}
<button type="submit" className="nav-btn active" style={{ justifyContent: 'center', background: 'var(--accent-green)', borderColor: 'var(--accent-green)' }} disabled={loading}>
{loading ? "Signing up..." : "Sign Up"}
</button>
</form>
<p style={{ marginTop: '20px' }}>Already have an account? <Link to="/login" className="text-blue">Login</Link></p>
</div>
</div>
);
}
export default Signup;

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
function SmartMoney() {
function SmartMoney({ onBuyClick, onResearchClick }) {
const [data, setData] = useState(null);
useEffect(() => {
@@ -12,7 +12,7 @@ function SmartMoney() {
const loadData = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/smartmoney");
const res = await axios.get("/api/scanner/smartmoney");
setData(res.data);
} catch (err) {
console.error(err);
@@ -23,7 +23,30 @@ function SmartMoney() {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
};
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Smart Money Data...</div>;
const removeStockFromSector = async (symbol, sector) => {
try {
await axios.delete("/api/scanner/sectors", {
data: { sector, symbol }
});
setData(prev => {
const newData = JSON.parse(JSON.stringify(prev));
if (newData.top_10) {
newData.top_10 = newData.top_10.filter(s => s.symbol !== symbol);
}
if (newData.all) {
newData.all = newData.all.filter(s => s.symbol !== symbol);
}
if (newData.trade_of_day && newData.trade_of_day.symbol === symbol) {
newData.trade_of_day = newData.top_10.length > 0 ? newData.top_10[0] : null;
}
return newData;
});
} catch (err) {
console.error("Error removing stock", err);
}
};
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Smart Money Data (this may take up to 60 seconds for large watchlists)...</div>;
return (
<div className="animate-fade-in">
@@ -80,7 +103,33 @@ function SmartMoney() {
<td>{s.breakout ? "🔥 YES" : "NO"}</td>
<td>{s.reason}</td>
<td>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={() => onBuyClick(s.symbol, s.price || s.current_price || 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
{s.sector && (
<button
onClick={() => removeStockFromSector(s.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td>
</tr>
))}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
function TrendScanner() {
function TrendScanner({ onBuyClick, onResearchClick }) {
const [data, setData] = useState(null);
useEffect(() => {
@@ -12,7 +12,7 @@ function TrendScanner() {
const loadData = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/trend");
const res = await axios.get("/api/scanner/trend");
setData(res.data);
} catch (err) {
console.error(err);
@@ -23,7 +23,24 @@ function TrendScanner() {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
};
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Trend Data...</div>;
const removeStockFromSector = async (symbol, sector) => {
try {
await axios.delete("/api/scanner/sectors", {
data: { sector, symbol }
});
setData(prev => {
const newData = JSON.parse(JSON.stringify(prev));
if (newData.top_20) {
newData.top_20 = newData.top_20.filter(s => s.symbol !== symbol);
}
return newData;
});
} catch (err) {
console.error("Error removing stock", err);
}
};
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Trend Data (this may take up to 60 seconds for large watchlists)...</div>;
return (
<div className="animate-fade-in">
@@ -63,7 +80,33 @@ function TrendScanner() {
</td>
<td>{s.trend_reason}</td>
<td>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={() => onBuyClick(s.symbol, s.price || s.current_price || 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
{s.sector && (
<button
onClick={() => removeStockFromSector(s.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td>
</tr>
))}

View File

@@ -0,0 +1,293 @@
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
function WatchlistManager({ userProfile, onProfileUpdated, onBuyClick, onResearchClick }) {
const [sectorMap, setSectorMap] = useState({});
const [newSectorName, setNewSectorName] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [searching, setSearching] = useState(false);
const [selectedSector, setSelectedSector] = useState("Personal");
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
const searchTimeout = useRef(null);
const formatMarketCap = (cap) => {
if (!cap) return "N/A";
if (cap >= 1e12) return "₹" + (cap / 1e12).toFixed(2) + "T";
if (cap >= 1e9) return "₹" + (cap / 1e9).toFixed(2) + "B";
if (cap >= 1e7) return "₹" + (cap / 1e7).toFixed(2) + "Cr";
return "₹" + cap.toLocaleString();
};
const getMarketCapCategory = (cap) => {
if (!cap) return null;
const cr = cap / 1e7; // Convert to Crores
if (cr >= 20000) return "Large Cap";
if (cr >= 5000) return "Mid Cap";
return "Small Cap";
};
const formatPrice = (price) => {
if (!price) return "N/A";
return "₹" + price.toFixed(2);
};
useEffect(() => {
if (userProfile && userProfile.sector_map) {
setSectorMap(userProfile.sector_map);
}
}, [userProfile]);
const saveSectors = async (updatedMap) => {
setSaving(true);
setMessage("");
try {
await axios.put("/api/auth/update-sectors", { sector_map: updatedMap });
setSectorMap(updatedMap);
if (onProfileUpdated) onProfileUpdated();
setMessage("Sectors saved! ✅");
setTimeout(() => setMessage(""), 3000);
} catch (err) {
console.error(err);
setMessage("Failed to save sectors.");
}
setSaving(false);
};
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
if (searchTimeout.current) clearTimeout(searchTimeout.current);
if (query.trim().length < 2) {
setSearchResults([]);
return;
}
searchTimeout.current = setTimeout(async () => {
setSearching(true);
try {
const res = await axios.get(`/api/scanner/search?q=${query}`);
setSearchResults(res.data);
} catch (err) {
console.error(err);
}
setSearching(false);
}, 500);
};
const addStockToSector = (symbol, targetSector) => {
const updatedMap = { ...sectorMap };
if (!updatedMap[targetSector]) updatedMap[targetSector] = [];
if (!updatedMap[targetSector].includes(symbol)) {
updatedMap[targetSector].push(symbol);
saveSectors(updatedMap);
}
};
const removeStockFromSector = (symbol, targetSector) => {
const updatedMap = { ...sectorMap };
if (updatedMap[targetSector]) {
updatedMap[targetSector] = updatedMap[targetSector].filter(s => s !== symbol);
saveSectors(updatedMap);
}
};
const createSector = () => {
const name = newSectorName.trim();
if (name && !sectorMap[name]) {
const updatedMap = { ...sectorMap, [name]: [] };
saveSectors(updatedMap);
setNewSectorName("");
}
};
const deleteSector = (sector) => {
if (sector === "Personal") return; // Protect Personal
if (window.confirm(`Are you sure you want to delete the ${sector} watchlist?`)) {
const updatedMap = { ...sectorMap };
delete updatedMap[sector];
saveSectors(updatedMap);
if (selectedSector === sector) setSelectedSector("Personal");
}
};
return (
<div className="animate-fade-in" style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', marginTop: '20px', alignItems: 'flex-start' }}>
{/* LEFT PANEL: Sector List & Creator */}
<div className="glass-card" style={{ flex: '1', minWidth: '250px' }}>
<h3 className="text-blue" style={{ marginTop: 0 }}>📋 Watchlists</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '20px' }}>
{Object.keys(sectorMap).map(sector => (
<div
key={sector}
onClick={() => setSelectedSector(sector)}
style={{
padding: '10px 15px',
background: selectedSector === sector ? 'rgba(59, 130, 246, 0.2)' : 'var(--bg-tertiary)',
border: selectedSector === sector ? '1px solid var(--accent-blue)' : '1px solid var(--border-color)',
borderRadius: '8px', cursor: 'pointer',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
transition: 'all 0.2s'
}}
>
<span style={{ fontWeight: selectedSector === sector ? 'bold' : 'normal', color: selectedSector === sector ? 'var(--accent-blue)' : 'white' }}>
{sector} <span className="text-muted text-small">({sectorMap[sector].length})</span>
</span>
{sector !== "Personal" && (
<button onClick={(e) => { e.stopPropagation(); deleteSector(sector); }} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '5px' }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
))}
</div>
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '15px' }}>
<p className="text-small text-muted" style={{ margin: '0 0 10px 0' }}>Create new watchlist</p>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text" value={newSectorName} onChange={e => setNewSectorName(e.target.value)}
placeholder="E.g., EV Stocks"
style={{ flex: 1, padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
/>
<button onClick={createSector} className="nav-btn active" style={{ padding: '0 15px' }}>Add</button>
</div>
</div>
</div>
{/* RIGHT PANEL: Search & Stock List */}
<div className="glass-card" style={{ flex: '3' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2 style={{ margin: 0, color: selectedSector === 'Personal' ? 'var(--accent-green)' : 'white' }}>
{selectedSector} Watchlist
</h2>
{saving && <span className="text-muted">Saving...</span>}
{message && <span className="text-green">{message}</span>}
</div>
{/* Search Bar */}
<div style={{ position: 'relative', marginBottom: '30px' }}>
<input
type="text"
value={searchQuery}
onChange={handleSearch}
placeholder={`Search Indian stocks to add to ${selectedSector}... (e.g. TATAMOTORS)`}
style={{ width: '100%', padding: '15px', borderRadius: '8px', border: '1px solid var(--accent-blue)', background: 'rgba(0,0,0,0.3)', color: 'white', fontSize: '1.1rem' }}
/>
{searching && <span style={{ position: 'absolute', right: '15px', top: '15px' }}>Searching...</span>}
{/* Search Results Dropdown */}
{searchResults.length > 0 && searchQuery.length >= 2 && (
<div className="glass-card" style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 10, marginTop: '5px', maxHeight: '300px', overflowY: 'auto', padding: '5px' }}>
{searchResults.map((result, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px', borderBottom: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', marginBottom: '5px', borderRadius: '5px' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<strong style={{ color: 'var(--accent-blue)', fontSize: '1.1rem' }}>{result.symbol}</strong>
{result.sector && (
<span style={{ fontSize: '0.75rem', background: 'rgba(59, 130, 246, 0.2)', color: 'var(--accent-blue)', padding: '2px 6px', borderRadius: '4px', border: '1px solid var(--accent-blue)' }}>
{result.sector}
</span>
)}
{result.marketCap && (
<span style={{ fontSize: '0.75rem', background: 'rgba(16, 185, 129, 0.2)', color: 'var(--accent-green)', padding: '2px 6px', borderRadius: '4px', border: '1px solid var(--accent-green)' }}>
{getMarketCapCategory(result.marketCap)}
</span>
)}
</div>
<div className="text-small text-muted" style={{ display: 'flex', gap: '10px', marginTop: '4px' }}>
<span>{result.longname || result.shortname}</span>
{result.currentPrice && (
<>
<span></span>
<span style={{ color: 'var(--text-primary)' }}>Price: {formatPrice(result.currentPrice)}</span>
</>
)}
{result.marketCap && (
<>
<span></span>
<span>{formatMarketCap(result.marketCap)}</span>
</>
)}
</div>
</div>
<button
onClick={() => { addStockToSector(result.symbol, selectedSector); setSearchResults([]); setSearchQuery(""); }}
style={{ background: 'var(--accent-green)', color: 'white', border: 'none', borderRadius: '50%', width: '35px', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center', cursor: 'pointer', fontSize: '1.2rem', flexShrink: 0, boxShadow: '0 4px 6px rgba(16, 185, 129, 0.3)' }}
title={`Add to ${selectedSector}`}
>
+
</button>
</div>
))}
</div>
)}
</div>
{/* Stock List Grid */}
{sectorMap[selectedSector] && sectorMap[selectedSector].length === 0 ? (
<div className="text-center text-muted" style={{ padding: '40px' }}>No stocks in this watchlist. Search above to add some!</div>
) : (
<div style={{ maxHeight: '60vh', overflowY: 'auto', paddingRight: '10px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '15px' }}>
{(sectorMap[selectedSector] || []).map(symbol => (
<div key={symbol} style={{ display: 'flex', flexDirection: 'column', gap: '10px', background: 'var(--bg-tertiary)', padding: '12px 15px', borderRadius: '8px', border: '1px solid var(--border-color)' }}>
{/* Top Row: Symbol and Trash */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 'bold' }}>{symbol}</span>
<button
onClick={() => removeStockFromSector(symbol, selectedSector)}
title="Remove from this watchlist"
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
{/* Bottom Row: Actions */}
<div style={{ display: 'flex', gap: '10px' }}>
{selectedSector !== "Personal" && (
<button
onClick={() => { addStockToSector(symbol, "Personal"); }}
title="Add to Personal"
style={{ flex: 1, background: 'rgba(16, 185, 129, 0.1)', border: '1px solid var(--accent-green)', color: 'var(--accent-green)', borderRadius: '4px', cursor: 'pointer', padding: '6px', fontSize: '0.85rem' }}
>
+ Personal
</button>
)}
<button
onClick={() => onBuyClick(symbol, 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
export default WatchlistManager;

View File

@@ -83,9 +83,9 @@ h2 {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 10px 20px;
padding: 8px 14px;
border-radius: 8px;
font-size: 0.95rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
@@ -214,3 +214,54 @@ h2 {
.text-orange { color: var(--accent-orange); }
.text-blue { color: var(--accent-blue); }
.text-muted { color: var(--text-muted); }
/* RESPONSIVE DESIGN */
@media (max-width: 768px) {
.app-container {
padding: 20px 10px;
}
.nav-container {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 10px;
margin-bottom: 25px;
}
.nav-btn {
white-space: nowrap;
flex-shrink: 0;
}
.glass-card {
padding: 16px;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.3rem; }
.data-table th, .data-table td {
padding: 10px 8px;
font-size: 0.85rem;
}
/* Adjust grid columns for extreme small devices */
.stock-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.app-container {
padding: 15px 8px;
}
h1 { font-size: 1.8rem; }
.action-btn {
padding: 4px 8px;
font-size: 0.75rem;
}
}

View File

@@ -1,8 +1,17 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import App from './App';
import axios from 'axios';
const root = ReactDOM.createRoot(document.getElementById("root"));
axios.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />