feat: update info panel (#367)

This commit is contained in:
Kaiyi
2023-03-28 19:01:39 +08:00
committed by GitHub
parent 6cbd49bea3
commit d877654cf6
11 changed files with 245 additions and 112 deletions

View File

@@ -21,6 +21,7 @@
"ukphone",
"usphone",
"vercel",
"Weixin",
"wordlist"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 57 KiB

BIN
src/assets/weChat-group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
src/assets/weChat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,68 +1,125 @@
import React, { useState } from 'react'
import React, { useCallback } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import InfoPanel from '@/components/InfoPanel'
import alipay from '@/assets/alipay.png'
import weChat from '@/assets/weChat.png'
import weChatGroup from '@/assets/weChat-group.png'
import vscLogo from '@/assets/vsc-logo.svg'
import { recordOpenInfoPanelAction } from '@/utils'
import { InfoPanelType } from '@/typings'
import { useAtom } from 'jotai'
import { infoPanelStateAtom } from '@/store'
const Footer: React.FC = () => {
const [showModal, setShowModal] = useState<boolean>(false)
const [content, setShowContent] = useState('donate')
const [infoPanelState, setInfoPanelState] = useAtom(infoPanelStateAtom)
const icon = content === 'donate' ? 'coffee' : 'terminal'
const color = content === 'donate' ? 'bg-yellow-100' : 'bg-blue-300'
const btnColor = content === 'donate' ? 'bg-yellow-300' : 'bg-blue-400'
const iconColor = content === 'donate' ? 'text-yellow-500' : 'text-blue-600'
const handleOpenInfoPanel = useCallback(
(modalType: InfoPanelType) => {
recordOpenInfoPanelAction(modalType, 'footer')
setInfoPanelState((state) => {
return {
...state,
[modalType]: true,
}
})
},
[setInfoPanelState],
)
const handleCloseInfoPanel = useCallback(
(modalType: InfoPanelType) => {
setInfoPanelState((state) => {
return {
...state,
[modalType]: false,
}
})
},
[setInfoPanelState],
)
return (
<>
{showModal && (
{infoPanelState.donate && (
<InfoPanel
state={showModal}
icon={icon}
color={color}
btnColor={btnColor}
iconColor={iconColor}
buttonOnclick={() => setShowModal(false)}
openState={infoPanelState.donate}
title="Buy us a coffee"
icon="coffee"
btnColor="bg-yellow-300"
iconColor="text-yellow-500"
iconBackgroundColor="bg-yellow-100"
onClose={() => handleCloseInfoPanel('donate')}
>
{content === 'donate' ? (
<>
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white dark:text-opacity-70" id="modal-headline">
Buy me a coffee
</h3>
<div className="mt-2 ">
<p className="text-sm text-gray-500 dark:text-gray-400">
使 Qwerty Learner,
使(使vercel部署)便访
</p>
<br />
<p className="text-sm text-gray-700 dark:text-gray-200">!</p>
<br />
<img className="ml-1 w-2/6 " src={alipay} alt="alipay" />
</div>
</>
) : (
<>
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white dark:text-opacity-70" id="modal-headline">
VSCode 🐟
</h3>
<div className="mt-2 ">
<p className="text-sm text-gray-500 dark:text-gray-400">
VSCode
</p>
<br /> <br />
<a className="mr-5 underline dark:text-gray-300" href="https://github.com/Kaiyiwing/qwerty-learner-vscode">
GitHub
</a>
<a className="underline dark:text-gray-300" href="https://marketplace.visualstudio.com/items?itemName=Kaiyi.qwerty-learner">
VSCode
</a>
<br />
</div>
</>
)}
<p className="text-sm text-gray-500 dark:text-gray-300">
使 Qwerty Learner, 使
<br />
<br />
</p>
<br />
<p className="text-sm text-gray-700 dark:text-gray-200">
Qwerty Learner
</p>
<br />
<div className="flex w-full justify-start">
<img src={alipay} alt="alipay" className="mx-4 w-1/3" />
<img src={weChat} alt="weChat" className="mx-4 w-1/3" />
</div>
</InfoPanel>
)}
{infoPanelState.vsc && (
<InfoPanel
openState={infoPanelState.vsc}
title="VSCode 摸🐟插件"
icon="terminal"
btnColor="bg-blue-400"
iconColor="text-blue-600"
iconBackgroundColor="bg-blue-300"
onClose={() => handleCloseInfoPanel('vsc')}
>
<p className="text-sm text-gray-500 dark:text-gray-400">
VSCode
</p>
<br /> <br />
<a className="mr-5 underline dark:text-gray-300" href="https://github.com/Kaiyiwing/qwerty-learner-vscode">
GitHub
</a>
<a className="underline dark:text-gray-300" href="https://marketplace.visualstudio.com/items?itemName=Kaiyi.qwerty-learner">
VSCode
</a>
<br />
</InfoPanel>
)}
{infoPanelState.community && (
<InfoPanel
openState={infoPanelState.community}
title="用户反馈社群"
icon={['fab', 'weixin']}
btnColor="bg-cyan-400"
iconColor="text-cyan-600"
iconBackgroundColor="bg-cyan-300"
onClose={() => handleCloseInfoPanel('community')}
>
<p className="text-sm text-gray-500 dark:text-gray-400">
Qwerty Learner
<br />
使
<br />
<br />
</p>
<p className="text-sm text-gray-700 dark:text-gray-200">
Qwerty Learner
</p>
<br />
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
<br />
<img className="ml-1 w-2/6 " src={weChatGroup} alt="alipay" />
<br />
</InfoPanel>
)}
<div className="mt-4 w-full pb-1 text-center text-sm ease-in" onClick={(e) => e.currentTarget.blur()}>
<a href="https://github.com/Kaiyiwing/qwerty-learner" target="_blank" rel="noreferrer">
<FontAwesomeIcon icon={['fab', 'github']} className="mr-3 text-gray-500 dark:text-gray-400" />
@@ -71,8 +128,17 @@ const Footer: React.FC = () => {
<span
className="cursor-pointer"
onClick={(e) => {
setShowContent('donate')
setShowModal(true)
handleOpenInfoPanel('community')
e.currentTarget.blur()
}}
>
<FontAwesomeIcon icon={['fab', 'weixin']} className="mr-3 text-gray-500 dark:text-gray-400" />
</span>
<span
className="cursor-pointer"
onClick={(e) => {
handleOpenInfoPanel('donate')
e.currentTarget.blur()
}}
>
@@ -82,15 +148,14 @@ const Footer: React.FC = () => {
<span
className="mr-3 cursor-pointer"
onClick={(e) => {
setShowContent('vscode')
setShowModal(true)
handleOpenInfoPanel('vsc')
e.currentTarget.blur()
}}
>
<img src={vscLogo} className="svg-inline--fa fill-current text-gray-500" alt="visual studio code" />
</span>
<a href="mailto:ZHANG.Kaiyi42@gmail.com" target="_blank" rel="noreferrer" onClick={(e) => e.currentTarget.blur()}>
<a href="mailto:me@kaiyi.cool" target="_blank" rel="noreferrer" onClick={(e) => e.currentTarget.blur()}>
<FontAwesomeIcon icon={['fas', 'envelope']} className="mr-3 text-gray-500 dark:text-gray-400" />
</a>
@@ -105,8 +170,7 @@ const Footer: React.FC = () => {
<span
className="cursor-pointer text-gray-500 no-underline hover:no-underline dark:text-gray-400 "
onClick={(e) => {
setShowContent('donate')
setShowModal(true)
handleOpenInfoPanel('donate')
e.currentTarget.blur()
}}
>

View File

@@ -1,68 +1,77 @@
import React, { MouseEvent } from 'react'
import { Transition } from '@headlessui/react'
import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
type InfoPanelProps = {
state: boolean
buttonOnclick: (e: MouseEvent) => void
openState: boolean
onClose: () => void
title: string
icon: IconProp
color: string
btnColor: string
iconColor: string
iconBackgroundColor: string
btnColor: string
children: React.ReactNode
}
const InfoPanel: React.FC<InfoPanelProps> = ({ state, buttonOnclick, icon, color, iconColor, btnColor, children }) => {
const InfoPanel: React.FC<InfoPanelProps> = ({ openState, title, onClose, icon, iconBackgroundColor, iconColor, btnColor, children }) => {
return (
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Transition
show={state}
enter="ease-out duration-30"
enterFrom="opacity-0 "
enterTo="opacity-100 "
<Transition.Root show={openState} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => onClose()}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
</Transition>
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div
className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:align-middle "
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
ref={(el) => el && el.classList.add('group')}
>
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-md dark:bg-opacity-70 ${color} sm:mx-0 sm:h-10 sm:w-10 `}
>
<FontAwesomeIcon icon={icon} className={`h-5 w-5 stroke-current ${iconColor}`} />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">{children}</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 dark:bg-gray-700 dark:bg-opacity-10 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
className={`inline-flex w-full justify-center rounded-md border border-transparent px-4 py-2 shadow-sm ${btnColor} text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-opacity-70 dark:text-opacity-80 sm:ml-3 sm:w-auto sm:text-sm `}
onClick={buttonOnclick}
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{'关闭'}
</button>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg ">
<div className="bg-white px-4 pt-5 pb-4 dark:bg-gray-800 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div
className={`${iconBackgroundColor} mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full dark:bg-opacity-50 sm:mx-0 sm:h-10 sm:w-10`}
>
<FontAwesomeIcon icon={icon} className={`h-5 w-5 stroke-current dark:bg-opacity-100 ${iconColor}`} />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-base font-semibold leading-6 text-gray-900 dark:text-white">
{title}
</Dialog.Title>
<div className="mt-2">{children}</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 dark:bg-gray-700 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
className={`${btnColor} mt-3 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none dark:bg-opacity-70 dark:text-opacity-80 sm:ml-3 sm:mt-0 sm:w-auto sm:w-auto sm:text-sm`}
onClick={() => onClose()}
>
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

View File

@@ -27,12 +27,13 @@ import {
faTimes,
faTimesCircle,
} from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { faGithub, faWeixin } from '@fortawesome/free-brands-svg-icons'
library.add(
faKeyboard,
faBookReader,
faGithub,
faWeixin,
faEnvelope,
faVolumeUp,
faVolumeMute,

View File

@@ -1,14 +1,16 @@
import { Transition } from '@headlessui/react'
import Tooltip from '@/components/Tooltip'
import { useWordList } from '@/pages/Typing/hooks/useWordList'
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import ConclusionBar from './ConclusionBar'
import RemarkRing from './RemarkRing'
import WordChip from './WordChip'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useAtomValue } from 'jotai'
import { currentChapterAtom, currentDictInfoAtom } from '@/store'
import { useAtomValue, useSetAtom } from 'jotai'
import { currentChapterAtom, currentDictInfoAtom, infoPanelStateAtom } from '@/store'
import { recordOpenInfoPanelAction } from '@/utils'
import { InfoPanelType } from '@/typings'
export type IncorrectInfo = {
word: string
@@ -41,6 +43,7 @@ const ResultScreen = ({
const wordList = useWordList()
const currentDictInfo = useAtomValue(currentDictInfoAtom)
const currentChapter = useAtomValue(currentChapterAtom)
const setInfoPanelState = useSetAtom(infoPanelStateAtom)
const isLastChapter = useMemo(() => {
return currentChapter >= currentDictInfo.length - 1
@@ -86,6 +89,19 @@ const ResultScreen = ({
invisibleButtonHandler()
})
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>
@@ -112,7 +128,7 @@ const ResultScreen = ({
<RemarkRing remark={timeString} caption="章节耗时" />
<RemarkRing remark={`${speedInfo.speed}个/s`} caption="输入字符" />
</div>
<div className="z-10 mx-6 flex-1 overflow-visible rounded-xl bg-indigo-50 dark:bg-gray-700">
<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">
{incorrectInfo.map((info) => (
<WordChip key={info.word} mistake={info} />
@@ -122,6 +138,26 @@ const ResultScreen = ({
<ConclusionBar mistakeLevel={mistakeLevel} mistakeCount={incorrectInfo.length} />
</div>
</div>
<div className="ml-2 flex flex-col items-center justify-end text-xl">
<span
className="cursor-pointer"
onClick={(e) => {
handleOpenInfoPanel('donate')
e.currentTarget.blur()
}}
>
<FontAwesomeIcon icon={['fas', 'coffee']} className=" w-10 text-gray-500 dark:text-gray-400" />
</span>
<span
className="cursor-pointer"
onClick={(e) => {
handleOpenInfoPanel('community')
e.currentTarget.blur()
}}
>
<FontAwesomeIcon icon={['fab', 'weixin']} className=" w-10 text-gray-500 dark:text-gray-400" />
</span>
</div>
</div>
<div className="mt-10 flex w-full justify-center gap-5 px-5 text-xl">
<Tooltip content="快捷键shift + enter">

View File

@@ -1,7 +1,7 @@
import { atomWithStorage } from 'jotai/utils'
import { atom, SetStateAction, WritableAtom } from 'jotai'
import { keySoundResources, wrongSoundResources, correctSoundResources } from '@/resources/soundResource'
import { PronunciationType, PhoneticType, Dictionary } from '@/typings'
import { PronunciationType, PhoneticType, Dictionary, InfoPanelState } from '@/typings'
import { idDictionaryMap } from '@/resources/dictionary'
import { CHAPTER_LENGTH } from '@/constants'
@@ -56,11 +56,17 @@ export const isChapterEndAtom = atom(false)
export const isInDevModeAtom = atom(false)
export const infoPanelStateAtom = atom<InfoPanelState>({
donate: false,
vsc: false,
community: false,
})
let dismissStartCardDateAtom: WritableAtom<Date | null, [SetStateAction<Date | null>], void>
if (process.env.NODE_ENV === 'production') {
dismissStartCardDateAtom = atomWithStorage<Date | null>('dismissStartCardDate', null)
} else {
// for dev test
dismissStartCardDateAtom = atom<Date | null>(null)
dismissStartCardDateAtom = atom<Date | null>(new Date())
}
export { dismissStartCardDateAtom }

View File

@@ -11,3 +11,9 @@ export type Word = {
usphone: string
ukphone: string
}
export type InfoPanelType = 'donate' | 'vsc' | 'community'
export type InfoPanelState = {
[key in InfoPanelType]: boolean
}

View File

@@ -1,3 +1,4 @@
import { InfoPanelType } from '@/typings'
import mixpanel from 'mixpanel-browser'
export type starAction = 'star' | 'dismiss'
@@ -8,3 +9,12 @@ export function recordStarAction(action: starAction) {
}
mixpanel.track('star', props)
}
export type openInfoPanelLocation = 'footer' | 'resultScreen'
export function recordOpenInfoPanelAction(type: InfoPanelType, location: openInfoPanelLocation) {
const props = {
type,
location,
}
mixpanel.track('openInfoPanel', props)
}