Online sample
Here is a finished product, as shown below:
You can also see online examples here.
For those of you who may at first glance look familiar, this website is from jsisweird.com/. I spent three days reconstructing the site with create-react-app + React + typescript. Unlike the site, I didn’t add any animation, and I added the Chinese-English switch and the effect of going back to the top.
Design and analysis
Look at the whole site, in fact, the overall structure is not complex, is a home page, 20 questions page and an analysis page. These questions and titles are actually a bunch of defined data. Let’s take a look at the definitions of these data.
Definition of problem data
Obviously, the data in question is an array of objects, with the following structure:
export const questions = [];
// Since the problem itself does not need to be switched between Chinese and English, we do not need to distinguish between Chinese and English. The structure of the array of items such as: {question: "true + false", answer: [" \ "truefalse \" ", "1", "NaN", "SyntaxError"], correct: "1"},
Copy the code
A. correct B. question C. answer D. correct Let’s move on.
Parse the definition of the data
Parsing data requires switching between Chinese and English, so we use an object as follows:
export const parseObject = {
"en": {output:"".// Output text
answer:"".// The user answers the text :[],
successMsg:"".// The user answers the correct text
errorMsg:"".// The user answered the wrong text
detail: [].// The answer to the question parses the text
tabs: [].// Array of Chinese and English toggle options
title:"".// Home page title text
startContent:"".// Home page paragraph text
endContent:"".// Parse the page paragraph text
startBtn:"".// Home start button textEndBtn:"".// Parse the page to restart the text
},
"zh": {// The option is the same as the en attribute value}}Copy the code
For more details, see the source code here.
In this case, since the data in the detail is just plain text, we need to convert it to HTML strings. Although a library like marked. I’ve encapsulated a simplified version of the marked utility function here, as follows:
export function marked(template) {
let result = "";
result = template.replace(/ \ \ [. +? \] (. +? \)/g.word= > {
const link = word.slice(word.indexOf('(') + 1, word.indexOf(') '));
const linkText = word.slice(word.indexOf('[') + 1, word.indexOf('] '));
return `<a href="${link}" target="blank">${linkText}</a>`;
}).replace(/\*\*\*([\s\S]*?) \*\*\*[\s]? /g.text= > '<code>' + text.slice(3,text.length - 4) + '</code>');
return result;
}
Copy the code
The conversion rule is also relatively simple, which is to match the A tag and the code tag. Here we write markdown syntax. For example, the a label should be written as follows:
[xxx] (xxx)
Copy the code
So in the above conversion function, we match a string of this structure, and its regular expression structure is as follows:
/ \ \ [. +? \] (. +? \)/g;Copy the code
The. +? To match any character, this regular expression is self-explanatory. In addition, our syntax for matching code highlighted markdown is defined as follows:
* * *//code***
Copy the code
Why did I design it this way? This is because if I also used markdown’s three template string symbols to define code highlighting, it would conflict with the JS template string, so for the sake of unnecessary trouble, I used three * instead, which is why the above regular expression matches *. As follows:
/\*\*\*([\s\S]*?) \*\*\*[\s]? /gCopy the code
So what to make of the above regular expressions? First, we need to determine \s and what \s stands for. * needs to be escaped in regular expressions, so \ is added. This regular expression means matching structures like ***//code***.
The above source code can be viewed here.
Definitions of other texts
There are also two text definitions, namely, the statistics of question options and the statistics of user answers, so we define two functions respectively to represent them, as follows:
export function getCurrentQuestion(lang="en",order= 1,total = questions.length){
return lang === 'en' ? `Question ${ order } of ${ total }` : The first `${ order }The topic,${ total }The topic `;
}
export function getCurrentAnswers(lang = "en",correctNum = 0,total= questions.length){
return lang === 'en' ? `You got ${ correctNum } out of ${ total }correct! ` : ` altogether${ total }You got the question right${ correctNum }Problem is! `;
}
Copy the code
The two utility functions take three parameters, the first parameter representing the language type, the default is “en” (English mode), the second parameter representing the current number of questions/correct questions, and the third parameter representing the total number of questions. And then returns a text based on those parameters, so there’s nothing to say about that.
Analysis of implementation ideas
Initialize the project
Skip it here. Refer to the documentation.
Implementation of the underlying components
Next, we can actually divide the page into three parts, the first part is the home page, the second part is the problem options page, and the third part is the problem resolution page. On the resolution page, because there is too much parsing, we need a back to the top effect. Before we get to the implementation of these three parts, we first need to encapsulate some common components. Let’s take a look.
Chinese and English TAB switching components
Whether it is the home page or the question page, we will see a Chinese and English switch TAB component in the upper right corner, the effect is nothing more, let’s think about how to implement. First, think about the DOM structure. We can quickly think of the structure as follows:
<div class="tab-container">
<div class="tab-item">en</div>
<div class="tab-item">zh</div>
</div>
Copy the code
At this point, we should know that the class name is going to be dynamic because we need to add a selection effect, tentatively named Active, and I’m using the event broker here to delegate the event to the parent element tab-Container. And its text is also dynamic because of the need to distinguish between Chinese and English. So we can quickly write code like this:
import React from "react";
import { parseObject } from '.. /data/data';
import ".. /style/lang.css";
export default class LangComponent extends React.Component {
constructor(props){
super(props);
this.state = {
activeIndex:0
};
}
onTabHandler(e){
const { nativeEvent } = e;
const { classList } = nativeEvent.target;
if(classList.contains('tab-item') && !classList.contains('tab-active')) {const { activeIndex } = this.state;
let newActiveIndex = activeIndex === 0 ? 1 : 0;
this.setState({
activeIndex:newActiveIndex
});
this.props.changeLang(newActiveIndex); }}render(){
const { lang } = this.props;
const { activeIndex } = this.state;
return (
<div className="tab-container" onClick = { this.onTabHandler.bind(this) }>
{
parseObject[lang]["tabs"].map(
(tab,index) =>
(
<div className={`tab-itemThe ${activeIndex= = =index ? 'tab-active' :"'} `}key={tab}>{ tab }</div>))}</div>)}}Copy the code
The CSS style code is as follows:
.tab-container {
display: flex;
align-items: center;
justify-content: center;
border:1px solid #f2f3f4;
border-radius: 5px;
position: fixed;
top: 15px;
right: 15px;
}
.tab-container > .tab-item {
padding: 8px 15px;
color: #e7eaec;
cursor: pointer;
background: linear-gradient(to right,# 515152.#f3f3f7);
transition: all .3s cubic-bezier(0.175.0.885.0.32.1.275);
}
.tab-container > .tab-item:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius:5px;
}
.tab-container > .tab-item:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius:5px;
}
.tab-container > .tab-item.tab-active..tab-container > .tab-item:hover {
color: #fff;
background: linear-gradient(to right,#53b6e7.#0c6bc9);
}
Copy the code
Js logic, we can see that we pass a lang parameter through the parent component to determine the Chinese and English schema, and then start accessing the tabs on the definition data, that is, the array. React.js renders the list usually using the Map method. Event proxy, we can see that we get the class name by getting the nativeEvent object nativeEvent, determine if the element contains the tab-item class name, and determine if the child element is clicked, and then call this.setState to change the current index item to determine which item is currently selected. Since there are only two entries, we can determine that the current index entry is either 0 or 1, and we also expose a changeLang event to the parent element so that the parent element can know the value of the language pattern in real time.
As for the styles, they are pretty basic, so there is nothing to say, except that we use a fixed position to fix the TAB components in the upper right corner. The above source code can be viewed here.
Next, let’s look at the implementation of the second component.
Bottom content component
The bottom content component is simple, with a tag wrapped around the content. The code is as follows:
import React from "react";
import ".. /style/bottom.css";
const BottomComponent = (props) = > {
return (
<div className="bottom" id="bottom">{ props.children }</div>)}export default BottomComponent;
Copy the code
The CSS code is as follows:
.bottom {
position: fixed;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 18px;
}
Copy the code
The function component is written with a fixed position at the bottom. The above source code can be viewed here. Let’s look at the implementation of the next component.
Implementation of content components
The implementation of this component is also relatively simple, with the P tag wrapped around it. As follows:
import React from "react";
import ".. /style/content.css";
const ContentComponent = (props) = > {
return (
<p className="content">{ props.children }</p>)}export default ContentComponent;
Copy the code
The CSS style code is as follows:
.content {
max-width: 35rem;
width: 100%;
line-height: 1.8;
text-align: center;
font-size: 18px;
color: #fff;
}
Copy the code
The above source code can be viewed here. Let’s look at the implementation of the next component.
Components that render HTML strings
This component uses the react.js dangerouslySetInnerHTML property to render HTML strings. The code is as follows:
import ".. /style/render.css";
export function createMarkup(template) {
return { __html: template };
}
const RenderHTMLComponent = (props) = > {
const { template } = props;
let renderTemplate = typeof template === 'string' ? template : "";
return <div dangerouslySetInnerHTML={createMarkup( renderTemplate )} className="render-content"></div>;
}
export default RenderHTMLComponent;
Copy the code
The CSS style code is as follows:
.render-content a..render-content{
color: #fff;
}
.render-content a {
border-bottom:1px solid #fff;
text-decoration: none;
}
.render-content code {
color: #245cd4;
background-color: #e5e2e2;
border-radius: 5px;
font-size: 16px;
display: block;
white-space: pre;
padding: 15px;
margin: 15px 0;
word-break: break-all;
overflow: auto;
}
.render-content a:hover {
color:#efa823;
border-color: #efa823;
}
Copy the code
As shown in the code, we can see that we are dangerouslySetInnerHTML property bind a function, pass the template string as an argument to this function component, in the function component, we return an object, structure :{__html:template}. There is little else to say.
The above source code can be viewed here. Let’s look at the implementation of the next component.
Implementation of the title component
The header component is an encapsulation of the H1 ~ H6 tag, with the following code:
import React from "react";
const TitleComponent = (props) = > {
let TagName = `h${ props.level || 1 }`;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>)}export default TitleComponent;
Copy the code
The overall logic is not complicated. It is based on a level attribute passed in by the parent element to determine which tag is h1 ~ H6, that is, dynamic component writing. Here, we use a Fragment to wrap the component. See the documentation for how to use the Fragment component. The react.js virtual DOM is limited by the need to provide a root node, so this placeholder tag is designed to solve this problem. Of course, with typescript, we also need to display a type like this:
import React, { FunctionComponent,ReactNode }from "react";
interface propType {
level:number, children? :ReactNode }// This line of code is required
type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const TitleComponent:FunctionComponent<propType> = (props:propType) = > {
// Only h1 to H6 tag names are asserted
let TagName = `h${ props.level }` as HeadingTag;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>)}export default TitleComponent;
Copy the code
The above source code can be viewed here. Let’s look at the implementation of the next component.
Implementation of the button component
The button component is one of the most basic components, and its default style is definitely not suitable for our needs, so we need to simply encapsulate it. As follows:
import React from "react";
import ".. /style/button.css";
export default class ButtonComponent extends React.Component {
constructor(props){
super(props);
this.state = {
typeArr: ["primary"."default"."danger"."success"."info"].sizeArr: ["mini".'default'."medium"."normal"."small"]}}onClickHandler(){
this.props.onClick && this.props.onClick();
}
render(){
const { nativeType,type,long,size,className,forwardedRef } = this.props;
const { typeArr,sizeArr } = this.state;
const buttonType = type && typeArr.indexOf(type) > -1 ? type : 'default';
const buttonSize = size && sizeArr.indexOf(size) > -1 ? size : 'default';
let longClassName = ' ';
let parentClassName = ' ';
if(className){
parentClassName = className;
}
if(long){
longClassName = "long-btn";
}
return (
<button
ref={forwardedRef}
type={nativeType}
className={ `btn btn-The ${buttonType } ${ longClassName } btn-size-The ${buttonSize} ${parentClassName} `}onClick={ this.onClickHandler.bind(this)}
>{ this.props.children }</button>)}}Copy the code
The CSS style code is as follows:
.btn {
padding: 14px 18px;
outline: none;
display: inline-block;
border: 1px solid var(--btn-default-border-color);
color: var(--btn-default-font-color);
border-radius: 8px;
background-color: var(--btn-default-color);
font-size: 14px;
letter-spacing: 2px;
cursor: pointer;
}
.btn.btn-size-default {
padding: 14px 18px;
}
.btn.btn-size-mini {
padding: 6px 8px;
}
.btn:not(.btn-no-hover):hover..btn:not(.btn-no-active):active..btn.btn-active {
border-color: var(--btn-default-hover-border-color);
background-color: var(--btn-default-hover-color);
color:var(--btn-default-hover-font-color);
}
.btn.long-btn {
width: 100%;
}
Copy the code
The encapsulation of buttons here is mainly to classify the buttons and add various class names to the buttons by superimposing class names, so as to achieve the realization of different types of buttons. An onClick event is then exposed. For style code, here’s the way CSS variables work. The code is as follows:
:root {
--btn-default-color:transparent;
--btn-default-border-color:#d8dbdd;
--btn-default-font-color:#ffffff;
--btn-default-hover-color:#fff;
--btn-default-hover-border-color:#a19f9f;
--btn-default-hover-font-color:# 535455;
/ * 1 * /
--bg-first-radial-first-color:rgba(50.4.157.0.271);
--bg-first-radial-second-color:rgba(7.58.255.0);
--bg-first-radial-third-color:rgba(17.195.201.1);
--bg-first-radial-fourth-color:rgba(220.78.78.0);
--bg-first-radial-fifth-color:#09a5ed;
--bg-first-radial-sixth-color:rgba(255.0.0.0);
--bg-first-radial-seventh-color:#3d06a3;
--bg-first-radial-eighth-color:#7eb4e6;
--bg-first-radial-ninth-color:#4407ed;
/ * 2 * /
--bg-second-radial-first-color:rgba(50.4.157.0.41);
--bg-second-radial-second-color:rgba(7.58.255.0.1);
--bg-second-radial-third-color:rgba(17.51.201.1);
--bg-second-radial-fourth-color:rgba(220.78.78.0.2);
--bg-second-radial-fifth-color:#090ded;
--bg-second-radial-sixth-color:rgba(255.0.0.0.1);
--bg-second-radial-seventh-color:#0691a3;
--bg-second-radial-eighth-color:#807ee6;
--bg-second-radial-ninth-color:#07ede1;
/ * * / 3
--bg-third-radial-first-color:rgba(50.4.157.0.111);
--bg-third-radial-second-color:rgba(7.58.255.0.21);
--bg-third-radial-third-color:rgba(118.17.201.1);
--bg-third-radial-fourth-color:rgba(220.78.78.0.2);
--bg-third-radial-fifth-color:#2009ed;
--bg-third-radial-sixth-color:rgba(255.0.0.0.3);
--bg-third-radial-seventh-color:#0610a3;
--bg-third-radial-eighth-color:#c07ee6;
--bg-third-radial-ninth-color:#9107ed;
/ * * / 4
--bg-fourth-radial-first-color:rgba(50.4.157.0.171);
--bg-fourth-radial-second-color:rgba(7.58.255.0.2);
--bg-fourth-radial-third-color:rgba(164.17.201.1);
--bg-fourth-radial-fourth-color:rgba(220.78.78.0.1);
--bg-fourth-radial-fifth-color:#09deed;
--bg-fourth-radial-sixth-color:rgba(255.0.0.0);
--bg-fourth-radial-seventh-color:#7106a3;
--bg-fourth-radial-eighth-color:#7eb4e6;
--bg-fourth-radial-ninth-color:#ac07ed;
}
Copy the code
The above source code can be viewed here. Let’s look at the implementation of the next component.
Note: the button component styles here are not actually written yet, and the other styles are not implemented because they are not used by the site we are implementing.
Question options component
This is actually the implementation of the part of the page in question. Let’s look at the actual code first:
import React from "react";
import { QuestionArray } from ".. /data/data";
import ButtonComponent from './buttonComponent';
import TitleComponent from './titleComponent';
import ".. /style/quiz-wrapper.css";
export default class QuizWrapperComponent extends React.Component {
constructor(props:PropType){
super(props);
this.state = {
}
}
onSelectHandler(select){
this.props.onSelect && this.props.onSelect(select);
}
render(){
const { question } = this.props;
return (
<div className="quiz-wrapper flex-center flex-direction-column">
<TitleComponent level={1}>{ question.question }</TitleComponent>
<div className="button-wrapper flex-center flex-direction-column">
{
question.answer.map((select,index) => (
<ButtonComponent
nativeType="button"
onClick={ this.onSelectHandler.bind(this.select)}
className="mt-10 btn-no-hover btn-no-active"
key={select}
long
>{ select }</ButtonComponent>))}</div>
</div>)}}Copy the code
The CSS style code is as follows:
.quiz-wrapper {
width: 100%;
height: 100vh;
padding: 1rem;
max-width: 600px;
}
.App {
height: 100vh;
overflow:hidden;
}
.App h1 {
color: #fff;
font-size: 32px;
letter-spacing: 2px;
margin-bottom: 15px;
text-align: center;
}
.App .button-wrapper {
max-width: 25rem;
width: 100%;
display: flex; {} *margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height:100vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI'.'Roboto'.'Oxygen'.'Ubuntu'.'Cantarell'.'Fira Sans'.'Droid Sans'.'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image: radial-gradient(49% 81% at 45% 47%.var(--bg-first-radial-first-color) 0.var(--bg-first-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%.var(--bg-first-radial-third-color) 1%.var(--bg-first-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%.var(--bg-first-radial-fifth-color) 1%.var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%.var(--bg-first-radial-seventh-color) 1%.var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%.var(--bg-first-radial-eighth-color) 0.var(--bg-first-radial-ninth-color) 100%);
animation:background 50s linear infinite;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.mt-10 {
margin-top: 10px;
}
.ml-5 {
margin-left: 5px;
}
.text-align {
text-align: center;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-direction-column {
flex-direction: column;
}
.w-100p {
width: 100%;
}
::-webkit-scrollbar {
width: 5px;
height: 10px;
background: linear-gradient(45deg.#e9bf89.#c9a120.#c0710a);
}
::-webkit-scrollbar-thumb {
width: 5px;
height: 5px;
background: linear-gradient(180deg.#d33606.#da5d4d.#f0c8b8);
}
@keyframes background {
0% {
background-image: radial-gradient(49% 81% at 45% 47%.var(--bg-first-radial-first-color) 0.var(--bg-first-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%.var(--bg-first-radial-third-color) 1%.var(--bg-first-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%.var(--bg-first-radial-fifth-color) 1%.var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%.var(--bg-first-radial-seventh-color) 1%.var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%.var(--bg-first-radial-eighth-color) 0.var(--bg-first-radial-ninth-color) 100%);
}
25%.50% {
background-image: radial-gradient(49% 81% at 45% 47%.var(--bg-second-radial-first-color) 0.var(--bg-second-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%.var(--bg-second-radial-third-color) 1%.var(--bg-second-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%.var(--bg-second-radial-fifth-color) 1%.var(--bg-second-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%.var(--bg-second-radial-seventh-color) 1%.var(--bg-second-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%.var(--bg-second-radial-eighth-color) 0.var(--bg-second-radial-ninth-color) 100%);
}
50%.75% {
background-image: radial-gradient(49% 81% at 45% 47%.var(--bg-third-radial-first-color) 0.var(--bg-third-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%.var(--bg-third-radial-third-color) 1%.var(--bg-third-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%.var(--bg-third-radial-fifth-color) 1%.var(--bg-third-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%.var(--bg-third-radial-seventh-color) 1%.var(--bg-third-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%.var(--bg-third-radial-eighth-color) 0.var(--bg-third-radial-ninth-color) 100%);
}
100% {
background-image: radial-gradient(49% 81% at 45% 47%.var(--bg-fourth-radial-first-color) 0.var(--bg-fourth-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%.var(--bg-fourth-radial-third-color) 1%.var(--bg-fourth-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%.var(--bg-fourth-radial-fifth-color) 1%.var(--bg-fourth-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%.var(--bg-fourth-radial-seventh-color) 1%.var(--bg-fourth-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%.var(--bg-fourth-radial-eighth-color) 0.var(--bg-fourth-radial-ninth-color) 100%); }}Copy the code
As you can see, we use the H1 tag to show the question, the button tag used by all four options, and we pass the button tag which item is selected by exposing the event onSelect. A set of questions and option answers can be determined by passing question data when using this component. Therefore, the implementation effect is shown in the figure below:
One of the more complex components in this component is the CSS layout, the use of elastic box layout and background gradient animation, etc., but not much else.
The above source code can be viewed here. Let’s look at the implementation of the next component.
Analytical components
The parsing component is really a wrapper around the parsing part of the page. Let’s look at the implementation first:
Based on the figure above, we can see that the parsing component is divided into six parts. The first part is an accurate count of user responses, which is essentially a header component, and the second part is also a header component, which is title information. The third part is the correct answer, the fourth part is the user’s answer, the fifth part is to determine whether the user answered correctly or incorrectly, and the sixth part is the actual parsing.
Let’s look at the implementation code:
import React from "react";
import { parseObject,questions } from ".. /data/data";
import { marked } from ".. /utils/marked";
import RenderHTMLComponent from './renderHTML';
import ".. /style/parse.css";
export default class ParseComponent extends React.Component {
constructor(props){
super(props);
this.state = {};
}
render(){
const { lang,userAnswers } = this.props;
const setTypeClassName = (index) = >
`answered-${ questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;
return (
<ul className="result-list">
{
parseObject[lang].detail.map((content,index) => (
<li
className={`result-itemThe ${setTypeClassName(index`)}}key={content}>
<span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span>
<div className="result-item-wrapper">
<span className="result-correct-answer">
{ parseObject[lang].output }:<span className="ml-5 result-correct-answer-value">{ questions[index].correct }</span>
</span>
<span className="result-user-answer">
{parseObject[lang].answer }:<span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
</span>
<span
className={`inline-answerThe ${setTypeClassName(index`)}} >
{
questions[index].correct === userAnswers[index]
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>
<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>
</div>
</li>))}</ul>)}}Copy the code
The CSS style code is as follows:
.result-wrapper {
width: 100%;
height: 100%;
padding: 60px 15px 40px;
overflow-x: hidden;
overflow-y: auto;
}
.result-wrapper .result-list {
list-style: none;
padding-left: 0;
width: 100%;
max-width: 600px;
}
.result-wrapper .result-list .result-item {
background-color: # 020304;
border-radius: 4px;
margin-bottom: 2rem;
color: #fff;
}
.result-content .render-content {
max-width: 600px;
line-height: 1.5;
font-size: 18px;
}
.result-wrapper .result-question {
padding:25px;
background-color: #1b132b;
font-size: 22px;
letter-spacing: 2px;
border-radius: 4px 4px 0 0;
}
.result-wrapper .result-question .order {
margin-right: 8px;
}
.result-wrapper .result-item-wrapper..result-wrapper .result-list .result-item {
display: flex;
flex-direction: column;
}
.result-wrapper .result-item-wrapper {
padding: 25px;
}
.result-wrapper .result-item-wrapper .result-user-answer {
letter-spacing: 1px;
}
.result-wrapper .result-item-wrapper .result-correct-answer .result-correct-answer-value..result-wrapper .result-item-wrapper .result-user-answer .result-user-answer-value {
font-weight: bold;
font-size: 20px;
}
.result-wrapper .result-item-wrapper .inline-answer {
padding:15px 25px;
max-width: 250px;
margin:1rem 0;
border-radius: 5px;
}
.result-wrapper .result-item-wrapper .inline-answer.answered-incorrectly {
background-color: #d82323;
}
.result-wrapper .result-item-wrapper .inline-answer.answered-correctly {
background-color: #4ee24e;
}
Copy the code
You can see that based on the six sections we analyzed earlier, we can already determine which components we need. The first step must be to render a list, since there are 20 questions to parse, and we also know that Chinese and English patterns are determined based on the lang passed. The other, userAnswers, is the user’s answers, and we know if they are right or wrong by matching them with the correct answers. That’s what this line of code means:
const setTypeClassName = (index) = > `answered-${ questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;
Copy the code
Is through the index, to determine whether the return is the correct class name or the wrong class name, through the class name to add the style, so as to determine whether the user answered correctly. Let’s break down the above code to make sense. As follows:
1. Topic information
<span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span>
Copy the code
2. Correct answer
<span className="result-correct-answer">
{ parseObject[lang].output }:
<span className="ml-5 result-correct-answer-value">{ questions[index].correct }</span>
</span>
Copy the code
3. The user answers
<span className="result-user-answer">
{parseObject[lang].answer }:
<span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
</span>
Copy the code
4. Information
<span className={`inline-answer ${ setTypeClassName(index) }`}>
{
questions[index].correct === userAnswers[index]
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>
Copy the code
5
Answer parsing is actually rendering HTML strings, so we can do this by using previously encapsulated components.
<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>
Copy the code
Once this component is complete, in fact, most of our project is complete, and it’s time to work out the details.
The above source code can be viewed here. Let’s look at the implementation of the next component.
Moving on, the implementation of the next component is also the most difficult, which is the implementation of the back to the top effect.
Go back to the top button component
The implementation idea of the back to the top component is actually very simple, which is to determine the explicit and implicit state of the back to the top button by listening to the scroll event. When the back to the top button is clicked, we need to calculate the scrollTop with a certain increment through the timer, so as to achieve the effect of smooth back to the top. Look at the code below:
import React, { useEffect } from "react";
import ButtonComponent from "./buttonComponent";
import ".. /style/top.css";
const TopButtonComponent = React.forwardRef((props, ref) = > {
const svgRef = React.createRef();
const setPathElementFill = (paths, color) = > {
if (paths) {
Array.from(paths).forEach((path) = > path.setAttribute("fill", color)); }};const onMouseEnterHandler = () = > {
constsvgPaths = svgRef? .current? .children;if (svgPaths) {
setPathElementFill(svgPaths, "#2396ef"); }};const onMouseLeaveHandler = () = > {
constsvgPaths = svgRef? .current? .children;if (svgPaths) {
setPathElementFill(svgPaths, "#ffffff"); }};const onTopHandler = () = > {
props.onClick && props.onClick();
};
return (
<ButtonComponent
onClick={onTopHandler.bind(this)}
className="to-Top-btn btn-no-hover btn-no-active"
size="mini"
forwardedRef={ref}
>
{props.children ? ( props.children) : (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4158"
onMouseEnter={onMouseEnterHandler.bind(this)}
onMouseLeave={onMouseLeaveHandler.bind(this)}
ref={svgRef}
>
<path
d="M508.214279 842.84615l34.71157 0c0 0 134.952598-188.651614 134.952598-390.030088 0-201.376427-102.047164-339.759147-118.283963-357.387643-12.227486-13.254885-51.380204-33.038464-51.380204-33.038464 s - 37 .809117 14.878872-51.379181 33.038464C443.247638 113.586988 338.550111 251.439636 338.550111 452.816063c0 201.378473 134.952598 390.030088 134.952598 390.030088L508.214279 842.84615zM457.26591 164.188456l50.948369 0 50.949392 0c9.344832 0 16.916275 7.522324 16.916275 16.966417 0 9.377578-7.688099 16.966417 16.916275 16.966417 L -50.949392 0-50.948369 0c-9.344832 0-16.917298-7.556093-16.917298-16.966417C440.347588 171.776272 448.036711 164.188456 457.26591 164.188456zM440.347588 333.852624 C0-37.47859 30.387078-67.865667 67.8656667 s67.865667 30.387078 67.865667 S440.347588 371.331213 440.347588 333.852624z"
p-id="4159"
fill={props.color}
></path>
<path
d="M460.214055 859.812567c-1.87265 5.300726-2.90005 11.000542-2.90005 16.966417 0 12.623505 4.606925 24.189935 12.244882 33.103956l21.903869 37.510312c1.325182 8.052396 8.317433 14.216793 16.750499 14.216793 8.135284 0 14.929014-5.732561 16.585747-13.386892 L0.398066 0 24.62177-42.117237c5.848195-8.284687 9.29469-18.425651 9.29469-29.325909 16.585747-13.386892 L0.398066 0 24.62177-42.117237c5.848195-8.284687 9.29469-29.325909 0-5.965875-1.027399-11.665691-2.90005-16.966417 L460.214055 859.81359 z"
p-id="4160"
fill={props.color}
></path>
<path
d="M312.354496 646.604674 C-18.358113 3.809769-28.697599 21.439288-23.246447 39.399335 L54.610782 179.871647c3.114944 10.304693 10.918677 19.086707 20.529569 24.454972l8.036024-99.843986c1.193175-14.745842 11.432377-29.226648 24.737404-36.517705-16.502859-31.912827-34.381042-71.079872-49.375547-114.721835 L312.354496 646.604674 z"
p-id="4161"
fill={props.color}
></path>
<path
d="M711.644481 646.604674 L-35.290761-7.356548 C-14.994506 43.641963-32.889061 82.810031-49.374524 114.721835 13.304004 7.291057 23.544229 21.770839 24.737404 36.517705l8.036024 99.843986c9.609869-5.368264 17.397229-14.150278 20.529569-24.454972L734.890928 686.004009C740.34208 668.043962 730.003618 650.414443 711.644481 646.604674z"
p-id="4162"
fill={props.color}
></path>
</svg>
)}
</ButtonComponent>); });const TopComponent = (props) = > {
const btnRef = React.createRef();
let scrollElement= null;
let top_value = 0,timer = null;
const updateTop = () = > {
top_value -= 20;
scrollElement && (scrollElement.scrollTop = top_value);
if (top_value < 0) {
if (timer) clearTimeout(timer);
scrollElement && (scrollElement.scrollTop = 0);
btnRef.current && (btnRef.current.style.display = "none");
} else {
timer = setTimeout(updateTop, 1); }};const topHandler = () = >{ scrollElement = props.scrollElement? .current ||document.body;
top_value = scrollElement.scrollTop;
updateTop();
props.onClick && props.onClick();
};
useEffect(() = > {
constscrollElement = props.scrollElement? .current ||document.body;
// listening the scroll event
scrollElement && scrollElement.addEventListener("scroll".(e: Event) = > {
const { scrollTop } = e.target;
if (btnRef.current) {
btnRef.current.style.display = scrollTop > 50 ? "block" : "none"; }}); });return (<TopButtonComponent ref={btnRef} {. props} onClick={topHandler.bind(this)}></TopButtonComponent>);
};
export default TopComponent;
Copy the code
The CSS style code is as follows:
.to-Top-btn {
position: fixed;
bottom: 15px;
right: 15px;
display: none;
transition: all .4s ease-in-out;
}
.to-Top-btn .icon {
width: 35px;
height: 35px;
}
Copy the code
The whole back to the top button component is divided into two parts. In the first part, we use the SVG icon as the back to the top button. The first component, TopButtonComponent, does two things. The first is to use the React. ForwardRef API to forward the ref property, or use the ref property for communication. See the documentation of the forwardRef API for details about this API. The next step is to use the ref attribute to fetch all the children of the SVG tag, and use the setAttribute method to add the function of levitating the font color to the SVG tag. That’s what this function does:
const setPathElementFill = (paths, color) = > {
// Pass in the color value and path tag array as parameters, and set the fill property value
if (paths) {
Array.from(paths).forEach((path) = > path.setAttribute("fill", color)); }};Copy the code
The second part is to listen for the element’s scroll event in the hook function useEffect to determine the implicit state of the return to the top button. And encapsulates a function that updates the value of scrollTop.
const updateTop = () = > {
top_value -= 20;
scrollElement && (scrollElement.scrollTop = top_value);
if (top_value < 0) {
if (timer) clearTimeout(timer);
scrollElement && (scrollElement.scrollTop = 0);
btnRef.current && (btnRef.current.style.display = "none");
} else {
timer = setTimeout(updateTop, 1); }};Copy the code
The timer is used to dynamically change scrollTop recursively. There’s not much else to tell.
The above source code can be viewed here. Let’s look at the implementation of the next component.
Implementation of APP components
This component is essentially a patchwork of all encapsulated common components. Let’s look at the detail code:
import React, { useReducer, useState } from "react";
import ".. /style/App.css";
import LangComponent from ".. /components/langComponent";
import TitleComponent from ".. /components/titleComponent";
import ContentComponent from ".. /components/contentComponent";
import ButtonComponent from ".. /components/buttonComponent";
import BottomComponent from ".. /components/bottomComponent";
import QuizWrapperComponent from ".. /components/quizWrapper";
import ParseComponent from ".. /components/parseComponent";
import RenderHTMLComponent from '.. /components/renderHTML';
import TopComponent from '.. /components/topComponent';
import { getCurrentQuestion, parseObject,questions,getCurrentAnswers,QuestionArray } from ".. /data/data";
import { LangContext, lang } from ".. /store/lang";
import { OrderReducer, initOrder } from ".. /store/count";
import { marked } from ".. /utils/marked";
import { computeSameAnswer } from ".. /utils/same";
let collectionUsersAnswers [] = [];
let collectionCorrectAnswers [] = questions.reduce((v,r) = > {
v.push(r.correct);
return v;
},[]);
let correctNum = 0;
function App() {
const [langValue, setLangValue] = useState(lang);
const [usersAnswers,setUsersAnswers] = useState(collectionUsersAnswers);
const [correctTotal,setCorrectTotal] = useState(0);
const [orderState,orderDispatch] = useReducer(OrderReducer,0,initOrder);
const changeLangHandler = (index: number) = > {
const value = index === 0 ? "en" : "zh";
setLangValue(value);
};
const startQuestionHandler = () = > orderDispatch({ type:"reset".payload:1 });
const endQuestionHandler = () = > {
orderDispatch({ type:"reset".payload:0 });
correctNum = 0;
};
const onSelectHandler = (select:string) = > {
// console.log(select)
orderDispatch({ type:"increment"});
if(orderState.count > 25){
orderDispatch({ type:"reset".payload:25 });
}
if(select){
collectionUsersAnswers.push(select);
}
correctNum = computeSameAnswer(correctNum,select,collectionCorrectAnswers,orderState.count);
setCorrectTotal(correctNum);
setUsersAnswers(collectionUsersAnswers);
}
const { count:order } = orderState;
const wrapperRef = React.createRef();
return (
<div className="App flex-center">
<LangContext.Provider value={langValue}>
<LangComponent lang={langValue} changeLang={changeLangHandler}></LangComponent>
{
order > 0 ? order <= 25 ?
(
<div className="flex-center flex-direction-column w-100p">
<QuizWrapperComponent
question={ questions[(order - 1 < 0 ? 0 : order - 1)]}onSelect={ onSelectHandler }
>
</QuizWrapperComponent>
<BottomComponent lang={langValue}>{getCurrentQuestion(langValue, order)}</BottomComponent>
</div>
)
:
(
<div className="w-100p result-wrapper" ref={wrapperRef}>
<div className="flex-center flex-direction-column result-content">
<TitleComponent level={1}>{ getCurrentAnswers(langValue,correctTotal)}</TitleComponent>
<ParseComponent lang={langValue} userAnswers={ usersAnswers} ></ParseComponent>
<RenderHTMLComponent template={marked(parseObject[langValue].endContent)}></RenderHTMLComponent>
<div className="button-wrapper mt-10">
<ButtonComponent nativeType="button" long onClick={endQuestionHandler}>
{parseObject[langValue].endBtn}
</ButtonComponent>
</div>
</div>
<TopComponent scrollElement={wrapperRef} color="#ffffff"></TopComponent>
</div>
)
:
(
<div className="flex-center flex-direction-column">
<TitleComponent level={1}>{parseObject[langValue].title}</TitleComponent>
<ContentComponent>{parseObject[langValue].startContent}</ContentComponent>
<div className="button-wrapper mt-10">
<ButtonComponent nativeType="button" long onClick={startQuestionHandler}>
{parseObject[langValue].startBtn}
</ButtonComponent>
</div>
</div>)}</LangContext.Provider>
</div>
);
}
export default App;
Copy the code
The above code refers to a utility function, as follows:
export function computeSameAnswer(correct = 0,userAnswer,correctAnswers,index) {
if(userAnswer === correctAnswers[index - 1] && correct <= 25){
correct++;
}
return correct;
}
Copy the code
As you can see, this function is used to calculate the correct number of answers.
In addition, we pass the lang value to each component by using context.provider, so we first need to create a context like this:
import { createContext } from "react";
export let lang = "en";
export const LangContext = createContext(lang);
Copy the code
The code is simple enough to create a context by calling the React. CreateContext API, which can be described in the documentation.
In addition, we have encapsulated a reducer function as follows:
export function initOrder(initialCount) {
return { count: initialCount };
}
export function OrderReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initOrder(action.payload ? action.payload : 0);
default:
throw new Error();
}
}
Copy the code
This is also a data communication mode for React. Js, state and behavior (or payload), and we can modify the data by calling a method. For example, this code is used like this:
const startQuestionHandler = () = > orderDispatch({ type:"reset".payload:1 });
const endQuestionHandler = () = > {
orderDispatch({ type:"reset".payload:0 });
correctNum = 0;
};
const onSelectHandler = (select:string) = > {
// console.log(select)
orderDispatch({ type:"increment"});
if(orderState.count > 25){
orderDispatch({ type:"reset".payload:25 });
}
if(select){
collectionUsersAnswers.push(select);
}
correctNum = computeSameAnswer(correctNum,select,collectionCorrectAnswers,orderState.count);
setCorrectTotal(correctNum);
setUsersAnswers(collectionUsersAnswers);
}
Copy the code
Then we use a status value or data value order to determine which part of the page to render. Order > 0 && order <= 25 renders the problem options page and order > 25 renders the parsing page.
The above source code can be viewed here.
About this website, I use vuE3.X also achieved a time, interested can refer to the source code.