auction.ts
// Description of an individual auction
// RI: minBid, maxBid >= 0
export type Auction = {
name: string,
description: string,
seller: string,
endTime: number, // ms since epoch
maxBid: number,
maxBidder: string
};
App.tsx
import React, { Component } from 'react';
import { AuctionList } from './AuctionList';
import { AuctionDetails } from './AuctionDetails';
import { NewAuction } from './NewAuction';
import { Auction } from './auction';
// Indicates which page to show. If it is the details page, the argument
// includes the specific auction to show the details of.
type Page = "list" | "new" | {kind: "details", auction: Auction};
type AppState = {page: Page, auctions: ReadonlyArray<Auction>};
// Whether to show debugging information in the console.
const DEBUG: boolean = true;
// Top-level component that displays the appropriate page.
export class App extends Component<{}, AppState> {
constructor(props: {}) {
super(props);
this.state = {page: "list", auctions: []};
}
render = (): JSX.Element => {
if (this.state.page === "list") {
if (DEBUG) console.debug("rendering list page");
return <AuctionList auctions={this.state.auctions}
onNew={this.doNewClick} onShow={this.doShowClick}/>;
} else if (this.state.page === "new") {
if (DEBUG) console.debug("rendering add page");
return <NewAuction onStart={this.doStartClick} onBack={this.doBackClick}/>;
} else { // details
const auction = this.state.page.auction;
if (DEBUG) console.debug(`rendering details page for "${auction.name}"`);
return <AuctionDetails auction={auction} onChange={this.doAuctionChange}
onBack={this.doBackClick}/>;
}
};
doNewClick = (): void => {
if (DEBUG) console.debug("set state to new");
this.setState({page: "new"});
};
doShowClick = (auction: Auction): void => {
if (DEBUG) console.debug(`set state to details for "${auction.name}"`);
this.setState({page: {kind: "details", auction}});
};
doStartClick = (auction: Auction): void => {
if (DEBUG) console.debug("add an auction and then show the full list");
const auctions = this.state.auctions.concat([auction]);
this.setState({page: "list", auctions});
};
doBackClick = (): void => {
if (DEBUG) console.debug("set state to list");
this.setState({page: "list"});
};
doAuctionChange = (oldAuction: Auction, newAuction: Auction): void => {
// Build a new list of auctions with the old auction replaced by new one.
const auctions = [];
for (const auction of this.state.auctions) {
auctions.push(auction === oldAuction ? newAuction : auction);
}
// Re-display the details page with the new auction.
this.setState({auctions, page: {kind: "details", auction: newAuction}});
};
}
AuctionList.tsx
import React, { Component, MouseEvent } from 'react';
import { Auction } from './auction';
type ListProps = {
auctions: ReadonlyArray<Auction>,
onNew: () => void,
onShow: (auction: Auction) => void
};
type ListState = {
now: number, // current time when rendering
};
// Shows the list of all the auctions.
export class AuctionList extends Component<ListProps, ListState> {
constructor(props: ListProps) {
super(props);
this.state = {now: Date.now()}
}
render = (): JSX.Element => {
return (
<div>
<h2>Current Auctions</h2>
<ul>{this.renderAuctions()}</ul>
<button type="button" onClick={this.doRefreshClick}>Refresh</button>
<button type="button" onClick={this.doNewClick}>New Auction</button>
</div>);
};
renderAuctions = (): JSX.Element[] => {
const auctions: JSX.Element[] = [];
for (const auction of this.props.auctions) {
const min = (auction.endTime - this.state.now) / 60 / 1000;
const desc = (min < 0) ? "" :
<span> – {Math.round(min)} minutes remaining</span>;
auctions.push(
<li key={auction.name}>
<a href="#" onClick={(evt) => this.doAuctionClick(evt, auction)}>{auction.name}</a>
{desc}
</li>);
}
return auctions;
};
doRefreshClick = (_evt: MouseEvent<HTMLButtonElement>): void => {
// Update the time at which we are showing
this.setState({now: Date.now()});
};
doNewClick = (_evt: MouseEvent<HTMLButtonElement>): void => {
this.props.onNew(); // tell the parent to show the new auction page
};
doAuctionClick = (evt: MouseEvent<HTMLAnchorElement>, auction: Auction): void => {
evt.preventDefault();
this.props.onShow(auction);
};
}
NewAuction.tsx
import React, { Component, ChangeEvent, MouseEvent } from 'react';
import { Auction } from './auction';
type NewAuctionProps = {
onStart: (auction: Auction) => void,
onBack: () => void
};
type NewAuctionState = {
name: string,
description: string,
seller: string,
minutes: string,
minBid: string,
error: string
};
// Allows the user to create a new auction.
export class NewAuction extends Component<NewAuctionProps, NewAuctionState> {
constructor(props: NewAuctionProps) {
super(props);
this.state = {name: "", description: "", seller: "", minutes: "60",
minBid: "1", error: ""};
}
render = (): JSX.Element => {
return (
<div>
<h2>New Auction</h2>
<div>
<label htmlFor="name">Item Name:</label>
<input id="name" type="text" value={this.state.name}
onChange={this.doNameChange}></input>
</div>
<div>
<label htmlFor="description">Description:</label>
<input id="description" type="text" value={this.state.description}
onChange={this.doDescChange}></input>
</div>
<div>
<label htmlFor="seller">Seller Name:</label>
<input id="seller" type="text" value={this.state.seller}
onChange={this.doSellerChange}></input>
</div>
<div>
<label htmlFor="minutes">Minutes:</label>
<input id="minutes" type="number" min="1" value={this.state.minutes}
onChange={this.doMinutesChange}></input>
</div>
<div>
<label htmlFor="minBid">Minimum Bid:</label>
<input id="minBid" type="number" min="1" value={this.state.minBid}
onChange={this.doMinBidChange}></input>
</div>
<button type="button" onClick={this.doAddClick}>Add</button>
{this.renderError()}
</div>);
};
renderError = (): JSX.Element => {
if (this.state.error.length === 0) {
return <div></div>;
} else {
const style = {width: '300px', backgroundColor: 'rgb(246,194,192)',
border: '1px solid rgb(137,66,61)', borderRadius: '5px', padding: '5px' };
return (<div style={{marginTop: '15px'}}>
<span style={style}><b>Error</b>: {this.state.error}</span>
</div>);
}
};
doNameChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({name: evt.target.value, error: ""});
};
doDescChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({description: evt.target.value, error: ""});
};
doSellerChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({seller: evt.target.value, error: ""});
};
doMinutesChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({minutes: evt.target.value, error: ""});
};
doMinBidChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({minBid: evt.target.value, error: ""});
};
doAddClick = (_: MouseEvent<HTMLButtonElement>): void => {
// Verify that the user entered all required information.
if (this.state.name.trim().length === 0 ||
this.state.description.trim().length === 0 ||
this.state.seller.trim().length === 0 ||
this.state.minutes.trim().length === 0 ||
this.state.minBid.trim().length === 0) {
this.setState({error: "a required field is missing."});
return;
}
// Verify that minutes is a number.
const minutes = parseFloat(this.state.minutes);
if (isNaN(minutes) || minutes < 1 || Math.floor(minutes) !== minutes) {
this.setState({error: "minutes is not a positive integer"});
return;
}
// Ignore this request if the minutes or minBid are not numbers.
const minBid = parseFloat(this.state.minBid);
if (isNaN(minBid) || minBid < 1 || Math.floor(minBid) !== minBid) {
this.setState({error: "min bid is not a positive integer"});
return;
}
// We will act as if the seller bid just below the min bid. That requires
// someone else to bid at least the min bid to win the auction.
const maxBid = minBid - 1;
const maxBidder = this.state.seller;
// Record the time since epoch when this ends.
const endTime = Date.now() + minutes * 60 * 1000;
// Ask the app to start this auction (adding it to the list).
this.props.onStart({
name: this.state.name,
description: this.state.description,
seller: this.state.seller,
endTime, maxBid, maxBidder });
};
}
AuctionDetails.tsx
import React, { Component, ChangeEvent, MouseEvent } from 'react';
import { Auction } from './auction';
type DetailsProps = {
auction: Auction,
onChange: (oldAuction: Auction, newAuction: Auction) => void,
onBack: () => void
};
type DetailsState = {
now: number,
bidder: string,
amount: string,
error: string
};
// Shows an individual auction and allows bidding (if ongoing).
export class AuctionDetails extends Component<DetailsProps, DetailsState> {
constructor(props: DetailsProps) {
super(props);
const amount = this.props.auction.maxBid + 1; // min bid is 1 more
this.state = {now: Date.now(), bidder: "", amount: '' + amount, error: ""};
}
componentDidUpdate = (prevProps: DetailsProps): void => {
if (prevProps !== this.props) {
// If the user's bid is too small, update it to be valid.
const amount = parseFloat(this.state.amount);
if (!isNaN(amount) && amount < this.props.auction.maxBid + 1) {
this.setState({amount: '' + (this.props.auction.maxBid + 1),
now: Date.now(), error: ""});
}
}
};
render = (): JSX.Element => {
const auction = this.props.auction;
if (auction.endTime <= this.state.now) {
return this.renderCompleted();
} else {
return this.renderOngoing();
}
};
renderCompleted = (): JSX.Element => {
const auction = this.props.auction;
return (
<div>
<h2>{auction.name}</h2>
<p>{auction.description}</p>
<p>Winning Bid: {auction.maxBid} (by {auction.maxBidder})</p>
</div>);
};
renderOngoing = (): JSX.Element => {
const auction = this.props.auction;
const min = Math.round((auction.endTime - this.state.now) / 60 / 100) / 10;
return (
<div>
<h2>{auction.name}</h2>
<p>{auction.description}</p>
<p><i>Bidding ends in {min} minutes...</i></p>
<p>Current Bid: ${auction.maxBid} by {auction.maxBidder}</p>
<div>
<label htmlFor="bidder">Name:</label>
<input type="text" id="bidder" value={this.state.bidder}
onChange={this.doBidderChange}></input>
</div>
<div>
<label htmlFor="amount">Amount:</label>
<input type="number" min={auction.maxBid + 1}
id="amount" value={this.state.amount}
onChange={this.doAmountChange}></input>
</div>
<button type="button" onClick={this.doBidClick}>Bid</button>
<button type="button" onClick={this.doRefreshClick}>Refresh</button>
<button type="button" onClick={this.doDoneClick}>Done</button>
{this.renderError()}
</div>);
};
renderError = (): JSX.Element => {
if (this.state.error.length === 0) {
return <div></div>;
} else {
const style = {width: '300px', backgroundColor: 'rgb(246,194,192)',
border: '1px solid rgb(137,66,61)', borderRadius: '5px', padding: '5px' };
return (<div style={{marginTop: '15px'}}>
<span style={style}><b>Error</b>: {this.state.error}</span>
</div>);
}
};
doBidderChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({bidder: evt.target.value, error: ""});
};
doAmountChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({amount: evt.target.value, error: ""});
};
doBidClick = (_: MouseEvent<HTMLButtonElement>): void => {
// Verify that the user entered all required information.
if (this.state.bidder.trim().length === 0 ||
this.state.amount.trim().length === 0) {
this.setState({error: "a required field is missing."});
return;
}
// Verify that amount is a positive integer.
const amount = parseFloat(this.state.amount);
if (isNaN(amount) || Math.floor(amount) !== amount) {
this.setState({error: "amount is not an integer"});
return;
}
// Verify that amount is bigger than the current bid.
if (amount < this.props.auction.maxBid + 1) {
this.setState({error: "amount is not bigger than current bid"});
return;
}
const auction = this.props.auction;
const newAuction: Auction = {
name: auction.name,
description: auction.description,
seller: auction.seller,
endTime: auction.endTime,
maxBid: amount,
maxBidder: this.state.bidder
};
this.props.onChange(auction, newAuction);
};
doRefreshClick = (_evt: MouseEvent<HTMLButtonElement>): void => {
// Update the time at which we are showing
this.setState({now: Date.now(), error: ""});
};
doDoneClick = (_: MouseEvent<HTMLButtonElement>): void => {
return this.props.onBack(); // tell the parent to show the full list again
};
}
Full Code