Compare commits
4 Commits
main
...
user_speci
| Author | SHA1 | Date | |
|---|---|---|---|
| f33011b219 | |||
| e4ad2233b8 | |||
| 53ad1abb78 | |||
| 076c4ff68f |
21
Dockerfile.frontend
Normal file
21
Dockerfile.frontend
Normal 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;"]
|
||||||
14
backend/Dockerfile.backend
Normal file
14
backend/Dockerfile.backend
Normal 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"]
|
||||||
BIN
backend/app/__pycache__/main.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/auth.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/portfolio.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/portfolio.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/scanner.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/scanner.cpython-313.pyc
Normal file
Binary file not shown.
116
backend/app/api/auth.py
Normal file
116
backend/app/api/auth.py
Normal 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"}
|
||||||
144
backend/app/api/portfolio.py
Normal file
144
backend/app/api/portfolio.py
Normal 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}
|
||||||
59
backend/app/api/scanner.py
Normal file
59
backend/app/api/scanner.py
Normal 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}"esCount=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 []
|
||||||
BIN
backend/app/core/__pycache__/config.cpython-313.pyc
Normal file
BIN
backend/app/core/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/security.cpython-313.pyc
Normal file
BIN
backend/app/core/__pycache__/security.cpython-313.pyc
Normal file
Binary file not shown.
6
backend/app/core/config.py
Normal file
6
backend/app/core/config.py
Normal 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
|
||||||
61
backend/app/core/security.py
Normal file
61
backend/app/core/security.py
Normal 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)
|
||||||
BIN
backend/app/db/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/app/db/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/db/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/app/db/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
15
backend/app/db/database.py
Normal file
15
backend/app/db/database.py
Normal 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
25
backend/app/db/models.py
Normal 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
27
backend/app/main.py
Normal 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."}
|
||||||
BIN
backend/app/services/__pycache__/stock_engine.cpython-313.pyc
Normal file
BIN
backend/app/services/__pycache__/stock_engine.cpython-313.pyc
Normal file
Binary file not shown.
@@ -1,20 +1,8 @@
|
|||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
import yfinance as yf
|
import yfinance as yf
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# ✅ PERSONAL
|
# ✅ PERSONAL
|
||||||
# ✅ PERSONAL
|
# ✅ PERSONAL
|
||||||
personal_stocks = [
|
personal_stocks = [
|
||||||
@@ -437,13 +425,12 @@ def analyze(stock):
|
|||||||
# =========================================
|
# =========================================
|
||||||
# ✅ API - FAST SCAN
|
# ✅ API - FAST SCAN
|
||||||
# =========================================
|
# =========================================
|
||||||
@app.get("/scan")
|
def get_scan(user_sector_map):
|
||||||
def scan():
|
|
||||||
|
|
||||||
sector_output = {}
|
sector_output = {}
|
||||||
all_stocks = []
|
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 = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
||||||
data = [x for x in data if x]
|
data = [x for x in data if x]
|
||||||
@@ -549,28 +536,28 @@ def analyze_ribbon(stock):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/ribbon")
|
def get_ribbon(user_sector_map):
|
||||||
def ribbon():
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for stocks in sector_map.values():
|
for sector, stocks in user_sector_map.items():
|
||||||
data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks))
|
data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks))
|
||||||
data = [x for x in data if x]
|
data = [x for x in data if x]
|
||||||
|
for x in data:
|
||||||
|
x["sector"] = sector
|
||||||
results.extend(data)
|
results.extend(data)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@app.get("/smartmoney")
|
def get_smartmoney(user_sector_map):
|
||||||
def smartmoney():
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for sector, stocks in sector_map.items():
|
for sector, stocks in user_sector_map.items():
|
||||||
|
|
||||||
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
||||||
data = [x for x in data if x]
|
data = [x for x in data if x]
|
||||||
|
for x in data:
|
||||||
|
x["sector"] = sector
|
||||||
results.extend(data)
|
results.extend(data)
|
||||||
|
|
||||||
|
|
||||||
@@ -590,16 +577,15 @@ def smartmoney():
|
|||||||
"all": results
|
"all": results
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/trend")
|
def get_trend(user_sector_map):
|
||||||
def trend():
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for sector, stocks in sector_map.items():
|
for sector, stocks in user_sector_map.items():
|
||||||
|
|
||||||
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
||||||
data = [x for x in data if x]
|
data = [x for x in data if x]
|
||||||
|
for x in data:
|
||||||
|
x["sector"] = sector
|
||||||
results.extend(data)
|
results.extend(data)
|
||||||
|
|
||||||
results.sort(
|
results.sort(
|
||||||
@@ -623,8 +609,7 @@ def trend():
|
|||||||
# - Full sector_map (too large)
|
# - Full sector_map (too large)
|
||||||
# - Deep MACD + Supertrend
|
# - Deep MACD + Supertrend
|
||||||
# - 6 month data calls
|
# - 6 month data calls
|
||||||
@app.get("/marketintel")
|
def get_marketintel():
|
||||||
def marketintel():
|
|
||||||
try:
|
try:
|
||||||
nifty_news = yf.Ticker("^NSEI").news or []
|
nifty_news = yf.Ticker("^NSEI").news or []
|
||||||
reliance_news = yf.Ticker("RELIANCE.NS").news or []
|
reliance_news = yf.Ticker("RELIANCE.NS").news or []
|
||||||
27
backend/private.pem
Normal file
27
backend/private.pem
Normal 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
9
backend/public.pem
Normal 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
114
backend/requirements.txt
Normal 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
50
build_and_push.sh
Executable 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
19
docker-compose.yml
Normal 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
10
nginx.conf
Normal 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
17
package-lock.json
generated
@@ -14,9 +14,10 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
"lightweight-charts": "^5.2.0",
|
"lightweight-charts": "^5.2.0",
|
||||||
|
"node-forge": "^1.4.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^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",
|
"react-scripts": "5.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
}
|
}
|
||||||
@@ -13958,9 +13959,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.18.0",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
|
||||||
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
|
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@@ -13980,12 +13981,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.18.0",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
|
||||||
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
|
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.18.0"
|
"react-router": "7.18.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
"lightweight-charts": "^5.2.0",
|
"lightweight-charts": "^5.2.0",
|
||||||
|
"node-forge": "^1.4.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^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",
|
"react-scripts": "5.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
|
"proxy": "http://127.0.0.1:8000",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
|
|||||||
27
private.pem
Normal file
27
private.pem
Normal 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
9
public.pem
Normal 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
18
run_backend.sh
Executable 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
|
||||||
309
src/App.js
309
src/App.js
@@ -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 Chart from "./components/Chart";
|
||||||
import Scanner from "./components/Scanner";
|
import Scanner from "./components/Scanner";
|
||||||
import Ribbon from "./components/Ribbon";
|
import Ribbon from "./components/Ribbon";
|
||||||
@@ -7,59 +9,141 @@ import Popup from "./components/Popup";
|
|||||||
import AboutScanner from "./components/AboutScanner";
|
import AboutScanner from "./components/AboutScanner";
|
||||||
import MarketIntel from "./components/MarketIntel";
|
import MarketIntel from "./components/MarketIntel";
|
||||||
import TrendScanner from "./components/TrendScanner";
|
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";
|
import "./index.css";
|
||||||
|
|
||||||
function App() {
|
function Dashboard() {
|
||||||
const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE");
|
const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE");
|
||||||
const [signal, setSignal] = useState(null);
|
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 (
|
return (
|
||||||
<div className="app-container">
|
<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>
|
||||||
|
|
||||||
|
{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)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="nav-container">
|
{dropdownOpen && (
|
||||||
<button
|
<div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '50px', right: '0', width: '200px', zIndex: 100, padding: '8px' }}>
|
||||||
className={`nav-btn ${view === "scanner" ? "active" : ""}`}
|
<button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap', padding: '8px' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
|
||||||
onClick={() => setView("scanner")}
|
⚙️ Profile Settings
|
||||||
>
|
</button>
|
||||||
📊 Scanner
|
<button className="nav-btn" style={{ width: '100%', border: 'none', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', whiteSpace: 'nowrap', padding: '8px' }} onClick={handleLogout}>
|
||||||
</button>
|
🚪 Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="nav-container" style={{ gap: '8px' }}>
|
||||||
className={`nav-btn ${view === "ribbon" ? "active" : ""}`}
|
<button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '6px 12px' }}>🏠</button>
|
||||||
onClick={() => setView("ribbon")}
|
<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>
|
||||||
📈 Ribbon Strategy
|
<button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>🏦 Smart Money</button>
|
||||||
</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
|
<button className={`nav-btn ${view === "watchlists" ? "active" : ""}`} onClick={() => setView("watchlists")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📋 Watchlists</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>
|
</div>
|
||||||
|
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
{view === "scanner" && (
|
{view === "scanner" && (
|
||||||
<>
|
<>
|
||||||
<AboutScanner />
|
<Scanner
|
||||||
<Scanner onSelectStock={setSelectedStock} onSignal={setSignal} />
|
onSelectStock={setSelectedStock}
|
||||||
|
onSignal={setSignal}
|
||||||
|
onBuyClick={onBuyClick}
|
||||||
|
onResearchClick={onResearchClick}
|
||||||
|
/>
|
||||||
<div className="glass-card" style={{ marginTop: "20px" }}>
|
<div className="glass-card" style={{ marginTop: "20px" }}>
|
||||||
<h2>TradingView Chart</h2>
|
<h2>TradingView Chart</h2>
|
||||||
<Chart symbol={selectedStock} />
|
<Chart symbol={selectedStock} />
|
||||||
@@ -67,17 +151,146 @@ function App() {
|
|||||||
{signal && <Popup signal={signal} />}
|
{signal && <Popup signal={signal} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{view === "dashboard" && <MainDashboard userProfile={userProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||||
{view === "ribbon" && <Ribbon />}
|
{view === "ribbon" && <Ribbon onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||||
|
{view === "smartmoney" && <SmartMoney onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||||
{view === "smartmoney" && <SmartMoney />}
|
{view === "marketintel" && <MarketIntel onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||||
|
{view === "trend" && <TrendScanner onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||||
{view === "marketintel" && <MarketIntel />}
|
{view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||||
|
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
|
||||||
{view === "trend" && <TrendScanner />}
|
|
||||||
</div>
|
</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>
|
</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;
|
export default App;
|
||||||
@@ -1,71 +1,90 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
function AboutScanner() {
|
function AboutScanner({ isLoaded }) {
|
||||||
return (
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
<div
|
|
||||||
style={{
|
useEffect(() => {
|
||||||
background: "#111827",
|
if (isLoaded) {
|
||||||
padding: "20px",
|
setIsCollapsed(true);
|
||||||
borderRadius: "10px",
|
}
|
||||||
marginBottom: "20px",
|
}, [isLoaded]);
|
||||||
color: "white"
|
|
||||||
}}
|
return (
|
||||||
>
|
<div
|
||||||
<h2>🧠 How The Scanner Works</h2>
|
style={{
|
||||||
|
background: "#111827",
|
||||||
<ul>
|
padding: "20px",
|
||||||
<li>
|
borderRadius: "10px",
|
||||||
<b>📈 Trend:</b> Price above EMA20 indicates bullish trend.
|
marginBottom: "20px",
|
||||||
</li>
|
color: "white"
|
||||||
|
}}
|
||||||
<li>
|
>
|
||||||
<b>⚡ Momentum:</b> RSI measures strength of buying momentum.
|
<div
|
||||||
</li>
|
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer" }}
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
<li>
|
>
|
||||||
<b>🚀 Breakout:</b> Detects stocks breaking recent resistance levels.
|
<h2 style={{ margin: 0 }}>🧠 How The Scanner Works</h2>
|
||||||
</li>
|
<span style={{ fontSize: "1.2rem", padding: "5px" }}>{isCollapsed ? "▼" : "▲"}</span>
|
||||||
|
</div>
|
||||||
<li>
|
|
||||||
<b>🔥 Volume Ratio:</b> Compares current volume with average volume.
|
{!isCollapsed && (
|
||||||
</li>
|
<div className="animate-fade-in" style={{ marginTop: "15px" }}>
|
||||||
|
|
||||||
<li>
|
<ul>
|
||||||
<b>💪 Relative Strength (RS):</b> Measures stock performance vs NIFTY.
|
<li>
|
||||||
</li>
|
<b>📈 Trend:</b> Price above EMA20 indicates bullish trend.
|
||||||
|
</li>
|
||||||
<li>
|
|
||||||
<b>🏦 Smart Money Score:</b> Estimates institutional accumulation probability.
|
<li>
|
||||||
</li>
|
<b>⚡ Momentum:</b> RSI measures strength of buying momentum.
|
||||||
|
</li>
|
||||||
<li>
|
|
||||||
<b>🤖 AI Score:</b> Combined score using trend, RSI, breakout, volume and RS.
|
<li>
|
||||||
</li>
|
<b>🚀 Breakout:</b> Detects stocks breaking recent resistance levels.
|
||||||
|
</li>
|
||||||
<li>
|
|
||||||
<b>✅ Confidence:</b> Probability of a quality setup based on AI Score.
|
<li>
|
||||||
</li>
|
<b>🔥 Volume Ratio:</b> Compares current volume with average volume.
|
||||||
|
</li>
|
||||||
<li>
|
|
||||||
<b>🎯 Fund Candidate:</b> High-volume breakout stocks showing potential accumulation.
|
<li>
|
||||||
</li>
|
<b>💪 Relative Strength (RS):</b> Measures stock performance vs NIFTY.
|
||||||
|
</li>
|
||||||
<li>
|
|
||||||
<b>🏆 Trade Of The Day:</b> Highest ranked stock across all sectors.
|
<li>
|
||||||
</li>
|
<b>🏦 Smart Money Score:</b> Estimates institutional accumulation probability.
|
||||||
</ul>
|
</li>
|
||||||
|
|
||||||
<h3>🏅 Score Guide</h3>
|
<li>
|
||||||
|
<b>🤖 AI Score:</b> Combined score using trend, RSI, breakout, volume and RS.
|
||||||
<ul>
|
</li>
|
||||||
<li>A+ (90-100) = Exceptional</li>
|
|
||||||
<li>A (80-89) = Strong</li>
|
<li>
|
||||||
<li>B (70-79) = Good</li>
|
<b>✅ Confidence:</b> Probability of a quality setup based on AI Score.
|
||||||
<li>C (60-69) = Average</li>
|
</li>
|
||||||
<li>D (<60) = Avoid</li>
|
|
||||||
</ul>
|
<li>
|
||||||
</div>
|
<b>🎯 Fund Candidate:</b> High-volume breakout stocks showing potential accumulation.
|
||||||
);
|
</li>
|
||||||
}
|
|
||||||
|
<li>
|
||||||
|
<b>🏆 Trade Of The Day:</b> Highest ranked stock across all sectors.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>🏅 Score Guide</h3>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>A+ (90-100) = Exceptional</li>
|
||||||
|
<li>A (80-89) = Strong</li>
|
||||||
|
<li>B (70-79) = Good</li>
|
||||||
|
<li>C (60-69) = Average</li>
|
||||||
|
<li>D (<60) = Avoid</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default AboutScanner;
|
export default AboutScanner;
|
||||||
@@ -1,163 +1,163 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function Backtest() {
|
function Backtest() {
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
|
|
||||||
// ✅ Fetch backtest data
|
// ✅ Fetch backtest data
|
||||||
const loadBacktest = async () => {
|
const loadBacktest = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/backtest");
|
const res = await axios.get("/api/backtest");
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Backtest Error:", err);
|
console.error("Backtest Error:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBacktest();
|
loadBacktest();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "#0d1117",
|
background: "#0d1117",
|
||||||
color: "#c9d1d9",
|
color: "#c9d1d9",
|
||||||
padding: 20,
|
padding: 20,
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1 style={{ color: "#58a6ff" }}>
|
<h1 style={{ color: "#58a6ff" }}>
|
||||||
📊 Backtest Dashboard (Strategy Performance)
|
📊 Backtest Dashboard (Strategy Performance)
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* ✅ EXPLANATION PANEL */}
|
{/* ✅ EXPLANATION PANEL */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "#161b22",
|
background: "#161b22",
|
||||||
border: "1px solid #30363d",
|
border: "1px solid #30363d",
|
||||||
padding: 15,
|
padding: 15,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ color: "#58a6ff" }}>📘 How to Read Backtest</h2>
|
<h2 style={{ color: "#58a6ff" }}>📘 How to Read Backtest</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
✅ <b>Trades</b> = Total opportunities generated
|
✅ <b>Trades</b> = Total opportunities generated
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
✅ <b>Wins</b> = Profitable trades
|
✅ <b>Wins</b> = Profitable trades
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
✅ <b>Loss</b> = Losing trades
|
✅ <b>Loss</b> = Losing trades
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
✅ <b>Win Rate</b> = Success % of strategy
|
✅ <b>Win Rate</b> = Success % of strategy
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
✅ <b>Profit</b> = Net gain (simulated)
|
✅ <b>Profit</b> = Net gain (simulated)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
💡 <b>Best Strategy Rules:</b>
|
💡 <b>Best Strategy Rules:</b>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Win Rate ≥ 60% ✅</li>
|
<li>Win Rate ≥ 60% ✅</li>
|
||||||
<li>Trades ≥ 10 ✅</li>
|
<li>Trades ≥ 10 ✅</li>
|
||||||
<li>Profit positive ✅</li>
|
<li>Profit positive ✅</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ✅ SECTORS */}
|
{/* ✅ SECTORS */}
|
||||||
{Object.entries(data).map(([sector, stocks]) => (
|
{Object.entries(data).map(([sector, stocks]) => (
|
||||||
<div
|
<div
|
||||||
key={sector}
|
key={sector}
|
||||||
style={{
|
style={{
|
||||||
background: "#161b22",
|
background: "#161b22",
|
||||||
border: "1px solid #30363d",
|
border: "1px solid #30363d",
|
||||||
padding: 15,
|
padding: 15,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ color: "#58a6ff" }}>
|
<h2 style={{ color: "#58a6ff" }}>
|
||||||
{sector}
|
{sector}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<table
|
<table
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
style={{
|
||||||
borderCollapse: "collapse",
|
borderCollapse: "collapse",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: "1px solid #30363d" }}>
|
<tr style={{ borderBottom: "1px solid #30363d" }}>
|
||||||
<th>Stock</th>
|
<th>Stock</th>
|
||||||
<th>Trades</th>
|
<th>Trades</th>
|
||||||
<th>Wins</th>
|
<th>Wins</th>
|
||||||
<th>Loss</th>
|
<th>Loss</th>
|
||||||
<th>Win %</th>
|
<th>Win %</th>
|
||||||
<th>Profit</th>
|
<th>Profit</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{stocks.map((s, i) => (
|
{stocks.map((s, i) => (
|
||||||
<tr
|
<tr
|
||||||
key={i}
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
borderBottom: "1px solid #30363d",
|
borderBottom: "1px solid #30363d",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td>{s.symbol}</td>
|
<td>{s.symbol}</td>
|
||||||
|
|
||||||
<td>{s.trades}</td>
|
<td>{s.trades}</td>
|
||||||
|
|
||||||
<td style={{ color: "#3fb950" }}>
|
<td style={{ color: "#3fb950" }}>
|
||||||
{s.wins}
|
{s.wins}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style={{ color: "#f85149" }}>
|
<td style={{ color: "#f85149" }}>
|
||||||
{s.loss}
|
{s.loss}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* ✅ WIN RATE COLOR */}
|
{/* ✅ WIN RATE COLOR */}
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
color:
|
color:
|
||||||
s.win_rate >= 70
|
s.win_rate >= 70
|
||||||
? "#3fb950"
|
? "#3fb950"
|
||||||
: s.win_rate >= 60
|
: s.win_rate >= 60
|
||||||
? "#d29922"
|
? "#d29922"
|
||||||
: "#f85149",
|
: "#f85149",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{s.win_rate}%
|
{s.win_rate}%
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* ✅ PROFIT COLOR */}
|
{/* ✅ PROFIT COLOR */}
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
color:
|
color:
|
||||||
s.profit > 0 ? "#3fb950" : "#f85149",
|
s.profit > 0 ? "#3fb950" : "#f85149",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{s.profit}
|
{s.profit}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Backtest;
|
export default Backtest;
|
||||||
296
src/components/Chart.js
vendored
296
src/components/Chart.js
vendored
@@ -1,149 +1,149 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { createChart, CandlestickSeries, LineSeries } from "lightweight-charts";
|
import { createChart, CandlestickSeries, LineSeries } from "lightweight-charts";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function Chart({ symbol }) {
|
function Chart({ symbol }) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !symbol) return;
|
if (!ref.current || !symbol) return;
|
||||||
|
|
||||||
ref.current.innerHTML = "";
|
ref.current.innerHTML = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chart = createChart(ref.current, {
|
const chart = createChart(ref.current, {
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 500,
|
height: 500,
|
||||||
layout: {
|
layout: {
|
||||||
background: { color: "#111" },
|
background: { color: "#111" },
|
||||||
textColor: "#DDD",
|
textColor: "#DDD",
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
vertLines: { color: "#333" },
|
vertLines: { color: "#333" },
|
||||||
horzLines: { color: "#333" },
|
horzLines: { color: "#333" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ MAIN CANDLE SERIES
|
// ✅ MAIN CANDLE SERIES
|
||||||
const candleSeries = chart.addSeries(CandlestickSeries);
|
const candleSeries = chart.addSeries(CandlestickSeries);
|
||||||
|
|
||||||
// ✅ EMA LINE
|
// ✅ EMA LINE
|
||||||
const emaSeries = chart.addSeries(LineSeries, {
|
const emaSeries = chart.addSeries(LineSeries, {
|
||||||
color: "yellow",
|
color: "yellow",
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ RSI LINE (drawn on same chart for simplicity)
|
// ✅ RSI LINE (drawn on same chart for simplicity)
|
||||||
const rsiSeries = chart.addSeries(LineSeries, {
|
const rsiSeries = chart.addSeries(LineSeries, {
|
||||||
color: "cyan",
|
color: "cyan",
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ EMA CALCULATION
|
// ✅ EMA CALCULATION
|
||||||
const calculateEMA = (data, period = 20) => {
|
const calculateEMA = (data, period = 20) => {
|
||||||
const k = 2 / (period + 1);
|
const k = 2 / (period + 1);
|
||||||
let ema = data[0].close;
|
let ema = data[0].close;
|
||||||
return data.map((d) => {
|
return data.map((d) => {
|
||||||
ema = d.close * k + ema * (1 - k);
|
ema = d.close * k + ema * (1 - k);
|
||||||
return { time: d.time, value: ema };
|
return { time: d.time, value: ema };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ RSI CALCULATION
|
// ✅ RSI CALCULATION
|
||||||
const calculateRSI = (data, period = 14) => {
|
const calculateRSI = (data, period = 14) => {
|
||||||
let gains = 0;
|
let gains = 0;
|
||||||
let losses = 0;
|
let losses = 0;
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (let i = 1; i < data.length; i++) {
|
for (let i = 1; i < data.length; i++) {
|
||||||
const diff = data[i].close - data[i - 1].close;
|
const diff = data[i].close - data[i - 1].close;
|
||||||
|
|
||||||
if (diff > 0) gains += diff;
|
if (diff > 0) gains += diff;
|
||||||
else losses -= diff;
|
else losses -= diff;
|
||||||
|
|
||||||
if (i >= period) {
|
if (i >= period) {
|
||||||
const rs = gains / (losses || 1);
|
const rs = gains / (losses || 1);
|
||||||
const rsi = 100 - 100 / (1 + rs);
|
const rsi = 100 - 100 / (1 + rs);
|
||||||
|
|
||||||
result.push({ time: data[i].time, value: rsi });
|
result.push({ time: data[i].time, value: rsi });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ LOAD DATA FUNCTION
|
// ✅ LOAD DATA FUNCTION
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
axios
|
axios
|
||||||
.get(`http://localhost:8000/history?symbol=${symbol}`)
|
.get(`http://localhost:8000/history?symbol=${symbol}`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.data || res.data.length === 0) return;
|
if (!res.data || res.data.length === 0) return;
|
||||||
|
|
||||||
const formatted = res.data.map((d) => ({
|
const formatted = res.data.map((d) => ({
|
||||||
time: Math.floor(d.time),
|
time: Math.floor(d.time),
|
||||||
open: Number(d.open),
|
open: Number(d.open),
|
||||||
high: Number(d.high),
|
high: Number(d.high),
|
||||||
low: Number(d.low),
|
low: Number(d.low),
|
||||||
close: Number(d.close),
|
close: Number(d.close),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ✅ SET CANDLES
|
// ✅ SET CANDLES
|
||||||
candleSeries.setData(formatted);
|
candleSeries.setData(formatted);
|
||||||
|
|
||||||
// ✅ EMA
|
// ✅ EMA
|
||||||
const emaData = calculateEMA(formatted);
|
const emaData = calculateEMA(formatted);
|
||||||
emaSeries.setData(emaData);
|
emaSeries.setData(emaData);
|
||||||
|
|
||||||
// ✅ RSI
|
// ✅ RSI
|
||||||
const rsiData = calculateRSI(formatted);
|
const rsiData = calculateRSI(formatted);
|
||||||
rsiSeries.setData(rsiData);
|
rsiSeries.setData(rsiData);
|
||||||
|
|
||||||
// ✅ ENTRY / SL / TARGET
|
// ✅ ENTRY / SL / TARGET
|
||||||
const lastPrice = formatted[formatted.length - 1].close;
|
const lastPrice = formatted[formatted.length - 1].close;
|
||||||
|
|
||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: lastPrice,
|
price: lastPrice,
|
||||||
color: "blue",
|
color: "blue",
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
title: "Entry",
|
title: "Entry",
|
||||||
});
|
});
|
||||||
|
|
||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: lastPrice * 0.97,
|
price: lastPrice * 0.97,
|
||||||
color: "red",
|
color: "red",
|
||||||
title: "SL",
|
title: "SL",
|
||||||
});
|
});
|
||||||
|
|
||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: lastPrice * 1.05,
|
price: lastPrice * 1.05,
|
||||||
color: "green",
|
color: "green",
|
||||||
title: "Target",
|
title: "Target",
|
||||||
});
|
});
|
||||||
|
|
||||||
chart.timeScale().fitContent();
|
chart.timeScale().fitContent();
|
||||||
})
|
})
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ INITIAL LOAD
|
// ✅ INITIAL LOAD
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
// ✅ LIVE UPDATE (every 1 min)
|
// ✅ LIVE UPDATE (every 1 min)
|
||||||
const interval = setInterval(loadData, 60000);
|
const interval = setInterval(loadData, 60000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
chart.remove();
|
chart.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Chart crash:", err);
|
console.error("Chart crash:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [symbol]);
|
}, [symbol]);
|
||||||
|
|
||||||
return <div ref={ref}></div>;
|
return <div ref={ref}></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Chart;
|
export default Chart;
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
function ChartPage({ symbol }) {
|
function ChartPage({ symbol }) {
|
||||||
const tvSymbol = symbol || "NSE:RELIANCE";
|
const tvSymbol = symbol || "NSE:RELIANCE";
|
||||||
|
|
||||||
const url = `https://www.tradingview.com/chart/?symbol=${tvSymbol}`;
|
const url = `https://www.tradingview.com/chart/?symbol=${tvSymbol}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: "600px", marginTop: "20px" }}>
|
<div style={{ width: "100%", height: "600px", marginTop: "20px" }}>
|
||||||
<iframe
|
<iframe
|
||||||
title="TradingView Chart"
|
title="TradingView Chart"
|
||||||
src={url}
|
src={url}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChartPage;
|
export default ChartPage;
|
||||||
70
src/components/Login.js
Normal file
70
src/components/Login.js
Normal 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;
|
||||||
@@ -1,114 +1,114 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function LongTerm() {
|
function LongTerm() {
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/longterm");
|
const res = await axios.get("/api/longterm");
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: "#0d1117",
|
background: "#0d1117",
|
||||||
color: "#c9d1d9",
|
color: "#c9d1d9",
|
||||||
padding: 20,
|
padding: 20,
|
||||||
minHeight: "100vh"
|
minHeight: "100vh"
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<h1 style={{ color: "#58a6ff" }}>
|
<h1 style={{ color: "#58a6ff" }}>
|
||||||
📈 Long-Term Investment Dashboard
|
📈 Long-Term Investment Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* ✅ EXPLANATION */}
|
{/* ✅ EXPLANATION */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: "#161b22",
|
background: "#161b22",
|
||||||
padding: 15,
|
padding: 15,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
marginBottom: 20
|
marginBottom: 20
|
||||||
}}>
|
}}>
|
||||||
<h3>📘 Strategy</h3>
|
<h3>📘 Strategy</h3>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>✅ Golden Cross = 50 EMA > 200 EMA</li>
|
<li>✅ Golden Cross = 50 EMA > 200 EMA</li>
|
||||||
<li>✅ Support = Price above 200 EMA</li>
|
<li>✅ Support = Price above 200 EMA</li>
|
||||||
<li>✅ Momentum = Breakout + Volume</li>
|
<li>✅ Momentum = Breakout + Volume</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>💡 Only strong trend reversal stocks are shown</p>
|
<p>💡 Only strong trend reversal stocks are shown</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Object.entries(data).map(([sector, info]) => {
|
{Object.entries(data).map(([sector, info]) => {
|
||||||
|
|
||||||
|
|
||||||
const filtered = info.stocks.filter(
|
const filtered = info.stocks.filter(
|
||||||
s => s.golden_cross || s.support_strength
|
s => s.golden_cross || s.support_strength
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (filtered.length === 0) return null;
|
if (filtered.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={sector} style={{
|
<div key={sector} style={{
|
||||||
background: "#161b22",
|
background: "#161b22",
|
||||||
padding: 15,
|
padding: 15,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
borderRadius: 10
|
borderRadius: 10
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<h2 style={{ color: "#3fb950" }}>
|
<h2 style={{ color: "#3fb950" }}>
|
||||||
{sector} ✅ Long-Term Strong
|
{sector} ✅ Long-Term Strong
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<table width="100%" style={{ textAlign: "center" }}>
|
<table width="100%" style={{ textAlign: "center" }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Stock</th>
|
<th>Stock</th>
|
||||||
<th>Score</th>
|
<th>Score</th>
|
||||||
<th>RSI</th>
|
<th>RSI</th>
|
||||||
<th>Golden Cross</th>
|
<th>Golden Cross</th>
|
||||||
<th>Support</th>
|
<th>Support</th>
|
||||||
<th>Signal</th>
|
<th>Signal</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{filtered.map((s, i) => (
|
{filtered.map((s, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td>{s.symbol}</td>
|
<td>{s.symbol}</td>
|
||||||
<td>{s.score}</td>
|
<td>{s.score}</td>
|
||||||
<td>{s.rsi}</td>
|
<td>{s.rsi}</td>
|
||||||
|
|
||||||
<td style={{ color: "#3fb950" }}>
|
<td style={{ color: "#3fb950" }}>
|
||||||
✅
|
✅
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style={{ color: "#3fb950" }}>
|
<td style={{ color: "#3fb950" }}>
|
||||||
✅
|
✅
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style={{ color: "#3fb950" }}>
|
<td style={{ color: "#3fb950" }}>
|
||||||
{s.signal}
|
{s.signal}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LongTerm;
|
export default LongTerm;
|
||||||
|
|||||||
282
src/components/MainDashboard.js
Normal file
282
src/components/MainDashboard.js
Normal 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;
|
||||||
@@ -10,7 +10,7 @@ function MarketIntel() {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/marketintel");
|
const res = await axios.get("/api/scanner/marketintel");
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
function Popup({ signal }) {
|
function Popup({ signal }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const popup = document.getElementById("popup");
|
const popup = document.getElementById("popup");
|
||||||
if (popup) popup.style.display = "none";
|
if (popup) popup.style.display = "none";
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="popup" className="popup">
|
<div id="popup" className="popup">
|
||||||
<h3>🚨 Trade Signal</h3>
|
<h3>🚨 Trade Signal</h3>
|
||||||
<p>Stock: {signal.name}</p>
|
<p>Stock: {signal.name}</p>
|
||||||
<p>Code: {signal.code}</p>
|
<p>Code: {signal.code}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Popup;
|
export default Popup;
|
||||||
173
src/components/Profile.js
Normal file
173
src/components/Profile.js
Normal 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;
|
||||||
12
src/components/ProtectedRoute.js
Normal file
12
src/components/ProtectedRoute.js
Normal 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;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function Ribbon() {
|
function Ribbon({ onBuyClick, onResearchClick }) {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/ribbon");
|
const res = await axios.get("/api/scanner/ribbon");
|
||||||
setData(res.data || []);
|
setData(res.data || []);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -26,7 +26,18 @@ function Ribbon() {
|
|||||||
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
|
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 (
|
return (
|
||||||
<div className="glass-card animate-fade-in">
|
<div className="glass-card animate-fade-in">
|
||||||
@@ -65,9 +76,35 @@ function Ribbon() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="action-btn" onClick={() => openChart(s.symbol)}>
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
Chart
|
<button
|
||||||
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import AboutScanner from "./AboutScanner";
|
||||||
|
|
||||||
function Scanner({ onSelectStock, onSignal }) {
|
function Scanner({ onSelectStock, onSignal, onBuyClick, onResearchClick }) {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [lastBest, setLastBest] = useState(null);
|
const [lastBest, setLastBest] = useState(null);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
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 newData = res.data;
|
||||||
|
|
||||||
const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null;
|
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");
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<AboutScanner isLoaded={true} />
|
||||||
|
|
||||||
{/* Top 5 Trades */}
|
{/* Top 5 Trades */}
|
||||||
{(data.best_5 || []).length > 0 && (
|
{(data.best_5 || []).length > 0 && (
|
||||||
<div className="glass-card animate-fade-in" style={{ marginBottom: "30px", borderLeft: "4px solid var(--accent-green)" }}>
|
<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">
|
<div className="data-table-container">
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -72,9 +100,35 @@ function Scanner({ onSelectStock, onSignal }) {
|
|||||||
<td>{s.grade}</td>
|
<td>{s.grade}</td>
|
||||||
<td>{s.institutional ? <span className="text-orange">🔥 Yes</span> : <span className="text-muted">No</span>}</td>
|
<td>{s.institutional ? <span className="text-orange">🔥 Yes</span> : <span className="text-muted">No</span>}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
View Chart
|
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
||||||
</button>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -125,9 +179,28 @@ function Scanner({ onSelectStock, onSignal }) {
|
|||||||
<td className="text-green">₹{s?.target}</td>
|
<td className="text-green">₹{s?.target}</td>
|
||||||
<td>{s?.ai_score}</td>
|
<td>{s?.ai_score}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
Chart
|
<button
|
||||||
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
69
src/components/Signup.js
Normal file
69
src/components/Signup.js
Normal 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;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function SmartMoney() {
|
function SmartMoney({ onBuyClick, onResearchClick }) {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -12,7 +12,7 @@ function SmartMoney() {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/smartmoney");
|
const res = await axios.get("/api/scanner/smartmoney");
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -23,7 +23,30 @@ function SmartMoney() {
|
|||||||
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
|
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 (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
@@ -80,7 +103,33 @@ function SmartMoney() {
|
|||||||
<td>{s.breakout ? "🔥 YES" : "NO"}</td>
|
<td>{s.breakout ? "🔥 YES" : "NO"}</td>
|
||||||
<td>{s.reason}</td>
|
<td>{s.reason}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function TrendScanner() {
|
function TrendScanner({ onBuyClick, onResearchClick }) {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -12,7 +12,7 @@ function TrendScanner() {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/trend");
|
const res = await axios.get("/api/scanner/trend");
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -23,7 +23,24 @@ function TrendScanner() {
|
|||||||
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
|
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 (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
@@ -63,7 +80,33 @@ function TrendScanner() {
|
|||||||
</td>
|
</td>
|
||||||
<td>{s.trend_reason}</td>
|
<td>{s.trend_reason}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
293
src/components/WatchlistManager.js
Normal file
293
src/components/WatchlistManager.js
Normal 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;
|
||||||
@@ -83,9 +83,9 @@ h2 {
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
padding: 10px 20px;
|
padding: 8px 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -214,3 +214,54 @@ h2 {
|
|||||||
.text-orange { color: var(--accent-orange); }
|
.text-orange { color: var(--accent-orange); }
|
||||||
.text-blue { color: var(--accent-blue); }
|
.text-blue { color: var(--accent-blue); }
|
||||||
.text-muted { color: var(--text-muted); }
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
src/index.js
13
src/index.js
@@ -1,8 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
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(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
Reference in New Issue
Block a user