-
Notifications
You must be signed in to change notification settings - Fork 1
Add floating heart icons with animation #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { createContext, Dispatch } from 'react'; | ||
| import { Actions, State } from './floatingHeartsReducer'; | ||
|
|
||
| export const FloatingHeartsContext = createContext<State | null>(null); | ||
| export const FloatingHeartsDispatchContext = createContext<Dispatch<Actions> | null>(null); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <> | ||
| <DonateFlow /> | ||
| <FloatingHearts /> | ||
| </> | ||
| <FloatingHeartsContext.Provider value={state}> | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| <FloatingHeartsDispatchContext.Provider value={dispatch}> | ||
| <DonateFlow /> | ||
| <FloatingHearts /> | ||
| </FloatingHeartsDispatchContext.Provider> | ||
| </FloatingHeartsContext.Provider> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded |
||
| } | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably, the number of icons should depends on width. |
||
| 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<HTMLDivElement | null>(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( | ||
| <span | ||
| style={{ | ||
| left: x, | ||
| bottom: y, | ||
| animationDelay: `${delay}s`, | ||
| animationDuration: `${duration}s`, | ||
| }} | ||
| className="icon" | ||
| key={`${size}-${x}-${i}`} | ||
| > | ||
| <HeartIcon size={String(size)} color="#f67e7e" /> | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| return _icons; | ||
| }, [boundingWidth]); | ||
|
|
||
| useLayoutEffect(() => { | ||
| if (ref.current) { | ||
| const { width } = ref.current.getBoundingClientRect(); | ||
| setBoundingRect(width); | ||
| } | ||
| }, []); | ||
|
|
||
| return ( | ||
| <HeartIcon /> | ||
| <div className="floating-hearts" ref={ref}> | ||
| {isFloatingHeartsShow && icons} | ||
| </div> | ||
| ); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <button>Donate</button> | ||
| <button onMouseEnter={handleHover} onMouseLeave={handleLeave}>Donate</button> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,5 +7,5 @@ type IconSvgProps = SVGProps<SVGSVGElement> & { | |
| }; | ||
|
|
||
| export const HeartIcon: React.FC<IconSvgProps> = ({ size = '128', className, color = '#000000' }) => ( | ||
| <svg stroke={color} fill={color} stroke-width="0" viewBox="0 0 512 512" height={size} width={size} xmlns="http://www.w3.org/2000/svg" className={className}><path d="M256 448a32 32 0 0 1-18-5.57c-78.59-53.35-112.62-89.93-131.39-112.8-40-48.75-59.15-98.8-58.61-153C48.63 114.52 98.46 64 159.08 64c44.08 0 74.61 24.83 92.39 45.51a6 6 0 0 0 9.06 0C278.31 88.81 308.84 64 352.92 64c60.62 0 110.45 50.52 111.08 112.64.54 54.21-18.63 104.26-58.61 153-18.77 22.87-52.8 59.45-131.39 112.8a32 32 0 0 1-18 5.56z"></path></svg> | ||
| ); | ||
| <svg stroke={color} fill={color} strokeWidth="0" viewBox="0 0 512 512" height={size} width={size} xmlns="http://www.w3.org/2000/svg" className={className}><path d="M256 448a32 32 0 0 1-18-5.57c-78.59-53.35-112.62-89.93-131.39-112.8-40-48.75-59.15-98.8-58.61-153C48.63 114.52 98.46 64 159.08 64c44.08 0 74.61 24.83 92.39 45.51a6 6 0 0 0 9.06 0C278.31 88.81 308.84 64 352.92 64c60.62 0 110.45 50.52 111.08 112.64.54 54.21-18.63 104.26-58.61 153-18.77 22.87-52.8 59.45-131.39 112.8a32 32 0 0 1-18 5.56z"></path></svg> | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix warning about |
||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks weird, so I removed it.