How to replace an underscore with letter React Hook - javascript

I was trying to build a hanging man game and in order to make this game, the word has been replaced by underscores like for instance (hello) ==> _ _ _ _ _. So every time when the player clicks on the keyButton it should replace the underscore by the matched letter.
But one important thing to keep in mind is that it should not replace the underscore for instance like when a user clicks on e --> _ e _ _ _ and then clicks on l --> _ _ l l _. You see the letter e that matched was being replaced by underscore which is the case in my code. The magic happens in the showMatchedLetter function
import React, { useState, useEffect } from 'react';
import { fetchButton } from '../actions';
import axios from 'axios';
import 'babel-polyfill';
const App = () => {
const [word, setWord] = useState([]);
const [underscore, setUnderscore] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
const runEffect = async () => {
const result = await axios('src/api/api.js');
setData(result.data)
}
runEffect();
}, []);
const randomWord = () => {
const chosenWord = data[Math.floor(Math.random() * data.length)];
replaceLetter(chosenWord.word);
}
const replaceLetter = (string) => {
let getString = string;
setWord(getString);
let stringToUnderScore = getString.replace(/[a-z]/gi, '_');
setUnderscore(stringToUnderScore);
}
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, [word]);
const checkMatchLetter = (word, keyButton) => {
if(word == undefined) {
return;
} else {
for(let i = 0; i < word.length; i++) {
if(word[i] == keyButton) {
let index = i;
showMatchedLetter(word[i], index);
}
}
}
}
const showMatchedLetter = (letter, index) => {
console.log(letter, index);
let string = word;
string = setCharAt(string, index, letter);
console.log(string)
}
const setCharAt = (string, index, letter) => {
if(index > string.length - 1) return string;
console.log(string)
return underscore.substr(0, index) + letter + underscore.substr(index + 1);
}
return (
<div>
<p>{word}</p>
<p>{underscore}</p>
<button onClick={randomWord}></button>
</div>
)
}
export default App;

A way to approach this would be to just keep a running list of the letters that have already been guessed.
Then simply replace any letter that wasn't guessed with an underscore to get your "hidden" word each time a new letter is guessed.
The revealOnly function below can be implemented in any way you want, but what's important is that it takes the full word string and a list of letters to replace and then returns a new string that omits any non-guessed letters.
const phrase = 'Hello World'
function revealOnly(str, guessed=[]){
const regExpr = new RegExp(`[^${guessed.join("")}\\s]`, 'ig')
return str.replace(regExpr, '_')
}
console.log(revealOnly(phrase, [])) // no letters guessed
console.log(revealOnly(phrase, ['e', 'l'])) // some letters guessed
The following is a working demo in react to demonstrate how you might use this:
const { useState } = React
const App = () => {
const [phrase, setPhrase] = useState('Hello World')
const [guessed, setGuessed] = useState([])
function onGuessLetter(letter){
if(guessed.includes(letter)) return
// add letter to guessed array
setGuessed(prev => [...prev, letter])
}
function revealOnly(str, guessed=[]){
const regExpr = new RegExp(`[^${guessed.join("")}\\s]`, 'ig')
return str.replace(regExpr, '_')
}
return (
<div>
<div> {revealOnly(phrase, guessed)} </div>
<LetterPicker onPick={onGuessLetter} />
</div>
)
}
const LetterPicker = (props)=>{
const letters = []
for(let i = 0; i < 26; i++){
letters.push(String.fromCharCode(97+i))
}
return (
<div className={'letter-picker'}>
{letters.map(l => <div onClick={()=>props.onPick(l)}>{l}</div>)}
</div>
)
}
ReactDOM.render(<App/>, document.querySelector("#root"))
.letter-picker > div {
display : inline-block;
cursor : pointer;
padding : 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Keeping a running list of letters guessed could also be useful for checking if a word has been fully revealed (although you could just check for the existence of any _ as well). Similarly in traditional hangman fashion you can show the previously letters somewhere as well.
EDIT:
How do I check if the entire word is guessed.
Just check if the revealed word is equal to the original phrase.
const curr = revealOnly(phrase, ['h','e','l','w','o','r', 'd']
const isGuessed = curr === phrase
const { useState } = React
const App = () => {
const [phrase, setPhrase] = useState('Hello World')
const [guessed, setGuessed] = useState([])
function onGuessLetter(letter){
if(guessed.includes(letter)) return
// add letter to guessed array
setGuessed(prev => [...prev, letter])
}
function revealOnly(str, guessed=[]){
const regExpr = new RegExp(`[^${guessed.join("")}\\s]`, 'ig')
return str.replace(regExpr, '_')
}
const curr = revealOnly(phrase, guessed)
const isGuessed = curr === phrase
return (
<div>
<div> {curr} </div>
<div>Gussed: {isGuessed.toString()} </div>
<LetterPicker onPick={onGuessLetter} />
</div>
)
}
const LetterPicker = (props)=>{
const letters = []
for(let i = 0; i < 26; i++){
letters.push(String.fromCharCode(97+i))
}
return (
<div className={'letter-picker'}>
{letters.map(l => <div onClick={()=>props.onPick(l)}>{l}</div>)}
</div>
)
}
ReactDOM.render(<App/>, document.querySelector("#root"))
.letter-picker > div {
display : inline-block;
cursor : pointer;
padding : 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

How can I get two non-repeated random objects from an array at first render in React?

I'm making a simple app that will simulate a hand of blackjack. I've got the 'hit' functionality (getting one random card), but I'm struggling with getting the two initial random, non repeated cards on first render. I feel like there must be a more elegant solution to what I have in mind (not to mentions it's not working).
I would appreciate any direction on this, since I'm not sure why filtering and updating the original array isn't working.
Here's the code snippet:
import {useState, useEffect} from 'react'
//import useCards from '../hooks/useCards'
import Card from '../components/Card';
import Total from '../components/Total';
import {deckArray} from '../utils/data'
export default function Home(){
const [dealCards, setDealCards] = useState(false)
const [usersCards, setUsersCards] = useState([])
const [deck, setDeck] = useState(deckArray)
const [isReset, setIsReset] = useState(true)
const [total, setTotal] = useState(0)
const [isStarted, setIsStarted] = useState(false)
useEffect(() => {
if(dealCards===true){
const randomCard = deck[Math.floor(Math.random()*deck.length)];
const newCardsArray = deck.filter(el => el.index !== randomCard.index)
const chosenCardArray = deck.filter(el => el.index === randomCard.index)
const chosenCard = chosenCardArray[0]
setDeck(newCardsArray)
setUsersCards(prevCards => [...prevCards, chosenCard])
console.log(newCardsArray.length)
setDealCards(false)
}
}, [usersCards, dealCards, deck])
useEffect(() => {
if(isReset){
setUsersCards([])
setDeck(deckArray)
setDealCards(false)
setTotal(0)
}
},[isReset])
//function to generate two random cards when user presses 'play'
useEffect(() => {
if(isStarted){
//generate two random cards
const randomCard = deck[Math.floor(Math.random()*deck.length)];
const newCardsArray = deck.filter(el => el.index !== randomCard.index)
const chosenCardArray = deck.filter(el => el.index === randomCard.index)
const chosenCard = chosenCardArray[0]
setDeck(newCardsArray)
setUsersCards(prevCards => [...prevCards, chosenCard])
const randomCard1 = deck[Math.floor(Math.random()*deck.length)];
const newCardsArray1 = deck.filter(el => el.index !== randomCard1.index)
const chosenCardArray1 = deck.filter(el => el.index === randomCard1.index)
const chosenCard1 = chosenCardArray1[1]
setDeck(newCardsArray1)
setUsersCards(prevCards => [...prevCards, chosenCard1])
console.log(newCardsArray1.length)
setDealCards(false)
}
}, [isStarted, deck, dealCards])
return (
<>
<Card usersCards={usersCards} />
{!isStarted && <button onClick={() => setIsStarted(true)}>PLAY</button>}
{isStarted && <>
<Total usersCards={usersCards} total={total} setTotal={setTotal}/>
<button onClick={() => setDealCards(true)}>HIT</button>
<button>STAND</button>
<button onClick={() => setIsReset(true)}>RESET</button>
</>}
</>
)
}
Many thanks for any help!
The code here is overusing useEffect to implement logic that should be done with simple event handlers. Only use useEffect when you're dealing with things that can't be determined before the render, like network calls, or depend on a reference to a DOM element outside of the normal rendering flow, like drawing on a <canvas>. These are side effects because they don't directly pertain to building the current render which the rest of the component body is working towards.
I don't have your utility and component imports, but here's an example that you should be able to adapt to your use case.
// utility library "import"
const cards = (() => {
const shuffle = a => {
a = a.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = ~~(Math.random() * (i + 1));
const x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
};
const frz = (...args) => Object.freeze(...args);
const suits = frz([..."HCSD"]);
const faces = frz([..."AJQK"]);
const pips = frz([...Array(9)].map((_, i) => i + 2));
const ranks = frz([...pips, ...faces]);
const cards = frz(
suits.flatMap(s => ranks.map(r => frz({
rank: r,
suit: s,
str: r + s,
value: isNaN(r) ? (r === "A" ? 11 : 10) : r,
})))
);
const shuffled = () => shuffle(cards);
return {shuffled};
})();
const Game = () => {
const startHandSize = 2;
const [deck, setDeck] = React.useState(cards.shuffled());
const [cardsDealt, setCardsDealt] =
React.useState(startHandSize);
const deal = () => {
setCardsDealt(startHandSize);
setDeck(cards.shuffled());
};
const hit = () => !bust && setCardsDealt(prev => prev + 1);
const cardsInPlay = deck.slice(-cardsDealt);
const total = cardsInPlay.reduce((a, e) => a + e.value, 0);
const bust = total > 21;
return (
<div>
<button onClick={deal}>Deal</button>
<button disabled={bust} onClick={hit}>
Hit
</button>
<div>
{cardsInPlay.map(e => (
<div key={e.str}>{e.str}</div>
))}
</div>
<div>Total: {total}</div>
<div>{bust && "Bust!"}</div>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<Game />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>

Read a text file in Javascript and count the number of each word

I am leaving this answer here because it took me about an hour+ to solve and I am hoping to help anyone else looking for a similar answer.
The question has a few parts that are all answered in separate SO posts:
Read a text file in javascript
Parse the text to get a space delimited string, excluding special characters, tabs, new lines, etc.
Count the number of each word
Display a list in descending order
Starting with an input that accepts a file and a function that will eventually sort our string, ezString:
return (
<div>
<input type="file" onChange={e => showFile(e)}/>
{ezString ? getSortedArr() : null}
</div>
);
and a function to turn that file into text (with a useState var ezString)
const [ezString, setEzString] = useState(null)
const showFile = async (e) => {
e.preventDefault()
const reader = new FileReader()
reader.onload = async (e) => {
const file = e.target.result
const goodString = file.replaceAll(/\s\s+/g, ' ')
.replaceAll(/(\r\n|\r|\n)/g, ' ')
.replaceAll(/[^a-zA-Z ]/g, "").toLowerCase()
setEzString(goodString);
};
reader.readAsText(e.target.files[0])
}
and a sorting function
const getSortedArr = () => {
let wordArray = ezString.split(' ').filter(n => n)
let wordCount = {};
for (let word of wordArray) {
if (wordCount[word]) {
wordCount[word] = wordCount[word] + 1
} else {
wordCount[word] = 1
}
}
let sortedArr = Object.entries(wordCount).sort((a, b) => b[1] - a[1])
return sortedArr ? sortedArr.map(arr => {
return (
<div key={arr[0]}>
<p style={{fontSize: 16}}>{arr[0]}: {arr[1]}</p>
</div>)
}) : null
}
With these parts we have the full component:
import React, {useState} from 'react'
const WordCounter = () => {
const [ezString, setEzString] = useState(null)
const showFile = async (e) => {
e.preventDefault()
const reader = new FileReader()
reader.onload = async (e) => {
const file = e.target.result
const goodString = file.replaceAll(/\s\s+/g, ' ')
.replaceAll(/(\r\n|\r|\n)/g, ' ')
.replaceAll(/[^a-zA-Z ]/g, "").toLowerCase()
setEzString(goodString);
};
reader.readAsText(e.target.files[0])
}
const getSortedArr = () => {
let wordArray = ezString.split(' ').filter(n => n)
let wordCount = {};
for (let word of wordArray) {
if (wordCount[word]) {
wordCount[word] = wordCount[word] + 1
} else {
wordCount[word] = 1
}
}
let sortedArr = Object.entries(wordCount).sort((a, b) => b[1] - a[1])
return sortedArr ? sortedArr.map(arr => {
return (
<div key={arr[0]}>
<p style={{fontSize: 16}}>{arr[0]}: {arr[1]}</p>
</div>)
}) : null
}
return (
<div className="App">
<input type="file" onChange={e => showFile(e)}/>
{ezString ? getSortedArr() : null}
</div>
);
}
export default WordCounter;
Areas for improvement:
I'm terrible with regex, thus the terrible regex and the need to filter empty strings out of the wordArray

Can't clear the inputs after submit - React

I am trying to clear all the inputs after pressing the submit button. However, every time I click on submit, all the spaces still stay with their input values. The app is basically a sentence guesser where you have to guess the sentence displayed. If it's correct it should display the next button, and display another sentence. The app component is the following:
function App() {
const [isNextVisible, setNextVisible] = useState(false);
const [sentence, setSentence] = useState("");
const [count, setCount] = useState(1);
const [score, setScore] = useState(0);
useEffect(() => {
axios
.get(`###`)
.then((response) => {
let sentence = response.data.data.sentence;
setSentence(sentence);
})
.catch((err) => {
console.log(err);
});
}, [count, sentence]);
let mix = function (str) {
let a = str.split("");
let n = a.length;
for (let j = n - 1; j > 0; j--) {
let x = Math.floor(Math.random() * (j + 1));
let tmp = a[j];
a[j] = a[x];
a[x] = tmp;
}
return a.join("");
};
const shuffle = (str) => {
if (str.length < 2) return str;
let splitted = str.split(" ");
let converted = [];
for (let i = 0; i < splitted.length; i++) {
converted.push(mix(splitted[i]));
}
return converted.join(" ");
};
let shuffled = shuffle(sentence);
let handleSubmit = (e) => {
e.preventDefault();
setCount(count + 1);
setScore(score + 1);
};
return (
<div className="App">
<form onSubmit={handleSubmit} className="main-container">
<h1>{shuffled}</h1>
<p>Guess the sentence! Starting typing</p>
<p>The Yellow Blocks are meant for spaces</p>
<p>Score:{score}</p>
<section className="input-container">
<Input
sentence={sentence}
setNextVisible={setNextVisible}
isNextVisible={isNextVisible}
setScore={setScore}
score={score}
/>
</section>
<section>{isNextVisible ? <button>next</button> : null}</section>
</form>
</div>
);
}```
The input component is this one:
function isValidCharacter(left = null, right = null) {
return left && right && left.toUpperCase() === right.toUpperCase();
}
export default function Input(props) {
const { sentence, setNextVisible } = props;
const refFocus = React.useRef({});
const [value, setValue] = React.useState(Array(sentence.length).fill(""));
const handleChange = (index) => (event) => {
if (isValidCharacter(event.target.value, sentence[index])) {
const nextFocus = index + 1;
if (nextFocus in refFocus.current) {
refFocus.current[nextFocus].focus();
}
}
setValue((prev) => {
const next = prev.slice(0);
next[index] = event.target.value;
return next;
});
};
React.useEffect(() => {
// compare inputs with the provided word
if (value.join("").toLocaleLowerCase() === sentence.toLocaleLowerCase()) {
setNextVisible(true);
} else {
setNextVisible(false);
}
}, [value, setNextVisible, sentence]);
return (
<div style={{ display: "flex" }}>
{sentence.split("").map((character, index) => {
const hasBackgroundGreen =
value[index] === ""
? {}
: isValidCharacter(value[index], character)
? { background: "#4caf50" }
: { background: "#e1e1e1" };
return (
<input
key={index}
ref={(ref) => (refFocus.current[index] = ref)}
type="text"
value={value[index]}
onChange={handleChange(index)}
style={hasBackgroundGreen}
/>
);
})}
</div>
);
}
You do not interact with the values in the form in any way, you yourself set them value = {* value *}, so in order to change them you need:
As an option, add this to the code "Input" component
const {sentence, setNextVisible, score} = props;
useEffect(() => {
setValue(Array(sentence.length).fill(""));
}, [score])

React text splitting with html generation

My input is
const text = 'Hello #kevin12 How are you?'
How I render it
<span>{text}</span>
How I want to render it
<span>Hello <em>#kevin12</em> How are you?</span>
My parsing function (incomplete)
const parseText = value => {
const mention = value.substring(value.lastIndexOf('#')+1)
const words = value.split(mention)
return words.map(word => word === mention ? React.createElement('em', {}, word) : word)
}
...
<span>{parseText(text)}</span>
Please help me complete this rendering function.
const parseText = value => {
const words = value.split(' ')
return words.map((word, index) => {
if (index !== words.length - 1) {
word += " "
}
return word[0] === '#' ? <em>{word}</em> : word;
})
}
Split by words, iterate over the array and find the item that start with #
export default function App() {
const text = "Hello #kevin12 How are you?";
const parseText = value => {
const mention = value.split(" ");
return mention.map(w => (w[0] === "#" ? <em> {w} </em> : <> {w}</>));
};
return (
<span>
<span>{parseText(text)}</span>
</span>
);
}
I will suggest going through the regex, here is the sample code
const text = 'Hello #kevin12 How are you?';
let finalResult = test;
let re =/(?:^|\W)#(\w+)(?!\w)/g;
let match,matches =[];
while(match =re.exec(test)){
let found ='#'+match[1];
let replace ='<em>'+found+'</em>';
finalresult =finalResult.replace(found,replace);
}
console.log(finalResult) ;

Making "Fill in the Blank" in react

I am quite new to react and I am trying to make a fill in the blank app with react. Basically, I have a passage with a word list. I want to replace all occurrences each word with a blank so that the user can type in the answer. After that, if the user clicks the submit button, it displays the result saying how many they got right.
After doing some research, I found reactStringReplace package which can safely replace strings with react components. This is how I generate the blanks in the passage:
getFillInTheBlank() {
let passage = this.passage;
for (var i = 0; i < this.wordList.length; i++) {
let regexp = new RegExp("\\b(" + this.wordList[i] + ")\\b", "gi");
passage = reactStringReplace(passage, regexp, (match, i) => (
<input type="text"></input>
));
}
return <div>passage</div>
}
However, I can't figure out a way to check each input text with respective words to calculate the score when the submit button is clicked. Can anyone suggest a way of doing this? Thank you in advance.
I made it using Mobx, but it can be easily edited to work without this library.
This is the model, which contains the word to guess and the main events callbacks
word-guess.tsx
import { makeAutoObservable } from "mobx";
export class WordGuess {
private wordToGuess: string;
// Needed to select the next empty char when the component gain focus
private nextEmptyCharIndex = 0;
guessedChars: string[];
focusedCharIndex = -1;
constructor(wordToGuess: string) {
this.wordToGuess = wordToGuess;
// In "guessedChars" all chars except white spaces are replaced with empty strings
this.guessedChars = wordToGuess.split('').map(char => char === ' ' ? char : '');
makeAutoObservable(this);
}
onCharInput = (input: string) => {
this.guessedChars[this.focusedCharIndex] = input;
this.focusedCharIndex += 1;
if(this.nextEmptyCharIndex < this.focusedCharIndex){
this.nextEmptyCharIndex = this.focusedCharIndex;
}
};
onFocus = () => this.focusedCharIndex =
this.nextEmptyCharIndex >= this.wordToGuess.length ? 0 : this.nextEmptyCharIndex;
onFocusLost = () => this.focusedCharIndex = -1;
}
Input Component
guess-input.tsx
interface GuessInputProps {
wordGuess: WordGuess;
}
export const GuessInput = observer((props: GuessInputProps) => {
const { guessedChars, focusedCharIndex, onCharInput, onFocus, onFocusLost } =
props.wordGuess;
const containerRef = useRef(null);
const onClick = useCallback(() => {
const ref: any = containerRef?.current;
ref?.focus();
}, []);
useEffect(() => {
const onKeyDown = (params: KeyboardEvent) => {
const key = params.key;
if (focusedCharIndex >= 0 && key.length === 1 && key.match(/[A-zÀ-ú]/)) {
onCharInput(params.key);
// Clear focus when last character is inserted
if(focusedCharIndex === guessedChars.length - 1) {
const ref: any = containerRef?.current;
ref?.blur();
}
}
};
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [focusedCharIndex, guessedChars]);
return <div className='guess-input'
onClick={onClick} ref={containerRef}
onFocus={onFocus} onBlur={onFocusLost} tabIndex={-1}>
{guessedChars.map((char, index) =>
<CharToGuess key={index} value={char} focused={index === focusedCharIndex} />)
}
</div>;
});
Component representing each one of the characters
char-to-guess.tsx
import './guess-input.scss';
interface CharToGuessProps {
value: string;
focused: boolean;
}
export const CharToGuess = (props: CharToGuessProps) => {
const { focused, value } = props;
return <span className={`char-to-guess ${focused ? ' focused-char' : ''}`}>
{value || '_'}
</span>;
};
You don't want to create blank strings.
Take a look at this code and see if you understand it.
var answer = document.getElementById('guess-input').name;
var hint = document.getElementById('guess-input').value;
function guessAnswer() {
$("button.guess-submit").click(function(event) {
var guess = $('#guess-input').val();
guess = guess.toLowerCase();
if ( guess == answer) {
$('#correct').show();
$('#wrong').hide();
} else {
$('#wrong').show().fadeOut(1000);
$('#guess-input').val(hint);
}
});
}
function enterSubmit() {
$("#guess-input").keyup(function(event){
if(event.keyCode == 13){
$("#guess-submit").click();
}
});
guessAnswer();
}
enterSubmit();
if ( $('#correct').css('display') == 'block') {
alert('hi');
}
I suggest to send a request to server with the questions and answers and return the results. If you save the points or the answers in the frontend, is possible that the game will be altered.

Categories