mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2026-04-04 22:09:04 +08:00
feat: add recite to error book (#587)
This commit is contained in:
@@ -52,5 +52,6 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'sort-imports': ['error', { ignoreDeclarationSort: true }],
|
'sort-imports': ['error', { ignoreDeclarationSort: true }],
|
||||||
'@typescript-eslint/consistent-type-imports': 1,
|
'@typescript-eslint/consistent-type-imports': 1,
|
||||||
|
'react/prop-types': 'off',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@radix-ui/react-slider": "^1.1.1",
|
"@radix-ui/react-slider": "^1.1.1",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
"daisyui": "^3.5.1",
|
||||||
"dayjs": "^1.11.8",
|
"dayjs": "^1.11.8",
|
||||||
"dexie": "^3.2.3",
|
"dexie": "^3.2.3",
|
||||||
"dexie-export-import": "^4.0.7",
|
"dexie-export-import": "^4.0.7",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const Header: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
<img src={logo} className="mr-3 h-16 w-16" alt="Qwerty Learner Logo" />
|
<img src={logo} className="mr-3 h-16 w-16" alt="Qwerty Learner Logo" />
|
||||||
<h1>Qwerty Learner</h1>
|
<h1>Qwerty Learner</h1>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<nav className="card on element flex w-auto content-center items-center justify-end space-x-3 rounded-xl bg-white p-4 transition-colors duration-300 dark:bg-gray-800">
|
<nav className="my-card on element flex w-auto content-center items-center justify-end space-x-3 rounded-xl bg-white p-4 transition-colors duration-300 dark:bg-gray-800">
|
||||||
{children}
|
{children}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.my-card {
|
||||||
box-shadow: 0px 100px 80px rgba(50, 46, 129, 0.07), 0px 41.7776px 33.4221px rgba(50, 46, 129, 0.0503198),
|
box-shadow: 0px 100px 80px rgba(50, 46, 129, 0.07), 0px 41.7776px 33.4221px rgba(50, 46, 129, 0.0503198),
|
||||||
0px 22.3363px 17.869px rgba(50, 46, 129, 0.0417275), 0px 12.5216px 10.0172px rgba(50, 46, 129, 0.035),
|
0px 22.3363px 17.869px rgba(50, 46, 129, 0.0417275), 0px 12.5216px 10.0172px rgba(50, 46, 129, 0.035),
|
||||||
0px 6.6501px 5.32008px rgba(50, 46, 129, 0.0282725), 0px 2.76726px 2.21381px rgba(50, 46, 129, 0.0196802);
|
0px 6.6501px 5.32008px rgba(50, 46, 129, 0.0282725), 0px 2.76726px 2.21381px rgba(50, 46, 129, 0.0196802);
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
|
import { LoadingWordUI } from './LoadingWordUI'
|
||||||
import useGetWord from './hooks/useGetWord'
|
import useGetWord from './hooks/useGetWord'
|
||||||
|
import { currentRowDetailAtom } from './store'
|
||||||
import type { groupedWordRecords } from './type'
|
import type { groupedWordRecords } from './type'
|
||||||
import { LoadingUI } from '@/components/Loading'
|
|
||||||
import { idDictionaryMap } from '@/resources/dictionary'
|
import { idDictionaryMap } from '@/resources/dictionary'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
type IErrorRowProps = {
|
type IErrorRowProps = {
|
||||||
record: groupedWordRecords
|
record: groupedWordRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorRow: FC<IErrorRowProps> = ({ record }) => {
|
const ErrorRow: FC<IErrorRowProps> = ({ record }) => {
|
||||||
|
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
|
||||||
const dictInfo = idDictionaryMap[record.dict]
|
const dictInfo = idDictionaryMap[record.dict]
|
||||||
const { word, isLoading } = useGetWord(record.word, dictInfo)
|
const { word, isLoading, hasError } = useGetWord(record.word, dictInfo)
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
setCurrentRowDetail(record)
|
||||||
|
}, [record, setCurrentRowDetail])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="opacity-85 flex w-full items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white">
|
<li
|
||||||
|
className="opacity-85 flex w-full cursor-pointer items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<span className="basis-2/12 break-normal">{record.word}</span>
|
<span className="basis-2/12 break-normal">{record.word}</span>
|
||||||
<span className="basis-6/12 break-normal">
|
<span className="basis-6/12 break-normal">
|
||||||
{!isLoading && word ? word.trans.join(', ') : <LoadingUI className="h-4 w-4 !border-2" />}
|
{word ? word.trans.join(';') : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}
|
||||||
</span>
|
</span>
|
||||||
<span className="basis-1/12 break-normal">{record.wrongCount}</span>
|
<span className="basis-1/12 break-normal">{record.wrongCount}</span>
|
||||||
<span className="basis-2/12 break-normal">{dictInfo.name}</span>
|
<span className="basis-2/12 break-normal">{dictInfo.name}</span>
|
||||||
|
|||||||
23
src/pages/ErrorBook/LoadingWordUI.tsx
Normal file
23
src/pages/ErrorBook/LoadingWordUI.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { LoadingUI } from '@/components/Loading'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import ErrorIcon from '~icons/ic/outline-error'
|
||||||
|
|
||||||
|
type LoadingWordUIProps = {
|
||||||
|
className?: string
|
||||||
|
isLoading: boolean
|
||||||
|
hasError: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingWordUI: FC<LoadingWordUIProps> = ({ className, isLoading, hasError }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${className}`}>
|
||||||
|
{hasError ? (
|
||||||
|
<div className="tooltip !bg-transparent" data-tip="数据加载失败">
|
||||||
|
<ErrorIcon className="text-red-500" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
isLoading && <LoadingUI />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,11 +7,12 @@ type IPaginationProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
page: number
|
page: number
|
||||||
setPage: (page: number) => void
|
setPage: (page: number) => void
|
||||||
|
totalPages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ITEM_PER_PAGE = 20
|
export const ITEM_PER_PAGE = 20
|
||||||
|
|
||||||
const Pagination: FC<IPaginationProps> = ({ className, page, setPage }) => {
|
const Pagination: FC<IPaginationProps> = ({ className, page, setPage, totalPages }) => {
|
||||||
const nextPage = useCallback(() => {
|
const nextPage = useCallback(() => {
|
||||||
setPage(page + 1)
|
setPage(page + 1)
|
||||||
}, [page, setPage])
|
}, [page, setPage])
|
||||||
@@ -28,7 +29,7 @@ const Pagination: FC<IPaginationProps> = ({ className, page, setPage }) => {
|
|||||||
>
|
>
|
||||||
<PrevIcon />
|
<PrevIcon />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-black dark:text-white">{page}</span>
|
<span className="text-black dark:text-white">{`${page} / ${totalPages}`}</span>
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer rounded-full bg-white p-2 text-indigo-500 shadow-md dark:bg-gray-800 dark:text-indigo-300"
|
className="cursor-pointer rounded-full bg-white p-2 text-indigo-500 shadow-md dark:bg-gray-800 dark:text-indigo-300"
|
||||||
onClick={nextPage}
|
onClick={nextPage}
|
||||||
|
|||||||
25
src/pages/ErrorBook/RowDetail/DataTag.tsx
Normal file
25
src/pages/ErrorBook/RowDetail/DataTag.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type React from 'react'
|
||||||
|
|
||||||
|
interface DataTagProps {
|
||||||
|
icon: React.ElementType
|
||||||
|
name: string
|
||||||
|
data: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataTag: React.FC<DataTagProps> = ({ icon, name, data }) => {
|
||||||
|
const IconComponent = icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="g flex h-10 w-40 flex-1 select-none items-center justify-between rounded-md border-gray-400 bg-gray-100 px-3 py-5 shadow dark:border-gray-600 dark:bg-gray-800">
|
||||||
|
<div className="flex items-center space-x-1 ">
|
||||||
|
<IconComponent className="h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||||
|
<span className="break-keep text-base font-normal text-gray-500 dark:text-gray-300">{name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-base font-normal text-gray-800 dark:text-gray-200">{data}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataTag
|
||||||
83
src/pages/ErrorBook/RowDetail/RowPagination.tsx
Normal file
83
src/pages/ErrorBook/RowDetail/RowPagination.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { currentRowDetailAtom } from '../store'
|
||||||
|
import type { groupedWordRecords } from '../type'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import NextIcon from '~icons/ooui/next-ltr'
|
||||||
|
import PrevIcon from '~icons/ooui/next-rtl'
|
||||||
|
|
||||||
|
type IRowPaginationProps = {
|
||||||
|
className?: string
|
||||||
|
allRecords: groupedWordRecords[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ITEM_PER_PAGE = 20
|
||||||
|
|
||||||
|
const RowPagination: FC<IRowPaginationProps> = ({ className, allRecords }) => {
|
||||||
|
const [currentRowDetail, setCurrentRowDetail] = useAtom(currentRowDetailAtom)
|
||||||
|
const currentIndex = useMemo(() => {
|
||||||
|
if (!currentRowDetail) return -1
|
||||||
|
return allRecords.findIndex((record) => record.word === currentRowDetail.word && record.dict === currentRowDetail.dict)
|
||||||
|
}, [currentRowDetail, allRecords])
|
||||||
|
|
||||||
|
const nextRowDetail = useCallback(() => {
|
||||||
|
if (!currentRowDetail) return
|
||||||
|
|
||||||
|
const index = currentIndex
|
||||||
|
if (index === -1) return
|
||||||
|
const nextIndex = index + 1
|
||||||
|
if (nextIndex >= allRecords.length) return
|
||||||
|
setCurrentRowDetail(allRecords[nextIndex])
|
||||||
|
}, [currentRowDetail, currentIndex, allRecords, setCurrentRowDetail])
|
||||||
|
|
||||||
|
const prevRowDetail = useCallback(() => {
|
||||||
|
if (!currentRowDetail) return
|
||||||
|
|
||||||
|
const index = currentIndex
|
||||||
|
if (index === -1) return
|
||||||
|
const prevIndex = index - 1
|
||||||
|
if (prevIndex < 0) return
|
||||||
|
setCurrentRowDetail(allRecords[prevIndex])
|
||||||
|
}, [currentRowDetail, currentIndex, setCurrentRowDetail, allRecords])
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'left',
|
||||||
|
(e) => {
|
||||||
|
prevRowDetail()
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'right',
|
||||||
|
(e) => {
|
||||||
|
nextRowDetail()
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`-gap-1 flex select-none items-center ${className}`}>
|
||||||
|
<button
|
||||||
|
className="d cursor-pointer rounded-full p-1 text-indigo-500 focus:outline-none dark:text-indigo-300"
|
||||||
|
onClick={prevRowDetail}
|
||||||
|
>
|
||||||
|
<PrevIcon />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-black dark:text-white">{`${currentIndex + 1} / ${allRecords.length}`}</span>
|
||||||
|
<button className="cursor-pointer rounded-full p-1 text-indigo-500 focus:outline-none dark:text-indigo-300" onClick={nextRowDetail}>
|
||||||
|
<NextIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RowPagination
|
||||||
96
src/pages/ErrorBook/RowDetail/index.tsx
Normal file
96
src/pages/ErrorBook/RowDetail/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
import { LoadingWordUI } from '../LoadingWordUI'
|
||||||
|
import useGetWord from '../hooks/useGetWord'
|
||||||
|
import { currentRowDetailAtom } from '../store'
|
||||||
|
import type { groupedWordRecords } from '../type'
|
||||||
|
import DataTag from './DataTag'
|
||||||
|
import RowPagination from './RowPagination'
|
||||||
|
import Phonetic from '@/pages/Typing/components/WordPanel/components/Phonetic'
|
||||||
|
import Letter from '@/pages/Typing/components/WordPanel/components/Word/Letter'
|
||||||
|
import { idDictionaryMap } from '@/resources/dictionary'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import HashtagIcon from '~icons/heroicons/chart-pie-20-solid'
|
||||||
|
import CheckCircle from '~icons/heroicons/check-circle-20-solid'
|
||||||
|
import ClockIcon from '~icons/heroicons/clock-20-solid'
|
||||||
|
import XCircle from '~icons/heroicons/x-circle-20-solid'
|
||||||
|
import IconX from '~icons/tabler/x'
|
||||||
|
|
||||||
|
type RowDetailProps = {
|
||||||
|
currentRowDetail: groupedWordRecords
|
||||||
|
allRecords: groupedWordRecords[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowDetail: React.FC<RowDetailProps> = ({ currentRowDetail, allRecords }) => {
|
||||||
|
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
|
||||||
|
const dictInfo = idDictionaryMap[currentRowDetail.dict]
|
||||||
|
const { word, isLoading, hasError } = useGetWord(currentRowDetail.word, dictInfo)
|
||||||
|
|
||||||
|
const rowDetailData: RowDetailData = useMemo(() => {
|
||||||
|
const time =
|
||||||
|
currentRowDetail.records.length > 0
|
||||||
|
? currentRowDetail.records.reduce((acc, cur) => acc + cur.totalTime, 0) / currentRowDetail.records.length
|
||||||
|
: 0
|
||||||
|
const timeStr = (time / 1000).toFixed(2)
|
||||||
|
const correctCount = currentRowDetail.records.length
|
||||||
|
const wrongCount = currentRowDetail.wrongCount
|
||||||
|
const sumCount = correctCount + wrongCount
|
||||||
|
return { time: timeStr, sumCount, correctCount, wrongCount }
|
||||||
|
}, [currentRowDetail.records, currentRowDetail.wrongCount])
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setCurrentRowDetail(null)
|
||||||
|
}, [setCurrentRowDetail])
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'esc',
|
||||||
|
(e) => {
|
||||||
|
onClose()
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
{ preventDefault: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center ">
|
||||||
|
<div className="my-card relative z-10 flex h-[32rem] min-w-[26rem] select-text flex-col items-center justify-around rounded-2xl bg-white px-3 py-10 dark:bg-gray-900">
|
||||||
|
<IconX className="absolute right-3 top-3 h-6 w-6 cursor-pointer text-gray-400" onClick={onClose} />
|
||||||
|
<div className="flex flex-col items-center justify-start">
|
||||||
|
<div>
|
||||||
|
{currentRowDetail.word.split('').map((t, index) => (
|
||||||
|
<Letter key={`${index}-${t}`} letter={t} visible state="normal" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>{word ? <Phonetic word={word} /> : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}</div>
|
||||||
|
<div className="flex max-w-[24rem] items-center">
|
||||||
|
<span className={`max-w-4xl text-center font-sans transition-colors duration-300 dark:text-white dark:text-opacity-80`}>
|
||||||
|
{word ? word.trans.join(';') : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item flex flex-col gap-4">
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<DataTag icon={ClockIcon} name="平均用时" data={rowDetailData.time} />
|
||||||
|
<DataTag icon={HashtagIcon} name="练习次数" data={rowDetailData.sumCount} />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<DataTag icon={CheckCircle} name="正确次数" data={rowDetailData.correctCount} />
|
||||||
|
<DataTag icon={XCircle} name="错误次数" data={rowDetailData.wrongCount} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<RowPagination className="absolute bottom-6 mt-10" allRecords={allRecords} />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 z-0 cursor-pointer bg-transparent" onClick={onClose}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowDetailData = {
|
||||||
|
time: string
|
||||||
|
sumCount: number
|
||||||
|
correctCount: number
|
||||||
|
wrongCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RowDetail
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
import type { Dictionary, Word } from '@/typings'
|
import type { Dictionary, Word } from '@/typings'
|
||||||
import { wordListFetcher } from '@/utils/wordListFetcher'
|
import { wordListFetcher } from '@/utils/wordListFetcher'
|
||||||
import { useMemo } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
|
|
||||||
export default function useGetWord(name: string, dict: Dictionary) {
|
export default function useGetWord(name: string, dict: Dictionary) {
|
||||||
const { data: wordList, error, isLoading } = useSWR(dict.url, wordListFetcher)
|
const { data: wordList, error, isLoading } = useSWR(dict?.url, wordListFetcher)
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
|
||||||
const word: Word | undefined = useMemo(() => {
|
const word: Word | undefined = useMemo(() => {
|
||||||
if (!wordList) return undefined
|
if (!wordList) return undefined
|
||||||
|
|
||||||
return wordList.find((word) => word.name === name)
|
const word = wordList.find((word) => word.name === name)
|
||||||
|
if (word) {
|
||||||
|
return word
|
||||||
|
} else {
|
||||||
|
setHasError(true)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}, [wordList, name])
|
}, [wordList, name])
|
||||||
|
|
||||||
return { word, isLoading, error }
|
useEffect(() => {
|
||||||
|
if (error) setHasError(true)
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
return { word, isLoading, hasError }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import ErrorRow from './ErrorRow'
|
|||||||
import type { ISortType } from './HeadWrongNumber'
|
import type { ISortType } from './HeadWrongNumber'
|
||||||
import HeadWrongNumber from './HeadWrongNumber'
|
import HeadWrongNumber from './HeadWrongNumber'
|
||||||
import Pagination, { ITEM_PER_PAGE } from './Pagination'
|
import Pagination, { ITEM_PER_PAGE } from './Pagination'
|
||||||
|
import RowDetail from './RowDetail'
|
||||||
|
import { currentRowDetailAtom } from './store'
|
||||||
import type { groupedWordRecords } from './type'
|
import type { groupedWordRecords } from './type'
|
||||||
import { db } from '@/utils/db'
|
import { db } from '@/utils/db'
|
||||||
|
import type { WordRecord } from '@/utils/db/record'
|
||||||
import * as ScrollArea from '@radix-ui/react-scroll-area'
|
import * as ScrollArea from '@radix-ui/react-scroll-area'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import IconX from '~icons/tabler/x'
|
import IconX from '~icons/tabler/x'
|
||||||
@@ -15,6 +19,7 @@ export function ErrorBook() {
|
|||||||
const totalPages = useMemo(() => Math.ceil(groupedRecords.length / ITEM_PER_PAGE), [groupedRecords.length])
|
const totalPages = useMemo(() => Math.ceil(groupedRecords.length / ITEM_PER_PAGE), [groupedRecords.length])
|
||||||
const [sortType, setSortType] = useState<ISortType>('asc')
|
const [sortType, setSortType] = useState<ISortType>('asc')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const currentRowDetail = useAtomValue(currentRowDetailAtom)
|
||||||
|
|
||||||
const onBack = useCallback(() => {
|
const onBack = useCallback(() => {
|
||||||
navigate('/')
|
navigate('/')
|
||||||
@@ -47,6 +52,12 @@ export function ErrorBook() {
|
|||||||
})
|
})
|
||||||
}, [groupedRecords, sortType])
|
}, [groupedRecords, sortType])
|
||||||
|
|
||||||
|
const renderRecords = useMemo(() => {
|
||||||
|
const start = (currentPage - 1) * ITEM_PER_PAGE
|
||||||
|
const end = start + ITEM_PER_PAGE
|
||||||
|
return sortedRecords.slice(start, end)
|
||||||
|
}, [currentPage, sortedRecords])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
db.wordRecords
|
db.wordRecords
|
||||||
.where('wrongCount')
|
.where('wrongCount')
|
||||||
@@ -61,7 +72,7 @@ export function ErrorBook() {
|
|||||||
group = { word: record.word, dict: record.dict, records: [], wrongCount: 0 }
|
group = { word: record.word, dict: record.dict, records: [], wrongCount: 0 }
|
||||||
groups.push(group)
|
groups.push(group)
|
||||||
}
|
}
|
||||||
group.records.push(record)
|
group.records.push(record as WordRecord)
|
||||||
})
|
})
|
||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
@@ -75,36 +86,37 @@ export function ErrorBook() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const renderRecords = useMemo(() => {
|
|
||||||
const start = (currentPage - 1) * ITEM_PER_PAGE
|
|
||||||
const end = start + ITEM_PER_PAGE
|
|
||||||
return sortedRecords.slice(start, end)
|
|
||||||
}, [currentPage, sortedRecords])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col items-center pb-4">
|
<>
|
||||||
<IconX className="absolute right-10 top-5 mr-2 h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
|
<div className={`relative flex h-screen w-full flex-col items-center pb-4 ease-in ${currentRowDetail && 'blur-sm'}`}>
|
||||||
<div className="flex w-full flex-1 select-text items-start justify-center overflow-hidden">
|
<div className="mr-8 mt-4 flex w-auto items-center justify-center self-end">
|
||||||
<div className="flex h-full w-5/6 flex-col pt-10">
|
<h1 className="font-lighter mr-4 w-auto self-end text-gray-500 opacity-70">Tip: 点击错误单词查看详细信息 </h1>
|
||||||
<div className="flex w-full justify-between rounded-lg bg-white px-6 py-5 text-lg text-black shadow-lg dark:bg-gray-800 dark:text-white">
|
<IconX className="h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
|
||||||
<span className="basis-2/12">单词</span>
|
|
||||||
<span className="basis-6/12">释义</span>
|
|
||||||
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
|
|
||||||
<span className="basis-2/12">词典</span>
|
|
||||||
</div>
|
|
||||||
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
|
|
||||||
<ScrollArea.Viewport className="h-full ">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{renderRecords.map((record) => (
|
|
||||||
<ErrorRow key={`${record.dict}-${record.word}`} record={record} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea.Viewport>
|
|
||||||
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent" orientation="vertical"></ScrollArea.Scrollbar>
|
|
||||||
</ScrollArea.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-1 select-text items-start justify-center overflow-hidden">
|
||||||
|
<div className="flex h-full w-5/6 flex-col pt-10">
|
||||||
|
<div className="flex w-full justify-between rounded-lg bg-white px-6 py-5 text-lg text-black shadow-lg dark:bg-gray-800 dark:text-white">
|
||||||
|
<span className="basis-2/12">单词</span>
|
||||||
|
<span className="basis-6/12">释义</span>
|
||||||
|
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
|
||||||
|
<span className="basis-2/12">词典</span>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
|
||||||
|
<ScrollArea.Viewport className="h-full ">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{renderRecords.map((record) => (
|
||||||
|
<ErrorRow key={`${record.dict}-${record.word}`} record={record} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent" orientation="vertical"></ScrollArea.Scrollbar>
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Pagination className="pt-3" page={currentPage} setPage={setPage} totalPages={totalPages} />
|
||||||
</div>
|
</div>
|
||||||
<Pagination className="pt-3" page={currentPage} setPage={setPage} />
|
{currentRowDetail && <RowDetail currentRowDetail={currentRowDetail} allRecords={sortedRecords} />}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/pages/ErrorBook/store/index.ts
Normal file
4
src/pages/ErrorBook/store/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import type { groupedWordRecords } from '../type'
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
|
export const currentRowDetailAtom = atom<groupedWordRecords | null>(null)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { IWordRecord } from '@/utils/db/record'
|
import type { WordRecord } from '@/utils/db/record'
|
||||||
|
|
||||||
export type groupedWordRecords = {
|
export type groupedWordRecords = {
|
||||||
word: string
|
word: string
|
||||||
dict: string
|
dict: string
|
||||||
records: IWordRecord[]
|
records: WordRecord[]
|
||||||
wrongCount: number
|
wrongCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ const ResultScreen = () => {
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="flex h-screen items-center justify-center">
|
<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="my-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">
|
<div className="text-center font-sans text-xl font-normal text-gray-900 dark:text-gray-400 md:text-2xl">
|
||||||
{`${currentDictInfo.name} 第 ${currentChapter + 1} 章`}
|
{`${currentDictInfo.name} 第 ${currentChapter + 1} 章`}
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +268,7 @@ const ResultScreen = () => {
|
|||||||
{!isLastChapter && (
|
{!isLastChapter && (
|
||||||
<Tooltip content="快捷键:enter">
|
<Tooltip content="快捷键:enter">
|
||||||
<button
|
<button
|
||||||
className={`btn-primary { isLastChapter ? 'cursor-not-allowed opacity-50' : ''} h-12 text-base font-bold `}
|
className={`{ isLastChapter ? 'cursor-not-allowed opacity-50' : ''} btn-primary h-12 text-base font-bold `}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={nextButtonHandler}
|
onClick={nextButtonHandler}
|
||||||
title="下一章节"
|
title="下一章节"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default function Speed() {
|
|||||||
const inputNumber = state.chapterData.correctCount + state.chapterData.wrongCount
|
const inputNumber = state.chapterData.correctCount + state.chapterData.wrongCount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card flex w-3/5 rounded-xl bg-white p-4 py-10 opacity-50 transition-colors duration-300 dark:bg-gray-800">
|
<div className="my-card flex w-3/5 rounded-xl bg-white p-4 py-10 opacity-50 transition-colors duration-300 dark:bg-gray-800">
|
||||||
<InfoBox info={`${minutesString}:${secondsString}`} description="时间" />
|
<InfoBox info={`${minutesString}:${secondsString}`} description="时间" />
|
||||||
<InfoBox info={inputNumber + ''} description="输入数" />
|
<InfoBox info={inputNumber + ''} description="输入数" />
|
||||||
<InfoBox info={state.timerData.wpm + ''} description="WPM" />
|
<InfoBox info={state.timerData.wpm + ''} description="WPM" />
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { isTextSelectableAtom, phoneticConfigAtom } from '@/store'
|
import { isTextSelectableAtom, phoneticConfigAtom } from '@/store'
|
||||||
import type { WordWithIndex } from '@/typings'
|
import type { Word, WordWithIndex } from '@/typings'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
export type PhoneticProps = {
|
export type PhoneticProps = {
|
||||||
word: WordWithIndex
|
word: WordWithIndex | Word
|
||||||
}
|
}
|
||||||
|
|
||||||
function Phonetic({ word }: PhoneticProps) {
|
function Phonetic({ word }: PhoneticProps) {
|
||||||
|
|||||||
@@ -55,5 +55,23 @@ module.exports = {
|
|||||||
backgroundOpacity: ['dark'],
|
backgroundOpacity: ['dark'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms')],
|
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms'), require('daisyui')],
|
||||||
|
daisyui: {
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
mytheme: {
|
||||||
|
primary: '#6366f1',
|
||||||
|
secondary: '#7dd3fc',
|
||||||
|
accent: '#cc8316',
|
||||||
|
neutral: '#272735',
|
||||||
|
'base-100': '#f0eff1',
|
||||||
|
info: '#f3f4f6',
|
||||||
|
success: '#6fe7ab',
|
||||||
|
warning: '#d6920a',
|
||||||
|
error: '#f43f5e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'dark',
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
68
yarn.lock
68
yarn.lock
@@ -2539,6 +2539,11 @@ color@^4.0.1:
|
|||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
color-string "^1.9.0"
|
color-string "^1.9.0"
|
||||||
|
|
||||||
|
colord@^2.9:
|
||||||
|
version "2.9.3"
|
||||||
|
resolved "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
|
||||||
|
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
||||||
|
|
||||||
colorette@^2.0.19:
|
colorette@^2.0.19:
|
||||||
version "2.0.20"
|
version "2.0.20"
|
||||||
resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz"
|
resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz"
|
||||||
@@ -2640,6 +2645,14 @@ css-color-names@^0.0.4:
|
|||||||
resolved "https://registry.npmmirror.com/css-color-names/-/css-color-names-0.0.4.tgz"
|
resolved "https://registry.npmmirror.com/css-color-names/-/css-color-names-0.0.4.tgz"
|
||||||
integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==
|
integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==
|
||||||
|
|
||||||
|
css-selector-tokenizer@^0.8:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.npmmirror.com/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz#88267ef6238e64f2215ea2764b3e2cf498b845dd"
|
||||||
|
integrity sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==
|
||||||
|
dependencies:
|
||||||
|
cssesc "^3.0.0"
|
||||||
|
fastparse "^1.1.2"
|
||||||
|
|
||||||
css-unit-converter@^1.1.1:
|
css-unit-converter@^1.1.1:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmmirror.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz"
|
resolved "https://registry.npmmirror.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz"
|
||||||
@@ -2655,6 +2668,17 @@ csstype@^3.0.2:
|
|||||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz"
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz"
|
||||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
|
daisyui@^3.5.1:
|
||||||
|
version "3.5.1"
|
||||||
|
resolved "https://registry.npmmirror.com/daisyui/-/daisyui-3.5.1.tgz#7b53668e2b68007b275fec88268f81b4aeaf3781"
|
||||||
|
integrity sha512-7GG+9QXnr2qQMCqnyFU8TxpaOYJigXiEtmzoivmiiZZHvxqIwYdaMAkgivqTVxEgy3Hot3m1suzZjmt1zUrvmA==
|
||||||
|
dependencies:
|
||||||
|
colord "^2.9"
|
||||||
|
css-selector-tokenizer "^0.8"
|
||||||
|
postcss "^8"
|
||||||
|
postcss-js "^4"
|
||||||
|
tailwindcss "^3"
|
||||||
|
|
||||||
damerau-levenshtein@^1.0.8:
|
damerau-levenshtein@^1.0.8:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
|
resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
|
||||||
@@ -3289,6 +3313,11 @@ fast-levenshtein@^2.0.6:
|
|||||||
resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
||||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||||
|
|
||||||
|
fastparse@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
|
||||||
|
integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
|
||||||
|
|
||||||
fastq@^1.6.0:
|
fastq@^1.6.0:
|
||||||
version "1.15.0"
|
version "1.15.0"
|
||||||
resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz"
|
resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz"
|
||||||
@@ -4624,7 +4653,7 @@ postcss-js@^2:
|
|||||||
camelcase-css "^2.0.1"
|
camelcase-css "^2.0.1"
|
||||||
postcss "^7.0.18"
|
postcss "^7.0.18"
|
||||||
|
|
||||||
postcss-js@^4.0.1:
|
postcss-js@^4, postcss-js@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz"
|
resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz"
|
||||||
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
|
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
|
||||||
@@ -4718,6 +4747,15 @@ postcss@^7, postcss@^7.0.18, postcss@^7.0.32:
|
|||||||
picocolors "^0.2.1"
|
picocolors "^0.2.1"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
|
postcss@^8:
|
||||||
|
version "8.4.28"
|
||||||
|
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
|
||||||
|
integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.6"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.0.0, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23:
|
postcss@^8.0.0, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23:
|
||||||
version "8.4.23"
|
version "8.4.23"
|
||||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz"
|
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz"
|
||||||
@@ -5397,6 +5435,34 @@ tabbable@^6.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/tabbable/-/tabbable-6.1.2.tgz"
|
resolved "https://registry.npmmirror.com/tabbable/-/tabbable-6.1.2.tgz"
|
||||||
integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==
|
integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==
|
||||||
|
|
||||||
|
tailwindcss@^3:
|
||||||
|
version "3.3.3"
|
||||||
|
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
|
||||||
|
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
|
||||||
|
dependencies:
|
||||||
|
"@alloc/quick-lru" "^5.2.0"
|
||||||
|
arg "^5.0.2"
|
||||||
|
chokidar "^3.5.3"
|
||||||
|
didyoumean "^1.2.2"
|
||||||
|
dlv "^1.1.3"
|
||||||
|
fast-glob "^3.2.12"
|
||||||
|
glob-parent "^6.0.2"
|
||||||
|
is-glob "^4.0.3"
|
||||||
|
jiti "^1.18.2"
|
||||||
|
lilconfig "^2.1.0"
|
||||||
|
micromatch "^4.0.5"
|
||||||
|
normalize-path "^3.0.0"
|
||||||
|
object-hash "^3.0.0"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
postcss "^8.4.23"
|
||||||
|
postcss-import "^15.1.0"
|
||||||
|
postcss-js "^4.0.1"
|
||||||
|
postcss-load-config "^4.0.1"
|
||||||
|
postcss-nested "^6.0.1"
|
||||||
|
postcss-selector-parser "^6.0.11"
|
||||||
|
resolve "^1.22.2"
|
||||||
|
sucrase "^3.32.0"
|
||||||
|
|
||||||
tailwindcss@^3.3.1:
|
tailwindcss@^3.3.1:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.2.tgz"
|
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.2.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user