Customize - Stock map list, watch list, home page
This commit is contained in:
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"]
|
||||||
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,9 @@ class ProfileUpdateRequest(BaseModel):
|
|||||||
gender: str = None
|
gender: str = None
|
||||||
password: str = None # Base64 encoded RSA-encrypted new password
|
password: str = None # Base64 encoded RSA-encrypted new password
|
||||||
|
|
||||||
|
class SectorMapUpdateRequest(BaseModel):
|
||||||
|
sector_map: dict
|
||||||
|
|
||||||
@router.get("/public-key")
|
@router.get("/public-key")
|
||||||
def get_public_key():
|
def get_public_key():
|
||||||
return {"public_key": public_key.decode("utf-8")}
|
return {"public_key": public_key.decode("utf-8")}
|
||||||
@@ -84,9 +87,16 @@ def get_me(current_user: User = Depends(get_current_user)):
|
|||||||
"display_name": current_user.display_name,
|
"display_name": current_user.display_name,
|
||||||
"email_id": current_user.email_id,
|
"email_id": current_user.email_id,
|
||||||
"mobile_no": current_user.mobile_no,
|
"mobile_no": current_user.mobile_no,
|
||||||
"gender": current_user.gender
|
"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")
|
@router.put("/update-profile")
|
||||||
def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
if req.display_name is not None:
|
if req.display_name is not None:
|
||||||
|
|||||||
@@ -4,24 +4,56 @@ from app.services.stock_engine import (
|
|||||||
get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel
|
get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/scan")
|
@router.get("/scan")
|
||||||
def scan(current_user = Depends(get_current_user)):
|
def scan(current_user = Depends(get_current_user)):
|
||||||
return get_scan()
|
return get_scan(current_user.sector_map or {})
|
||||||
|
|
||||||
@router.get("/ribbon")
|
@router.get("/ribbon")
|
||||||
def ribbon(current_user = Depends(get_current_user)):
|
def ribbon(current_user = Depends(get_current_user)):
|
||||||
return get_ribbon()
|
return get_ribbon(current_user.sector_map or {})
|
||||||
|
|
||||||
@router.get("/smartmoney")
|
@router.get("/smartmoney")
|
||||||
def smartmoney(current_user = Depends(get_current_user)):
|
def smartmoney(current_user = Depends(get_current_user)):
|
||||||
return get_smartmoney()
|
return get_smartmoney(current_user.sector_map or {})
|
||||||
|
|
||||||
@router.get("/trend")
|
@router.get("/trend")
|
||||||
def trend(current_user = Depends(get_current_user)):
|
def trend(current_user = Depends(get_current_user)):
|
||||||
return get_trend()
|
return get_trend(current_user.sector_map or {})
|
||||||
|
|
||||||
@router.get("/marketintel")
|
@router.get("/marketintel")
|
||||||
def marketintel(current_user = Depends(get_current_user)):
|
def marketintel(current_user = Depends(get_current_user)):
|
||||||
return get_marketintel()
|
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 []
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
DATABASE_URL = "postgresql://postgres:M%40triXPostgr3s%406202@103.125.129.116:5333/stock_scanner"
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal:5333/stock_scanner")
|
||||||
SECRET_KEY = "stock-scanner-super-secret-key-12345"
|
SECRET_KEY = "stock-scanner-super-secret-key-12345"
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String, JSON
|
||||||
from app.db.database import Base
|
from app.db.database import Base
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
@@ -11,3 +11,4 @@ class User(Base):
|
|||||||
email_id = Column(String, nullable=True)
|
email_id = Column(String, nullable=True)
|
||||||
mobile_no = Column(String, nullable=True)
|
mobile_no = Column(String, nullable=True)
|
||||||
gender = Column(String, nullable=True)
|
gender = Column(String, nullable=True)
|
||||||
|
sector_map = Column(JSON, nullable=True)
|
||||||
|
|||||||
Binary file not shown.
@@ -425,12 +425,12 @@ def analyze(stock):
|
|||||||
# =========================================
|
# =========================================
|
||||||
# ✅ API - FAST SCAN
|
# ✅ API - FAST SCAN
|
||||||
# =========================================
|
# =========================================
|
||||||
def get_scan():
|
def get_scan(user_sector_map):
|
||||||
|
|
||||||
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]
|
||||||
@@ -536,26 +536,28 @@ def analyze_ribbon(stock):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_ribbon():
|
def get_ribbon(user_sector_map):
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def get_smartmoney():
|
def get_smartmoney(user_sector_map):
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -575,15 +577,15 @@ def get_smartmoney():
|
|||||||
"all": results
|
"all": results
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_trend():
|
def get_trend(user_sector_map):
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
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
|
||||||
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:5333/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,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",
|
||||||
|
|||||||
22
src/App.js
22
src/App.js
@@ -9,22 +9,25 @@ 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 Login from "./components/Login";
|
||||||
import Signup from "./components/Signup";
|
import Signup from "./components/Signup";
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
import ProtectedRoute from "./components/ProtectedRoute";
|
||||||
import Profile from "./components/Profile";
|
import Profile from "./components/Profile";
|
||||||
|
import MainDashboard from "./components/MainDashboard";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
function Dashboard() {
|
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 [userProfile, setUserProfile] = useState(null);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [scannerLoaded, setScannerLoaded] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const fetchProfile = () => {
|
const fetchProfile = () => {
|
||||||
axios.get("http://127.0.0.1:8000/api/auth/me")
|
axios.get("/api/auth/me")
|
||||||
.then(res => setUserProfile(res.data))
|
.then(res => setUserProfile(res.data))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.response?.status === 401) handleLogout();
|
if (err.response?.status === 401) handleLogout();
|
||||||
@@ -44,7 +47,10 @@ function Dashboard() {
|
|||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
|
||||||
<h1 style={{ margin: 0 }}>📈 Stock Scanner Pro</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<span style={{ fontSize: '2.5rem' }}>📊</span>
|
||||||
|
<h1 style={{ margin: 0 }}>Stock Scanner Pro</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
{userProfile && (
|
{userProfile && (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
@@ -74,18 +80,22 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nav-container">
|
<div className="nav-container">
|
||||||
|
<button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '8px 12px' }}>🏠</button>
|
||||||
<button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")}>📊 Scanner</button>
|
<button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")}>📊 Scanner</button>
|
||||||
<button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")}>📈 Ribbon Strategy</button>
|
<button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")}>📈 Ribbon Strategy</button>
|
||||||
<button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")}>🏦 Smart Money</button>
|
<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 === "trend" ? "active" : ""}`} onClick={() => setView("trend")}>⚡ Trend Analysis</button>
|
||||||
<button className={`nav-btn ${view === "marketintel" ? "active" : ""}`} onClick={() => setView("marketintel")}>📰 Market Intel</button>
|
<button className={`nav-btn ${view === "marketintel" ? "active" : ""}`} onClick={() => setView("marketintel")}>📰 Market Intel</button>
|
||||||
|
<button className={`nav-btn ${view === "watchlists" ? "active" : ""}`} onClick={() => setView("watchlists")}>📋 Watchlists</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}
|
||||||
|
/>
|
||||||
<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} />
|
||||||
@@ -93,10 +103,12 @@ function Dashboard() {
|
|||||||
{signal && <Popup signal={signal} />}
|
{signal && <Popup signal={signal} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{view === "dashboard" && <MainDashboard userProfile={userProfile} />}
|
||||||
{view === "ribbon" && <Ribbon />}
|
{view === "ribbon" && <Ribbon />}
|
||||||
{view === "smartmoney" && <SmartMoney />}
|
{view === "smartmoney" && <SmartMoney />}
|
||||||
{view === "marketintel" && <MarketIntel />}
|
{view === "marketintel" && <MarketIntel />}
|
||||||
{view === "trend" && <TrendScanner />}
|
{view === "trend" && <TrendScanner />}
|
||||||
|
{view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} />}
|
||||||
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
|
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
function AboutScanner({ isLoaded }) {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoaded) {
|
||||||
|
setIsCollapsed(true);
|
||||||
|
}
|
||||||
|
}, [isLoaded]);
|
||||||
|
|
||||||
function AboutScanner() {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -11,7 +19,16 @@ function AboutScanner() {
|
|||||||
color: "white"
|
color: "white"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2>🧠 How The Scanner Works</h2>
|
<div
|
||||||
|
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer" }}
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
>
|
||||||
|
<h2 style={{ margin: 0 }}>🧠 How The Scanner Works</h2>
|
||||||
|
<span style={{ fontSize: "1.2rem", padding: "5px" }}>{isCollapsed ? "▼" : "▲"}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="animate-fade-in" style={{ marginTop: "15px" }}>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@@ -64,6 +81,8 @@ function AboutScanner() {
|
|||||||
<li>C (60-69) = Average</li>
|
<li>C (60-69) = Average</li>
|
||||||
<li>D (<60) = Avoid</li>
|
<li>D (<60) = Avoid</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Backtest() {
|
|||||||
// ✅ 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);
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ function Login() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key");
|
const resKey = await axios.get("/api/auth/public-key");
|
||||||
const publicKeyPem = resKey.data.public_key;
|
const publicKeyPem = resKey.data.public_key;
|
||||||
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
||||||
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
|
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
|
||||||
const encryptedBase64 = forge.util.encode64(encrypted);
|
const encryptedBase64 = forge.util.encode64(encrypted);
|
||||||
|
|
||||||
const res = await axios.post("http://127.0.0.1:8000/api/auth/login", {
|
const res = await axios.post("/api/auth/login", {
|
||||||
username,
|
username,
|
||||||
password: encryptedBase64
|
password: encryptedBase64
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function LongTerm() {
|
|||||||
|
|
||||||
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);
|
||||||
|
|||||||
155
src/components/MainDashboard.js
Normal file
155
src/components/MainDashboard.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
function MainDashboard({ userProfile }) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
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;
|
||||||
|
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={{ 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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/api/scanner/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);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function Profile({ userProfile, onProfileUpdated }) {
|
|||||||
|
|
||||||
if (payload.password) {
|
if (payload.password) {
|
||||||
// Fetch Public Key to encrypt new password
|
// Fetch Public Key to encrypt new password
|
||||||
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key");
|
const resKey = await axios.get("/api/auth/public-key");
|
||||||
const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key);
|
const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key);
|
||||||
const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP');
|
const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP');
|
||||||
payload.password = forge.util.encode64(encrypted);
|
payload.password = forge.util.encode64(encrypted);
|
||||||
@@ -51,7 +51,7 @@ function Profile({ userProfile, onProfileUpdated }) {
|
|||||||
delete payload.password;
|
delete payload.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.put("http://127.0.0.1:8000/api/auth/update-profile", payload);
|
await axios.put("/api/auth/update-profile", payload);
|
||||||
setMessage("Profile updated successfully! ✅");
|
setMessage("Profile updated successfully! ✅");
|
||||||
if (onProfileUpdated) onProfileUpdated();
|
if (onProfileUpdated) onProfileUpdated();
|
||||||
setFormData(prev => ({ ...prev, password: "" })); // Clear password field
|
setFormData(prev => ({ ...prev, password: "" })); // Clear password field
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Ribbon() {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/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,23 @@ 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 className="action-btn" onClick={() => openChart(s.symbol)}>
|
||||||
</button>
|
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,5 +1,6 @@
|
|||||||
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 }) {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@@ -7,7 +8,7 @@ function Scanner({ onSelectStock, onSignal }) {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/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,10 +34,37 @@ 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)" }}>
|
||||||
@@ -72,9 +100,23 @@ 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>
|
||||||
|
{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 +167,21 @@ 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 className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
||||||
</button>
|
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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ function Signup() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key");
|
const resKey = await axios.get("/api/auth/public-key");
|
||||||
const publicKeyPem = resKey.data.public_key;
|
const publicKeyPem = resKey.data.public_key;
|
||||||
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
||||||
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
|
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
|
||||||
const encryptedBase64 = forge.util.encode64(encrypted);
|
const encryptedBase64 = forge.util.encode64(encrypted);
|
||||||
|
|
||||||
await axios.post("http://127.0.0.1:8000/api/auth/signup", {
|
await axios.post("/api/auth/signup", {
|
||||||
username,
|
username,
|
||||||
password: encryptedBase64
|
password: encryptedBase64
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/api/scanner/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,21 @@ 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 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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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/api/scanner/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,21 @@ 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 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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
279
src/components/WatchlistManager.js
Normal file
279
src/components/WatchlistManager.js
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
function WatchlistManager({ userProfile, onProfileUpdated }) {
|
||||||
|
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', 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: + Personal */}
|
||||||
|
{selectedSector !== "Personal" && (
|
||||||
|
<button
|
||||||
|
onClick={() => { addStockToSector(symbol, "Personal"); }}
|
||||||
|
title="Add to Personal"
|
||||||
|
style={{ width: '100%', 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' }}
|
||||||
|
>
|
||||||
|
+ Add to Personal
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</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;
|
||||||
|
|||||||
Reference in New Issue
Block a user