Randomizing quiz-answers fetched from a rest-API - arrays

So I've created a quiz-app that fetches 10 random questions with three incorrect and one correct answer to choose from. Everything is working great at the moment except that I can't get the answers to pop up randomly. And by that I mean that the correct answer is always at the bottom of the choices presented.
I know that the answer is Math.floor(Math.random() * ... ) but I honestly have no idea where to put it. I've tried everything. I could really use some help.
import React, { Component } from "react";
import "./App.css";
const API =
"https://opentdb.com/api.php?amount=10&category=20&difficulty=medium";
class App extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
score: []
};
}
componentDidMount() {
this.populateAppWithData();
}
populateAppWithData() {
fetch(API)
.then(response => response.json())
.then(data => this.setState({ results: data.results }))
.catch(error => console.error(error))
}
render() {
const { results } = this.state;
return (
<div className="App">
<h1>Quiz App</h1>
<TheCounter results={results}
Counter={this.state.score}
/>
</div>
);
}
}
class MythologyAnswers extends Component {
constructor(props) {
super(props);
this.state = {
answered: "",
isRight: null,
};
}
answerClicked(answer) {
const { hasAnswered, correct_answer } = this.props;
return event => {
const isRight = correct_answer === answer;
hasAnswered(isRight);
this.setState({
answered: answer,
isRight,
});
};
}
render() {
const { question, correct_answer, incorrect_answers } = this.props;
const { answered, isRight } = this.state;
return (
<div className="allAnswers">
{question}
{incorrect_answers && incorrect_answers
.concat(correct_answer)
.map(answer => (<div onClick={this.answerClicked(answer)}>{answer} </div>))}<br />
{answered && `You answered ${answered}`} {" "} <br />
<div className="correctAnswer"> {" "}{answered && isRight && "This is correct!"} </div> <br />
<div className="incorrectAnswer"> {" "}{answered && !isRight && `This is incorrect. The correct answer is ${this.props.correct_answer}`} {" "}</div>
</div>
)
}
}
class TheCounter extends Component {
constructor(props) {
super(props);
this.state = {
right: 0,
Counter: 0,
unanswered: 0,
};
}
questionAnswered = isRight => {
this.setState(({ Counter, right }) => ({
Counter: Counter + 1,
right: right + isRight,
}));
}
render() {
const { results } = this.props;
const { Counter } = this.state;
const unanswered = this.props.results && Counter;
if (unanswered >= 10) {
return <div className="theUnanswered"> You got {this.state.right} right out of {this.state.Counter} </div>;
}
const question = results[Counter];
return (
<div className="newQuestion">
<MythologyAnswers {...question} hasAnswered={this.questionAnswered} />
</div>
)
}
}
export default App;

We just need to apply the randomizer in the right place. You use .concat() to combine the two arrays, so it makes sense to use the randomizer right after that and before we call .map()
I've set up something that preserves alot of the existing logic you already wrote.
This will help create the new Arr and set-up the markup for your component.
renderChoices = () => {
const { correct_answer, incorrect_answers } = this.props;
let allAnswers = incorrect_answers
? incorrect_answers.concat(correct_answer)
: [];
//simple shuffle algorithm. Just inject your array and it'll pop out a new one.
function createRandom(arr) {
let myArr = [...arr]; //copy arr we pass in
let randomizedArr = []; //gets popuated by loop
while (myArr.length > 0) {
var randomIndex = Math.floor(Math.random() * myArr.length); //create random number
randomizedArr.push(myArr[randomIndex]); //add choice randomly to arr
myArr.splice(randomIndex, 1); //cut out a piece of the array then resart loop
}
//when loop has finished, return random array
return randomizedArr;
}
//call randomizer and get new Arr
let randomizedArr = createRandom(allAnswers);
//use .map to create markup with randomizedArr
return randomizedArr.map(answer => {
return <div onClick={this.answerClicked(answer)}>{answer}</div>;
});
};
So if you were to call the above function inside render, it will create the answer-set for you.
render() {
const { question } = this.props;
const { answered, isRight } = this.state;
return (
<div className="allAnswers">
{question}
{ this.renderChoices()}
<br />
{answered && `You answered ${answered}`} <br />
<div className="correctAnswer">
{answered && isRight && "This is correct!"}
</div>
<br />
<div className="incorrectAnswer">
{answered &&
!isRight &&
`This is incorrect. The correct answer is ${
this.props.correct_answer
}`}
</div>
</div>
);
}
Clean and not too-complex :)
Edit: so without changing your original code too much:
createRandom(arr) {
let myArr = [...arr]; //copy arr we pass in
let randomizedArr = []; //gets popuated by loop
while (myArr.length > 0) {
var randomIndex = Math.floor(Math.random() * myArr.length); //create random number
randomizedArr.push(myArr[randomIndex]); //add choice randomly to arr
myArr.splice(randomIndex, 1); //cut out a piece of the array then resart loop
}
//when loop has finished, return random array
return randomizedArr;
}
render() {
const { question, correct_answer, incorrect_answers } = this.props;
const { answered, isRight } = this.state;
const allAnswers =
incorrect_answers ? incorrect_answers.concat(correct_answer) : [];
const randomizedAnswers = this.createRandom(allAnswers)
return (
<div className="allAnswers">
{question}
{randomizedAnswers
.map(answer => (
<div onClick={this.answerClicked(answer)}>{answer} </div>
))}
<br />
{answered && `You answered ${answered}`} <br />
<div className="correctAnswer">
{answered && isRight && "This is correct!"}
</div>
<br />
<div className="incorrectAnswer">
{answered &&
!isRight &&
`This is incorrect. The correct answer is ${
this.props.correct_answer
}`}
</div>
</div>
);
}
So in the edited version we do a couple things:
Defined a function called createRandom() ... all it does is
randomize your answers choices.
In render, we created a new variable called allAnswers which just
concat() the incorrect_answers and correct_answer like you did
previously. If there are no incorrect_answers defined, we will use
an empty array [] as the default value.
Create a new variable called randomizedAnswers. We call
createRandom() and pass in allAnswers as the argument. It returns
the randomized array for us to use.
Then just .map() over randomizedAnswers to create your
answer-choices markup.

In below code , I suppose you are trying to concat incorrect_answer array with the correct_answer
incorrect_answers && incorrect_answers
.concat(correct_answer)
so array becomes [incorrect_answers,incorrect_answers,incorrect_answers,correct_answer]
i.e correct answer is at end
so if you want to insert correct answer at random position and suppose there are 4 options then first generate a random number between 0 and 3
let randonIndex = Math.floor(Math.random() * 4)
then insert correct answer at random index
incorrect_answers && incorrect_answers
.splice(randomIndex, 0, correct_answer);

I'd personally add it right after fetching the data. Using an inline shuffle like Fisher-Yales you don't need to modifiy your structure at all.
const fisherYalesShuffle = (a) => {
let j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
}
However, since you generally want to move as much data-logic as possible to your backend, you should also consider shuffling them there instead.

You can define a shuffle function (This one is Fisher-Yates (aka Knuth) shuffle):
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
And then just apply the function when setting the state like this:
this.setState({ results: this.shuffle(data.results) })

Related

Issue with adding conditions to secret santa app

I'm working on creating secret santa app for my christmas party.
For now it works, but I need to add some conditions.
function App() {
var names = ["John", "Martha", "Adam", "Jane", "Michael"];
const shuffle = (arr: string[]) => {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
};
const randomNames = shuffle(names);
const matches = randomNames.map((name, index) => {
return {
santa: name,
receiver: randomNames[index + 1] || randomNames[0],
};
});
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Secret santa game</p>
<select>
<option>Select your name...</option>
{names.map((name) => (
<option> {name}</option>
))}
</select>
<div>
{matches.map((match) => {
return (
<div>
{match.receiver},{match.santa}
<br />
</div>
);
})}
</div>
</header>
</div>
);
}
export default App;
John and Martha are couple so they will buy themselves presents outside of the party anyway, so if one of them is receiver and another is santa, I want to generate results again so they'll be assigned to someone else.
I'm not sure how I can accomplish this.
You could define your relationships like this:
const couples = { John: "Martha" };
then have a function that verifies that your conditions are met:
const conditions_check = (santa: string, receiver: string) => {
if (couples[santa] === receiver) {
return false;
} else if (couples[receiver] === santa) {
return false;
}
return true;
};
and then finally you can check if your conditions are met while generating your matches and shuffle your pool of names again if needed:
let matches = randomNames.map((name, index) => {
let receiver = randomNames[(index + 1) % randomNames.length];
for (let i = 0; i < 25; i++) {
if (conditions_check(name, receiver)) {
break;
}
randomNames = shuffle(names);
}
return {
santa: name,
receiver: receiver
};
});
You can check out the whole implementation in this playground.

Storing array objects using index on radio button click in react [duplicate]

This question already has answers here:
How to sort an array of integers correctly
(32 answers)
Closed 2 years ago.
How to store radio button values in ascending order of index in an array in react?
let questions;
class App extends Component {
constructor(props) {
super(props);
this.state = {
btnDisabled: true,
questions: [],
};
this.changeRadioHandler = this.changeRadioHandler.bind(this);
this.submitHandler = this.submitHandler.bind(this);
}
changeRadioHandler = (event) => {
const qn = event.target.name;
const id = event.target.value;
let text = this.props.data.matrix.options;
let userAnswer = [];
for (let j = 0; j < text.length; j++) {
userAnswer.push(false);
}
const option = text.map((t, index) => ({
text: t.text,
userAnswer: userAnswer[index],
}));
const elIndex = option.findIndex((element, i) => element.text === id);
let options = [...option];
options[elIndex] = {
...options[elIndex],
userAnswer: true,
};
const question = {
id: event.target.value,
qn,
options,
};
if (this.state.questions.some((question) => question.qn === qn)) {
questions = [
...this.state.questions.filter((question) => question.qn !== qn),
question,
];
} else {
questions = [...this.state.questions, question];
}
console.log(questions);
this.setState({ questions });
if (questions.length === text.length) {
this.setState({
btnDisabled: false,
});
}
};
submitHandler = () => {
console.log("btn clkd", questions);
};
render() {
return (
<div class="matrix-bd">
{this.props.data.header_text && (
<div class="header-qn">
<h5>{this.props.data.header_text} </h5>
</div>
)}
{this.props.data.matrix && (
<div class="grid">
{this.props.data.matrix.option_questions.map((questions, j) => {
return (
<div class="rows" key={j}>
<div class="cell main">{questions.text}</div>
{this.props.data.matrix.options.map((element, i) => {
return (
<div class="cell" key={i}>
<input
type="radio"
id={"rad" + j + i}
name={questions.text}
value={element.text}
onChange={this.changeRadioHandler}
></input>
<label htmlFor={"rad" + j + i}>{element.text}</label>
</div>
);
})}
</div>
);
})}
</div>
)}
<div class="buttonsubmit text-right">
<button
type="button"
id="QstnSubmit"
name="QstnSubmit"
class="btn btn-primary"
disabled={this.state.btnDisabled}
onClick={this.submitHandler}
>
{this.props.labels.LBLSUBMIT}
</button>
</div>
</div>
);
}
}
export default App;
I have added my code where the array is unordered with respect to index, I want to order the array using qn(const qn = event.target.name;) object. Like, how it came from the database, likewise in the same order, it should go into the db.
You can use the sort() method, then have a callback function with two arguments and return them in an accending order, like this:
ans.sort((a, b) =>{return a-b});

Dynamic React Text Inputs

I am relatively new to React so sorry if this is a really easy/stupid question.
So I am making an app where you provide a number of months and depending on the number of months that number of text fields need to show up. Ex. 5 months = 5 new text fields. How would I go about doing that? I have tried various methods to no avail hence why I am asking here. Thank you for your help.
class MonthlyInputs extends React.Component {
render() {
let monthInputs = [1, 2, 3];
let items = [];
function updateInputs(event) {
monthInputs = [];
for (let i = 0; i < event.target.value; i++) {
monthInputs.push(i);
}
console.log(monthInputs)
for (let i = 0; i < monthInputs.length; i++) {
items.push(<li key={i}>list item {i}</li>);
}
}
for (const [index, value] of monthInputs.entries()) {
items.push(<li key={index}>{value}</li>)
}
return (
<div>
<input onChange={updateInputs} />
{items}
</div>
)
}
}
I attempted what you're trying to implement.
Let me know if you have any questions.
https://codesandbox.io/s/tender-liskov-iicor?fontsize=14&hidenavigation=1&theme=dark
Consider saving your variables in state. Also you shouldn't declare funcions in render funcion. Here is code:
class App extends Component{
constructor(){
super();
this.state = {
monthInputs: [],
}
}
updateInputs = (event) => {
let monthInputs = [];
for (let i = 0; i < event.target.value; i++) {
monthInputs.push(i);
}
this.setState({
monthInputs
})
}
render(){
const {monthInputs} = this.state;
return(
<div>
<input type="number" value={monthInputs} onChange={this.updateInputs} />
{monthInputs.length > 0 && monthInputs.map((number, index)=>
<li key={number}>{number}</li> )}
</div>
)
}
}
And here is link to codesandbox

Clearing dynamically added input fields

I am creating an app, where users should compose a correct sentence from shuffled one. Like on the following picture. The problem is that I am not implementing the app in a right way. For example, I do not change inputs through state. And my main question is how to clear input of a particular line (a bunch of inputs) ? I need it when a user types something wrong and needs to start again. I know that input field should be controlled through state, but it is impossible since all inputs are generated according to number of words. And in one line there maybe more than one input. Let me explain how my code works, so that you have an idea my implementation.
This is where I get data from.
export default {
id:'1',
parts:[
{
speaker:'Speaker1',
words:'Hello how are you?'
},
{ speaker:'Speaker2',
words:'I am OK, thanks'
},
{ speaker:'Speaker1',
words:'What are your plans for Saturday?'
}
]
}
import React, { Component } from 'react';
import {CopyToClipboard} from 'react-copy-to-clipboard';
import { MdDoneAll } from "react-icons/md";
// This is the array where data from inputs is pushed to
var pushArr = [];
// Initial points
var points = 0;
class DialogueShuffleFrame extends Component {
constructor(props) {
super(props)
this.state = {
// Variable to show correct or incorrect notification
showCorrect:false
}
this.writeSometihng = this.writeSometihng.bind(this)
}
// Function that is triggered when a tick is clicked
// Pushes value to the pushArr array when onBlur function is triggered
writeSometihng(e) {
e.preventDefault()
pushArr.push(e.target.value)
console.log(pushArr)
}
// Function check if array of value from input matches to an array from
// initial data source
checkLines(arr, lines) {
let joinedStr = arr.join(' ');
//console.log(joinedStr);
lines[0].parts.map((obj) => {
let line = obj.words
if (joinedStr === line) {
this.setState({
showCorrect:true
})
pushArr = [];
points += 80;
} else {
pushArr = [];
}
})
}
// Resets pushArr array
reset() {
pushArr.length = 0
console.log('clicked')
}
// Shuffles words
formatWords(words) {
const splittedWords = words.split(' ')
const shuffledArray = this.shuffle(splittedWords)
return (
shuffledArray.map((word, index) => (
<>
<input className="word-to-drop-input" id={index} onBlur={this.writeSometihng} size={2} />
<CopyToClipboard text={word}>
<span key={uid(word)} value={word} className="word-to-drop">{word}</span>
</CopyToClipboard>
</>
))
)
}
shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
render() {
const {lines} = this.props
const shuffles = lines[0].parts && (
lines[0].parts.map((element,i) => (
<>
<li className="line" key={i}><span>{element.speaker}{": "}</span><span>{this.formatWords(element.words)}</span></li>
<MdDoneAll type="button" onClick={() => {this.checkLines(pushArr, lines)}} style={{color:'white'}}/>
</>
))
)
return (
<>
<h1 className="centered" style={{color:'white'}}>Dialogue shuffle frame</h1>
<ul className="lines-container">
{shuffles}
</ul>
{<div className="reactangular">{this.state.showCorrect ? 'Correct' : 'Incorrect'}</div>}
<div>{points}</div>
<div className="reactangular" onClick={() => this.reset()}>Reset</div>
</>
)
}
}
export default DialogueShuffleFrame;
I'd recommend to use proper data structure with state management, but that's a different story.
To solve your specific issue of clearing input elements, you can use ReactDOM.findDOMNode to access the DOM node and traverse the input elements and set the value to empty string.
Something like this:
class App extends React.Component {
check = (ref) => {
const inputElements = ReactDOM.findDOMNode(ref.target).parentNode.getElementsByTagName('input');
[...inputElements].forEach(el => el.value = '')
}
render() {
return (
<div>
<ul>
<li>
<input />
<input />
<button onClick={this.check}>Check</button>
</li>
<li>
<input />
<input />
<input />
<button onClick={this.check}>Check</button>
</li>
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>

React render from a loop

I'm trying to do something very simple but its not playing well with my code. I can see it render but only 3 times and not 9
const renderTempBoxes = () => {
for (var i = 0; i < 10; i++) {
console.log('i = ', i);
return <div className={styles.box} key={i} />;
}
};
const Component = () => {
return (
{renderTempBoxes()}
)
}
This doesn't even work, which is overkill to use an array when I just want 9 boxes to render.
UPDATE:
const Component = () => {
return (
<div>
{
[...Array(10)].map((x, i) => {
console.log('i = ', i);
return <div className={styles.box} key={i} />;
})
}
</div>
)
}
The first issue is that you simply cannot return individual elements from within the for loop like that. This is not specific to React, this is simply a JavaScript issue. Instead you can try something like this using Array.from to map an array of elements:
const renderTempBoxes = () => Array.from({ length: 10 }).map((v, i) =>
<div className={styles.box} key={i}>{i}</div>
);
Or simply the for loop with Array.prototype.push to generate an array of elements and return it:
const renderTempBoxes = () => {
let els = [];
for (let i = 0; i < 10; i++) {
els.push(<div className={styles.box} key={i}>{i}</div>);
}
return els;
};
Rendering the elements:
const Component = () => {
return (
<div>
{renderTempBoxes()}
</div>
)
}
Or with React.Fragment to forgo the wrapping extra node:
const Component = () => {
return (
<React.Fragment>
{renderTempBoxes()}
</React.Fragment>
)
}
The second issue with your example is that <div /> isn't going to really render anything, it's not a void/self-closing element such as <meta />. Instead you would need to do return the div element as <div className={styles.box} key={i}>{whatever}</div>.
Regarding the syntax [...Array(10)], there must be an Webpack in terms of how it handles/transpiles Array(10), [...Array(10)], [...new Array(10)], or even `[...new Array(10).keys()]. Either of the approaches described in the answer should solve your issue.
I've created a StackBlitz to demonstrate the functionality.
When trying to render multiple times the same components use an array an map over it.
export default class MyComp extends Component {
constructor(props) {
super(props)
this.state = {
array: [{key: 1, props: {...}}, {key: 2, props: {...}, ...]
}
}
render () {
return (
<div>
{this.state.array.map((element) => {
return <div key={element.key} {...element.props}>
})}
</div>
)
}
}
Remember to always set a unique key to every component you render

Resources