Skip to content

Commit

Permalink
Merge pull request #488 from bounswe/feature/FE-stock-details-page
Browse files Browse the repository at this point in the history
Add stock details page
  • Loading branch information
mahmutbugramert authored Dec 14, 2024
2 parents 85f62b1 + 29879ce commit 826f79e
Show file tree
Hide file tree
Showing 13 changed files with 691 additions and 2 deletions.
23 changes: 23 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"axios": "^1.7.7",
"jest": "^27.5.1",
"jwt-decode": "^4.0.0",
"lightweight-charts": "^4.2.2",
"loglevel": "^1.9.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -20,6 +21,7 @@
"react-router-dom": "^6.27.0",
"react-scripts": "5.0.1",
"react-toastify": "^10.0.6",
"seedrandom": "^3.0.5",
"tsutils": "^3.21.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import PostView from "./components/community/PostView.js";
import CreatePostPage from "./components/community/CreatePostPage.js";
import { ToastContainer } from "react-toastify";
import { AlertModalProvider } from "./components/alert/AlertModalContext.js";
import StockOverviewPage from "./components/markets/stocks/StockOverviewPage.js";

function App() {
return (
Expand All @@ -39,7 +40,9 @@ function App() {
<Route path="community" element={<CommunityPage />} />
<Route path="community/create-post" element={<CreatePostPage />} />
<Route path="markets" element={<MarketsPage />} />
<Route path="stocks/:indexId" element={<StocksPage />} />
<Route path="stocks/:indexId" element={<StockOverviewPage />} />
<Route path="indices/:indexId" element={<StocksPage />} />

<Route path="news" element={<NewsPage />} />
<Route path="portfolio" element={<PortfolioPage />} />
<Route path="profile" element={<ProfilePage />} />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/markets/MarketsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const MarketsPage = () => {
const navigate = useNavigate();

const handleIndexClick = (id) => {
navigate(`/stocks/${id}`);
navigate(`/indices/${id}`);
};

return (
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/components/markets/stocks/StockAboutSection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import "../../../styles/markets/stocks/StockOverviewPage.css";


const StockAboutSection = ({ stockDetails }) => {
return (
<div className="stock-tab-section">
<h2>General Information</h2>
<div className="stock-tab-section-content">
<div className="stock-tab-section-item">
<p><strong>Company:</strong> {stockDetails?.longName}</p>
<p><strong>Industry:</strong> {stockDetails?.industryDisp}</p>
<p><strong>Sector:</strong> {stockDetails?.sectorDisp}</p>
<p><strong>Address:</strong> {stockDetails?.address1}, {stockDetails?.address2}, {stockDetails?.city}, {stockDetails?.country}</p>
<p><strong>Website:</strong> <a href={stockDetails?.website} target="_blank" rel="noreferrer">{stockDetails?.website}</a></p>
<p><strong>Description:</strong> {stockDetails?.longBusinessSummary}</p>
</div>
</div>
</div>
);
}

export default StockAboutSection;
184 changes: 184 additions & 0 deletions frontend/src/components/markets/stocks/StockChartSection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React, { useState, useEffect } from "react";
import { createChart } from "lightweight-charts";
import "../../../styles/markets/stocks/StockOverviewPage.css";
import RandomUtil from "../../../utils/randomUtil";


const StockChartSection = ({ indexId, stockData }) => {
const [duration, setDuration] = useState("1D");

const generateRandomData = (mean, deviation) => {
const data = new Map([
["1D", []],
["1W", []],
["1M", []],
["1Y", []],
]);

let value = mean;
let time = new Date(); // Start from the current date and time

// Calculate the time for 1 year ago from today
const yearAgo = new Date();
yearAgo.setFullYear(yearAgo.getFullYear() - 1); // Set to 1 year ago

const rng = RandomUtil.createGenerator(indexId);
// Loop through the time intervals and generate random data
// 1d: 30 min intervals
// 1w: 4h intervals
// 1m: 12h intervals
// 1y: 1d intervals
const step = [30, 240, 720, 1440];
const elapsedTimes = [1440, 10080, 43200, 525600];
const keys = ["1D", "1W", "1M", "1Y"];
for (let i = 0; i < step.length; i++) {
const interval = step[i];
time = new Date();
// 30 min floor
time.setMinutes(Math.floor(time.getMinutes() / interval) * interval);
value = mean;
while (time >= yearAgo) {

console.log("Value:", value);
const timestamp = Math.floor(time.getTime() / 1000);
const elapsedTime = (new Date() - time) / (1000 * 60);

if (elapsedTime <= elapsedTimes[i]) {
data.get(keys[i]).push({ time: timestamp, value });
}
value = value + RandomUtil.generateRandomNumber(rng) * deviation - deviation / 2;
time = new Date(time - interval * 60 * 1000);
}
}


data.forEach((seriesData, key) => {
data.set(key, seriesData.reverse());
});


console.log("Generated data:", data);
return data;
};

const mean = stockData.price;
const deviation = 0.2;
const seriesesData = generateRandomData(mean, deviation);

useEffect(() => {
const container = document.getElementById('tradingview_chart');
const chartOptions = {
layout: {
textColor: 'black',
background: { type: 'solid', color: 'white' },

},
height: 400,
};
const chart = createChart(container, chartOptions);

const resizeHandler = () => {
const width = container.offsetWidth;
const height = container.offsetHeight;
chart.applyOptions({ width, height });
chart.timeScale().fitContent();
};
resizeHandler();
window.addEventListener('resize', resizeHandler);

function setChartInterval(interval) {
chart.timeScale().fitContent();
}

setChartInterval(duration);

const intervals = ['1D', '1W', '1M', '1Y'];
intervals.forEach(interval => {
// Create buttons if needed
let button = document.getElementById(interval);
if (button) {
return;
}
button = document.createElement('button');
button.id = interval;
button.innerText = interval;
button.addEventListener('click', () => {
setDuration(interval);
setChartInterval(interval);
});
document.getElementById('buttonsContainer').appendChild(button);
});

// first vs last point comparison
const inProfit = seriesesData.get(duration)[0].value < seriesesData.get(duration)[seriesesData.get(duration).length - 1].value;
// porfit ratio
const profitRatio = (seriesesData.get(duration)[seriesesData.get(duration).length - 1].value - seriesesData.get(duration)[0].value) / seriesesData.get(duration)[0].value;
// Gradient color
console.log("Profit ratio:", profitRatio);
// First value
console.log("First value:", seriesesData.get(duration)[0].value);
// Last value
console.log("Last value:", seriesesData.get(duration)[seriesesData.get(duration).length - 1].value);
console.log("Last date in readable:", new Date(seriesesData.get(duration)[seriesesData.get(duration).length - 1].time * 1000).toLocaleDateString());
console.log("First date in readable:", new Date(seriesesData.get(duration)[0].time * 1000).toLocaleDateString());

const topAreaColor = inProfit ? 'rgba(0, 150, 136, 0.3)' : 'rgba(255, 82, 82, 0.3)';
// Gradient fading
const bottomAreaColor = inProfit ? 'rgba(0, 150, 136, 0)' : 'rgba(255, 82, 82, 0)';
const lineColor = inProfit ? 'rgba(0, 150, 136, 1)' : 'rgba(255, 82, 82, 1)';

chart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
timeVisible: true,
secondsVisible: false,
},
rightPriceScale: {
scaleMargins: {
top: 0.3,
bottom: 0.25,
},
},
crosshair: {
horzLine: {
visible: true,
labelVisible: true,
},
},
grid: {
vertLines: {
visible: false,
},
horzLines: {
visible: false,
},
},
});

const areaSeries = chart.addAreaSeries({
topColor: topAreaColor,
bottomColor: bottomAreaColor,
lineColor: lineColor,
lineWidth: 2,
crossHairMarkerVisible: false,
});

areaSeries.setData(seriesesData.get(duration));

return () => {
window.removeEventListener('resize', resizeHandler);
chart.remove();
};
}, [duration]);

return (
<div className="stock-tab-section">
<h3>Price Chart</h3>
<div id="buttonsContainer" className="duration-buttons"></div>
<div id="tradingview_chart"></div>
</div>
);
};

export default StockChartSection;
79 changes: 79 additions & 0 deletions frontend/src/components/markets/stocks/StockMetricsSection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { FaDollarSign, FaChartLine, FaArrowUp, FaArrowDown } from "react-icons/fa";
import '../../../styles/markets/stocks/StockOverviewPage.css';

const StockMetricsSection = ({ stockDetails }) => {
return (
<div className="stock-tab-section">
<h2 className="section-title">Metrics</h2>
<div className="stock-tab-section-content">

{/* Price and Valuation */}
<div className="stock-tab-section-item">
<h3 className="section-subtitle">Price & Valuation</h3>
<p><FaDollarSign className="icon" /> <strong>Current Price:</strong> {stockDetails?.currentPrice}$</p>
<p><FaArrowDown className="icon" /> <strong>52 Week Low:</strong> {stockDetails?.fiftyTwoWeekLow}$</p>
<p><FaArrowUp className="icon" /> <strong>52 Week High:</strong> {stockDetails?.fiftyTwoWeekHigh}$</p>
<p><FaChartLine className="icon" /> <strong>Market Cap:</strong> {formatNumber(stockDetails?.marketCap)}</p>
<p><FaChartLine className="icon" /> <strong>Enterprise Value:</strong> {formatNumber(stockDetails?.enterpriseValue)}</p>
<p><FaChartLine className="icon" /> <strong>Trailing PE:</strong> {stockDetails?.trailingPE}</p>
<p><FaChartLine className="icon" /> <strong>Forward PE:</strong> {stockDetails?.forwardPE}</p>
<p><FaChartLine className="icon" /> <strong>Price to Book:</strong> {stockDetails?.priceToBook}</p>
</div>

{/* Dividends */}
<div className="stock-tab-section-item">
<h3 className="section-subtitle">Dividends</h3>
<p><strong>Dividend Rate:</strong> {stockDetails?.dividendRate}%</p>
<p><strong>Dividend Yield:</strong> {stockDetails?.dividendYield}%</p>
<p><strong>Payout Ratio:</strong> {stockDetails?.payoutRatio}%</p>
<p><strong>Five Year Avg Dividend Yield:</strong> {stockDetails?.fiveYearAvgDividendYield}%</p>
<p><strong>Last Dividend Date:</strong> {stockDetails?.lastDividendDate}</p>
</div>

{/* Financials */}
<div className="stock-tab-section-item">
<h3 className="section-subtitle">Financials</h3>
<p><strong>Total Revenue:</strong> {formatNumber(stockDetails?.totalRevenue)}</p>
<p><strong>Net Income to Common:</strong> {formatNumber(stockDetails?.netIncomeToCommon)}</p>
<p><strong>Profit Margins:</strong> {stockDetails?.profitMargins}%</p>
<p><strong>Return on Assets:</strong> {stockDetails?.returnOnAssets}%</p>
<p><strong>Return on Equity:</strong> {stockDetails?.returnOnEquity}%</p>
</div>

{/* Analysts & Recommendations */}
<div className="stock-tab-section-item">
<h3 className="section-subtitle">Analysts & Recommendations</h3>
<p><strong>Target High Price:</strong> {stockDetails?.targetHighPrice}$</p>
<p><strong>Target Low Price:</strong> {stockDetails?.targetLowPrice}$</p>
<p><strong>Target Mean Price:</strong> {stockDetails?.targetMeanPrice}$</p>
<p><strong>Recommendation:</strong> {stockDetails?.recommendationKey}</p>
<p><strong>Number of Analyst Opinions:</strong> {stockDetails?.numberOfAnalystOpinions}</p>
</div>

{/* Stock Metrics */}
<div className="stock-tab-section-item">
<h3 className="section-subtitle">Stock Metrics</h3>
<p><strong>Beta:</strong> {stockDetails?.beta}</p>
<p><strong>Trailing EPS:</strong> {stockDetails?.trailingEps}</p>
<p><strong>Forward EPS:</strong> {stockDetails?.forwardEps}</p>
<p><strong>Revenue per Share:</strong> {stockDetails?.revenuePerShare}$</p>
<p><strong>Total Cash per Share:</strong> {stockDetails?.totalCashPerShare}$</p>
<p><strong>Previous Close:</strong> {stockDetails?.previousClose}$</p>
<p><strong>Day Low:</strong> {stockDetails?.dayLow}$</p>
<p><strong>Day High:</strong> {stockDetails?.dayHigh}$</p>
<p><strong>Volume:</strong> {formatNumber(stockDetails?.volume)}</p>
<p><strong>Average Volume:</strong> {formatNumber(stockDetails?.averageVolume)}</p>
</div>

</div>
</div>
);
}

// Function to format large numbers with commas for better readability
const formatNumber = (num) => {
return num ? num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "N/A";
}

export default StockMetricsSection;
Loading

0 comments on commit 826f79e

Please sign in to comment.