From 2516bcb203ca37dae1c6230877982f0536391cd9 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:52:59 +0530
Subject: [PATCH 01/13] feat: add Supabase backend infrastructure and setup
documentation
---
backend/SUPABASE_SETUP.md | 61 +++++++++++++++++++++++++++++++
backend/add-user-id-migration.sql | 48 ++++++++++++++++++++++++
backend/clean-database.js | 39 ++++++++++++++++++++
backend/fix-rls-policies.sql | 43 ++++++++++++++++++++++
backend/init-db.js | 33 +++++++++++++++++
backend/src/config/supabase.js | 16 ++++++++
backend/src/middleware/auth.js | 55 ++++++++++++++++++++++++++++
backend/supabase-schema.sql | 32 ++++++++++++++++
8 files changed, 327 insertions(+)
create mode 100644 backend/SUPABASE_SETUP.md
create mode 100644 backend/add-user-id-migration.sql
create mode 100644 backend/clean-database.js
create mode 100644 backend/fix-rls-policies.sql
create mode 100644 backend/init-db.js
create mode 100644 backend/src/config/supabase.js
create mode 100644 backend/src/middleware/auth.js
create mode 100644 backend/supabase-schema.sql
diff --git a/backend/SUPABASE_SETUP.md b/backend/SUPABASE_SETUP.md
new file mode 100644
index 0000000..eea403e
--- /dev/null
+++ b/backend/SUPABASE_SETUP.md
@@ -0,0 +1,61 @@
+# Supabase Setup Guide
+
+This guide will help you set up your Supabase project for the Portfolio Tracker application.
+
+## 1. Create a Supabase Project
+
+1. Go to [https://supabase.com/](https://supabase.com/) and sign up or log in
+2. Click "New Project" and follow the steps to create a new project
+3. Choose a name for your project and set a secure database password
+4. Select a region closest to you
+5. Wait for your project to be created (this may take a few minutes)
+
+## 2. Create Database Tables
+
+1. In your Supabase dashboard, go to the "SQL Editor" section
+2. Create a new query
+3. Copy and paste the contents of the `supabase-schema.sql` file into the editor
+4. Click "Run" to execute the SQL and create the tables
+
+## 3. Set Up API Access
+
+1. In your Supabase dashboard, go to the "Settings" section
+2. Click on "API" in the sidebar
+3. Under "Project API keys", copy the "anon" public key
+4. Also copy the "URL" value from the "Project URL" section
+
+## 4. Configure Environment Variables
+
+Update your `.env` file with the Supabase URL and key:
+
+```
+SUPABASE_URL=your_project_url
+SUPABASE_KEY=your_anon_key
+FINNHUB_API_KEY=your_finnhub_api_key
+PORT=5000
+```
+
+## 5. Test the Connection
+
+Run the initialization script to test your connection:
+
+```bash
+npm run init-db
+```
+
+If successful, you should see a message confirming the connection to Supabase.
+
+## 6. Row Level Security (Optional but Recommended)
+
+For production environments, you should set up Row Level Security (RLS) policies in Supabase:
+
+1. Go to the "Authentication" section, then "Policies"
+2. For each table, create appropriate RLS policies based on your requirements
+
+## 7. Additional Configuration
+
+- Consider setting up Supabase Auth if you want to add user authentication
+- Set up automated backups for your database
+- Configure CORS settings if needed
+
+For more information, refer to the [Supabase documentation](https://supabase.com/docs).
\ No newline at end of file
diff --git a/backend/add-user-id-migration.sql b/backend/add-user-id-migration.sql
new file mode 100644
index 0000000..fe110af
--- /dev/null
+++ b/backend/add-user-id-migration.sql
@@ -0,0 +1,48 @@
+-- Migration to add user_id columns for user-specific data isolation
+-- Run this in your Supabase SQL Editor
+
+-- Add user_id column to stocks table
+ALTER TABLE stocks
+ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
+
+-- Add user_id column to watchlists table
+ALTER TABLE watchlists
+ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
+
+-- Create indexes for better performance
+CREATE INDEX IF NOT EXISTS idx_stocks_user_id ON stocks(user_id);
+CREATE INDEX IF NOT EXISTS idx_watchlists_user_id ON watchlists(user_id);
+
+-- Add RLS (Row Level Security) policies
+-- Enable RLS on both tables
+ALTER TABLE stocks ENABLE ROW LEVEL SECURITY;
+ALTER TABLE watchlists ENABLE ROW LEVEL SECURITY;
+
+-- Create policy for stocks table - users can only see their own stocks
+CREATE POLICY "Users can view own stocks" ON stocks
+ FOR SELECT USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can insert own stocks" ON stocks
+ FOR INSERT WITH CHECK (auth.uid() = user_id);
+
+CREATE POLICY "Users can update own stocks" ON stocks
+ FOR UPDATE USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can delete own stocks" ON stocks
+ FOR DELETE USING (auth.uid() = user_id);
+
+-- Create policy for watchlists table - users can only see their own watchlist items
+CREATE POLICY "Users can view own watchlist" ON watchlists
+ FOR SELECT USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can insert own watchlist items" ON watchlists
+ FOR INSERT WITH CHECK (auth.uid() = user_id);
+
+CREATE POLICY "Users can update own watchlist items" ON watchlists
+ FOR UPDATE USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can delete own watchlist items" ON watchlists
+ FOR DELETE USING (auth.uid() = user_id);
+
+-- Note: After running this migration, existing data will have NULL user_id
+-- You may want to clean up existing data or assign it to a specific user
\ No newline at end of file
diff --git a/backend/clean-database.js b/backend/clean-database.js
new file mode 100644
index 0000000..f56be3d
--- /dev/null
+++ b/backend/clean-database.js
@@ -0,0 +1,39 @@
+require('dotenv').config();
+const supabase = require('./src/config/supabase');
+
+async function cleanDatabase() {
+ try {
+ console.log('Cleaning database...');
+
+ // Delete all records from stocks table
+ const { error: stocksError } = await supabase
+ .from('stocks')
+ .delete()
+ .neq('id', 0); // This will delete all records
+
+ if (stocksError) {
+ console.error('Error deleting stocks:', stocksError);
+ } else {
+ console.log('Successfully deleted all stocks');
+ }
+
+ // Delete all records from watchlists table
+ const { error: watchlistsError } = await supabase
+ .from('watchlists')
+ .delete()
+ .neq('id', 0); // This will delete all records
+
+ if (watchlistsError) {
+ console.error('Error deleting watchlists:', watchlistsError);
+ } else {
+ console.log('Successfully deleted all watchlists');
+ }
+
+ console.log('Database cleaning completed');
+ } catch (error) {
+ console.error('Error cleaning database:', error);
+ }
+}
+
+// Run the cleaning
+cleanDatabase();
\ No newline at end of file
diff --git a/backend/fix-rls-policies.sql b/backend/fix-rls-policies.sql
new file mode 100644
index 0000000..d5c32e2
--- /dev/null
+++ b/backend/fix-rls-policies.sql
@@ -0,0 +1,43 @@
+-- Fix RLS policies to work with backend API authentication
+-- Run this in your Supabase SQL Editor
+
+-- Drop existing policies
+DROP POLICY IF EXISTS "Users can view own stocks" ON stocks;
+DROP POLICY IF EXISTS "Users can insert own stocks" ON stocks;
+DROP POLICY IF EXISTS "Users can update own stocks" ON stocks;
+DROP POLICY IF EXISTS "Users can delete own stocks" ON stocks;
+
+DROP POLICY IF EXISTS "Users can view own watchlist" ON watchlists;
+DROP POLICY IF EXISTS "Users can insert own watchlist items" ON watchlists;
+DROP POLICY IF EXISTS "Users can update own watchlist items" ON watchlists;
+DROP POLICY IF EXISTS "Users can delete own watchlist items" ON watchlists;
+
+-- Create new policies that work with backend API
+-- For stocks table
+CREATE POLICY "Users can view own stocks" ON stocks
+ FOR SELECT USING (user_id IS NOT NULL);
+
+CREATE POLICY "Users can insert own stocks" ON stocks
+ FOR INSERT WITH CHECK (user_id IS NOT NULL);
+
+CREATE POLICY "Users can update own stocks" ON stocks
+ FOR UPDATE USING (user_id IS NOT NULL);
+
+CREATE POLICY "Users can delete own stocks" ON stocks
+ FOR DELETE USING (user_id IS NOT NULL);
+
+-- For watchlists table
+CREATE POLICY "Users can view own watchlist" ON watchlists
+ FOR SELECT USING (user_id IS NOT NULL);
+
+CREATE POLICY "Users can insert own watchlist items" ON watchlists
+ FOR INSERT WITH CHECK (user_id IS NOT NULL);
+
+CREATE POLICY "Users can update own watchlist items" ON watchlists
+ FOR UPDATE USING (user_id IS NOT NULL);
+
+CREATE POLICY "Users can delete own watchlist items" ON watchlists
+ FOR DELETE USING (user_id IS NOT NULL);
+
+-- Note: This approach relies on the backend to properly set user_id
+-- The backend authentication middleware ensures only authenticated users can access the API
\ No newline at end of file
diff --git a/backend/init-db.js b/backend/init-db.js
new file mode 100644
index 0000000..10bc244
--- /dev/null
+++ b/backend/init-db.js
@@ -0,0 +1,33 @@
+require('dotenv').config();
+const supabase = require('./src/config/supabase');
+
+async function initializeDatabase() {
+ try {
+ console.log('Testing connection to Supabase...');
+
+ // Test connection by fetching version
+ const { data, error } = await supabase.from('stocks').select('*').limit(1);
+
+ if (error) {
+ console.error('Error connecting to Supabase:', error);
+ console.log('Please make sure:');
+ console.log('1. Your Supabase URL and API key are correct in .env');
+ console.log('2. You have created the "stocks" and "watchlists" tables in Supabase');
+ console.log('3. You have proper permissions set up');
+ process.exit(1);
+ }
+
+ console.log('Successfully connected to Supabase!');
+ console.log('Tables should be created in the Supabase dashboard.');
+ console.log('Make sure you have the following tables:');
+ console.log('1. stocks - with columns: id, name, ticker, shares, buy_price, current_price, target_price, is_in_watchlist, last_updated');
+ console.log('2. watchlists - with columns: id, name, ticker, target_price, current_price, last_updated, created_at, updated_at');
+
+ } catch (error) {
+ console.error('Error initializing database:', error);
+ process.exit(1);
+ }
+}
+
+// Run the initialization
+initializeDatabase();
\ No newline at end of file
diff --git a/backend/src/config/supabase.js b/backend/src/config/supabase.js
new file mode 100644
index 0000000..8440ba7
--- /dev/null
+++ b/backend/src/config/supabase.js
@@ -0,0 +1,16 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config();
+
+const SUPABASE_URL = process.env.SUPABASE_URL;
+const SUPABASE_KEY = process.env.SUPABASE_KEY;
+
+if (!SUPABASE_URL || !SUPABASE_KEY) {
+ console.warn('SUPABASE_URL or SUPABASE_KEY environment variables are not set. Some features will be limited.');
+}
+
+const supabase = createClient(
+ SUPABASE_URL || 'https://example.supabase.co',
+ SUPABASE_KEY || 'demo-key'
+);
+
+module.exports = supabase;
\ No newline at end of file
diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js
new file mode 100644
index 0000000..ea169e9
--- /dev/null
+++ b/backend/src/middleware/auth.js
@@ -0,0 +1,55 @@
+const { createClient } = require('@supabase/supabase-js');
+
+// Debug environment variables
+console.log('π Auth Middleware Environment Check:', {
+ hasSupabaseUrl: !!process.env.SUPABASE_URL,
+ hasSupabaseKey: !!process.env.SUPABASE_KEY,
+ supabaseUrl: process.env.SUPABASE_URL ? 'Set' : 'Missing',
+ supabaseKey: process.env.SUPABASE_KEY ? 'Set' : 'Missing'
+});
+
+// Create Supabase client for auth verification
+const supabase = createClient(
+ process.env.SUPABASE_URL,
+ process.env.SUPABASE_KEY
+);
+
+const authenticateUser = async (req, res, next) => {
+ try {
+ console.log('π Auth Middleware Debug:', {
+ url: req.url,
+ method: req.method,
+ hasAuthHeader: !!req.headers.authorization,
+ authHeaderPreview: req.headers.authorization ? `${req.headers.authorization.substring(0, 30)}...` : 'No header'
+ });
+
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ console.log('β No valid auth header found');
+ return res.status(401).json({ error: 'No token provided' });
+ }
+
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
+ console.log('π Token extracted, length:', token.length);
+
+ // Verify the JWT token with Supabase
+ const { data: { user }, error } = await supabase.auth.getUser(token);
+
+ if (error || !user) {
+ console.error('β Token verification failed:', error);
+ return res.status(401).json({ error: 'Invalid token' });
+ }
+
+ console.log('β
User authenticated:', user.id);
+
+ // Add user to request object
+ req.user = user;
+ next();
+ } catch (error) {
+ console.error('β Auth middleware error:', error);
+ res.status(500).json({ error: 'Authentication failed' });
+ }
+};
+
+module.exports = { authenticateUser };
\ No newline at end of file
diff --git a/backend/supabase-schema.sql b/backend/supabase-schema.sql
new file mode 100644
index 0000000..af04695
--- /dev/null
+++ b/backend/supabase-schema.sql
@@ -0,0 +1,32 @@
+-- Create stocks table
+CREATE TABLE IF NOT EXISTS stocks (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL,
+ ticker TEXT NOT NULL,
+ shares FLOAT NOT NULL DEFAULT 0,
+ buy_price FLOAT NOT NULL DEFAULT 0,
+ current_price FLOAT NOT NULL DEFAULT 0,
+ target_price FLOAT NOT NULL DEFAULT 0,
+ is_in_watchlist BOOLEAN NOT NULL DEFAULT false,
+ last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Create watchlists table
+CREATE TABLE IF NOT EXISTS watchlists (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL,
+ ticker TEXT NOT NULL,
+ target_price DECIMAL(10, 2) NOT NULL,
+ current_price DECIMAL(10, 2),
+ last_updated TIMESTAMP WITH TIME ZONE,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Create indexes for better performance
+CREATE INDEX IF NOT EXISTS idx_stocks_ticker ON stocks(ticker);
+CREATE INDEX IF NOT EXISTS idx_watchlists_ticker ON watchlists(ticker);
+
+-- Note: Run this script in the Supabase SQL Editor
\ No newline at end of file
From 4e63d41e285d1ab39168b55ef397faf4518f686d Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:53:14 +0530
Subject: [PATCH 02/13] feat: add Finnhub API integration and market calendar
endpoints
---
backend/src/routes/stocks.js | 526 ++++++++++++++++++++--
backend/src/routes/watchlist.js | 206 ++++++---
backend/src/services/stockPriceService.js | 44 +-
3 files changed, 672 insertions(+), 104 deletions(-)
diff --git a/backend/src/routes/stocks.js b/backend/src/routes/stocks.js
index b69fb37..499f00c 100644
--- a/backend/src/routes/stocks.js
+++ b/backend/src/routes/stocks.js
@@ -2,9 +2,9 @@ const express = require('express');
const router = express.Router();
const Stock = require('../models/Stock');
const stockPriceService = require('../services/stockPriceService');
-const { Op } = require('sequelize');
-const { finnhubClient } = require('../config/finnhub');
const axios = require('axios');
+const supabase = require('../config/supabase');
+const { authenticateUser } = require('../middleware/auth');
// Test Finnhub connection
router.get('/test-finnhub/:ticker', async (req, res) => {
@@ -31,19 +31,122 @@ router.get('/test-finnhub/:ticker', async (req, res) => {
}
});
-// Get all stocks
-router.get('/', async (req, res) => {
+// Test Finnhub search
+router.get('/test-search/:query', async (req, res) => {
try {
- console.log('Fetching stocks...');
- const stocks = await Stock.findAll({
- where: {
- shares: {
- [Op.gt]: 0
- }
- },
- order: [['name', 'ASC']]
+ const { query } = req.params;
+ console.log('Testing Finnhub search for query:', query);
+
+ const response = await axios.get('https://finnhub.io/api/v1/search', {
+ params: {
+ q: query,
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Finnhub search test response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Finnhub search test error:', error.response?.data || error.message);
+ res.status(500).json({
+ error: 'Finnhub search test failed',
+ details: error.response?.data || error.message,
+ apiKey: process.env.FINNHUB_API_KEY ? 'Present' : 'Missing'
+ });
+ }
+});
+
+// Search stocks using Finnhub API
+router.get('/search', async (req, res) => {
+ try {
+ const { q } = req.query;
+
+ if (!q || q.length < 2) {
+ return res.json({ result: [] });
+ }
+
+ console.log('Searching stocks for query:', q);
+ console.log('Using API key:', process.env.FINNHUB_API_KEY ? 'Present' : 'Missing');
+
+ const url = `https://finnhub.io/api/v1/search?q=${encodeURIComponent(q)}&token=${process.env.FINNHUB_API_KEY}`;
+ console.log('Making request to:', url.replace(process.env.FINNHUB_API_KEY, '***'));
+
+ const response = await axios.get(url);
+
+ console.log('Finnhub search response status:', response.status);
+ console.log('Finnhub search response data:', JSON.stringify(response.data, null, 2));
+
+ if (response.data && response.data.result) {
+ console.log('Raw results count:', response.data.result.length);
+
+ // First try to filter for common stock exchanges
+ let filteredResults = response.data.result
+ .filter(stock =>
+ stock.symbol &&
+ stock.description &&
+ (stock.primaryExchange === 'NASDAQ' ||
+ stock.primaryExchange === 'NYSE' ||
+ stock.primaryExchange === 'BATS' ||
+ stock.primaryExchange === 'ARCA')
+ );
+
+ // If no results, try less restrictive filtering
+ if (filteredResults.length === 0) {
+ console.log('No results with strict filtering, trying less restrictive...');
+ filteredResults = response.data.result
+ .filter(stock =>
+ stock.symbol &&
+ stock.description &&
+ stock.type === 'Common Stock'
+ );
+ }
+
+ // If still no results, return all results with symbols
+ if (filteredResults.length === 0) {
+ console.log('No results with type filtering, returning all with symbols...');
+ filteredResults = response.data.result
+ .filter(stock => stock.symbol && stock.description);
+ }
+
+ // Limit to 10 results
+ filteredResults = filteredResults.slice(0, 10);
+
+ console.log(`Found ${filteredResults.length} filtered stocks for query: ${q}`);
+ console.log('Filtered results:', JSON.stringify(filteredResults, null, 2));
+
+ res.json({ result: filteredResults });
+ } else {
+ console.log('No results found in response');
+ res.json({ result: [] });
+ }
+ } catch (error) {
+ console.error('Error searching stocks:', error.message);
+ console.error('Error response:', error.response?.data);
+ console.error('Error status:', error.response?.status);
+ res.status(500).json({
+ error: 'Failed to search stocks',
+ details: error.response?.data || error.message
});
- console.log(`Found ${stocks.length} stocks`);
+ }
+});
+
+// Get all stocks (portfolio)
+router.get('/', authenticateUser, async (req, res) => {
+ try {
+ console.log('Fetching portfolio stocks for user:', req.user.id);
+
+ // Use direct Supabase query to be more explicit about filtering
+ const { data: stocks, error } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('user_id', req.user.id)
+ .gt('shares', 0)
+ .eq('is_in_watchlist', false) // Only get stocks that are NOT in watchlist
+ .order('name', { ascending: true });
+
+ if (error) throw error;
+
+ console.log(`Found ${stocks.length} portfolio stocks for user ${req.user.id}`);
res.json(stocks);
} catch (error) {
console.error('Error fetching stocks:', error);
@@ -93,10 +196,10 @@ router.get('/:ticker/quote', async (req, res) => {
});
// Add new stock
-router.post('/', async (req, res) => {
+router.post('/', authenticateUser, async (req, res) => {
try {
const { name, ticker, shares, buy_price, target_price } = req.body;
- console.log('Received request to add stock:', req.body);
+ console.log('Received request to add stock for user:', req.user.id, req.body);
// Validate required fields
if (!name || !ticker || !shares || !buy_price) {
@@ -129,17 +232,28 @@ router.post('/', async (req, res) => {
throw new Error('Unable to fetch current price for ticker');
}
- console.log('Creating stock in database');
- const stock = await Stock.create({
+ console.log('Creating stock in database for user:', req.user.id);
+ console.log('User object:', {
+ id: req.user.id,
+ email: req.user.email,
+ aud: req.user.aud
+ });
+
+ const stockData = {
name: name.trim(),
ticker: ticker.toUpperCase().trim(),
shares: parsedShares,
buy_price: parsedBuyPrice,
current_price: quote.c,
target_price: parsedTargetPrice,
- is_in_watchlist: false,
+ is_in_watchlist: false, // Explicitly set to false for portfolio stocks
+ user_id: req.user.id, // Add user_id
last_updated: new Date()
- });
+ };
+
+ console.log('Stock data to insert:', stockData);
+
+ const stock = await Stock.create(stockData);
console.log('Stock added successfully:', stock.id);
res.status(201).json(stock);
@@ -150,17 +264,22 @@ router.post('/', async (req, res) => {
});
// Update stock
-router.put('/:id', async (req, res) => {
+router.put('/:id', authenticateUser, async (req, res) => {
try {
const { id } = req.params;
const { name, ticker, shares, buy_price } = req.body;
- console.log('Updating stock:', id, { name, ticker, shares, buy_price });
+ console.log('Updating stock for user:', req.user.id, id, { name, ticker, shares, buy_price });
const stock = await Stock.findByPk(id);
if (!stock) {
return res.status(404).json({ error: 'Stock not found' });
}
+ // Check if the stock belongs to the user
+ if (stock.user_id !== req.user.id) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
// Validate numeric values if provided
let parsedShares = shares !== undefined ? parseFloat(shares) : stock.shares;
let parsedBuyPrice = buy_price !== undefined ? parseFloat(buy_price) : stock.buy_price;
@@ -201,16 +320,21 @@ router.put('/:id', async (req, res) => {
});
// Delete stock
-router.delete('/:id', async (req, res) => {
+router.delete('/:id', authenticateUser, async (req, res) => {
try {
const { id } = req.params;
- console.log('Deleting stock:', id);
+ console.log('Deleting stock for user:', req.user.id, id);
const stock = await Stock.findByPk(id);
if (!stock) {
return res.status(404).json({ error: 'Stock not found' });
}
+ // Check if the stock belongs to the user
+ if (stock.user_id !== req.user.id) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
await stock.destroy();
console.log('Stock deleted:', id);
res.json({ message: 'Stock deleted successfully' });
@@ -221,16 +345,19 @@ router.delete('/:id', async (req, res) => {
});
// Get portfolio summary
-router.get('/summary', async (req, res) => {
+router.get('/summary', authenticateUser, async (req, res) => {
try {
- console.log('Getting portfolio summary...');
- const stocks = await Stock.findAll({
- where: {
- shares: {
- [Op.gt]: 0
- }
- }
- });
+ console.log('Getting portfolio summary for user:', req.user.id);
+
+ // Use direct Supabase query to be more explicit about filtering
+ const { data: stocks, error } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('user_id', req.user.id)
+ .gt('shares', 0)
+ .eq('is_in_watchlist', false); // Only get stocks that are NOT in watchlist
+
+ if (error) throw error;
const summary = {
totalValue: 0,
@@ -255,11 +382,91 @@ router.get('/summary', async (req, res) => {
console.log('Portfolio summary:', summary);
res.json(summary);
} catch (error) {
- console.error('Error getting summary:', error);
+ console.error('Error getting portfolio summary:', error);
res.status(500).json({ error: 'Failed to get portfolio summary' });
}
});
+// Diagnostic endpoint to check for data inconsistencies
+router.get('/diagnostic/check-overlap', async (req, res) => {
+ try {
+ console.log('Running diagnostic check for portfolio/watchlist overlap...');
+
+ // Get all stocks
+ const { data: allStocks, error } = await supabase
+ .from('stocks')
+ .select('*');
+
+ if (error) throw error;
+
+ const portfolioStocks = allStocks.filter(stock => stock.shares > 0);
+ const watchlistStocks = allStocks.filter(stock => stock.is_in_watchlist === true);
+ const overlapStocks = allStocks.filter(stock => stock.shares > 0 && stock.is_in_watchlist === true);
+
+ const diagnostic = {
+ totalStocks: allStocks.length,
+ portfolioStocks: portfolioStocks.length,
+ watchlistStocks: watchlistStocks.length,
+ overlapStocks: overlapStocks.length,
+ overlapDetails: overlapStocks.map(stock => ({
+ id: stock.id,
+ ticker: stock.ticker,
+ name: stock.name,
+ shares: stock.shares,
+ is_in_watchlist: stock.is_in_watchlist
+ }))
+ };
+
+ console.log('Diagnostic results:', diagnostic);
+ res.json(diagnostic);
+ } catch (error) {
+ console.error('Error running diagnostic:', error);
+ res.status(500).json({ error: 'Failed to run diagnostic' });
+ }
+});
+
+// Cleanup endpoint to fix overlapping stocks
+router.post('/cleanup/fix-overlap', async (req, res) => {
+ try {
+ console.log('Running cleanup to fix portfolio/watchlist overlap...');
+
+ // Get all stocks
+ const { data: allStocks, error } = await supabase
+ .from('stocks')
+ .select('*');
+
+ if (error) throw error;
+
+ // Find stocks that have both shares > 0 and is_in_watchlist = true
+ const overlapStocks = allStocks.filter(stock => stock.shares > 0 && stock.is_in_watchlist === true);
+
+ console.log(`Found ${overlapStocks.length} stocks with overlap`);
+
+ // Fix each overlapping stock by setting is_in_watchlist to false
+ // (since they have shares > 0, they should be portfolio stocks)
+ for (const stock of overlapStocks) {
+ const { error: updateError } = await supabase
+ .from('stocks')
+ .update({ is_in_watchlist: false })
+ .eq('id', stock.id);
+
+ if (updateError) {
+ console.error(`Error fixing stock ${stock.ticker}:`, updateError);
+ } else {
+ console.log(`Fixed stock ${stock.ticker} - removed from watchlist`);
+ }
+ }
+
+ res.json({
+ message: `Fixed ${overlapStocks.length} overlapping stocks`,
+ fixedStocks: overlapStocks.map(stock => stock.ticker)
+ });
+ } catch (error) {
+ console.error('Error running cleanup:', error);
+ res.status(500).json({ error: 'Failed to run cleanup' });
+ }
+});
+
// Get stock price history
router.get('/history/:ticker', async (req, res) => {
try {
@@ -331,7 +538,18 @@ router.get('/:ticker/historical', async (req, res) => {
from = to - 24 * 60 * 60;
}
- const data = await finnhubClient.stockCandles(ticker, resolution, from, to);
+ // Use direct API call instead of finnhubClient
+ const response = await axios.get('https://finnhub.io/api/v1/stock/candle', {
+ params: {
+ symbol: ticker,
+ resolution: resolution,
+ from: from,
+ to: to,
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ const data = response.data;
if (data.s !== 'ok') {
throw new Error('Failed to fetch historical data');
@@ -349,4 +567,244 @@ router.get('/:ticker/historical', async (req, res) => {
}
});
+// Get stock profile information
+router.get('/:ticker/profile', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ console.log('Fetching stock profile for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/stock/profile2', {
+ params: {
+ symbol: ticker,
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Profile response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Error fetching stock profile:', error);
+ res.status(500).json({ error: 'Failed to fetch stock profile' });
+ }
+});
+
+// Get stock metrics (P/E, P/B, etc.)
+router.get('/:ticker/metrics', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ console.log('Fetching stock metrics for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/stock/metric', {
+ params: {
+ symbol: ticker,
+ metric: 'all',
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Metrics response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Error fetching stock metrics:', error);
+ res.status(500).json({ error: 'Failed to fetch stock metrics' });
+ }
+});
+
+// Get company news
+router.get('/:ticker/news', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ const { from, to } = req.query;
+
+ console.log('Fetching company news for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/company-news', {
+ params: {
+ symbol: ticker,
+ from: from || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 30 days ago
+ to: to || new Date().toISOString().split('T')[0], // today
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('News response count:', response.data?.length || 0);
+ res.json(response.data || []);
+ } catch (error) {
+ console.error('Error fetching company news:', error);
+ res.status(500).json({ error: 'Failed to fetch company news' });
+ }
+});
+
+// Get analyst recommendations
+router.get('/:ticker/recommendations', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ console.log('Fetching analyst recommendations for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/stock/recommendation', {
+ params: {
+ symbol: ticker,
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Recommendations response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Error fetching analyst recommendations:', error);
+ res.status(500).json({ error: 'Failed to fetch analyst recommendations' });
+ }
+});
+
+// Get upcoming earnings calendar (MUST come before /:ticker/earnings)
+router.get('/calendar/earnings', async (req, res) => {
+ try {
+ const { from, to } = req.query;
+ console.log('π
Fetching earnings calendar...');
+
+ const fromDate = from || new Date().toISOString().split('T')[0];
+ const toDate = to || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
+
+ console.log('π
Date range:', { from: fromDate, to: toDate });
+
+ const response = await axios.get('https://finnhub.io/api/v1/calendar/earnings', {
+ params: {
+ from: fromDate,
+ to: toDate,
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('π
Earnings calendar response:', {
+ status: response.status,
+ dataLength: response.data?.earningsCalendar?.length || 0,
+ hasData: !!response.data?.earningsCalendar
+ });
+
+ // Return the real data from Finnhub API
+ res.json(response.data);
+ } catch (error) {
+ console.error('β Error fetching earnings calendar:', error.response?.data || error.message);
+ res.status(500).json({ error: 'Failed to fetch earnings calendar' });
+ }
+});
+
+// Get earnings calendar for specific ticker
+router.get('/:ticker/earnings', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ console.log('Fetching earnings calendar for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/calendar/earnings', {
+ params: {
+ symbol: ticker,
+ from: new Date().toISOString().split('T')[0],
+ to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 1 year from now
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Earnings response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Error fetching earnings calendar:', error);
+ res.status(500).json({ error: 'Failed to fetch earnings calendar' });
+ }
+});
+
+// Get stock peers (competitors)
+router.get('/:ticker/peers', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ console.log('Fetching stock peers for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/stock/peers', {
+ params: {
+ symbol: ticker,
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Peers response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Error fetching stock peers:', error);
+ res.status(500).json({ error: 'Failed to fetch stock peers' });
+ }
+});
+
+// Get dividend history
+router.get('/:ticker/dividends', async (req, res) => {
+ try {
+ const { ticker } = req.params;
+ const { from, to } = req.query;
+
+ console.log('Fetching dividend history for:', ticker);
+
+ const response = await axios.get('https://finnhub.io/api/v1/stock/dividend', {
+ params: {
+ symbol: ticker,
+ from: from || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 1 year ago
+ to: to || new Date().toISOString().split('T')[0], // today
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('Dividends response:', response.data);
+ res.json(response.data);
+ } catch (error) {
+ console.error('Error fetching dividend history:', error);
+ res.status(500).json({ error: 'Failed to fetch dividend history' });
+ }
+});
+
+// Get market status
+router.get('/market/status', async (req, res) => {
+ try {
+ console.log('ποΈ Fetching market status...');
+
+ const response = await axios.get('https://finnhub.io/api/v1/stock/market-status', {
+ params: {
+ exchange: 'US',
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('ποΈ Market status response:', {
+ status: response.status,
+ data: response.data,
+ hasData: !!response.data
+ });
+
+ res.json(response.data);
+ } catch (error) {
+ console.error('β Error fetching market status:', error.response?.data || error.message);
+ res.status(500).json({ error: 'Failed to fetch market status' });
+ }
+});
+
+
+
+// Get IPO calendar
+router.get('/calendar/ipo', async (req, res) => {
+ try {
+ const { from, to } = req.query;
+ console.log('Fetching IPO calendar');
+
+ const response = await axios.get('https://finnhub.io/api/v1/calendar/ipo', {
+ params: {
+ from: from || new Date().toISOString().split('T')[0],
+ to: to || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 90 days from now
+ token: process.env.FINNHUB_API_KEY
+ }
+ });
+
+ console.log('IPO calendar response count:', response.data?.length || 0);
+ res.json(response.data || []);
+ } catch (error) {
+ console.error('Error fetching IPO calendar:', error);
+ res.status(500).json({ error: 'Failed to fetch IPO calendar' });
+ }
+});
+
module.exports = router;
\ No newline at end of file
diff --git a/backend/src/routes/watchlist.js b/backend/src/routes/watchlist.js
index 81e0293..8f5918c 100644
--- a/backend/src/routes/watchlist.js
+++ b/backend/src/routes/watchlist.js
@@ -2,16 +2,22 @@ const express = require('express');
const router = express.Router();
const Stock = require('../models/Stock');
const stockPriceService = require('../services/stockPriceService');
-const { Op } = require('sequelize');
+const supabase = require('../config/supabase');
+const { authenticateUser } = require('../middleware/auth');
// Get all watchlist stocks
-router.get('/', async (req, res) => {
+router.get('/', authenticateUser, async (req, res) => {
try {
- console.log('Fetching watchlist stocks');
- const stocks = await Stock.findAll({
- where: { is_in_watchlist: true },
- order: [['name', 'ASC']]
- });
+ console.log('Fetching watchlist stocks for user:', req.user.id);
+ const { data: stocks, error } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('user_id', req.user.id)
+ .eq('is_in_watchlist', true)
+ .order('name', { ascending: true });
+
+ if (error) throw error;
+
console.log('Found watchlist stocks:', stocks.length);
res.json(stocks);
} catch (error) {
@@ -21,10 +27,10 @@ router.get('/', async (req, res) => {
});
// Add stock to watchlist
-router.post('/', async (req, res) => {
+router.post('/', authenticateUser, async (req, res) => {
try {
const { name, ticker, target_price } = req.body;
- console.log('Adding stock to watchlist:', { name, ticker, target_price });
+ console.log('Adding stock to watchlist for user:', req.user.id, { name, ticker, target_price });
// Validate numeric values
const parsedTargetPrice = parseFloat(target_price);
@@ -32,25 +38,71 @@ router.post('/', async (req, res) => {
throw new Error('Invalid target price');
}
- // Get current price from Finnhub
- const quote = await stockPriceService.getStockQuote(ticker);
- if (!quote || !quote.c) {
- throw new Error('Unable to fetch current price for ticker');
+ // Validate ticker format (basic validation)
+ const tickerRegex = /^[A-Z]{1,5}$/;
+ if (!tickerRegex.test(ticker.toUpperCase().trim())) {
+ throw new Error('Invalid ticker format. Please use a valid US stock ticker (1-5 letters, e.g., AAPL, GOOG, MSFT)');
+ }
+
+ // Check if stock already exists in portfolio for this user
+ const { data: existingStock, error: checkError } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('user_id', req.user.id)
+ .eq('ticker', ticker.toUpperCase().trim())
+ .single();
+
+ if (checkError && checkError.code !== 'PGRST116') { // PGRST116 is "not found"
+ throw checkError;
}
- const stock = await Stock.create({
- name: name.trim(),
- ticker: ticker.toUpperCase().trim(),
- shares: 0,
- buy_price: 0,
- current_price: quote.c,
- target_price: parsedTargetPrice,
- is_in_watchlist: true,
- last_updated: new Date()
- });
-
- console.log('Stock added to watchlist:', stock.id);
- res.status(201).json(stock);
+ if (existingStock) {
+ // If stock exists in portfolio, just add it to watchlist
+ const { data: updatedStock, error: updateError } = await supabase
+ .from('stocks')
+ .update({
+ is_in_watchlist: true,
+ target_price: parsedTargetPrice,
+ last_updated: new Date().toISOString()
+ })
+ .eq('id', existingStock.id)
+ .select()
+ .single();
+
+ if (updateError) throw updateError;
+
+ console.log('Stock added to watchlist (existing):', updatedStock.id);
+ res.status(200).json(updatedStock);
+ } else {
+ // Get current price from Finnhub
+ const quote = await stockPriceService.getStockQuote(ticker);
+ if (!quote || !quote.c) {
+ console.error(`Failed to fetch price for ${ticker}:`, quote);
+ throw new Error(`Unable to fetch current price for ${ticker}. This ticker may not be available in your current plan or may not exist. Please try a different ticker (e.g., GOOG instead of GOOG.MX).`);
+ }
+
+ // Create new stock record for watchlist only
+ const { data: stock, error } = await supabase
+ .from('stocks')
+ .insert([{
+ name: name.trim(),
+ ticker: ticker.toUpperCase().trim(),
+ shares: 0, // Explicitly set to 0 to indicate it's not in portfolio
+ buy_price: 0, // Explicitly set to 0 to indicate it's not in portfolio
+ current_price: quote.c,
+ target_price: parsedTargetPrice,
+ is_in_watchlist: true, // Explicitly set to true
+ user_id: req.user.id, // Add user_id
+ last_updated: new Date().toISOString()
+ }])
+ .select()
+ .single();
+
+ if (error) throw error;
+
+ console.log('Stock added to watchlist (new):', stock.id);
+ res.status(201).json(stock);
+ }
} catch (error) {
console.error('Error adding stock to watchlist:', error);
res.status(400).json({ error: error.message || 'Failed to add stock to watchlist' });
@@ -58,25 +110,33 @@ router.post('/', async (req, res) => {
});
// Sync portfolio stocks to watchlist
-router.post('/sync-portfolio', async (req, res) => {
+router.post('/sync-portfolio', authenticateUser, async (req, res) => {
try {
- console.log('Syncing portfolio stocks to watchlist');
- const portfolioStocks = await Stock.findAll({
- where: { shares: { [Op.gt]: 0 } }
- });
+ console.log('Syncing portfolio stocks to watchlist for user:', req.user.id);
+ const { data: portfolioStocks, error } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('user_id', req.user.id)
+ .gt('shares', 0);
+
+ if (error) throw error;
console.log('Found portfolio stocks:', portfolioStocks.length);
for (const stock of portfolioStocks) {
// If no target price is set, use current price + 10%
- if (!stock.target_price) {
- await stock.update({
+ const targetPrice = !stock.target_price ? stock.current_price * 1.1 : stock.target_price;
+
+ const { error: updateError } = await supabase
+ .from('stocks')
+ .update({
is_in_watchlist: true,
- target_price: stock.current_price * 1.1
- });
- } else {
- await stock.update({ is_in_watchlist: true });
- }
+ target_price: targetPrice,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', stock.id);
+
+ if (updateError) throw updateError;
}
console.log('Portfolio stocks synced to watchlist');
@@ -88,21 +148,38 @@ router.post('/sync-portfolio', async (req, res) => {
});
// Delete stock from watchlist
-router.delete('/:id', async (req, res) => {
+router.delete('/:id', authenticateUser, async (req, res) => {
try {
- console.log('Deleting stock from watchlist:', req.params.id);
- const stock = await Stock.findByPk(req.params.id);
+ console.log('Deleting stock from watchlist for user:', req.user.id, req.params.id);
- if (!stock) {
+ // First check if stock exists and belongs to user
+ const { data: stock, error: fetchError } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('id', req.params.id)
+ .eq('user_id', req.user.id)
+ .single();
+
+ if (fetchError || !stock) {
return res.status(404).json({ error: 'Stock not found' });
}
if (stock.shares > 0) {
// If stock is in portfolio, just remove from watchlist
- await stock.update({ is_in_watchlist: false });
+ const { error: updateError } = await supabase
+ .from('stocks')
+ .update({ is_in_watchlist: false })
+ .eq('id', stock.id);
+
+ if (updateError) throw updateError;
} else {
// If stock is not in portfolio, delete it completely
- await stock.destroy();
+ const { error: deleteError } = await supabase
+ .from('stocks')
+ .delete()
+ .eq('id', stock.id);
+
+ if (deleteError) throw deleteError;
}
console.log('Stock removed from watchlist');
@@ -114,11 +191,11 @@ router.delete('/:id', async (req, res) => {
});
// Update stock target price
-router.put('/:id', async (req, res) => {
+router.put('/:id', authenticateUser, async (req, res) => {
try {
const { id } = req.params;
const { target_price } = req.body;
- console.log('Updating stock target price:', { id, target_price });
+ console.log('Updating stock target price for user:', req.user.id, { id, target_price });
// Validate numeric values
const parsedTargetPrice = parseFloat(target_price);
@@ -126,18 +203,31 @@ router.put('/:id', async (req, res) => {
throw new Error('Invalid target price');
}
- const stock = await Stock.findByPk(id);
- if (!stock) {
+ // First check if stock exists
+ const { data: stock, error: fetchError } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('id', id)
+ .single();
+
+ if (fetchError || !stock) {
return res.status(404).json({ error: 'Stock not found' });
}
- await stock.update({
- target_price: parsedTargetPrice,
- last_updated: new Date()
- });
+ const { data: updatedStock, error: updateError } = await supabase
+ .from('stocks')
+ .update({
+ target_price: parsedTargetPrice,
+ last_updated: new Date().toISOString()
+ })
+ .eq('id', id)
+ .select()
+ .single();
+
+ if (updateError) throw updateError;
- console.log('Stock target price updated:', stock.id);
- res.json(stock);
+ console.log('Stock target price updated:', id);
+ res.json(updatedStock);
} catch (error) {
console.error('Error updating stock target price:', error);
res.status(400).json({ error: error.message || 'Failed to update stock target price' });
@@ -148,9 +238,13 @@ router.put('/:id', async (req, res) => {
router.get('/:id/history', async (req, res) => {
try {
const { id } = req.params;
- const stock = await Stock.findByPk(id);
+ const { data: stock, error: fetchError } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('id', id)
+ .single();
- if (!stock) {
+ if (fetchError || !stock) {
return res.status(404).json({ error: 'Stock not found' });
}
diff --git a/backend/src/services/stockPriceService.js b/backend/src/services/stockPriceService.js
index 32861b7..61e4987 100644
--- a/backend/src/services/stockPriceService.js
+++ b/backend/src/services/stockPriceService.js
@@ -1,6 +1,6 @@
const axios = require('axios');
const Stock = require('../models/Stock');
-const { Op } = require('sequelize');
+const supabase = require('../config/supabase');
const FINNHUB_API_KEY = process.env.FINNHUB_API_KEY;
const FINNHUB_BASE_URL = 'https://finnhub.io/api/v1';
@@ -51,14 +51,21 @@ const getHistoricalData = async (ticker) => {
const updateStockPrices = async () => {
try {
console.log('Starting stock price update...');
- const stocks = await Stock.findAll({
- where: {
- [Op.or]: [
- { shares: { [Op.gt]: 0 } },
- { is_in_watchlist: true }
- ]
- }
- });
+ // Query stocks with shares > 0 or in watchlist
+ const { data: stocks, error } = await supabase
+ .from('stocks')
+ .select('*')
+ .or('shares.gt.0,is_in_watchlist.eq.true');
+
+ if (error) {
+ console.error('Error fetching stocks for update:', error);
+ return;
+ }
+
+ if (!stocks || stocks.length === 0) {
+ console.log('No stocks found in database. Skipping price updates.');
+ return;
+ }
console.log(`Found ${stocks.length} stocks to update`);
@@ -66,11 +73,20 @@ const updateStockPrices = async () => {
try {
const quote = await getStockQuote(stock.ticker);
if (quote && quote.c > 0) {
- await stock.update({
- current_price: quote.c,
- last_updated: new Date()
- });
- console.log(`Updated price for ${stock.ticker}: $${quote.c}`);
+ // Update stock price directly with Supabase
+ const { error: updateError } = await supabase
+ .from('stocks')
+ .update({
+ current_price: quote.c,
+ last_updated: new Date().toISOString()
+ })
+ .eq('id', stock.id);
+
+ if (updateError) {
+ console.error(`Error updating ${stock.ticker}:`, updateError);
+ } else {
+ console.log(`Updated price for ${stock.ticker}: $${quote.c}`);
+ }
} else {
console.log(`Failed to get valid price for ${stock.ticker}`);
}
From a7fdf174308b0641fc4740e8b43ac674e65c2aa2 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:53:40 +0530
Subject: [PATCH 03/13] feat: implement Supabase authentication system with
protected routes
---
src/components/auth/ProtectedRoute.jsx | 23 +++++
src/context/AuthContext.jsx | 121 +++++++++++++++++++++++++
src/services/api.js | 67 ++++++++++++++
3 files changed, 211 insertions(+)
create mode 100644 src/components/auth/ProtectedRoute.jsx
create mode 100644 src/context/AuthContext.jsx
create mode 100644 src/services/api.js
diff --git a/src/components/auth/ProtectedRoute.jsx b/src/components/auth/ProtectedRoute.jsx
new file mode 100644
index 0000000..ededcc2
--- /dev/null
+++ b/src/components/auth/ProtectedRoute.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+
+const ProtectedRoute = ({ children }) => {
+ const { user, loading } = useAuth();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ return children;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx
new file mode 100644
index 0000000..be2fda2
--- /dev/null
+++ b/src/context/AuthContext.jsx
@@ -0,0 +1,121 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { supabase } from '../config/supabase';
+
+const AuthContext = createContext({});
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ // Get initial session
+ const getSession = async () => {
+ const { data: { session } } = await supabase.auth.getSession();
+ setUser(session?.user ?? null);
+ setLoading(false);
+ };
+
+ getSession();
+
+ // Listen for auth changes
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
+ async (event, session) => {
+ setUser(session?.user ?? null);
+ setLoading(false);
+ }
+ );
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ const signUp = async (email, password, name) => {
+ try {
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ data: {
+ full_name: name,
+ },
+ },
+ });
+
+ if (error) throw error;
+ return { data, error: null };
+ } catch (error) {
+ return { data: null, error };
+ }
+ };
+
+ const signIn = async (email, password) => {
+ try {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error) throw error;
+ return { data, error: null };
+ } catch (error) {
+ return { data: null, error };
+ }
+ };
+
+ const signOut = async () => {
+ try {
+ const { error } = await supabase.auth.signOut();
+ if (error) throw error;
+ return { error: null };
+ } catch (error) {
+ return { error };
+ }
+ };
+
+ const resetPassword = async (email) => {
+ try {
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: `${window.location.origin}/reset-password`,
+ });
+ if (error) throw error;
+ return { error: null };
+ } catch (error) {
+ return { error };
+ }
+ };
+
+ const updateProfile = async (updates) => {
+ try {
+ const { data, error } = await supabase.auth.updateUser({
+ data: updates,
+ });
+ if (error) throw error;
+ return { data, error: null };
+ } catch (error) {
+ return { data: null, error };
+ }
+ };
+
+ const value = {
+ user,
+ loading,
+ signUp,
+ signIn,
+ signOut,
+ resetPassword,
+ updateProfile,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/services/api.js b/src/services/api.js
new file mode 100644
index 0000000..9ff4b6f
--- /dev/null
+++ b/src/services/api.js
@@ -0,0 +1,67 @@
+import axios from 'axios';
+import { supabase } from '../config/supabase';
+
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://portfolio-tracker-backend-y7ne.onrender.com/api';
+
+// Create axios instance with base URL
+const api = axios.create({
+ baseURL: API_BASE_URL,
+ timeout: 10000
+});
+
+// Add request interceptor to include auth token
+api.interceptors.request.use(
+ async (config) => {
+ try {
+ // Get the current session
+ const { data: { session } } = await supabase.auth.getSession();
+
+ console.log('π API Request Debug:', {
+ url: config.url,
+ method: config.method,
+ hasSession: !!session,
+ hasToken: !!session?.access_token,
+ tokenPreview: session?.access_token ? `${session.access_token.substring(0, 20)}...` : 'No token'
+ });
+
+ if (session?.access_token) {
+ config.headers.Authorization = `Bearer ${session.access_token}`;
+ console.log('β
Auth token added to request');
+ } else {
+ console.log('β No auth token available');
+ }
+ } catch (error) {
+ console.error('β Error getting auth token:', error);
+ }
+
+ return config;
+ },
+ (error) => {
+ console.error('β Request interceptor error:', error);
+ return Promise.reject(error);
+ }
+);
+
+// Add response interceptor to handle auth errors
+api.interceptors.response.use(
+ (response) => {
+ console.log('β
API Response success:', response.config.url);
+ return response;
+ },
+ async (error) => {
+ console.error('β API Response error:', {
+ url: error.config?.url,
+ status: error.response?.status,
+ message: error.response?.data?.error || error.message
+ });
+
+ if (error.response?.status === 401) {
+ // Token expired or invalid, redirect to login
+ console.log('π Authentication failed, redirecting to login...');
+ window.location.href = '/';
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api;
\ No newline at end of file
From 772652b114d343ab211e81e8e09d5a5cdbdeedc1 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:54:05 +0530
Subject: [PATCH 04/13] feat: add Homepage, Market Calendar, and News pages
with enhanced UI
---
src/pages/Homepage.jsx | 1687 ++++++++++++++++++++++++++++++++++
src/pages/MarketCalendar.jsx | 490 ++++++++++
src/pages/News.jsx | 588 ++++++++++++
3 files changed, 2765 insertions(+)
create mode 100644 src/pages/Homepage.jsx
create mode 100644 src/pages/MarketCalendar.jsx
create mode 100644 src/pages/News.jsx
diff --git a/src/pages/Homepage.jsx b/src/pages/Homepage.jsx
new file mode 100644
index 0000000..d247021
--- /dev/null
+++ b/src/pages/Homepage.jsx
@@ -0,0 +1,1687 @@
+import React, { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { HiOutlineEye, HiOutlineEyeOff, HiOutlineMail, HiOutlineLockClosed, HiOutlineUser, HiOutlineChartBar, HiOutlineTrendingUp, HiOutlineBell, HiOutlineShieldCheck } from 'react-icons/hi';
+
+const Homepage = () => {
+ const [isLogin, setIsLogin] = useState(true);
+ const [showPassword, setShowPassword] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [success, setSuccess] = useState('');
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ name: ''
+ });
+ const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [selectedBlog, setSelectedBlog] = useState(null);
+ const [isBlogModalOpen, setIsBlogModalOpen] = useState(false);
+ const [isStatusTooltipOpen, setIsStatusTooltipOpen] = useState(false);
+ const [systemStatus, setSystemStatus] = useState({
+ overall: 'operational',
+ api: 'operational',
+ database: 'operational',
+ lastChecked: new Date().toLocaleTimeString()
+ });
+ const navigate = useNavigate();
+ const { user, signIn, signUp } = useAuth();
+
+ // Initialize theme and redirect if user is already logged in
+ useEffect(() => {
+ // Initialize theme
+ const savedTheme = localStorage.getItem('theme') || 'light';
+ setTheme(savedTheme);
+ document.documentElement.classList.toggle('dark', savedTheme === 'dark');
+
+ // Redirect to dashboard if user is already logged in
+ if (user) {
+ navigate('/dashboard');
+ }
+ }, [user, navigate]);
+
+ // Close status tooltip when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (isStatusTooltipOpen && !event.target.closest('.status-tooltip-container')) {
+ setIsStatusTooltipOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isStatusTooltipOpen]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError('');
+ setSuccess('');
+
+ try {
+ if (isLogin) {
+ // Sign in
+ const { error } = await signIn(formData.email, formData.password);
+ if (error) {
+ setError(error.message);
+ } else {
+ setSuccess('Successfully signed in!');
+ navigate('/dashboard');
+ }
+ } else {
+ // Sign up
+ const { error } = await signUp(formData.email, formData.password, formData.name);
+ if (error) {
+ setError(error.message);
+ } else {
+ setSuccess('Account created successfully! Please check your email to verify your account.');
+ }
+ }
+ } catch (err) {
+ setError('An unexpected error occurred. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleInputChange = (e) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value
+ });
+ };
+
+ const toggleTheme = () => {
+ const newTheme = theme === 'light' ? 'dark' : 'light';
+ setTheme(newTheme);
+ localStorage.setItem('theme', newTheme);
+ document.documentElement.classList.toggle('dark', newTheme === 'dark');
+
+ // Dispatch custom event for theme change
+ window.dispatchEvent(new CustomEvent('themeChange', {
+ detail: { theme: newTheme }
+ }));
+ };
+
+ // Blog data
+ const blogPosts = [
+ {
+ id: 1,
+ title: "Building a Diversified Portfolio in 2024",
+ category: "Investment Strategy",
+ date: "Jan 15, 2024",
+ author: "Sarah Johnson",
+ readTime: "8 min read",
+ image: "from-blue-400 to-purple-500",
+ content: `
+
+
+ In today's rapidly evolving financial landscape, building a diversified portfolio has never been more crucial. The traditional 60/40 stock-bond split that worked for decades is being challenged by new market dynamics, technological disruption, and changing economic policies.
+
+
+
Understanding Modern Diversification
+
+ Modern portfolio diversification goes beyond simply mixing stocks and bonds. It involves understanding correlation, risk factors, and the impact of global events on different asset classes. A truly diversified portfolio should include:
+
+
+
+ Domestic and international equities
+ Government and corporate bonds
+ Real estate investment trusts (REITs)
+ Commodities and precious metals
+ Alternative investments (private equity, hedge funds)
+ Cryptocurrencies (with proper risk management)
+
+
+
The Role of Technology in Portfolio Management
+
+ Technology has revolutionized how we approach portfolio diversification. AI-powered tools can now analyze thousands of data points to identify optimal asset allocations, while robo-advisors provide sophisticated portfolio management at a fraction of traditional costs.
+
+
+
+ However, it's important to remember that technology should enhance, not replace, fundamental investment principles. The core tenets of diversificationβspreading risk across uncorrelated assetsβremain unchanged.
+
+
+
Risk Management in 2024
+
+ With increased market volatility and geopolitical uncertainty, risk management has become paramount. Consider implementing:
+
+
+
+ Regular portfolio rebalancing (quarterly or annually)
+ Stress testing your portfolio against various scenarios
+ Maintaining adequate cash reserves for opportunities
+ Using stop-loss orders for individual positions
+
+
+
+ Remember, diversification is not about eliminating riskβit's about managing it intelligently while positioning your portfolio for long-term growth.
+
+
+ `
+ },
+ {
+ id: 2,
+ title: "Understanding Market Volatility",
+ category: "Market Analysis",
+ date: "Jan 12, 2024",
+ author: "Michael Chen",
+ readTime: "6 min read",
+ image: "from-green-400 to-blue-500",
+ content: `
+
+
+ Market volatility is often misunderstood and feared by investors. However, understanding volatility is crucial for making informed investment decisions and maintaining a long-term perspective during turbulent times.
+
+
+
What is Volatility?
+
+ Volatility measures the rate at which the price of a security or market index increases or decreases for a given set of returns. It's essentially a measure of risk and uncertainty in the market. Higher volatility means larger price swings, while lower volatility indicates more stable prices.
+
+
+
Types of Volatility
+
+ There are several types of volatility that investors should understand:
+
+
+
+ Historical Volatility: Based on past price movements
+ Implied Volatility: Market's expectation of future volatility
+ Realized Volatility: Actual volatility experienced over a period
+
+
+
Causes of Market Volatility
+
+ Market volatility can be caused by various factors:
+
+
+
+ Economic data releases and policy changes
+ Geopolitical events and conflicts
+ Corporate earnings announcements
+ Changes in interest rates and monetary policy
+ Technological disruptions and innovations
+ Natural disasters and global health crises
+
+
+
Strategies for Volatile Markets
+
+ During volatile periods, consider these strategies:
+
+
+
+ Maintain a long-term perspective
+ Dollar-cost averaging into positions
+ Focus on quality companies with strong fundamentals
+ Consider defensive sectors and dividend-paying stocks
+ Keep adequate cash reserves for opportunities
+
+
+
+ Remember, volatility creates opportunities for disciplined investors. The key is to stay calm, stick to your investment plan, and use volatility to your advantage rather than letting it drive emotional decisions.
+
+
+ `
+ },
+ {
+ id: 3,
+ title: "AI in Investment Management",
+ category: "Technology",
+ date: "Jan 10, 2024",
+ author: "Dr. Emily Rodriguez",
+ readTime: "10 min read",
+ image: "from-purple-400 to-pink-500",
+ content: `
+
+
+ Artificial Intelligence is revolutionizing the investment management industry, from algorithmic trading to portfolio optimization and risk assessment. As we move further into 2024, AI technologies are becoming increasingly sophisticated and accessible to both institutional and individual investors.
+
+
+
The Evolution of AI in Finance
+
+ The integration of AI in investment management has evolved significantly over the past decade. What started with simple rule-based algorithms has now progressed to sophisticated machine learning models that can process vast amounts of data and identify patterns invisible to human analysts.
+
+
+
Key Applications of AI in Investment Management
+
+
1. Algorithmic Trading
+
+ AI-powered trading algorithms can execute trades at optimal times based on market conditions, news sentiment, and technical indicators. These systems can process information in milliseconds and make decisions faster than any human trader.
+
+
+
2. Portfolio Optimization
+
+ Machine learning algorithms can analyze historical data, market conditions, and individual preferences to create optimized portfolio allocations that maximize returns while minimizing risk.
+
+
+
3. Risk Assessment
+
+ AI models can identify potential risks by analyzing market data, news sentiment, and economic indicators. This helps investors make more informed decisions about their investments.
+
+
+
4. Sentiment Analysis
+
+ Natural language processing (NLP) algorithms can analyze news articles, social media posts, and earnings calls to gauge market sentiment and predict potential market movements.
+
+
+
Benefits and Challenges
+
+
Benefits:
+
+ Improved decision-making through data-driven insights
+ Reduced emotional bias in trading decisions
+ 24/7 market monitoring and analysis
+ Cost-effective portfolio management
+ Access to sophisticated strategies previously available only to institutions
+
+
+
Challenges:
+
+ Over-reliance on historical data
+ Potential for algorithmic bias
+ Regulatory and ethical considerations
+ Need for human oversight and interpretation
+
+
+
The Future of AI in Investment Management
+
+ As AI technology continues to advance, we can expect to see even more sophisticated applications in investment management. The key will be finding the right balance between automation and human judgment, ensuring that AI enhances rather than replaces human decision-making.
+
+
+
+ For individual investors, the democratization of AI-powered investment tools means access to strategies and insights that were once the exclusive domain of large financial institutions.
+
+
+ `
+ }
+ ];
+
+ const openBlogModal = (blog) => {
+ setSelectedBlog(blog);
+ setIsBlogModalOpen(true);
+ document.body.style.overflow = 'hidden';
+ };
+
+ const closeBlogModal = () => {
+ setIsBlogModalOpen(false);
+ setSelectedBlog(null);
+ document.body.style.overflow = 'unset';
+ };
+
+ const toggleStatusTooltip = () => {
+ setIsStatusTooltipOpen(!isStatusTooltipOpen);
+ };
+
+ const getStatusColor = (status) => {
+ switch (status) {
+ case 'operational':
+ return 'text-green-500';
+ case 'degraded':
+ return 'text-yellow-500';
+ case 'down':
+ return 'text-red-500';
+ default:
+ return 'text-gray-500';
+ }
+ };
+
+ const getStatusIcon = (status) => {
+ switch (status) {
+ case 'operational':
+ return (
+
+
+
+ );
+ case 'degraded':
+ return (
+
+
+
+ );
+ case 'down':
+ return (
+
+
+
+ );
+ default:
+ return (
+
+
+
+ );
+ }
+ };
+
+ return (
+
+ {/* Clean Modern Navigation */}
+
+
+
+ {/* Logo Section */}
+
+
+
+
+
+ PortfolioTracker
+
+
+
+ {/* Navigation Links - Center */}
+
+
+ {/* Right Side Actions */}
+
+ {/* Cool Theme Toggle */}
+
+ {/* Background gradient effect */}
+
+
+ {/* Icon container */}
+
+ {/* Sun icon for dark mode */}
+
+
+
+
+ {/* Moon icon for light mode */}
+
+
+
+
+
+ {/* Ripple effect */}
+
+
+
+ {/* Login Button */}
+
setIsLogin(true)}
+ className={`hidden md:block px-4 py-2 rounded-lg font-medium transition-all duration-200 border hover:scale-105 ${
+ theme === 'dark'
+ ? 'bg-transparent border-gray-700 text-gray-300 hover:bg-gray-800 hover:border-gray-600'
+ : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400'
+ }`}
+ >
+ Log in
+
+
+ {/* Sign Up Button */}
+
setIsLogin(false)}
+ className={`hidden md:block px-4 py-2 rounded-lg font-medium transition-all duration-200 hover:scale-105 ${
+ theme === 'dark'
+ ? 'bg-white text-gray-900 hover:bg-gray-100'
+ : 'bg-gray-900 text-white hover:bg-gray-800'
+ }`}
+ >
+ Sign up
+
+
+ {/* Mobile Menu Button */}
+
setIsMobileMenuOpen(!isMobileMenuOpen)}
+ className={`md:hidden p-2 rounded-lg transition-colors duration-200 ${
+ theme === 'dark'
+ ? 'hover:bg-gray-800 text-gray-300'
+ : 'hover:bg-gray-100 text-gray-600'
+ }`}
+ >
+
+
+
+
+
+
+
+
+ {/* Mobile Menu */}
+
+
+
+ Home
+
+
+ Features
+
+
+ Resources
+
+
+ Pricing
+
+
+ Blog
+
+
+ setIsLogin(true)}
+ className={`w-full px-4 py-2 rounded-lg font-medium border transition-all duration-200 ${
+ theme === 'dark'
+ ? 'bg-transparent border-gray-700 text-gray-300 hover:bg-gray-800'
+ : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
+ }`}
+ >
+ Log in
+
+ setIsLogin(false)}
+ className={`w-full px-4 py-2 rounded-lg font-medium transition-all duration-200 ${
+ theme === 'dark'
+ ? 'bg-white text-gray-900 hover:bg-gray-100'
+ : 'bg-gray-900 text-white hover:bg-gray-800'
+ }`}
+ >
+ Sign up
+
+
+
+
+
+
+ {/* Hero Section */}
+
+
+
+ {/* Left Side - Hero Content */}
+
+
+
+ Track Your
+
+ Portfolio
+
+ Like a Pro
+
+
+ Monitor your investments, track performance, and make informed decisions with our comprehensive portfolio tracking platform.
+
+
+
+ {/* Features */}
+
+
+
+
+
+ Real-time Tracking
+
+
+ Live stock prices and updates
+
+
+
+
+
+
+
+
+ Smart Alerts
+
+
+ Price alerts and notifications
+
+
+
+
+
+
+
+
+ Analytics
+
+
+ Detailed performance insights
+
+
+
+
+
+
+
+
+ Secure
+
+
+ Your data is protected
+
+
+
+
+
+
+ {/* Right Side - Auth Form */}
+
+
+
+ {isLogin ? 'Welcome Back' : 'Create Account'}
+
+
+ {isLogin ? 'Sign in to your account' : 'Start tracking your portfolio today'}
+
+
+
+ {/* Toggle Buttons */}
+
+ setIsLogin(true)}
+ className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all duration-200 ${
+ isLogin
+ ? 'bg-blue-600 text-white shadow-sm'
+ : theme === 'dark' ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'
+ }`}
+ >
+ Sign In
+
+ setIsLogin(false)}
+ className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all duration-200 ${
+ !isLogin
+ ? 'bg-blue-600 text-white shadow-sm'
+ : theme === 'dark' ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'
+ }`}
+ >
+ Sign Up
+
+
+
+ {/* Error and Success Messages */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {success && (
+
+ {success}
+
+ )}
+
+
+
+
+ {isLogin ? "Don't have an account? " : "Already have an account? "}
+ setIsLogin(!isLogin)}
+ className={`font-semibold ${theme === 'dark' ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'}`}
+ >
+ {isLogin ? 'Sign up' : 'Sign in'}
+
+
+
+
+
+
+
+ {/* Features Section */}
+
+
+
+
+ Powerful Features
+
+
+ Everything you need to manage your portfolio effectively
+
+
+
+
+ {/* Feature 1 */}
+
+
+
+
+
+ Real-time Tracking
+
+
+ Get live stock prices, real-time updates, and instant notifications for your portfolio.
+
+
+
+ {/* Feature 2 */}
+
+
+
+
+
+ Advanced Analytics
+
+
+ Detailed performance insights, portfolio analysis, and investment recommendations.
+
+
+
+ {/* Feature 3 */}
+
+
+
+
+
+ Smart Alerts
+
+
+ Custom price alerts, news notifications, and portfolio milestone tracking.
+
+
+
+ {/* Feature 4 */}
+
+
+
+
+
+ Secure & Private
+
+
+ Bank-level security, data encryption, and complete privacy protection.
+
+
+
+ {/* Feature 5 */}
+
+
+
+ Portfolio Insights
+
+
+ Comprehensive portfolio analysis, risk assessment, and performance metrics.
+
+
+
+ {/* Feature 6 */}
+
+
+
+ Stock News
+
+
+ Stay updated with the latest stock news, market updates, and financial insights.
+
+
+
+
+
+
+ {/* Resources Section */}
+
+
+
+
+ Resources & Learning
+
+
+ Learn, grow, and make better investment decisions
+
+
+
+
+ {/* Resource 1 */}
+
+
+
+ Investment Guide
+
+
+ Comprehensive guide to building and managing your investment portfolio.
+
+
+ Read More β
+
+
+
+ {/* Resource 2 */}
+
+
+
+ Video Tutorials
+
+
+ Step-by-step video tutorials to help you master portfolio management.
+
+
+ Watch Now β
+
+
+
+ {/* Resource 3 */}
+
+
+
+ FAQ & Support
+
+
+ Find answers to common questions and get expert support when needed.
+
+
+ Get Help β
+
+
+
+
+
+
+ {/* Pricing Section */}
+
+
+
+
+ Simple, Transparent Pricing
+
+
+ Choose the plan that fits your investment needs
+
+
+
+
+ {/* Free Plan */}
+
+
+ Free
+
+
+ $0
+
+ /month
+
+
+
+
+
+
+
+ Up to 10 stocks
+
+
+
+
+
+ Basic analytics
+
+
+
+
+
+ Price alerts
+
+
+
+ Get Started
+
+
+
+ {/* Pro Plan */}
+
+
+ Most Popular
+
+
+ Pro
+
+
+ $9
+
+ /month
+
+
+
+
+
+
+
+ Unlimited stocks
+
+
+
+
+
+ Advanced analytics
+
+
+
+
+
+ Smart alerts
+
+
+
+
+
+ Portfolio insights
+
+
+
+ Start Free Trial
+
+
+
+ {/* Enterprise Plan */}
+
+
+ Enterprise
+
+
+ $29
+
+ /month
+
+
+
+
+
+
+
+ Everything in Pro
+
+
+
+
+
+ Team collaboration
+
+
+
+
+
+ Priority support
+
+
+
+
+
+ Custom integrations
+
+
+
+ Contact Sales
+
+
+
+
+
+
+ {/* Blog Section */}
+
+
+
+
+ Latest Insights
+
+
+ Stay updated with the latest investment trends and strategies
+
+
+
+
+ {blogPosts.map((blog, index) => (
+
openBlogModal(blog)}
+ >
+
+
+
+ {blog.category}
+
+
+ {blog.title}
+
+
+ {blog.category === 'Investment Strategy' ? 'Learn the key principles of portfolio diversification and how to apply them in today\'s market.' :
+ blog.category === 'Market Analysis' ? 'A comprehensive guide to navigating market volatility and protecting your investments.' :
+ 'How artificial intelligence is revolutionizing portfolio management and investment decisions.'}
+
+
+
+ {blog.date}
+
+
+ Read More β
+
+
+
+
+ ))}
+
+
+
+
+ {/* Footer */}
+
+
+
+ {/* Company Info */}
+
+
+
+
+
+
+ PortfolioTracker
+
+
+
+ The ultimate platform for tracking your investments, analyzing performance, and making informed financial decisions.
+
+
+
+
+ {/* Quick Links */}
+
+
+ {/* Support */}
+
+
+
+ {/* Bottom Bar */}
+
+
+
+ Β© 2024 PortfolioTracker. All rights reserved.
+
+
+
+ {theme === 'dark' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ Status
+
+
+ {/* Status Tooltip */}
+ {isStatusTooltipOpen && (
+
+ {/* Arrow */}
+
+
+ {/* Header */}
+
+
+ System Status
+
+
+ {getStatusIcon(systemStatus.overall)}
+
+ {systemStatus.overall}
+
+
+
+
+ {/* Status Items */}
+
+
+
+ API
+
+
+ {getStatusIcon(systemStatus.api)}
+
+ {systemStatus.api}
+
+
+
+
+
+
+ Database
+
+
+ {getStatusIcon(systemStatus.database)}
+
+ {systemStatus.database}
+
+
+
+
+
+ {/* Footer */}
+
+
+
+ Last checked: {systemStatus.lastChecked}
+
+ {
+ setSystemStatus({
+ ...systemStatus,
+ lastChecked: new Date().toLocaleTimeString()
+ });
+ }}
+ className={`text-xs font-medium ${
+ theme === 'dark'
+ ? 'text-blue-400 hover:text-blue-300'
+ : 'text-blue-600 hover:text-blue-500'
+ } transition-colors duration-200`}
+ >
+ Refresh
+
+
+
+
+ )}
+
+
+ Security
+
+
+
+
+
+
+
+ {/* Blog Modal */}
+ {isBlogModalOpen && selectedBlog && (
+
+ {/* Backdrop */}
+
+
+ {/* Modal Content */}
+
+ {/* Header */}
+
+
+
+
+
+ {selectedBlog.category}
+
+
+ {selectedBlog.title}
+
+
+ By {selectedBlog.author}
+ β’
+ {selectedBlog.date}
+ β’
+ {selectedBlog.readTime}
+
+
+
+
+
+ {/* Close Button */}
+
+
+
+
+
+
+ {/* Content */}
+
+
+
+ {/* Author Bio */}
+
+
+
+
+ {selectedBlog.author.split(' ').map(n => n[0]).join('')}
+
+
+
+
+ {selectedBlog.author}
+
+
+ Investment Expert & Financial Analyst
+
+
+
+
+
+ {/* Action Buttons */}
+
+
+ Close Article
+
+
+ Share Article
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default Homepage;
\ No newline at end of file
diff --git a/src/pages/MarketCalendar.jsx b/src/pages/MarketCalendar.jsx
new file mode 100644
index 0000000..c00d15e
--- /dev/null
+++ b/src/pages/MarketCalendar.jsx
@@ -0,0 +1,490 @@
+import React, { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { Card, Title, Text, Badge } from '@tremor/react';
+import {
+ HiCalendar,
+ HiTrendingUp,
+ HiTrendingDown,
+ HiCurrencyDollar,
+ HiClock,
+ HiStar,
+ HiNewspaper
+} from 'react-icons/hi';
+import api from '../services/api';
+import Tabs from '../components/ui/Tabs';
+
+const MarketCalendar = ({ theme = 'light' }) => {
+ const [earnings, setEarnings] = useState([]);
+ const [ipos, setIpos] = useState([]);
+ const [marketStatus, setMarketStatus] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchMarketData();
+ }, []);
+
+ const fetchMarketData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ console.log('π Fetching market data...');
+
+ // Fetch market data in parallel
+ const [earningsRes, iposRes, statusRes] = await Promise.allSettled([
+ api.get('/stocks/calendar/earnings'),
+ api.get('/stocks/calendar/ipo'),
+ api.get('/stocks/market/status')
+ ]);
+
+ console.log('π API Responses:', {
+ earnings: earningsRes.status === 'fulfilled' ? earningsRes.value.data : 'Failed',
+ ipos: iposRes.status === 'fulfilled' ? iposRes.value.data : 'Failed',
+ status: statusRes.status === 'fulfilled' ? statusRes.value.data : 'Failed'
+ });
+
+
+
+ // Handle nested data structure from Finnhub API
+ const earningsData = earningsRes.status === 'fulfilled'
+ ? (earningsRes.value.data?.earningsCalendar || earningsRes.value.data || [])
+ : [];
+
+ const iposData = iposRes.status === 'fulfilled'
+ ? (iposRes.value.data?.ipoCalendar || iposRes.value.data || [])
+ : [];
+
+ const statusData = statusRes.status === 'fulfilled'
+ ? statusRes.value.data
+ : null;
+
+ setEarnings(earningsData);
+ setIpos(iposData);
+ setMarketStatus(statusData);
+
+ console.log('β
Market data loaded:', {
+ earningsCount: earningsData.length,
+ iposCount: iposData.length,
+ hasStatus: !!statusData
+ });
+ } catch (error) {
+ console.error('β Error fetching market data:', error);
+ setError('Failed to fetch market data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getMarketStatusColor = (status) => {
+ if (status?.isOpen) {
+ return 'green';
+ } else {
+ return 'red';
+ }
+ };
+
+ const getMarketStatusText = (status) => {
+ if (status?.isOpen) {
+ return 'Open';
+ } else {
+ return 'Closed';
+ }
+ };
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric'
+ });
+ };
+
+ const formatTime = (timeString) => {
+ if (!timeString) return 'TBD';
+
+ // Handle Finnhub time codes
+ switch (timeString) {
+ case 'bmo':
+ return 'Before Market Open';
+ case 'dmt':
+ return 'During Market Trading';
+ case 'amc':
+ return 'After Market Close';
+ default:
+ // Handle regular time strings
+ try {
+ return new Date(`2000-01-01T${timeString}`).toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } catch {
+ return timeString;
+ }
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ Market Calendar
+
+
+ Track earnings, IPOs, and market events
+
+
+
+ {/* Market Status */}
+ {marketStatus && (
+
+
+
+
+
+
+
+
+
Market Status
+
+
+ {getMarketStatusText(marketStatus)}
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Animated Tabs */}
+
+
+
+
+ Upcoming Earnings
+
+ {earnings.length} events
+
+
+
+ {earnings.length > 0 ? (
+ earnings.slice(0, 20).map((earning, index) => (
+
+
+
+
+
+
+
+
{earning.symbol}
+
{earning.quarter}
+
+
+ {earning.name || earning.company}
+
+
+
+
+ {formatDate(earning.date)}
+
+
+
+ {formatTime(earning.hour)}
+
+
+
+
+
+
Est. EPS
+
+ {earning.estimate ? `$${earning.estimate}` : 'N/A'}
+
+
+
+ ))
+ ) : (
+
+ No upcoming earnings found
+
+ )}
+
+
+ ),
+ },
+ {
+ title: "IPOs",
+ value: "ipos",
+ content: (
+
+
+
+ Upcoming IPOs
+
+ {ipos.length} events
+
+
+
+ {ipos.length > 0 ? (
+ ipos.slice(0, 20).map((ipo, index) => (
+
+
+
+
+
+
+
+
+ {ipo.companyName}
+
+
+
+
+ {formatDate(ipo.date)}
+
+
+
+ ${ipo.priceLow} - ${ipo.priceHigh}
+
+
+
+
+
+
Shares
+
+ {ipo.numberOfShares ? (ipo.numberOfShares / 1e6).toFixed(1) + 'M' : 'N/A'}
+
+
+
+ ))
+ ) : (
+
+ No upcoming IPOs found
+
+ )}
+
+
+ ),
+ },
+ {
+ title: "Market Events",
+ value: "events",
+ content: (
+
+
+ Market Events
+
+
+
+
+
+
+
+
+
Market Open
+
9:30 AM ET
+
+
+
Daily
+
+
+
+
+
+
+
+
+
Market Close
+
4:00 PM ET
+
+
+
Daily
+
+
+
+
+
+
+
+
+
Fed Meeting
+
Next: TBD
+
+
+
Monthly
+
+
+
+ ),
+ },
+ ]}
+ containerClassName="mb-8"
+ activeTabClassName={`${theme === 'dark' ? 'bg-gray-700 border-gray-600' : 'bg-blue-500'} shadow-lg`}
+ tabClassName={`${theme === 'dark' ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'} transition-all duration-200`}
+ />
+
+
+ );
+};
+
+export default MarketCalendar;
\ No newline at end of file
diff --git a/src/pages/News.jsx b/src/pages/News.jsx
new file mode 100644
index 0000000..70a0cd5
--- /dev/null
+++ b/src/pages/News.jsx
@@ -0,0 +1,588 @@
+import React, { useState, useEffect } from 'react';
+import { HiOutlineNewspaper, HiOutlineClock, HiOutlineUser, HiOutlineTrendingUp, HiOutlineTrendingDown, HiOutlineEye, HiOutlineRefresh } from 'react-icons/hi';
+import axios from 'axios';
+
+const News = () => {
+ const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
+ const [selectedCategory, setSelectedCategory] = useState('all');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [lastUpdated, setLastUpdated] = useState(null);
+ const [newsArticles, setNewsArticles] = useState([]);
+ const [error, setError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const [hasMore, setHasMore] = useState(true);
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
+
+ // Initialize theme and listen for changes
+ useEffect(() => {
+ const handleThemeChange = (e) => {
+ const newTheme = e.detail?.theme || e.detail || localStorage.getItem('theme') || 'light';
+ console.log('News: Theme change detected:', newTheme);
+ setTheme(newTheme);
+ document.documentElement.classList.toggle('dark', newTheme === 'dark');
+ };
+
+ // Set initial theme
+ const savedTheme = localStorage.getItem('theme') || 'light';
+ setTheme(savedTheme);
+ document.documentElement.classList.toggle('dark', savedTheme === 'dark');
+
+ // Listen to theme change events
+ window.addEventListener('themeChange', handleThemeChange);
+ window.addEventListener('storage', () => {
+ const storedTheme = localStorage.getItem('theme') || 'light';
+ handleThemeChange({ detail: { theme: storedTheme } });
+ });
+ document.addEventListener('themeChanged', handleThemeChange);
+
+ return () => {
+ window.removeEventListener('themeChange', handleThemeChange);
+ window.removeEventListener('storage', handleThemeChange);
+ document.removeEventListener('themeChanged', handleThemeChange);
+ };
+ }, []);
+
+ // Fallback news data
+ const fallbackNews = [
+ {
+ id: 1,
+ title: "Federal Reserve Signals Potential Rate Cuts in 2024",
+ category: "Market News",
+ date: "Jan 20, 2024",
+ author: "Financial Times",
+ readTime: "5 min read",
+ image: "from-blue-500 to-purple-600",
+ summary: "The Federal Reserve's latest meeting minutes indicate a more dovish stance, with officials discussing potential interest rate reductions in the coming months.",
+ trending: "up",
+ views: "2.4k",
+ url: "#"
+ },
+ {
+ id: 2,
+ title: "Tech Giants Report Strong Q4 Earnings",
+ category: "Earnings",
+ date: "Jan 19, 2024",
+ author: "Reuters",
+ readTime: "4 min read",
+ image: "from-green-500 to-blue-600",
+ summary: "Major technology companies exceeded analyst expectations in their fourth-quarter earnings reports, driven by strong cloud services and AI investments.",
+ trending: "up",
+ views: "1.8k",
+ url: "#"
+ },
+ {
+ id: 3,
+ title: "Oil Prices Volatile Amid Geopolitical Tensions",
+ category: "Commodities",
+ date: "Jan 18, 2024",
+ author: "Bloomberg",
+ readTime: "6 min read",
+ image: "from-yellow-500 to-orange-600",
+ summary: "Crude oil prices experienced significant volatility as tensions in key oil-producing regions intensified, affecting global energy markets.",
+ trending: "down",
+ views: "1.2k",
+ url: "#"
+ },
+ {
+ id: 4,
+ title: "Cryptocurrency Market Shows Signs of Recovery",
+ category: "Crypto",
+ date: "Jan 17, 2024",
+ author: "CoinDesk",
+ readTime: "7 min read",
+ image: "from-purple-500 to-pink-600",
+ summary: "Bitcoin and other major cryptocurrencies have shown strong recovery signals, with institutional adoption continuing to grow.",
+ trending: "up",
+ views: "3.1k",
+ url: "#"
+ },
+ {
+ id: 5,
+ title: "Housing Market Shows Mixed Signals",
+ category: "Real Estate",
+ date: "Jan 16, 2024",
+ author: "Wall Street Journal",
+ readTime: "5 min read",
+ image: "from-indigo-500 to-purple-600",
+ summary: "Recent housing market data reveals a complex picture, with some regions showing strength while others face challenges from high interest rates.",
+ trending: "down",
+ views: "956",
+ url: "#"
+ },
+ {
+ id: 6,
+ title: "ESG Investing Continues to Gain Momentum",
+ category: "ESG",
+ date: "Jan 15, 2024",
+ author: "Financial News",
+ readTime: "6 min read",
+ image: "from-teal-500 to-green-600",
+ summary: "Environmental, Social, and Governance (ESG) investing strategies are attracting record inflows as investors prioritize sustainability and responsible business practices.",
+ trending: "up",
+ views: "1.5k",
+ url: "#"
+ }
+ ];
+
+ // Fetch news from Finnhub API with pagination
+ const fetchNews = async (page = 1, append = false) => {
+ try {
+ if (page === 1) {
+ setIsLoading(true);
+ } else {
+ setIsLoadingMore(true);
+ }
+ setError(null);
+
+ // Get Finnhub API key from environment variables
+ const FINNHUB_API_KEY = import.meta.env.VITE_FINNHUB_API_KEY;
+
+ console.log('π Checking for Finnhub API key...');
+ console.log('API Key exists:', !!FINNHUB_API_KEY);
+
+ if (!FINNHUB_API_KEY) {
+ console.warn('β οΈ Finnhub API key not found. Using fallback data.');
+ const paginatedFallback = paginateData(fallbackNews, page, 12);
+ setNewsArticles(paginatedFallback.articles);
+ setTotalPages(paginatedFallback.totalPages);
+ setHasMore(paginatedFallback.hasMore);
+ setLastUpdated(new Date());
+ setIsLoading(false);
+ setIsLoadingMore(false);
+ return;
+ }
+
+ console.log('π Fetching news from Finnhub API...');
+
+ // Fetch general market news with pagination
+ const response = await axios.get(`https://finnhub.io/api/v1/news?category=general&token=${FINNHUB_API_KEY}&minId=0`);
+
+ console.log('π‘ API Response received:', response.data?.length || 0, 'articles');
+
+ if (response.data && Array.isArray(response.data)) {
+ console.log('β
Transforming API data...');
+ // Transform the data to match our component structure
+ const transformedNews = response.data.map((article, index) => ({
+ id: article.id || index,
+ title: article.headline || 'No title available',
+ category: article.category || 'Market News',
+ date: new Date(article.datetime * 1000).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ }),
+ author: article.source || 'Financial News',
+ readTime: `${Math.max(1, Math.floor(article.headline?.length / 200))} min read`,
+ image: getRandomGradient(),
+ summary: article.summary || 'No summary available',
+ trending: Math.random() > 0.5 ? 'up' : 'down',
+ views: `${Math.floor(Math.random() * 5) + 1}k`,
+ url: article.url || '#'
+ }));
+
+ const paginatedData = paginateData(transformedNews, page, 12);
+
+ if (append) {
+ setNewsArticles(prev => [...prev, ...paginatedData.articles]);
+ } else {
+ setNewsArticles(paginatedData.articles);
+ }
+
+ setTotalPages(paginatedData.totalPages);
+ setHasMore(paginatedData.hasMore);
+ setCurrentPage(page);
+ setLastUpdated(new Date());
+ console.log('π Successfully loaded', paginatedData.articles.length, 'articles from Finnhub API (page', page, ')');
+ } else {
+ throw new Error('Invalid response format from API');
+ }
+ } catch (err) {
+ console.error('β Error fetching news:', err);
+ setError(err.message || 'Failed to fetch news. Please try again later.');
+ // Use fallback data on error
+ console.log('π Using fallback data due to error');
+ const paginatedFallback = paginateData(fallbackNews, page, 12);
+ setNewsArticles(paginatedFallback.articles);
+ setTotalPages(paginatedFallback.totalPages);
+ setHasMore(paginatedFallback.hasMore);
+ } finally {
+ setIsLoading(false);
+ setIsLoadingMore(false);
+ }
+ };
+
+ // Pagination helper function
+ const paginateData = (data, page, itemsPerPage) => {
+ const startIndex = (page - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const articles = data.slice(startIndex, endIndex);
+ const totalPages = Math.ceil(data.length / itemsPerPage);
+ const hasMore = page < totalPages;
+
+ return {
+ articles,
+ totalPages,
+ hasMore,
+ currentPage: page
+ };
+ };
+
+ // Get random gradient for article images
+ const getRandomGradient = () => {
+ const gradients = [
+ 'from-blue-500 to-purple-600',
+ 'from-green-500 to-blue-600',
+ 'from-yellow-500 to-orange-600',
+ 'from-purple-500 to-pink-600',
+ 'from-indigo-500 to-purple-600',
+ 'from-teal-500 to-green-600',
+ 'from-red-500 to-pink-600',
+ 'from-orange-500 to-red-600'
+ ];
+ return gradients[Math.floor(Math.random() * gradients.length)];
+ };
+
+ // Initialize news data
+ useEffect(() => {
+ if (newsArticles.length === 0) {
+ fetchNews();
+ }
+ }, []);
+
+ // Generate categories from news data
+ const generateCategories = () => {
+ const categoryCounts = {};
+ newsArticles.forEach(article => {
+ const category = article.category;
+ categoryCounts[category] = (categoryCounts[category] || 0) + 1;
+ });
+
+ const categories = [
+ { id: 'all', name: 'All News', count: newsArticles.length }
+ ];
+
+ Object.entries(categoryCounts).forEach(([category, count]) => {
+ categories.push({
+ id: category.toLowerCase().replace(/\s+/g, '-'),
+ name: category,
+ count: count
+ });
+ });
+
+ return categories;
+ };
+
+ const categories = generateCategories();
+
+ // Filter articles based on category and search
+ const filteredArticles = newsArticles.filter(article => {
+ const matchesCategory = selectedCategory === 'all' ||
+ article.category.toLowerCase().includes(selectedCategory.replace('-', ' '));
+ const matchesSearch = article.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ article.summary.toLowerCase().includes(searchQuery.toLowerCase());
+ return matchesCategory && matchesSearch;
+ });
+
+ // Get category color styling
+ const getCategoryColor = (category) => {
+ switch (category) {
+ case 'Market News':
+ return 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20';
+ case 'Earnings':
+ return 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900/20';
+ case 'Commodities':
+ return 'text-yellow-600 bg-yellow-100 dark:text-yellow-400 dark:bg-yellow-900/20';
+ case 'Crypto':
+ return 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-900/20';
+ case 'Real Estate':
+ return 'text-indigo-600 bg-indigo-100 dark:text-indigo-400 dark:bg-indigo-900/20';
+ case 'ESG':
+ return 'text-teal-600 bg-teal-100 dark:text-teal-400 dark:bg-teal-900/20';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:text-gray-400 dark:bg-gray-900/20';
+ }
+ };
+
+ // Handle refresh
+ const handleRefresh = () => {
+ setCurrentPage(1);
+ fetchNews(1, false);
+ };
+
+ // Handle load more
+ const handleLoadMore = () => {
+ if (hasMore && !isLoadingMore) {
+ fetchNews(currentPage + 1, true);
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
+ Financial News
+
+
+ Stay updated with the latest market insights
+
+
+
+
+ {/* Search Bar */}
+
+
+
setSearchQuery(e.target.value)}
+ className={`w-full pl-10 pr-4 py-2 bg-transparent border-0 focus:outline-none focus:ring-0 ${
+ theme === 'dark' ? 'text-white placeholder-gray-400' : 'text-gray-900 placeholder-gray-500'
+ }`}
+ />
+
+
+
+
+ {/* Refresh Button and Last Updated */}
+
+ {lastUpdated && (
+
+ Last updated: {lastUpdated.toLocaleTimeString()}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Loading State */}
+ {isLoading && (
+
+
+
+ Loading latest news...
+
+
+ Fetching the most recent financial news
+
+
+ )}
+
+ {/* Error State */}
+ {error && !isLoading && (
+
+
+
+ API Error - Using Fallback Data
+
+
+ {error}
+
+
+ Try Again
+
+
+ )}
+
+ {/* Categories */}
+ {!isLoading && (
+
+
+ {categories.map((category) => (
+ setSelectedCategory(category.id)}
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
+ selectedCategory === category.id
+ ? 'bg-blue-600 text-white shadow-lg'
+ : theme === 'dark'
+ ? 'bg-gray-800 text-gray-300 hover:bg-gray-700'
+ : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
+ }`}
+ >
+ {category.name} ({category.count})
+
+ ))}
+
+
+ )}
+
+ {/* News Grid */}
+ {!isLoading && (
+
+ {filteredArticles.map((article, index) => (
+
window.open(article.url, '_blank')}
+ >
+ {/* Image */}
+
+
+
+
+ {article.category}
+
+
+
+ {article.trending === 'up' ? (
+
+ ) : (
+
+ )}
+
+ {article.trending === 'up' ? 'Trending' : 'Declining'}
+
+
+
+
+ {/* Content */}
+
+
+ {article.title}
+
+
+ {article.summary}
+
+
+ {/* Meta */}
+
+
+
+
+ {article.author}
+
+
+
+ {article.readTime}
+
+
+
+
+ {article.views}
+
+
+
+ {/* Date */}
+
+ {article.date}
+
+
+
+ ))}
+
+ )}
+
+ {/* No Results */}
+ {!isLoading && filteredArticles.length === 0 && (
+
+
+
+ No news found
+
+
+ Try adjusting your search or category filters
+
+
+ )}
+
+ {/* Pagination */}
+ {!isLoading && filteredArticles.length > 0 && (
+
+ {/* Load More Button */}
+ {hasMore && (
+
+ {isLoadingMore ? (
+
+ ) : (
+ 'Load More Articles'
+ )}
+
+ )}
+
+ {/* Page Info */}
+
+ Showing {newsArticles.length} articles
+ {totalPages > 1 && ` β’ Page ${currentPage} of ${totalPages}`}
+
+
+ {/* End of Results */}
+ {!hasMore && (
+
+ You've reached the end of all available articles
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default News;
\ No newline at end of file
From 4a2e96b770f56225bd1441b71bf0f0beaeb070e5 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:54:17 +0530
Subject: [PATCH 05/13] feat: add custom UI components including animated tabs
and utility functions
---
src/components/ui/Tabs.jsx | 115 +++++++++++++++++++++++++++++++++++++
src/config/supabase.js | 13 +++++
src/lib/utils.js | 6 ++
3 files changed, 134 insertions(+)
create mode 100644 src/components/ui/Tabs.jsx
create mode 100644 src/config/supabase.js
create mode 100644 src/lib/utils.js
diff --git a/src/components/ui/Tabs.jsx b/src/components/ui/Tabs.jsx
new file mode 100644
index 0000000..6444ad7
--- /dev/null
+++ b/src/components/ui/Tabs.jsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { motion } from "motion/react";
+import { cn } from "../../lib/utils";
+
+const Tabs = ({
+ tabs: propTabs,
+ containerClassName,
+ activeTabClassName,
+ tabClassName,
+ contentClassName,
+ theme = 'light',
+}) => {
+ const currentTheme = theme;
+ const [active, setActive] = useState(propTabs[0]);
+ const [tabs, setTabs] = useState(propTabs);
+
+ // Force re-render when theme changes
+ useEffect(() => {
+ setTabs([...propTabs]);
+ }, [theme, propTabs]);
+
+ const moveSelectedTabToTop = (idx) => {
+ const newTabs = [...propTabs];
+ const selectedTab = newTabs.splice(idx, 1);
+ newTabs.unshift(selectedTab[0]);
+ setTabs(newTabs);
+ setActive(newTabs[0]);
+ };
+
+ const [hovering, setHovering] = useState(false);
+
+ return (
+ <>
+
+ {propTabs.map((tab, idx) => (
+ {
+ moveSelectedTabToTop(idx);
+ }}
+ onMouseEnter={() => setHovering(true)}
+ onMouseLeave={() => setHovering(false)}
+ className={cn("relative px-4 py-2 rounded-full", tabClassName)}
+ style={{
+ transformStyle: "preserve-3d",
+ }}
+ >
+ {active.value === tab.value && (
+
+ )}
+
+
+ {tab.title}
+
+
+ ))}
+
+
+ >
+ );
+};
+
+const FadeInDiv = ({
+ className,
+ tabs,
+ hovering,
+}) => {
+ const isActive = (tab) => {
+ return tab.value === tabs[0].value;
+ };
+ return (
+
+ {tabs.map((tab, idx) => (
+
+ {tab.content}
+
+ ))}
+
+ );
+};
+
+export default Tabs;
\ No newline at end of file
diff --git a/src/config/supabase.js b/src/config/supabase.js
new file mode 100644
index 0000000..4109244
--- /dev/null
+++ b/src/config/supabase.js
@@ -0,0 +1,13 @@
+import { createClient } from '@supabase/supabase-js';
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+if (!supabaseUrl || !supabaseAnonKey) {
+ console.warn('VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY environment variables are not set. Authentication will not work.');
+}
+
+export const supabase = createClient(
+ supabaseUrl || 'https://example.supabase.co',
+ supabaseAnonKey || 'demo-key'
+);
\ No newline at end of file
diff --git a/src/lib/utils.js b/src/lib/utils.js
new file mode 100644
index 0000000..42f72ba
--- /dev/null
+++ b/src/lib/utils.js
@@ -0,0 +1,6 @@
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs) {
+ return twMerge(clsx(inputs));
+}
\ No newline at end of file
From 8aad16a54edb96ca9f6f816307ab13447e100a6d Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:54:44 +0530
Subject: [PATCH 06/13] feat: update existing pages with improved theme support
and white backgrounds
---
src/pages/Dashboard.jsx | 72 ++-
src/pages/Portfolio.jsx | 1198 ++++++++++++++++++++++---------------
src/pages/StockDetail.jsx | 377 +++++++++---
src/pages/Watchlist.jsx | 765 ++++++++++++++---------
4 files changed, 1539 insertions(+), 873 deletions(-)
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 497e3ba..7fd2a46 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -2,17 +2,11 @@ import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { AreaChart, Card, Title, Badge, TabGroup, TabList, Tab } from '@tremor/react';
import { HiTrendingUp, HiTrendingDown, HiCurrencyDollar, HiChartPie, HiClock } from 'react-icons/hi';
-import axios from 'axios';
import PortfolioAnalytics from '../components/dashboard/PortfolioAnalytics';
import StockDetail from '../components/dashboard/StockDetail';
+import api from '../services/api';
-const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://portfolio-tracker-backend-y7ne.onrender.com/api';
-// Create axios instance with base URL
-const api = axios.create({
- baseURL: API_BASE_URL,
- timeout: 10000
-});
const containerVariants = {
hidden: { opacity: 0 },
@@ -128,7 +122,7 @@ export default function Dashboard({ theme: propTheme }) {
const fetchStocks = async () => {
try {
- console.log('Fetching stocks from:', API_BASE_URL);
+ console.log('Fetching stocks from API');
const response = await api.get('/stocks');
console.log('Stocks fetched successfully:', response.data);
setStocks(response.data);
@@ -481,19 +475,67 @@ export default function Dashboard({ theme: propTheme }) {
{
const periods = ['1D', '1W', '1M', '3M', '1Y', 'ALL'];
setSelectedPeriod(periods[index]);
}}
>
- 1D
- 1W
- 1M
- 3M
- 1Y
- ALL
+ 1D
+ 1W
+ 1M
+ 3M
+ 1Y
+ ALL
{
const [stocks, setStocks] = useState([]);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [editingStock, setEditingStock] = useState(null);
- const [newStock, setNewStock] = useState({
- name: '',
- ticker: '',
- shares: 1,
- buy_price: ''
- });
+ const [newStock, setNewStock] = useState({ name: '', ticker: '', shares: 1, buy_price: '' });
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
+ const [buttonHover, setButtonHover] = useState(false);
+
+ // Autocomplete states
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+ const [isSearching, setIsSearching] = useState(false);
+ const [showDropdown, setShowDropdown] = useState(false);
useEffect(() => {
- // Listen for theme changes
- const handleThemeChange = () => {
- setTheme(localStorage.getItem('theme') || 'light');
+ console.log('Portfolio component mounted');
+ fetchStocks();
+
+ const handleThemeChange = (e) => {
+ const newTheme = e.detail || localStorage.getItem('theme') || 'light';
+ console.log('Theme changed to:', newTheme);
+ setTheme(newTheme);
};
- window.addEventListener('storage', handleThemeChange);
+ // Listen for theme changes
+ window.addEventListener('themeChange', handleThemeChange);
document.addEventListener('themeChanged', handleThemeChange);
return () => {
- window.removeEventListener('storage', handleThemeChange);
+ window.removeEventListener('themeChange', handleThemeChange);
document.removeEventListener('themeChanged', handleThemeChange);
};
}, []);
- useEffect(() => {
- fetchStocks();
- }, []);
-
const fetchStocks = async () => {
try {
- console.log('Fetching stocks from:', API_BASE_URL);
+ console.log('Fetching stocks from API');
const response = await api.get('/stocks');
- console.log('Stocks fetched successfully:', response.data);
+ console.log('Stocks response:', response.data);
setStocks(response.data);
- setError(null);
- } catch (err) {
- console.error('Error fetching stocks:', err);
- setError('Failed to fetch stocks. Please try again.');
- } finally {
+ setLoading(false);
+ } catch (error) {
+ console.error('Error fetching stocks:', error);
+ setError('Failed to fetch stocks');
setLoading(false);
}
};
@@ -86,58 +82,119 @@ export default function Portfolio() {
const handleAddStock = async (e) => {
e.preventDefault();
try {
+ console.log('Adding stock:', newStock);
const response = await api.post('/stocks', {
name: newStock.name,
ticker: newStock.ticker,
shares: parseFloat(newStock.shares),
buy_price: parseFloat(newStock.buy_price)
});
-
- setStocks([response.data, ...stocks]);
- setShowAddModal(false);
- setNewStock({ name: '', ticker: '', shares: 1, buy_price: '' });
- setError(null);
- } catch (err) {
- console.error('Error adding stock:', err);
- setError(err.response?.data?.error || 'Failed to add stock. Please try again.');
+ handleModalClose();
+ await fetchStocks();
+ } catch (error) {
+ console.error('Error adding stock:', error);
+ setError(error.response?.data?.error || 'Failed to add stock');
}
};
const handleEditStock = async (e) => {
e.preventDefault();
try {
- const response = await api.put(`/stocks/${editingStock.id}`, {
+ console.log('Updating stock:', editingStock);
+ await api.put(`/stocks/${editingStock.id}`, {
name: editingStock.name,
ticker: editingStock.ticker,
shares: parseFloat(editingStock.shares),
buy_price: parseFloat(editingStock.buy_price)
});
-
- setStocks(stocks.map(stock =>
- stock.id === editingStock.id ? response.data : stock
- ));
setShowEditModal(false);
setEditingStock(null);
- setError(null);
- } catch (err) {
- console.error('Error updating stock:', err);
- setError(err.response?.data?.error || 'Failed to update stock. Please try again.');
+ await fetchStocks();
+ } catch (error) {
+ console.error('Error updating stock:', error);
+ setError(error.response?.data?.error || 'Failed to update stock');
}
};
const handleDeleteStock = async (id) => {
if (window.confirm('Are you sure you want to delete this stock?')) {
try {
+ console.log('Deleting stock:', id);
await api.delete(`/stocks/${id}`);
- setStocks(stocks.filter(stock => stock.id !== id));
- setError(null);
- } catch (err) {
- console.error('Error deleting stock:', err);
- setError('Failed to delete stock. Please try again.');
+ await fetchStocks();
+ } catch (error) {
+ console.error('Error deleting stock:', error);
+ setError('Failed to delete stock');
+ }
+ }
+ };
+
+ const handleEditClick = (stock) => {
+ setEditingStock(stock);
+ setShowEditModal(true);
+ };
+
+ // Stock search and autocomplete functions
+ const searchStocks = async (query) => {
+ if (!query || query.length < 2) {
+ setSearchResults([]);
+ setShowDropdown(false);
+ return;
+ }
+
+ setIsSearching(true);
+ try {
+ // Use our backend endpoint to search for stocks
+ const response = await api.get(`/stocks/search?q=${encodeURIComponent(query)}`);
+
+ if (response.data && response.data.result) {
+ setSearchResults(response.data.result);
+ setShowDropdown(true);
+ } else {
+ setSearchResults([]);
+ setShowDropdown(false);
}
+ } catch (error) {
+ console.error('Error searching stocks:', error);
+ setSearchResults([]);
+ setShowDropdown(false);
+ } finally {
+ setIsSearching(false);
}
};
+ const handleSearchChange = (e) => {
+ const query = e.target.value;
+ setSearchQuery(query);
+ setNewStock({ ...newStock, name: '', ticker: '' });
+
+ // Debounce the search
+ const timeoutId = setTimeout(() => {
+ searchStocks(query);
+ }, 300);
+
+ return () => clearTimeout(timeoutId);
+ };
+
+ const selectStock = (stock) => {
+ setNewStock({
+ ...newStock,
+ name: stock.description,
+ ticker: stock.symbol
+ });
+ setSearchQuery(stock.symbol);
+ setShowDropdown(false);
+ setSearchResults([]);
+ };
+
+ const handleModalClose = () => {
+ setShowAddModal(false);
+ setNewStock({ name: '', ticker: '', shares: 1, buy_price: '' });
+ setSearchQuery('');
+ setSearchResults([]);
+ setShowDropdown(false);
+ };
+
const getTotalValue = () => {
return stocks.reduce((total, stock) => total + (stock.current_price * stock.shares), 0);
};
@@ -150,496 +207,677 @@ export default function Portfolio() {
return stocks.length > 0 ? totalGainLoss / stocks.length : 0;
};
+ console.log('Rendering Portfolio component with theme:', theme);
+
return (
-
- {/* Header Section */}
-
-
-
Portfolio
- Manage your stock investments
-
-
setShowAddModal(true)}
- className={`px-6 py-2.5 flex items-center gap-2 rounded-xl font-semibold transition-all duration-200 ${
- theme === 'dark'
- ? 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 text-white shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40'
- : 'bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30'
- } hover:-translate-y-0.5`}
- >
-
- Add Stock
-
-
-
- {/* Quick Stats */}
-
-
-
+
+ {/* Header Section */}
+
+
+
+ Portfolio
+
+
+ Manage your stock investments
+
+
+
setShowAddModal(true)}
+ onMouseEnter={() => setButtonHover(true)}
+ onMouseLeave={() => setButtonHover(false)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ className={`px-6 py-2.5 flex items-center gap-2 rounded-xl font-semibold transition-all duration-300 relative overflow-hidden ${
+ theme === 'dark'
+ ? 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 text-white shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40'
+ : 'bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30'
}`}
- decoration="none"
>
-
-
-
-
+ {buttonHover && (
+
+ )}
+
+
+
+ Add Stock
+
+
+
+ {/* Quick Stats */}
+
+
+
+
-
-
Total Holdings
-
{stocks.length} Stocks
+
+
+
+
+
+ Total Holdings
+
{stocks.length} Stocks
+
-
-
-
+
+
-
-
-
-
-
-
+
+
-
-
Portfolio Value
-
${getTotalValue().toLocaleString()}
+
+
+
+
+
+ Portfolio Value
+
${getTotalValue().toLocaleString()}
+
-
-
-
-
-
-
+
+
+
-
-
= 0 ? 'bg-emerald-500' : 'bg-red-500'
- } opacity-20 rounded-full blur-2xl`}>
-
-
-
= 0
- ? `bg-gradient-to-br from-emerald-500 to-emerald-600 ${
- theme === 'dark' ? 'shadow-emerald-500/20' : 'shadow-emerald-500/30'
- }`
- : `bg-gradient-to-br from-red-500 to-red-600 ${
- theme === 'dark' ? 'shadow-red-500/20' : 'shadow-red-500/30'
- }`
- }`}>
-
-
-
-
Average Return
-
- {getTotalGainLoss() >= 0 ? (
-
- ) : (
-
- )}
-
= 0 ? 'text-emerald-500' : 'text-red-500'
- }`}>
- {getTotalGainLoss().toFixed(2)}%
-
+
+
+
= 0 ? 'bg-emerald-500' : 'bg-red-500'
+ } opacity-20 rounded-full blur-2xl`}>
+
+
+
= 0
+ ? `bg-gradient-to-br from-emerald-500 to-emerald-600 ${
+ theme === 'dark' ? 'shadow-emerald-500/20' : 'shadow-emerald-500/30'
+ }`
+ : `bg-gradient-to-br from-red-500 to-red-600 ${
+ theme === 'dark' ? 'shadow-red-500/20' : 'shadow-red-500/30'
+ }`
+ }`}>
+
-
+
+
Average Return
+
+ {getTotalGainLoss() >= 0 ? (
+
+ ) : (
+
+ )}
+
= 0 ? 'text-emerald-500' : 'text-red-500'
+ }`}>
+ {getTotalGainLoss().toFixed(2)}%
+
+
-
-
-
-
- {/* Error Message */}
- {error && (
-
- {error}
-
- )}
+
+
+
+
- {/* Stocks Table */}
-
-
-
-
-
+ {error}
+
+ )}
+
+ {/* Stocks Table */}
+
+
+
+
+
+ Stock
+ Current Price
+ Holdings
+ Total Value
+ Return
+ Actions
+
+
+
- Stock
- Current Price
- Holdings
- Total Value
- Return
- Actions
-
-
-
- {stocks.map((stock) => {
- const value = stock.current_price * stock.shares;
- const gainLoss = ((stock.current_price - stock.buy_price) / stock.buy_price) * 100;
- const valueChange = (stock.current_price - stock.buy_price) * stock.shares;
-
- return (
-
-
-
-
{stock.name}
-
{stock.ticker}
+ {loading ? (
+
+
+
-
- ${stock.current_price.toLocaleString()}
-
-
-
- {stock.shares.toLocaleString()} shares
- @ ${stock.buy_price.toLocaleString()}
-
-
-
+ ) : stocks.length === 0 ? (
+
+
- ${value.toLocaleString()}
-
-
-
-
= 0
- ? theme === 'dark'
- ? 'bg-emerald-900/30 text-emerald-400'
- : 'bg-emerald-100 text-emerald-800'
- : theme === 'dark'
- ? 'bg-red-900/30 text-red-400'
- : 'bg-red-100 text-red-800'
- }`}>
-
- {gainLoss >= 0 ? (
-
- ) : (
-
- )}
-
- {gainLoss >= 0 ? '+' : ''}{gainLoss.toFixed(2)}%
-
-
= 0
- ? theme === 'dark'
- ? 'text-emerald-400'
- : 'text-emerald-600'
- : theme === 'dark'
- ? 'text-red-400'
- : 'text-red-600'
- }`}>
- {valueChange >= 0 ? '+' : ''}${valueChange.toLocaleString()}
-
-
-
-
-
+
+
+
No stocks in portfolio
{
- setEditingStock({
- ...stock,
- shares: stock.shares,
- });
- setShowEditModal(true);
- }}
- className={`p-2 rounded-lg transition-colors ${
- theme === 'dark'
- ? 'bg-gray-800 hover:bg-gray-700 text-gray-200 hover:text-white'
- : 'bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-900'
- }`}
- >
-
-
-
handleDeleteStock(stock.id)}
- className={`p-2 rounded-lg transition-colors ${
- theme === 'dark'
- ? 'bg-red-900/30 hover:bg-red-900/50 text-red-400 hover:text-red-300'
- : 'bg-red-100 hover:bg-red-200 text-red-600 hover:text-red-700'
- }`}
+ onClick={() => setShowAddModal(true)}
+ className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors"
>
-
+ Add Your First Stock
- );
- })}
-
-
+ ) : (
+ stocks.map((stock) => {
+ const value = stock.current_price * stock.shares;
+ const gainLoss = ((stock.current_price - stock.buy_price) / stock.buy_price) * 100;
+ const valueChange = (stock.current_price - stock.buy_price) * stock.shares;
+
+ return (
+
+
+
+ {stock.name}
+ {stock.ticker}
+
+
+
+ ${stock.current_price.toLocaleString()}
+
+
+
+ {stock.shares.toLocaleString()} shares
+ @ ${stock.buy_price.toLocaleString()}
+
+
+
+ ${value.toLocaleString()}
+
+
+
+
= 0
+ ? theme === 'dark'
+ ? 'bg-emerald-900/30 text-emerald-400'
+ : 'bg-emerald-100 text-emerald-800'
+ : theme === 'dark'
+ ? 'bg-red-900/30 text-red-400'
+ : 'bg-red-100 text-red-800'
+ }`}>
+
+ {gainLoss >= 0 ? (
+
+ ) : (
+
+ )}
+
+ {gainLoss >= 0 ? '+' : ''}{gainLoss.toFixed(2)}%
+
+
= 0
+ ? theme === 'dark'
+ ? 'text-emerald-400'
+ : 'text-emerald-600'
+ : theme === 'dark'
+ ? 'text-red-400'
+ : 'text-red-600'
+ }`}>
+ {valueChange >= 0 ? '+' : ''}${valueChange.toLocaleString()}
+
+
+
+
+
+ handleEditClick(stock)}
+ className={`p-2 rounded-lg transition-colors ${
+ theme === 'dark'
+ ? 'bg-gray-800 hover:bg-gray-700 text-gray-200 hover:text-white'
+ : 'bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-900'
+ }`}
+ >
+
+
+ handleDeleteStock(stock.id)}
+ className={`p-2 rounded-lg transition-colors ${
+ theme === 'dark'
+ ? 'bg-red-900/30 hover:bg-red-900/50 text-red-400 hover:text-red-300'
+ : 'bg-red-100 hover:bg-red-200 text-red-600 hover:text-red-700'
+ }`}
+ >
+
+
+
+
+
+ );
+ })
+ )}
+
+
+
-
+
- {/* Add Stock Modal */}
- {showAddModal && (
-
+ {/* Modals using Portal */}
+ {showAddModal && createPortal(
+
-
-
Add New Stock
+
+
+ Add New Stock
+
setShowAddModal(false)}
- className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
+ onClick={handleModalClose}
+ className={`p-2 rounded-lg transition-colors ${
+ theme === 'dark'
+ ? 'hover:bg-gray-700 text-gray-400 hover:text-white'
+ : 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'
+ }`}
>
-
+
+
+
-
@@ -377,9 +501,7 @@ const Watchlist = () => {
{error && (
-
{
}`}
>
{error}
-
+
)}
{/* Watchlist Table */}
-
+ {/* Blurry background decoration */}
+
+
-
-
-
+
+ Stock
- Current Price
- Target Price
- Distance to Target
- Actions
-
+
{loading ? (
@@ -450,20 +586,24 @@ const Watchlist = () => {
filteredWatchlist.map((stock) => (
-
{stock.name}
+
{stock.name}
{stock.ticker}
-
+
${parseFloat(stock.current_price || 0).toFixed(2)}
-
+
${parseFloat(stock.target_price || 0).toFixed(2)}
@@ -478,10 +618,10 @@ const Watchlist = () => {
handleViewStock(stock)}
- className={`p-2 rounded-lg transition-colors ${
+ className={`p-2 rounded-lg transition-all duration-200 ${
theme === 'dark'
- ? 'hover:bg-blue-500/10 text-blue-400 hover:text-blue-300'
- : 'hover:bg-blue-50 text-blue-500 hover:text-blue-600'
+ ? 'hover:bg-blue-500/20 text-blue-400 hover:text-blue-300 hover:shadow-lg'
+ : 'hover:bg-blue-100 text-blue-500 hover:text-blue-600 hover:shadow-md'
}`}
title="View Details"
>
@@ -489,10 +629,10 @@ const Watchlist = () => {
handleUpdateClick(stock)}
- className={`p-2 rounded-lg transition-colors ${
+ className={`p-2 rounded-lg transition-all duration-200 ${
theme === 'dark'
- ? 'hover:bg-green-500/10 text-green-400 hover:text-green-300'
- : 'hover:bg-green-50 text-green-500 hover:text-green-600'
+ ? 'hover:bg-green-500/20 text-green-400 hover:text-green-300 hover:shadow-lg'
+ : 'hover:bg-green-100 text-green-500 hover:text-green-600 hover:shadow-md'
}`}
title="Update Target"
>
@@ -500,10 +640,10 @@ const Watchlist = () => {
handleDeleteStock(stock.id)}
- className={`p-2 rounded-lg transition-colors ${
+ className={`p-2 rounded-lg transition-all duration-200 ${
theme === 'dark'
- ? 'hover:bg-red-500/10 text-red-400 hover:text-red-300'
- : 'hover:bg-red-50 text-red-500 hover:text-red-600'
+ ? 'hover:bg-red-500/20 text-red-400 hover:text-red-300 hover:shadow-lg'
+ : 'hover:bg-red-100 text-red-500 hover:text-red-600 hover:shadow-md'
}`}
title="Remove from Watchlist"
>
@@ -517,73 +657,255 @@ const Watchlist = () => {
-
+
+
{/* Add Stock Modal */}
{showAddModal && (
-
+
-
+
+
Add Stock to Watchlist
+
+
+
+
+
+
+
+ {/* Stock Search with Autocomplete */}
+
+
+ Search Stock
+
+
+
+ {isSearching && (
+
+ )}
+
+
+ {/* Autocomplete Dropdown */}
+ {showDropdown && searchResults.length > 0 && (
+
+ {searchResults.map((stock, index) => (
+
selectStock(stock)}
+ className={`w-full px-4 py-3 text-left hover:bg-blue-500 hover:text-white transition-colors ${
+ theme === 'dark'
+ ? 'text-gray-200 hover:bg-blue-600'
+ : 'text-gray-900 hover:bg-blue-500'
+ } ${index === 0 ? 'rounded-t-xl' : ''} ${index === searchResults.length - 1 ? 'rounded-b-xl' : ''}`}
+ >
+
+
+
{stock.symbol}
+
+ {stock.description}
+
+
+
+ {stock.primaryExchange}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Selected Stock Display */}
+ {newStock.ticker && newStock.name && (
+
+
+
+
+ {newStock.ticker}
+
+
+ {newStock.name}
+
+
+
+ Selected
+
+
+
+ )}
+
+
+
+ Target Price ($)
+
+ setNewStock({ ...newStock, target_price: e.target.value })}
+ className={`w-full px-3 py-3 text-base rounded-xl border-2 transition-all duration-200 focus:outline-none focus:ring-4 ${
+ theme === 'dark'
+ ? 'bg-gray-800 border-gray-600 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-blue-500/20'
+ : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500 focus:ring-blue-500/20'
+ }`}
+ placeholder="0.00"
+ required
+ />
+
+
+
+
+ Cancel
+
+
+ Add Stock
+
+
+
+
+
+ )}
+
+ {/* Update Stock Modal */}
+ {showUpdateModal && selectedStock && (
+
+
+
+
+ Update Target Price
+
+
{
+ setShowUpdateModal(false);
+ setSelectedStock(null);
+ }}
+ className={`p-2 rounded-lg transition-colors ${
+ theme === 'dark'
+ ? 'hover:bg-gray-700 text-gray-400 hover:text-white'
+ : 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'
+ }`}
+ >
+
+
+
+
+
+
)}
{/* View Stock Modal */}
{showViewModal && selectedStock && (
-
-
+
-
+
{selectedStock.name}
@@ -638,8 +961,10 @@ const Watchlist = () => {
setShowViewModal(false);
setSelectedStock(null);
}}
- className={`p-2 rounded-lg hover:bg-gray-100 ${
- theme === 'dark' ? 'hover:bg-gray-800' : ''
+ className={`p-2 rounded-lg transition-colors ${
+ theme === 'dark'
+ ? 'hover:bg-gray-700 text-gray-400 hover:text-white'
+ : 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'
}`}
>
@@ -649,8 +974,12 @@ const Watchlist = () => {
{/* Price Chart */}
-
-
+
+
Price History
@@ -698,7 +1027,7 @@ const Watchlist = () => {
type="monotone"
dataKey="price"
stroke={theme === 'dark' ? '#60A5FA' : '#3B82F6'}
- fillOpacity={1}
+ strokeWidth={2}
fill="url(#colorPrice)"
/>
@@ -706,182 +1035,34 @@ const Watchlist = () => {
-
- {/* Price Information */}
-
-
- Price Information
-
-
-
- Current Price
-
- ${parseFloat(selectedStock.current_price || 0).toFixed(2)}
-
-
-
- Target Price
-
- ${parseFloat(selectedStock.target_price || 0).toFixed(2)}
-
-
-
- Distance to Target
- = parseFloat(selectedStock.target_price || 0) ? 'emerald' : 'red'}
- icon={parseFloat(selectedStock.current_price || 0) >= parseFloat(selectedStock.target_price || 0) ? HiTrendingUp : HiTrendingDown}
- >
- {(((parseFloat(selectedStock.current_price || 0) - parseFloat(selectedStock.target_price || 0)) / parseFloat(selectedStock.target_price || 1)) * 100).toFixed(1)}%
-
-
-
-
-
- {/* Portfolio Status */}
-
-
- Portfolio Status
-
-
-
- Shares Held
-
- {parseInt(selectedStock.shares || 0, 10)}
-
-
- {selectedStock.shares > 0 && (
- <>
-
- Buy Price
-
- ${parseFloat(selectedStock.buy_price || 0).toFixed(2)}
-
-
-
- Total Value
-
- ${(parseFloat(selectedStock.current_price || 0) * parseInt(selectedStock.shares || 0, 10)).toFixed(2)}
-
-
-
- Profit/Loss
- = parseFloat(selectedStock.buy_price || 0) ? 'emerald' : 'red'}
- icon={parseFloat(selectedStock.current_price || 0) >= parseFloat(selectedStock.buy_price || 0) ? HiTrendingUp : HiTrendingDown}
- >
- {(((parseFloat(selectedStock.current_price || 0) - parseFloat(selectedStock.buy_price || 0)) / parseFloat(selectedStock.buy_price || 1)) * 100).toFixed(1)}%
-
-
- >
- )}
-
-
-
-
-
-
{
- setShowViewModal(false);
- setSelectedStock(null);
- }}
- className={`px-4 py-2 rounded-xl ${
+ {/* Stock Details */}
+
+
- Close
-
- {
- setShowViewModal(false);
- handleUpdateClick(selectedStock);
- }}
- className="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700"
- >
- Update Target
-
-
-
-
- )}
-
- {/* Update Stock Modal */}
- {showUpdateModal && selectedStock && (
-
-
-
- Update Target Price
-
-
-
-
-
- Stock
-
-
- {selectedStock.name} ({selectedStock.ticker})
-
-
-
-
+ ? 'bg-gray-800 border-gray-600'
+ : 'bg-white border-gray-200'
+ }`}>
+
Current Price
-
-
+
+
${parseFloat(selectedStock.current_price || 0).toFixed(2)}
-
-
- New Target Price ($)
-
-
setSelectedStock({ ...selectedStock, target_price: e.target.value })}
- className={`mt-1 block w-full rounded-xl ${
+
+ ? 'bg-gray-800 border-gray-600'
+ : 'bg-white border-gray-200'
+ }`}>
+
+ Target Price
+
+
+ ${parseFloat(selectedStock.target_price || 0).toFixed(2)}
+
-
- {
- setShowUpdateModal(false);
- setSelectedStock(null);
- }}
- className={`px-4 py-2 rounded-xl ${
- theme === 'dark'
- ? 'bg-gray-800 text-gray-300 hover:bg-gray-700'
- : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
- }`}
- >
- Cancel
-
-
- Update Target
-
-
-
)}
From 3218b170a8f9e520a835051520c5d1aa6e2e63f4 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:54:57 +0530
Subject: [PATCH 07/13] feat: update layout components and navigation with
theme support
---
src/App.jsx | 88 ++++++++++++---
src/components/layout/DashboardLayout.jsx | 26 ++---
src/components/layout/Sidebar.jsx | 128 ++++------------------
src/index.css | 10 ++
4 files changed, 113 insertions(+), 139 deletions(-)
diff --git a/src/App.jsx b/src/App.jsx
index 74f34bd..9b9575e 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,9 +1,17 @@
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
-import Sidebar from './components/layout/Sidebar';
+import { AuthProvider } from './context/AuthContext';
+import ProtectedRoute from './components/auth/ProtectedRoute';
+import Homepage from './pages/Homepage';
+import DashboardLayout from './components/layout/DashboardLayout';
import Dashboard from './pages/Dashboard';
import Portfolio from './pages/Portfolio';
import Watchlist from './pages/Watchlist';
+import Profile from './pages/Profile';
+import Settings from './pages/Settings';
+import StockDetail from './pages/StockDetail';
+import News from './pages/News';
+import MarketCalendar from './pages/MarketCalendar';
export default function App() {
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
@@ -39,17 +47,71 @@ export default function App() {
}, [theme]);
return (
-
-
-
-
-
- } />
- } />
- } />
-
-
-
-
+
+
+
+ {/* Public routes */}
+ } />
+
+ {/* Protected dashboard routes */}
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
);
}
diff --git a/src/components/layout/DashboardLayout.jsx b/src/components/layout/DashboardLayout.jsx
index 4e20194..e217a2f 100644
--- a/src/components/layout/DashboardLayout.jsx
+++ b/src/components/layout/DashboardLayout.jsx
@@ -1,23 +1,15 @@
-import { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import Sidebar from './Sidebar';
-import Navbar from './Navbar';
-
-export default function DashboardLayout({ children }) {
- const [sidebarOpen, setSidebarOpen] = useState(false);
+const DashboardLayout = ({ children, theme }) => {
return (
-
-
-
-
-
setSidebarOpen(true)} />
-
-
-
- {children}
-
-
+
);
-}
\ No newline at end of file
+};
+
+export default DashboardLayout;
\ No newline at end of file
diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx
index 706dbdd..7366c8f 100644
--- a/src/components/layout/Sidebar.jsx
+++ b/src/components/layout/Sidebar.jsx
@@ -1,11 +1,14 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
-import { Link, useLocation } from 'react-router-dom';
+import { Link, useLocation, useNavigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
import axios from 'axios';
import {
HiHome,
HiChartBar,
HiStar,
+ HiNewspaper,
+ HiCalendar,
HiChevronLeft,
HiChevronRight,
HiSun,
@@ -33,134 +36,41 @@ const LoadingOverlay = ({ message }) => (
export default function Sidebar({ theme }) {
const location = useLocation();
+ const navigate = useNavigate();
+ const { signOut } = useAuth();
const [isExpanded, setIsExpanded] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [loadingMessage, setLoadingMessage] = useState('');
const navItems = [
- { path: '/', icon: HiHome, label: 'Dashboard' },
+ { path: '/dashboard', icon: HiHome, label: 'Dashboard' },
{ path: '/portfolio', icon: HiChartBar, label: 'Portfolio' },
- { path: '/watchlist', icon: HiStar, label: 'Watchlist' }
+ { path: '/watchlist', icon: HiStar, label: 'Watchlist' },
+ { path: '/news', icon: HiNewspaper, label: 'News' },
+ { path: '/calendar', icon: HiCalendar, label: 'Calendar' }
];
- const randomStocks = [
- { name: 'Apple Inc.', ticker: 'AAPL', buy_price: 175.50, targetPrice: 200.00, current_price: 175.50 },
- { name: 'Microsoft Corporation', ticker: 'MSFT', buy_price: 340.20, targetPrice: 380.00, current_price: 340.20 },
- { name: 'Amazon.com Inc.', ticker: 'AMZN', buy_price: 125.30, targetPrice: 150.00, current_price: 125.30 },
- { name: 'Alphabet Inc.', ticker: 'GOOGL', buy_price: 135.60, targetPrice: 160.00, current_price: 135.60 },
- { name: 'NVIDIA Corporation', ticker: 'NVDA', buy_price: 450.80, targetPrice: 500.00, current_price: 450.80 },
- { name: 'Meta Platforms Inc.', ticker: 'META', buy_price: 290.40, targetPrice: 320.00, current_price: 290.40 },
- { name: 'Tesla Inc.', ticker: 'TSLA', buy_price: 240.50, targetPrice: 280.00, current_price: 240.50 },
- { name: 'Netflix Inc.', ticker: 'NFLX', buy_price: 385.70, targetPrice: 420.00, current_price: 385.70 },
- { name: 'Adobe Inc.', ticker: 'ADBE', buy_price: 420.30, targetPrice: 460.00, current_price: 420.30 },
- { name: 'Salesforce Inc.', ticker: 'CRM', buy_price: 210.90, targetPrice: 240.00, current_price: 210.90 }
- ];
+
const handleLogout = async () => {
- if (window.confirm('Are you sure you want to logout? This will reset your portfolio..')) {
+ if (window.confirm('Are you sure you want to logout?')) {
try {
setIsLoading(true);
- setLoadingMessage('Resetting your portfolio...');
-
- // Get all current stocks
- const portfolioResponse = await api.get('/stocks');
- const portfolioStocks = portfolioResponse.data || [];
+ setLoadingMessage('Logging out...');
- console.log('Current portfolio stocks:', portfolioStocks);
-
- // Delete all current stocks if there are any
- if (portfolioStocks.length > 0) {
- await Promise.all(portfolioStocks.map(stock =>
- api.delete(`/stocks/${stock.id}`)
- ));
- console.log('Successfully deleted all portfolio stocks');
+ const { error } = await signOut();
+ if (error) {
+ throw error;
}
-
- // Shuffle and pick 5 random stocks
- const shuffled = [...randomStocks].sort(() => 0.5 - Math.random());
- const selectedStocks = shuffled.slice(0, 5);
- console.log('Selected stocks to add:', selectedStocks);
-
- // Keep track of successful and failed additions
- const results = {
- successful: [],
- failed: []
- };
-
- // Add the random stocks one by one
- for (const stock of selectedStocks) {
- try {
- console.log('Adding stock:', stock);
-
- // Add to portfolio with all required fields
- const portfolioStock = {
- name: stock.name,
- ticker: stock.ticker,
- shares: 1,
- buy_price: parseFloat(stock.buy_price),
- current_price: parseFloat(stock.current_price),
- target_price: parseFloat(stock.targetPrice)
- };
-
- // Add to portfolio
- const response = await api.post('/stocks', portfolioStock);
- console.log(`Successfully added stock ${stock.ticker}`);
- results.successful.push(stock.ticker);
- } catch (error) {
- console.error(`Error adding stock ${stock.ticker}:`, error);
- results.failed.push(stock.ticker);
-
- // If a stock fails, try to add a different one from our list
- const remainingStocks = randomStocks.filter(s =>
- !selectedStocks.includes(s) &&
- !results.successful.includes(s.ticker) &&
- !results.failed.includes(s.ticker)
- );
-
- if (remainingStocks.length > 0) {
- const replacementStock = remainingStocks[0];
- console.log(`Trying replacement stock: ${replacementStock.ticker}`);
- try {
- const portfolioStock = {
- name: replacementStock.name,
- ticker: replacementStock.ticker,
- shares: 1,
- buy_price: parseFloat(replacementStock.buy_price),
- current_price: parseFloat(replacementStock.current_price),
- target_price: parseFloat(replacementStock.targetPrice)
- };
-
- const response = await api.post('/stocks', portfolioStock);
- console.log(`Successfully added replacement stock ${replacementStock.ticker}`);
- results.successful.push(replacementStock.ticker);
- } catch (retryError) {
- console.error(`Error adding replacement stock ${replacementStock.ticker}:`, retryError);
- results.failed.push(replacementStock.ticker);
- }
- }
- }
- }
-
- // Check if we have 5 successful additions
- if (results.successful.length === 5) {
- console.log('Successfully added all 5 stocks:', results.successful);
- } else {
- console.warn(`Only added ${results.successful.length} stocks successfully:`, results.successful);
- console.warn('Failed stocks:', results.failed);
- throw new Error(`Could only add ${results.successful.length} out of 5 stocks`);
- }
-
- // Small delay before refreshing
- await new Promise(resolve => setTimeout(resolve, 1000));
- // Refresh the page
- window.location.reload();
+ // Navigate to homepage
+ navigate('/');
} catch (error) {
console.error('Error during logout:', error);
setLoadingMessage('Error occurred. Please try again.');
await new Promise(resolve => setTimeout(resolve, 2000));
setIsLoading(false);
- alert(error.message || 'Failed to logout. Please try again.');
+ alert('Failed to logout. Please try again.');
}
}
};
diff --git a/src/index.css b/src/index.css
index 3c0aa88..d6df601 100644
--- a/src/index.css
+++ b/src/index.css
@@ -2,6 +2,16 @@
@tailwind components;
@tailwind utilities;
+.no-visible-scrollbar {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ -webkit-overflow-scrolling: touch;
+}
+
+.no-visible-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
From c38b789d7f29edced838139d277ed46a7d347766 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:55:13 +0530
Subject: [PATCH 08/13] feat: update backend configuration and database models
for Supabase integration
---
backend/package-lock.json | 157 +++++++++++++++++-
backend/package.json | 9 +-
backend/src/config/database.js | 51 +++---
backend/src/config/finnhub.js | 6 +-
.../migrations/20240115_create_watchlist.js | 16 ++
backend/src/migrations/init.js | 35 +---
backend/src/models/Stock.js | 131 ++++++++++-----
backend/src/models/Watchlist.js | 91 ++++++----
8 files changed, 347 insertions(+), 149 deletions(-)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 1913023..9243bed 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -8,9 +8,10 @@
"name": "portfolio-tracker-backend",
"version": "1.0.0",
"dependencies": {
+ "@supabase/supabase-js": "^2.52.0",
"axios": "^1.6.2",
"cors": "^2.8.5",
- "dotenv": "^16.3.1",
+ "dotenv": "^16.6.1",
"express": "^4.18.2",
"mysql2": "^3.11.5",
"sequelize": "^6.37.5"
@@ -19,6 +20,81 @@
"nodemon": "^3.0.2"
}
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.71.1",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz",
+ "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz",
+ "integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/node-fetch": {
+ "version": "2.6.15",
+ "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
+ "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "1.19.4",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
+ "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.11.15",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.15.tgz",
+ "integrity": "sha512-HQKRnwAqdVqJW/P9TjKVK+/ETpW4yQ8tyDPPtRMKOH4Uh3vQD74vmj353CYs8+YwVBKubeUOOEpI9CT8mT4obw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.13",
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "isows": "^1.0.7",
+ "ws": "^8.18.2"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
+ "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.52.0",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.52.0.tgz",
+ "integrity": "sha512-jbs3CV1f2+ge7sgBeEduboT9v/uGjF22v0yWi/5/XFn5tbM8MfWRccsMtsDwAwu24XK8H6wt2LJDiNnZLtx/bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.71.1",
+ "@supabase/functions-js": "2.4.5",
+ "@supabase/node-fetch": "2.6.15",
+ "@supabase/postgrest-js": "1.19.4",
+ "@supabase/realtime-js": "2.11.15",
+ "@supabase/storage-js": "2.7.1"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -43,12 +119,27 @@
"undici-types": "~6.20.0"
}
},
+ "node_modules/@types/phoenix": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
+ "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
+ "license": "MIT"
+ },
"node_modules/@types/validator": {
"version": "13.12.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
"license": "MIT"
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -354,9 +445,9 @@
}
},
"node_modules/dotenv": {
- "version": "16.4.7",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
- "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -801,6 +892,21 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
+ "node_modules/isows": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
+ "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "ws": "*"
+ }
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1565,6 +1671,12 @@
"nodetouch": "bin/nodetouch.js"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1636,6 +1748,22 @@
"node": ">= 0.8"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/wkx": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
@@ -1644,6 +1772,27 @@
"dependencies": {
"@types/node": "*"
}
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/backend/package.json b/backend/package.json
index d39c9b4..73a3679 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -5,15 +5,16 @@
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
- "dev": "nodemon src/server.js"
+ "dev": "nodemon src/server.js",
+ "init-db": "node init-db.js",
+ "clean-db": "node clean-database.js"
},
"dependencies": {
+ "@supabase/supabase-js": "^2.39.7",
"axios": "^1.6.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
- "express": "^4.18.2",
- "mysql2": "^3.11.5",
- "sequelize": "^6.37.5"
+ "express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.2"
diff --git a/backend/src/config/database.js b/backend/src/config/database.js
index e84d25c..accf209 100644
--- a/backend/src/config/database.js
+++ b/backend/src/config/database.js
@@ -1,42 +1,29 @@
-const { Sequelize } = require('sequelize');
+const { createClient } = require('@supabase/supabase-js');
require('dotenv').config();
-let sequelize;
+const SUPABASE_URL = process.env.SUPABASE_URL;
+const SUPABASE_KEY = process.env.SUPABASE_KEY;
-if (process.env.DATABASE_URL) {
- // Production configuration for Railway
- sequelize = new Sequelize(process.env.DATABASE_URL, {
- dialect: 'mysql',
- dialectOptions: {
- ssl: {
- require: true,
- rejectUnauthorized: false
- }
- },
- logging: false
- });
-} else {
- // Local configuration
- sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
- host: process.env.DB_HOST,
- dialect: 'mysql',
- logging: console.log,
- pool: {
- max: 5,
- min: 0,
- acquire: 30000,
- idle: 10000
- }
- });
+if (!SUPABASE_URL || !SUPABASE_KEY) {
+ console.warn('SUPABASE_URL or SUPABASE_KEY environment variables are not set. Some features will be limited.');
}
+const supabase = createClient(
+ SUPABASE_URL || 'https://example.supabase.co',
+ SUPABASE_KEY || 'demo-key'
+);
+
// Test the connection
-sequelize.authenticate()
- .then(() => {
- console.log('Database connection has been established successfully.');
+supabase.from('stocks').select('count', { count: 'exact', head: true })
+ .then(({ count, error }) => {
+ if (error) {
+ console.error('Unable to connect to Supabase:', error);
+ } else {
+ console.log('Supabase connection has been established successfully.');
+ }
})
.catch(err => {
- console.error('Unable to connect to the database:', err);
+ console.error('Unable to connect to Supabase:', err);
});
-module.exports = sequelize;
\ No newline at end of file
+module.exports = supabase;
\ No newline at end of file
diff --git a/backend/src/config/finnhub.js b/backend/src/config/finnhub.js
index 45046a7..6936742 100644
--- a/backend/src/config/finnhub.js
+++ b/backend/src/config/finnhub.js
@@ -3,11 +3,11 @@ require('dotenv').config();
const FINNHUB_API_KEY = process.env.FINNHUB_API_KEY;
if (!FINNHUB_API_KEY) {
- console.error('FINNHUB_API_KEY environment variable is not set');
- process.exit(1);
+ console.warn('FINNHUB_API_KEY environment variable is not set. Some features will be limited.');
+ // We'll continue running but with limited functionality
}
module.exports = {
- API_KEY: FINNHUB_API_KEY,
+ API_KEY: FINNHUB_API_KEY || 'demo', // Use 'demo' as a fallback
BASE_URL: 'https://finnhub.io/api/v1'
};
diff --git a/backend/src/migrations/20240115_create_watchlist.js b/backend/src/migrations/20240115_create_watchlist.js
index b619a01..b0421fa 100644
--- a/backend/src/migrations/20240115_create_watchlist.js
+++ b/backend/src/migrations/20240115_create_watchlist.js
@@ -1,3 +1,8 @@
+// This migration file is for Sequelize and is not used with Supabase
+// For Supabase, tables are created through the Supabase dashboard or SQL migrations
+// See supabase-schema.sql for the actual table creation
+
+/*
const { DataTypes } = require('sequelize');
module.exports = {
@@ -44,4 +49,15 @@ module.exports = {
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('watchlists');
}
+};
+*/
+
+// Supabase migration placeholder
+module.exports = {
+ up: async () => {
+ console.log('Migration not needed for Supabase - tables are managed separately');
+ },
+ down: async () => {
+ console.log('Migration not needed for Supabase - tables are managed separately');
+ }
};
\ No newline at end of file
diff --git a/backend/src/migrations/init.js b/backend/src/migrations/init.js
index d215eb6..0c36320 100644
--- a/backend/src/migrations/init.js
+++ b/backend/src/migrations/init.js
@@ -1,34 +1,13 @@
-const Stock = require('../models/Stock');
-const sequelize = require('../config/database');
+const supabase = require('../config/supabase');
async function initializeDatabase() {
try {
- // Sync all models with the database
- await sequelize.sync({ force: true });
- console.log('Database tables created successfully');
-
- // Create some initial stocks for testing
- await Stock.bulkCreate([
- {
- name: 'Apple Inc.',
- ticker: 'AAPL',
- shares: 10,
- buy_price: 150.00,
- current_price: 155.00,
- target_price: 180.00,
- is_in_watchlist: false
- },
- {
- name: 'Microsoft Corporation',
- ticker: 'MSFT',
- shares: 5,
- buy_price: 280.00,
- current_price: 290.00,
- target_price: 320.00,
- is_in_watchlist: false
- }
- ]);
- console.log('Initial stocks created successfully');
+ console.log('Database initialization not needed for Supabase - tables are managed separately');
+
+ // Note: For Supabase, tables are created through the Supabase dashboard or SQL migrations
+ // This function is kept for compatibility but doesn't need to do anything
+
+ console.log('Database initialization completed');
} catch (error) {
console.error('Error initializing database:', error);
diff --git a/backend/src/models/Stock.js b/backend/src/models/Stock.js
index 113962b..44b3cf9 100644
--- a/backend/src/models/Stock.js
+++ b/backend/src/models/Stock.js
@@ -1,53 +1,92 @@
-const { DataTypes } = require('sequelize');
-const sequelize = require('../config/database');
+const supabase = require('../config/supabase');
-const Stock = sequelize.define('Stock', {
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- name: {
- type: DataTypes.STRING,
- allowNull: false
- },
- ticker: {
- type: DataTypes.STRING,
- allowNull: false
- },
- shares: {
- type: DataTypes.FLOAT,
- allowNull: false,
- defaultValue: 0
- },
- buy_price: {
- type: DataTypes.FLOAT,
- allowNull: false,
- defaultValue: 0
- },
- current_price: {
- type: DataTypes.FLOAT,
- allowNull: false,
- defaultValue: 0
+const Stock = {
+ async findAll(options = {}) {
+ let query = supabase.from('stocks').select('*');
+
+ if (options.where) {
+ // Handle where clauses
+ Object.entries(options.where).forEach(([key, value]) => {
+ if (typeof value === 'object' && value !== null) {
+ // Handle operators like Op.gt
+ if (value.hasOwnProperty('gt')) {
+ query = query.gt(key, value.gt);
+ } else if (value.hasOwnProperty('lt')) {
+ query = query.lt(key, value.lt);
+ }
+ // Add more operators as needed
+ } else {
+ // Simple equality
+ query = query.eq(key, value);
+ }
+ });
+ }
+
+ if (options.order) {
+ // Handle order clauses - Supabase uses orderBy instead of order
+ options.order.forEach(([column, direction]) => {
+ query = query.order(column, { ascending: direction === 'ASC' });
+ });
+ }
+
+ const { data, error } = await query;
+
+ if (error) throw error;
+ return data || [];
},
- target_price: {
- type: DataTypes.FLOAT,
- allowNull: false,
- defaultValue: 0
+
+ async findByPk(id) {
+ const { data, error } = await supabase
+ .from('stocks')
+ .select('*')
+ .eq('id', id)
+ .single();
+
+ if (error) throw error;
+ return data ? {
+ ...data,
+ update: async (values) => {
+ const { data: updatedData, error: updateError } = await supabase
+ .from('stocks')
+ .update(values)
+ .eq('id', id)
+ .select()
+ .single();
+
+ if (updateError) throw updateError;
+ return updatedData;
+ },
+ destroy: async () => {
+ const { error: deleteError } = await supabase
+ .from('stocks')
+ .delete()
+ .eq('id', id);
+
+ if (deleteError) throw deleteError;
+ return true;
+ }
+ } : null;
},
- is_in_watchlist: {
- type: DataTypes.BOOLEAN,
- allowNull: false,
- defaultValue: false
+
+ async create(values) {
+ const { data, error } = await supabase
+ .from('stocks')
+ .insert([values])
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
},
- last_updated: {
- type: DataTypes.DATE,
- allowNull: false,
- defaultValue: DataTypes.NOW
+
+ async count() {
+ const { count, error } = await supabase
+ .from('stocks')
+ .select('*', { count: 'exact', head: true });
+
+ if (error) throw error;
+ return count || 0;
}
-}, {
- timestamps: true,
- tableName: 'stocks'
-});
+};
module.exports = Stock;
\ No newline at end of file
diff --git a/backend/src/models/Watchlist.js b/backend/src/models/Watchlist.js
index 6c446b8..196215d 100644
--- a/backend/src/models/Watchlist.js
+++ b/backend/src/models/Watchlist.js
@@ -1,38 +1,65 @@
-const { DataTypes } = require('sequelize');
-const sequelize = require('../config/database');
+const supabase = require('../config/supabase');
-const Watchlist = sequelize.define('Watchlist', {
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
+const Watchlist = {
+ async findAll() {
+ const { data, error } = await supabase
+ .from('watchlists')
+ .select('*');
+
+ if (error) throw error;
+ return data || [];
},
- name: {
- type: DataTypes.STRING,
- allowNull: false
- },
- ticker: {
- type: DataTypes.STRING,
- allowNull: false
- },
- target_price: {
- type: DataTypes.DECIMAL(10, 2),
- allowNull: false
- },
- current_price: {
- type: DataTypes.DECIMAL(10, 2),
- allowNull: true
+
+ async findByPk(id) {
+ const { data, error } = await supabase
+ .from('watchlists')
+ .select('*')
+ .eq('id', id)
+ .single();
+
+ if (error) throw error;
+ return data ? {
+ ...data,
+ update: async (values) => {
+ const { data: updatedData, error: updateError } = await supabase
+ .from('watchlists')
+ .update(values)
+ .eq('id', id)
+ .select()
+ .single();
+
+ if (updateError) throw updateError;
+ return updatedData;
+ },
+ destroy: async () => {
+ const { error: deleteError } = await supabase
+ .from('watchlists')
+ .delete()
+ .eq('id', id);
+
+ if (deleteError) throw deleteError;
+ return true;
+ }
+ } : null;
},
- last_updated: {
- type: DataTypes.DATE,
- allowNull: true
+
+ async create(values) {
+ // Add timestamps if they don't exist
+ const dataToInsert = {
+ ...values,
+ created_at: values.created_at || new Date().toISOString(),
+ updated_at: values.updated_at || new Date().toISOString()
+ };
+
+ const { data, error } = await supabase
+ .from('watchlists')
+ .insert([dataToInsert])
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
}
-}, {
- tableName: 'watchlists',
- underscored: true,
- timestamps: true,
- createdAt: 'created_at',
- updatedAt: 'updated_at'
-});
+};
module.exports = Watchlist;
\ No newline at end of file
From d3012928d6c9a6184eff0d09a96c5f32b38a096b Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:55:56 +0530
Subject: [PATCH 09/13] docs: add comprehensive setup documentation and
environment configuration
---
.env.example | 6 ++
EMAIL_TEMPLATES_SETUP.md | 202 ++++++++++++++++++++++++++++++++++
README.md | 22 ++--
SUPABASE_AUTH_SETUP.md | 124 +++++++++++++++++++++
USER_SETUP_INSTRUCTIONS.md | 216 +++++++++++++++++++++++++++++++++++++
5 files changed, 560 insertions(+), 10 deletions(-)
create mode 100644 .env.example
create mode 100644 EMAIL_TEMPLATES_SETUP.md
create mode 100644 SUPABASE_AUTH_SETUP.md
create mode 100644 USER_SETUP_INSTRUCTIONS.md
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..f529a35
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+# Supabase Configuration
+VITE_SUPABASE_URL=your_supabase_project_url
+VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
+
+# API Configuration
+VITE_API_BASE_URL=https://portfolio-tracker-backend-y7ne.onrender.com/api
\ No newline at end of file
diff --git a/EMAIL_TEMPLATES_SETUP.md b/EMAIL_TEMPLATES_SETUP.md
new file mode 100644
index 0000000..33aeb1c
--- /dev/null
+++ b/EMAIL_TEMPLATES_SETUP.md
@@ -0,0 +1,202 @@
+# π§ Email Templates Setup Guide
+
+This guide will help you implement the beautiful, aesthetic email templates for your Portfolio Tracker project in Supabase.
+
+## π¨ Template Overview
+
+We've created three stunning email templates:
+
+1. **Confirm Signup** - Welcome new users with feature highlights
+2. **Reset Password** - Secure password reset with security tips
+3. **Magic Link** - Passwordless authentication with benefits showcase
+
+## π Implementation Steps
+
+### Step 1: Access Supabase Email Templates
+
+1. Go to your [Supabase Dashboard](https://supabase.com/dashboard)
+2. Select your Portfolio Tracker project
+3. Navigate to **Authentication** β **Email Templates**
+
+### Step 2: Configure Confirm Signup Template
+
+1. Click on **"Confirm signup"** template
+2. Replace the default content with the content from `email-templates/confirm-signup.html`
+3. **Important**: Replace `{{ .Email }}` with `{{ .Email }}` (Supabase uses this format)
+4. Replace `{{ .ConfirmationURL }}` with `{{ .ConfirmationURL }}`
+5. Click **Save**
+
+### Step 3: Configure Reset Password Template
+
+1. Click on **"Reset password"** template
+2. Replace the default content with the content from `email-templates/reset-password.html`
+3. Replace `{{ .Email }}` with `{{ .Email }}`
+4. Replace `{{ .ConfirmationURL }}` with `{{ .ConfirmationURL }}`
+5. Click **Save**
+
+### Step 4: Configure Magic Link Template
+
+1. Click on **"Magic link"** template
+2. Replace the default content with the content from `email-templates/magic-link.html`
+3. Replace `{{ .Email }}` with `{{ .Email }}`
+4. Replace `{{ .ConfirmationURL }}` with `{{ .ConfirmationURL }}`
+5. Click **Save**
+
+## π― Template Features
+
+### β¨ Design Highlights
+
+- **Modern Gradient Headers**: Each template has a unique color scheme
+- **Responsive Design**: Works perfectly on mobile and desktop
+- **Brand Consistency**: Matches your Portfolio Tracker theme
+- **Professional Typography**: Clean, readable fonts
+- **Interactive Elements**: Hover effects and smooth transitions
+
+### π§ Customization Options
+
+#### Colors & Branding
+- **Confirm Signup**: Blue gradient (#3b82f6 to #1d4ed8)
+- **Reset Password**: Red gradient (#ef4444 to #dc2626)
+- **Magic Link**: Purple gradient (#8b5cf6 to #7c3aed)
+
+#### Content Sections
+- **Welcome Messages**: Personalized greetings
+- **Feature Highlights**: Showcase key benefits
+- **Security Information**: Important safety reminders
+- **Call-to-Action Buttons**: Clear, prominent CTAs
+- **Footer Links**: Support and social media
+
+## π± Mobile Optimization
+
+All templates are fully responsive and include:
+- Mobile-first design approach
+- Optimized typography for small screens
+- Touch-friendly button sizes
+- Proper spacing for mobile viewing
+
+## π Security Features
+
+### Built-in Security Elements
+- **Expiration Notices**: Clear time limits for links
+- **Security Warnings**: Alerts for unintended requests
+- **Privacy Information**: Data protection notices
+- **Support Contacts**: Easy access to help
+
+### Template Variables
+- `{{ .Email }}` - User's email address
+- `{{ .ConfirmationURL }}` - Secure confirmation link
+- `{{ .TokenHash }}` - Security token (if needed)
+- `{{ .SiteURL }}` - Your application URL
+
+## π¨ Customization Guide
+
+### Changing Colors
+To modify the color scheme, update these CSS variables:
+
+```css
+/* Confirm Signup - Blue Theme */
+background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+
+/* Reset Password - Red Theme */
+background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+
+/* Magic Link - Purple Theme */
+background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
+```
+
+### Updating Content
+- **Company Name**: Replace "Portfolio Tracker" with your brand
+- **Support Email**: Update `support@portfoliotracker.com`
+- **Social Links**: Add your actual social media URLs
+- **Features**: Customize the feature highlights for your app
+
+### Adding Your Logo
+Replace the SVG icon in the header with your actual logo:
+
+```html
+
+
+
+
+```
+
+## π Testing Your Templates
+
+### 1. Enable Email Confirmations
+1. Go to **Authentication** β **Settings**
+2. Enable **"Enable email confirmations"**
+3. Set **Site URL** to your domain
+4. Add redirect URLs for your app
+
+### 2. Test Each Template
+1. **Signup Test**: Create a new account
+2. **Password Reset Test**: Use "Forgot Password" feature
+3. **Magic Link Test**: Enable magic link authentication
+
+### 3. Preview Templates
+Use Supabase's built-in preview feature to see how templates look before sending.
+
+## π¨ Troubleshooting
+
+### Common Issues
+
+#### Template Not Loading
+- Check for syntax errors in HTML
+- Ensure all CSS is properly formatted
+- Verify template variables are correct
+
+#### Styling Issues
+- Test in different email clients
+- Use inline CSS for better compatibility
+- Avoid complex CSS animations
+
+#### Links Not Working
+- Verify `{{ .ConfirmationURL }}` is properly set
+- Check redirect URLs in Supabase settings
+- Test link expiration times
+
+### Email Client Compatibility
+- **Gmail**: Full support
+- **Outlook**: Good support (some CSS limitations)
+- **Apple Mail**: Full support
+- **Mobile Apps**: Responsive design works well
+
+## π Best Practices
+
+### Content Guidelines
+- Keep subject lines under 50 characters
+- Use clear, action-oriented language
+- Include your brand name in subject lines
+- Test with different email addresses
+
+### Design Guidelines
+- Use high contrast for readability
+- Keep images under 1MB
+- Use web-safe fonts as fallbacks
+- Test on multiple devices
+
+### Security Guidelines
+- Never include sensitive data in emails
+- Use HTTPS for all links
+- Implement proper link expiration
+- Monitor for suspicious activity
+
+## π Success Metrics
+
+Track these metrics to measure template effectiveness:
+- **Open Rates**: Aim for 20-30%
+- **Click-Through Rates**: Target 2-5%
+- **Conversion Rates**: Measure signup completions
+- **Bounce Rates**: Keep under 5%
+
+## π Support
+
+If you need help with the templates:
+1. Check the Supabase documentation
+2. Review email client compatibility guides
+3. Test with different email providers
+4. Contact Supabase support if needed
+
+---
+
+**π¨ Your Portfolio Tracker now has beautiful, professional email templates that will enhance user experience and build trust with your users!**
\ No newline at end of file
diff --git a/README.md b/README.md
index 4098128..a3796a5 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://portfolio-tracker-backend-y7ne.onrender.com/api)
[](https://reactjs.org/)
[](https://vitejs.dev/)
-[](https://www.mysql.com/)
+[](https://supabase.com/)
[](https://tailwindcss.com/)
[](https://nodejs.org/)
[](https://finnhub.io/)
@@ -41,7 +41,7 @@
- Node.js 14+
- npm or yarn
-- MySQL
+- Supabase account
- Finnhub API Key (get one at [finnhub.io](https://finnhub.io/))
### Frontend Setup
@@ -73,15 +73,18 @@ npm install
2. Configure `.env`:
```env
-DATABASE_URL=mysql://user:password@localhost:3306/portfolio_tracker
+SUPABASE_URL=your_supabase_url
+SUPABASE_KEY=your_supabase_anon_key
PORT=5000
-NODE_ENV=development
FINNHUB_API_KEY=your_finnhub_api_key
```
-3. Run migrations and start:
+3. Set up Supabase:
+ - Follow the instructions in `backend/SUPABASE_SETUP.md`
+
+4. Test connection and start:
```bash
-npm run migrate
+npm run init-db
npm start
```
@@ -142,8 +145,7 @@ Visit our [API Documentation](https://hackstyx.github.io/portfolio-tracker/)
### π Backend
- **Runtime**: [Node.js](https://nodejs.org/) - JavaScript runtime
- **Framework**: [Express](https://expressjs.com/) - Web framework for Node.js
-- **Database**: [MySQL](https://www.mysql.com/) - Open-source relational database
-- **ORM**: [Sequelize](https://sequelize.org/) - Modern TypeScript and Node.js ORM
+- **Database**: [Supabase](https://supabase.com/) - Open source Firebase alternative
- **Market Data**: [Finnhub](https://finnhub.io/) - Real-time RESTful APIs for stocks
- **API Documentation**: [Github Pages](https://hackstyx.github.io/portfolio-tracker/)
@@ -156,8 +158,8 @@ Visit our [API Documentation](https://hackstyx.github.io/portfolio-tracker/)
- Containerized deployment
- Automatic scaling
- Built-in monitoring
-- **Database Hosting**: [Railway](https://railway.app/)
- - Managed MySQL database
+- **Database Hosting**: [Supabase](https://supabase.com/)
+ - PostgreSQL database
- Automated backups
- High availability
diff --git a/SUPABASE_AUTH_SETUP.md b/SUPABASE_AUTH_SETUP.md
new file mode 100644
index 0000000..264f1dd
--- /dev/null
+++ b/SUPABASE_AUTH_SETUP.md
@@ -0,0 +1,124 @@
+# Supabase Authentication Setup
+
+This guide will help you set up Supabase authentication for the Portfolio Tracker application.
+
+## Prerequisites
+
+1. A Supabase account (sign up at [supabase.com](https://supabase.com))
+2. A Supabase project created
+
+## Setup Steps
+
+### 1. Create a Supabase Project
+
+1. Go to [supabase.com](https://supabase.com) and sign in
+2. Click "New Project"
+3. Choose your organization
+4. Enter a project name (e.g., "portfolio-tracker")
+5. Enter a database password
+6. Choose a region close to your users
+7. Click "Create new project"
+
+### 2. Get Your Project Credentials
+
+1. In your Supabase dashboard, go to Settings > API
+2. Copy the following values:
+ - **Project URL** (looks like: `https://your-project-id.supabase.co`)
+ - **anon public** key (starts with `eyJ...`)
+
+### 3. Configure Environment Variables
+
+1. Create a `.env` file in the root of your project
+2. Add the following variables:
+
+```env
+VITE_SUPABASE_URL=https://your-project-id.supabase.co
+VITE_SUPABASE_ANON_KEY=your_anon_key_here
+VITE_API_BASE_URL=https://portfolio-tracker-backend-y7ne.onrender.com/api
+```
+
+### 4. Configure Authentication Settings
+
+1. In your Supabase dashboard, go to Authentication > Settings
+2. Configure the following:
+
+#### Site URL
+- Set to `http://localhost:5173` for development
+- Set to your production URL for production
+
+#### Redirect URLs
+Add these redirect URLs:
+- `http://localhost:5173/dashboard`
+- `http://localhost:5173/`
+- Your production URLs when deployed
+
+### 5. Email Templates (Optional)
+
+1. Go to Authentication > Email Templates
+2. Customize the email templates for:
+ - Confirm signup
+ - Reset password
+ - Magic link
+
+### 6. Enable Email Confirmation (Recommended)
+
+1. Go to Authentication > Settings
+2. Enable "Enable email confirmations"
+3. This ensures users verify their email before accessing the app
+
+## Features Implemented
+
+### Authentication Functions
+- **Sign Up**: Users can create accounts with email, password, and name
+- **Sign In**: Users can sign in with email and password
+- **Sign Out**: Users can sign out and return to homepage
+- **Password Reset**: Users can request password reset emails
+- **Email Verification**: Users receive confirmation emails (if enabled)
+
+### Protected Routes
+- All dashboard routes (`/dashboard`, `/portfolio`, `/watchlist`, etc.) are protected
+- Unauthenticated users are redirected to the homepage
+- Loading states are shown during authentication checks
+
+### User Experience
+- Form validation and error handling
+- Loading states during authentication
+- Success/error messages
+- Automatic redirect to dashboard after successful login
+- Persistent sessions across browser refreshes
+
+## Testing the Setup
+
+1. Start the development server: `npm run dev`
+2. Navigate to `http://localhost:5173`
+3. Try creating a new account
+4. Try signing in with the created account
+5. Test the logout functionality
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"Invalid API key" error**
+ - Check that your `VITE_SUPABASE_ANON_KEY` is correct
+ - Make sure you're using the "anon public" key, not the service role key
+
+2. **"Invalid redirect URL" error**
+ - Add your localhost URL to the redirect URLs in Supabase settings
+ - Make sure the URL matches exactly (including protocol and port)
+
+3. **Email not received**
+ - Check spam folder
+ - Verify email templates are configured in Supabase
+ - Check Supabase logs for email delivery issues
+
+4. **Authentication not working**
+ - Verify environment variables are loaded correctly
+ - Check browser console for errors
+ - Ensure Supabase project is active and not paused
+
+### Getting Help
+
+- Check the [Supabase documentation](https://supabase.com/docs)
+- Review the [Supabase Auth documentation](https://supabase.com/docs/guides/auth)
+- Check the browser console for detailed error messages
\ No newline at end of file
diff --git a/USER_SETUP_INSTRUCTIONS.md b/USER_SETUP_INSTRUCTIONS.md
new file mode 100644
index 0000000..896be18
--- /dev/null
+++ b/USER_SETUP_INSTRUCTIONS.md
@@ -0,0 +1,216 @@
+# π User-Specific Data Setup Instructions
+
+## π― **Overview**
+This guide will help you set up user-specific data isolation so that each user only sees their own portfolio and watchlist data.
+
+## π **Prerequisites**
+- Supabase project with authentication enabled
+- Access to Supabase SQL Editor
+- Backend server running
+
+## ποΈ **Step 1: Database Migration**
+
+### **1.1 Run the Migration Script**
+1. Go to your **Supabase Dashboard**
+2. Navigate to **SQL Editor**
+3. Copy and paste the following SQL script:
+
+```sql
+-- Migration to add user_id columns for user-specific data isolation
+-- Run this in your Supabase SQL Editor
+
+-- Add user_id column to stocks table
+ALTER TABLE stocks
+ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
+
+-- Add user_id column to watchlists table
+ALTER TABLE watchlists
+ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
+
+-- Create indexes for better performance
+CREATE INDEX IF NOT EXISTS idx_stocks_user_id ON stocks(user_id);
+CREATE INDEX IF NOT EXISTS idx_watchlists_user_id ON watchlists(user_id);
+
+-- Add RLS (Row Level Security) policies
+-- Enable RLS on both tables
+ALTER TABLE stocks ENABLE ROW LEVEL SECURITY;
+ALTER TABLE watchlists ENABLE ROW LEVEL SECURITY;
+
+-- Create policy for stocks table - users can only see their own stocks
+CREATE POLICY "Users can view own stocks" ON stocks
+ FOR SELECT USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can insert own stocks" ON stocks
+ FOR INSERT WITH CHECK (auth.uid() = user_id);
+
+CREATE POLICY "Users can update own stocks" ON stocks
+ FOR UPDATE USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can delete own stocks" ON stocks
+ FOR DELETE USING (auth.uid() = user_id);
+
+-- Create policy for watchlists table - users can only see their own watchlist items
+CREATE POLICY "Users can view own watchlist" ON watchlists
+ FOR SELECT USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can insert own watchlist items" ON watchlists
+ FOR INSERT WITH CHECK (auth.uid() = user_id);
+
+CREATE POLICY "Users can update own watchlist items" ON watchlists
+ FOR UPDATE USING (auth.uid() = user_id);
+
+CREATE POLICY "Users can delete own watchlist items" ON watchlists
+ FOR DELETE USING (auth.uid() = user_id);
+```
+
+4. Click **Run** to execute the migration
+
+### **1.2 Verify Migration**
+Run this query to verify the changes:
+
+```sql
+-- Check if user_id columns were added
+SELECT column_name, data_type
+FROM information_schema.columns
+WHERE table_name = 'stocks' AND column_name = 'user_id';
+
+SELECT column_name, data_type
+FROM information_schema.columns
+WHERE table_name = 'watchlists' AND column_name = 'user_id';
+
+-- Check if RLS is enabled
+SELECT schemaname, tablename, rowsecurity
+FROM pg_tables
+WHERE tablename IN ('stocks', 'watchlists');
+```
+
+## π§ **Step 2: Backend Setup**
+
+### **2.1 Install Dependencies**
+Make sure your backend has the required dependencies:
+
+```bash
+cd backend
+npm install @supabase/supabase-js
+```
+
+### **2.2 Environment Variables**
+Ensure your backend `.env` file has:
+
+```env
+SUPABASE_URL=your_supabase_url
+SUPABASE_ANON_KEY=your_supabase_anon_key
+SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
+```
+
+### **2.3 Restart Backend**
+Restart your backend server to apply the changes:
+
+```bash
+npm run dev
+```
+
+## π¨ **Step 3: Frontend Setup**
+
+### **3.1 Verify API Service**
+The frontend has been updated to use a centralized API service with authentication. Check that `src/services/api.js` exists and includes:
+
+- Request interceptor for adding auth tokens
+- Response interceptor for handling auth errors
+- Automatic token refresh
+
+### **3.2 Test User Isolation**
+1. Create two different user accounts
+2. Log in with the first user and add some stocks
+3. Log out and log in with the second user
+4. Verify that the second user sees an empty portfolio/watchlist
+
+## π§ͺ **Step 4: Testing**
+
+### **4.1 Test User Registration**
+1. Go to your application homepage
+2. Create a new account with email/password
+3. Verify you can log in successfully
+
+### **4.2 Test Data Isolation**
+1. **User A**: Add stocks to portfolio and watchlist
+2. **User B**: Create a different account and verify empty data
+3. **User A**: Log back in and verify data is still there
+
+### **4.3 Test API Endpoints**
+Test these endpoints with authentication:
+
+```bash
+# Get portfolio stocks (requires auth)
+curl -H "Authorization: Bearer YOUR_TOKEN" \
+ http://localhost:5000/api/stocks
+
+# Get watchlist (requires auth)
+curl -H "Authorization: Bearer YOUR_TOKEN" \
+ http://localhost:5000/api/watchlist
+
+# Add stock to portfolio (requires auth)
+curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Apple Inc","ticker":"AAPL","shares":10,"buy_price":150}' \
+ http://localhost:5000/api/stocks
+```
+
+## π **Step 5: Troubleshooting**
+
+### **5.1 Common Issues**
+
+**Issue**: "No token provided" error
+**Solution**: Check that the frontend is properly sending auth headers
+
+**Issue**: "Access denied" error
+**Solution**: Verify RLS policies are correctly set up
+
+**Issue**: Users seeing each other's data
+**Solution**: Check that `user_id` is being set correctly in database operations
+
+### **5.2 Debug Commands**
+
+```sql
+-- Check current user's stocks
+SELECT * FROM stocks WHERE user_id = auth.uid();
+
+-- Check all stocks (admin only)
+SELECT * FROM stocks;
+
+-- Check RLS policies
+SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
+FROM pg_policies
+WHERE tablename IN ('stocks', 'watchlists');
+```
+
+## π **Step 6: Verification Checklist**
+
+- [ ] Database migration completed successfully
+- [ ] RLS policies are active
+- [ ] Backend authentication middleware is working
+- [ ] Frontend API service includes auth tokens
+- [ ] User registration and login works
+- [ ] Data isolation between users is working
+- [ ] All CRUD operations work with authentication
+- [ ] Error handling for unauthorized access works
+
+## π **Benefits After Setup**
+
+β
**Complete Data Isolation**: Each user only sees their own data
+β
**Security**: Row Level Security prevents unauthorized access
+β
**Scalability**: System can handle multiple users efficiently
+β
**Compliance**: Meets data privacy requirements
+β
**User Experience**: Clean, personalized interface for each user
+
+## π **Support**
+
+If you encounter any issues:
+1. Check the browser console for frontend errors
+2. Check the backend logs for API errors
+3. Verify Supabase authentication is working
+4. Test with a fresh user account
+
+---
+
+**π― Result**: Each user will now have their own isolated portfolio and watchlist data!
\ No newline at end of file
From 91447ab4df8acd8abc8a970d94ee3a30332ef795 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 13:56:07 +0530
Subject: [PATCH 10/13] chore: update package dependencies and server
configuration
---
backend/src/server.js | 81 +------
email-templates/confirm-signup.html | 298 +++++++++++++++++++++++
email-templates/magic-link.html | 360 ++++++++++++++++++++++++++++
email-templates/reset-password.html | 278 +++++++++++++++++++++
package-lock.json | 292 ++++++++++++++++++++--
package.json | 11 +-
6 files changed, 1222 insertions(+), 98 deletions(-)
create mode 100644 email-templates/confirm-signup.html
create mode 100644 email-templates/magic-link.html
create mode 100644 email-templates/reset-password.html
diff --git a/backend/src/server.js b/backend/src/server.js
index 5ff7c7a..d1e0181 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -2,60 +2,10 @@ const express = require('express');
const cors = require('cors');
require('dotenv').config();
-const sequelize = require('./config/database');
+const supabase = require('./config/supabase');
const stockRoutes = require('./routes/stocks');
const watchlistRoutes = require('./routes/watchlist');
const stockPriceService = require('./services/stockPriceService');
-const Stock = require('./models/Stock');
-
-// Sample stocks data for initialization
-const defaultStocks = [
- {
- name: 'Apple Inc.',
- ticker: 'AAPL',
- shares: 1,
- buy_price: 175.50,
- current_price: 175.50,
- target_price: 200.00,
- is_in_watchlist: true
- },
- {
- name: 'Microsoft Corporation',
- ticker: 'MSFT',
- shares: 1,
- buy_price: 350.00,
- current_price: 350.00,
- target_price: 400.00,
- is_in_watchlist: true
- },
- {
- name: 'Amazon.com Inc.',
- ticker: 'AMZN',
- shares: 1,
- buy_price: 145.00,
- current_price: 145.00,
- target_price: 170.00,
- is_in_watchlist: true
- },
- {
- name: 'NVIDIA Corporation',
- ticker: 'NVDA',
- shares: 1,
- buy_price: 480.00,
- current_price: 480.00,
- target_price: 550.00,
- is_in_watchlist: true
- },
- {
- name: 'Tesla Inc.',
- ticker: 'TSLA',
- shares: 1,
- buy_price: 240.00,
- current_price: 240.00,
- target_price: 280.00,
- is_in_watchlist: true
- }
-];
const app = express();
@@ -94,36 +44,9 @@ app.use('/api/watchlist', watchlistRoutes);
const PORT = process.env.PORT || 5000;
-const initializeStocks = async () => {
- try {
- // Check if there are any existing stocks
- const stockCount = await Stock.count();
-
- if (stockCount === 0) {
- console.log('No stocks found. Initializing with default stocks...');
-
- // Create default stocks
- await Promise.all(defaultStocks.map(stock => Stock.create(stock)));
-
- console.log('Default stocks created successfully');
- } else {
- console.log(`Found ${stockCount} existing stocks. Skipping initialization.`);
- }
- } catch (error) {
- console.error('Error initializing stocks:', error);
- }
-};
-
const start = async () => {
try {
- // Sync database
- await sequelize.sync();
- console.log('Database synced successfully');
-
- // Initialize stocks
- await initializeStocks();
-
- // Start periodic stock price updates
+ // Start periodic stock price updates for existing stocks
stockPriceService.startPeriodicUpdates();
app.listen(PORT, () => {
diff --git a/email-templates/confirm-signup.html b/email-templates/confirm-signup.html
new file mode 100644
index 0000000..a7b1bad
--- /dev/null
+++ b/email-templates/confirm-signup.html
@@ -0,0 +1,298 @@
+
+
+
+
+
+ Welcome to Portfolio Tracker
+
+
+
+
+
+
+
+
+
Welcome aboard! π
+
Hi {{ .Email }}, thank you for joining Portfolio Tracker. We're excited to help you track your investments and make informed decisions.
+
+
+
+
+
+
Real-time Tracking
+
Monitor your portfolio with live stock prices and updates
+
+
+
+
+
Smart Analytics
+
Get detailed insights and performance analytics
+
+
+
+
+
Price Alerts
+
Set custom alerts for your target prices
+
+
+
+
+
Secure & Private
+
Your data is protected with enterprise-grade security
+
+
+
+
+
Ready to start tracking?
+
Click the button below to verify your email and access your dashboard.
+
Verify Email & Get Started
+
This link will expire in 24 hours for security reasons.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/email-templates/magic-link.html b/email-templates/magic-link.html
new file mode 100644
index 0000000..bf5ace3
--- /dev/null
+++ b/email-templates/magic-link.html
@@ -0,0 +1,360 @@
+
+
+
+
+
+ Magic Link - Portfolio Tracker
+
+
+
+
+
+
+
+
+
Your Magic Link is Ready! β¨
+
Hi {{ .Email }}, click the button below to instantly sign in to your Portfolio Tracker account. No password needed!
+
+
+
+
Why Magic Links?
+
Magic links provide the most secure and convenient way to access your portfolio. No passwords to remember, no security risks.
+
+
+
+
+
+
Ultra Secure
+
One-time use links that expire automatically
+
+
+
+
+
Instant Access
+
No passwords to remember or type
+
+
+
+
+
Convenient
+
Just click and you're in
+
+
+
+
+
Smart Notifications
+
Get alerts when someone tries to access
+
+
+
+
+
Ready to access your portfolio?
+
Click the button below to instantly sign in to your Portfolio Tracker dashboard.
+
Sign In Instantly
+
This link will expire in 10 minutes for security reasons.
+
+
+
+
π Security Notice
+
If you didn't request this magic link, you can safely ignore this email. Your account remains secure.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/email-templates/reset-password.html b/email-templates/reset-password.html
new file mode 100644
index 0000000..4e0b4e7
--- /dev/null
+++ b/email-templates/reset-password.html
@@ -0,0 +1,278 @@
+
+
+
+
+
+ Reset Your Password - Portfolio Tracker
+
+
+
+
+
+
+
+
+
Reset Your Password π
+
Hi {{ .Email }}, we received a request to reset your Portfolio Tracker password. Click the button below to create a new secure password.
+
+
+
+
Security Reminders
+
+ Use a strong password with at least 8 characters
+ Include a mix of letters, numbers, and symbols
+ Don't use the same password for multiple accounts
+ Never share your password with anyone
+
+
+
+
+
Ready to reset?
+
Click the button below to securely reset your password and regain access to your portfolio.
+
Reset Password
+
This link will expire in 1 hour for security reasons.
+
+
+
+
β οΈ Didn't request this?
+
If you didn't request a password reset, you can safely ignore this email. Your account remains secure.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index d64293e..0fe93cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,19 +10,23 @@
"dependencies": {
"@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.1",
+ "@supabase/supabase-js": "^2.52.0",
"axios": "^1.7.9",
+ "clsx": "^2.1.1",
"date-fns": "^3.6.0",
+ "motion": "^12.23.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
- "recharts": "^2.15.0"
+ "recharts": "^2.15.0",
+ "tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@heroicons/react": "^2.2.0",
"@tremor/react": "^3.18.6",
- "@types/react": "^18.3.17",
- "@types/react-dom": "^18.3.5",
+ "@types/react": "^18.3.23",
+ "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
@@ -35,6 +39,7 @@
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.0",
"tailwindcss": "^3.4.17",
+ "typescript": "^5.8.3",
"vite": "^6.0.3"
}
},
@@ -1525,6 +1530,81 @@
"win32"
]
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.71.1",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz",
+ "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz",
+ "integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/node-fetch": {
+ "version": "2.6.15",
+ "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
+ "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "1.19.4",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
+ "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.11.15",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.15.tgz",
+ "integrity": "sha512-HQKRnwAqdVqJW/P9TjKVK+/ETpW4yQ8tyDPPtRMKOH4Uh3vQD74vmj353CYs8+YwVBKubeUOOEpI9CT8mT4obw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.13",
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "isows": "^1.0.7",
+ "ws": "^8.18.2"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
+ "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.52.0",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.52.0.tgz",
+ "integrity": "sha512-jbs3CV1f2+ge7sgBeEduboT9v/uGjF22v0yWi/5/XFn5tbM8MfWRccsMtsDwAwu24XK8H6wt2LJDiNnZLtx/bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.71.1",
+ "@supabase/functions-js": "2.4.5",
+ "@supabase/node-fetch": "2.6.15",
+ "@supabase/postgrest-js": "1.19.4",
+ "@supabase/realtime-js": "2.11.15",
+ "@supabase/storage-js": "2.7.1"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1611,6 +1691,17 @@
"react-dom": ">=16.8.0"
}
},
+ "node_modules/@tremor/react/node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1740,6 +1831,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "24.0.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
+ "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
+ "node_modules/@types/phoenix": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
+ "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
+ "license": "MIT"
+ },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -1748,9 +1854,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
- "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1759,15 +1865,24 @@
}
},
"node_modules/@types/react-dom": {
- "version": "18.3.5",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
- "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@@ -2251,9 +2366,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001690",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
- "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
+ "version": "1.0.30001727",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
+ "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"dev": true,
"funding": [
{
@@ -4218,6 +4333,21 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
+ "node_modules/isows": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
+ "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "ws": "*"
+ }
+ },
"node_modules/iterator.prototype": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz",
@@ -4510,6 +4640,32 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/motion": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.6.tgz",
+ "integrity": "sha512-6U55IW5i6Vut2ryKEhrZKg55490k9d6qdGXZoNSf98oQgDj5D7bqTnVJotQ6UW3AS6QfbW6KSLa7/e1gy+a07g==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/motion-dom": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
@@ -4524,6 +4680,48 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/motion/node_modules/framer-motion": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.6.tgz",
+ "integrity": "sha512-dsJ389QImVE3lQvM8Mnk99/j8tiZDM/7706PCqvkQ8sSCnpmWxsgX+g0lj7r5OBVL0U36pIecCTBoIWcM2RuKw==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.6",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion/node_modules/motion-dom": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.6.tgz",
+ "integrity": "sha512-G2w6Nw7ZOVSzcQmsdLc0doMe64O/Sbuc2bVAbgMz6oP/6/pRStKRiVRV4bQfHp5AHYAKEGhEdVHTM+R3FDgi5w==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion/node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5940,10 +6138,9 @@
"license": "MIT"
},
"node_modules/tailwind-merge": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
- "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==",
- "dev": true,
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
+ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
"license": "MIT",
"funding": {
"type": "github",
@@ -6046,6 +6243,12 @@
"node": ">=8.0"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -6156,6 +6359,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -6175,6 +6392,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "license": "MIT"
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
@@ -6316,6 +6539,22 @@
}
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6517,6 +6756,27 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/package.json b/package.json
index f333f84..eb4a96c 100644
--- a/package.json
+++ b/package.json
@@ -12,19 +12,23 @@
"dependencies": {
"@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.1",
+ "@supabase/supabase-js": "^2.52.0",
"axios": "^1.7.9",
+ "clsx": "^2.1.1",
"date-fns": "^3.6.0",
+ "motion": "^12.23.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
- "recharts": "^2.15.0"
+ "recharts": "^2.15.0",
+ "tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@heroicons/react": "^2.2.0",
"@tremor/react": "^3.18.6",
- "@types/react": "^18.3.17",
- "@types/react-dom": "^18.3.5",
+ "@types/react": "^18.3.23",
+ "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
@@ -37,6 +41,7 @@
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.0",
"tailwindcss": "^3.4.17",
+ "typescript": "^5.8.3",
"vite": "^6.0.3"
}
}
From 233be08b81cdc4b3ce74d900f00b152bb71a0448 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 14:02:16 +0530
Subject: [PATCH 11/13] docs: add comprehensive deployment guide for Vercel and
Render
---
DEPLOYMENT_GUIDE.md | 274 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 274 insertions(+)
create mode 100644 DEPLOYMENT_GUIDE.md
diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md
new file mode 100644
index 0000000..ceff8a4
--- /dev/null
+++ b/DEPLOYMENT_GUIDE.md
@@ -0,0 +1,274 @@
+# π Portfolio Tracker Deployment Guide
+
+This guide will help you deploy your Portfolio Tracker application to production.
+
+## π Prerequisites
+
+1. **Supabase Account** - [Sign up here](https://supabase.com)
+2. **Finnhub API Key** - [Get free API key here](https://finnhub.io/register)
+3. **GitHub Account** - Your code is already there
+4. **Render Account** - [Sign up here](https://render.com) (for backend)
+5. **Vercel Account** - [Sign up here](https://vercel.com) (for frontend)
+
+## π― Deployment Strategy
+
+- **Frontend (React/Vite)** β **Vercel**
+- **Backend (Express.js)** β **Render**
+
+---
+
+## π§ Step 1: Backend Deployment (Render)
+
+### 1.1 Create Supabase Project
+
+1. Go to [supabase.com](https://supabase.com) and sign in
+2. Click "New Project"
+3. Choose your organization
+4. Enter project name: `portfolio-tracker`
+5. Enter a database password (save this!)
+6. Choose a region close to your users
+7. Click "Create new project"
+
+### 1.2 Get Supabase Credentials
+
+1. In your Supabase dashboard, go to **Settings > API**
+2. Copy these values:
+ - **Project URL**: `https://your-project-id.supabase.co`
+ - **anon public key**: `eyJ...` (starts with eyJ)
+ - **service_role key**: `eyJ...` (starts with eyJ)
+
+### 1.3 Get Finnhub API Key
+
+1. Go to [finnhub.io/register](https://finnhub.io/register)
+2. Sign up for a free account
+3. Copy your API key from the dashboard
+
+### 1.4 Deploy Backend to Render
+
+1. Go to [render.com](https://render.com) and sign up/login
+2. Click **"New +"** β **"Web Service"**
+3. Connect your GitHub repository: `HackStyx/portfolio-tracker`
+4. Configure the service:
+
+ **Basic Settings:**
+ - **Name**: `portfolio-tracker-backend`
+ - **Environment**: `Node`
+ - **Region**: Choose closest to your users
+ - **Branch**: `main`
+ - **Root Directory**: `backend`
+ - **Build Command**: `npm install`
+ - **Start Command**: `npm start`
+ - **Plan**: Free
+
+5. **Add Environment Variables** (click "Advanced" β "Add Environment Variable"):
+
+ ```
+ NODE_ENV=production
+ PORT=5000
+ FINNHUB_API_KEY=your_finnhub_api_key_here
+ SUPABASE_URL=https://your-project-id.supabase.co
+ SUPABASE_KEY=your_supabase_service_role_key_here
+ ```
+
+6. Click **"Create Web Service"**
+
+7. **Wait for deployment** (usually 2-5 minutes)
+
+8. **Copy your backend URL** (e.g., `https://portfolio-tracker-backend-abc123.onrender.com`)
+
+---
+
+## π Step 2: Frontend Deployment (Vercel)
+
+### 2.1 Deploy Frontend to Vercel
+
+1. Go to [vercel.com](https://vercel.com) and sign up/login
+2. Click **"New Project"**
+3. Import your GitHub repository: `HackStyx/portfolio-tracker`
+4. Configure the project:
+
+ **Build Settings:**
+ - **Framework Preset**: Vite
+ - **Root Directory**: `./` (leave empty)
+ - **Build Command**: `npm run build`
+ - **Output Directory**: `dist`
+ - **Install Command**: `npm install`
+
+5. **Add Environment Variables** (click "Environment Variables"):
+
+ ```
+ VITE_SUPABASE_URL=https://your-project-id.supabase.co
+ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here
+ VITE_API_BASE_URL=https://your-render-backend-url.onrender.com/api
+ ```
+
+6. Click **"Deploy"**
+
+7. **Wait for deployment** (usually 1-3 minutes)
+
+8. **Copy your frontend URL** (e.g., `https://portfolio-tracker-abc123.vercel.app`)
+
+---
+
+## βοΈ Step 3: Configure Supabase Settings
+
+### 3.1 Update Site URL
+
+1. Go to your Supabase dashboard
+2. Navigate to **Authentication > Settings**
+3. Update **Site URL** to your Vercel frontend URL:
+ ```
+ https://your-frontend-url.vercel.app
+ ```
+
+### 3.2 Add Redirect URLs
+
+1. In the same Authentication settings
+2. Add these **Redirect URLs**:
+ ```
+ https://your-frontend-url.vercel.app/dashboard
+ https://your-frontend-url.vercel.app/
+ http://localhost:5173/dashboard
+ http://localhost:5173/
+ ```
+
+### 3.3 Enable Email Confirmation (Recommended)
+
+1. In Authentication settings
+2. Enable **"Enable email confirmations"**
+3. This ensures users verify their email before accessing the app
+
+---
+
+## π§ͺ Step 4: Test Your Deployment
+
+### 4.1 Test Backend
+
+1. Visit your backend URL: `https://your-backend-url.onrender.com/api/health`
+2. You should see a response indicating the server is running
+
+### 4.2 Test Frontend
+
+1. Visit your frontend URL: `https://your-frontend-url.vercel.app`
+2. Try creating a new account
+3. Test the login functionality
+4. Navigate through the dashboard
+
+### 4.3 Test API Integration
+
+1. Sign in to your app
+2. Try adding stocks to your watchlist
+3. Check if stock data loads correctly
+4. Test the news and calendar features
+
+---
+
+## π Troubleshooting
+
+### Common Issues
+
+#### Backend Issues
+
+1. **"Module not found" errors**
+ - Check that all dependencies are in `backend/package.json`
+ - Verify the build command is correct
+
+2. **"Environment variable not set" errors**
+ - Double-check all environment variables in Render dashboard
+ - Make sure there are no extra spaces
+
+3. **"Port already in use" errors**
+ - Render automatically sets the PORT environment variable
+ - Make sure your code uses `process.env.PORT`
+
+#### Frontend Issues
+
+1. **"Invalid API key" errors**
+ - Verify your Supabase anon key is correct
+ - Check that environment variables are set in Vercel
+
+2. **"CORS errors"**
+ - Backend should already be configured for CORS
+ - Check that your frontend URL is allowed
+
+3. **"Authentication not working"**
+ - Verify Supabase site URL and redirect URLs
+ - Check browser console for detailed errors
+
+#### Database Issues
+
+1. **"Table not found" errors**
+ - Run the database migrations in Supabase
+ - Check the SQL editor in Supabase dashboard
+
+2. **"RLS policy errors"**
+ - Verify Row Level Security policies are set up
+ - Check the authentication is working properly
+
+---
+
+## π Monitoring
+
+### Render Monitoring
+
+1. Go to your Render dashboard
+2. Click on your backend service
+3. Monitor logs, performance, and uptime
+
+### Vercel Monitoring
+
+1. Go to your Vercel dashboard
+2. Click on your project
+3. Monitor deployments, performance, and analytics
+
+### Supabase Monitoring
+
+1. Go to your Supabase dashboard
+2. Monitor database usage, API calls, and authentication
+
+---
+
+## π Updating Your Deployment
+
+### Backend Updates
+
+1. Push changes to your GitHub repository
+2. Render will automatically redeploy
+3. Monitor the deployment logs
+
+### Frontend Updates
+
+1. Push changes to your GitHub repository
+2. Vercel will automatically redeploy
+3. Monitor the deployment status
+
+---
+
+## π Success!
+
+Your Portfolio Tracker is now live!
+
+- **Frontend**: `https://your-frontend-url.vercel.app`
+- **Backend**: `https://your-backend-url.onrender.com`
+- **Database**: Supabase dashboard
+
+### Next Steps
+
+1. **Customize your app** - Update branding, colors, and features
+2. **Add more features** - Implement additional portfolio tracking features
+3. **Monitor performance** - Keep an eye on usage and performance
+4. **Scale up** - Upgrade plans as your user base grows
+
+---
+
+## π Support
+
+If you encounter issues:
+
+1. Check the troubleshooting section above
+2. Review the deployment logs in Render/Vercel
+3. Check the browser console for frontend errors
+4. Review Supabase logs for backend errors
+5. Check the GitHub repository for updates
+
+**Happy deploying! π**
\ No newline at end of file
From ea347ef84fff9b1c56bdec3434d325721b9fc5f9 Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 14:21:39 +0530
Subject: [PATCH 12/13] docs: update README with deployment status and trigger
redeploy
---
README.md | 215 +++++++-----------------------------------------------
1 file changed, 28 insertions(+), 187 deletions(-)
diff --git a/README.md b/README.md
index a3796a5..5c7f36e 100644
--- a/README.md
+++ b/README.md
@@ -1,198 +1,39 @@
-# Stock Portfolio Tracker π
+# Portfolio Tracker
-
+A modern portfolio tracking application built with React, Vite, and Supabase.
-[](https://portfolio-tracker-hackstyx.vercel.app)
-[](https://portfolio-tracker-backend-y7ne.onrender.com/api)
-[](https://reactjs.org/)
-[](https://vitejs.dev/)
-[](https://supabase.com/)
-[](https://tailwindcss.com/)
-[](https://nodejs.org/)
-[](https://finnhub.io/)
-[](https://hackstyx.github.io/portfolio-tracker/)
+## π Deployment Status
-
- A modern, responsive stock portfolio tracker built with React and Node.js
-
+- **Frontend**: [https://portfolio-tracker-hackstyx.vercel.app/](https://portfolio-tracker-hackstyx.vercel.app/)
+- **Backend**: [https://portfolio-tracker-backend-eh7r.onrender.com](https://portfolio-tracker-backend-eh7r.onrender.com)
+- **Database**: Supabase
-[Live Demo](https://portfolio-tracker-hackstyx.vercel.app) β’ [API Documentation](https://hackstyx.github.io/portfolio-tracker/) β’ [Report Bug](https://github.com/HackStyx/portfolio-tracker/issues)
+## Features
-> **Note**: Due to the free tier limitations of Render, the initial load of the demo might take 30-60 seconds. If no data appears, please refresh the page. If issues persist, click the logout button to reset the application state. The backend spins down after 15 minutes of inactivity and needs time to restart.
+- Real-time stock tracking
+- Portfolio management
+- Watchlist functionality
+- Market calendar
+- News integration
+- Dark/Light theme
+- Responsive design
-
+## Tech Stack
-
+- **Frontend**: React 18, Vite, Tailwind CSS, Tremor UI
+- **Backend**: Node.js, Express.js
+- **Database**: Supabase (PostgreSQL)
+- **Authentication**: Supabase Auth
+- **Stock Data**: Finnhub API
+- **Deployment**: Vercel (Frontend), Render (Backend)
-## β¨ Features
+## Getting Started
-- π **Portfolio Management** - Track your stocks with real-time updates
-- π **Watchlist** - Monitor potential investments
-- π **Dark/Light Mode** - Easy on the eyes, day and night
-- π± **Fully Responsive** - Perfect on desktop and mobile
-- π **Live Updates** - Real-time stock prices via Finnhub API
-- π **Price History** - Visualize stock performance with historical data
-- πΉ **Market Data** - Real-time quotes and market status tracking
-- π― **Price Targets** - Set and monitor stock price targets
+1. Clone the repository
+2. Install dependencies: `npm install`
+3. Set up environment variables (see `.env.example`)
+4. Start development server: `npm run dev`
-## π Quick Start
+## Documentation
-### Prerequisites
-
-- Node.js 14+
-- npm or yarn
-- Supabase account
-- Finnhub API Key (get one at [finnhub.io](https://finnhub.io/))
-
-### Frontend Setup
-
-1. Clone and install:
-```bash
-git clone https://github.com/HackStyx/portfolio-tracker.git
-cd portfolio-tracker
-npm install
-```
-
-2. Create `.env`:
-```env
-VITE_API_BASE_URL=http://localhost:5000/api
-```
-
-3. Start development server:
-```bash
-npm run dev
-```
-
-### Backend Setup
-
-1. Setup backend:
-```bash
-cd backend
-npm install
-```
-
-2. Configure `.env`:
-```env
-SUPABASE_URL=your_supabase_url
-SUPABASE_KEY=your_supabase_anon_key
-PORT=5000
-FINNHUB_API_KEY=your_finnhub_api_key
-```
-
-3. Set up Supabase:
- - Follow the instructions in `backend/SUPABASE_SETUP.md`
-
-4. Test connection and start:
-```bash
-npm run init-db
-npm start
-```
-
-## π API Documentation
-
-Our API is fully documented and available at [https://hackstyx.github.io/portfolio-tracker/](https://hackstyx.github.io/portfolio-tracker/)
-
-### Base URL
-```
-https://portfolio-tracker-backend-y7ne.onrender.com/api
-```
-
-### Key Endpoints
-
-#### Portfolio Management
-```http
-GET /stocks # Get all stocks in portfolio
-POST /stocks # Add new stock
-PUT /stocks/:id # Update existing stock
-DELETE /stocks/:id # Remove stock from portfolio
-```
-
-#### Watchlist Management
-```http
-GET /watchlist # Get watchlist items
-POST /watchlist # Add stock to watchlist
-DELETE /watchlist/:id # Remove from watchlist
-```
-
-#### Market Data
-```http
-GET /stocks/:ticker/quote # Get real-time stock quote
-GET /stocks/:ticker/history # Get historical price data
-GET /stocks/summary # Get portfolio analytics
-```
-
-For detailed API documentation including:
-- Request/Response formats
-- Authentication details
-- Rate limiting
-- Error handling
-- Code examples
-
-Visit our [API Documentation](https://hackstyx.github.io/portfolio-tracker/)
-
-## β‘οΈ Tech Stack
-
-### π¨ Frontend
-- **Framework**: [React](https://reactjs.org/) - A JavaScript library for building user interfaces
-- **Build Tool**: [Vite](https://vitejs.dev/) - Next generation frontend tooling
-- **Styling**:
- - [TailwindCSS](https://tailwindcss.com/) - Utility-first CSS framework
- - [Tremor](https://www.tremor.so/) - React library for dashboards and charts
- - [Framer Motion](https://www.framer.com/motion/) - Animation library
-- **State Management**: React Context API
-- **HTTP Client**: [Axios](https://axios-http.com/) - Promise based HTTP client
-
-### π Backend
-- **Runtime**: [Node.js](https://nodejs.org/) - JavaScript runtime
-- **Framework**: [Express](https://expressjs.com/) - Web framework for Node.js
-- **Database**: [Supabase](https://supabase.com/) - Open source Firebase alternative
-- **Market Data**: [Finnhub](https://finnhub.io/) - Real-time RESTful APIs for stocks
-- **API Documentation**: [Github Pages](https://hackstyx.github.io/portfolio-tracker/)
-
-### π DevOps & Infrastructure
-- **Frontend Hosting**: [Vercel](https://vercel.com/)
- - Zero-config deployments
- - Automatic HTTPS
- - Edge Network for optimal performance
-- **Backend Hosting**: [Render](https://render.com/)
- - Containerized deployment
- - Automatic scaling
- - Built-in monitoring
-- **Database Hosting**: [Supabase](https://supabase.com/)
- - PostgreSQL database
- - Automated backups
- - High availability
-
-### π¦ Additional Tools
-- **Version Control**: Git & GitHub
-- **Code Quality**: ESLint & Prettier
-- **API Testing**: Thunder Client
-- **Environment Variables**: dotenv
-- **Security**: CORS
-- **Real-time Monitoring**: UpTimeRobot
-
-## β οΈ Limitations
-
-- Uses simulated stock data when Finnhub API rate limit is reached
-- Single-user environment
-- Price updates every minute
-- Best viewed in modern browsers
-- Backend spins down after 15 minutes of inactivity and needs time to restart
-- Limited to 60 API calls per minute (Finnhub free tier)
-
-## π License
-
-This project is licensed under the MIT License.
-[](https://opensource.org/licenses/MIT)
-
-For more information, see the [LICENSE](LICENSE) file in the repository.
-
-
-
-
-
THANK YOU FOR VISITING β€οΈ
-
-
-
-
-
+See [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) for detailed deployment instructions.
From 6206f8833d4e7e410d956eb71e73abe9620b1e7e Mon Sep 17 00:00:00 2001
From: HackStyx <1bi22cs118@bit-bangalore.edu.in>
Date: Sun, 20 Jul 2025 20:18:21 +0530
Subject: [PATCH 13/13] fix: remove fallback to old backend URL, use only env
variable for API base URL
---
src/components/layout/Sidebar.jsx | 2 +-
src/components/modals/AddStockModal.jsx | 2 +-
src/services/api.js | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx
index 7366c8f..9fd63a6 100644
--- a/src/components/layout/Sidebar.jsx
+++ b/src/components/layout/Sidebar.jsx
@@ -16,7 +16,7 @@ import {
HiLogout
} from 'react-icons/hi';
-const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://portfolio-tracker-backend-y7ne.onrender.com/api';
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// Create axios instance with base URL
const api = axios.create({
diff --git a/src/components/modals/AddStockModal.jsx b/src/components/modals/AddStockModal.jsx
index 247a5be..6391d39 100644
--- a/src/components/modals/AddStockModal.jsx
+++ b/src/components/modals/AddStockModal.jsx
@@ -5,7 +5,7 @@ import { TextInput, NumberInput, Button } from '@tremor/react';
import axios from 'axios';
// Get API URL from environment variable, fallback to production URL if not set
-const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://portfolio-tracker-backend-y7ne.onrender.com/api';
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// Create axios instance with base URL
const api = axios.create({
diff --git a/src/services/api.js b/src/services/api.js
index 9ff4b6f..8f80f54 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import { supabase } from '../config/supabase';
-const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://portfolio-tracker-backend-y7ne.onrender.com/api';
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// Create axios instance with base URL
const api = axios.create({