diff options
Diffstat (limited to 'client/src/components/SubmitBidPage.jsx')
-rw-r--r-- | client/src/components/SubmitBidPage.jsx | 188 |
1 files changed, 188 insertions, 0 deletions
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 ( + <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> + + <Card + 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 + }} + > + <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} + </Typography.Paragraph> + </div> + + <Form + form={form} + layout="vertical" + onSubmit={handleSubmit} + initialValues={{ quantity: 0, price: 0 }} + 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' }]} + > + <DatePicker + showTime={{ + format: 'h A', + defaultValue: dayjs().hour(0).minute(0), + disabledMinutes: () => 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]} + /> + </Form.Item> + + <Form.Item + label="Quantity (MW)" + field="quantity" + rules={[ + { required: true, message: 'Please enter quantity' }, + { type: 'number', min: 0.01, message: 'Quantity must be > 0' } + ]} + > + <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' } + ]} + > + <InputNumber style={{ width: '100%' }} placeholder="Enter price" /> + </Form.Item> + + <Form.Item> + <Button + type="primary" + htmlType="submit" + loading={loading} + style={{ + width: '100%', + transition: 'transform 0.2s ease', + }} + onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} + > + Submit Bid + </Button> + </Form.Item> + </Form> + </Card> + + <style>{` + @keyframes fadeSlideIn { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } + } + `}</style> + </div> + ); +} + +export default SubmitBidPage; |