aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2025-04-28 00:20:28 -0600
committerNavan Chauhan <navanchauhan@gmail.com>2025-04-28 00:20:28 -0600
commitd418dd77f23ab24e91dfebad94832f9496eb567d (patch)
tree063698ec71e71a8c0c9c79814c4b1e575bb8a1c3
parent6ca0b8d9488bb16a118d00aadba1b6c7955f71b6 (diff)
prettifierHEADmaster
-rw-r--r--client/src/App.js170
-rw-r--r--client/src/components/BidsPage.jsx134
-rw-r--r--client/src/components/MarketDataPage.jsx257
-rw-r--r--client/src/components/SubmitBidPage.jsx136
-rw-r--r--client/src/components/index.js6
-rw-r--r--client/src/config.js3
-rw-r--r--client/src/index.js24
-rw-r--r--client/src/utils/reportWebVitals.js4
8 files changed, 451 insertions, 283 deletions
diff --git a/client/src/App.js b/client/src/App.js
index c85e46c..0bd1dd4 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -1,33 +1,34 @@
-import React, { useState, createContext } from 'react';
-import { Routes, Route, Navigate, Link, useLocation } from 'react-router-dom';
-import { MarketDataPage, BidsPage, SubmitBidPage } from './components';
-import { Menu, Select, PageHeader, Typography } from '@arco-design/web-react';
-import '@arco-design/web-react/dist/css/arco.css';
+import React, { useState, createContext } from "react";
+import { Routes, Route, Navigate, Link, useLocation } from "react-router-dom";
+import { MarketDataPage, BidsPage, SubmitBidPage } from "./components";
+import { Menu, Select, PageHeader, Typography } from "@arco-design/web-react";
+import "@arco-design/web-react/dist/css/arco.css";
const ghostBgStyle = {
- backgroundImage: 'radial-gradient(var(--color-fill-3) 1px, rgba(0, 0, 0, 0) 1px)',
- backgroundSize: '16px 16px',
+ backgroundImage:
+ "radial-gradient(var(--color-fill-3) 1px, rgba(0, 0, 0, 0) 1px)",
+ backgroundSize: "16px 16px",
padding: 20,
};
export const MarketContext = createContext();
export const MARKET_FULL_NAMES = {
- CAISO: 'California ISO',
- ERCOT: 'Electric Reliability Council of Texas',
- ISONE: 'ISO New England',
- MISO: 'Midcontinent ISO',
- NYISO: 'New York ISO',
- PJM: 'PJM Interconnection',
+ CAISO: "California ISO",
+ ERCOT: "Electric Reliability Council of Texas",
+ ISONE: "ISO New England",
+ MISO: "Midcontinent ISO",
+ NYISO: "New York ISO",
+ PJM: "PJM Interconnection",
};
function App() {
- const [selectedMarket, setSelectedMarket] = useState('ISONE');
+ const [selectedMarket, setSelectedMarket] = useState("ISONE");
const location = useLocation();
return (
<MarketContext.Provider value={{ selectedMarket, setSelectedMarket }}>
- <div>
- <style>
- {`
+ <div>
+ <style>
+ {`
@media (max-width: 600px) {
.page-header-container {
flex-direction: column;
@@ -53,57 +54,92 @@ function App() {
}
}
`}
- </style>
- <div style={ghostBgStyle}>
- <div className="page-header-container" style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap' }}>
- <div>
- <PageHeader
- title={
- <Typography.Title heading={3} className="page-header-title">
- PolyEnergy
- </Typography.Title>
- }
- subTitle={
- <Typography.Title type="secondary" heading={4} className="page-header-subtitle">
- Virtual Energy Trading
- </Typography.Title>
- }
- />
- </div>
- <div className="market-select-container">
- <Select
- className="market-select"
- value={selectedMarket}
- onChange={setSelectedMarket}
- style={{ width: 200 }}
- options={['ISONE', 'MISO', 'NYISO'].map(market => ({ label: market, value: market }))}
- />
- </div>
- </div>
- <Menu mode="horizontal" selectedKeys={[location.pathname.split('/')[1] || 'market-data']}>
- <Menu.Item key="market-data">
- <Link to="/market-data">Market Data</Link>
- </Menu.Item>
- <Menu.Item key="submit-bid">
- <Link to="/submit-bid">Submit Bid</Link>
- </Menu.Item>
- <Menu.Item key="bids">
- <Link to="/bids">My Bids</Link>
- </Menu.Item>
- </Menu>
- </div>
- <div style={{ backgroundColor: 'var(--color-fill-2)', minHeight: '80vh', display: 'flex', flexDirection: 'column' }}>
- <Routes>
- <Route path="/" element={<Navigate to="/market-data" replace />} />
- <Route path="/market-data" element={<MarketDataPage />} />
- <Route path="/submit-bid" element={<SubmitBidPage />} />
- <Route path="/bids" element={<BidsPage />} />
- </Routes>
+ </style>
+ <div style={ghostBgStyle}>
+ <div
+ className="page-header-container"
+ style={{
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "flex-end",
+ flexWrap: "wrap",
+ }}
+ >
+ <div>
+ <PageHeader
+ title={
+ <Typography.Title heading={3} className="page-header-title">
+ PolyEnergy
+ </Typography.Title>
+ }
+ subTitle={
+ <Typography.Title
+ type="secondary"
+ heading={4}
+ className="page-header-subtitle"
+ >
+ Virtual Energy Trading
+ </Typography.Title>
+ }
+ />
+ </div>
+ <div className="market-select-container">
+ <Select
+ className="market-select"
+ value={selectedMarket}
+ onChange={setSelectedMarket}
+ style={{ width: 200 }}
+ options={["ISONE", "MISO", "NYISO"].map((market) => ({
+ label: market,
+ value: market,
+ }))}
+ />
+ </div>
+ </div>
+ <Menu
+ mode="horizontal"
+ selectedKeys={[location.pathname.split("/")[1] || "market-data"]}
+ >
+ <Menu.Item key="market-data">
+ <Link to="/market-data">Market Data</Link>
+ </Menu.Item>
+ <Menu.Item key="submit-bid">
+ <Link to="/submit-bid">Submit Bid</Link>
+ </Menu.Item>
+ <Menu.Item key="bids">
+ <Link to="/bids">My Bids</Link>
+ </Menu.Item>
+ </Menu>
+ </div>
+ <div
+ style={{
+ backgroundColor: "var(--color-fill-2)",
+ minHeight: "80vh",
+ display: "flex",
+ flexDirection: "column",
+ }}
+ >
+ <Routes>
+ <Route path="/" element={<Navigate to="/market-data" replace />} />
+ <Route path="/market-data" element={<MarketDataPage />} />
+ <Route path="/submit-bid" element={<SubmitBidPage />} />
+ <Route path="/bids" element={<BidsPage />} />
+ </Routes>
+ </div>
+ <footer
+ style={{
+ flexShrink: 0,
+ marginTop: 20,
+ textAlign: "center",
+ fontSize: 14,
+ color: "var(--color-text-3)",
+ paddingBottom: 20,
+ }}
+ >
+ © {new Date().getFullYear()} PolyEnergy. All rights reserved.
+ </footer>
</div>
- <footer style={{ flexShrink: 0, marginTop: 20, textAlign: 'center', fontSize: 14, color: 'var(--color-text-3)', paddingBottom: 20 }}>
- © {new Date().getFullYear()} PolyEnergy. All rights reserved.
- </footer>
- </div>
</MarketContext.Provider>
);
}
diff --git a/client/src/components/BidsPage.jsx b/client/src/components/BidsPage.jsx
index 2b97e99..3e596f0 100644
--- a/client/src/components/BidsPage.jsx
+++ b/client/src/components/BidsPage.jsx
@@ -1,56 +1,75 @@
-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';
+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',
+ 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',
+ title: "Quantity (MW)",
+ dataIndex: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
render: (val) => val.toFixed(2),
width: 180,
},
{
- title: 'Price ($/MWh)',
- dataIndex: 'price',
+ title: "Price ($/MWh)",
+ dataIndex: "price",
sorter: (a, b) => a.price - b.price,
render: (val) => `$${val.toFixed(2)}`,
width: 180,
},
{
- title: 'Status',
- dataIndex: 'status',
+ 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' },
+ { text: "Submitted", value: "Submitted" },
+ { text: "Success", value: "Success" },
+ { text: "Fail", value: "Fail" },
],
onFilter: (value, record) => record.status === value,
render: (val) => (
- <Badge status={val === 'Success' ? 'success' : val === 'Fail' ? 'error' : 'processing'} text={val} />
+ <Badge
+ status={
+ val === "Success"
+ ? "success"
+ : val === "Fail"
+ ? "error"
+ : "processing"
+ }
+ text={val}
+ />
),
width: 180,
},
{
- title: 'PnL',
- dataIndex: 'pnl',
+ title: "PnL",
+ dataIndex: "pnl",
sorter: (a, b) => (a.pnl || 0) - (b.pnl || 0),
render: (val) => {
- if (val === null) return 'N/A';
+ if (val === null) return "N/A";
const isProfit = val >= 0;
- const color = isProfit ? 'green' : 'red';
+ const color = isProfit ? "green" : "red";
return (
- <Typography.Text style={{ color, display: 'flex', alignItems: 'center', gap: 4 }}>
+ <Typography.Text
+ style={{ color, display: "flex", alignItems: "center", gap: 4 }}
+ >
{isProfit ? <IconArrowRise /> : <IconArrowFall />}
{val.toFixed(2)}
</Typography.Text>
@@ -59,13 +78,13 @@ const columns = (bids) => [
width: 180,
},
{
- title: 'Market',
- dataIndex: 'market',
+ 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' },
+ { text: "ISONE", value: "ISONE" },
+ { text: "NYISO", value: "NYISO" },
+ { text: "MISO", value: "MISO" },
],
onFilter: (value, record) => record.market === value,
render: (val) => val,
@@ -80,20 +99,30 @@ function BidsPage() {
useEffect(() => {
fetch(`${API_BASE_URL}/bids/`)
.then((res) => {
- if (!res.ok) throw new Error('Failed to fetch bids');
+ 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.');
+ Message.error("Failed to load bids.");
})
.finally(() => setLoading(false));
}, []);
return (
- <div style={{ padding: 20, backgroundColor: 'var(--color-fill-2)', minHeight: 'calc(100vh - 150px)', animation: 'fadeSlideIn 0.6s ease' }}>
- <Typography.Title heading={3} style={{ textAlign: 'center', marginBottom: 20 }}>
+ <div
+ style={{
+ padding: 20,
+ backgroundColor: "var(--color-fill-2)",
+ minHeight: "calc(100vh - 150px)",
+ animation: "fadeSlideIn 0.6s ease",
+ }}
+ >
+ <Typography.Title
+ heading={3}
+ style={{ textAlign: "center", marginBottom: 20 }}
+ >
Your Submitted Bids
</Typography.Title>
@@ -101,37 +130,38 @@ function BidsPage() {
style={{
marginTop: 16,
borderRadius: 16,
- boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
- background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(245,245,245,0.95) 100%)',
- backdropFilter: 'blur(8px)',
- overflow: 'hidden',
- padding: 20
+ boxShadow: "0 8px 24px rgba(0,0,0,0.08)",
+ background:
+ "linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(245,245,245,0.95) 100%)",
+ backdropFilter: "blur(8px)",
+ overflow: "hidden",
+ padding: 20,
}}
>
{loading ? (
- <div style={{ textAlign: 'center', padding: 80 }}>
+ <div style={{ textAlign: "center", padding: 80 }}>
<Spin />
</div>
) : bids.length === 0 ? (
- <div style={{ textAlign: 'center', padding: 80 }}>
+ <div style={{ textAlign: "center", padding: 80 }}>
<Empty description="No bids found yet." />
</div>
) : (
<div className="responsive-table-wrapper">
- <Table
- columns={columns(bids)}
- data={bids}
- rowKey="id"
- pagination={{
- pageSize: 50,
- sizeCanChange: true,
- showTotal: true,
- }}
- scroll={{ x: true }}
- border
- style={{ transition: 'opacity 0.5s ease-in-out' }}
- rowClassName={() => 'table-row-hover'}
- />
+ <Table
+ columns={columns(bids)}
+ data={bids}
+ rowKey="id"
+ pagination={{
+ pageSize: 50,
+ sizeCanChange: true,
+ showTotal: true,
+ }}
+ scroll={{ x: true }}
+ border
+ style={{ transition: "opacity 0.5s ease-in-out" }}
+ rowClassName={() => "table-row-hover"}
+ />
</div>
)}
</Card>
diff --git a/client/src/components/MarketDataPage.jsx b/client/src/components/MarketDataPage.jsx
index 9d98269..eed970f 100644
--- a/client/src/components/MarketDataPage.jsx
+++ b/client/src/components/MarketDataPage.jsx
@@ -1,10 +1,10 @@
-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';
+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:
//
@@ -22,7 +22,7 @@ function TimeAgo({ timestamp, prefix }) {
}, []);
const formatTimeAgo = () => {
- if (!timestamp) return '';
+ if (!timestamp) return "";
const seconds = Math.floor((now - timestamp) / 1000);
if (seconds < 60) return `${seconds} sec ago`;
const minutes = Math.floor(seconds / 60);
@@ -32,7 +32,10 @@ function TimeAgo({ timestamp, prefix }) {
};
return (
- <Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 8 }}>
+ <Typography.Text
+ type="secondary"
+ style={{ fontSize: 12, display: "block", marginTop: 8 }}
+ >
{prefix}: {formatTimeAgo()}
</Typography.Text>
);
@@ -41,7 +44,7 @@ function TimeAgo({ timestamp, prefix }) {
function MarketDataPage() {
const { selectedMarket } = useContext(MarketContext);
const fiveMinChartRef = useRef(null);
- const dayAheadChartRef = 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([]);
@@ -52,38 +55,42 @@ function MarketDataPage() {
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 });
- }
- };
+ 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 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.');
+ 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 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.');
+ Message.error("Failed to load day-ahead market data.");
}
};
@@ -93,8 +100,8 @@ function MarketDataPage() {
setLoading(false);
initVChartArcoTheme({
- defaultMode: 'light',
- isWatchingMode: true
+ defaultMode: "light",
+ isWatchingMode: true,
});
};
@@ -111,79 +118,102 @@ function MarketDataPage() {
};
}, [selectedMarket]);
- const fiveMinSpec = useMemo(() => ({
- type: 'line',
+ const fiveMinSpec = useMemo(
+ () => ({
+ type: "line",
data: {
- values: fiveMinData.map(d => ({
+ values: fiveMinData.map((d) => ({
timestamp: new Date(d.timestamp).toLocaleString(),
LMP: d.lmp,
Energy: d.energy,
Congestion: d.congestion,
- Loss: d.loss
+ Loss: d.loss,
})),
- transforms: [{
- type: 'fold',
- options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] }
- }]
+ transforms: [
+ {
+ type: "fold",
+ options: {
+ key: "name",
+ value: "value",
+ fields: ["LMP", "Energy", "Congestion", "Loss"],
+ },
+ },
+ ],
},
- xField: 'timestamp',
- yField: 'value',
- seriesField: 'name',
+ xField: "timestamp",
+ yField: "value",
+ seriesField: "name",
smooth: true,
- legend: { position: 'top' },
+ legend: { position: "top" },
tooltip: {
formatter: (datum) => ({
name: datum.name,
- value: datum.value.toFixed(2)
- })
+ value: datum.value.toFixed(2),
+ }),
},
- dataZoom: [{
- orient: 'bottom',
- height: 20,
- start: fiveMinZoom.start,
- end: fiveMinZoom.end
- }]
- }), [fiveMinData, fiveMinZoom]); // <== DEPENDENCIES
+ dataZoom: [
+ {
+ orient: "bottom",
+ height: 20,
+ start: fiveMinZoom.start,
+ end: fiveMinZoom.end,
+ },
+ ],
+ }),
+ [fiveMinData, fiveMinZoom],
+ ); // <== DEPENDENCIES
- const dayAheadSpec = useMemo(() => ({
- type: 'line',
+ const dayAheadSpec = useMemo(
+ () => ({
+ type: "line",
data: {
- values: dayAheadData.map(d => ({
+ values: dayAheadData.map((d) => ({
timestamp: new Date(d.timestamp).toLocaleString(),
LMP: d.lmp,
Energy: d.energy,
Congestion: d.congestion,
- Loss: d.loss
+ Loss: d.loss,
})),
- transforms: [{
- type: 'fold',
- options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] }
- }]
+ transforms: [
+ {
+ type: "fold",
+ options: {
+ key: "name",
+ value: "value",
+ fields: ["LMP", "Energy", "Congestion", "Loss"],
+ },
+ },
+ ],
},
- xField: 'timestamp',
- yField: 'value',
- seriesField: 'name',
+ xField: "timestamp",
+ yField: "value",
+ seriesField: "name",
smooth: true,
- legend: { position: 'top' },
+ legend: { position: "top" },
tooltip: {
formatter: (datum) => ({
name: datum.name,
- value: datum.value.toFixed(2)
- })
+ value: datum.value.toFixed(2),
+ }),
},
- dataZoom: [{
- orient: 'bottom',
- height: 20,
- start: dayAheadZoom.start,
- end: dayAheadZoom.end
- }]
- }), [dayAheadData, dayAheadZoom]);
+ 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 };
+ 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 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 };
};
@@ -193,32 +223,44 @@ function MarketDataPage() {
return (
<div className="page-container">
- <Typography.Title heading={3} style={{ textAlign: 'center', marginBottom: 20 }} className="market-page-title" >
+ <Typography.Title
+ heading={3}
+ style={{ textAlign: "center", marginBottom: 20 }}
+ className="market-page-title"
+ >
Market Data for {MARKET_FULL_NAMES[selectedMarket]}
</Typography.Title>
{loading ? (
- <Spin style={{ display: 'block', margin: '80px auto' }} />
+ <Spin style={{ display: "block", margin: "80px auto" }} />
) : (
<>
- <Row gutter={24} style={{ marginBottom: 30 }}>
+ <Row gutter={24} style={{ marginBottom: 30 }}>
{[
- { 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 }
+ { 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) => (
<Col xs={12} md={6} key={idx} style={{ marginBottom: 16 }}>
<Card
hoverable
style={{
- textAlign: 'center',
+ textAlign: "center",
borderRadius: 12,
- boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
+ boxShadow: "0 4px 12px rgba(0,0,0,0.05)",
}}
title={item.label}
>
- <Typography.Title heading={5}>${item.value.toFixed(2)}</Typography.Title>
+ <Typography.Title heading={5}>
+ ${item.value.toFixed(2)}
+ </Typography.Title>
</Card>
</Col>
))}
@@ -226,22 +268,33 @@ function MarketDataPage() {
<Row gutter={24} style={{ marginBottom: 30 }}>
{[
- { 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 }
+ {
+ 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) => (
<Col xs={12} md={6} key={idx} style={{ marginBottom: 16 }}>
<Card
hoverable
style={{
- textAlign: 'center',
+ textAlign: "center",
borderRadius: 12,
- boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
+ boxShadow: "0 4px 12px rgba(0,0,0,0.05)",
}}
title={item.label}
>
- <Typography.Title heading={5}>${item.value.toFixed(2)}</Typography.Title>
+ <Typography.Title heading={5}>
+ ${item.value.toFixed(2)}
+ </Typography.Title>
</Card>
</Col>
))}
@@ -249,24 +302,36 @@ function MarketDataPage() {
<Row gutter={24}>
<Col xs={24} md={12}>
- <Card title="Real-Time Market Data" extra={<TimeAgo timestamp={fiveMinLastUpdated} prefix="Updated" />} style={{ borderRadius: 12, marginBottom: 24 }}>
+ <Card
+ title="Real-Time Market Data"
+ extra={
+ <TimeAgo timestamp={fiveMinLastUpdated} prefix="Updated" />
+ }
+ style={{ borderRadius: 12, marginBottom: 24 }}
+ >
<div style={{ padding: 10 }}>
<VChart
- spec={fiveMinSpec}
- ref={fiveMinChartRef}
- onDataZoom={(e) => handleZoomChange(e.detail, 'fiveMin')}
- />
+ spec={fiveMinSpec}
+ ref={fiveMinChartRef}
+ onDataZoom={(e) => handleZoomChange(e.detail, "fiveMin")}
+ />
</div>
</Card>
</Col>
<Col xs={24} md={12}>
- <Card title="Day-Ahead Market Data" extra={<TimeAgo timestamp={dayAheadLastUpdated} prefix="Updated" />} style={{ borderRadius: 12, marginBottom: 24 }}>
+ <Card
+ title="Day-Ahead Market Data"
+ extra={
+ <TimeAgo timestamp={dayAheadLastUpdated} prefix="Updated" />
+ }
+ style={{ borderRadius: 12, marginBottom: 24 }}
+ >
<div style={{ padding: 10 }}>
<VChart
- spec={dayAheadSpec}
- ref={dayAheadChartRef}
- onDataZoom={(e) => handleZoomChange(e.detail, 'dayAhead')}
- />
+ spec={dayAheadSpec}
+ ref={dayAheadChartRef}
+ onDataZoom={(e) => handleZoomChange(e.detail, "dayAhead")}
+ />
</div>
</Card>
</Col>
diff --git a/client/src/components/SubmitBidPage.jsx b/client/src/components/SubmitBidPage.jsx
index bed92be..5d1e642 100644
--- a/client/src/components/SubmitBidPage.jsx
+++ b/client/src/components/SubmitBidPage.jsx
@@ -1,35 +1,43 @@
-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';
+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'
+ 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('');
+ 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 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');
+ const marketFormatted = marketTime.format("dddd, MMMM D, h:mm A");
setLocalNow(localFormatted);
setMarketNow(marketFormatted);
};
@@ -45,7 +53,7 @@ function SubmitBidPage() {
const picked = dayjs(values.timestamp);
const adjusted = picked.minute(0).second(0).millisecond(0);
- const marketTz = MARKET_TIMEZONES[selectedMarket] || 'America/New_York';
+ const marketTz = MARKET_TIMEZONES[selectedMarket] || "America/New_York";
const timestampInMarketTz = adjusted.tz(marketTz).format();
const payload = {
@@ -53,33 +61,43 @@ function SubmitBidPage() {
quantity: values.quantity,
price: values.price,
market: selectedMarket,
- user_id: 1
+ user_id: 1,
};
const res = await fetch(`${API_BASE_URL}/bids/`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
+ 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');
+ throw new Error(error.detail || "Failed to submit bid");
}
- Message.success('Bid submitted successfully!');
+ Message.success("Bid submitted successfully!");
form.resetFields();
} catch (err) {
console.error(err);
- Message.error(err.message || 'Submission failed');
+ Message.error(err.message || "Submission failed");
} finally {
setLoading(false);
}
};
return (
- <div style={{ padding: 20, backgroundColor: 'var(--color-fill-2)', minHeight: 'calc(100vh - 150px)', animation: 'fadeSlideIn 0.6s ease' }}>
- <Typography.Title heading={3} style={{ textAlign: 'center', marginBottom: 20 }}>
+ <div
+ style={{
+ padding: 20,
+ backgroundColor: "var(--color-fill-2)",
+ minHeight: "calc(100vh - 150px)",
+ animation: "fadeSlideIn 0.6s ease",
+ }}
+ >
+ <Typography.Title
+ heading={3}
+ style={{ textAlign: "center", marginBottom: 20 }}
+ >
Submit New Bid
</Typography.Title>
@@ -87,19 +105,29 @@ function SubmitBidPage() {
style={{
marginTop: 16,
borderRadius: 16,
- boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
- background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(245,245,245,0.95) 100%)',
- backdropFilter: 'blur(8px)',
- overflow: 'hidden',
- padding: 32
+ boxShadow: "0 8px 24px rgba(0,0,0,0.08)",
+ background:
+ "linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(245,245,245,0.95) 100%)",
+ backdropFilter: "blur(8px)",
+ overflow: "hidden",
+ padding: 32,
}}
>
- <div style={{ marginBottom: 32, padding: 16, border: '1px solid var(--color-border)', borderRadius: 8, background: 'var(--color-fill-1)' }}>
+ <div
+ style={{
+ marginBottom: 32,
+ padding: 16,
+ border: "1px solid var(--color-border)",
+ borderRadius: 8,
+ background: "var(--color-fill-1)",
+ }}
+ >
<Typography.Paragraph style={{ marginBottom: 8 }}>
<strong>Current Local Time:</strong> {localNow}
</Typography.Paragraph>
<Typography.Paragraph>
- <strong>Current {MARKET_FULL_NAMES[selectedMarket]} Time:</strong> {marketNow}
+ <strong>Current {MARKET_FULL_NAMES[selectedMarket]} Time:</strong>{" "}
+ {marketNow}
</Typography.Paragraph>
</div>
@@ -108,21 +136,24 @@ function SubmitBidPage() {
layout="vertical"
onSubmit={handleSubmit}
initialValues={{ quantity: 0, price: 0 }}
- style={{ maxWidth: 600, margin: '0 auto' }}
+ style={{ maxWidth: 600, margin: "0 auto" }}
>
<Form.Item
label="Bid Date and Hour"
field="timestamp"
- rules={[{ required: true, message: 'Please select bid date and hour' }]}
+ rules={[
+ { required: true, message: "Please select bid date and hour" },
+ ]}
>
<DatePicker
showTime={{
- format: 'h A',
+ format: "h A",
defaultValue: dayjs().hour(0).minute(0),
- disabledMinutes: () => Array.from({ length: 60 }, (_, i) => i !== 0),
- step: { minute: 60 }
+ disabledMinutes: () =>
+ Array.from({ length: 60 }, (_, i) => i !== 0),
+ step: { minute: 60 },
}}
- style={{ width: '100%' }}
+ style={{ width: "100%" }}
format="YYYY-MM-DD HH:00"
placeholder="Select date and hour"
timezone={MARKET_TIMEZONES[selectedMarket]}
@@ -133,22 +164,25 @@ function SubmitBidPage() {
label="Quantity (MW)"
field="quantity"
rules={[
- { required: true, message: 'Please enter quantity' },
- { type: 'number', min: 0.01, message: 'Quantity must be > 0' }
+ { required: true, message: "Please enter quantity" },
+ { type: "number", min: 0.01, message: "Quantity must be > 0" },
]}
>
- <InputNumber style={{ width: '100%' }} placeholder="Enter quantity" />
+ <InputNumber
+ style={{ width: "100%" }}
+ placeholder="Enter quantity"
+ />
</Form.Item>
<Form.Item
label="Price ($/MWh)"
field="price"
rules={[
- { required: true, message: 'Please enter price' },
- { type: 'number', min: 0, message: 'Price must be >= 0' }
+ { required: true, message: "Please enter price" },
+ { type: "number", min: 0, message: "Price must be >= 0" },
]}
>
- <InputNumber style={{ width: '100%' }} placeholder="Enter price" />
+ <InputNumber style={{ width: "100%" }} placeholder="Enter price" />
</Form.Item>
<Form.Item>
@@ -157,11 +191,15 @@ function SubmitBidPage() {
htmlType="submit"
loading={loading}
style={{
- width: '100%',
- transition: 'transform 0.2s ease',
+ width: "100%",
+ transition: "transform 0.2s ease",
}}
- onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
- onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
+ onMouseEnter={(e) =>
+ (e.currentTarget.style.transform = "scale(1.02)")
+ }
+ onMouseLeave={(e) =>
+ (e.currentTarget.style.transform = "scale(1)")
+ }
>
Submit Bid
</Button>
diff --git a/client/src/components/index.js b/client/src/components/index.js
index 9ca2287..7010e38 100644
--- a/client/src/components/index.js
+++ b/client/src/components/index.js
@@ -1,3 +1,3 @@
-export { default as MarketDataPage } from './MarketDataPage';
-export { default as BidsPage } from './BidsPage';
-export { default as SubmitBidPage } from './SubmitBidPage';
+export { default as MarketDataPage } from "./MarketDataPage";
+export { default as BidsPage } from "./BidsPage";
+export { default as SubmitBidPage } from "./SubmitBidPage";
diff --git a/client/src/config.js b/client/src/config.js
index 2ca0e70..68acdf2 100644
--- a/client/src/config.js
+++ b/client/src/config.js
@@ -1,3 +1,4 @@
-const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://10.0.0.164:8000';
+const API_BASE_URL =
+ process.env.REACT_APP_API_BASE_URL || "http://10.0.0.164:8000";
export default API_BASE_URL;
diff --git a/client/src/index.js b/client/src/index.js
index 92a3918..a026940 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -1,23 +1,21 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './styles/index.css';
-import App from './App';
-import reportWebVitals from './utils/reportWebVitals';
-import { BrowserRouter } from 'react-router-dom';import { ConfigProvider } from '@arco-design/web-react';
-import enUS from '@arco-design/web-react/es/locale/en-US';
+import React from "react";
+import ReactDOM from "react-dom/client";
+import "./styles/index.css";
+import App from "./App";
+import reportWebVitals from "./utils/reportWebVitals";
+import { BrowserRouter } from "react-router-dom";
+import { ConfigProvider } from "@arco-design/web-react";
+import enUS from "@arco-design/web-react/es/locale/en-US";
-
-
-
-const root = ReactDOM.createRoot(document.getElementById('root'));
+const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<ConfigProvider locale={enUS}>
- <App />
+ <App />
</ConfigProvider>
</BrowserRouter>
- </React.StrictMode>
+ </React.StrictMode>,
);
reportWebVitals();
diff --git a/client/src/utils/reportWebVitals.js b/client/src/utils/reportWebVitals.js
index 5253d3a..9ecd33f 100644
--- a/client/src/utils/reportWebVitals.js
+++ b/client/src/utils/reportWebVitals.js
@@ -1,6 +1,6 @@
-const reportWebVitals = onPerfEntry => {
+const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);