From 66aa9b133525b57e34d23c07090dbaaca5ebedca Mon Sep 17 00:00:00 2001 From: michael carrillo <143856362+michaelangell@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:37:35 -0400 Subject: [PATCH 1/5] Add files via upload --- HypeTrade307/src/assets/area_Graph.tsx | 96 +++++++++++++-------- HypeTrade307/src/assets/basic_Graph.tsx | 106 +++++++++++++++--------- 2 files changed, 129 insertions(+), 73 deletions(-) diff --git a/HypeTrade307/src/assets/area_Graph.tsx b/HypeTrade307/src/assets/area_Graph.tsx index 6a8c618c..b99cf5e1 100644 --- a/HypeTrade307/src/assets/area_Graph.tsx +++ b/HypeTrade307/src/assets/area_Graph.tsx @@ -2,63 +2,92 @@ import React, { useEffect, useState } from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; - +import { TooltipProps } from "recharts"; interface DataPoint { date: string; value: number; } -const fetchData = async (): Promise => { - try { - const response = await fetch("/data/.json"); - return await response.json(); - } catch (error) { - console.error("Error fetching data:", error); - return []; +interface AreaGraphProps { + file: string; +} +let check = 0; + +const CustomTooltip = ({ active, payload }: TooltipProps) => { + if (active && payload && payload.length > 0) { + const data = payload[0].payload; + + console.log("Hovered data:", data); // Debug: check what keys exist + + return ( +
+

Value: {data.value}

+

Date: {data.date}

+
+ ); } -}; + return null; + }; + -// Function to calculate the gradient offset based on current data -const calculateGradientOffset = (data: DataPoint[]) => { - if (data.length === 0) return 1; // default value when there's no data - const dataMax = Math.max(...data.map((i) => i.value)); - const dataMin = Math.min(...data.map((i) => i.value)); - if (dataMax <= 0) { - return 0; - } - if (dataMin >= 0) { - return 1; - } - return dataMax / (dataMax - dataMin); -}; - -const Example: React.FC = () => { +const AreaGraph: React.FC = ({ file }) => { const [chartData, setChartData] = useState([]); - const [lastN, setLastN] = useState(3); // default to last 3 items + const [lastN, setLastN] = useState(3); + + + + const filePath = `seniment_value/${file}_random.json`; + + const fetchData = async (filePath: string): Promise => { + try { + const response = await fetch(filePath); + if (!response.ok) throw new Error("File not found"); + const data = await response.json(); + check = 1; + return data; + } catch (error) { + console.error("Error fetching data:", error); + check = 0; + return []; + } + }; useEffect(() => { - fetchData().then(setChartData); - }, []); + fetchData(filePath).then(setChartData); + }, [filePath]); const handleFetchData = async () => { - const fetchedData = await fetchData(); - // Ensure lastN isn't greater than data length. + const fetchedData = await fetchData(filePath); const lastNItems = fetchedData.slice(-lastN); setChartData(lastNItems); }; const handleResetData = async () => { - const fetchedData = await fetchData(); + const fetchedData = await fetchData(filePath); setChartData(fetchedData); }; + const calculateGradientOffset = (data: DataPoint[]) => { + if (data.length === 0) return 1; + const dataMax = Math.max(...data.map((i) => i.value)); + const dataMin = Math.min(...data.map((i) => i.value)); + if (dataMax <= 0) return 0; + if (dataMin >= 0) return 1; + return dataMax / (dataMax - dataMin); + }; + const off = calculateGradientOffset(chartData); + + if (check == 0) { + return

no seniment data found {file} is not a top 20 stock

; + } return (
+

hiiiii {file} hhhhhhhhh

{ - + - + } /> - {/* Linear gradient to split between green (positive) and red (negative) */} @@ -103,4 +131,4 @@ const Example: React.FC = () => { ); }; -export default Example; +export default AreaGraph; diff --git a/HypeTrade307/src/assets/basic_Graph.tsx b/HypeTrade307/src/assets/basic_Graph.tsx index 338f4761..231ec465 100644 --- a/HypeTrade307/src/assets/basic_Graph.tsx +++ b/HypeTrade307/src/assets/basic_Graph.tsx @@ -1,39 +1,66 @@ -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid, } from "recharts"; - +import { TooltipProps } from "recharts"; // Define TypeScript type for data interface DataPoint { date: string; value: number; } -// Function to fetch data from JSON file -const fetchData = async (): Promise => { - try { - const response = await fetch("/data.json"); +interface MarketValueProps { + file: string; // Prop to dynamically select the JSON file +} + - return await response.json(); - } catch (error) { - console.error("Error fetching data:", error); - return []; +const CustomTooltip = ({ active, payload }: TooltipProps) => { + if (active && payload && payload.length > 0) { + const data = payload[0].payload; + + console.log("Hovered data:", data); // Debug: check what keys exist + + return ( +
+

Value: {data.value}

+

Date: {data.date}

+
+ ); } -}; + return null; + }; + -const MyGraph: React.FC = () => { +let check = 0; +const MarketValue: React.FC = ({ file }) => { const [data, setChartData] = useState([]); - const [lastN, setLastN] = useState(3); // default to last 3 items + const [lastN, setLastN] = useState(3); // Default to last 3 items + + // Construct the file path dynamically based on the passed file prop + const filePath = `/market_value/${file}_history.json`; + + // Function to fetch data from JSON file + const fetchData = async (): Promise => { + try { + const response = await fetch(filePath); + check = 1; + return await response.json(); + + } catch (error) { + console.error("Error fetching data:", error); + check = 0; + return []; + } + }; + useEffect(() => { fetchData().then(setChartData); + }, [filePath]); // Re-fetch data when filePath changes - }, []); - - // Function to fetch data on button click + // Function to fetch last N data on button click const handleFetchData = async () => { const fetchedData = await fetchData(); - // Ensure lastN isn't greater than data length. - const lastNItems = fetchedData.slice(-lastN); + const lastNItems = fetchedData.slice(-lastN); // Get last N items setChartData(lastNItems); }; @@ -41,6 +68,9 @@ const MyGraph: React.FC = () => { const fetchedData = await fetchData(); setChartData(fetchedData); }; + if (check == 0) { + return

no market data found {file} is not a top 20 stock

; + } return (
@@ -62,28 +92,26 @@ const MyGraph: React.FC = () => { Reset Data
- - - - - - - - (point.value ? point : { ...point, value: null }))} - /> - - - + + + + + } /> + + + (point.value ? point : { ...point, value: null }))} + /> + +
); }; -export default MyGraph; +export default MarketValue; From 7cff1b6baac7c305828ded1f07dc9cc217ec3291 Mon Sep 17 00:00:00 2001 From: michael carrillo <143856362+michaelangell@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:40:12 -0400 Subject: [PATCH 2/5] Update ViewStock.tsx --- HypeTrade307/src/pages/ViewStock.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/HypeTrade307/src/pages/ViewStock.tsx b/HypeTrade307/src/pages/ViewStock.tsx index c32900a3..932a3481 100644 --- a/HypeTrade307/src/pages/ViewStock.tsx +++ b/HypeTrade307/src/pages/ViewStock.tsx @@ -5,7 +5,8 @@ import Navbar from "../components/NavbarSection/Navbar.tsx"; import CssBaseline from "@mui/material/CssBaseline"; import AppTheme from "../components/shared-theme/AppTheme.tsx"; import axios from "axios"; - +import MarketValue from "../assets/basic_Graph.tsx"; +import AreaGraph from "@/assets/area_Graph.tsx"; // Define API base URL to fetch data in mySQL const API_BASE_URL = "http://127.0.0.1:8000"; @@ -939,6 +940,8 @@ function ViewStock(props: { disableCustomTheme?: boolean }) { No sentiment data available
)} + +
@@ -949,4 +952,4 @@ function ViewStock(props: { disableCustomTheme?: boolean }) { ) } -export default ViewStock; \ No newline at end of file +export default ViewStock; From 54b6932b9ae1c50ae9d6455eaa83e1d8d30cda2a Mon Sep 17 00:00:00 2001 From: Owen Metzger Date: Sat, 12 Apr 2025 22:24:29 -0400 Subject: [PATCH 3/5] Added the ViewStockPage that allows you to view a stock by navigating to the portfolio page and clicking on the stock. --- HypeTrade307/src/App.tsx | 2 + HypeTrade307/src/pages/Portfoilos_viewer.tsx | 31 +++- HypeTrade307/src/pages/ViewStockPage.tsx | 155 +++++++++++++++++++ 3 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 HypeTrade307/src/pages/ViewStockPage.tsx diff --git a/HypeTrade307/src/App.tsx b/HypeTrade307/src/App.tsx index 8d7be08c..d7ff36db 100644 --- a/HypeTrade307/src/App.tsx +++ b/HypeTrade307/src/App.tsx @@ -17,6 +17,7 @@ import Thread_page from "./pages/thread_viewer.tsx"; import Post_page from "./pages/post_viewer.tsx"; import PortfolioPage from './pages/Portfoilos_viewer.tsx'; import Specific_Stock from './pages/specific_stock_request.tsx' +import ViewStockPage from "@/pages/ViewStockPage.tsx"; function App() { return ( @@ -36,6 +37,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/HypeTrade307/src/pages/Portfoilos_viewer.tsx b/HypeTrade307/src/pages/Portfoilos_viewer.tsx index aefb3067..59f3a1ad 100644 --- a/HypeTrade307/src/pages/Portfoilos_viewer.tsx +++ b/HypeTrade307/src/pages/Portfoilos_viewer.tsx @@ -55,6 +55,13 @@ export default function PortfolioPage() { fetchPortfolio(); }, [id]); + //1.5 Add the id to local storage as a navigation crumb when you go to view a stock + useEffect(() => { + if (id) { + localStorage.setItem("currentPortfolioId", id); + } + }, [id]); + // 2. Fetch all stocks for the typeahead useEffect(() => { async function fetchStocks() { @@ -209,14 +216,26 @@ export default function PortfolioPage() { {/* LIST STOCKS IN PORTFOLIO */} -
    +
      {portfolio?.stocks && portfolio.stocks.length > 0 ? ( portfolio.stocks.map((st, index) => ( -
    • - {st.ticker} - {st.stock_name} - +
    • +
      navigate(`/stocks/${st.ticker}`)} + style={{ + cursor: "pointer", + color: "#007bff", + flex: 1, + padding: "4px", + borderRadius: "4px", + transition: "background 0.2s" + }} + onMouseEnter={(e) => (e.currentTarget.style.background = "#f0f0f0")} + onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")} + > + {st.ticker} - {st.stock_name} +
      +
    • )) ) : ( diff --git a/HypeTrade307/src/pages/ViewStockPage.tsx b/HypeTrade307/src/pages/ViewStockPage.tsx new file mode 100644 index 00000000..8980daf5 --- /dev/null +++ b/HypeTrade307/src/pages/ViewStockPage.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../stocks.css"; + +const API_BASE_URL = "http://127.0.0.1:8000"; + +export default function ViewStockPopupPage() { + const { tkr } = useParams(); + const navigate = useNavigate(); + const [stock, setStock] = useState(null); + const [sentimentData, setSentimentData] = useState([]); + const [loading, setLoading] = useState(true); + const [timeButton, setTimeButton] = useState<"Day" | "Week" | "Month">("Day"); + + const portfolioId = localStorage.getItem("currentPortfolioId"); + + useEffect(() => { + const fetchStock = async () => { + try { + const token = localStorage.getItem("token"); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + + const stockRes = await axios.get(`${API_BASE_URL}/stocks/`, { headers }); + const match = stockRes.data.find((s: any) => s.ticker === tkr?.toUpperCase()); + + if (match) { + setStock(match); + } + } catch (e) { + console.error("Error fetching stock", e); + } finally { + setLoading(false); + } + }; + fetchStock(); + }, [tkr]); + + useEffect(() => { + const fetchSentiment = async () => { + if (!stock) return; + + try { + const token = localStorage.getItem("token"); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + const interval = timeButton === "Day" ? 1 : timeButton === "Week" ? 7 : 30; + + const res = await axios.get( + `${API_BASE_URL}/stocks/sentiment/${stock.stock_id}?interval=${interval}`, + { headers } + ); + + setSentimentData(res.data); + } catch (e) { + console.error("Error fetching sentiment", e); + } + }; + + fetchSentiment(); + }, [stock, timeButton]); + + const getSentimentColor = (sentiment: number | undefined): string => { + if (sentiment === undefined) return "#888888"; + if (sentiment > 5) return "#4CAF50"; + if (sentiment > 0) return "#8BC34A"; + if (sentiment === 0) return "#9E9E9E"; + if (sentiment > -5) return "#FF9800"; + return "#F44336"; + }; + + const getGraphTitle = () => { + if (!stock) return "No stock selected"; + return `${stock.stock_name}'s performance over the last ${timeButton}`; + }; + + if (loading) return
      Loading...
      ; + if (!stock) return
      Stock not found.
      ; + + return ( + <> + {portfolioId && ( + + )} + +
      +
      + +
      +

      {stock.stock_name} ({stock.ticker})

      +
      ${stock.value?.toLocaleString()}
      +
      + +
      +
      + Analysis Mode + {stock.analysis_mode} +
      +
      + Sentiment + + {stock.sentiment} + +
      +
      + +
      +
      Time Period
      +
      + {["Day", "Week", "Month"].map((period) => ( + + ))} +
      +
      + +
      +

      {getGraphTitle()}

      + {sentimentData.length > 0 ? ( +
      + Sentiment data: {sentimentData.join(", ")} +
      + ) : ( +
      No sentiment data available
      + )} +
      +
      +
      + + ); +} From 93fc054271a7186110f8c22faf79b5a881f9d3bd Mon Sep 17 00:00:00 2001 From: Owen Metzger Date: Sun, 13 Apr 2025 15:41:32 -0400 Subject: [PATCH 4/5] Added a bunch of tutorials for new pages --- HypeTrade307/src/App.tsx | 3 +- .../src/components/NavbarSection/Navbar.tsx | 4 +- HypeTrade307/src/pages/HelpPage.css | 19 + HypeTrade307/src/pages/HelpPage.tsx | 59 ++ HypeTrade307/src/pages/Portfoilos_viewer.tsx | 107 ++- HypeTrade307/src/pages/Profile_Page.tsx | 244 ++++-- HypeTrade307/src/pages/ViewStock.tsx | 55 +- HypeTrade307/src/pages/forum_page.css | 13 +- HypeTrade307/src/pages/forum_page.tsx | 109 ++- HypeTrade307/src/pages/post_viewer.tsx | 760 +++++++++--------- HypeTrade307/src/pages/thread_viewer.tsx | 100 ++- 11 files changed, 912 insertions(+), 561 deletions(-) create mode 100644 HypeTrade307/src/pages/HelpPage.css create mode 100644 HypeTrade307/src/pages/HelpPage.tsx diff --git a/HypeTrade307/src/App.tsx b/HypeTrade307/src/App.tsx index d7ff36db..5f2912df 100644 --- a/HypeTrade307/src/App.tsx +++ b/HypeTrade307/src/App.tsx @@ -18,6 +18,7 @@ import Post_page from "./pages/post_viewer.tsx"; import PortfolioPage from './pages/Portfoilos_viewer.tsx'; import Specific_Stock from './pages/specific_stock_request.tsx' import ViewStockPage from "@/pages/ViewStockPage.tsx"; +import HelpPage from "@/pages/HelpPage.tsx"; function App() { return ( @@ -38,7 +39,7 @@ function App() { } /> } /> } /> - + } /> } /> diff --git a/HypeTrade307/src/components/NavbarSection/Navbar.tsx b/HypeTrade307/src/components/NavbarSection/Navbar.tsx index 643ce0b1..0b1e8fc1 100644 --- a/HypeTrade307/src/components/NavbarSection/Navbar.tsx +++ b/HypeTrade307/src/components/NavbarSection/Navbar.tsx @@ -15,7 +15,7 @@ import ColorModeIconDropdown from '../shared-theme/ColorModeIconDropdown.tsx'; import Sitemark from '../SitemarkIcon'; import type {} from '@mui/material/themeCssVarsAugmentation'; -const pages = ['Home', 'ViewStock', 'Portfolio', 'Profile', 'Search', 'Friends', 'Chat', 'Forum']; +const pages = ['Home', 'ViewStock', 'Portfolio', 'Profile', 'Search', 'Friends', 'Chat', 'Forum', 'Help']; const StyledToolbar = styled(Toolbar)(({ theme }) => ({ display: 'flex', @@ -65,6 +65,8 @@ const Navbar = () => { window.location.href = `/forum`; } else if (pageName === "Chat") { window.location.href = `/chat`; + } else if (pageName === "Help") { + window.location.href = `/help`; } else { try { // TODO: Update once we have login working with database diff --git a/HypeTrade307/src/pages/HelpPage.css b/HypeTrade307/src/pages/HelpPage.css new file mode 100644 index 00000000..ea7ab39a --- /dev/null +++ b/HypeTrade307/src/pages/HelpPage.css @@ -0,0 +1,19 @@ +.help-container { + margin-top: 20px; + padding: 16px; +} + +.help-header { + margin-top: 10px; + font-size: 5rem; + font-weight: bold; +} + +.help-list { + padding-left: 20px; + margin-bottom: 16px; +} + +.help-list li { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/HypeTrade307/src/pages/HelpPage.tsx b/HypeTrade307/src/pages/HelpPage.tsx new file mode 100644 index 00000000..d2c0bfa5 --- /dev/null +++ b/HypeTrade307/src/pages/HelpPage.tsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from 'react'; +import { CssBaseline, Container, Typography, Switch, FormControlLabel, Box } from '@mui/material'; +import Navbar from "../components/NavbarSection/Navbar.tsx"; +import AppTheme from "../components/shared-theme/AppTheme.tsx"; +import "./HelpPage.css"; // Import CSS file + +const HelpPage = () => { + const [tutorialMode, setTutorialMode] = useState( + JSON.parse(localStorage.getItem('tutorialMode') || 'false') + ); + + useEffect(() => { + console.log("useEffect ran!"); + localStorage.setItem('tutorialMode', JSON.stringify(tutorialMode)); + console.log("Tutorial Mode Updated:", tutorialMode); // Debugging log + }, [tutorialMode]); + + const handleToggle = () => { + setTutorialMode((prev) => !prev); + }; + + return ( + + + + + + Help & Getting Started + + + + + Welcome to HypeTrade! This platform uses sentiment analysis from stock discussions + to evaluate market sentiment and help predict stock movements. By analyzing + large-scale social media and news data, HypeTrade provides insights into whether + a stock may rise or fall based on public perception. + + + To use HypeTrade effectively: + + + Search for a stock to view sentiment analysis. + Analyze the sentiment trend over time. + Use insights to complement your research and make informed investment decisions. + + } + label="Enable Tutorial Mode" + /> + + Disclaimer: HypeTrade provides sentiment-based insights and does not constitute financial advice. + We are not responsible for any trading losses incurred while using this software. + + + + ); +}; + +export default HelpPage; \ No newline at end of file diff --git a/HypeTrade307/src/pages/Portfoilos_viewer.tsx b/HypeTrade307/src/pages/Portfoilos_viewer.tsx index 59f3a1ad..7a6b8720 100644 --- a/HypeTrade307/src/pages/Portfoilos_viewer.tsx +++ b/HypeTrade307/src/pages/Portfoilos_viewer.tsx @@ -1,7 +1,6 @@ // portfolios_viewer.tsx -import React from "react"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import axios from "axios"; import Page_Not_found from "./Page_Not_found"; @@ -15,7 +14,7 @@ interface StockBase { interface Portfolio { portfolio_id: number; name: string; - stocks: StockBase[]; // an array of objects + stocks: StockBase[]; } export default function PortfolioPage() { @@ -26,12 +25,35 @@ export default function PortfolioPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - // For typeahead const [availableStocks, setAvailableStocks] = useState([]); const [stockSearch, setStockSearch] = useState(""); const [filteredStocks, setFilteredStocks] = useState([]); - // 1. Fetch the portfolio + const [tutorialOpen, setTutorialOpen] = useState(false); + const [step, setStep] = useState(0); + + const tutorialSteps = [ + { title: "View Portfolio", description: "This page shows all the stocks in your selected portfolio." }, + { title: "Add Stocks", description: "Use the search box to find stocks and add them to your portfolio." }, + { title: "Import CSV", description: "You can also import a CSV file to bulk add stocks." }, + { title: "Click to View", description: "Click on a stock to view all the stock details (similar to the ViewStock page)." }, + { title: "Done!", description: "You're ready to manage your portfolio!" } + ]; + + const nextStep = () => { + if (step < tutorialSteps.length - 1) { + setStep(step + 1); + } else { + setTutorialOpen(false); + } + }; + + useEffect(() => { + if (localStorage.getItem("tutorialMode") === "true") { + setTutorialOpen(true); + } + }, []); + useEffect(() => { async function fetchPortfolio() { try { @@ -55,14 +77,12 @@ export default function PortfolioPage() { fetchPortfolio(); }, [id]); - //1.5 Add the id to local storage as a navigation crumb when you go to view a stock useEffect(() => { if (id) { localStorage.setItem("currentPortfolioId", id); } }, [id]); - // 2. Fetch all stocks for the typeahead useEffect(() => { async function fetchStocks() { try { @@ -80,7 +100,6 @@ export default function PortfolioPage() { fetchStocks(); }, []); - // 3. Filter for typeahead whenever stockSearch changes useEffect(() => { if (!stockSearch) { setFilteredStocks([]); @@ -94,7 +113,6 @@ export default function PortfolioPage() { setFilteredStocks(match); }, [stockSearch, availableStocks]); - // 4. Add Stock to Portfolio async function addStockToPortfolio(stock_id: number) { try { const token = localStorage.getItem("token"); @@ -103,9 +121,7 @@ export default function PortfolioPage() { const response = await axios.post( `http://127.0.0.1:8000/portfolios/${portfolio.portfolio_id}/stocks/${stock_id}`, {}, - { - headers: { Authorization: `Bearer ${token}` } - } + { headers: { Authorization: `Bearer ${token}` } } ); setPortfolio(response.data); setStockSearch(""); @@ -115,7 +131,6 @@ export default function PortfolioPage() { } } - // 5. Remove Stock async function removeStock(stock_id: number) { try { const token = localStorage.getItem("token"); @@ -131,13 +146,9 @@ export default function PortfolioPage() { } } - // 6. Import existing portfolio as csv file upload. Update the current portfolio to include csv data - async function uploadFile(file) { - - // if (!file) return; + async function uploadFile(file: File) { if (file) { try { - const formData = new FormData(); formData.append("file", file); @@ -145,46 +156,41 @@ export default function PortfolioPage() { if (!token || !portfolio) return; const response = await axios.post( - `http://127.0.0.1:8000/portfolios/${portfolio.portfolio_id}/upload`, formData, - { - headers: { - "Content-Type": "multipart/form-data", - Authorization: `Bearer ${token}` - } + `http://127.0.0.1:8000/portfolios/${portfolio.portfolio_id}/upload`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + Authorization: `Bearer ${token}` } + } ); - setPortfolio(response.data); - } catch (error) { console.error("Error updating portfolio", error); } } } - - async function importPortfolioFromCSV(event) { - - const inputFile = event.target.files[0]; - + async function importPortfolioFromCSV(event: React.ChangeEvent) { + const inputFile = event.target.files?.[0]; try { - await uploadFile(inputFile); - + if (inputFile) { + await uploadFile(inputFile); + } } catch (error) { console.log(error); } } - if (loading) return
      Loading...
      ; if (error) return ; return ( -
      +

      Portfolio: {portfolio?.name}

      - {/* STOCK SEARCH INPUT */}
      - {/* LIST STOCKS IN PORTFOLIO */}
        {portfolio?.stocks && portfolio.stocks.length > 0 ? ( portfolio.stocks.map((st, index) => ( @@ -243,14 +248,38 @@ export default function PortfolioPage() { )}
      - {/* Upload File Input */}
      -
      importPortfolioFromCSV(event)}> +
      + {tutorialOpen && ( +
      +

      {tutorialSteps[step].title}

      +

      {tutorialSteps[step].description}

      + +
      + )}
      ); } diff --git a/HypeTrade307/src/pages/Profile_Page.tsx b/HypeTrade307/src/pages/Profile_Page.tsx index 578c9727..6adadd84 100644 --- a/HypeTrade307/src/pages/Profile_Page.tsx +++ b/HypeTrade307/src/pages/Profile_Page.tsx @@ -1,92 +1,168 @@ - import { useState } from "react"; - import Home_page_button from "./Home_page_button.tsx"; - import Navbar from "../components/NavbarSection/Navbar.tsx"; - import CssBaseline from "@mui/material/CssBaseline"; - import AppTheme from "../components/shared-theme/AppTheme.tsx"; - import FriendRemove from "./FriendRemove.tsx"; - import Portfolios_creation from "./Portfolios_creation.tsx"; - import SettingsMenu from "./SettingsMenu.tsx"; - import PortfolioViewer from "./Portfoilos_viewer.tsx"; - import "./Profile_Page.css"; - - function Profile_page(props: { disableCustomTheme?: boolean }) { - const [showPortfolioViewer, setShowPortfolioViewer] = useState(false); - const [showSettings, setShowSettings] = useState(false); - - // Toggle between portfolio creation and viewer - const togglePortfolioViewer = () => { - setShowPortfolioViewer(!showPortfolioViewer); - }; - - // Toggle settings menu visibility - const toggleSettings = () => { - setShowSettings(!showSettings); - }; - - // Close settings when clicking outside - const handleClickOff = (e: React.MouseEvent) => { - if ((e.target as HTMLElement).classList.contains("settings-overlay")) { - setShowSettings(false); - } - }; - - return ( - <> - - - - -
      - {/* Top Navigation */} -
      - -

      My Profile

      - +import { useState } from "react"; +import Home_page_button from "./Home_page_button.tsx"; +import Navbar from "../components/NavbarSection/Navbar.tsx"; +import CssBaseline from "@mui/material/CssBaseline"; +import AppTheme from "../components/shared-theme/AppTheme.tsx"; +import FriendRemove from "./FriendRemove.tsx"; +import Portfolios_creation from "./Portfolios_creation.tsx"; +import SettingsMenu from "./SettingsMenu.tsx"; +import PortfolioViewer from "./Portfoilos_viewer.tsx"; +import "./Profile_Page.css"; + +function Profile_page(props: { disableCustomTheme?: boolean }) { + const [showPortfolioViewer, setShowPortfolioViewer] = useState(false); + const [showSettings, setShowSettings] = useState(false); + + // Tutorial Left + const [showTutorialLeft, setShowTutorialLeft] = useState(true); + const [stepLeft, setStepLeft] = useState(0); + const tutorialStepsLeft = [ + { title: "Friends List", description: "Here is the friends list. You can add friends by going to the 'Search' page" }, + { title: "Friend Actions", description: "You can remove friends, view their profiles, and view there portfolios (if you have access)" }, + { title: "You're all set", description: "You can now interact with the friends list" }, + ]; + + // Tutorial Right + const [showTutorialRight, setShowTutorialRight] = useState(true); + const [stepRight, setStepRight] = useState(0); + const tutorialStepsRight = [ + { title: "Managing Portfolios", description: "Here is the portfolio page. You can add portfolios and access them." }, + { title: "Adding a portfolio", description: "Type the name of the portfolio you wish to add, then click 'Create Portfolio' to add it to the list of portfolios." }, + { title: "Removing a portfolio", description: "Click on the trash icon to remove a portfolio from your list." }, + { title: "Adding stocks to a portfolio", description: "Click on a portfolio. This will take you to a page where you can add stocks to them." }, + { title: "You're all set", description: "Now you can manage your portfolios!" }, + ]; + + const nextStepLeft = () => { + if (stepLeft < tutorialStepsLeft.length - 1) { + setStepLeft(stepLeft + 1); + } else { + setShowTutorialLeft(false); + } + }; + + const nextStepRight = () => { + if (stepRight < tutorialStepsRight.length - 1) { + setStepRight(stepRight + 1); + } else { + setShowTutorialRight(false); + } + }; + + const togglePortfolioViewer = () => { + setShowPortfolioViewer(!showPortfolioViewer); + }; + + const toggleSettings = () => { + setShowSettings(!showSettings); + }; + + const handleClickOff = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).classList.contains("settings-overlay")) { + setShowSettings(false); + } + }; + + return ( + <> + + + + +
      +
      + +

      My Profile

      + +
      + +
      +
      +

      Friends

      +
      - {/* Main Content Area */} -
      - {/* Left Sidebar - Friends List */} -
      -

      Friends

      - -
      +
      + {showPortfolioViewer ? ( + + ) : ( + <> +
      +

      My Portfolios

      + +
      + + + )} +
      +
      - {/* Main Content - Portfolios */} -
      - {showPortfolioViewer ? ( - - ) : ( - <> -
      -

      My Portfolios

      - -
      - - - )} + {showSettings && ( +
      +
      + +
      + )} +
      - {/* HUD for settings */} - {showSettings && ( -
      -
      - - -
      -
      - )} + {/* Left Tutorial Popup */} + {showTutorialLeft && ( +
      +

      {tutorialStepsLeft[stepLeft].title}

      +

      {tutorialStepsLeft[stepLeft].description}

      + +
      + )} + + {/* Right Tutorial Popup */} + {showTutorialRight && ( +
      +

      {tutorialStepsRight[stepRight].title}

      +

      {tutorialStepsRight[stepRight].description}

      +
      - - - ); - } + )} + + + ); +} - export default Profile_page; \ No newline at end of file +export default Profile_page; diff --git a/HypeTrade307/src/pages/ViewStock.tsx b/HypeTrade307/src/pages/ViewStock.tsx index 932a3481..c569e2ca 100644 --- a/HypeTrade307/src/pages/ViewStock.tsx +++ b/HypeTrade307/src/pages/ViewStock.tsx @@ -63,7 +63,25 @@ function ViewStock(props: { disableCustomTheme?: boolean }) { const [lastStockFetch, setLastStockFetch] = useState(null); const navigate = useNavigate(); - + const [tutorialOpen, setTutorialOpen] = useState(false); + const [step, setStep] = useState(0); + + const tutorialSteps = [ + { title: "Explore Stocks", description: "Here you can view the top S&P 500 stocks and track their sentiment." }, + { title: "Select a stock", description: "Click on a stock you would like to see more information about." }, + { title: "Stock Page", description: "Here information about the stock is displayed. You can see the sentiment and various graphs (stocks market data and stocks overall sentiment). Use this page to gather information on whether a stock might go up or down in the future." }, + { title: "Changing the timespan", description: "You may also view different information based on how far back you would like to see the sentiment. Click through the 'Day', 'Week', and 'Month' buttons."}, + { title: "Exiting the stock view", description: "To exit, either click the red X in the top right of the pop up, or click anywhere off of the pop up window."}, + { title: "You're Ready!", description: "Now you can use this dashboard to monitor your investment insights! Happy trading!" } + ]; + + const nextStep = () => { + if (step < tutorialSteps.length - 1) { + setStep(step + 1); + } else { + setTutorialOpen(false); + } + }; // Fetch authenticated user from API useEffect(() => { const fetchUser = async () => { @@ -112,7 +130,14 @@ function ViewStock(props: { disableCustomTheme?: boolean }) { } } }, [isAuthenticated]); - + + //tutorial mode check + useEffect(() => { + const tutorialMode = localStorage.getItem("tutorialMode"); + if (tutorialMode === "true") { + setTutorialOpen(true); + } + }, []); // Format date for display const formatNotificationTime = (dateString: string): string => { const date = new Date(dateString); @@ -947,6 +972,32 @@ function ViewStock(props: { disableCustomTheme?: boolean }) {
      )}
      + {tutorialOpen && tutorialSteps[step] && ( +
      +

      {tutorialSteps[step].title}

      +

      {tutorialSteps[step].description}

      + +
      + )}
      ) diff --git a/HypeTrade307/src/pages/forum_page.css b/HypeTrade307/src/pages/forum_page.css index 3b6b3ba2..d416ed59 100644 --- a/HypeTrade307/src/pages/forum_page.css +++ b/HypeTrade307/src/pages/forum_page.css @@ -1,5 +1,7 @@ /* Forum.css */ + + /* Main container */ .forum-container { width: 100%; @@ -107,9 +109,10 @@ bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; - justify-content: center; - align-items: center; + justify-content: flex-start; /* <--- changed from center */ + align-items: flex-start; /* <--- changed from center */ z-index: 1000; + padding: 40px; /* optional: gives breathing room */ } .modal-content { @@ -244,4 +247,10 @@ .thread-date { align-self: flex-end; } +} + +.tutorial-modal { + margin-left: 5%; + margin-right: auto; + align-self: flex-start; } \ No newline at end of file diff --git a/HypeTrade307/src/pages/forum_page.tsx b/HypeTrade307/src/pages/forum_page.tsx index c895915e..16bc2318 100644 --- a/HypeTrade307/src/pages/forum_page.tsx +++ b/HypeTrade307/src/pages/forum_page.tsx @@ -5,6 +5,12 @@ import Navbar from "../components/NavbarSection/Navbar"; import CssBaseline from "@mui/material/CssBaseline"; import AppTheme from "../components/shared-theme/AppTheme"; import "./Forum.css"; +import { + Dialog, + DialogTitle, + DialogContent, + Button +} from "@mui/material"; // Define interfaces interface Thread { @@ -41,17 +47,23 @@ function Forum() { const [stockSearch, setStockSearch] = useState(""); const [filteredStocks, setFilteredStocks] = useState([]); + // Tutorial state + const [tutorialOpen, setTutorialOpen] = useState(false); + const [step, setStep] = useState(0); + + const tutorialSteps = [ + { title: "Forum Overview", description: "Here you can view, create, and engage in threads related to stocks." }, + { title: "Viewing Threads", description: "Click on a thread to see more details or join the conversation." }, + { title: "Creating a Thread", description: "Click the 'Create Thread' button to start a new discussion." }, + { title: "Creating a Thread", description: "A popup will appear. Enter the stock you want to talk about and the topic related to the stock, then click 'Create Thread'." }, + { title: "You're All Set!", description: "You're now ready to engage in the forum. Enjoy!" } + ]; + // Fetch threads useEffect(() => { async function fetchThreads() { try { const token = localStorage.getItem("token"); - // if (!token) { - // setError("Not authenticated"); - // setLoading(false); - // return; - // } - const response = await axios.get("http://127.0.0.1:8000/threads", { headers: { Authorization: `Bearer ${token}` } }); @@ -73,11 +85,6 @@ function Forum() { async function fetchStocks() { try { const token = localStorage.getItem("token"); - // if (!token) { - // setError("Not authenticated"); - // return; - // } - const response = await axios.get("http://127.0.0.1:8000/stocks", { headers: { Authorization: `Bearer ${token}` } }); @@ -107,7 +114,23 @@ function Forum() { setFilteredStocks(matches); }, [stockSearch, availableStocks]); - // Create thread + // Start the tutorial if it's not already started + useEffect(() => { + const tutorialMode = JSON.parse(localStorage.getItem("tutorialMode") || "false"); + if (tutorialMode) { + setTutorialOpen(true); + } + }, []); + + // Handle tutorial navigation + const nextStep = () => { + if (step < tutorialSteps.length - 1) { + setStep(step + 1); + } else { + setTutorialOpen(false); + } + }; + const handleCreateThread = async () => { if (!title || !selectedStock) { return; @@ -115,7 +138,6 @@ function Forum() { try { const token = localStorage.getItem("token"); - console.log("Token being sent:", token); if (!token) { setError("Not authenticated"); return; @@ -123,22 +145,15 @@ function Forum() { const response = await axios.post( "http://127.0.0.1:8000/threads/", - { - title: title, - stock_id: selectedStock.stock_id, - // creator_id: - }, - { - headers: { Authorization: `Bearer ${token}` } } + { title: title, stock_id: selectedStock.stock_id }, + { headers: { Authorization: `Bearer ${token}` } } ); // Navigate to the new thread page navigate(`/thread/${response.data.thread_id}`); } catch (err: any) { - console.error("Full error response:", err.response?.data); - setError(JSON.stringify(err.response?.data, null, 2)); // Pretty print error - // console.error("Error creating thread:", err); - // setError(err.response?.data?.detail || "Failed to create thread"); + console.error("Error creating thread:", err); + setError("Failed to create thread"); } }; @@ -195,9 +210,9 @@ function Forum() {
      {thread.title}
      {thread.stock_ref && (
      - - {thread.stock_ref.ticker} - + + {thread.stock_ref.ticker} +
      )}
      @@ -274,9 +289,47 @@ function Forum() {
      )} + + {/* Tutorial Modal */} + {}} + hideBackdrop + disableEscapeKeyDown + disableEnforceFocus + PaperProps={{ + sx: { + position: 'absolute', + top: '45%', + left: '5%', + transform: 'translateY(-50%)', + maxWidth: 400, + padding: 2, + zIndex: 1301, // just above AppBar (default is 1201) + pointerEvents: 'auto', // allow this dialog to receive clicks + }, + }} + sx={{ + pointerEvents: 'none', // allow clicks through the dialog wrapper + }} + > + {tutorialSteps[step].title} + +

      {tutorialSteps[step].description}

      + +
      +
      +
      ); } -export default Forum; \ No newline at end of file +export default Forum; diff --git a/HypeTrade307/src/pages/post_viewer.tsx b/HypeTrade307/src/pages/post_viewer.tsx index b29e8bf6..c990b5c4 100644 --- a/HypeTrade307/src/pages/post_viewer.tsx +++ b/HypeTrade307/src/pages/post_viewer.tsx @@ -4,406 +4,410 @@ import axios from "axios"; import Navbar from "../components/NavbarSection/Navbar"; import CssBaseline from "@mui/material/CssBaseline"; import AppTheme from "../components/shared-theme/AppTheme"; +import { + Dialog, + DialogTitle, + DialogContent, + Button +} from "@mui/material"; +import { Box, Paper } from "@mui/material"; import "./Post.css"; // Define interfaces interface Comment { - comment_id: number; - content: string; - author_id: number; - created_at: string; - author?: { - username: string; - }; - liked_by?: { user_id: number }[]; + comment_id: number; + content: string; + author_id: number; + created_at: string; + author?: { username: string }; + liked_by?: { user_id: number }[]; } interface Post { - post_id: number; + post_id: number; + title: string; + content: string; + author_id: number; + thread_id: number; + created_at: string; + updated_at: string; + author?: { username: string }; + liked_by?: { user_id: number }[]; + thread?: { title: string; - content: string; - author_id: number; - thread_id: number; - created_at: string; - updated_at: string; - author?: { - username: string; - }; - liked_by?: { user_id: number }[]; - thread?: { - title: string; - stock_ref?: { - ticker: string; - stock_name: string; - }; + stock_ref?: { + ticker: string; + stock_name: string; }; + }; } function PostViewer() { - const { threadId, postId } = useParams(); - const navigate = useNavigate(); - const [post, setPost] = useState(null); - const [comments, setComments] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [userId, setUserId] = useState(null); - - // Comment form state - const [commentContent, setCommentContent] = useState(""); - const [showCommentForm, setShowCommentForm] = useState(false); - const [submitting, setSubmitting] = useState(false); - - // Fetch user ID from local storage on component mount - useEffect(() => { - const userData = localStorage.getItem("user"); - if (userData) { - try { - const parsedUser = JSON.parse(userData); - setUserId(parsedUser.user_id); - } catch (err) { - console.error("Error parsing user data:", err); - } - } - }, []); - - // Fetch post details - useEffect(() => { - async function fetchPost() { - try { - const token = localStorage.getItem("token"); - if (!postId) return; - - const response = await axios.get(`http://127.0.0.1:8000/post/${postId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - - setPost(response.data); - } catch (err: any) { - console.error("Error fetching post:", err); - setError(err.response?.data?.detail || "Failed to load post"); - } - } - - fetchPost(); - }, [postId]); - - // Fetch comments for post - useEffect(() => { - async function fetchComments() { - if (!postId) return; - - try { - const token = localStorage.getItem("token"); - - const response = await axios.get(`http://127.0.0.1:8000/post/${postId}/comments`, { - headers: { Authorization: `Bearer ${token}` } - }); - console.log("recd Comments:", response); - setComments(response.data); - } catch (err: any) { - console.error("Error fetching comments:", err); - setError(err.response?.data?.detail || "Failed to load comments"); - } finally { - setLoading(false); - } - } - - fetchComments(); - }, [postId]); - - // Handle comment creation - const handleCreateComment = async (e: React.FormEvent) => { - e.preventDefault(); - if (!commentContent.trim()) return; - - setSubmitting(true); - try { - const token = localStorage.getItem("token"); - if (!token) { - setError("Not authenticated"); - return; - } - - await axios.post( - `http://127.0.0.1:8000/post/${postId}/comments`, - { content: commentContent }, - { headers: { Authorization: `Bearer ${token}` } } - ); - console.log("sent a comment:", commentContent); - - // Refresh comments - const response = await axios.get(`http://127.0.0.1:8000/post/${postId}/comments`, { - headers: { Authorization: `Bearer ${token}` } - }); - - setComments(response.data); - setCommentContent(""); - setShowCommentForm(false); - } catch (err: any) { - console.error("Error creating comment:", err); - setError(err.response?.data?.detail || "Failed to create comment"); - } finally { - setSubmitting(false); - } - }; - function myFunction() { - handleDeletePost(); - navigate(`/thread/${threadId}`); + const { threadId, postId } = useParams(); + const navigate = useNavigate(); + const [post, setPost] = useState(null); + const [comments, setComments] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [userId, setUserId] = useState(null); + + // Comment form state + const [commentContent, setCommentContent] = useState(""); + const [showCommentForm, setShowCommentForm] = useState(false); + const [submitting, setSubmitting] = useState(false); + + // Tutorial state + const [tutorialOpen, setTutorialOpen] = useState(false); + const [step, setStep] = useState(0); + + const tutorialSteps = [ + { title: "Welcome to the Post Page", description: "Here you can view a post and all its comments." }, + { title: "Interacting", description: "You can like posts and comments, and add your own." }, + { title: "Navigation", description: "Use the back button to return to the thread at any time." }, + { title: "You're Ready!", description: "Start exploring and engaging in discussions." } + ]; + + const nextStep = () => { + if (step < tutorialSteps.length - 1) { + setStep(step + 1); + } else { + setTutorialOpen(false); } + }; + + // Fetch user ID from local storage + useEffect(() => { + const userData = localStorage.getItem("user"); + if (userData) { + try { + const parsedUser = JSON.parse(userData); + setUserId(parsedUser.user_id); + } catch (err) { + console.error("Error parsing user data:", err); + } + } + }, []); - // Handle post deletion - const handleDeletePost = async() => { - try { - const token = localStorage.getItem("token"); - if (!token) { - setError("Not authenticated"); - return; - } - - await axios.delete( - `http://127.0.0.1:8000/post/${postId}`, - { headers: { Authorization: `Bearer ${token}` } } - ); - console.log("sent a comment:", commentContent); - - // delete - await axios.get(`http://127.0.0.1:8000/post/${postId}/comments`, { - headers: { Authorization: `Bearer ${token}` } - }); - - } catch (err: any) { - console.error("Error creating comment:", err); - setError(err.response?.data?.detail || "Failed to delete post"); - } finally { - setSubmitting(false); - } - }; + // Fetch post details + useEffect(() => { + async function fetchPost() { + try { + const token = localStorage.getItem("token"); + if (!postId) return; - // Handle post like/unlike - const handleLikePost = async () => { - if (!post || !userId) return; - - try { - const token = localStorage.getItem("token"); - if (!token) { - setError("Not authenticated"); - return; - } - - const isLiked = post.liked_by?.some(like => like.user_id === userId); - const endpoint = `http://127.0.0.1:8000/post/${postId}/${isLiked ? 'unlike' : 'like'}`; - - await axios.post(endpoint, {}, { - headers: { Authorization: `Bearer ${token}` } - }); - - // Update post with new like status - const response = await axios.get(`http://127.0.0.1:8000/post/${postId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - - setPost(response.data); - } catch (err: any) { - console.error("Error updating like status:", err); - setError(err.response?.data?.detail || "Failed to update like status"); - } - }; + const response = await axios.get(`http://127.0.0.1:8000/post/${postId}`, { + headers: { Authorization: `Bearer ${token}` } + }); - // Handle comment like/unlike - const handleLikeComment = async (commentId: number, isLiked: boolean) => { - try { - const token = localStorage.getItem("token"); - if (!token) { - setError("Not authenticated"); - return; - } - - const endpoint = `http://127.0.0.1:8000/comment/${commentId}/${isLiked ? 'unlike' : 'like'}`; - - await axios.post(endpoint, {}, { - headers: { Authorization: `Bearer ${token}` } - }); - - // Refresh comments - const response = await axios.get(`http://127.0.0.1:8000/post/${postId}/comments`, { - headers: { Authorization: `Bearer ${token}` } - }); - - setComments(response.data); - } catch (err: any) { - console.error("Error updating comment like status:", err); - setError(err.response?.data?.detail || "Failed to update comment like status"); - } - }; + setPost(response.data); + } catch (err: any) { + console.error("Error fetching post:", err); + setError(err.response?.data?.detail || "Failed to load post"); + } + } - // Format date - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' + fetchPost(); + }, [postId]); + + // Fetch comments + useEffect(() => { + async function fetchComments() { + if (!postId) return; + + try { + const token = localStorage.getItem("token"); + + const response = await axios.get(`http://127.0.0.1:8000/post/${postId}/comments`, { + headers: { Authorization: `Bearer ${token}` } }); - }; - return ( - - - -
      - {loading ? ( -
      Loading post...
      - ) : error ? ( -
      {error}
      - ) : post ? ( - <> - {/* Post Header */} -
      -
      - - {post.thread && post.thread.stock_ref && ( - - {post.thread.stock_ref.ticker} - {post.thread.stock_ref.stock_name} - - )} -
      -

      {post.title}

      -
      - - Posted by {post.author?.username} on {formatDate(post.created_at)} - - {post.updated_at !== post.created_at && ( - - (edited on {formatDate(post.updated_at)}) - - )} -
      -
      + setComments(response.data); + } catch (err: any) { + console.error("Error fetching comments:", err); + setError(err.response?.data?.detail || "Failed to load comments"); + } finally { + setLoading(false); + } + } - {/* Post Content */} -
      -
      - {post.content} -
      -
      - - - -
      -
      + fetchComments(); + }, [postId]); - {/* Comment Form */} - {showCommentForm && ( -
      -
      -
      - -