diff --git a/package.json b/package.json index 0f232b22..859ed55c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@radix-ui/react-radio-group": "^1.1.2", "@radix-ui/react-scroll-area": "^1.0.3", "@radix-ui/react-slider": "^1.1.1", + "canvas-confetti": "^1.6.0", "classnames": "^2.3.2", "dexie": "^3.2.3", "dexie-export-import": "^4.0.7", @@ -67,6 +68,7 @@ "@tailwindcss/forms": "^0.5.3", "@tailwindcss/postcss7-compat": "^2.2.17", "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/canvas-confetti": "^1.6.0", "@types/file-saver": "^2.0.5", "@types/howler": "^2.2.3", "@types/mixpanel-browser": "^2.38.1", diff --git a/src/constants/index.ts b/src/constants/index.ts index 46409f08..5341ff28 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -3,3 +3,9 @@ export const EXPLICIT_SPACE = '␣' export const CHAPTER_LENGTH = 20 export const DISMISS_START_CARD_DATE_KEY = 'dismissStartCardDate' + +export const CONFETTI_DEFAULTS = { + colors: ['#5D8C7B', '#F2D091', '#F2A679', '#D9695F', '#8C4646'], + shapes: ['square'], + ticks: 500, +} as confetti.Options diff --git a/src/pages/Typing/hooks/useConfetti.ts b/src/pages/Typing/hooks/useConfetti.ts new file mode 100644 index 00000000..1d09daa9 --- /dev/null +++ b/src/pages/Typing/hooks/useConfetti.ts @@ -0,0 +1,34 @@ +import { CONFETTI_DEFAULTS } from '@/constants' +import confetti from 'canvas-confetti' +import { useEffect } from 'react' + +export function useConfetti(state: boolean) { + useEffect(() => { + let leftConfettiTimer: number | undefined + let rightConfettiTimer: number | undefined + if (state) { + leftConfettiTimer = window.setTimeout(() => { + confetti({ + ...CONFETTI_DEFAULTS, + particleCount: 50, + angle: 60, + spread: 100, + origin: { x: 0 }, + }) + }, 250) + rightConfettiTimer = window.setTimeout(() => { + confetti({ + ...CONFETTI_DEFAULTS, + particleCount: 50, + angle: 120, + spread: 100, + origin: { x: 1 }, + }) + }, 400) + } + return () => { + window.clearTimeout(leftConfettiTimer) + window.clearTimeout(rightConfettiTimer) + } + }, [state]) +} diff --git a/src/pages/Typing/index.tsx b/src/pages/Typing/index.tsx index fc3c1579..9391205c 100644 --- a/src/pages/Typing/index.tsx +++ b/src/pages/Typing/index.tsx @@ -6,6 +6,7 @@ import StartButton from './components/StartButton' import Switcher from './components/Switcher' import WordList from './components/WordList' import WordPanel from './components/WordPanel' +import { useConfetti } from './hooks/useConfetti' import { useWordList } from './hooks/useWordList' import { TypingContext, TypingStateActionType, initialState, typingReducer } from './store' import Header from '@/components/Header' @@ -118,6 +119,8 @@ const App: React.FC = () => { return () => clearInterval(intervalId) }, [state.isTyping, dispatch]) + useConfetti(state.isFinished) + return ( diff --git a/yarn.lock b/yarn.lock index 02c176ca..98a3c311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,6 +1751,11 @@ javascript-natural-sort "0.7.1" lodash "^4.17.21" +"@types/canvas-confetti@^1.6.0": + version "1.6.0" + resolved "https://registry.npmmirror.com/@types/canvas-confetti/-/canvas-confetti-1.6.0.tgz#c525f22f1042e73dec6a7e86c1f5bb0191c09fea" + integrity sha512-Yq6rIccwcco0TLD5SMUrIM7Fk7Fe/C0jmNRxJJCLtAF6gebDkPuUjK5EHedxecm69Pi/aA+It39Ux4OHmFhjRw== + "@types/file-saver@^2.0.5": version "2.0.5" resolved "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7" @@ -2337,6 +2342,11 @@ caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.300014 resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001489.tgz#ca82ee2d4e4dbf2bd2589c9360d3fcc2c7ba3bd8" integrity sha512-x1mgZEXK8jHIfAxm+xgdpHpk50IN3z3q3zP261/WS+uvePxW8izXuCu6AHz0lkuYTlATDehiZ/tNyYBdSQsOUQ== +canvas-confetti@^1.6.0: + version "1.6.0" + resolved "https://registry.npmmirror.com/canvas-confetti/-/canvas-confetti-1.6.0.tgz#193f71aa8f38fc850a5ba94f59091a7afdb43ead" + integrity sha512-ej+w/m8Jzpv9Z7W7uJZer14Ke8P2ogsjg4ZMGIuq4iqUOqY2Jq8BNW42iGmNfRwREaaEfFIczLuZZiEVSYNHAA== + chalk@5.2.0: version "5.2.0" resolved "https://registry.npmmirror.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3"