diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/src/App.js | 170 | ||||
-rw-r--r-- | client/src/components/BidsPage.jsx | 134 | ||||
-rw-r--r-- | client/src/components/MarketDataPage.jsx | 257 | ||||
-rw-r--r-- | client/src/components/SubmitBidPage.jsx | 136 | ||||
-rw-r--r-- | client/src/components/index.js | 6 | ||||
-rw-r--r-- | client/src/config.js | 3 | ||||
-rw-r--r-- | client/src/index.js | 24 | ||||
-rw-r--r-- | client/src/utils/reportWebVitals.js | 4 |
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); |