Want to make a Quiz application on WordPress Gutenberg with React? The problem goes, when I try to make a repeater field of the quiz section. I use UseState to make the components Repeat the section onClick. But I can't edit the input field after repeating it.
index.js
import { registerBlockType } from "#wordpress/blocks";
import "./style.scss";
import Edit from "./edit";
import save from "./save";
import metadata from "./block.json";
registerBlockType(metadata.name, {
attributes: {
ids: { type: "array", default: 1},
question: { type: "string", default:"Example Question"},
answers: { type: "array", default: [""] },
correctAnswer: { type: "number", default: undefined },
bgColor: { type: "string", default: "#ededed" },
},
edit: Edit,
save,
});
edit.js `
import { __ } from "#wordpress/i18n";
import {
TextControl,
Flex,
FlexBlock,
FlexItem,
Button,
Icon,
} from "#wordpress/components";
import { useBlockProps } from "#wordpress/block-editor";
const { useState} = wp.element;
import "./editor.scss";
import QuizWrapper from "./components/QuizWrapper";
import TestComponent from "./components/TestComponent";
export default function Edit(props) {
const [quizwrapper, setQuizwrapper] = useState([<QuizWrapper props={props} key={0} />])
const blockProps = useBlockProps({
className: "quiz-wrapper",
});
function addSection() {
setQuizwrapper([...quizwrapper,<QuizWrapper props={props} key={quizwrapper.length} />]);
}
return (
<div {...blockProps}>
<QuizWrapper props={props}></QuizWrapper>
{quizwrapper}
<Button isPrimary onClick={addSection}>
Add Another Section
</Button>
</div>
);
}
When I use <QuizWraper /> as a component it works fine on editing and changing on Question and Answer section, image has been uploaded below
but for the UseState hook for repeating the component {quizwrapper} repeates the section but doesn't allow me to edit and change the section
Image of the editor WordPress dashboard
QuizWrapper.jsx
import React from 'react'
import Question from "./Question"
import Answers from "./Answers"
const QuizWrapper = ({props}) => {
return (
<div className='quizes'>
<Question props={props}></Question>
<Answers props={props}></Answers>
</div>
)
}
export default QuizWrapper
Question.js
import { TextControl } from "#wordpress/components";
export default function Question({props}) {
function updateQuestion(value) {
props.setAttributes({ question: value });
}
return (
<div className="question">
<h2 style={{ fontSize: "20px", margin: "20px 0 8px 0" }}>Questions: </h2>
<TextControl
value={props.attributes.question}
onChange={updateQuestion}
/>
</div>
);
}
Answer.js
import { TextControl, Button, Icon } from "#wordpress/components";
export default function Answers({ ids, props }) {
function markasCorrect(index) {
props.setAttributes({ correctAnswer: index });
}
function deleteAnswer(indextoDelete) {
const newAnswer = props.attributes.answers.filter(function (x, index) {
return index != indextoDelete;
});
props.setAttributes({ answers: newAnswer });
if (indextoDelete == props.attributes.correctAnswer) {
props.setAttributes({ correctAnswer: undefined });
}
}
return (
<div className="answers" key={ids}>
<p style={{ fontSize: "13px", margin: "20px 0 8px 0" }}>Answers: </p>
{props.attributes.answers.map((answer, index) => {
return (
<div className="answer-wrapper">
<Button onClick={() => markasCorrect(index)}>
<Icon
className="mark-as-correct"
icon={
props.attributes.correctAnswer == index ? "yes-alt" : "yes"
}
/>
</Button>
<TextControl
value={answer}
onChange={(updateanswer) => {
const newAnswers = props.attributes.answers.concat([]);
newAnswers[index] = updateanswer;
props.setAttributes({ answers: newAnswers });
}}
></TextControl>
<Button
isLink
className="deleteBtn"
onClick={() => deleteAnswer(index)}
>
Delete
</Button>
</div>
);
})}
<Button
isPrimary
onClick={() => {
props.setAttributes({
answers: props.attributes.answers.concat([""]),
});
}}
>
Add Another Answer
</Button>
</div>
);
}
Need Solution: How I can make the repeater section of the <Quizwapper>component and how I can make the repeater section where the <Question/> and <Answer/> components will work fine.
Related
Currently I am working on a function that exports components to PDFs
`
import { PDFExport } from "#progress/kendo-react-pdf";
import { useAppDispatch, useAppSelector } from "hooks";
import { get } from "lodash";
import { ManagerGeotechnicalDetail } from "pages/GeotechnicalDetail";
import PointDetail from "pages/PointDetail";
import DataChilAnalyticsDetails from "pages/Project/DataChildAnalyticsDetails";
import { FC, useEffect, useRef } from "react";
import { getListPoints } from "store/actions/datamap";
import { ETypeParent } from "../../state/reducer";
interface Props {
dataOverview: any[];
title: string;
idButton: string;
}
const DownloadPDF: FC<Props> = ({ dataOverview, title, idButton }) => {
const dispatch = useAppDispatch();
const [dataPointsSelected, projectState] = useAppSelector((state) => [
state.datamap.data,
state.project.project,
]);
const pdfExportComponent = useRef<PDFExport>(null);
useEffect(() => {
if (projectState.projectnumber) {
dispatch(
getListPoints(
get(projectState, "bounds"),
projectState,
dataPointsSelected.dataSummaryPoints
)
);
}
//eslint-disable-next-line
}, [projectState]);
const renderUIPDF = (data) => {
switch (data.typeChild) {
case ETypeParent.RAW_DATA:
return (
<PointDetail
match={{
params: {
idpoint: data.pointid,
id: projectState.projectid,
isHideForExport: true,
},
}}
/>
);
case ETypeParent.ASSIGNING_GEOLOGICAL_UNITS:
return (
<ManagerGeotechnicalDetail
match={{
params: {
idpointGeotechnical: data.pointid,
id: projectState.projectid,
isHideForExport: true,
},
}}
/>
);
case ETypeParent.DATA_ANALYTICSANALYTICS:
return (
<DataChilAnalyticsDetails
match={{
params: {
idDataChildAnalytics: data.childanalysisid,
id: projectState.projectid,
isHideForExport: true,
},
}}
/>
);
default:
return;
}
};
return (
<div>
<div className="example-config">
<button
id={idButton}
className="btn btn-success mt-2 me-4 k-button k-button-md k-rounded-md k-button-solid k-button-solid-base"
onClick={() => {
if (pdfExportComponent.current) {
pdfExportComponent.current.save();
}
}}
disabled={title.length === 0}
>
Export PDF
</button>
</div>
<div className="hide-ui">
<PDFExport
paperSize="A3"
forcePageBreak=".page-break"
landscape={true}
ref={pdfExportComponent}
fileName={title}
title={title}
scale={0.6}
margin={{
top: "0.75cm",
left: "3.6cm",
right: "3.6cm",
bottom: "0.75cm",
}}
>
{dataOverview &&
dataOverview.map((data, index) => (
<div
key={index}
style={{
width: "1620px",
}}
>
{renderUIPDF(data)}
{index !== dataOverview.length - 1 && (
<div className="page-break" />
)}
</div>
))}
</PDFExport>
</div>
</div>
);
};
export default DownloadPDF;
`
The problem I am facing now is when the dataOverview list has many objects, it will map the data to the components in renderUIPDF then I will call multiple APIs at the same time in those components.
At that time, there will be lag or website freeze :(
So is there any way I can improve the performance of my website without lag, freeze when calling too many APIs at once, the number can be up to more than 100 APIs at once?
I am testing a React component using Jest and need to mock data to override the default value of the provider. The issue I am having is that I cannot seem to structure the data properly to pass to the provider in my test.
Here is the test:
// mocked props
const commentId = 'commentId'
const page = '/user'
// mocked data for provider
const mockData = {
commentsById: {
commentId: 'string',
},
}
test('should render CommentBlock correctly with props', () => {
render(
<PostContext.Provider value={mockData}>
<CommentBlock commentId={commentId} page={page} />
</PostContext.Provider>
)
screen.debug()
})
I believe the mockData value in the test has to be changed. The error is thrown when I run the test and line 28 and 29 in the component get an undefined value the data overriding the provider is structured improperly.
Here is the component I am testing:
// import BlockButton from './blockButton';
import { useState, useContext } from 'react'
import { useHistory } from 'react-router-dom'
import dateformat from 'dateformat'
import { observer } from 'mobx-react-lite'
import UnirepContext from '../../context/Unirep'
import PostContext from '../../context/Post'
import { EXPLORER_URL } from '../../config'
import { Page, ButtonType } from '../../constants'
import BlockButton from './blockButton'
import MarkdownIt from 'markdown-it'
const markdown = new MarkdownIt({
breaks: true,
html: false,
linkify: true,
})
type Props = {
commentId: string
page: Page
}
const CommentBlock = ({ commentId, page }: Props) => {
const postContext = useContext(PostContext)
const comment = postContext.commentsById[commentId]
const commentHtml = markdown.render(comment.content)
const unirepConfig = useContext(UnirepContext)
const date = dateformat(new Date(comment.post_time), 'dd/mm/yyyy hh:MM TT')
const history = useHistory()
const [isEpkHovered, setEpkHovered] = useState<boolean>(false)
const gotoPost = () => {
if (page === Page.User) {
history.push(`/post/${comment.post_id}`, { commentId: comment.id })
}
}
return (
<div className="comment-block">
<div className="block-header comment-block-header no-padding">
<div className="info">
<span className="date">{date} |</span>
<span
className="user"
onMouseEnter={() => setEpkHovered(true)}
onMouseLeave={() => setEpkHovered(false)}
>
Post by {comment.epoch_key}{' '}
<img
src={require('../../../public/images/lighting.svg')}
/>
{isEpkHovered ? (
<span className="show-off-rep">
{comment.reputation ===
unirepConfig.commentReputation
? `This person is very modest, showing off only ${unirepConfig.commentReputation} Rep.`
: `This person is showing off ${comment.reputation} Rep.`}
</span>
) : (
<span></span>
)}
</span>
</div>
<a
className="etherscan"
target="_blank"
href={`${EXPLORER_URL}/tx/${comment.id}`}
>
<span>Etherscan</span>
<img
src={require('../../../public/images/etherscan.svg')}
/>
</a>
</div>
<div
className="block-content no-padding-horizontal"
onClick={gotoPost}
>
<div
style={{
maxHeight: page == Page.Home ? '300px' : undefined,
overflow: 'hidden',
}}
dangerouslySetInnerHTML={{
__html: commentHtml,
}}
/>
</div>
<div className="block-buttons no-padding">
<BlockButton
type={ButtonType.Boost}
count={comment.upvote}
data={comment}
/>
<BlockButton
type={ButtonType.Squash}
count={comment.downvote}
data={comment}
/>
<BlockButton type={ButtonType.Share} count={0} data={comment} />
</div>
</div>
)
}
export default observer(CommentBlock)
This was solved by properly mocking the data with this structure:
const postData = {
commentsById: {
commentId: {
id: 'commentId',
content: 'string',
post_time: '00',
reputation: 30,
epoch_key: 'epoch_key test',
},
},
}
I try make an text Animation with framer motion, React
The Animation work fine still.
Since I use text Animation after switch route. So after change path.
But will be lot of components.
So I try solving things inside the component.
Not animation when path change but when the props change.
Now the Animation also work but not perfect
The essence:
The First Animation Fine with "A A"
The second and third Animation not appear one by one but at once
codesandbox
I guess based on other post something problem with stagger children. Bit I dont know exatly what should i do that working text animation one by one when props change
More simple if I show you on codesandbox:
Data.js
const data = [
{
id: 0,
maintext: "A A",
answerButtons: [{ answerText: "Next", isCorrect: true }],
image: "",
},
{
id: 1,
maintext: "B B B B",
answerButtons: [{ answerText: "Next", isCorrect: true }],
},
{
id: 2,
maintext: "C C C C C C",
answerButtons: [{ answerText: "Next", isCorrect: true }],
},
];
export default data;
MobilBase.js
import React, { useState, useEffect } from "react";
import SpeechBubble from "../../../components/SpeechBubble/SpeechBubble";
import Data from "../../../utils/data";
import "./MobilBase.css";
const MobilBase = () => {
const [counter, setCounter] = useState(0);
const [data, setData] = useState(Data);
const nextHandler = (i) => {
if (Data[counter].answerButtons[i].isCorrect === true) {
setCounter((prevState) => {
return prevState + 1;
});
}
};
return (
<div className="base base-page-grid">
<div className="counter">
<h2>
{counter} / {data.length - 1}
</h2>
</div>
<div className="speech-bubble-outer">
<SpeechBubble maintext={data[counter].maintext} />
</div>
{data[counter].answerButtons.map((answerbutton, i) => (
<div key={i} className="base-buttons-outer">
<div>
<button
onClick={() => {
nextHandler(i);
}}
>
<strong>{answerbutton.answerText}</strong>
</button>
</div>
</div>
))}
</div>
);
};
export default MobilBase;
SpeechBubble.js
import React, { useState, useEffect } from "react";
import Animation from "./Animation";
export default function SpeechBubble(props) {
return (
<div className="frame-speech-bubble">
<div className="zumzum-animation-grid-frame">
<Animation maintext={props.maintext} />
</div>
</div>
);
}
Animation.js
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion/dist/framer-motion";
import AnimatedText from "./AnimatedText";
import "./Animate.css";
export default function Animation(props) {
// Placeholder text data, as if from API
const placeholderText = [{ type: "heading1", text: props.maintext }];
const container = {
visible: {
transition: {
staggerChildren: 0.025,
},
},
};
return (
<motion.div
className="animation-frame"
initial="hidden"
animate="visible"
variants={container}
>
<div className="container-animated-text">
{placeholderText.map((item, index) => {
return <AnimatedText {...item} key={index} />;
})}
</div>
</motion.div>
);
}
AnimatedText.js
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion/dist/framer-motion";
// Word wrapper
const Wrapper = (props) => {
// We'll do this to prevent wrapping of words using CSS
return <span className="word-wrapper">{props.children}</span>;
};
// Map API "type" vaules to JSX tag names
const tagMap = {
paragraph: "p",
heading1: "h1",
heading2: "p"
};
// AnimatedCharacters
// Handles the deconstruction of each word and character to setup for the
// individual character animations
const AnimatedCharacters = (props) => {
// Framer Motion variant object, for controlling animation
const item = {
hidden: {
x: "-200%",
color: "#0055FF",
transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.85 }
},
visible: {
x: 0,
color: "red",
transition: { ease: [0.455, 0.03, 0.515, 0.955] }
}
};
// Split each word of props.text into an array
const splitWords = props.text.split(" ");
// Create storage array
let words = [];
// Push each word into words array
for (const item of splitWords) {
words.push(item.split(""));
}
// Add a space ("\u00A0") to the end of each word
words.map((word) => {
return word.push("\u00A0");
});
// Get the tag name from tagMap
const Tag = tagMap[props.type];
return (
<Tag>
{words.map((word, index) => {
console.log(`return: word ${word} index: ${index}`);
return (
// Wrap each word in the Wrapper component
<Wrapper key={word.id}>
{words[index].flat().map((element, index) => {
return (
<span
style={{
overflow: "hidden",
display: "inline-block"
}}
key={word.id}
>
<motion.span
className="animatedtext spell"
style={{ display: "inline-block" }}
variants={item}
>
{element}
</motion.span>
</span>
);
})}
</Wrapper>
);
})}
</Tag>
);
};
export default AnimatedCharacters;
You should not use the loop index as the key in the elements output from map. Since the indices (and thus the keys) will always be the same, it doesn't let Framer track when elements have been added or removed in order to animate them.
Instead use a value like a unique id property for each element. This way React (and Framer) will know when it's rendering a different element (vs the same element with different data). This is what triggers the animations.
Here's a more thorough explanation:
react key props and why you shouldn’t be using index
I am currently working on a app similar to MyAnimeList and I'm using React. I want to click the image of the anime and have it redirect to MyAnimeList for more information. The API i am using is called Jikan APi(https://jikan.moe/). I have seen many ways to do it however most have not worked for my app? I am new to React so im still getting the hang of it.
I am currently using react-bootstrap to for the card.
import React, { useState, useEffect } from 'react';
import { Card, CardColumns, Button } from 'react-bootstrap';
import Nav from '../components/Nav';
import Auth from '../utils/auth';
import { Link } from 'react-router-dom';
import { searchJikanApi } from '../utils/API';
import { saveAnimeIds, getSavedAnimeIds } from '../utils/localStorage';
import video from '../imgs/video.mp4';
import { SAVE_ANIME } from '../utils/mutations';
import { useMutation } from '#apollo/react-hooks';
import Carousel from 'react-elastic-carousel';
import Item from '../components/Carousel/item';
import PopularAnime from '../components/PopularAnime';
import Footer from '../components/Footer';
function Home() {
const [searchedAnimes, setSearchedAnimes] = useState([]);
// create state for holding our search field data
const [searchInput, setSearchInput] = useState('');
// create state to hold saved animeId values
const [savedAnimeIds, setSavedAnimeIds] = useState(getSavedAnimeIds());
const [saveAnime] = useMutation(SAVE_ANIME);
useEffect(() => {
return () => saveAnimeIds(savedAnimeIds);
});
const breakPoints = [
{ width: 1, itemsToShow: 1 },
{ width: 550, itemsToShow: 2, itemsToScroll: 2 },
{ width: 768, itemsToShow: 3 },
{ width: 1200, itemsToShow: 4 },
];
const handleFormSubmit = async (event) => {
event.preventDefault();
if (!searchInput) {
return false;
}
try {
const response = await searchJikanApi(searchInput);
if (!response.ok) {
throw new Error('something went wrong!');
}
const { results } = await response.json();
const animeData = results.map((anime) => ({
animeId: anime.mal_id,
rating: anime.rated || ['No rating to display'],
title: anime.title,
score: anime.score,
description: anime.synopsis,
image: anime.image_url || '',
link: anime.url,
}));
setSearchedAnimes(animeData);
setSearchInput('');
} catch (err) {
console.error(err);
}
};
// function to handle saving an anime to our database
const handleSaveAnime = async (animeId) => {
// find the book in `searchedAnime` state by the matching id
const animeToSave = searchedAnimes.find(
(anime) => anime.animeId === animeId
);
// get token
const token = Auth.loggedIn() ? Auth.getToken() : null;
if (!token) {
return false;
}
try {
await saveAnime({
variables: { ...animeToSave },
});
// if book successfully saves to user's account, save book id to state
setSavedAnimeIds([...savedAnimeIds, animeToSave.animeId]);
} catch (err) {
console.error(err);
}
};
return (
<div className='container'>
<section className='header'>
<Nav></Nav>
<video className='bg-video' autoPlay muted loop>
<source src={video} type='video/mp4' />
Your browser is not supported!
</video>
<div className='heading-primary'>
<form className='search-bar' onSubmit={handleFormSubmit}>
<input
className='heading-search-bar'
name='searchInput'
type='text'
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
/>
<button className='heading-search-btn' type='submit'>
Search
</button>
</form>
</div>
</section>
<div>
<h2>
{searchedAnimes.length
? `Viewing ${searchedAnimes.length} results:`
: 'Search Anime!'}
</h2>
<Link to=''>
<CardColumns className='search-container carousel'>
<Carousel breakPoints={breakPoints}>
{searchedAnimes.map((anime) => {
return (
<Item>
<Card key={anime.animeId} className='anime-card'>
{anime.image ? (
<Card.Img
src={anime.image}
alt={`The cover for ${anime.title}`}
variant='top'
/>
) : null}
<Card.Body>
<Card.Title>{anime.title}</Card.Title>
<p className='small'>Rating: {anime.rating}</p>
<p className='small'>Score: {anime.score}/10</p>
<Card.Text>{anime.description}</Card.Text>
{Auth.loggedIn() && (
<Button
disabled={savedAnimeIds?.some(
(savedAnimeId) => savedAnimeId === anime.animeId
)}
className='btn-block btn-info'
onClick={() => handleSaveAnime(anime.animeId)}
>
{savedAnimeIds?.some(
(savedAnimeId) => savedAnimeId === anime.animeId
)
? 'This anime has already been saved!'
: 'Save this anime!'}
</Button>
)}
</Card.Body>
</Card>
</Item>
);
})}
</Carousel>
</CardColumns>
</Link>
<PopularAnime />
</div>
<Footer />
</div>
);
}
export default Home;
Thank You For Any Help!
try to add onPress to your image to go to our link
<TouchableHighlight
onPress={() => Linking.openURL('https://website.com')}>
<Image
source={{uri: image source}}
/>
</TouchableHighlight>
I am trying to update an object to the nested array
and the code below works well when I tried with React Hooks, using useState to update the state.
Then I tried to add a state management library named Recoil, a new library from Facebook, which is super simple and behaves like Hooks.
But after I changed my code adding some Recoil codes, like the atom, useRecoilState, adding a new object to the nested array stopped working.
I tried a few different methods to solve this and find other solutions but didn't go well.
Does anyone know how to solve this? You can check the code and codesandbox link below.
https://codesandbox.io/s/nested-arrays-qrw95?file=/src/App.js:0-1547
import React from "react";
import { RecoilRoot, atom, useRecoilState } from "recoil";
const todoListState = atom({
key: "TodoList",
default: [
{
name: "Lee",
device: [
{ name: "MBPR", price: 3000 },
{ name: "MBA", price: 2000 }
]
}
]
});
export function TodoApp() {
const [list, setList] = useRecoilState(todoListState);
return (
<>
{list.map((x, i) => {
return (
<div style={{ border: "1px solid black", textAlign: "left" }} key={i}>
<div>{x.name}</div>
<div>Devices</div>
<ul>
{x.device.map((y, j) => {
return (
<>
<li key={j}>{y.name}</li>
</>
);
})}
</ul>
<button
onClick={() => {
const clone = [...list];
clone[i].device = [...clone[i].device, { name: "IPX" }];
setList(clone);
}}
>
Add List
</button>
</div>
);
})}
<button
onClick={() => {
const newData = { name: "Kristoffer", device: [{ name: "Nokia" }] };
setList([...list, newData]);
}}
>
Add Item
</button>
</>
);
}
export default function App() {
return (
<RecoilRoot>
<div className="App">
<TodoApp />
</div>
</RecoilRoot>
);
}