Skip to content

Commit

Permalink
Merge branch 'main' into sandino/translation-in-form
Browse files Browse the repository at this point in the history
  • Loading branch information
ssandino authored Jan 25, 2025
2 parents 26568b2 + 554a150 commit bbb9d95
Show file tree
Hide file tree
Showing 17 changed files with 499 additions and 51 deletions.
3 changes: 3 additions & 0 deletions shared/locales/de/website-newsletter.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"toast-failure": "Oops! Etwas ist schief gelaufen. Bitte versuche es nochmals, oder kontaktiere uns auf [email protected]",
"email-placeholder": "Deine E-Mail Adresse",
"button-subscribe": "Newsletter abonnieren"
},
"campaign": {
"information-label": "Erhalte Updates von diesem Fundraiser und von Social Income."
}
}
3 changes: 3 additions & 0 deletions shared/locales/en/website-newsletter.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"toast-failure": "Something went wrong. Try again or contact us at [email protected]",
"email-placeholder": "Your email",
"button-subscribe": "Subscribe now"
},
"campaign": {
"information-label": "Get updates from the fundraiser and Social Income."
}
}
3 changes: 3 additions & 0 deletions shared/locales/fr/website-newsletter.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"toast-failure": "Oups! Ça n’a pas marché. Essaie encore une fois ou prend contact avec nous ([email protected])",
"email-placeholder": "Ton adresse E-Mail",
"button-subscribe": "S'abonner"
},
"campaign": {
"information-label": "Reçois des mises à jour du collecteur de fonds et de Social Income."
}
}
3 changes: 3 additions & 0 deletions shared/locales/it/website-newsletter.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"toast-failure": "Qualcosa è andato storto. Prova di nuovo o contattaci a [email protected]",
"email-placeholder": "La tua email",
"button-subscribe": "Iscriviti ora"
},
"campaign": {
"information-label": "Ricevi aggiornamenti dal fundraiser e da Social Income."
}
}
9 changes: 6 additions & 3 deletions ui/src/components/containers/base-container.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import { twMerge } from 'tailwind-merge';

type BaseContainerProps = {
export type BaseContainerProps = {
wrapperClassName?: string;
wrapperRef?: React.Ref<HTMLDivElement>;
baseClassNames?: string;
} & React.HTMLAttributes<HTMLDivElement>;

export const BaseContainer = React.forwardRef<HTMLDivElement, BaseContainerProps>(
({ children, className, baseClassNames, ...props }, ref) => {
({ children, className, baseClassNames, wrapperClassName, wrapperRef, ...props }, ref) => {
return (
<div className={baseClassNames}>
<div className={twMerge(wrapperClassName, baseClassNames)} ref={wrapperRef}>
<div className="mx-auto max-w-6xl px-3 md:px-6">
<div className={className} ref={ref} {...props}>
{children}
Expand Down
23 changes: 23 additions & 0 deletions ui/src/components/containers/glow-hover-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import React from 'react';
import { twMerge } from 'tailwind-merge';
import { useGlowHover } from '../use-glow-hover';
import { BaseContainer } from './base-container';

import { BaseContainerProps } from './base-container';

export const GlowHoverContainer = React.forwardRef<HTMLDivElement, Omit<BaseContainerProps, 'wrapperRef'>>(
({ className, ...props }, ref) => {
const refCard = useGlowHover({ lightColor: '#CEFF00' });

return (
<BaseContainer
wrapperClassName={twMerge('theme-blue', className)}
ref={ref}
{...props}
wrapperRef={refCard as React.Ref<HTMLDivElement>}
/>
);
},
);
1 change: 1 addition & 0 deletions ui/src/components/containers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './base-container';
export * from './glow-hover-container';
266 changes: 266 additions & 0 deletions ui/src/components/use-glow-hover/glow-hover-effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
'use client';

import { linearAnimation } from './linear-animation';
import { presets } from './presets';

export type GlowHoverOptions = {
hoverBg?: string;
lightSize?: number;
lightSizeEnterAnimationTime?: number;
lightSizeLeaveAnimationTime?: number;
isElementMovable?: boolean;
customStaticBg?: string;
enableBurst?: boolean;
} & (
| {
preset: keyof typeof presets;
lightColor?: string;
}
| {
preset?: undefined;
lightColor: string;
}
);

type Coords = {
x: number;
y: number;
};

const BURST_TIME = 300;

export function parseColor(colorToParse: string) {
const div = document.createElement('div');
div.style.color = colorToParse;
div.style.position = 'absolute';
div.style.display = 'none';
document.body.appendChild(div);
const colorFromEl = getComputedStyle(div).color;
document.body.removeChild(div);
const parsedColor = colorFromEl.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
if (parsedColor) {
const alpha = typeof parsedColor[4] === 'undefined' ? 1 : parsedColor[4];
return [parsedColor[1], parsedColor[2], parsedColor[3], alpha];
} else {
console.error(`Color ${colorToParse} could not be parsed.`);
return [0, 0, 0, 0];
}
}

export const glowHoverEffect = (el: HTMLElement, { preset, ...options }: GlowHoverOptions) => {
if (!el) {
return () => {};
}

const lightColor = options.lightColor ?? '#CEFF00';
const lightSize = options.lightSize ?? 130;
const lightSizeEnterAnimationTime = options.lightSizeEnterAnimationTime ?? 100;
const lightSizeLeaveAnimationTime = options.lightSizeLeaveAnimationTime ?? 50;
const isElementMovable = options.isElementMovable ?? false;
const customStaticBg = options.customStaticBg ?? null;

const enableBurst = options.enableBurst ?? false;

const getResolvedHoverBg = () => getComputedStyle(el).backgroundColor;

let resolvedHoverBg = getResolvedHoverBg();

// default bg (if not defined) is rgba(0, 0, 0, 0) which is bugged in gradients in Safari
// so we use transparent lightColor instead
const parsedLightColor = parseColor(lightColor);
const parsedLightColorRGBString = parsedLightColor.slice(0, 3).join(',');
const resolvedGradientBg = `rgba(${parsedLightColorRGBString},0)`;

let isMouseInside = false;
let currentLightSize = 0;
let blownSize = 0;
let lightSizeEnterAnimationId: number | undefined = undefined;
let lightSizeLeaveAnimationId: number | undefined = undefined;
let blownSizeIncreaseAnimationId: number | undefined = undefined;
let blownSizeDecreaseAnimationId: number | undefined = undefined;
let lastMousePos: Coords;
const defaultBox = el.getBoundingClientRect();
let lastElPos: Coords = { x: defaultBox.left, y: defaultBox.top };

const updateGlowEffect = () => {
if (!lastMousePos) {
return;
}
const gradientXPos = lastMousePos.x - lastElPos.x;
const gradientYPos = lastMousePos.y - lastElPos.y;
// we do not use transparent color here because of dirty gradient in Safari (more info: https://stackoverflow.com/questions/38391457/linear-gradient-to-transparent-bug-in-latest-safari)
const gradient = `radial-gradient(circle at ${gradientXPos}px ${gradientYPos}px, ${lightColor} 0%, ${resolvedGradientBg} calc(${
blownSize * 2.5
}% + ${currentLightSize}px)) no-repeat`;

// we duplicate resolvedHoverBg layer here because of transition "blinking" without it
el.style.background = `${gradient} border-box border-box ${resolvedHoverBg}`;
};

const updateEffectWithPosition = () => {
if (isMouseInside) {
const curBox = el.getBoundingClientRect();
lastElPos = { x: curBox.left, y: curBox.top };
updateGlowEffect();
}
};

const onMouseEnter = (e: MouseEvent) => {
resolvedHoverBg = getResolvedHoverBg();
lastMousePos = { x: e.clientX, y: e.clientY };
const curBox = el.getBoundingClientRect();
lastElPos = { x: curBox.left, y: curBox.top };
isMouseInside = true;
if (lightSizeEnterAnimationId !== undefined) {
window.cancelAnimationFrame(lightSizeEnterAnimationId);
}
if (lightSizeLeaveAnimationId !== undefined) {
window.cancelAnimationFrame(lightSizeLeaveAnimationId);
}

// animate currentLightSize from 0 to lightSize
linearAnimation({
onProgress: (progress) => {
currentLightSize = lightSize * progress;
updateGlowEffect();
},
time: lightSizeEnterAnimationTime,
initialProgress: currentLightSize / lightSize,
onIdUpdate: (newId) => (lightSizeEnterAnimationId = newId),
});
};

const onMouseMove = (e: MouseEvent) => {
lastMousePos = { x: e.clientX, y: e.clientY };
if (isElementMovable) {
updateEffectWithPosition();
} else {
updateGlowEffect();
}
};

const onMouseLeave = () => {
isMouseInside = false;
if (lightSizeEnterAnimationId !== undefined) {
window.cancelAnimationFrame(lightSizeEnterAnimationId);
}
if (lightSizeLeaveAnimationId !== undefined) {
window.cancelAnimationFrame(lightSizeLeaveAnimationId);
}
if (blownSizeIncreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeIncreaseAnimationId);
}
if (blownSizeDecreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeDecreaseAnimationId);
}

// animate currentLightSize from lightSize to 0
linearAnimation({
onProgress: (progress) => {
currentLightSize = lightSize * (1 - progress);
blownSize = Math.min(blownSize, (1 - progress) * 100);

if (progress < 1) {
updateGlowEffect();
} else {
el.style.background = customStaticBg ? customStaticBg : '';
}
},
time: lightSizeLeaveAnimationTime,
initialProgress: 1 - currentLightSize / lightSize,
onIdUpdate: (newId) => (lightSizeLeaveAnimationId = newId),
});
};

const onMouseDown = (e: MouseEvent) => {
lastMousePos = { x: e.clientX, y: e.clientY };
const curBox = el.getBoundingClientRect();
lastElPos = { x: curBox.left, y: curBox.top };
if (blownSizeIncreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeIncreaseAnimationId);
}
if (blownSizeDecreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeDecreaseAnimationId);
}

// animate blownSize from 0 to 100
linearAnimation({
onProgress: (progress) => {
blownSize = 100 * progress;
updateGlowEffect();
},
time: BURST_TIME,
initialProgress: blownSize / 100,
onIdUpdate: (newId) => (blownSizeIncreaseAnimationId = newId),
});
};

const onMouseUp = (e: MouseEvent) => {
lastMousePos = { x: e.clientX, y: e.clientY };
const curBox = el.getBoundingClientRect();
lastElPos = { x: curBox.left, y: curBox.top };
if (blownSizeIncreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeIncreaseAnimationId);
}
if (blownSizeDecreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeDecreaseAnimationId);
}

// animate blownSize from 100 to 0
linearAnimation({
onProgress: (progress) => {
blownSize = (1 - progress) * 100;
updateGlowEffect();
},
time: BURST_TIME,
initialProgress: 1 - blownSize / 100,
onIdUpdate: (newId) => (blownSizeDecreaseAnimationId = newId),
});
};

document.addEventListener('scroll', updateEffectWithPosition);
window.addEventListener('resize', updateEffectWithPosition);
el.addEventListener('mouseenter', onMouseEnter);
el.addEventListener('mousemove', onMouseMove);
el.addEventListener('mouseleave', onMouseLeave);
if (enableBurst) {
el.addEventListener('mousedown', onMouseDown);
el.addEventListener('mouseup', onMouseUp);
}

let resizeObserver: ResizeObserver;
if (window.ResizeObserver) {
resizeObserver = new ResizeObserver(updateEffectWithPosition);
resizeObserver.observe(el);
}

return () => {
if (lightSizeEnterAnimationId !== undefined) {
window.cancelAnimationFrame(lightSizeEnterAnimationId);
}
if (lightSizeLeaveAnimationId !== undefined) {
window.cancelAnimationFrame(lightSizeLeaveAnimationId);
}
if (blownSizeIncreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeIncreaseAnimationId);
}
if (blownSizeDecreaseAnimationId !== undefined) {
window.cancelAnimationFrame(blownSizeDecreaseAnimationId);
}

document.removeEventListener('scroll', updateEffectWithPosition);
window.removeEventListener('resize', updateEffectWithPosition);
el.removeEventListener('mouseenter', onMouseEnter);
el.removeEventListener('mousemove', onMouseMove);
el.removeEventListener('mouseleave', onMouseLeave);
if (enableBurst) {
el.removeEventListener('mousedown', onMouseDown);
el.removeEventListener('mouseup', onMouseUp);
}

if (resizeObserver) {
resizeObserver.unobserve(el);
resizeObserver.disconnect();
}
};
};
4 changes: 4 additions & 0 deletions ui/src/components/use-glow-hover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { glowHoverEffect } from './glow-hover-effect';
export type { GlowHoverOptions } from './glow-hover-effect';
export { useGlowHover } from './use-glow-hover';
export type { GlowHoverHookOptions } from './use-glow-hover';
38 changes: 38 additions & 0 deletions ui/src/components/use-glow-hover/linear-animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

interface LinearAnimationParams {
onProgress: (progress: number) => void;
onIdUpdate?: (id: number | undefined) => void;
time: number;
initialProgress?: number;
}

export const linearAnimation = ({
onProgress,
onIdUpdate = () => {},
time,
initialProgress = 0,
}: LinearAnimationParams) => {
if (time === 0) {
onProgress(1);
onIdUpdate(undefined);
return;
}

let start: number | undefined = undefined;
const step = (timestamp: number) => {
if (start === undefined) start = timestamp;
const progress = Math.min((timestamp - start) / time + initialProgress, 1);

onProgress(progress);

if (progress < 1) {
const id = window.requestAnimationFrame(step);
onIdUpdate(id);
} else {
onIdUpdate(undefined);
}
};
const id = window.requestAnimationFrame(step);
onIdUpdate(id);
};
Loading

0 comments on commit bbb9d95

Please sign in to comment.