mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2026-04-04 22:09:04 +08:00
Revert "feat: wrong words revision (#717)"
This reverts commit 61d80e39db.
This commit is contained in:
@@ -25,7 +25,6 @@
|
||||
"immer": "^9.0.21",
|
||||
"jotai": "^2.0.3",
|
||||
"mixpanel-browser": "^2.45.0",
|
||||
"moment": "^2.29.4",
|
||||
"pako": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-activity-calendar": "^2.0.2",
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "com.litongjava.qwerty.learner",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
|
||||
@@ -20,9 +20,7 @@ const Tooltip = ({ children, content, className, placement = 'top' }: TooltipPro
|
||||
visible ? 'opacity-100' : 'opacity-0'
|
||||
} ${placementClasses} pointer-events-none absolute left-1/2 flex -translate-x-1/2 transform items-center justify-center transition-opacity`}
|
||||
>
|
||||
<span className="tooltip" style={{ whiteSpace: 'pre' }}>
|
||||
{content}
|
||||
</span>
|
||||
<span className="tooltip">{content}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import useIntersectionObserver from '@/hooks/useIntersectionObserver'
|
||||
import { useChapterStats } from '@/pages/Gallery-N/hooks/useChapterStats'
|
||||
import { isInRevisionModeAtom } from '@/store'
|
||||
import noop from '@/utils/noop'
|
||||
import classNames from 'classnames'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type ChapterRowProps = {
|
||||
@@ -18,7 +15,6 @@ export default function ChapterRow({ index, dictID, checked, onChange }: Chapter
|
||||
const entry = useIntersectionObserver(rowRef, {})
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
const chapterStatus = useChapterStats(index, dictID, isVisible)
|
||||
const isRevisionMode = useAtomValue(isInRevisionModeAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (checked && rowRef.current !== null) {
|
||||
@@ -47,28 +43,13 @@ export default function ChapterRow({ index, dictID, checked, onChange }: Chapter
|
||||
/>
|
||||
</td>
|
||||
<td className="flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200">{index + 1}</td>
|
||||
<td
|
||||
className={classNames(
|
||||
isRevisionMode ? 'invisible' : 'visible',
|
||||
'flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200',
|
||||
)}
|
||||
>
|
||||
<td className="flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200">
|
||||
{chapterStatus ? chapterStatus.exerciseCount : 0}
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
isRevisionMode ? 'invisible' : 'visible',
|
||||
'flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200',
|
||||
)}
|
||||
>
|
||||
<td className="flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200">
|
||||
{chapterStatus ? chapterStatus.avgWrongWordCount : 0}
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
isRevisionMode ? 'invisible' : 'visible',
|
||||
'flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200',
|
||||
)}
|
||||
>
|
||||
<td className="flex-1 px-6 py-4 text-center text-sm text-gray-700 dark:text-gray-200">
|
||||
{chapterStatus ? chapterStatus.avgWrongInputCount : 0}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useRevisionWordCount } from '../hooks/useRevisionWordCount'
|
||||
import { isInRevisionModeAtom, isRestartRevisionProgressAtom } from '@/store'
|
||||
import { db } from '@/utils/db'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import * as Progress from '@radix-ui/react-progress'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import moment from 'moment'
|
||||
import { Fragment, useState } from 'react'
|
||||
|
||||
type RevisionSwitcherProps = {
|
||||
dictId: string
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
const RevisionSwitcher: React.FC<RevisionSwitcherProps> = ({ dictId, onConfirm }: RevisionSwitcherProps) => {
|
||||
const [revisionIndex, setRevisionIndex] = useState<number | undefined>(0)
|
||||
const [createdTime, setCreatedTime] = useState<number | undefined>(0)
|
||||
const setRevisionMode = useSetAtom(isInRevisionModeAtom)
|
||||
const revisionWordCount = useRevisionWordCount(dictId)
|
||||
const setRestartRevision = useSetAtom(isRestartRevisionProgressAtom)
|
||||
const onContinueProgress = () => {
|
||||
onConfirm()
|
||||
setRevisionMode(true)
|
||||
}
|
||||
const onCreateNewProgress = () => {
|
||||
onConfirm()
|
||||
setRevisionMode(true)
|
||||
setRestartRevision(true)
|
||||
}
|
||||
|
||||
const fetchRevisionIndex = async () => {
|
||||
let index, timeStamp
|
||||
await db.revisionDictRecords
|
||||
.where('dict')
|
||||
.equals(dictId)
|
||||
.first((record) => {
|
||||
index = record?.revisionIndex
|
||||
timeStamp = record?.createdTime
|
||||
console.log('index', index)
|
||||
console.log('timeStamp', timeStamp)
|
||||
})
|
||||
setRevisionIndex(index)
|
||||
setCreatedTime(timeStamp)
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
|
||||
// fetchRevisionIndex()
|
||||
// }, [dictId])
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`flex h-8 min-w-max cursor-pointer items-center justify-center rounded-md border-2 px-2 text-gray-700 transition-colors duration-300 ease-in-out hover:bg-indigo-400 hover:text-white focus:outline-none dark:text-white dark:text-opacity-60 dark:hover:text-opacity-100 ${
|
||||
open ? 'bg-indigo-400 text-white' : 'bg-transparent'
|
||||
}`}
|
||||
onFocus={(e) => {
|
||||
e.target.blur()
|
||||
}}
|
||||
onClick={fetchRevisionIndex}
|
||||
>
|
||||
复习错误单词
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute left-0 z-20 mt-2 flex max-w-max -translate-x-1/2 px-4 ">
|
||||
<div className="shadow-upper box-border flex w-60 select-none flex-col items-center justify-center gap-3 rounded-xl bg-white p-4 text-gray-700 drop-shadow transition duration-1000 ease-in-out dark:bg-gray-800 dark:text-gray-200">
|
||||
{revisionWordCount === 0 ? (
|
||||
<span className="text-sm">暂无错误单词,快去练习吧</span>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex w-full flex-col items-start py-0">
|
||||
<span>复习进度</span>
|
||||
</div>
|
||||
{revisionIndex === undefined || createdTime === undefined ? (
|
||||
<span>暂无进度</span>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex w-full flex-col items-start gap-0 py-0">
|
||||
<div className=" flex w-full items-center py-0">
|
||||
<Progress.Root
|
||||
value={revisionIndex}
|
||||
max={revisionWordCount}
|
||||
className="mr-4 h-2 w-full rounded-full border border-indigo-400 bg-white"
|
||||
>
|
||||
<Progress.Indicator
|
||||
className="h-full rounded-full bg-indigo-400 pl-0"
|
||||
style={{ width: `calc(${(revisionIndex / revisionWordCount) * 100}% )` }}
|
||||
/>
|
||||
</Progress.Root>
|
||||
<span className="p-0 text-xs">
|
||||
{revisionIndex}/{revisionWordCount}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs">({moment(createdTime).format('YYYY-MM-DD HH:mm:ss')}创建)</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{revisionWordCount !== 0 && (
|
||||
<div className="flex w-full items-start justify-center gap-3">
|
||||
{revisionIndex !== undefined && createdTime !== undefined && (
|
||||
<button
|
||||
className="my-btn-primary flex-1 px-1 text-sm disabled:bg-gray-300"
|
||||
type="button"
|
||||
title="继续当前进度"
|
||||
onClick={onContinueProgress}
|
||||
>
|
||||
继续当前进度
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="my-btn-primary max-w-[98px] flex-1 px-1 text-sm disabled:bg-gray-300"
|
||||
type="button"
|
||||
title="创建新进度"
|
||||
onClick={onCreateNewProgress}
|
||||
>
|
||||
创建新进度
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default RevisionSwitcher
|
||||
@@ -1,16 +1,12 @@
|
||||
import { GalleryContext } from '..'
|
||||
import { useRevisionWordCount } from '../hooks/useRevisionWordCount'
|
||||
import ChapterRow from './ChapterRow'
|
||||
import RevisionSwitcher from './RevisionSwitcher'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import { currentChapterAtom, currentDictIdAtom, isInRevisionModeAtom } from '@/store'
|
||||
import { currentChapterAtom, currentDictIdAtom } from '@/store'
|
||||
import { calcChapterCount } from '@/utils'
|
||||
import range from '@/utils/range'
|
||||
import { Dialog, Switch, Transition } from '@headlessui/react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Fragment, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import IconInfo from '~icons/ic/outline-info'
|
||||
import IconX from '~icons/tabler/x'
|
||||
|
||||
export default function ChapterList() {
|
||||
@@ -22,13 +18,11 @@ export default function ChapterList() {
|
||||
|
||||
const [currentChapter, setCurrentChapter] = useAtom(currentChapterAtom)
|
||||
const [currentDictId, setCurrentDictId] = useAtom(currentDictIdAtom)
|
||||
const [isRevisionMode, setRevisionMode] = useAtom(isInRevisionModeAtom)
|
||||
const [checkedChapter, setCheckedChapter] = useState(dict?.id === currentDictId ? currentChapter : 0)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const chapterCount = calcChapterCount(dict?.length ?? 0)
|
||||
const showChapterList = dict !== null
|
||||
const revisionWordCount = useRevisionWordCount(currentDictId)
|
||||
|
||||
useEffect(() => {
|
||||
if (dict) {
|
||||
@@ -55,12 +49,6 @@ export default function ChapterList() {
|
||||
setState((state) => {
|
||||
state.chapterListDict = null
|
||||
})
|
||||
setRevisionMode(false)
|
||||
}
|
||||
|
||||
const onToggleRevisionMode = () => {
|
||||
setRevisionMode((old) => !old)
|
||||
setCheckedChapter(0)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -94,67 +82,54 @@ export default function ChapterList() {
|
||||
<>
|
||||
<div className="flex w-full items-end justify-between py-4 pl-5">
|
||||
<span className="text-lg text-gray-700 dark:text-gray-200">{dict.name}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<RevisionSwitcher dictId={dict?.id} onConfirm={onConfirm} />
|
||||
|
||||
<IconX className="mr-2 cursor-pointer text-gray-400" onClick={onCloseDialog} />
|
||||
</div>
|
||||
<IconX className="mr-2 cursor-pointer text-gray-400" onClick={onCloseDialog} />
|
||||
</div>
|
||||
|
||||
<div className="w-full flex-1 overflow-y-auto">
|
||||
{isRevisionMode && revisionWordCount === 0 ? (
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
<span className="text-sm font-normal leading-5 text-gray-900 dark:text-white dark:text-opacity-60">
|
||||
暂无错误单词
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<table className="block min-w-full divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<thead className="sticky top-0 block h-10 w-full bg-gray-50 dark:bg-gray-700">
|
||||
<tr className="flex">
|
||||
<th
|
||||
scope="col"
|
||||
className="w-15 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
></th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
Chapter
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
练习次数
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
平均错误单词数
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
平均错误输入数
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="block h-full w-full divide-y divide-gray-100 overflow-y-scroll bg-white dark:divide-gray-800">
|
||||
{range(0, chapterCount, 1).map((index) => (
|
||||
<ChapterRow
|
||||
key={`${dict.id}-${index}`}
|
||||
index={index}
|
||||
dictID={dict.id}
|
||||
checked={checkedChapter === index}
|
||||
onChange={onChangeChapter}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<div className="w-full flex-1 overflow-y-auto ">
|
||||
<table className="block min-w-full divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<thead className="sticky top-0 block h-10 w-full bg-gray-50 dark:bg-gray-700">
|
||||
<tr className="flex">
|
||||
<th
|
||||
scope="col"
|
||||
className="w-15 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
></th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
Chapter
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
练习次数
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
平均错误单词数
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="flex-1 px-2 py-3 text-center text-sm font-bold tracking-wider text-gray-600 dark:text-gray-200"
|
||||
>
|
||||
平均错误输入数
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="block h-full w-full divide-y divide-gray-100 overflow-y-scroll bg-white dark:divide-gray-800">
|
||||
{range(0, chapterCount, 1).map((index) => (
|
||||
<ChapterRow
|
||||
key={`${dict.id}-${index}`}
|
||||
index={index}
|
||||
dictID={dict.id}
|
||||
checked={checkedChapter === index}
|
||||
onChange={onChangeChapter}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button className="text-bold h-15 w-full bg-indigo-400 text-white" onClick={onConfirm}>
|
||||
确定
|
||||
|
||||
@@ -30,6 +30,7 @@ async function getDictStats(dict: string): Promise<IDictStats> {
|
||||
const uniqueChapter = allChapter.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index
|
||||
})
|
||||
|
||||
const exercisedChapterCount = uniqueChapter.length
|
||||
|
||||
return { exercisedChapterCount }
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { db } from '@/utils/db'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useRevisionWordCount(dictID: string) {
|
||||
const [wordCount, setWordCount] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWordCount = async () => {
|
||||
const count = await getRevisionWordCount(dictID)
|
||||
setWordCount(count)
|
||||
}
|
||||
|
||||
if (dictID) {
|
||||
fetchWordCount()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dictID])
|
||||
|
||||
return wordCount
|
||||
}
|
||||
|
||||
async function getRevisionWordCount(dict: string): Promise<number> {
|
||||
const wordCount = await db.wordRecords
|
||||
.where('dict')
|
||||
.equals(dict)
|
||||
.and((wordRecord) => wordRecord.wrongCount > 0)
|
||||
.toArray()
|
||||
.then((wordRecords) => {
|
||||
const res = new Map()
|
||||
const reducedRecords = wordRecords.filter((item) => !res.has(item['word'] + item['dict']) && res.set(item['word'] + item['dict'], 1))
|
||||
return reducedRecords.length
|
||||
})
|
||||
return wordCount
|
||||
}
|
||||
@@ -1,34 +1,16 @@
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import { currentChapterAtom, currentDictInfoAtom, isInRevisionModeAtom } from '@/store'
|
||||
import { db } from '@/utils/db'
|
||||
import { currentChapterAtom, currentDictInfoAtom } from '@/store'
|
||||
import range from '@/utils/range'
|
||||
import { Listbox, Transition } from '@headlessui/react'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { Fragment, useCallback } from 'react'
|
||||
import { Fragment } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import IconCheck from '~icons/tabler/check'
|
||||
|
||||
type DictChapterButtonProps = {
|
||||
index: number
|
||||
}
|
||||
export const DictChapterButton = ({ index }: DictChapterButtonProps) => {
|
||||
export const DictChapterButton = () => {
|
||||
const currentDictInfo = useAtomValue(currentDictInfoAtom)
|
||||
const [currentChapter, setCurrentChapter] = useAtom(currentChapterAtom)
|
||||
const chapterCount = currentDictInfo.chapterCount
|
||||
const [isRevision, setRevisionMode] = useAtom(isInRevisionModeAtom)
|
||||
|
||||
const saveDictRevisionIndex = useCallback(
|
||||
async (dictId: string, index: number) => {
|
||||
// 保存错题进度(点击章节切换时)
|
||||
setRevisionMode(false)
|
||||
// if ((await db.revisionDictRecords.where('dict').equals(dictId).count()) > 1) {
|
||||
// console.log('delete')
|
||||
// await db.revisionDictRecords.where('dict').equals(dictId).delete()
|
||||
// }
|
||||
await db.revisionDictRecords.where('dict').equals(dictId).modify({ revisionIndex: index })
|
||||
},
|
||||
[setRevisionMode],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -36,38 +18,35 @@ export const DictChapterButton = ({ index }: DictChapterButtonProps) => {
|
||||
<NavLink
|
||||
className="block rounded-lg px-3 py-1 text-lg transition-colors duration-300 ease-in-out hover:bg-indigo-400 hover:text-white focus:outline-none dark:text-white dark:text-opacity-60 dark:hover:text-opacity-100"
|
||||
to="/gallery"
|
||||
onClick={() => isRevision && saveDictRevisionIndex(currentDictInfo.id, index)}
|
||||
>
|
||||
{currentDictInfo.name} {isRevision && '错题练习'}
|
||||
{currentDictInfo.name}
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
{!isRevision && (
|
||||
<Tooltip content="章节切换">
|
||||
<Listbox value={currentChapter} onChange={setCurrentChapter}>
|
||||
<Listbox.Button className="rounded-lg px-3 py-1 text-lg transition-colors duration-300 ease-in-out hover:bg-indigo-400 hover:text-white focus:outline-none dark:text-white dark:text-opacity-60 dark:hover:text-opacity-100">
|
||||
第 {currentChapter + 1} 章
|
||||
</Listbox.Button>
|
||||
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||
<Listbox.Options className="listbox-options z-10 w-32">
|
||||
{range(0, chapterCount, 1).map((index) => (
|
||||
<Listbox.Option key={index} value={index}>
|
||||
{({ selected }) => (
|
||||
<div className="group flex cursor-pointer items-center justify-between">
|
||||
{selected ? (
|
||||
<span className="listbox-options-icon">
|
||||
<IconCheck className="focus:outline-none" />
|
||||
</span>
|
||||
) : null}
|
||||
<span>第 {index + 1} 章</span>
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip content="章节切换">
|
||||
<Listbox value={currentChapter} onChange={setCurrentChapter}>
|
||||
<Listbox.Button className="rounded-lg px-3 py-1 text-lg transition-colors duration-300 ease-in-out hover:bg-indigo-400 hover:text-white focus:outline-none dark:text-white dark:text-opacity-60 dark:hover:text-opacity-100">
|
||||
第 {currentChapter + 1} 章
|
||||
</Listbox.Button>
|
||||
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||
<Listbox.Options className="listbox-options z-10 w-32">
|
||||
{range(0, chapterCount, 1).map((index) => (
|
||||
<Listbox.Option key={index} value={index}>
|
||||
{({ selected }) => (
|
||||
<div className="group flex cursor-pointer items-center justify-between">
|
||||
{selected ? (
|
||||
<span className="listbox-options-icon">
|
||||
<IconCheck className="focus:outline-none" />
|
||||
</span>
|
||||
) : null}
|
||||
<span>第 {index + 1} 章</span>
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,17 +5,9 @@ import RemarkRing from './RemarkRing'
|
||||
import WordChip from './WordChip'
|
||||
import styles from './index.module.css'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import {
|
||||
currentChapterAtom,
|
||||
currentDictInfoAtom,
|
||||
infoPanelStateAtom,
|
||||
isInRevisionModeAtom,
|
||||
randomConfigAtom,
|
||||
wordDictationConfigAtom,
|
||||
} from '@/store'
|
||||
import { currentChapterAtom, currentDictInfoAtom, infoPanelStateAtom, randomConfigAtom, wordDictationConfigAtom } from '@/store'
|
||||
import type { InfoPanelType } from '@/typings'
|
||||
import { recordOpenInfoPanelAction } from '@/utils'
|
||||
import { db } from '@/utils/db'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||
@@ -36,7 +28,6 @@ const ResultScreen = () => {
|
||||
const [currentChapter, setCurrentChapter] = useAtom(currentChapterAtom)
|
||||
const setInfoPanelState = useSetAtom(infoPanelStateAtom)
|
||||
const randomConfig = useAtomValue(randomConfigAtom)
|
||||
const isRevisionMode = useAtomValue(isInRevisionModeAtom)
|
||||
|
||||
useEffect(() => {
|
||||
// tick a zero timer to calc the stats
|
||||
@@ -79,8 +70,8 @@ const ResultScreen = () => {
|
||||
}, [state.chapterData.userInputLogs, state.chapterData.words])
|
||||
|
||||
const isLastChapter = useMemo(() => {
|
||||
return isRevisionMode ? true : currentChapter >= currentDictInfo.chapterCount - 1
|
||||
}, [currentChapter, currentDictInfo, isRevisionMode])
|
||||
return currentChapter >= currentDictInfo.chapterCount - 1
|
||||
}, [currentChapter, currentDictInfo])
|
||||
|
||||
const correctRate = useMemo(() => {
|
||||
const chapterLength = state.chapterData.words.length
|
||||
@@ -107,7 +98,7 @@ const ResultScreen = () => {
|
||||
return `${minuteString}:${secondString}`
|
||||
}, [state.timerData.time])
|
||||
|
||||
const repeatButtonHandler = useCallback(async () => {
|
||||
const repeatButtonHandler = useCallback(() => {
|
||||
setWordDictationConfig((old) => {
|
||||
if (old.isOpen) {
|
||||
if (old.openBy === 'auto') {
|
||||
@@ -116,15 +107,14 @@ const ResultScreen = () => {
|
||||
}
|
||||
return old
|
||||
})
|
||||
await db.revisionDictRecords.where('dict').equals(currentDictInfo.id).modify({ revisionIndex: 0 })
|
||||
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: randomConfig.isOpen })
|
||||
}, [dispatch, randomConfig.isOpen, setWordDictationConfig, currentDictInfo.id])
|
||||
}, [dispatch, randomConfig.isOpen, setWordDictationConfig])
|
||||
|
||||
const dictationButtonHandler = useCallback(async () => {
|
||||
const dictationButtonHandler = useCallback(() => {
|
||||
setWordDictationConfig((old) => ({ ...old, isOpen: true, openBy: 'auto' }))
|
||||
await db.revisionDictRecords.where('dict').equals(currentDictInfo.id).modify({ revisionIndex: 0 })
|
||||
|
||||
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: randomConfig.isOpen })
|
||||
}, [dispatch, randomConfig.isOpen, setWordDictationConfig, currentDictInfo.id])
|
||||
}, [dispatch, randomConfig.isOpen, setWordDictationConfig])
|
||||
|
||||
const nextButtonHandler = useCallback(() => {
|
||||
setWordDictationConfig((old) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TypingContext, TypingStateActionType } from '../../store'
|
||||
import WordCard from './WordCard'
|
||||
import Drawer from '@/components/Drawer'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import { currentChapterAtom, currentDictInfoAtom, isInRevisionModeAtom } from '@/store'
|
||||
import { currentChapterAtom, currentDictInfoAtom } from '@/store'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import * as ScrollArea from '@radix-ui/react-scroll-area'
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
@@ -20,7 +20,6 @@ export default function WordList() {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const currentDictTitleValue = useAtomValue(currentDictTitle)
|
||||
const isRevision = useAtomValue(isInRevisionModeAtom)
|
||||
|
||||
function closeModal() {
|
||||
setIsOpen(false)
|
||||
@@ -45,7 +44,7 @@ export default function WordList() {
|
||||
|
||||
<Drawer open={isOpen} onClose={closeModal} classNames="bg-stone-50 dark:bg-gray-900">
|
||||
<Dialog.Title as="h3" className="flex items-center justify-between p-4 text-lg font-medium leading-6 dark:text-gray-50">
|
||||
{isRevision && '错题练习 ' + currentDictTitleValue}
|
||||
{currentDictTitleValue}
|
||||
<IconX onClick={closeModal} className="cursor-pointer" />
|
||||
</Dialog.Title>
|
||||
<ScrollArea.Root className="flex-1 select-none overflow-y-auto ">
|
||||
|
||||
@@ -5,11 +5,10 @@ import Phonetic from './components/Phonetic'
|
||||
import Translation from './components/Translation'
|
||||
import WordComponent from './components/Word'
|
||||
import { usePrefetchPronunciationSound } from '@/hooks/usePronunciation'
|
||||
import { currentDictIdAtom, isInRevisionModeAtom, isShowPrevAndNextWordAtom, loopWordConfigAtom, phoneticConfigAtom } from '@/store'
|
||||
import { isShowPrevAndNextWordAtom, loopWordConfigAtom, phoneticConfigAtom } from '@/store'
|
||||
import type { Word } from '@/typings'
|
||||
import { db } from '@/utils/db'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
export default function WordPanel() {
|
||||
@@ -22,8 +21,6 @@ export default function WordPanel() {
|
||||
const { times: loopWordTimes } = useAtomValue(loopWordConfigAtom)
|
||||
const currentWord = state.chapterData.words[state.chapterData.index]
|
||||
const nextWord = state.chapterData.words[state.chapterData.index + 1] as Word | undefined
|
||||
const currentDictId = useAtomValue(currentDictIdAtom)
|
||||
const isRevision = useAtomValue(isInRevisionModeAtom)
|
||||
|
||||
const prevIndex = useMemo(() => {
|
||||
const newIndex = state.chapterData.index - 1
|
||||
@@ -40,22 +37,6 @@ export default function WordPanel() {
|
||||
setWordComponentKey((old) => old + 1)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 复习错题模式下,当组件挂载时,检查是否存在错题进度,如果存在,则跳转到该进度
|
||||
const fetchRevisionIndex = async () => {
|
||||
let index
|
||||
await db.revisionDictRecords
|
||||
.where('dict')
|
||||
.equals(currentDictId)
|
||||
.first((record) => (index = record?.revisionIndex))
|
||||
index && dispatch({ type: TypingStateActionType.SKIP_2_WORD_INDEX, newIndex: index })
|
||||
reloadCurrentWordComponent()
|
||||
}
|
||||
if (isRevision) {
|
||||
fetchRevisionIndex()
|
||||
}
|
||||
}, [currentDictId, dispatch, isRevision, reloadCurrentWordComponent])
|
||||
|
||||
const onFinish = useCallback(() => {
|
||||
if (state.chapterData.index < state.chapterData.words.length - 1 || currentWordExerciseCount < loopWordTimes - 1) {
|
||||
// 用户完成当前单词
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { isRestartRevisionProgressAtom } from './../../../store/index'
|
||||
import { WordRecord } from './../../../utils/db/record'
|
||||
import { CHAPTER_LENGTH } from '@/constants'
|
||||
import { currentChapterAtom, currentDictInfoAtom, isInRevisionModeAtom } from '@/store'
|
||||
import { currentChapterAtom, currentDictInfoAtom } from '@/store'
|
||||
import type { WordWithIndex } from '@/typings/index'
|
||||
import { db } from '@/utils/db'
|
||||
import type { IWordRecord } from '@/utils/db/record'
|
||||
import { wordListFetcher } from '@/utils/wordListFetcher'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export type UseWordListResult = {
|
||||
@@ -22,72 +18,26 @@ export type UseWordListResult = {
|
||||
export function useWordList(): UseWordListResult {
|
||||
const currentDictInfo = useAtomValue(currentDictInfoAtom)
|
||||
const [currentChapter, setCurrentChapter] = useAtom(currentChapterAtom)
|
||||
const [isInRevisionMode] = useAtom(isInRevisionModeAtom)
|
||||
const [isRestartRevisionProgress, setRestartRevisionProgress] = useAtom(isRestartRevisionProgressAtom)
|
||||
const [wrongListDesc, setWrongListDesc] = useState<IWordRecord[]>()
|
||||
|
||||
const isFirstChapter = currentDictInfo.id === 'cet4' && currentChapter === 0 && !isInRevisionMode
|
||||
const isFirstChapter = currentDictInfo.id === 'cet4' && currentChapter === 0
|
||||
|
||||
// Reset current chapter to 0, when currentChapter is greater than chapterCount.
|
||||
if (currentChapter >= currentDictInfo.chapterCount) {
|
||||
setCurrentChapter(0)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWrongList = async () => {
|
||||
let lastErrorList: IWordRecord[] = []
|
||||
lastErrorList = await db.revisionWordRecords.where('dict').equals(currentDictInfo.id).toArray()
|
||||
console.log(lastErrorList)
|
||||
if (lastErrorList.length === 0 || isRestartRevisionProgress) {
|
||||
//获取最新的wrongRecords
|
||||
if (lastErrorList.length === 0)
|
||||
await db.revisionDictRecords.add({ dict: currentDictInfo.id, revisionIndex: 0, createdTime: Date.now() })
|
||||
if (isRestartRevisionProgress)
|
||||
await db.revisionDictRecords.where('dict').equals(currentDictInfo.id).modify({ revisionIndex: 0, createdTime: Date.now() })
|
||||
setRestartRevisionProgress(false)
|
||||
const wrongList = await db.wordRecords
|
||||
.where('wrongCount')
|
||||
.above(0)
|
||||
.and((record) => record.dict === currentDictInfo.id)
|
||||
.reverse()
|
||||
.toArray()
|
||||
const processedWrongList = wrongList.map((record) => {
|
||||
return new WordRecord(record.word, record.dict, record.chapter, record.timing, record.wrongCount, record.mistakes)
|
||||
})
|
||||
wrongList.length !== 0 && (await db.revisionWordRecords.bulkAdd(processedWrongList))
|
||||
lastErrorList = await db.revisionWordRecords.where('dict').equals(currentDictInfo.id).toArray()
|
||||
}
|
||||
setWrongListDesc(lastErrorList)
|
||||
}
|
||||
if (isInRevisionMode) {
|
||||
fetchWrongList()
|
||||
}
|
||||
}, [isInRevisionMode, currentDictInfo.id, isRestartRevisionProgress, setRestartRevisionProgress])
|
||||
|
||||
const { data: wordList, error, isLoading } = useSWR(currentDictInfo.url, wordListFetcher)
|
||||
|
||||
const words: WordWithIndex[] = useMemo(() => {
|
||||
const newWords = isFirstChapter
|
||||
? firstChapter
|
||||
: wordList
|
||||
? isInRevisionMode
|
||||
? wordList
|
||||
.filter(
|
||||
(word, index, self) =>
|
||||
self.findIndex((w) => w.name === word.name) === index &&
|
||||
wrongListDesc?.find((wrong) => wrong.dict === currentDictInfo.id && wrong.word === word.name),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aWrong = wrongListDesc?.find((wrong) => wrong.dict === currentDictInfo.id && wrong.word === a.name)
|
||||
const bWrong = wrongListDesc?.find((wrong) => wrong.dict === currentDictInfo.id && wrong.word === b.name)
|
||||
if ((bWrong?.wrongCount ?? 0) - (aWrong?.wrongCount ?? 0) === 0) return a.name.localeCompare(b.name)
|
||||
else return (bWrong?.wrongCount ?? 0) - (aWrong?.wrongCount ?? 0)
|
||||
})
|
||||
: wordList.slice(currentChapter * CHAPTER_LENGTH, (currentChapter + 1) * CHAPTER_LENGTH)
|
||||
? wordList.slice(currentChapter * CHAPTER_LENGTH, (currentChapter + 1) * CHAPTER_LENGTH)
|
||||
: []
|
||||
|
||||
// 记录原始 index
|
||||
return newWords.map((word, index) => ({ ...word, index }))
|
||||
}, [isFirstChapter, wordList, currentChapter, currentDictInfo, isInRevisionMode, wrongListDesc])
|
||||
}, [isFirstChapter, wordList, currentChapter])
|
||||
|
||||
return { words: wordList === undefined ? undefined : words, isLoading, error }
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@ const App: React.FC = () => {
|
||||
|
||||
const [currentDictId, setCurrentDictId] = useAtom(currentDictIdAtom)
|
||||
const randomConfig = useAtomValue(randomConfigAtom)
|
||||
|
||||
const chapterLogUploader = useMixPanelChapterLogUploader(state)
|
||||
const saveChapterRecord = useSaveChapterRecord()
|
||||
|
||||
useEffect(() => {
|
||||
// 检测用户设备
|
||||
if (!IsDesktop()) {
|
||||
@@ -125,7 +127,7 @@ const App: React.FC = () => {
|
||||
{state.isFinished && <ResultScreen />}
|
||||
<Layout>
|
||||
<Header>
|
||||
<DictChapterButton index={state.chapterData.index} />
|
||||
<DictChapterButton />
|
||||
<PronunciationSwitcher />
|
||||
<Switcher />
|
||||
<StartButton isLoading={isLoading} />
|
||||
|
||||
@@ -57,7 +57,6 @@ export enum TypingStateActionType {
|
||||
SET_IS_SAVING_RECORD = 'SET_IS_SAVING_RECORD',
|
||||
SET_IS_LOOP_SINGLE_WORD = 'SET_IS_LOOP_SINGLE_WORD',
|
||||
TOGGLE_IS_LOOP_SINGLE_WORD = 'TOGGLE_IS_LOOP_SINGLE_WORD',
|
||||
SET_REVISION_INDEX = 'SET_REVISION_INDEX',
|
||||
}
|
||||
|
||||
export type TypingStateAction =
|
||||
@@ -148,8 +147,6 @@ export const typingReducer = (state: TypingState, action: TypingStateAction) =>
|
||||
case TypingStateActionType.SKIP_2_WORD_INDEX: {
|
||||
const newIndex = action.newIndex
|
||||
if (newIndex >= state.chapterData.words.length) {
|
||||
console.log('newIndex', newIndex)
|
||||
console.log('state.chapterData.words', state.chapterData.words)
|
||||
state.isTyping = false
|
||||
state.isFinished = true
|
||||
}
|
||||
|
||||
@@ -76,10 +76,6 @@ export const isShowAnswerOnHoverAtom = atomWithStorage('isShowAnswerOnHover', tr
|
||||
|
||||
export const isTextSelectableAtom = atomWithStorage('isTextSelectable', false)
|
||||
|
||||
export const isInRevisionModeAtom = atomWithStorage('isInRevisionMode', false)
|
||||
|
||||
export const isRestartRevisionProgressAtom = atomWithStorage('isRestartRevisionProgress', false)
|
||||
|
||||
export const phoneticConfigAtom = atomForConfig('phoneticConfig', {
|
||||
isOpen: true,
|
||||
type: 'us' as PhoneticType,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { IChapterRecord, IRevisionDictRecord, IWordRecord, LetterMistakes } from './record'
|
||||
import { ChapterRecord, RevisionDictRecord, WordRecord } from './record'
|
||||
import type { IChapterRecord, IWordRecord, LetterMistakes } from './record'
|
||||
import { ChapterRecord, WordRecord } from './record'
|
||||
import { TypingContext, TypingStateActionType } from '@/pages/Typing/store'
|
||||
import type { TypingState } from '@/pages/Typing/store/type'
|
||||
import { currentChapterAtom, currentDictIdAtom, isInRevisionModeAtom } from '@/store'
|
||||
import { currentChapterAtom, currentDictIdAtom } from '@/store'
|
||||
import type { Table } from 'dexie'
|
||||
import Dexie from 'dexie'
|
||||
import { useAtomValue } from 'jotai'
|
||||
@@ -11,16 +11,16 @@ import { useCallback, useContext } from 'react'
|
||||
class RecordDB extends Dexie {
|
||||
wordRecords!: Table<IWordRecord, number>
|
||||
chapterRecords!: Table<IChapterRecord, number>
|
||||
revisionDictRecords!: Table<IRevisionDictRecord, number>
|
||||
revisionWordRecords!: Table<IWordRecord, number>
|
||||
|
||||
constructor() {
|
||||
super('RecordDB')
|
||||
this.version(3).stores({
|
||||
this.version(1).stores({
|
||||
wordRecords: '++id,word,timeStamp,dict,chapter,errorCount,[dict+chapter]',
|
||||
chapterRecords: '++id,timeStamp,dict,chapter,time,[dict+chapter]',
|
||||
})
|
||||
this.version(2).stores({
|
||||
wordRecords: '++id,word,timeStamp,dict,chapter,wrongCount,[dict+chapter]',
|
||||
chapterRecords: '++id,timeStamp,dict,chapter,time,[dict+chapter]',
|
||||
revisionDictRecords: '++id,dict,revisionIndex,createdTime',
|
||||
revisionWordRecords: '++id,word,timeStamp,dict,chapter,errorCount,[dict+chapter]',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,9 @@ export const db = new RecordDB()
|
||||
|
||||
db.wordRecords.mapToClass(WordRecord)
|
||||
db.chapterRecords.mapToClass(ChapterRecord)
|
||||
db.revisionDictRecords.mapToClass(RevisionDictRecord)
|
||||
db.revisionWordRecords.mapToClass(WordRecord)
|
||||
|
||||
export function useSaveChapterRecord() {
|
||||
const currentChapter = useAtomValue(currentChapterAtom)
|
||||
const isRevision = useAtomValue(isInRevisionModeAtom)
|
||||
const dictID = useAtomValue(currentDictIdAtom)
|
||||
|
||||
const saveChapterRecord = useCallback(
|
||||
@@ -47,7 +44,7 @@ export function useSaveChapterRecord() {
|
||||
|
||||
const chapterRecord = new ChapterRecord(
|
||||
dictID,
|
||||
isRevision ? -1 : currentChapter,
|
||||
currentChapter,
|
||||
time,
|
||||
correctCount,
|
||||
wrongCount,
|
||||
@@ -58,7 +55,7 @@ export function useSaveChapterRecord() {
|
||||
)
|
||||
db.chapterRecords.add(chapterRecord)
|
||||
},
|
||||
[currentChapter, dictID, isRevision],
|
||||
[currentChapter, dictID],
|
||||
)
|
||||
|
||||
return saveChapterRecord
|
||||
@@ -70,7 +67,6 @@ export type WordKeyLogger = {
|
||||
}
|
||||
|
||||
export function useSaveWordRecord() {
|
||||
const isRevision = useAtomValue(isInRevisionModeAtom)
|
||||
const currentChapter = useAtomValue(currentChapterAtom)
|
||||
const dictID = useAtomValue(currentDictIdAtom)
|
||||
|
||||
@@ -94,7 +90,7 @@ export function useSaveWordRecord() {
|
||||
timing.push(diff)
|
||||
}
|
||||
|
||||
const wordRecord = new WordRecord(word, dictID, isRevision ? -1 : currentChapter, timing, wrongCount, letterMistake)
|
||||
const wordRecord = new WordRecord(word, dictID, currentChapter, timing, wrongCount, letterMistake)
|
||||
|
||||
let dbID = -1
|
||||
try {
|
||||
@@ -107,7 +103,7 @@ export function useSaveWordRecord() {
|
||||
dispatch({ type: TypingStateActionType.SET_IS_SAVING_RECORD, payload: false })
|
||||
}
|
||||
},
|
||||
[currentChapter, dictID, dispatch, isRevision],
|
||||
[currentChapter, dictID, dispatch],
|
||||
)
|
||||
|
||||
return saveWordRecord
|
||||
|
||||
@@ -113,42 +113,3 @@ export class ChapterRecord implements IChapterRecord {
|
||||
return Math.round((this.correctWordIndexes.length / this.wordNumber) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRevisionDictRecord {
|
||||
dict: string
|
||||
revisionIndex: number
|
||||
createdTime: number
|
||||
}
|
||||
|
||||
export class RevisionDictRecord implements IRevisionDictRecord {
|
||||
dict: string
|
||||
revisionIndex: number
|
||||
createdTime: number
|
||||
|
||||
constructor(dict: string, revisionIndex: number, createdTime: number) {
|
||||
this.dict = dict
|
||||
this.revisionIndex = revisionIndex
|
||||
this.createdTime = createdTime
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRevisionWordRecord {
|
||||
word: string
|
||||
timeStamp: number
|
||||
dict: string
|
||||
errorCount: number
|
||||
}
|
||||
|
||||
export class RevisionWordRecord implements IRevisionWordRecord {
|
||||
word: string
|
||||
timeStamp: number
|
||||
dict: string
|
||||
errorCount: number
|
||||
|
||||
constructor(word: string, dict: string, errorCount: number) {
|
||||
this.word = word
|
||||
this.timeStamp = getUTCUnixTimestamp()
|
||||
this.dict = dict
|
||||
this.errorCount = errorCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4291,11 +4291,6 @@ modern-normalize@^1.1.0:
|
||||
resolved "https://registry.npmmirror.com/modern-normalize/-/modern-normalize-1.1.0.tgz"
|
||||
integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==
|
||||
|
||||
moment@^2.29.4:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user