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/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 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..5c7f36e 100644 --- a/README.md +++ b/README.md @@ -1,196 +1,39 @@ -# Stock Portfolio Tracker ๐Ÿ“ˆ +# Portfolio Tracker -
+A modern portfolio tracking application built with React, Vite, and Supabase. -[![Vercel](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white)](https://portfolio-tracker-hackstyx.vercel.app) -[![Render](https://img.shields.io/badge/Render-46E3B7?style=for-the-badge&logo=render&logoColor=white)](https://portfolio-tracker-backend-y7ne.onrender.com/api) -[![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://reactjs.org/) -[![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white)](https://vitejs.dev/) -[![MySQL](https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=white)](https://www.mysql.com/) -[![TailwindCSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)](https://tailwindcss.com/) -[![Node.js](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/) -[![Finnhub](https://img.shields.io/badge/Finnhub-1B1B1B?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMyA2aDJsMTUgMk0xNyA2djE0TTcgMTZoMTAiLz48L3N2Zz4=)](https://finnhub.io/) -[![API Docs](https://img.shields.io/badge/API_Docs-000000?style=for-the-badge&logo=github&logoColor=white)](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 -![Portfolio Dashboard](https://github.com/user-attachments/assets/c18f253c-2ac2-4df9-8025-c91858b74237) +## 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 -- MySQL -- 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 -DATABASE_URL=mysql://user:password@localhost:3306/portfolio_tracker -PORT=5000 -NODE_ENV=development -FINNHUB_API_KEY=your_finnhub_api_key -``` - -3. Run migrations and start: -```bash -npm run migrate -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**: [MySQL](https://www.mysql.com/) - Open-source relational database -- **ORM**: [Sequelize](https://sequelize.org/) - Modern TypeScript and Node.js ORM -- **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**: [Railway](https://railway.app/) - - Managed MySQL 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. -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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. 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 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/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/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/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 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/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/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}`); } 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 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 + + + +
+
+ +

Portfolio Tracker

+

Your journey to smart investing starts here

+
+ +
+
+

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 + + + +
+
+ +

Magic Link

+

Secure, passwordless access

+
+ +
+
+

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 + + + +
+
+ +

Password Reset

+

Secure access to your portfolio

+
+ +
+
+

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" } } 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/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/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} -
-
+
+ +
+ {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..9fd63a6 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, @@ -13,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({ @@ -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/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/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) => ( + + ))} +
+ + + ); +}; + +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/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/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; 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 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 [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 */} + + + {/* 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 */} +
+ + +
+ + {/* Error and Success Messages */} + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + +
+ {!isLogin && ( +
+ +
+ + +
+
+ )} + +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ + {isLogin && ( +
+ + +
+ )} + + +
+ +
+ {isLogin ? "Don't have an account? " : "Already have an account? "} + +
+
+
+
+
+ + {/* 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. +

+ +
+ + {/* Resource 2 */} + +
+ + + +
+

+ Video Tutorials +

+

+ Step-by-step video tutorials to help you master portfolio management. +

+ +
+ + {/* Resource 3 */} + +
+ + + +
+

+ FAQ & Support +

+

+ Find answers to common questions and get expert support when needed. +

+ +
+
+
+
+ + {/* 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 +
  • +
+ +
+ + {/* Pro Plan */} + +
+ Most Popular +
+

+ Pro +

+

+ $9 + + /month + +

+
    +
  • + + + + Unlimited stocks +
  • +
  • + + + + Advanced analytics +
  • +
  • + + + + Smart alerts +
  • +
  • + + + + Portfolio insights +
  • +
+ +
+ + {/* Enterprise Plan */} + +

+ Enterprise +

+

+ $29 + + /month + +

+
    +
  • + + + + Everything in Pro +
  • +
  • + + + + Team collaboration +
  • +
  • + + + + Priority support +
  • +
  • + + + + Custom integrations +
  • +
+ +
+
+
+
+ + {/* 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} + + +
+
+
+ ))} +
+
+
+ + {/* Footer */} +
+
+
+ {/* Company Info */} +
+
+
+ +
+ + PortfolioTracker + +
+

+ The ultimate platform for tracking your investments, analyzing performance, and making informed financial decisions. +

+ +
+ + {/* Quick Links */} +
+

+ Product +

+ +
+ + {/* Support */} + +
+ + {/* Bottom Bar */} +
+
+

+ ยฉ 2024 PortfolioTracker. All rights reserved. +

+
+ +
+ + + {/* 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} + + +
+
+
+ )} +
+ + 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 */} +
+ + +
+
+ +
+ )} +
+ ); +}; + +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 ( +
+
{error}
+
+ ); + } + + 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.symbol}
+ IPO +
+
+ {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} +

+ +
+ )} + + {/* Categories */} + {!isLoading && ( +
+
+ {categories.map((category) => ( + + ))} +
+
+ )} + + {/* 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 && ( + + )} + + {/* 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 diff --git a/src/pages/Portfolio.jsx b/src/pages/Portfolio.jsx index b1da257..2bd4317 100644 --- a/src/pages/Portfolio.jsx +++ b/src/pages/Portfolio.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; -import axios from 'axios'; import { motion } from 'framer-motion'; +import { createPortal } from 'react-dom'; +import api from '../services/api'; import { Card, Title, @@ -20,65 +21,60 @@ import { HiTrash, HiTrendingUp, HiTrendingDown, - HiX, HiCurrencyDollar, HiCollection, HiChartBar } from 'react-icons/hi'; -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 -}); -export default function Portfolio() { +const Portfolio = () => { 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 -
- -
- - {/* 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 */} +
+
+
+ + + + + + + + + + + - - - - - - - - - - {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 ( - - + - - - + - - - ); - })} - -
StockCurrent PriceHoldingsTotal ValueReturnActions
StockCurrent PriceHoldingsTotal ValueReturnActions
-
- {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

-
+ ) : ( + 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()} + +
+ + +
+ + +
+ + + ); + }) + )} + + +
-
+ - {/* Add Stock Modal */} - {showAddModal && ( -
+ {/* Modals using Portal */} + {showAddModal && createPortal( +
-
- Add New Stock +
+

+ Add New Stock +

-
-
- - setNewStock({...newStock, name: e.target.value})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> -
-
- - setNewStock({...newStock, ticker: e.target.value.toUpperCase()})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> + +
+ {/* Stock Search with Autocomplete */} +
+ +
+ + {isSearching && ( +
+
+
+ )}
-
- - setNewStock({...newStock, shares: Math.max(1, parseInt(e.target.value) || 0)})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> + + {/* Autocomplete Dropdown */} + {showDropdown && searchResults.length > 0 && ( +
+ {searchResults.map((stock, index) => ( + + ))} +
+ )} +
+ + {/* Selected Stock Display */} + {newStock.ticker && newStock.name && ( +
+
+
+
+ {newStock.ticker} +
+
+ {newStock.name} +
+
+
+ Selected +
+
+
+ )} + +
+ + setNewStock({ ...newStock, shares: Math.max(1, parseInt(e.target.value) || 0) })} + 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' + }`} + required + /> +
+
+ + setNewStock({ ...newStock, buy_price: Math.max(0.01, parseFloat(e.target.value) || 0) })} + 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 + />
-
- - setNewStock({...newStock, buy_price: Math.max(0.01, parseFloat(e.target.value) || 0)})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - />
-
- - +
-
+
, + document.body )} - {/* Edit Stock Modal */} - {showEditModal && editingStock && ( -
+ {showEditModal && editingStock && createPortal( +
-
- Edit Stock +
+

+ Edit Stock +

-
-
-
- - setEditingStock({...editingStock, name: e.target.value})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> +
+ +
+
+ + setEditingStock({ ...editingStock, name: 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' + }`} + required + />
-
- - setEditingStock({...editingStock, ticker: e.target.value.toUpperCase()})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> -
-
- - setEditingStock({...editingStock, shares: Math.max(1, parseInt(e.target.value) || 0)})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> -
-
- - setEditingStock({...editingStock, buy_price: Math.max(0.01, parseFloat(e.target.value) || 0)})} - className="w-full px-4 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500" - required - /> -
-
-
+
+ + setEditingStock({ ...editingStock, shares: Math.max(1, parseInt(e.target.value) || 0) })} + 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' + }`} + required + /> +
+
+ + setEditingStock({ ...editingStock, buy_price: Math.max(0.01, parseFloat(e.target.value) || 0) })} + 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' + }`} + required + /> +
+
+
+ - -
+ +
-
-
+ +
, + document.body )} - + ); -} \ No newline at end of file +}; + +export default Portfolio; \ No newline at end of file diff --git a/src/pages/StockDetail.jsx b/src/pages/StockDetail.jsx index 6335b41..ebbdfe3 100644 --- a/src/pages/StockDetail.jsx +++ b/src/pages/StockDetail.jsx @@ -1,94 +1,299 @@ -import { useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { Card, Title, Text, AreaChart, TabGroup, TabList, Tab, TabPanels, TabPanel, Metric, Flex, Badge } from '@tremor/react'; -import { ArrowTrendingUpIcon, ArrowTrendingDownIcon } from '@heroicons/react/24/solid'; - -const chartdata = [ - { date: "1D", price: 150.20 }, - { date: "2D", price: 151.40 }, - { date: "3D", price: 152.80 }, - { date: "4D", price: 149.90 }, - { date: "5D", price: 153.20 }, -]; - -const stockStats = { - marketCap: "2.5T", - peRatio: "25.6", - dividend: "0.88%", - volume: "52.4M", - avgVolume: "48.2M", - high52: "$182.94", - low52: "$124.17", -}; +import React, { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { Card, Title, Text, Badge, TabGroup, TabList, Tab, TabPanels, TabPanel } from '@tremor/react'; +import { + HiTrendingUp, + HiTrendingDown, + HiCurrencyDollar, + HiChartBar, + HiNewspaper, + HiUserGroup, + HiCalendar, + HiLightningBolt, + HiStar +} from 'react-icons/hi'; +import api from '../services/api'; + +const StockDetail = ({ ticker }) => { + const [stockData, setStockData] = useState(null); + const [financials, setFinancials] = useState(null); + const [recommendations, setRecommendations] = useState(null); + const [news, setNews] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light'); + + useEffect(() => { + if (ticker) { + fetchStockData(); + } + }, [ticker]); + + useEffect(() => { + const handleThemeChange = (e) => { + const newTheme = e.detail || localStorage.getItem('theme') || 'light'; + setTheme(newTheme); + }; + + window.addEventListener('themeChange', handleThemeChange); + document.addEventListener('themeChanged', handleThemeChange); + + return () => { + window.removeEventListener('themeChange', handleThemeChange); + document.removeEventListener('themeChanged', handleThemeChange); + }; + }, []); + + const fetchStockData = async () => { + try { + setLoading(true); + setError(null); + + // Fetch multiple data points in parallel + const [quoteRes, profileRes, metricsRes, newsRes] = await Promise.allSettled([ + api.get(`/stocks/${ticker}/quote`), + api.get(`/stocks/${ticker}/profile`), + api.get(`/stocks/${ticker}/metrics`), + api.get(`/stocks/${ticker}/news`) + ]); + + const data = { + quote: quoteRes.status === 'fulfilled' ? quoteRes.value.data : null, + profile: profileRes.status === 'fulfilled' ? profileRes.value.data : null, + metrics: metricsRes.status === 'fulfilled' ? metricsRes.value.data : null, + news: newsRes.status === 'fulfilled' ? newsRes.value.data : [] + }; + + setStockData(data); + setNews(data.news.slice(0, 5)); // Show latest 5 news items + } catch (error) { + console.error('Error fetching stock data:', error); + setError('Failed to fetch stock data'); + } finally { + setLoading(false); + } + }; + + const getPriceChangeColor = (change) => { + return change >= 0 ? 'text-green-500' : 'text-red-500'; + }; + + const getPriceChangeIcon = (change) => { + return change >= 0 ? : ; + }; + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + if (!stockData) { + return ( +
+
No data available
+
+ ); + } -export default function StockDetail() { - const { symbol } = useParams(); - const [selectedView, setSelectedView] = useState(0); + const { quote, profile, metrics } = stockData; return ( -
- - -
- Apple Inc. (AAPL) - NASDAQ -
-
- $150.20 - - - +2.5% - - Today - -
-
- - - - 1D - 1W - 1M - 3M - 1Y - ALL - - - - `$${number.toFixed(2)}`} - /> - - - -
- -
- - Key Statistics -
- {Object.entries(stockStats).map(([key, value]) => ( -
- {key} - {value} + + {/* Header */} +
+
+

+ {profile?.name || ticker} +

+

+ {profile?.exchange} โ€ข {profile?.finnhubIndustry} +

+
+ + {ticker} + +
+ + {/* Price and Change */} + {quote && ( + +
+
+
+ ${quote.c?.toFixed(2)}
- ))} +
+ {getPriceChangeIcon(quote.d)} + + {quote.d >= 0 ? '+' : ''}{quote.d?.toFixed(2)} ({quote.dp >= 0 ? '+' : ''}{quote.dp?.toFixed(2)}%) + +
+
+
+
+ Day Range: ${quote.l?.toFixed(2)} - ${quote.h?.toFixed(2)} +
+
+ Volume: {quote.v?.toLocaleString()} +
+
+ )} - - About - - Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, Mac, iPad, and Wearables, Home and Accessories. - - -
-
+ {/* Tabs */} + + + Overview + News + Analysts + Earnings + + + {/* Overview Tab */} + +
+ {/* Key Metrics */} + {metrics && ( + + Key Metrics +
+
+ P/E Ratio + {metrics.pe?.toFixed(2) || 'N/A'} +
+
+ P/B Ratio + {metrics.pb?.toFixed(2) || 'N/A'} +
+
+ Market Cap + + ${(metrics.marketCapitalization / 1e9)?.toFixed(2)}B + +
+
+
+ )} + + {/* Company Info */} + {profile && ( + + Company Info +
+
+ Sector + {profile.finnhubIndustry} +
+
+ Country + {profile.country} +
+
+ IPO Date + + {profile.ipo ? new Date(profile.ipo).toLocaleDateString() : 'N/A'} + +
+
+
+ )} + + {/* Quick Actions */} + + Quick Actions +
+ + + +
+
+
+
+ + {/* News Tab */} + +
+ {news.length > 0 ? ( + news.map((article, index) => ( + +
+
+

{article.headline}

+

{article.summary}

+
+ {article.source} + {new Date(article.datetime * 1000).toLocaleDateString()} +
+
+ + Read + +
+
+ )) + ) : ( +
+ No news available +
+ )} +
+
+ + {/* Analysts Tab */} + +
+ + Analyst Recommendations +
+ Analyst data will be available here +
+
+
+
+ + {/* Earnings Tab */} + +
+ + Earnings Calendar +
+ Earnings data will be available here +
+
+
+
+
+
+ ); -} \ No newline at end of file +}; + +export default StockDetail; \ No newline at end of file diff --git a/src/pages/Watchlist.jsx b/src/pages/Watchlist.jsx index e6a4cfa..7dc89cb 100644 --- a/src/pages/Watchlist.jsx +++ b/src/pages/Watchlist.jsx @@ -1,17 +1,11 @@ import React, { useState, useEffect } from 'react'; -import axios from 'axios'; import { motion } from 'framer-motion'; import { HiOutlineEye, HiOutlineTrash, HiSearch, HiPlus, HiTrendingUp, HiTrendingDown, HiOutlineBell, HiOutlineChartBar, HiOutlineClock, HiOutlinePencil } from 'react-icons/hi'; +import api from '../services/api'; import { Card, Text, Metric, Badge, ProgressBar } from '@tremor/react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; -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 Watchlist = () => { const [watchlist, setWatchlist] = useState([]); @@ -25,11 +19,17 @@ const Watchlist = () => { const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light'); + + // Autocomplete states + const [stockSearchQuery, setStockSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [showDropdown, setShowDropdown] = useState(false); useEffect(() => { console.log('Watchlist component mounted'); fetchWatchlist(); - syncPortfolioStocks(); + // Removed automatic sync - watchlist should only show manually added stocks const handleThemeChange = (e) => { const newTheme = e.detail || localStorage.getItem('theme') || 'light'; @@ -49,7 +49,7 @@ const Watchlist = () => { const fetchWatchlist = async () => { try { - console.log('Fetching watchlist from:', API_BASE_URL); + console.log('Fetching watchlist from API'); const response = await api.get('/watchlist'); console.log('Watchlist response:', response.data); setWatchlist(response.data); @@ -77,8 +77,7 @@ const Watchlist = () => { try { console.log('Adding stock:', newStock); await api.post('/watchlist', newStock); - setShowAddModal(false); - setNewStock({ name: '', ticker: '', target_price: '' }); + handleModalClose(); await fetchWatchlist(); } catch (error) { console.error('Error adding stock:', error); @@ -99,6 +98,40 @@ const Watchlist = () => { } }; + const clearAllWatchlist = async () => { + if (window.confirm('Are you sure you want to clear all stocks from your watchlist? This action cannot be undone.')) { + try { + console.log('Clearing all watchlist stocks...'); + + // Get current watchlist and delete each stock individually + const response = await api.get('/watchlist'); + const currentWatchlist = response.data; + + console.log(`Found ${currentWatchlist.length} stocks to remove`); + + // Delete each stock one by one + for (const stock of currentWatchlist) { + try { + console.log(`Removing ${stock.ticker} (ID: ${stock.id}) from watchlist...`); + await api.delete(`/watchlist/${stock.id}`); + console.log(`Successfully removed ${stock.ticker}`); + } catch (deleteError) { + console.error(`Failed to remove ${stock.ticker}:`, deleteError); + // Continue with other stocks + } + } + + // Refresh the watchlist + await fetchWatchlist(); + console.log('Watchlist clearing completed'); + + } catch (error) { + console.error('Error in clearAllWatchlist:', error); + setError('Failed to clear watchlist. Please try refreshing the page and deleting stocks individually.'); + } + } + }; + const handleUpdateStock = async (e) => { e.preventDefault(); try { @@ -136,6 +169,67 @@ const Watchlist = () => { setShowUpdateModal(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; + setStockSearchQuery(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 + }); + setStockSearchQuery(stock.symbol); + setShowDropdown(false); + setSearchResults([]); + }; + + const handleModalClose = () => { + setShowAddModal(false); + setNewStock({ name: '', ticker: '', target_price: '' }); + setStockSearchQuery(''); + setSearchResults([]); + setShowDropdown(false); + }; + const filteredWatchlist = watchlist.filter(stock => stock.name.toLowerCase().includes(searchQuery.toLowerCase()) || stock.ticker.toLowerCase().includes(searchQuery.toLowerCase()) @@ -184,8 +278,8 @@ const Watchlist = () => { {/* Header Section */}
@@ -222,6 +316,15 @@ const Watchlist = () => { Add to Watchlist + {watchlist.length > 0 && ( + + )}
@@ -229,15 +332,21 @@ const Watchlist = () => {
{/* Portfolio Overview Card */} + {/* Blurry background decoration */} +
+
@@ -247,8 +356,8 @@ const Watchlist = () => { {stats.totalStocks} Stocks
-
@@ -265,21 +374,28 @@ const Watchlist = () => {
Below Target {stats.belowTarget} +
{/* Target Progress Card */} + {/* Blurry background decoration */} +
+
@@ -294,8 +410,8 @@ const Watchlist = () => {
-
@@ -323,20 +439,27 @@ const Watchlist = () => {
)} +
{/* Value Analysis Card */} + {/* Blurry background decoration */} +
+
@@ -348,8 +471,8 @@ const Watchlist = () => {
-
@@ -370,6 +493,7 @@ const Watchlist = () => { {new Date().toLocaleTimeString()} +
@@ -377,9 +501,7 @@ const Watchlist = () => {
{error && ( - { }`} > {error} - +
)} {/* Watchlist Table */} - + {/* Blurry background decoration */} +
+
- - - + - - - - - + {loading ? ( - -
+
StockCurrent PriceTarget PriceDistance to TargetActions
@@ -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 = () => {
- +
+
{/* Add Stock Modal */} {showAddModal && ( -
+
-

+
+

Add Stock to Watchlist

+ +
+
+ {/* Stock Search with Autocomplete */} +
+ +
+ + {isSearching && ( +
+
+
+ )} +
+ + {/* Autocomplete Dropdown */} + {showDropdown && searchResults.length > 0 && ( +
+ {searchResults.map((stock, index) => ( + + ))} +
+ )} +
+ + {/* Selected Stock Display */} + {newStock.ticker && newStock.name && ( +
+
+
+
+ {newStock.ticker} +
+
+ {newStock.name} +
+
+
+ Selected +
+
+
+ )} + +
+ + 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 + /> +
+
+
+ + +
+
+ +

+ )} + + {/* Update Stock Modal */} + {showUpdateModal && selectedStock && ( +
+
+
+

+ Update Target Price +

+ +
+
-
-
-
@@ -591,40 +913,41 @@ const Watchlist = () => {
- +
)} {/* 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)}% - -
- - )} -
-
-
- -
- - -
- -

- )} - - {/* Update Stock Modal */} - {showUpdateModal && selectedStock && ( -
- -

- Update Target Price -

-
-
-
- -

- {selectedStock.name} ({selectedStock.ticker}) -

-
-
-
-
- - 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)} +

-
- -
- -
)}
diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..8f80f54 --- /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; + +// 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