Files
qwerty-learner/src/pages/Typing/index.tsx
Kai d5cf54e588 feat: add word review feature and new dict detail ui (#728)
* feat: wrong words revision (#717)

* fix: db error

* feat: add shadon ui

* feat: basic implement of review mode and new ui

* feat: update default tab of dict detail

* chore: add new line

* chore: polish detail

* feat: add dark mode

---------

Co-authored-by: William Yu <52456186+WilliamYPY@users.noreply.github.com>
2023-12-25 23:03:39 +08:00

176 lines
6.4 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 Layout from '../../components/Layout'
import { DictChapterButton } from './components/DictChapterButton'
import PronunciationSwitcher from './components/PronunciationSwitcher'
import ResultScreen from './components/ResultScreen'
import Speed from './components/Speed'
import StartButton from './components/StartButton'
import Switcher from './components/Switcher'
import WordList from './components/WordList'
import WordPanel from './components/WordPanel'
import { useConfetti } from './hooks/useConfetti'
import { useWordList } from './hooks/useWordList'
import { TypingContext, TypingStateActionType, initialState, typingReducer } from './store'
import { DonateCard } from '@/components/DonateCard'
import Header from '@/components/Header'
import StarCard from '@/components/StarCard'
import Tooltip from '@/components/Tooltip'
import { idDictionaryMap } from '@/resources/dictionary'
import { currentChapterAtom, currentDictIdAtom, isReviewModeAtom, randomConfigAtom, reviewModeInfoAtom } from '@/store'
import { IsDesktop, isLegal } from '@/utils'
import { useSaveChapterRecord } from '@/utils/db'
import { useMixPanelChapterLogUploader } from '@/utils/mixpanel'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import type React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useImmerReducer } from 'use-immer'
const App: React.FC = () => {
const [state, dispatch] = useImmerReducer(typingReducer, structuredClone(initialState))
const [isLoading, setIsLoading] = useState<boolean>(true)
const { words } = useWordList()
const [currentDictId, setCurrentDictId] = useAtom(currentDictIdAtom)
const setCurrentChapter = useSetAtom(currentChapterAtom)
const randomConfig = useAtomValue(randomConfigAtom)
const chapterLogUploader = useMixPanelChapterLogUploader(state)
const saveChapterRecord = useSaveChapterRecord()
const reviewModeInfo = useAtomValue(reviewModeInfoAtom)
const isReviewMode = useAtomValue(isReviewModeAtom)
useEffect(() => {
// 检测用户设备
if (!IsDesktop()) {
setTimeout(() => {
alert(
' Qwerty Learner 目的为提高键盘工作者的英语输入效率,目前暂未适配移动端,希望您使用桌面端浏览器访问。如您使用的是 Ipad 等平板电脑设备,可以使用外接键盘使用本软件。',
)
}, 500)
}
}, [])
// 在组件挂载和currentDictId改变时检查当前字典是否存在如果不存在则将其重置为默认值
useEffect(() => {
const id = currentDictId
if (!(id in idDictionaryMap)) {
setCurrentDictId('cet4')
setCurrentChapter(0)
return
}
}, [currentDictId, setCurrentChapter, setCurrentDictId])
const skipWord = useCallback(() => {
dispatch({ type: TypingStateActionType.SKIP_WORD })
}, [dispatch])
useEffect(() => {
const onBlur = () => {
dispatch({ type: TypingStateActionType.SET_IS_TYPING, payload: false })
}
window.addEventListener('blur', onBlur)
return () => {
window.removeEventListener('blur', onBlur)
}
}, [dispatch])
useEffect(() => {
state.chapterData.words?.length > 0 ? setIsLoading(false) : setIsLoading(true)
}, [state.chapterData.words])
useEffect(() => {
if (!state.isTyping) {
const onKeyDown = (e: KeyboardEvent) => {
if (!isLoading && e.key !== 'Enter' && (isLegal(e.key) || e.key === ' ') && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
dispatch({ type: TypingStateActionType.SET_IS_TYPING, payload: true })
}
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}
}, [state.isTyping, isLoading, dispatch])
useEffect(() => {
if (words !== undefined) {
const initialIndex = isReviewMode && reviewModeInfo.reviewRecord?.index ? reviewModeInfo.reviewRecord.index : 0
dispatch({
type: TypingStateActionType.SETUP_CHAPTER,
payload: { words, shouldShuffle: randomConfig.isOpen, initialIndex },
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [words])
useEffect(() => {
// 当用户完成章节后且完成 word Record 数据保存,记录 chapter Record 数据,
if (state.isFinished && !state.isSavingRecord) {
chapterLogUploader()
saveChapterRecord(state)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.isFinished, state.isSavingRecord])
useEffect(() => {
// 启动计时器
let intervalId: number
if (state.isTyping) {
intervalId = window.setInterval(() => {
dispatch({ type: TypingStateActionType.TICK_TIMER })
}, 1000)
}
return () => clearInterval(intervalId)
}, [state.isTyping, dispatch])
useConfetti(state.isFinished)
return (
<TypingContext.Provider value={{ state: state, dispatch }}>
<StarCard />
{state.isFinished && <DonateCard />}
{state.isFinished && <ResultScreen />}
<Layout>
<Header>
<DictChapterButton />
<PronunciationSwitcher />
<Switcher />
<StartButton isLoading={isLoading} />
<Tooltip content="跳过该词">
<button
className={`${
state.isShowSkip ? 'bg-orange-400' : 'invisible w-0 bg-gray-300 px-0 opacity-0'
} my-btn-primary transition-all duration-300 `}
onClick={skipWord}
>
Skip
</button>
</Tooltip>
</Header>
<div className="container mx-auto flex h-full flex-1 flex-col items-center justify-center pb-5">
<div className="container relative mx-auto flex h-full flex-col items-center">
<div className="container flex flex-grow items-center justify-center">
{isLoading ? (
<div className="flex flex-col items-center justify-center ">
<div
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"
role="status"
></div>
</div>
) : (
!state.isFinished && <WordPanel />
)}
</div>
<Speed />
</div>
</div>
</Layout>
<WordList />
</TypingContext.Provider>
)
}
export default App