Online example
The following is a finished product, as shown in the figure:
You can also click here view online examples.
Someone may look at it and see that this website is a bit familiar, yes, this website comes from https://jsisweird.com/ . I spent three days create-react-app + react + typescript
this website with 0613f2f44c7d5d. Unlike the website effect, I did not add any animation, and I added the effect of switching between Chinese and English and returning to the top.
Design analysis
Viewing the entire website, the overall structure is actually not complicated. It consists of a homepage, 20 question pages, and an analysis page. Regardless of the issues involved or the title, they are actually a bunch of well-defined data. Let's look at the definitions of these data one by one:
Definition of problem data
Obviously, the problem data is an array of objects. Let's look at the structure as follows:
export const questions = [];
//因为问题本身不需要实现中英文切换,所以我们这里也不需要区分,数组项的结构如:{question:"true + false",answer:["\"truefalse\"","1","NaN","SyntaxError"],correct:"1"},
The representation of the data can be seen at a glance, question
represents the question, answer
represents the answer option, and correct
represents the correct answer. let's continue.
Analyze the definition of data
To parse the data, you need to switch between Chinese and English, so we use an object to represent it, as follows:
export const parseObject = {
"en":{
output:"",//输出文本
answer:"",//用户回答文本:[],
successMsg:"",//用户回答正确文本
errorMsg:"",//用户回答错误文本
detail:[],//问题答案解析文本
tabs:[],//中英文切换选项数组
title:"",//首页标题文本
startContent:"",//首页段落文本
endContent:"",//解析页段落文本
startBtn:"",//首页开始按钮文本
endBtn:"",//解析页重新开始文本
},
"zh":{
//选项同en属性值一致
}
}
For more details, please check here and the source code .
Among them, because detail
is just normal text, we need to convert it into HTML string. Although there are
marked.js
such as 0613f2f44c7f8f that can help us, our conversion rules here are relatively simple, and there is no need to use marked.js
such as 0613f2f44c7fab. Therefore, I encapsulated a simplified version of marked
tool function here, as shown below:
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;
}
The conversion rule is also relatively simple, that is, to match the a
tag and the code
tag. Here we write a grammar markdown
For example, the a
should be written as follows:
[xxx](xxx)
Therefore, in the above conversion function, we match the string with this structure, and its regular expression structure is as follows:
/\[.+?\]\(.+?\)/g;
Among them, .+?
means to match any character, and this regular expression is self-evident. In addition, our matching code highlighting markdown
syntax is defined as follows:
***//code***
Why should I design this way? This is because if I also use markdown
of three symbols to define the template string syntax highlighting, and will js of
template string conflict, so in order to unnecessary trouble, I switched to three
*
to represent, So the above regular expression will match *
. as follows:
/\*\*\*([\s\S]*?)\*\*\*[\s]?/g
So how should the above regular expressions be understood? First of all, what we need to determine is \s
and \S
mean. *
needs to be escaped in the regular expression, so \
is added. This regular expression means to match a structure like ***//code***
The above source code can be viewed here .
Definition of other texts
There are also two text definitions, that is, the statistics of question options and the statistics of users answering questions, so we have defined two functions to represent them, as follows:
export function getCurrentQuestion(lang="en",order= 1,total = questions.length){
return lang === 'en' ? `Question ${ order } of ${ total }` : `第${ order }题,共${ total }题`;
}
export function getCurrentAnswers(lang = "en",correctNum = 0,total= questions.length){
return lang === 'en' ? `You got ${ correctNum } out of ${ total } correct!` : `共 ${ total }道题,您答对了 ${ correctNum } 道题!`;
}
These 2 tool functions accept 3 parameters, the first parameter represents the language type, the default value is "en", which is the English mode, the second represents the current number of questions/the number of correct questions, and the third parameter represents the total number of questions . Then return a piece of text based on these parameters, there is nothing to say about this.
Analysis of realization ideas
Initialize the project
Skip it here. You can refer to the document .
Implementation of basic components
Next, we can actually divide the page into three parts. The first part is the home page, the second part is the problem option page, and the third part is the problem analysis page. In the analysis page, because there are too many parsing content, we need a response. To the top effect. Before mentioning the implementation of these three parts, we first need to encapsulate some public components, let's take a look together!
Chinese and English tab switching component
Whether it’s the homepage or the question page, we will see a tab component for switching between Chinese and English in the upper right corner. The effect is more than that, let us think about how to achieve it. 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>
Here, we should know that the class name should be dynamic, because we need to add a selected effect, the tentative class name is active
, I am using event proxy here, and proxy the event to the parent element tab-container
. And its text is also dynamic, because it needs to distinguish between Chinese and English. So we can quickly write the following code:
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-item ${ activeIndex === index ? 'tab-active' : ''}`} key={tab}>{ tab }</div>
)
)
}
</div>
)
}
}
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);
}
js
logic, we can see that we pass a lang
parameter through the parent component to determine the Chinese and English mode, and then start to access the tabs
on the definition data, that is, the array, the react.js
rendering list usually uses the map
method. Event agent, we can see that we nativeEvent
, determine whether the element contains the tab-item
, so as to confirm that the child element is clicked, and then call this.setState
change the current index item to determine whether the current is Which item is selected. Since there are only two items, we can determine whether the current index item is 0
or 1
, and we also expose an event changeLang
to the parent element so that the parent element can know the value of the language mode in real time.
As for the styles, they are all relatively basic styles. There is nothing to say. It should be noted that we use fixed positioning to fix the tab component to the upper right corner. The above source code can be viewed here .
Next, we look at the implementation of the second component.
Bottom content component
The bottom content component is relatively simple, that is, a label wraps the content. code show as below:
import React from "react";
import "../style/bottom.css";
const BottomComponent = (props) => {
return (
<div className="bottom" id="bottom">{ props.children }</div>
)
}
export default BottomComponent;
The CSS code is as follows:
.bottom {
position: fixed;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 18px;
}
That is, the writing method of the function component is fixed 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, that is, it is p
label. as follows:
import React from "react";
import "../style/content.css";
const ContentComponent = (props) => {
return (
<p className="content">{ props.children }</p>
)
}
export default ContentComponent;
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;
}
The above source code can be viewed here . Let's look at the implementation of the next component.
Components that render HTML strings
In fact, this component is the use of react.js
of dangerouslySetInnerHTML
property to render html
string. code show as below:
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;
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;
}
As shown in the code, we can see that we actually dangerouslySetInnerHTML
property, and pass the template string as a parameter to this function component. In the function component, we return an object, the structure is: { __html:template }
. There is nothing 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 title component is h1~h6
label, the code is as follows:
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;
The overall logic is not complicated, it is to determine h1 ~ h6
level
attribute passed in by the parent element, which is the writing of the dynamic component. Here, we used Fragment
to wrap the components. Fragment
the usage of the 0613f2f44c8788 components, please refer to the document . My understanding is that it is a placeholder tag. Due to react.js
virtual DOM, a root node needs to be provided, so this placeholder tag appears to solve this problem. Of course, if it is typescript
, we also need to define a type explicitly, as follows:
import React, { FunctionComponent,ReactNode }from "react";
interface propType {
level:number,
children?:ReactNode
}
//这一行代码是需要的
type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const TitleComponent:FunctionComponent<propType> = (props:propType) => {
//这里断言一下只能是h1~h6的标签名
let TagName = `h${ props.level }` as HeadingTag;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>
)
}
export default TitleComponent;
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 certainly does not meet 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-${ buttonType } ${ longClassName } btn-size-${buttonSize} ${parentClassName}`}
onClick={ this.onClickHandler.bind(this)}
>{ this.props.children }</button>
)
}
}
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%;
}
The encapsulation of the buttons here is mainly to classify the buttons, and add various class names to the buttons by superimposing the class names, so as to achieve the realization of different types of buttons. Then expose a onClick
event. Regarding the style code, here is the way through CSS variables. code show as below:
: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;
}
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 actually not finished yet, other types of styles are not implemented because the website we want to implement is not used.
Problem option component
In fact, it is the realization of the problem part of the page, let's first look at the actual code:
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>
)
}
}
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%);
}
}
As you can see, we use the h1
label to display the problem, the button label used by the four options, which item we select the button label, and pass it out onSelect
question
data when using this component, a set of questions and options can be determined. So the realization effect is shown in the figure below:
What may be a bit more complicated in this component is the CSS
layout, which uses flexible box layout and background color gradient animation, etc. There is nothing else to say.
The above source code can be viewed here . Let's look at the implementation of the next component.
Parsing component
The parsing component is actually an encapsulation of the parsing part of the page. Let's first look at the implementation effect:
According to the above figure, we can know that the parsing component is divided into six parts. The first part is a correct statistic of the user's answers, which is actually a title component, and the second part is also a title component, that 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's answer is correct or wrong, and the sixth part is the actual analysis.
Let's take a 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-item ${ 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-answer ${ setTypeClassName(index) }`}>
{
questions[index].correct === userAnswers[index]
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>
<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>
</div>
</li>
))
}
</ul>
)
}
}
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;
}
It can be seen that according to the six major parts of our previous analysis, we can already determine which components we need. First, we must render a list, because there are 20 questions to analyze, and we also know that the Chinese and English modes are determined lang
The other userAnswers
is the user's answer. According to the user's answer and the correct answer, we can know whether the user's answer is correct or wrong. This is the meaning of the following line of code:
const setTypeClassName = (index) => `answered-${ questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;
It is through the index to determine whether the correct class name or the wrong class name is returned, and the style is added through the class name to determine whether the user's answer is correct. Let's split the above code and it will be easy to understand. as follows:
1. Subject information
<span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span>
2. The correct answer
<span className="result-correct-answer">
{ parseObject[lang].output }:
<span className="ml-5 result-correct-answer-value">{ questions[index].correct }</span>
</span>
3. User answer
<span className="result-user-answer">
{parseObject[lang].answer }:
<span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
</span>
4. Prompt information
<span className={`inline-answer ${ setTypeClassName(index) }`}>
{
questions[index].correct === userAnswers[index]
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>
5. Answer analysis
The answer analysis is actually to render the HTML
string, so we can use the previously encapsulated components.
<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>
After this component is completed, in fact, most of our entire project has been completed, and the next step is to deal with some details.
The above source code can be viewed here . Let's look at the implementation of the next component.
Let us continue, the realization of the next component is also the most difficult, that is, the realization of the return to the top effect.
Back to the top button component
The realization of the back to the top component is actually very simple. It is to determine the hidden 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 use the timer to calculate scrollTop
in a certain increment, thus Achieve the effect of smoothly returning to the top. Please see the code as follows:
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 = () => {
const svgPaths = svgRef?.current?.children;
if (svgPaths) {
setPathElementFill(svgPaths, "#2396ef");
}
};
const onMouseLeaveHandler = () => {
const svgPaths = 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.038464s-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.966417l-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.852624c0-37.47859 30.387078-67.865667 67.865667-67.865667s67.865667 30.387078 67.865667 67.865667-30.387078 67.865667-67.865667 67.865667S440.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.386892l0.398066 0 24.62177-42.117237c5.848195-8.284687 9.29469-18.425651 9.29469-29.325909 0-5.965875-1.027399-11.665691-2.90005-16.966417L460.214055 859.81359z"
p-id="4160"
fill={props.color}
></path>
<path
d="M312.354496 646.604674c-18.358113 3.809769-28.697599 21.439288-23.246447 39.399335l54.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.721835L312.354496 646.604674z"
p-id="4161"
fill={props.color}
></path>
<path
d="M711.644481 646.604674l-35.290761-7.356548c-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(() => {
const scrollElement = 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;
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;
}
The whole back to top button component is divided into two parts. In the first part, we use svg
as the click button back to the top. The first is the first component TopButtonComponent
. We mainly do two tasks. The first task is to use React.forwardRef API
to forward the ref
attribute, or to use the ref
attribute for communication. For details on this 0613f2f44c9136, please API
forwardRef API . Then get all the sub-elements under the svg tag through the ref
attribute, and add the function of floating and changing the font color for the svg
setAttribute
This is what the following function does:
const setPathElementFill = (paths, color) => {
//将颜色值和path标签数组作为参数传入,然后设置fill属性值
if (paths) {
Array.from(paths).forEach((path) => path.setAttribute("fill", color));
}
};
The second part is useEffect
, so as to determine the hidden state of the button back to the top. And encapsulated a function to 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);
}
};
Use timer to recursively realize dynamic change scrollTop
. There is nothing else to say.
The above source code can be viewed here . Let's look at the implementation of the next component.
Implementation of app component
In fact, this component is a piece of all encapsulated common components. Let's look at the detailed 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;
The above code involves a utility function, as shown below:
export function computeSameAnswer(correct = 0,userAnswer,correctAnswers,index) {
if(userAnswer === correctAnswers[index - 1] && correct <= 25){
correct++;
}
return correct;
}
As you can see, the function of this function is to calculate the correct number of user answers.
In addition, we pass lang
context.provider
, so we first need to create a context
as follows:
import { createContext } from "react";
export let lang = "en";
export const LangContext = createContext(lang);
The code is also very simple, that is, call React.createContext API
to create a context. For more description of this API
, please document .
In addition, we also encapsulated a reducer
function, as shown below:
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();
}
}
This is also react.js
, state and behavior (or load), yes 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);
}
Then we use a state value or data value order
to determine which part of the page is rendered. order <= 0
is to render the home page, order > 0 && order <= 25
is to render the problem option page, and order > 25
is to render the analysis page.
The above source code can be viewed here .
Regarding this website, I vue3.X
achieve it again. If you are interested, please refer to the source code .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。