Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/floatingHeartsContext.ts
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);
24 changes: 24 additions & 0 deletions app/floatingHeartsReducer.ts
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);
}
}
}
6 changes: 0 additions & 6 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(

Copy link
Copy Markdown
Author

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.

to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}

@layer utilities {
Expand Down
16 changes: 12 additions & 4 deletions app/page.tsx
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}>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use React context to avoid props drilling, but we use Redux, Mobs, etc.

<FloatingHeartsDispatchContext.Provider value={dispatch}>
<DonateFlow />
<FloatingHearts />
</FloatingHeartsDispatchContext.Provider>
</FloatingHeartsContext.Provider>
);
}
39 changes: 39 additions & 0 deletions components/FloatingHearts.css
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);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded 50px just for the test task.

}

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;
}
}
65 changes: 63 additions & 2 deletions components/FloatingHearts.tsx
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;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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>
);
}
};
18 changes: 16 additions & 2 deletions components/donate-elements/DonateButton.tsx
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>
);
}
}
4 changes: 2 additions & 2 deletions components/icons/HeartIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix warning about stroke-width

);