App.tsx
import React, { Component } from 'react';
import { AuctionList } from './AuctionList';
import { AuctionDetails } from './AuctionDetails';
import { NewAuction } from './NewAuction';
import { Auction } from './auction';
type Page = "list" | "add" | {kind: "details", auction: Auction};
type AppState = {page: Page};
// Top-level component that displays the appropriate page.
export class App extends Component<{}, AppState> {
constructor(props: {}) {
super(props);
this.state = {page: "list"};
}
render = (): JSX.Element => {
if (this.state.page === "list") {
return <AuctionList onAdd={this.handleAdd}
onShow={this.handleShow}/>;
} else if (this.state.page === "add") {
return <NewAuction onShow={this.handleShow}
onBack={this.handleBack}/>;
} else { // details
return <AuctionDetails auction={this.state.page.auction}
onShow={this.handleShow} onBack={this.handleBack}/>;
}
};
handleAdd = (): void => {
this.setState({page: "add"});
};
handleBack = (): void => {
this.setState({page: "list"});
};
handleShow = (auction: Auction): void => {
this.setState({page: {kind: "details", auction}});
};
}
AuctionList.tsx
import React, { Component, MouseEvent } from 'react';
import { Auction, parseAuction } from './auction';
type ListProps = {
onAdd: () => void,
onShow: (auction: Auction) => void
};
type ListState = {
auctions: undefined | ReadonlyArray<Auction>;
};
// Shows the list of all the auctions.
export class AuctionList extends Component<ListProps, ListState> {
constructor(props: ListProps) {
super(props);
this.state = {auctions: undefined};
}
componentDidMount = () => {
fetch("/api/list")
.then(this.handleList)
.catch(this.handleServerError);
};
render = (): JSX.Element => {
if (this.state.auctions === undefined) {
return <p>Loading...</p>;
} else {
const now = Date.now();
const auctions: JSX.Element[] = [];
for (const auction of this.state.auctions) {
const min = (auction.endTime - 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.handleShow(evt, auction)}>{auction.name}</a>
{desc}
</li>);
}
return (
<div>
<h2>Current Auctions</h2>
<ul>{auctions}</ul>
<button type="button" onClick={this.props.onAdd}>Add</button>
</div>);
}
};
handleShow = (evt: MouseEvent<HTMLAnchorElement>, auction: Auction) => {
evt.preventDefault();
this.props.onShow(auction);
};
// Called with the response from a request to /list
handleList = (res: Response) => {
if (res.status === 200) {
res.json().then(this.handleListJson).catch(this.handleServerError);
} else {
this.handleServerError(res);
}
};
// Called with the JSON of the response from /list
handleListJson = (vals: any) => {
if (typeof vals !== "object" || vals === null || !('auctions' in vals) ||
!Array.isArray(vals.auctions)) {
console.error("bad data from /list: no auctions", vals)
return;
}
const auctions: Auction[] = [];
for (const val of vals.auctions) {
const auction = parseAuction(val);
if (auction !== undefined) {
auctions.push(auction);
}
}
this.setState({auctions: auctions});
};
// Called when we fail to communicate correctly with the server.
handleServerError = (_: Response) => {
// TODO: show the error to the user, with more information
console.error(`unknown error talking to server`);
};
}
AuctionDtails.tsx
import React, { Component, ChangeEvent, MouseEvent } from 'react';
import { Auction, parseAuction } from './auction';
type DetailsProps = {
auction: Auction,
onShow: (auction: Auction) => void,
onBack: () => void
};
type DetailsState = {
bidder: string,
amount: number
};
// Shows an individual auction and allows bidding (if ongoing).
export class AuctionDetails extends Component<DetailsProps, DetailsState> {
constructor(props: DetailsProps) {
super(props);
this.state = {bidder: "", amount: this.props.auction.maxBid+1};
}
componentDidUpdate = (_: DetailsProps): void => {
if (this.state.amount <= this.props.auction.maxBid) {
this.setState({amount: this.props.auction.maxBid+1})
}
};
render = (): JSX.Element => {
const auction = this.props.auction;
const now = Date.now()
if (auction.endTime <= now) {
return (
<div>
<h2>{auction.name}</h2>
<p>{auction.description}</p>
<p>Winning Bid: {auction.maxBid} (by {auction.maxBidder})</p>
</div>);
} else {
const min = Math.round((auction.endTime - 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}</p>
<div>
<label htmlFor="bidder">Name:</label>
<input type="text" id="bidder" value={this.state.bidder}
onChange={this.handleBidderChange}></input>
</div>
<div>
<label htmlFor="amount">Amount:</label>
<input type="number" min={auction.maxBid + 1}
id="amount" value={this.state.amount}
onChange={this.handleAmountChange}></input>
</div>
<button type="button" onClick={this.handleBid}>Bid</button>
<button type="button" onClick={this.props.onBack}>Done</button>
</div>);
}
};
handleBidderChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({bidder: evt.target.value});
};
handleAmountChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({amount: parseInt(evt.target.value)});
};
handleBid = (_: MouseEvent<HTMLButtonElement>): void => {
if (this.state.bidder.length > 0) {
const url = "/api/bid" +
"?name=" + encodeURIComponent(this.props.auction.name) +
"&bidder=" + encodeURIComponent(this.state.bidder) +
"&amount=" + encodeURIComponent(this.state.amount);
fetch(url, {method: "POST"})
.then(this.handleBidResponse)
.catch(this.handleServerError);
}
};
handleBidResponse = (res: Response) => {
if (res.status === 200) {
res.json().then(this.handleBidJson).catch(this.handleServerError);
} else {
this.handleServerError(res);
}
};
handleBidJson = (val: any) => {
if (typeof val !== "object" || val === null) {
console.error("bad data from /bid: not a record", val)
return;
}
const auction = parseAuction(val);
if (auction !== undefined) {
this.props.onShow(auction);
}
};
// Called when we fail to communicate correctly with the server.
handleServerError = (_: Response) => {
// TODO: show the error to the user, with more information
console.error(`unknown error talking to server`);
};
}
NewAuction.tsx
import React, { Component, ChangeEvent, MouseEvent } from 'react';
import { Auction, parseAuction } from './auction';
type NewAuctionProps = {
onShow: (auction: Auction) => void,
onBack: () => void
};
type NewAuctionState = {
name: string,
description: string,
seller: string,
minutes: number,
minBid: number
};
// 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};
}
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.handleNameChange}></input>
</div>
<div>
<label htmlFor="description">Description:</label>
<input id="description" type="text" value={this.state.description}
onChange={this.handleDescChange}></input>
</div>
<div>
<label htmlFor="seller">Seller Name:</label>
<input id="seller" type="text" value={this.state.seller}
onChange={this.handleSellerChange}></input>
</div>
<div>
<label htmlFor="minutes">Minutes:</label>
<input id="minutes" type="number" min="1" value={this.state.minutes}
onChange={this.handleMinutesChange}></input>
</div>
<div>
<label htmlFor="minBid">Minimum Bid:</label>
<input id="minBid" type="number" min="1" value={this.state.minBid}
onChange={this.handleMinBidChange}></input>
</div>
<button type="button" onClick={this.handleAdd}>Add</button>
</div>);
};
handleNameChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({name: evt.target.value});
};
handleDescChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({description: evt.target.value});
};
handleSellerChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({seller: evt.target.value});
};
handleMinutesChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({minutes: parseInt(evt.target.value)});
};
handleMinBidChange = (evt: ChangeEvent<HTMLInputElement>): void => {
this.setState({minBid: parseInt(evt.target.value)});
};
handleAdd = (_: MouseEvent<HTMLButtonElement>): void => {
if (this.state.name.length > 0 && this.state.description.length > 0 &&
this.state.seller.length > 0) {
const url = "/api/add" +
"?name=" + encodeURIComponent(this.state.name) +
"&description=" + encodeURIComponent(this.state.description) +
"&seller=" + encodeURIComponent(this.state.seller) +
"&minutes=" + encodeURIComponent(''+this.state.minutes) +
"&minBid=" + encodeURIComponent(''+this.state.minBid);
fetch(url, {method: "POST"})
.then(this.handleAddResponse)
.catch(this.handleServerError);
}
};
handleAddResponse = (res: Response) => {
if (res.status === 200) {
res.json().then(this.handleAddJson).catch(this.handleServerError);
} else {
this.handleServerError(res);
}
};
handleAddJson = (val: any) => {
if (typeof val !== "object" || val === null) {
console.error("bad data from /bid: not a record", val)
return;
}
const auction = parseAuction(val);
if (auction !== undefined) {
this.props.onShow(val);
}
};
// Called when we fail to communicate correctly with the server.
handleServerError = (_: Response) => {
// TODO: show the error to the user, with more information
console.error(`unknown error talking to server`);
};
}
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
};
// Parses unkonwn data into an Auction. Will log an error and return undefined
// if it is not a valid Auction.
export function parseAuction(val: any): undefined | Auction {
if (typeof val !== "object" || val === null) {
console.error("not an auction", val)
return undefined;
}
if (!('name' in val) || typeof val.name !== "string") {
console.error("not an object: missing or invalid 'name'", val)
return undefined;
}
if (!('description' in val) || typeof val.description !== "string") {
console.error("not an object: missing or invalid 'description'", val)
return undefined;
}
if (!('seller' in val) || typeof val.seller !== "string") {
console.error("not an object: missing or invalid 'seller'", val)
return undefined;
}
if (!('maxBid' in val) || typeof val.maxBid !== "number" ||
val.maxBid < 0 || isNaN(val.maxBid)) {
console.error("not an object: missing or invalid 'maxBid'", val)
return undefined;
}
if (!('maxBidder' in val) || typeof val.maxBidder !== "string") {
console.error("not an object: missing or invalid 'maxBidder'", val)
return undefined;
}
if (!('endTime' in val) || typeof val.endTime !== "number" ||
val.endTime < 0 || isNaN(val.endTime)) {
console.error("not an object: missing or invalid 'endTime'", val)
return undefined;
}
return {
name: val.name,
description: val.description,
seller: val.seller,
maxBid: val.maxBid,
maxBidder: val.maxBidder,
endTime: val.endTime
};
}
Full Code