feat: add donate card (#677)

This commit is contained in:
Kai 2023-10-15 03:23:23 -05:00 committed by GitHub
parent f726091d05
commit d520444f25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 272 additions and 14 deletions

View File

@ -0,0 +1,71 @@
import { db } from '@/utils/db'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
export function useChapterNumber() {
const [chapterNumber, setChapterNumber] = useState<number>(0)
useEffect(() => {
const fetchChapterNumber = async () => {
const number = await db.chapterRecords.count()
setChapterNumber(number)
}
fetchChapterNumber()
}, [])
return chapterNumber
}
export function useDayFromFirstWordRecord() {
const [dayFromFirstWordRecord, setDayFromFirstWordRecord] = useState<number>(0)
useEffect(() => {
const fetchDayFromFirstWordRecord = async () => {
const firstWordRecord = await db.wordRecords.orderBy('timeStamp').first()
const firstWordRecordTimeStamp = firstWordRecord?.timeStamp || 0
const now = dayjs()
const timestamp = dayjs.unix(firstWordRecordTimeStamp)
const daysPassed = now.diff(timestamp, 'day')
setDayFromFirstWordRecord(daysPassed)
}
fetchDayFromFirstWordRecord()
}, [])
return dayFromFirstWordRecord
}
export function useWordNumber() {
const [wordNumber, setWordNumber] = useState<number>(0)
useEffect(() => {
const fetchWordNumber = async () => {
const number = await db.wordRecords.count()
setWordNumber(number)
}
fetchWordNumber()
}, [])
return wordNumber
}
export function useSumWrongCount() {
const [sumWrongCount, setSumWrongCount] = useState<number>(0)
useEffect(() => {
const fetchSumWrongCount = async () => {
let totalWrongCount = 0
await db.chapterRecords.each((record) => {
totalWrongCount += record.wrongCount || 0
})
setSumWrongCount(totalWrongCount)
}
fetchSumWrongCount()
}, [])
return sumWrongCount
}

View File

@ -0,0 +1,147 @@
import { useChapterNumber, useDayFromFirstWordRecord, useSumWrongCount, useWordNumber } from './hooks/useWordStats'
import alipay from '@/assets/alipay.jpg'
import weChat from '@/assets/weChat.jpg'
import { DONATE_DATE } from '@/constants'
import { reportDonateCard } from '@/utils'
import noop from '@/utils/noop'
import { Dialog, Transition } from '@headlessui/react'
import dayjs from 'dayjs'
import type React from 'react'
import { Fragment, useLayoutEffect, useMemo, useState } from 'react'
import IconParty from '~icons/logos/partytown-icon'
export const DonateCard = () => {
const [show, setShow] = useState(false)
const chapterNumber = useChapterNumber()
const wordNumber = useWordNumber()
const sumWrongCount = useSumWrongCount()
const dayFromFirstWord = useDayFromFirstWordRecord()
const dayFromQwerty = useMemo(() => {
const now = dayjs()
const past = dayjs('2021-01-21')
return now.diff(past, 'day')
}, [])
const HighlightedText = ({ children, className }: { children: React.ReactNode; className?: string }) => {
return <span className={`font-bold ${className ? className : 'text-indigo-500'}`}>{children}</span>
}
const onClickHasDonated = () => {
reportDonateCard({
type: 'donate',
chapterNumber,
wordNumber,
sumWrongCount,
dayFromFirstWord,
dayFromQwerty,
})
setShow(false)
const now = dayjs()
window.localStorage.setItem(DONATE_DATE, now.format())
}
const onClickRemindMeLater = () => {
reportDonateCard({
type: 'dismiss',
chapterNumber,
wordNumber,
sumWrongCount,
dayFromFirstWord,
dayFromQwerty,
})
setShow(false)
}
useLayoutEffect(() => {
if (chapterNumber && chapterNumber !== 0 && chapterNumber % 10 === 0) {
const storedDate = window.localStorage.getItem(DONATE_DATE)
const date = dayjs(storedDate)
const now = dayjs()
const diff = now.diff(date, 'day')
if (!storedDate || diff > 30) {
setShow(true)
}
}
}, [chapterNumber])
return (
<Transition.Root show={show} 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-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<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">
<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="relative my-8 w-[35rem] transform select-text overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all">
<div className="flex w-full flex-col justify-center gap-4 bg-white px-2 pb-4 pt-5 dark:bg-gray-800">
<h1 className="gradient-text w-full pb-2 pt-3 text-center text-[2.4rem] font-bold">{`${chapterNumber} Chapters Achievement !`}</h1>
<div className="flex w-full flex-col gap-4 px-4">
<p className="mx-auto px-4 indent-4">
<HighlightedText> {chapterNumber} </HighlightedText>, Qwerty Learner
<HighlightedText> {dayFromFirstWord} </HighlightedText>
<HighlightedText> {wordNumber} </HighlightedText>
, <HighlightedText> {sumWrongCount} </HighlightedText>
<IconParty className="ml-2 inline-block" fontSize={16} />
<IconParty className="inline-block" fontSize={16} />
<IconParty className="inline-block" fontSize={16} />
<br />
</p>
<p className="mx-auto px-4 indent-4">
Qwerty Learner <span className="font-medium ">广</span>
<HighlightedText className="text-indigo-500"> {dayFromQwerty} </HighlightedText> ,
便
</p>
<p className="mx-auto px-4 indent-4">
Qwerty, Qwerty Qwerty
</p>
</div>
<div className="flex w-full justify-center pt-6">
<img src={alipay} alt="alipay" className="mx-4 w-1/3" />
<img src={weChat} alt="weChat" className="mx-4 w-1/3" />
</div>
<div className="flex w-full justify-between px-14 py-3 ">
<button type="button" className="my-btn-primary w-36 bg-amber-500 font-medium" onClick={onClickHasDonated}>
</button>
<button type="button" className="my-btn-primary w-36 font-medium" onClick={onClickRemindMeLater}>
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

View File

@ -4,6 +4,8 @@ export const CHAPTER_LENGTH = 20
export const DISMISS_START_CARD_DATE_KEY = 'dismissStartCardDate'
export const DONATE_DATE = 'donateDate'
export const CONFETTI_DEFAULTS = {
colors: ['#5D8C7B', '#F2D091', '#F2A679', '#D9695F', '#8C4646'],
shapes: ['square'],

View File

@ -79,11 +79,11 @@ body,
shadow-md dark:bg-gray-800 dark:text-gray-300;
}
.btn-primary {
.my-btn-primary {
@apply flex items-center justify-center rounded-lg bg-indigo-400 px-6 py-1 text-lg text-white hover:opacity-90 focus:outline-none dark:text-opacity-80;
}
.btn-info-panel {
.my-btn-info-panel {
@apply mt-3 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm
font-semibold text-white shadow-sm transition-colors focus:outline-none
dark:bg-opacity-70 dark:text-opacity-80 sm:ml-3 sm:mt-0 sm:w-auto
@ -145,3 +145,20 @@ body,
@apply block h-4 w-4 cursor-pointer rounded-full border border-gray-200 bg-white drop-shadow-md;
}
}
.gradient-text {
background-image: linear-gradient(90deg, #f66, #f90);
background-clip: text;
color: transparent;
animation: gradient-text-hue 5s linear infinite;
}
@keyframes gradient-text-hue {
from {
filter: hue-rotate(0);
}
to {
filter: hue-rotate(-1turn);
}
}

View File

@ -247,7 +247,7 @@ const ResultScreen = () => {
<div className="mt-10 flex w-full justify-center gap-5 px-5 text-xl">
<Tooltip content="快捷键shift + enter">
<button
className="btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
className="my-btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
type="button"
onClick={dictationButtonHandler}
title="默写本章节"
@ -257,7 +257,7 @@ const ResultScreen = () => {
</Tooltip>
<Tooltip content="快捷键space">
<button
className="btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
className="my-btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
type="button"
onClick={repeatButtonHandler}
title="重复本章节"
@ -268,7 +268,7 @@ const ResultScreen = () => {
{!isLastChapter && (
<Tooltip content="快捷键enter">
<button
className={`{ isLastChapter ? 'cursor-not-allowed opacity-50' : ''} btn-primary h-12 text-base font-bold `}
className={`{ isLastChapter ? 'cursor-not-allowed opacity-50' : ''} my-btn-primary h-12 text-base font-bold `}
type="button"
onClick={nextButtonHandler}
title="下一章节"

View File

@ -81,7 +81,7 @@ export default function DataSetting() {
</div>
<button
className="btn-primary ml-4 disabled:bg-gray-300"
className="my-btn-primary ml-4 disabled:bg-gray-300"
type="button"
onClick={onClickExport}
disabled={isExporting}
@ -110,7 +110,7 @@ export default function DataSetting() {
</div>
<button
className="btn-primary ml-4 disabled:bg-gray-300"
className="my-btn-primary ml-4 disabled:bg-gray-300"
type="button"
onClick={onClickImport}
disabled={isImporting}

View File

@ -79,7 +79,7 @@ export default function ViewSetting() {
</div>
</div>
</div>
<button className="btn-primary ml-4 disabled:bg-gray-300" type="button" onClick={onResetFontSize} title="重置字体设置">
<button className="my-btn-primary ml-4 disabled:bg-gray-300" type="button" onClick={onResetFontSize} title="重置字体设置">
</button>
</div>

View File

@ -145,7 +145,7 @@ export default function SharePicDialog({ showState, setShowState, randomChoose }
</div>
<button
ref={dialogFocusRef}
className="btn-primary mr-9 mt-10 h-10"
className="my-btn-primary mr-9 mt-10 h-10"
type="button"
onClick={handleDownload}
title="保存"

View File

@ -45,7 +45,9 @@ export default function StartButton({ isLoading }: { isLoading: boolean }) {
} flex-column absolute left-0 top-0 w-20 rounded-lg shadow-lg transition-colors duration-200`}
>
<button
className={`${state.isTyping ? 'bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-500' : 'bg-indigo-500'} btn-primary w-20 shadow`}
className={`${
state.isTyping ? 'bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-500' : 'bg-indigo-500'
} my-btn-primary w-20 shadow`}
type="button"
onClick={onToggleIsTyping}
aria-label={state.isTyping ? '暂停' : '开始'}
@ -57,7 +59,7 @@ export default function StartButton({ isLoading }: { isLoading: boolean }) {
<button
className={`${
state.isTyping ? 'bg-gray-500 dark:bg-gray-700 dark:hover:bg-gray-500 ' : 'bg-indigo-400 '
} btn-primary mb-1 mt-1 w-18 transition-colors duration-200`}
} my-btn-primary mb-1 mt-1 w-18 transition-colors duration-200`}
type="button"
onClick={onClickRestart}
aria-label={'重新开始'}

View File

@ -9,6 +9,7 @@ import WordPanel from './components/WordPanel'
import { useConfetti } from './hooks/useConfetti'
import { useWordList } from './hooks/useWordList'
import { TypingContext, TypingStateActionType, initialState, typingReducer } from './store'
import { DonateCard } from '@/components/DonateCard'
import Header from '@/components/Header'
import StarCard from '@/components/StarCard'
import Tooltip from '@/components/Tooltip'
@ -124,6 +125,7 @@ const App: React.FC = () => {
return (
<TypingContext.Provider value={{ state: state, dispatch }}>
<StarCard />
{state.isFinished && <DonateCard />}
{state.isFinished && <ResultScreen />}
<Layout>
<Header>
@ -142,7 +144,7 @@ const App: React.FC = () => {
<button
className={`${
state.isShowSkip ? 'bg-orange-400' : 'invisible w-0 bg-gray-300 px-0 opacity-0'
} btn-primary transition-all duration-300 `}
} my-btn-primary transition-all duration-300 `}
onClick={skipWord}
>
Skip

View File

@ -55,6 +55,23 @@ export function recordErrorBookAction(type: errorBookType) {
mixpanel.track('error-book', props)
}
export type donateCardInfo = {
type: 'donate' | 'dismiss'
chapterNumber: number
wordNumber: number
sumWrongCount: number
dayFromFirstWord: number
dayFromQwerty: number
}
export function reportDonateCard(info: donateCardInfo) {
const props = {
...info,
}
mixpanel.track('donate-card', props)
}
/**
* mixpanel
*/

View File

@ -10,7 +10,7 @@ import { defineConfig } from 'vite'
import type { PluginOption } from 'vite'
// https://vitejs.dev/config/
export default defineConfig(async () => {
export default defineConfig(async ({ mode }) => {
const latestCommitHash = await new Promise<string>((resolve) => {
return getLastCommit((err, commit) => (err ? 'unknown' : resolve(commit.shortHash)))
})
@ -34,7 +34,7 @@ export default defineConfig(async () => {
sourcemap: false,
},
esbuild: {
drop: ['console', 'debugger'],
drop: mode === 'development' ? [] : ['console', 'debugger'],
},
define: {
REACT_APP_DEPLOY_ENV: JSON.stringify(process.env.REACT_APP_DEPLOY_ENV),