307 lines
7.6 KiB
JavaScript
307 lines
7.6 KiB
JavaScript
import React from "react";
|
|
import ReactDOM from "react-dom";
|
|
import "./index.css";
|
|
|
|
function shuffleArray(array) {
|
|
/* https://stackoverflow.com/a/12646864 */
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
}
|
|
|
|
function choicesToMarkdown(choices) {
|
|
const choiceNames = choices.map((c) => c.choice);
|
|
const choiceWidth = Math.max(
|
|
...choiceNames.concat("Choice").map((x) => x.length + 3)
|
|
);
|
|
const countWidth = 6;
|
|
|
|
let r = "| Choice".padEnd(choiceWidth, " ") + "| Count |\n";
|
|
r += "|".padEnd(choiceWidth, "-") + "|-------|\n";
|
|
for (const c of choices) {
|
|
r += ("| " + c.choice).padEnd(choiceWidth, " ") + "| ";
|
|
r += c.count.toString().padStart(countWidth, " ") + "|\n";
|
|
}
|
|
return r;
|
|
}
|
|
|
|
class Decision {
|
|
constructor(indexA, indexB, choices) {
|
|
this.indexA = indexA;
|
|
this.indexB = indexB;
|
|
this.choiceA = choices[indexA];
|
|
this.choiceB = choices[indexB];
|
|
}
|
|
|
|
static createDecisions(choices) {
|
|
let decisions = [];
|
|
for (var i = 0; i < choices.length; i++) {
|
|
for (var j = 0; j < choices.length; j++) {
|
|
if (i !== j) {
|
|
let d = new Decision(i, j, choices);
|
|
decisions.push(d);
|
|
}
|
|
}
|
|
}
|
|
shuffleArray(decisions);
|
|
return decisions.slice(0, 50);
|
|
}
|
|
}
|
|
|
|
class Options extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
choicesText: "",
|
|
choices: [],
|
|
decisions: null,
|
|
decisionsMade: null,
|
|
currentDecision: null,
|
|
};
|
|
}
|
|
|
|
startChoosing() {
|
|
this.setState({
|
|
decisions: Decision.createDecisions(this.state.choices),
|
|
decisionsMade: [],
|
|
currentDecision: 0,
|
|
});
|
|
}
|
|
|
|
choicesTextToChoices(choicesText) {
|
|
let choices = choicesText.split("\n");
|
|
choices = choices.filter((choice) => choice.length > 0);
|
|
return choices;
|
|
}
|
|
|
|
choicesOnChange(event) {
|
|
let choicesText = event.target.value;
|
|
let choices = this.choicesTextToChoices(choicesText);
|
|
this.setState({
|
|
choicesText: choicesText,
|
|
choices: choices,
|
|
});
|
|
}
|
|
|
|
makeChoice(i) {
|
|
this.setState({
|
|
currentDecision: this.state.currentDecision + 1,
|
|
decisionsMade: this.state.decisionsMade.concat([i]),
|
|
});
|
|
}
|
|
|
|
startAgain() {
|
|
this.setState({
|
|
decisions: null,
|
|
currentDecision: null,
|
|
decisionsMade: null,
|
|
});
|
|
}
|
|
|
|
renderChoosing() {
|
|
const current = this.state.currentDecision;
|
|
const decision = this.state.decisions[current];
|
|
const progress = (current / this.state.decisions.length) * 100;
|
|
|
|
let choiceA = decision.choiceA;
|
|
let choiceB = decision.choiceB;
|
|
|
|
if (choiceA.length > 30) {
|
|
choiceA = choiceA.slice(0, 30);
|
|
}
|
|
if (choiceB.length > 30) {
|
|
choiceB = choiceB.slice(0, 40);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="pure-g">
|
|
<div className="pure-u-1 space-1">
|
|
<button
|
|
className="pure-button pure-u-1 pure-u-sm-9-24 pure-button-primary"
|
|
onClick={() => this.makeChoice(decision.indexA)}
|
|
>
|
|
{choiceA}
|
|
</button>
|
|
<div className="pure-u-1-24" />
|
|
<button
|
|
className="pure-button pure-u-1 pure-u-sm-9-24 pure-button-primary"
|
|
onClick={() => this.makeChoice(decision.indexB)}
|
|
>
|
|
{choiceB}
|
|
</button>
|
|
<div className="pure-u-1-24" />
|
|
<button
|
|
className="pure-button pure-u-1 pure-u-sm-4-24"
|
|
onClick={() => this.makeChoice(-1)}
|
|
>
|
|
skip
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="pure-g space-1">
|
|
<progress className="pure-u-1" max="100" value={progress}>
|
|
{" "}
|
|
</progress>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderButton() {
|
|
let choices = this.state.choices;
|
|
let button = null;
|
|
if (choices.length > 1 && choices[1]) {
|
|
button = (
|
|
<button
|
|
className="pure-button pure-button-primary space-1"
|
|
onClick={() => this.startChoosing()}
|
|
>
|
|
Start choosing
|
|
</button>
|
|
);
|
|
} else {
|
|
button = (
|
|
<button className="pure-button pure-button-disabled space-1">
|
|
Start choosing
|
|
</button>
|
|
);
|
|
}
|
|
return button;
|
|
}
|
|
|
|
renderChoiceInputField() {
|
|
return (
|
|
<div className="pure-u-1">
|
|
<h2 className="content-subhead">Type or paste your choices</h2>
|
|
<form className="pure-form" onSubmit={() => this.onSubmit}>
|
|
<fieldset className="pure-group">
|
|
<textarea
|
|
onChange={(event) => this.choicesOnChange(event)}
|
|
value={this.state.choicesText}
|
|
className="pure-input-1"
|
|
placeholder="Enter one choice per line"
|
|
/>
|
|
</fieldset>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderCurrentChoices() {
|
|
let choices = this.state.choices;
|
|
let choicesToRender = choices.map((choice, index) => {
|
|
return <li key={index}>{choice}</li>;
|
|
});
|
|
|
|
let button = this.renderButton();
|
|
return (
|
|
<div className="pure-u-1">
|
|
<h2 className="content-subhead">Your choices</h2>
|
|
<div className="pure-menu">
|
|
<ul>{choicesToRender}</ul>
|
|
</div>
|
|
<div>{button}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderSpecifyChoices() {
|
|
let input = this.renderChoiceInputField();
|
|
let output = this.renderCurrentChoices();
|
|
|
|
return (
|
|
<div className="pure-g">
|
|
{input}
|
|
{output}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderResult() {
|
|
let choices = this.state.choices.map((choice, index) => {
|
|
return {
|
|
choice: choice,
|
|
count: 0,
|
|
relativeCount: 0,
|
|
};
|
|
});
|
|
|
|
let totalCount = 0;
|
|
for (const i of this.state.decisionsMade) {
|
|
if (i !== -1) {
|
|
choices[i].count += 1;
|
|
totalCount += 1;
|
|
}
|
|
}
|
|
|
|
for (let c of choices) {
|
|
c.relativeCount = (c.count / totalCount) * 100;
|
|
}
|
|
|
|
choices.sort((a, b) => b.count - a.count);
|
|
|
|
let choicesToRender = choices.map((choice, index) => {
|
|
return (
|
|
<tr key={index}>
|
|
<td>{choice.choice}</td>
|
|
<td>{choice.count}</td>
|
|
<td>
|
|
<progress max="100" value={choice.relativeCount} />
|
|
</td>
|
|
</tr>
|
|
);
|
|
});
|
|
|
|
let choicesMarkdown = choicesToMarkdown(choices);
|
|
|
|
return (
|
|
<div className="pure-g">
|
|
<div className="pure-u-1">
|
|
<table className="pure-table space-1">
|
|
<thead>
|
|
<tr>
|
|
<th>Choice</th>
|
|
<th>Count</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>{choicesToRender}</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="pure-u-1 space-1">
|
|
<p>
|
|
<button
|
|
className="pure-button button-warning pure-button-again"
|
|
onClick={() => this.startAgain()}
|
|
>
|
|
Start again
|
|
</button>
|
|
<button
|
|
className="pure-button button-secondary"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(choicesMarkdown);
|
|
}}
|
|
>
|
|
Copy result
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
if (!this.state.decisions) {
|
|
return this.renderSpecifyChoices();
|
|
} else if (this.state.currentDecision < this.state.decisions.length) {
|
|
return this.renderChoosing();
|
|
} else {
|
|
return this.renderResult();
|
|
}
|
|
}
|
|
}
|
|
|
|
ReactDOM.render(<Options />, document.getElementById("root"));
|