App.tsx
import React, { Component } from 'react';
import { QuarterPicker } from './QuarterPicker';
import { ClassPicker } from './ClassPicker';
interface AppState {
quarter: string | undefined; // quarter whose classes are being picked OR
// undefined if still picking quarter
}
// Top-level application that lets the user pick a quarter and then pick
// classes within that quarter.
export class App extends Component<{}, AppState> {
constructor(props: {}) {
super(props);
this.state = {quarter: undefined};
}
render = (): JSX.Element => {
if (this.state.quarter === undefined) {
return <QuarterPicker onPick={this.setQuarter}/>;
} else {
return <ClassPicker quarter={this.state.quarter}
onBack={this.back}/>;
}
};
setQuarter = (qtr: string): void => {
this.setState({quarter: qtr});
};
back = (): void => {
this.setState({quarter: undefined});
};
}
QuarterPicker.tsx
import React, { Component, ChangeEvent } from 'react';
import { QUARTERS } from './classes';
interface QuarterPickerProps {
onPick: (qtr: string) => void; // called when a quarter is picked
}
// Displays UI that allows the user to choose a quarter.
export class QuarterPicker extends Component<QuarterPickerProps, {}> {
render = (): JSX.Element => {
const options: JSX.Element[] = [
<option value="TBD">Pick a Quarter</option>
];
for (let i = 0; i < QUARTERS.length; i++) {
options.push(<option value={QUARTERS[i]}>{QUARTERS[i]}</option>);
}
return (
<select onChange={this.handleChange}>
{options}
</select>); // all buttons added as children
}
handleChange = (evt: ChangeEvent<HTMLSelectElement>): void => {
if (evt.target.value !== "TBD")
this.props.onPick(evt.target.value);
};
}
ClassPicker.tsx
import React, { Component, ChangeEvent } from 'react';
import { CLASSES } from './classes';
import { TotalCredits } from './credits';
interface ClassPickerProps {
quarter: string; // quarter whose classes are being picked
onBack: () => any; // called when the user wants to pick a new quarter
}
interface ClassPickerState {
classes: Array<string>; // list of classes currently chosen
}
// UI that allows the user to choose classes within a quarter. Displays the
// number of credits for those classes.
export class ClassPicker extends Component<ClassPickerProps, ClassPickerState> {
constructor(props: ClassPickerProps) {
super(props);
this.state = {classes: []};
}
render = (): JSX.Element => {
const allClasses = CLASSES.get(this.props.quarter)!;
let choices: JSX.Element[] = [];
for (let i = 0; i < allClasses.length; i++) {
const name = allClasses[i];
const checked = this.state.classes.indexOf(name) >= 0;
choices.push(
<div className="form-check">
<input className="form-check-input" type="checkbox"
value={allClasses[i]} checked={checked}
onChange={this.onChange} />
<label className="form-check-label">{name}</label>
</div>);
}
return (
<div>
<div>
<button type="button" className="btn btn-link"
onClick={() => this.props.onBack()}>
Back
</button>
</div>
<p>Choose your classes:</p>
<div className="choices">
{choices}
</div>
<p>Total of {TotalCredits(this.state.classes)} credits.</p>
</div>);
};
onChange = (evt: ChangeEvent<HTMLInputElement>): void => {
// NOTE: We cannot directly mutate this.state.classes!
// We need to make a copy to pass to setState.
const cls = evt.target.value as string; // class name
const index = this.state.classes.indexOf(cls);
if (evt.target.checked) {
if (index < 0) {
this.setState({classes: this.state.classes.concat(cls)});
}
} else {
if (index >= 0) {
const before = this.state.classes.slice(0, index);
const after = this.state.classes.slice(index+1);
this.setState({classes: before.concat(after)});
}
}
};
}
classes.tsx
// Quarters for which we have information.
export const QUARTERS = ["Fall 2020", "Winter 2021"];
// Classes available in each quarter.
export const CLASSES: Map<string, string[]> = new Map([
["Fall 2020", ["CSE 341", "CSE 344", "CSE 421", "CSE 431"]],
["Winter 2021", ["CSE 341", "CSE 344", "CSE 421", "CSE 444"]]
]);
credits.tsx
// Records the credits for each known course.
const CREDITS: Map<string, number> = new Map([
["CSE 341", 4],
["CSE 344", 4],
["CSE 421", 3],
["CSE 431", 3],
["CSE 444", 4],
]);
// Returns the total credits for the given list of classes. (The list should
// have no duplicates.)
export function TotalCredits(classes: string[]) {
let total : number = 0;
for (let i = 0; i < classes.length; i++) {
if (CREDITS.has(classes[i])) {
total += CREDITS.get(classes[i])!;
} else {
throw new Error("no such class: " + classes[i]);
}
}
return total;
}
Full Code