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

Price Chart

+
+
+
+ ); +}; + +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 ? ( + Stock + ) : ( +
{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;