Files
qwerty-learner/src/pages/Typing/components/ResultScreen/index.tsx
2023-04-25 11:52:32 +08:00

257 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { TypingContext, TypingStateActionType } from '../../store'
import ShareButton from '../ShareButton'
import ConclusionBar from './ConclusionBar'
import RemarkRing from './RemarkRing'
import WordChip from './WordChip'
import styles from './index.module.css'
import redBookLogo from '@/assets/redBook-color-logo.svg'
import Tooltip from '@/components/Tooltip'
import { currentChapterAtom, currentDictInfoAtom, infoPanelStateAtom, randomConfigAtom } from '@/store'
import { InfoPanelType } from '@/typings'
import { WordWithIndex } from '@/typings'
import { recordOpenInfoPanelAction } from '@/utils'
import { Transition } from '@headlessui/react'
import { IconX } from '@tabler/icons-react'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useCallback, useContext, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
const ResultScreen = () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { state, dispatch } = useContext(TypingContext)!
const currentDictInfo = useAtomValue(currentDictInfoAtom)
const [currentChapter, setCurrentChapter] = useAtom(currentChapterAtom)
const setInfoPanelState = useSetAtom(infoPanelStateAtom)
const randomConfig = useAtomValue(randomConfigAtom)
const wrongWords = useMemo(() => {
const wordList = state.chapterData.wrongWordIndexes.map((index) => state.chapterData.words.find((word) => word.index === index))
return wordList.filter((word) => word !== undefined) as WordWithIndex[]
}, [state.chapterData.wrongWordIndexes, state.chapterData.words])
const isLastChapter = useMemo(() => {
return currentChapter >= currentDictInfo.chapterCount - 1
}, [currentChapter, currentDictInfo])
const correctRate = useMemo(() => {
const chapterLength = state.chapterData.words.length
const correctCount = chapterLength - state.chapterData.wrongWordIndexes.length
return Math.floor((correctCount / chapterLength) * 100)
}, [state.chapterData.words.length, state.chapterData.wrongWordIndexes])
const mistakeLevel = useMemo(() => {
if (correctRate >= 85) {
return 0
} else if (correctRate >= 70) {
return 1
} else {
return 2
}
}, [correctRate])
const timeString = useMemo(() => {
const seconds = state.timerData.time
const minutes = Math.floor(seconds / 60)
const minuteString = minutes < 10 ? '0' + minutes : minutes + ''
const restSeconds = seconds % 60
const secondString = restSeconds < 10 ? '0' + restSeconds : restSeconds + ''
return `${minuteString}:${secondString}`
}, [state.timerData.time])
const repeatButtonHandler = useCallback(() => {
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: randomConfig.isOpen })
}, [dispatch, randomConfig.isOpen])
const dictationButtonHandler = useCallback(() => {
dispatch({ type: TypingStateActionType.DICTATION_CHAPTER, shouldShuffle: randomConfig.isOpen })
}, [dispatch, randomConfig.isOpen])
const nextButtonHandler = useCallback(() => {
if (!isLastChapter) {
setCurrentChapter((old) => old + 1)
dispatch({ type: TypingStateActionType.NEXT_CHAPTER })
}
}, [dispatch, isLastChapter, setCurrentChapter])
const exitButtonHandler = useCallback(() => {
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: false })
}, [dispatch])
useHotkeys(
'enter',
() => {
nextButtonHandler()
},
{ preventDefault: true },
)
useHotkeys(
'space',
() => {
repeatButtonHandler()
},
{ preventDefault: true },
)
useHotkeys(
'shift+enter',
() => {
dictationButtonHandler()
},
{ preventDefault: true },
)
const handleOpenInfoPanel = useCallback(
(modalType: InfoPanelType) => {
recordOpenInfoPanelAction(modalType, 'resultScreen')
setInfoPanelState((state) => {
return {
...state,
[modalType]: true,
}
})
},
[setInfoPanelState],
)
return (
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="absolute inset-0 bg-gray-300 opacity-80 dark:bg-gray-600"></div>
<Transition
show={true}
enter="ease-in duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="flex h-screen items-center justify-center">
<div className="card fixed flex w-[90vw] max-w-6xl flex-col overflow-hidden rounded-3xl bg-white pb-14 pl-10 pr-5 pt-10 shadow-lg dark:bg-gray-800 md:w-4/5 lg:w-3/5">
<div className="text-center font-sans text-xl font-normal text-gray-900 dark:text-gray-400 md:text-2xl">
{`${currentDictInfo.name}${currentChapter + 1}`}
</div>
<button className="absolute right-7 top-5" onClick={exitButtonHandler}>
<IconX className="text-gray-400" />
</button>
<div className="mt-10 flex flex-row gap-2 overflow-hidden">
<div className="flex flex-shrink-0 flex-grow-0 flex-col gap-3 px-4 sm:px-1 md:px-2 lg:px-4">
<RemarkRing remark={`${state.timerData.accuracy}%`} caption="正确率" percentage={state.timerData.accuracy} />
<RemarkRing remark={timeString} caption="章节耗时" />
<RemarkRing remark={state.timerData.wpm + ''} caption="WPM" />
</div>
<div className="z-10 ml-6 flex-1 overflow-visible rounded-xl bg-indigo-50 dark:bg-gray-700">
<div className="customized-scrollbar z-20 ml-8 mr-1 flex h-80 flex-row flex-wrap content-start gap-4 overflow-y-auto overflow-x-hidden pr-7 pt-9">
{wrongWords.map((word, index) => (
<WordChip key={`${index}-${word.name}`} word={word} />
))}
</div>
<div className="align-center flex w-full flex-row justify-start rounded-b-xl bg-indigo-200 px-4 dark:bg-indigo-400">
<ConclusionBar mistakeLevel={mistakeLevel} mistakeCount={state.chapterData.wrongWordIndexes.length} />
</div>
</div>
<div className="ml-2 flex flex-col items-center justify-end gap-3.5 text-xl">
<ShareButton />
<img
src={redBookLogo}
onClick={(e) => {
handleOpenInfoPanel('redBook')
e.currentTarget.blur()
}}
className="h-5 cursor-pointer fill-current text-gray-50"
alt="red book"
style={{ fill: '#6B7280' }}
/>
<button
onClick={(e) => {
handleOpenInfoPanel('donate')
e.currentTarget.blur()
}}
className="cursor-pointer text-gray-500 dark:text-gray-400"
type="button"
title="捐赠我们的项目"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
className={`h-5.5 w-5.5 fill-current text-gray-500 focus:outline-none dark:text-gray-400 ${styles.imgShake}`}
>
<path d="M96 64c0-17.7 14.3-32 32-32H448h64c70.7 0 128 57.3 128 128s-57.3 128-128 128H480c0 53-43 96-96 96H192c-53 0-96-43-96-96V64zM480 224h32c35.3 0 64-28.7 64-64s-28.7-64-64-64H480V224zM32 416H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32z" />
</svg>
</button>
<button
onClick={(e) => {
handleOpenInfoPanel('community')
e.currentTarget.blur()
}}
className="cursor-pointer text-gray-500 dark:text-gray-400"
type="button"
title="加入我们的社区"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
className="h-5.5 w-5.5 fill-current text-gray-500 focus:outline-none dark:text-gray-400"
>
<path d="M385.2 167.6c6.4 0 12.6.3 18.8 1.1C387.4 90.3 303.3 32 207.7 32 100.5 32 13 104.8 13 197.4c0 53.4 29.3 97.5 77.9 131.6l-19.3 58.6 68-34.1c24.4 4.8 43.8 9.7 68.2 9.7 6.2 0 12.1-.3 18.3-.8-4-12.9-6.2-26.6-6.2-40.8-.1-84.9 72.9-154 165.3-154zm-104.5-52.9c14.5 0 24.2 9.7 24.2 24.4 0 14.5-9.7 24.2-24.2 24.2-14.8 0-29.3-9.7-29.3-24.2.1-14.7 14.6-24.4 29.3-24.4zm-136.4 48.6c-14.5 0-29.3-9.7-29.3-24.2 0-14.8 14.8-24.4 29.3-24.4 14.8 0 24.4 9.7 24.4 24.4 0 14.6-9.6 24.2-24.4 24.2zM563 319.4c0-77.9-77.9-141.3-165.4-141.3-92.7 0-165.4 63.4-165.4 141.3S305 460.7 397.6 460.7c19.3 0 38.9-5.1 58.6-9.9l53.4 29.3-14.8-48.6C534 402.1 563 363.2 563 319.4zm-219.1-24.5c-9.7 0-19.3-9.7-19.3-19.6 0-9.7 9.7-19.3 19.3-19.3 14.8 0 24.4 9.7 24.4 19.3 0 10-9.7 19.6-24.4 19.6zm107.1 0c-9.7 0-19.3-9.7-19.3-19.6 0-9.7 9.7-19.3 19.3-19.3 14.5 0 24.4 9.7 24.4 19.3.1 10-9.9 19.6-24.4 19.6z" />
</svg>
</button>
<a href="https://github.com/Kaiyiwing/qwerty-learner" target="_blank" rel="noreferrer" className="leading-[0px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 496 512"
className="h-5.5 w-5.5 fill-current text-gray-500 focus:outline-none dark:text-gray-400"
>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg>
</a>
</div>
</div>
<div className="mt-10 flex w-full justify-center gap-5 px-5 text-xl">
<Tooltip content="快捷键shift + enter">
<button
className="btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
type="button"
onClick={dictationButtonHandler}
title="默写本章节"
>
</button>
</Tooltip>
<Tooltip content="快捷键space">
<button
className="btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
type="button"
onClick={repeatButtonHandler}
title="重复本章节"
>
</button>
</Tooltip>
{!isLastChapter && (
<Tooltip content="快捷键enter">
<button
className={`btn-primary { isLastChapter ? 'cursor-not-allowed opacity-50' : ''} h-12 text-base font-bold `}
type="button"
onClick={nextButtonHandler}
title="下一章节"
>
</button>
</Tooltip>
)}
</div>
</div>
</div>
</Transition>
</div>
)
}
export default ResultScreen