From f6b55ecc0acf374f54d3f4bcbe360d85fed984fb Mon Sep 17 00:00:00 2001 From: Dalinar Date: Sat, 6 Apr 2024 12:14:58 +0300 Subject: [PATCH 1/2] Add floating heart icons with animation --- app/floatingHeartsContext.ts | 5 ++ app/floatingHeartsReducer.ts | 24 ++++++++ app/globals.css | 6 -- app/page.tsx | 16 +++-- components/FloatingHearts.css | 39 +++++++++++++ components/FloatingHearts.tsx | 65 ++++++++++++++++++++- components/donate-elements/DonateButton.tsx | 16 ++++- components/icons/HeartIcon.tsx | 2 +- 8 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 app/floatingHeartsContext.ts create mode 100644 app/floatingHeartsReducer.ts create mode 100644 components/FloatingHearts.css diff --git a/app/floatingHeartsContext.ts b/app/floatingHeartsContext.ts new file mode 100644 index 0000000..82144fe --- /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); \ No newline at end of file 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..43b9941 --- /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; + } +} \ No newline at end of file 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..52f384f 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..e421ab1 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 From dd41d84212ba936eebca745e0d0a2216081cb04b Mon Sep 17 00:00:00 2001 From: Roman Dyachenko Date: Sat, 6 Apr 2024 12:23:00 +0300 Subject: [PATCH 2/2] fix --- app/floatingHeartsContext.ts | 2 +- components/FloatingHearts.css | 2 +- components/donate-elements/DonateButton.tsx | 2 +- components/icons/HeartIcon.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/floatingHeartsContext.ts b/app/floatingHeartsContext.ts index 82144fe..6cc3449 100644 --- a/app/floatingHeartsContext.ts +++ b/app/floatingHeartsContext.ts @@ -2,4 +2,4 @@ import { createContext, Dispatch } from 'react'; import { Actions, State } from './floatingHeartsReducer'; export const FloatingHeartsContext = createContext(null); -export const FloatingHeartsDispatchContext = createContext | null>(null); \ No newline at end of file +export const FloatingHeartsDispatchContext = createContext | null>(null); diff --git a/components/FloatingHearts.css b/components/FloatingHearts.css index 43b9941..8b95998 100644 --- a/components/FloatingHearts.css +++ b/components/FloatingHearts.css @@ -36,4 +36,4 @@ transform: translateY(-250px) rotate(30deg); opacity: 0; } -} \ No newline at end of file +} diff --git a/components/donate-elements/DonateButton.tsx b/components/donate-elements/DonateButton.tsx index 52f384f..f295a56 100644 --- a/components/donate-elements/DonateButton.tsx +++ b/components/donate-elements/DonateButton.tsx @@ -16,4 +16,4 @@ export const DonateButton = () => { return ( ); -} \ No newline at end of file +} diff --git a/components/icons/HeartIcon.tsx b/components/icons/HeartIcon.tsx index e421ab1..92bcc1d 100644 --- a/components/icons/HeartIcon.tsx +++ b/components/icons/HeartIcon.tsx @@ -8,4 +8,4 @@ type IconSvgProps = SVGProps & { export const HeartIcon: React.FC = ({ size = '128', className, color = '#000000' }) => ( -); \ No newline at end of file +);