From 37787895d56888ab44362252f21fb05c05e97250 Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sun, 27 Apr 2025 22:44:20 -0600 Subject: reorganize --- client/src/App.css | 38 ---- client/src/App.test.js | 8 - client/src/BidsPage.jsx | 182 ------------------ client/src/MarketDataPage.jsx | 320 ------------------------------- client/src/SubmitBidPage.jsx | 188 ------------------ client/src/components/BidsPage.jsx | 182 ++++++++++++++++++ client/src/components/MarketDataPage.jsx | 320 +++++++++++++++++++++++++++++++ client/src/components/SubmitBidPage.jsx | 188 ++++++++++++++++++ client/src/config.js | 3 - client/src/config/config.js | 3 + client/src/index.css | 13 -- client/src/logo.svg | 1 - client/src/reportWebVitals.js | 13 -- client/src/setupTests.js | 5 - client/src/styles/index.css | 13 ++ client/src/tests/App.test.js | 8 + client/src/tests/setupTests.js | 5 + client/src/utils/reportWebVitals.js | 13 ++ 18 files changed, 732 insertions(+), 771 deletions(-) delete mode 100644 client/src/App.css delete mode 100644 client/src/App.test.js delete mode 100644 client/src/BidsPage.jsx delete mode 100644 client/src/MarketDataPage.jsx delete mode 100644 client/src/SubmitBidPage.jsx create mode 100644 client/src/components/BidsPage.jsx create mode 100644 client/src/components/MarketDataPage.jsx create mode 100644 client/src/components/SubmitBidPage.jsx delete mode 100644 client/src/config.js create mode 100644 client/src/config/config.js delete mode 100644 client/src/index.css delete mode 100644 client/src/logo.svg delete mode 100644 client/src/reportWebVitals.js delete mode 100644 client/src/setupTests.js create mode 100644 client/src/styles/index.css create mode 100644 client/src/tests/App.test.js create mode 100644 client/src/tests/setupTests.js create mode 100644 client/src/utils/reportWebVitals.js (limited to 'client/src') diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/client/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/client/src/App.test.js b/client/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/client/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/client/src/BidsPage.jsx b/client/src/BidsPage.jsx deleted file mode 100644 index e0c4128..0000000 --- a/client/src/BidsPage.jsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Table, Typography, Spin, Message, Card, Empty, Badge } from '@arco-design/web-react'; -import { IconArrowRise, IconArrowFall } from '@arco-design/web-react/icon'; -import '@arco-design/web-react/dist/css/arco.css'; -import API_BASE_URL from './config'; - -const columns = (bids) => [ - { - title: 'Timestamp', - dataIndex: 'timestamp', - sorter: (a, b) => new Date(a.timestamp) - new Date(b.timestamp), - render: (val) => new Date(val).toLocaleString(), - width: 180, - }, - { - title: 'Quantity (MW)', - dataIndex: 'quantity', - sorter: (a, b) => a.quantity - b.quantity, - render: (val) => val.toFixed(2), - width: 180, - }, - { - title: 'Price ($/MWh)', - dataIndex: 'price', - sorter: (a, b) => a.price - b.price, - render: (val) => `$${val.toFixed(2)}`, - width: 180, - }, - { - title: 'Status', - dataIndex: 'status', - sorter: (a, b) => a.status.localeCompare(b.status), - filters: [ - { text: 'Submitted', value: 'Submitted' }, - { text: 'Success', value: 'Success' }, - { text: 'Fail', value: 'Fail' }, - ], - onFilter: (value, record) => record.status === value, - render: (val) => ( - - ), - width: 180, - }, - { - title: 'PnL', - dataIndex: 'pnl', - sorter: (a, b) => (a.pnl || 0) - (b.pnl || 0), - render: (val) => { - if (val === null) return 'N/A'; - const isProfit = val >= 0; - const color = isProfit ? 'green' : 'red'; - return ( - - {isProfit ? : } - {val.toFixed(2)} - - ); - }, - width: 180, - }, - { - title: 'Market', - dataIndex: 'market', - sorter: (a, b) => a.market.localeCompare(b.market), - filters: [ - { text: 'ISONE', value: 'ISONE' }, - { text: 'NYISO', value: 'NYISO' }, - { text: 'MISO', value: 'MISO' }, - ], - onFilter: (value, record) => record.market === value, - render: (val) => val, - width: 180, - }, -]; - -function BidsPage() { - const [bids, setBids] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch(`${API_BASE_URL}/bids/`) - .then((res) => { - if (!res.ok) throw new Error('Failed to fetch bids'); - return res.json(); - }) - .then((json) => setBids(json)) - .catch((err) => { - console.error(err); - Message.error('Failed to load bids.'); - }) - .finally(() => setLoading(false)); - }, []); - - return ( -
- - Your Submitted Bids - - - - {loading ? ( -
- -
- ) : bids.length === 0 ? ( -
- -
- ) : ( -
- 'table-row-hover'} - /> - - )} - - - - - ); -} - -export default BidsPage; diff --git a/client/src/MarketDataPage.jsx b/client/src/MarketDataPage.jsx deleted file mode 100644 index 5e028f8..0000000 --- a/client/src/MarketDataPage.jsx +++ /dev/null @@ -1,320 +0,0 @@ -import React, { useEffect, useState, useContext, useRef, useMemo } from 'react'; -import { Typography, Spin, Message, Card, Grid } from '@arco-design/web-react'; -import '@arco-design/web-react/dist/css/arco.css'; -import { initVChartArcoTheme } from '@visactor/vchart-arco-theme'; -import { VChart } from '@visactor/react-vchart'; -import API_BASE_URL from './config'; -import { MarketContext, MARKET_FULL_NAMES } from './App'; - -// TODO: -// -// - [ ] Cancel Promise if market changes -// - [ ] Fix Data Zoom being reset - -const { Row, Col } = Grid; - -function TimeAgo({ timestamp, prefix }) { - const [now, setNow] = useState(new Date()); - - useEffect(() => { - const timer = setInterval(() => setNow(new Date()), 1000); - return () => clearInterval(timer); - }, []); - - const formatTimeAgo = () => { - if (!timestamp) return ''; - const seconds = Math.floor((now - timestamp) / 1000); - if (seconds < 60) return `${seconds} sec ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} min ago`; - const hours = Math.floor(minutes / 60); - return `${hours} hr ago`; - }; - - return ( - - {prefix}: {formatTimeAgo()} - - ); -} - -function MarketDataPage() { - const { selectedMarket } = useContext(MarketContext); - const fiveMinChartRef = useRef(null); - const dayAheadChartRef = useRef(null); - const [fiveMinZoom, setFiveMinZoom] = useState({ start: 0.9, end: 1 }); - const [dayAheadZoom, setDayAheadZoom] = useState({ start: 0.05, end: 1 }); - const [fiveMinData, setFiveMinData] = useState([]); - const [dayAheadData, setDayAheadData] = useState([]); - const [loading, setLoading] = useState(true); - const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null); - const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null); - const [, setNow] = useState(new Date()); - - const handleZoomChange = (e, title) => { - if (!e?.start || !e?.end) return; - if (title === 'fiveMin') { - setFiveMinZoom({ start: e.start, end: e.end }); - } else { - setDayAheadZoom({ start: e.start, end: e.end }); - } - }; - - useEffect(() => { - const fetchFiveMinData = async () => { - try { - const res = await fetch(`${API_BASE_URL}/market/real-time?market=${selectedMarket}`); - if (!res.ok) throw new Error('Failed to fetch real-time data'); - const json = await res.json(); - setFiveMinData(json); - setFiveMinLastUpdated(new Date()); - } catch (err) { - console.error(err); - Message.error('Failed to load real-time market data.'); - } - }; - - const fetchDayAheadData = async () => { - try { - const res = await fetch(`${API_BASE_URL}/market/day-ahead?market=${selectedMarket}`); - if (!res.ok) throw new Error('Failed to fetch day-ahead data'); - const json = await res.json(); - setDayAheadData(json); - setDayAheadLastUpdated(new Date()); - } catch (err) { - console.error(err); - Message.error('Failed to load day-ahead market data.'); - } - }; - - const initialize = async () => { - setLoading(true); - await Promise.all([fetchFiveMinData(), fetchDayAheadData()]); - setLoading(false); - - initVChartArcoTheme({ - defaultMode: 'light', - isWatchingMode: true - }); - }; - - initialize(); - - const fiveMinInterval = setInterval(fetchFiveMinData, 5 * 60 * 1000); - const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); - const timer = setInterval(() => setNow(new Date()), 1000); - - return () => { - clearInterval(fiveMinInterval); - clearInterval(dayAheadInterval); - clearInterval(timer); - }; - }, [selectedMarket]); - - const fiveMinSpec = useMemo(() => ({ - type: 'line', - data: { - values: fiveMinData.map(d => ({ - timestamp: new Date(d.timestamp).toLocaleString(), - LMP: d.lmp, - Energy: d.energy, - Congestion: d.congestion, - Loss: d.loss - })), - transforms: [{ - type: 'fold', - options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } - }] - }, - xField: 'timestamp', - yField: 'value', - seriesField: 'name', - smooth: true, - legend: { position: 'top' }, - tooltip: { - formatter: (datum) => ({ - name: datum.name, - value: datum.value.toFixed(2) - }) - }, - dataZoom: [{ - orient: 'bottom', - height: 20, - start: fiveMinZoom.start, - end: fiveMinZoom.end - }] - }), [fiveMinData, fiveMinZoom]); // <== DEPENDENCIES - - const dayAheadSpec = useMemo(() => ({ - type: 'line', - data: { - values: dayAheadData.map(d => ({ - timestamp: new Date(d.timestamp).toLocaleString(), - LMP: d.lmp, - Energy: d.energy, - Congestion: d.congestion, - Loss: d.loss - })), - transforms: [{ - type: 'fold', - options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } - }] - }, - xField: 'timestamp', - yField: 'value', - seriesField: 'name', - smooth: true, - legend: { position: 'top' }, - tooltip: { - formatter: (datum) => ({ - name: datum.name, - value: datum.value.toFixed(2) - }) - }, - dataZoom: [{ - orient: 'bottom', - height: 20, - start: dayAheadZoom.start, - end: dayAheadZoom.end - }] - }), [dayAheadData, dayAheadZoom]); - - const computeKPIs = (data) => { - if (!data.length) return { avgLmp: 0, totalEnergy: 0, maxCongestion: 0, avgLoss: 0 }; - const avgLmp = data.reduce((sum, d) => sum + d.lmp, 0) / data.length; - const totalEnergy = data.reduce((sum, d) => sum + d.energy, 0); - const maxCongestion = Math.max(...data.map(d => d.congestion)); - const avgLoss = data.reduce((sum, d) => sum + d.loss, 0) / data.length; - return { avgLmp, totalEnergy, maxCongestion, avgLoss }; - }; - - const fiveMinKPIs = computeKPIs(fiveMinData); - const dayAheadKPIs = computeKPIs(dayAheadData); - - return ( -
- - Market Data for {MARKET_FULL_NAMES[selectedMarket]} - - - {loading ? ( - - ) : ( - <> - - {[ - { label: 'Avg Real-Time LMP ($/MWh)', value: fiveMinKPIs.avgLmp }, - { label: 'Total Real-Time Energy (MWh)', value: fiveMinKPIs.totalEnergy }, - { label: 'Max Real-Time Congestion ($)', value: fiveMinKPIs.maxCongestion }, - { label: 'Avg Real-Time Loss ($)', value: fiveMinKPIs.avgLoss } - ].map((item, idx) => ( -
- - ${item.value.toFixed(2)} - - - ))} - - - - {[ - { label: 'Avg Day-Ahead LMP ($/MWh)', value: dayAheadKPIs.avgLmp }, - { label: 'Total Day-Ahead Energy (MWh)', value: dayAheadKPIs.totalEnergy }, - { label: 'Max Day-Ahead Congestion ($)', value: dayAheadKPIs.maxCongestion }, - { label: 'Avg Day-Ahead Loss ($)', value: dayAheadKPIs.avgLoss } - ].map((item, idx) => ( - - - ${item.value.toFixed(2)} - - - ))} - - - - - } style={{ borderRadius: 12, marginBottom: 24 }}> -
- handleZoomChange(e.detail, 'fiveMin')} - /> -
-
- - - } style={{ borderRadius: 12, marginBottom: 24 }}> -
- handleZoomChange(e.detail, 'dayAhead')} - /> -
-
- - - - )} - - - - ); -} - -export default MarketDataPage; diff --git a/client/src/SubmitBidPage.jsx b/client/src/SubmitBidPage.jsx deleted file mode 100644 index 7737c20..0000000 --- a/client/src/SubmitBidPage.jsx +++ /dev/null @@ -1,188 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { Form, InputNumber, DatePicker, Button, Message, Typography, Card } from '@arco-design/web-react'; -import '@arco-design/web-react/dist/css/arco.css'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; -import API_BASE_URL from './config'; -import { MarketContext, MARKET_FULL_NAMES } from './App'; - -dayjs.extend(utc); -dayjs.extend(timezone); - -const MARKET_TIMEZONES = { - ISONE: 'America/New_York', - NYISO: 'America/New_York', - MISO: 'America/Chicago' -}; - -function SubmitBidPage() { - const { selectedMarket } = useContext(MarketContext); - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [localNow, setLocalNow] = useState(''); - const [marketNow, setMarketNow] = useState(''); - - useEffect(() => { - const updateTime = () => { - const now = dayjs(); - const localFormatted = now.format('dddd, MMMM D, h:mm A'); - const marketTz = MARKET_TIMEZONES[selectedMarket] || 'America/New_York'; - const marketTime = now.tz(marketTz); - const marketFormatted = marketTime.format('dddd, MMMM D, h:mm A'); - setLocalNow(localFormatted); - setMarketNow(marketFormatted); - }; - - updateTime(); - const interval = setInterval(updateTime, 1000); - return () => clearInterval(interval); - }, [selectedMarket]); - - const handleSubmit = async (values) => { - setLoading(true); - try { - const picked = dayjs(values.timestamp); - const adjusted = picked.minute(0).second(0).millisecond(0); - - const marketTz = MARKET_TIMEZONES[selectedMarket] || 'America/New_York'; - const timestampInMarketTz = adjusted.tz(marketTz).format(); - - const payload = { - timestamp: timestampInMarketTz, - quantity: values.quantity, - price: values.price, - market: selectedMarket, - user_id: 1 - }; - - const res = await fetch(`${API_BASE_URL}/bids/`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!res.ok) { - const error = await res.json(); - throw new Error(error.detail || 'Failed to submit bid'); - } - - Message.success('Bid submitted successfully!'); - form.resetFields(); - } catch (err) { - console.error(err); - Message.error(err.message || 'Submission failed'); - } finally { - setLoading(false); - } - }; - - return ( -
- - Submit New Bid - - - -
- - Current Local Time: {localNow} - - - Current {MARKET_FULL_NAMES[selectedMarket]} Time: {marketNow} - -
- -
- - Array.from({ length: 60 }, (_, i) => i !== 0), - step: { minute: 60 } - }} - style={{ width: '100%' }} - format="YYYY-MM-DD HH:00" - placeholder="Select date and hour" - timezone={MARKET_TIMEZONES[selectedMarket]} - /> - - - 0' } - ]} - > - - - - = 0' } - ]} - > - - - - - - - -
- - -
- ); -} - -export default SubmitBidPage; diff --git a/client/src/components/BidsPage.jsx b/client/src/components/BidsPage.jsx new file mode 100644 index 0000000..e0c4128 --- /dev/null +++ b/client/src/components/BidsPage.jsx @@ -0,0 +1,182 @@ +import React, { useEffect, useState } from 'react'; +import { Table, Typography, Spin, Message, Card, Empty, Badge } from '@arco-design/web-react'; +import { IconArrowRise, IconArrowFall } from '@arco-design/web-react/icon'; +import '@arco-design/web-react/dist/css/arco.css'; +import API_BASE_URL from './config'; + +const columns = (bids) => [ + { + title: 'Timestamp', + dataIndex: 'timestamp', + sorter: (a, b) => new Date(a.timestamp) - new Date(b.timestamp), + render: (val) => new Date(val).toLocaleString(), + width: 180, + }, + { + title: 'Quantity (MW)', + dataIndex: 'quantity', + sorter: (a, b) => a.quantity - b.quantity, + render: (val) => val.toFixed(2), + width: 180, + }, + { + title: 'Price ($/MWh)', + dataIndex: 'price', + sorter: (a, b) => a.price - b.price, + render: (val) => `$${val.toFixed(2)}`, + width: 180, + }, + { + title: 'Status', + dataIndex: 'status', + sorter: (a, b) => a.status.localeCompare(b.status), + filters: [ + { text: 'Submitted', value: 'Submitted' }, + { text: 'Success', value: 'Success' }, + { text: 'Fail', value: 'Fail' }, + ], + onFilter: (value, record) => record.status === value, + render: (val) => ( + + ), + width: 180, + }, + { + title: 'PnL', + dataIndex: 'pnl', + sorter: (a, b) => (a.pnl || 0) - (b.pnl || 0), + render: (val) => { + if (val === null) return 'N/A'; + const isProfit = val >= 0; + const color = isProfit ? 'green' : 'red'; + return ( + + {isProfit ? : } + {val.toFixed(2)} + + ); + }, + width: 180, + }, + { + title: 'Market', + dataIndex: 'market', + sorter: (a, b) => a.market.localeCompare(b.market), + filters: [ + { text: 'ISONE', value: 'ISONE' }, + { text: 'NYISO', value: 'NYISO' }, + { text: 'MISO', value: 'MISO' }, + ], + onFilter: (value, record) => record.market === value, + render: (val) => val, + width: 180, + }, +]; + +function BidsPage() { + const [bids, setBids] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`${API_BASE_URL}/bids/`) + .then((res) => { + if (!res.ok) throw new Error('Failed to fetch bids'); + return res.json(); + }) + .then((json) => setBids(json)) + .catch((err) => { + console.error(err); + Message.error('Failed to load bids.'); + }) + .finally(() => setLoading(false)); + }, []); + + return ( +
+ + Your Submitted Bids + + + + {loading ? ( +
+ +
+ ) : bids.length === 0 ? ( +
+ +
+ ) : ( +
+
'table-row-hover'} + /> + + )} + + + + + ); +} + +export default BidsPage; diff --git a/client/src/components/MarketDataPage.jsx b/client/src/components/MarketDataPage.jsx new file mode 100644 index 0000000..5e028f8 --- /dev/null +++ b/client/src/components/MarketDataPage.jsx @@ -0,0 +1,320 @@ +import React, { useEffect, useState, useContext, useRef, useMemo } from 'react'; +import { Typography, Spin, Message, Card, Grid } from '@arco-design/web-react'; +import '@arco-design/web-react/dist/css/arco.css'; +import { initVChartArcoTheme } from '@visactor/vchart-arco-theme'; +import { VChart } from '@visactor/react-vchart'; +import API_BASE_URL from './config'; +import { MarketContext, MARKET_FULL_NAMES } from './App'; + +// TODO: +// +// - [ ] Cancel Promise if market changes +// - [ ] Fix Data Zoom being reset + +const { Row, Col } = Grid; + +function TimeAgo({ timestamp, prefix }) { + const [now, setNow] = useState(new Date()); + + useEffect(() => { + const timer = setInterval(() => setNow(new Date()), 1000); + return () => clearInterval(timer); + }, []); + + const formatTimeAgo = () => { + if (!timestamp) return ''; + const seconds = Math.floor((now - timestamp) / 1000); + if (seconds < 60) return `${seconds} sec ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} min ago`; + const hours = Math.floor(minutes / 60); + return `${hours} hr ago`; + }; + + return ( + + {prefix}: {formatTimeAgo()} + + ); +} + +function MarketDataPage() { + const { selectedMarket } = useContext(MarketContext); + const fiveMinChartRef = useRef(null); + const dayAheadChartRef = useRef(null); + const [fiveMinZoom, setFiveMinZoom] = useState({ start: 0.9, end: 1 }); + const [dayAheadZoom, setDayAheadZoom] = useState({ start: 0.05, end: 1 }); + const [fiveMinData, setFiveMinData] = useState([]); + const [dayAheadData, setDayAheadData] = useState([]); + const [loading, setLoading] = useState(true); + const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null); + const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null); + const [, setNow] = useState(new Date()); + + const handleZoomChange = (e, title) => { + if (!e?.start || !e?.end) return; + if (title === 'fiveMin') { + setFiveMinZoom({ start: e.start, end: e.end }); + } else { + setDayAheadZoom({ start: e.start, end: e.end }); + } + }; + + useEffect(() => { + const fetchFiveMinData = async () => { + try { + const res = await fetch(`${API_BASE_URL}/market/real-time?market=${selectedMarket}`); + if (!res.ok) throw new Error('Failed to fetch real-time data'); + const json = await res.json(); + setFiveMinData(json); + setFiveMinLastUpdated(new Date()); + } catch (err) { + console.error(err); + Message.error('Failed to load real-time market data.'); + } + }; + + const fetchDayAheadData = async () => { + try { + const res = await fetch(`${API_BASE_URL}/market/day-ahead?market=${selectedMarket}`); + if (!res.ok) throw new Error('Failed to fetch day-ahead data'); + const json = await res.json(); + setDayAheadData(json); + setDayAheadLastUpdated(new Date()); + } catch (err) { + console.error(err); + Message.error('Failed to load day-ahead market data.'); + } + }; + + const initialize = async () => { + setLoading(true); + await Promise.all([fetchFiveMinData(), fetchDayAheadData()]); + setLoading(false); + + initVChartArcoTheme({ + defaultMode: 'light', + isWatchingMode: true + }); + }; + + initialize(); + + const fiveMinInterval = setInterval(fetchFiveMinData, 5 * 60 * 1000); + const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); + const timer = setInterval(() => setNow(new Date()), 1000); + + return () => { + clearInterval(fiveMinInterval); + clearInterval(dayAheadInterval); + clearInterval(timer); + }; + }, [selectedMarket]); + + const fiveMinSpec = useMemo(() => ({ + type: 'line', + data: { + values: fiveMinData.map(d => ({ + timestamp: new Date(d.timestamp).toLocaleString(), + LMP: d.lmp, + Energy: d.energy, + Congestion: d.congestion, + Loss: d.loss + })), + transforms: [{ + type: 'fold', + options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } + }] + }, + xField: 'timestamp', + yField: 'value', + seriesField: 'name', + smooth: true, + legend: { position: 'top' }, + tooltip: { + formatter: (datum) => ({ + name: datum.name, + value: datum.value.toFixed(2) + }) + }, + dataZoom: [{ + orient: 'bottom', + height: 20, + start: fiveMinZoom.start, + end: fiveMinZoom.end + }] + }), [fiveMinData, fiveMinZoom]); // <== DEPENDENCIES + + const dayAheadSpec = useMemo(() => ({ + type: 'line', + data: { + values: dayAheadData.map(d => ({ + timestamp: new Date(d.timestamp).toLocaleString(), + LMP: d.lmp, + Energy: d.energy, + Congestion: d.congestion, + Loss: d.loss + })), + transforms: [{ + type: 'fold', + options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } + }] + }, + xField: 'timestamp', + yField: 'value', + seriesField: 'name', + smooth: true, + legend: { position: 'top' }, + tooltip: { + formatter: (datum) => ({ + name: datum.name, + value: datum.value.toFixed(2) + }) + }, + dataZoom: [{ + orient: 'bottom', + height: 20, + start: dayAheadZoom.start, + end: dayAheadZoom.end + }] + }), [dayAheadData, dayAheadZoom]); + + const computeKPIs = (data) => { + if (!data.length) return { avgLmp: 0, totalEnergy: 0, maxCongestion: 0, avgLoss: 0 }; + const avgLmp = data.reduce((sum, d) => sum + d.lmp, 0) / data.length; + const totalEnergy = data.reduce((sum, d) => sum + d.energy, 0); + const maxCongestion = Math.max(...data.map(d => d.congestion)); + const avgLoss = data.reduce((sum, d) => sum + d.loss, 0) / data.length; + return { avgLmp, totalEnergy, maxCongestion, avgLoss }; + }; + + const fiveMinKPIs = computeKPIs(fiveMinData); + const dayAheadKPIs = computeKPIs(dayAheadData); + + return ( +
+ + Market Data for {MARKET_FULL_NAMES[selectedMarket]} + + + {loading ? ( + + ) : ( + <> + + {[ + { label: 'Avg Real-Time LMP ($/MWh)', value: fiveMinKPIs.avgLmp }, + { label: 'Total Real-Time Energy (MWh)', value: fiveMinKPIs.totalEnergy }, + { label: 'Max Real-Time Congestion ($)', value: fiveMinKPIs.maxCongestion }, + { label: 'Avg Real-Time Loss ($)', value: fiveMinKPIs.avgLoss } + ].map((item, idx) => ( +
+ + ${item.value.toFixed(2)} + + + ))} + + + + {[ + { label: 'Avg Day-Ahead LMP ($/MWh)', value: dayAheadKPIs.avgLmp }, + { label: 'Total Day-Ahead Energy (MWh)', value: dayAheadKPIs.totalEnergy }, + { label: 'Max Day-Ahead Congestion ($)', value: dayAheadKPIs.maxCongestion }, + { label: 'Avg Day-Ahead Loss ($)', value: dayAheadKPIs.avgLoss } + ].map((item, idx) => ( + + + ${item.value.toFixed(2)} + + + ))} + + + + + } style={{ borderRadius: 12, marginBottom: 24 }}> +
+ handleZoomChange(e.detail, 'fiveMin')} + /> +
+
+ + + } style={{ borderRadius: 12, marginBottom: 24 }}> +
+ handleZoomChange(e.detail, 'dayAhead')} + /> +
+
+ + + + )} + + + + ); +} + +export default MarketDataPage; diff --git a/client/src/components/SubmitBidPage.jsx b/client/src/components/SubmitBidPage.jsx new file mode 100644 index 0000000..7737c20 --- /dev/null +++ b/client/src/components/SubmitBidPage.jsx @@ -0,0 +1,188 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { Form, InputNumber, DatePicker, Button, Message, Typography, Card } from '@arco-design/web-react'; +import '@arco-design/web-react/dist/css/arco.css'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import API_BASE_URL from './config'; +import { MarketContext, MARKET_FULL_NAMES } from './App'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +const MARKET_TIMEZONES = { + ISONE: 'America/New_York', + NYISO: 'America/New_York', + MISO: 'America/Chicago' +}; + +function SubmitBidPage() { + const { selectedMarket } = useContext(MarketContext); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [localNow, setLocalNow] = useState(''); + const [marketNow, setMarketNow] = useState(''); + + useEffect(() => { + const updateTime = () => { + const now = dayjs(); + const localFormatted = now.format('dddd, MMMM D, h:mm A'); + const marketTz = MARKET_TIMEZONES[selectedMarket] || 'America/New_York'; + const marketTime = now.tz(marketTz); + const marketFormatted = marketTime.format('dddd, MMMM D, h:mm A'); + setLocalNow(localFormatted); + setMarketNow(marketFormatted); + }; + + updateTime(); + const interval = setInterval(updateTime, 1000); + return () => clearInterval(interval); + }, [selectedMarket]); + + const handleSubmit = async (values) => { + setLoading(true); + try { + const picked = dayjs(values.timestamp); + const adjusted = picked.minute(0).second(0).millisecond(0); + + const marketTz = MARKET_TIMEZONES[selectedMarket] || 'America/New_York'; + const timestampInMarketTz = adjusted.tz(marketTz).format(); + + const payload = { + timestamp: timestampInMarketTz, + quantity: values.quantity, + price: values.price, + market: selectedMarket, + user_id: 1 + }; + + const res = await fetch(`${API_BASE_URL}/bids/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!res.ok) { + const error = await res.json(); + throw new Error(error.detail || 'Failed to submit bid'); + } + + Message.success('Bid submitted successfully!'); + form.resetFields(); + } catch (err) { + console.error(err); + Message.error(err.message || 'Submission failed'); + } finally { + setLoading(false); + } + }; + + return ( +
+ + Submit New Bid + + + +
+ + Current Local Time: {localNow} + + + Current {MARKET_FULL_NAMES[selectedMarket]} Time: {marketNow} + +
+ +
+ + Array.from({ length: 60 }, (_, i) => i !== 0), + step: { minute: 60 } + }} + style={{ width: '100%' }} + format="YYYY-MM-DD HH:00" + placeholder="Select date and hour" + timezone={MARKET_TIMEZONES[selectedMarket]} + /> + + + 0' } + ]} + > + + + + = 0' } + ]} + > + + + + + + + +
+ + +
+ ); +} + +export default SubmitBidPage; diff --git a/client/src/config.js b/client/src/config.js deleted file mode 100644 index 8007eda..0000000 --- a/client/src/config.js +++ /dev/null @@ -1,3 +0,0 @@ -const API_BASE_URL = 'http://10.0.0.164:8000'; - -export default API_BASE_URL; diff --git a/client/src/config/config.js b/client/src/config/config.js new file mode 100644 index 0000000..8007eda --- /dev/null +++ b/client/src/config/config.js @@ -0,0 +1,3 @@ +const API_BASE_URL = 'http://10.0.0.164:8000'; + +export default API_BASE_URL; diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/client/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/client/src/logo.svg b/client/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/client/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/reportWebVitals.js b/client/src/reportWebVitals.js deleted file mode 100644 index 5253d3a..0000000 --- a/client/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/client/src/setupTests.js b/client/src/setupTests.js deleted file mode 100644 index 8f2609b..0000000 --- a/client/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/client/src/styles/index.css b/client/src/styles/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/client/src/styles/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/client/src/tests/App.test.js b/client/src/tests/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/client/src/tests/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/client/src/tests/setupTests.js b/client/src/tests/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/client/src/tests/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/client/src/utils/reportWebVitals.js b/client/src/utils/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/client/src/utils/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; -- cgit v1.2.3