mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2026-04-04 22:09:04 +08:00
feat: add vercel analysis track
This commit is contained in:
166
src/components/EnhancedPromotionModal/index.tsx
Normal file
166
src/components/EnhancedPromotionModal/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import noop from '../../utils/noop'
|
||||
import { hasSeenEnhancedPromotionAtom } from '@/store'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { track } from '@vercel/analytics'
|
||||
import { useAtom } from 'jotai'
|
||||
import type React from 'react'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import IconStar from '~icons/heroicons/star-solid'
|
||||
import IconX from '~icons/tabler/x'
|
||||
|
||||
const EnhancedPromotionModal: React.FC = () => {
|
||||
const [hasSeenPromotion, setHasSeenPromotion] = useAtom(hasSeenEnhancedPromotionAtom)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Only show modal if user hasn't seen it before
|
||||
if (!hasSeenPromotion) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsOpen(true)
|
||||
}, 2000) // Show after 2 seconds to let the page load
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasSeenPromotion])
|
||||
|
||||
const handleTryNow = () => {
|
||||
track('promotion_event', {
|
||||
from: 'enhanced_promotion_modal',
|
||||
action: 'open',
|
||||
})
|
||||
setHasSeenPromotion(true)
|
||||
// setIsOpen(false)
|
||||
// Open in new tab
|
||||
window.open('https://qwertylearner.ai', '_blank')
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
track('promotion_event', {
|
||||
from: 'enhanced_promotion_modal',
|
||||
action: 'close',
|
||||
})
|
||||
setHasSeenPromotion(true)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={noop}>
|
||||
<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-black bg-opacity-40 backdrop-blur-sm transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<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"
|
||||
>
|
||||
<Dialog.Panel className="dark:via-gray-850 relative transform overflow-hidden rounded-2xl border border-blue-200 bg-gradient-to-br from-white via-blue-50 to-indigo-100 p-0 text-left shadow-2xl transition-all dark:border-gray-700 dark:from-gray-800 dark:to-gray-900 sm:my-8 sm:w-full sm:max-w-xl">
|
||||
{/* Header with close button */}
|
||||
<div className="absolute right-4 top-4 z-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||
title="关闭"
|
||||
>
|
||||
<IconX className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-8 pb-8 pt-10">
|
||||
{/* Icon and title */}
|
||||
<div className="mb-6 text-center">
|
||||
<div className="mx-auto mb-4 flex h-20 w-20 animate-pulse items-center justify-center rounded-full bg-gradient-to-r from-blue-500 to-purple-600 shadow-lg">
|
||||
<IconStar className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
<h3 className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-3xl font-bold text-transparent">
|
||||
体验 QwertyLearner.ai
|
||||
</h3>
|
||||
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">解锁更强大的学习体验 ✨</p>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="space-y-4 py-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="text-center font-medium text-gray-900 dark:text-white">
|
||||
不会编程?想拥有自己的专属学习词典?操作简单,一键上传,点击即用
|
||||
<br />
|
||||
<div className="my-2"></div>
|
||||
那么,推荐您尝试由英国 DeepLearningAI 专业团队开发运营的 QwertyLearner.ai
|
||||
</p>
|
||||
|
||||
<div className="rounded-lg bg-white p-4 shadow-sm dark:bg-gray-800">
|
||||
<h4 className="mb-3 font-semibold text-gray-900 dark:text-white">🚀 专业功能</h4>
|
||||
<ul className="space-y-2.5">
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>AI 智能词库</strong> - 一键上传,智能生成释义和词性,打造专属自定义词库
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>文章练习</strong> - 自定义文章内容,提升实战能力
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>云端同步</strong> - 多设备练习记录、错题库同步
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>词典选择</strong> - 更多丰富的专业词库
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-amber-50 p-3 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||
<p>
|
||||
<strong>说明:</strong>QwertyLearner.ai 由英国 DeepLearningAI 独立开发运营,为开源版 QwertyLearner
|
||||
的独立衍生版本,开源版将持续维持开源与开放运营。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mt-8 space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTryNow}
|
||||
className="w-full transform rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 px-8 py-4 text-lg font-bold text-white shadow-lg transition-all duration-200 hover:scale-105 hover:from-blue-600 hover:to-purple-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
🚀 立即体验 QwertyLearner.ai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnhancedPromotionModal
|
||||
@@ -1,4 +1,5 @@
|
||||
import InfoPanel from '@/components/InfoPanel'
|
||||
import { track } from '@vercel/analytics'
|
||||
import { useCallback, useState } from 'react'
|
||||
import IconBook2 from '~icons/tabler/book-2'
|
||||
|
||||
@@ -7,10 +8,18 @@ export default function DictRequest() {
|
||||
|
||||
const onOpenPanel = useCallback(() => {
|
||||
setShowPanel(true)
|
||||
track('promotion_event', {
|
||||
from: 'dict_request_button',
|
||||
action: 'open',
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onClosePanel = useCallback(() => {
|
||||
setShowPanel(false)
|
||||
track('promotion_event', {
|
||||
from: 'dict_request_panel',
|
||||
action: 'close',
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -18,36 +27,91 @@ export default function DictRequest() {
|
||||
{showPanel && (
|
||||
<InfoPanel
|
||||
openState={showPanel}
|
||||
title="申请词典"
|
||||
title="寻找更多词典?"
|
||||
icon={IconBook2}
|
||||
buttonClassName="bg-indigo-500 hover:bg-indigo-400"
|
||||
iconClassName="text-indigo-500 bg-indigo-100 dark:text-indigo-300 dark:bg-indigo-500"
|
||||
onClose={onClosePanel}
|
||||
>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
如果您有相关的编程基础,可以参考
|
||||
<a
|
||||
href="https://github.com/Kaiyiwing/qwerty-learner/blob/master/docs/toBuildDict.md"
|
||||
className="px-2 text-blue-500"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
导入词典
|
||||
</a>
|
||||
,给项目贡献新的词典。
|
||||
<br />
|
||||
<br />
|
||||
如果您没有相关的编程基础,可以将您的字典需求发送邮件到{' '}
|
||||
<a href="mailto:me@kaiyi.cool" className="px-2 text-blue-500" aria-label="发送邮件到 me@kaiyi.cool">
|
||||
me@kaiyi.cool
|
||||
</a>
|
||||
,或者在网页底部添加我们的用户社群进行反馈。
|
||||
</p>
|
||||
<br />
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-lg bg-white p-4 shadow-sm dark:bg-gray-800">
|
||||
<h4 className="mb-3 font-semibold text-gray-900 dark:text-white">👨💻 具备编程技能?</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
您可以参考我们的
|
||||
<a
|
||||
href="https://github.com/Kaiyiwing/qwerty-learner/blob/master/docs/toBuildDict.md"
|
||||
className="mx-1 font-medium text-blue-500 hover:text-blue-600"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
词典贡献指南
|
||||
</a>
|
||||
,为开源项目贡献新的词典内容。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 p-4 shadow-sm dark:from-gray-800 dark:to-gray-700">
|
||||
<h4 className="mb-3 font-semibold text-gray-900 dark:text-white">🚀 尝试 QwertyLearner.ai</h4>
|
||||
<p className="mb-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
不会编程?想拥有自己的专属学习词典?操作简单,一键上传,点击即用
|
||||
<br />
|
||||
<div className="my-2"></div>
|
||||
那么,推荐您尝试由英国 DeepLearningAI 专业团队开发运营的
|
||||
<span className="mx-1 font-semibold text-blue-600 dark:text-blue-400">QwertyLearner.ai</span>
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>AI 智能词库</strong> - 一键上传,智能生成释义和词性,打造专属自定义词库
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>文章练习</strong> - 自定义文章内容,提升实战能力
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>云端同步</strong> - 多设备练习记录、错题库同步
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>词典选择</strong> - 更多丰富的专业词库
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
window.open('https://qwertylearner.ai', '_blank')
|
||||
onClosePanel()
|
||||
}}
|
||||
className="mt-4 w-full transform rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 px-4 py-2 text-sm font-semibold text-white transition-all duration-200 hover:scale-105 hover:from-blue-600 hover:to-purple-700 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
🚀 体验 QwertyLearner.ai
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-amber-50 p-3 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||
<p>
|
||||
<strong>说明:</strong>QwertyLearner.ai 由英国 DeepLearningAI 独立开发运营,为开源版 QwertyLearner
|
||||
的独立衍生版本,开源版将持续维持开源与开放运营。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</InfoPanel>
|
||||
)}
|
||||
<button className="cursor-pointer pr-6 text-sm text-indigo-500" onClick={onOpenPanel}>
|
||||
没有找到想要的词典?
|
||||
<button
|
||||
onClick={onOpenPanel}
|
||||
className="group flex items-center space-x-2 rounded-lg border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 px-4 py-2.5 text-sm font-medium text-indigo-600 shadow-sm transition-all duration-200 hover:scale-105 hover:border-indigo-300 hover:from-indigo-100 hover:to-blue-100 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:border-indigo-400 dark:from-gray-800 dark:to-gray-700 dark:text-indigo-400 dark:hover:from-gray-700 dark:hover:to-gray-600"
|
||||
>
|
||||
<IconBook2 className="h-4 w-4" />
|
||||
<span>寻找更多词典</span>
|
||||
<span className="transform transition-transform group-hover:translate-x-1">✨</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function GalleryPage() {
|
||||
<IconX className="absolute right-20 top-10 mr-2 h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
|
||||
<div className="mt-20 flex w-full flex-1 flex-col items-center justify-center overflow-y-auto">
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<div className="flex h-20 w-full items-center justify-between pb-6">
|
||||
<div className="flex h-20 w-full items-center justify-between pb-6 pr-20">
|
||||
<LanguageTabSwitcher />
|
||||
<DictRequest />
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useConfetti } from './hooks/useConfetti'
|
||||
import { useWordList } from './hooks/useWordList'
|
||||
import { TypingContext, TypingStateActionType, initialState, typingReducer } from './store'
|
||||
import { DonateCard } from '@/components/DonateCard'
|
||||
import EnhancedPromotionModal from '@/components/EnhancedPromotionModal'
|
||||
import Header from '@/components/Header'
|
||||
import StarCard from '@/components/StarCard'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import { idDictionaryMap } from '@/resources/dictionary'
|
||||
import { currentChapterAtom, currentDictIdAtom, isReviewModeAtom, randomConfigAtom, reviewModeInfoAtom } from '@/store'
|
||||
@@ -129,7 +129,7 @@ const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<TypingContext.Provider value={{ state: state, dispatch }}>
|
||||
<StarCard />
|
||||
<EnhancedPromotionModal />
|
||||
{state.isFinished && <DonateCard />}
|
||||
{state.isFinished && <ResultScreen />}
|
||||
<Layout>
|
||||
|
||||
@@ -110,5 +110,8 @@ export const wordDictationConfigAtom = atomForConfig('wordDictationConfig', {
|
||||
|
||||
export const dismissStartCardDateAtom = atomWithStorage<Date | null>(DISMISS_START_CARD_DATE_KEY, null)
|
||||
|
||||
// Enhanced version promotion popup state
|
||||
export const hasSeenEnhancedPromotionAtom = atomWithStorage('hasSeenEnhancedPromotion', false)
|
||||
|
||||
// for dev test
|
||||
// dismissStartCardDateAtom = atom<Date | null>(new Date())
|
||||
|
||||
Reference in New Issue
Block a user