diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ab3ce713..7deee5fb 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,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",
@@ -25,6 +26,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"
@@ -8786,6 +8788,12 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/fancy-canvas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz",
+ "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -13083,6 +13091,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lightweight-charts": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.2.tgz",
+ "integrity": "sha512-H5u9BfUeWzOA90QaqAlGjz1tePa7kUihKSOMiE4538/xMuM3k3n6bWYxuZSj9zwIj1PhgY/J5HTA/4lMk/lYYQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "fancy-canvas": "2.1.0"
+ }
+ },
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -16977,6 +16994,12 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
+ "node_modules/seedrandom": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
+ "license": "MIT"
+ },
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 6752876d..a73b7118 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
@@ -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"
diff --git a/frontend/src/App.js b/frontend/src/App.js
index acef44dd..7345842c 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -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 (
@@ -39,7 +40,9 @@ function App() {
} />
} />
} />
- } />
+ } />
+ } />
+
} />
} />
} />
diff --git a/frontend/src/components/markets/MarketsPage.js b/frontend/src/components/markets/MarketsPage.js
index 0540381d..a1770fbc 100644
--- a/frontend/src/components/markets/MarketsPage.js
+++ b/frontend/src/components/markets/MarketsPage.js
@@ -7,7 +7,7 @@ const MarketsPage = () => {
const navigate = useNavigate();
const handleIndexClick = (id) => {
- navigate(`/stocks/${id}`);
+ navigate(`/indices/${id}`);
};
return (
diff --git a/frontend/src/components/markets/stocks/StockAboutSection.js b/frontend/src/components/markets/stocks/StockAboutSection.js
new file mode 100644
index 00000000..191e3583
--- /dev/null
+++ b/frontend/src/components/markets/stocks/StockAboutSection.js
@@ -0,0 +1,23 @@
+import React from "react";
+import "../../../styles/markets/stocks/StockOverviewPage.css";
+
+
+const StockAboutSection = ({ stockDetails }) => {
+ return (
+
+
General Information
+
+
+
Company: {stockDetails?.longName}
+
Industry: {stockDetails?.industryDisp}
+
Sector: {stockDetails?.sectorDisp}
+
Address: {stockDetails?.address1}, {stockDetails?.address2}, {stockDetails?.city}, {stockDetails?.country}
+
Website: {stockDetails?.website}
+
Description: {stockDetails?.longBusinessSummary}
+
+
+
+ );
+}
+
+export default StockAboutSection;
\ No newline at end of file
diff --git a/frontend/src/components/markets/stocks/StockChartSection.js b/frontend/src/components/markets/stocks/StockChartSection.js
new file mode 100644
index 00000000..461b0191
--- /dev/null
+++ b/frontend/src/components/markets/stocks/StockChartSection.js
@@ -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 (
+
+ );
+};
+
+export default StockChartSection;
\ No newline at end of file
diff --git a/frontend/src/components/markets/stocks/StockMetricsSection.js b/frontend/src/components/markets/stocks/StockMetricsSection.js
new file mode 100644
index 00000000..ced272dd
--- /dev/null
+++ b/frontend/src/components/markets/stocks/StockMetricsSection.js
@@ -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 (
+
+
Metrics
+
+
+ {/* Price and Valuation */}
+
+
Price & Valuation
+
Current Price: {stockDetails?.currentPrice}$
+
52 Week Low: {stockDetails?.fiftyTwoWeekLow}$
+
52 Week High: {stockDetails?.fiftyTwoWeekHigh}$
+
Market Cap: {formatNumber(stockDetails?.marketCap)}
+
Enterprise Value: {formatNumber(stockDetails?.enterpriseValue)}
+
Trailing PE: {stockDetails?.trailingPE}
+
Forward PE: {stockDetails?.forwardPE}
+
Price to Book: {stockDetails?.priceToBook}
+
+
+ {/* Dividends */}
+
+
Dividends
+
Dividend Rate: {stockDetails?.dividendRate}%
+
Dividend Yield: {stockDetails?.dividendYield}%
+
Payout Ratio: {stockDetails?.payoutRatio}%
+
Five Year Avg Dividend Yield: {stockDetails?.fiveYearAvgDividendYield}%
+
Last Dividend Date: {stockDetails?.lastDividendDate}
+
+
+ {/* Financials */}
+
+
Financials
+
Total Revenue: {formatNumber(stockDetails?.totalRevenue)}
+
Net Income to Common: {formatNumber(stockDetails?.netIncomeToCommon)}
+
Profit Margins: {stockDetails?.profitMargins}%
+
Return on Assets: {stockDetails?.returnOnAssets}%
+
Return on Equity: {stockDetails?.returnOnEquity}%
+
+
+ {/* Analysts & Recommendations */}
+
+
Analysts & Recommendations
+
Target High Price: {stockDetails?.targetHighPrice}$
+
Target Low Price: {stockDetails?.targetLowPrice}$
+
Target Mean Price: {stockDetails?.targetMeanPrice}$
+
Recommendation: {stockDetails?.recommendationKey}
+
Number of Analyst Opinions: {stockDetails?.numberOfAnalystOpinions}
+
+
+ {/* Stock Metrics */}
+
+
Stock Metrics
+
Beta: {stockDetails?.beta}
+
Trailing EPS: {stockDetails?.trailingEps}
+
Forward EPS: {stockDetails?.forwardEps}
+
Revenue per Share: {stockDetails?.revenuePerShare}$
+
Total Cash per Share: {stockDetails?.totalCashPerShare}$
+
Previous Close: {stockDetails?.previousClose}$
+
Day Low: {stockDetails?.dayLow}$
+
Day High: {stockDetails?.dayHigh}$
+
Volume: {formatNumber(stockDetails?.volume)}
+
Average Volume: {formatNumber(stockDetails?.averageVolume)}
+
+
+
+
+ );
+}
+
+// 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;
diff --git a/frontend/src/components/markets/stocks/StockOverviewPage.js b/frontend/src/components/markets/stocks/StockOverviewPage.js
new file mode 100644
index 00000000..718b29fb
--- /dev/null
+++ b/frontend/src/components/markets/stocks/StockOverviewPage.js
@@ -0,0 +1,104 @@
+import React, { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import StockChartSection from "./StockChartSection";
+import "../../../styles/markets/stocks/StockOverviewPage.css";
+import "../../../styles/Page.css";
+import { StockService } from "../../../service/stockService";
+import StockMetricsSection from "./StockMetricsSection";
+import StockAboutSection from "./StockAboutSection";
+import StockRelatedPostsSection from "./StockRelatedPostsSection";
+
+import CircleAnimation from "../../CircleAnimation";
+
+const StockOverviewPage = () => {
+ const { indexId } = useParams();
+ const [activeTab, setActiveTab] = useState("overview");
+
+ const [stockData, setStockData] = useState(null);
+ const [stockDetails, setStockDetails] = useState(null);
+
+ useEffect(() => {
+ // Fetch stock data
+ StockService.fetchStockById(indexId).then((stock) => {
+ setStockData(stock);
+ }
+ ).catch((error) => {
+ console.error("Error fetching stock data:", error);
+ }
+ );
+ // Fetch stock details
+ StockService.fetchStockDetails(indexId).then((details) => {
+ setStockDetails(details);
+ }
+ ).catch((error) => {
+ console.error("Error fetching stock details:", error);
+ }
+ );
+
+ }, [indexId]);
+
+
+ const renderContent = () => {
+
+ if (!stockData) {
+ return ;
+ }
+ switch (activeTab) {
+ case "chart":
+ return ;
+ case "financials":
+ return ;
+ case "about":
+ return ;
+ case "posts":
+ return ;
+ case "overview":
+ default:
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
+ };
+
+ return (
+
+
+
+
+
+ {stockData?.imageSrc ? (
+
+ ) : (
+
{stockData?.code?.charAt(0)}
+ )}
+
+
{stockData?.code} - {stockData?.name}
+
Price: {stockData?.price}$
+
+
+
+
+
+
+
+
+
+
+
+ {renderContent()}
+
+
+
+
+ );
+};
+
+export default StockOverviewPage;
\ No newline at end of file
diff --git a/frontend/src/components/markets/stocks/StockRelatedPostsSection.js b/frontend/src/components/markets/stocks/StockRelatedPostsSection.js
new file mode 100644
index 00000000..ca69c9b2
--- /dev/null
+++ b/frontend/src/components/markets/stocks/StockRelatedPostsSection.js
@@ -0,0 +1,14 @@
+import React from "react";
+
+const StockRelatedPostsSection = ({ indexId }) => {
+ return (
+
+
Related Posts
+
+
Related posts will be shown here.
+
+
+ );
+}
+
+export default StockRelatedPostsSection;
\ No newline at end of file
diff --git a/frontend/src/data/mockStockDetails.js b/frontend/src/data/mockStockDetails.js
new file mode 100644
index 00000000..6926cebf
--- /dev/null
+++ b/frontend/src/data/mockStockDetails.js
@@ -0,0 +1,82 @@
+export const mockStockDetails = {
+ // General Info
+ longName: "Akbank T.A.S.",
+ shortName: "Akbank",
+ industryDisp: "Banks - Regional",
+ sectorDisp: "Financial Services",
+ address1: "Sabanci Center",
+ address2: "4.Levent",
+ city: "Istanbul",
+ country: "Turkey",
+ website: "https://www.akbank.com",
+ longBusinessSummary: "Akbank T.A.S. provides banking services in Turkey and internationally. It offers consumer, commercial, corporate, and SME banking services along with treasury and investment products.",
+
+ // Pricing and Market Data
+ currentPrice: 63.55,
+ fiftyTwoWeekLow: 34.86,
+ fiftyTwoWeekHigh: 70.75,
+ marketCap: 330460000000,
+ enterpriseValue: 408330000000,
+ trailingPE: 6.86,
+ forwardPE: 3.96,
+ priceToBook: 1.43,
+
+ // Dividends
+ dividendRate: 1.92,
+ dividendYield: 3.02,
+ payoutRatio: 20.7,
+ fiveYearAvgDividendYield: 2.71,
+ lastDividendDate: "2024-04-25",
+
+ // Financials
+ totalRevenue: 133340000000,
+ netIncomeToCommon: 48160000000,
+ profitMargins: 36.12,
+ returnOnAssets: 2.34,
+ returnOnEquity: 22.88,
+
+ // Analysts and Recommendations
+ targetHighPrice: 91.00,
+ targetLowPrice: 60.50,
+ targetMeanPrice: 76.68,
+ recommendationKey: "Buy",
+ numberOfAnalystOpinions: 14,
+
+ // Company Officers
+ companyOfficers: [
+ { name: "Mr. Cenk Kaan Gur", title: "CEO", age: 59 },
+ { name: "Mr. Sabri Hakan Binbasgil", title: "Executive Vice Chairman", age: 63 },
+ { name: "Mr. Erol Sabanci", title: "Honorary Chairman", age: 85 },
+ { name: "Mr. Levent Celebioglu", title: "Corporate & Investment Banking" },
+ { name: "Ms. Pinar Anapa", title: "People & Culture" }
+ ],
+
+ // Stock Metrics
+ beta: 0.72,
+ trailingEps: 9.26,
+ forwardEps: 17.48,
+ revenuePerShare: 25.64,
+ totalCashPerShare: 72.70,
+ previousClose: 63.65,
+ dayLow: 62.80,
+ dayHigh: 63.90,
+ volume: 60370000,
+ averageVolume: 79390000,
+
+ // Historical Data
+ firstTradeDateEpochUtc: "2000-05-20",
+ lastSplitFactor: "133.33:100",
+ lastSplitDate: "2010-04-16",
+ fiftyTwoWeekChange: 61.29,
+
+ // Contact Information
+ phone: "+90 212 385 55 55",
+ fax: "+90 212 269 77 87",
+ irWebsite: "http://www.akbank.com/investor-relations.aspx",
+
+ // Miscellaneous
+ imageSrc: "https://example.com/akbank-logo.png",
+ code: "AKBNK.IS",
+ name: "Akbank T.A.S.",
+ price: 63.55
+};
\ No newline at end of file
diff --git a/frontend/src/service/stockService.js b/frontend/src/service/stockService.js
index b8732aea..431fbc8d 100644
--- a/frontend/src/service/stockService.js
+++ b/frontend/src/service/stockService.js
@@ -1,5 +1,6 @@
import { apiClient } from './apiClient';
import log from '../utils/logger';
+import { mockStockDetails } from '../data/mockStockDetails';
const transformStockItem = (stockItem) => {
log.debug('Transforming stock item:', {
@@ -47,4 +48,9 @@ export const StockService = {
}
},
+ async fetchStockDetails(id) {
+ // Swap this with the real implementation
+ return mockStockDetails;
+ }
+
};
diff --git a/frontend/src/styles/markets/stocks/StockOverviewPage.css b/frontend/src/styles/markets/stocks/StockOverviewPage.css
new file mode 100644
index 00000000..1e894017
--- /dev/null
+++ b/frontend/src/styles/markets/stocks/StockOverviewPage.css
@@ -0,0 +1,147 @@
+* {
+ box-sizing: border-box;
+}
+
+.stock-overview-page {
+ padding: 20px;
+}
+
+.metadata {
+ align-items: center;
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+.metadata img {
+ border-radius: 50%;
+ height: 128px;
+ width: 128px;
+}
+
+.metadata h2 {
+ color: var(--color-neutral-800);
+ font-size: 2.5rem;
+ margin: 0;
+}
+
+.metadata p {
+ color: var(--color-neutral-800);
+ font-size: 1.5rem;
+ margin: 0;
+}
+
+.metadata-text {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.tabs {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+.tabs button {
+ background-color: var(--color-primary-500);
+ border: none;
+ border-radius: 5px;
+ color: var(--color-neutral-white);
+ cursor: pointer;
+ font-weight: bold;
+ padding: 10px 15px;
+ transition: background-color 0.3s;
+}
+
+.tabs button:hover {
+ background-color: var(--color-primary-700);
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.stock-tab-section {
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--border-light);
+ border-radius: 8px;
+ box-shadow: var(--color-neutral-300);
+ color: var(--color-neutral-600);
+ cursor: pointer;
+ flex-direction: column;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ padding: 20px;
+ text-align: left;
+ transition: background-color 0.3s ease, transform 0.3s ease;
+ width: 100%;
+}
+
+.stock-tab-section-content{
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.stock-tab-section-item {
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--border-light);
+ border-radius: 8px;
+ box-shadow: var(--color-neutral-300);
+ color: var(--color-neutral-600);
+ padding: 20px;
+ width: 100%;
+}
+
+.placeholder {
+ align-items: center;
+ background-color: var(--color-primary-500);
+ border-radius: 50%;
+ color: var(--color-neutral-white);
+ display: flex;
+ font-size: 3.5rem;
+ font-weight: bold;
+ height: 128px;
+ justify-content: center;
+ text-transform: uppercase;
+ width: 128px;
+}
+
+/* Chart Section */
+.duration-buttons {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+.duration-buttons button {
+ all: initial;
+ background-color: rgba(240, 243, 250, 1);
+ border-radius: 8px;
+ color: rgba(19, 23, 34, 1);
+ cursor: pointer;
+ font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 510;
+ letter-spacing: -0.32px;
+ line-height: 24px;
+ padding: 8px 24px;
+}
+
+.duration-buttons button:hover {
+ background-color: rgba(224, 227, 235, 1);
+}
+
+.duration-buttons button:active {
+ background-color: rgba(209, 212, 220, 1);
+}
+
+#tradingview_chart {
+ height: 400px;
+ width: 100%;
+}
+
diff --git a/frontend/src/utils/randomUtil.js b/frontend/src/utils/randomUtil.js
new file mode 100644
index 00000000..d0d3c289
--- /dev/null
+++ b/frontend/src/utils/randomUtil.js
@@ -0,0 +1,22 @@
+import seedrandom from "seedrandom";
+
+const RandomUtil = {
+ createGenerator(seed) {
+ return seedrandom(seed);
+ },
+
+ generateRandomArray(rng, length = 10, min = 0, max = 1) {
+ const randomNumbers = Array.from({ length }, () => min + rng() * (max - min));
+ return randomNumbers;
+ },
+
+ generateRandomNumber(rng, min = 0, max = 1) {
+ return min + rng() * (max - min);
+ },
+
+ generateRandomInt(rng, min = 0, max = 100) {
+ return Math.floor(min + rng() * (max - min + 1));
+ },
+};
+
+export default RandomUtil;