diff --git a/app/floatingHeartsContext.ts b/app/floatingHeartsContext.ts new file mode 100644 index 0000000..6cc3449 --- /dev/null +++ b/app/floatingHeartsContext.ts @@ -0,0 +1,5 @@ +import { createContext, Dispatch } from 'react'; +import { Actions, State } from './floatingHeartsReducer'; + +export const FloatingHeartsContext = createContext(null); +export const FloatingHeartsDispatchContext = createContext | null>(null); diff --git a/app/floatingHeartsReducer.ts b/app/floatingHeartsReducer.ts new file mode 100644 index 0000000..38d199b --- /dev/null +++ b/app/floatingHeartsReducer.ts @@ -0,0 +1,24 @@ +export interface State { + isFloatingHeartsShow: boolean; +} + +export enum ActionKind { + SHOW = 'SHOW', + HIDE = 'HIDE', +} + +export interface Actions { + type: ActionKind; +} + +export function floatingHeartsReducer(state: State, action: Actions) { + switch (action.type) { + case ActionKind.SHOW: + return { ...state, isFloatingHeartsShow: true }; + case ActionKind.HIDE: + return { ...state, isFloatingHeartsShow: false }; + default: { + throw Error("Unknown action: " + action.type); + } + } +} diff --git a/app/globals.css b/app/globals.css index 875c01e..5188d6b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -18,12 +18,6 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); } @layer utilities { diff --git a/app/page.tsx b/app/page.tsx index e298a48..f81db24 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,11 +1,19 @@ +'use client'; import { DonateFlow } from "@/components/DonateFlow"; import { FloatingHearts } from "@/components/FloatingHearts"; +import { FloatingHeartsContext, FloatingHeartsDispatchContext } from "./floatingHeartsContext"; +import { floatingHeartsReducer } from "./floatingHeartsReducer"; +import { useReducer } from "react"; export default function Home() { + const [state, dispatch] = useReducer(floatingHeartsReducer, { isFloatingHeartsShow: false }) + return ( - <> - - - + + + + + + ); } diff --git a/components/FloatingHearts.css b/components/FloatingHearts.css new file mode 100644 index 0000000..8b95998 --- /dev/null +++ b/components/FloatingHearts.css @@ -0,0 +1,39 @@ +.floating-hearts { + height: 250px; + width: 100%; + overflow: hidden; + position: relative; +} + +.floating-hearts .icon { + position: absolute; + animation: animate 5s linear infinite; +} + +@keyframes animate { + 0% { + transform: translateY(0) rotate(-30deg); + opacity: 1; + } + + 20% { + transform: translateY(-50px) rotate(30deg); + } + + 40% { + transform: translateY(-100px) rotate(-30deg); + } + + 60% { + transform: translateY(-150px) rotate(30deg); + } + + 80% { + transform: translateY(-200px) rotate(-30deg); + } + + 100% { + transform: translateY(-250px) rotate(30deg); + opacity: 0; + } +} diff --git a/components/FloatingHearts.tsx b/components/FloatingHearts.tsx index 70f0db7..43a2467 100644 --- a/components/FloatingHearts.tsx +++ b/components/FloatingHearts.tsx @@ -1,7 +1,68 @@ +"use client"; +import { useMemo, useState, useRef, useLayoutEffect, useContext } from "react"; import { HeartIcon } from "./icons/HeartIcon"; +import "./FloatingHearts.css"; +import { FloatingHeartsContext } from "@/app/floatingHeartsContext"; + +const MIN_ICON_SIZE = 14; +const MAX_ICON_SIZE = 48; +const ICONS_NUMBER = 35; +const ICON_MARGIN = 20; +const MAX_DELAY = 5; +const MIN_DURATION = 5; +const MAX_DURATION = 15; + +function getRandomNumber(min: number, max: number) { + const minCeiled = Math.ceil(min); + const maxFloored = Math.floor(max); + + return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); +} export const FloatingHearts = () => { + const { isFloatingHeartsShow } = useContext(FloatingHeartsContext) || {}; + const ref = useRef(null); + const [boundingWidth, setBoundingRect] = useState(0); + + const icons = useMemo(() => { + const _icons = []; + + for (let i = 0; i < ICONS_NUMBER; i++) { + const size = getRandomNumber(MIN_ICON_SIZE, MAX_ICON_SIZE); + const x = getRandomNumber(0, boundingWidth); + const y = -(MAX_ICON_SIZE + ICON_MARGIN); + const delay = getRandomNumber(0, MAX_DELAY); + const duration = getRandomNumber(MIN_DURATION, MAX_DURATION); + + _icons.push( + + + + ); + } + + return _icons; + }, [boundingWidth]); + + useLayoutEffect(() => { + if (ref.current) { + const { width } = ref.current.getBoundingClientRect(); + setBoundingRect(width); + } + }, []); + return ( - +
+ {isFloatingHeartsShow && icons} +
); -} \ No newline at end of file +}; diff --git a/components/donate-elements/DonateButton.tsx b/components/donate-elements/DonateButton.tsx index 331e2a5..f295a56 100644 --- a/components/donate-elements/DonateButton.tsx +++ b/components/donate-elements/DonateButton.tsx @@ -1,5 +1,19 @@ +import { useContext } from "react"; +import { FloatingHeartsDispatchContext } from "@/app/floatingHeartsContext"; +import { ActionKind } from '@/app/floatingHeartsReducer' + export const DonateButton = () => { + const dispatch = useContext(FloatingHeartsDispatchContext); + + const handleHover = () => { + dispatch!({type: ActionKind.SHOW}); + } + + const handleLeave = () => { + dispatch!({type: ActionKind.HIDE}); + } + return ( - + ); -} \ No newline at end of file +} diff --git a/components/icons/HeartIcon.tsx b/components/icons/HeartIcon.tsx index b1c016c..92bcc1d 100644 --- a/components/icons/HeartIcon.tsx +++ b/components/icons/HeartIcon.tsx @@ -7,5 +7,5 @@ type IconSvgProps = SVGProps & { }; export const HeartIcon: React.FC = ({ size = '128', className, color = '#000000' }) => ( - -); \ No newline at end of file + +);