mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-06 03:30:25 -05:00
127 lines
3.1 KiB
TypeScript
127 lines
3.1 KiB
TypeScript
import type {
|
|
GlobalOptions as ConfettiGlobalOptions,
|
|
CreateTypes as ConfettiInstance,
|
|
Options as ConfettiOptions,
|
|
} from 'canvas-confetti';
|
|
import confetti from 'canvas-confetti';
|
|
import type { ReactNode } from 'react';
|
|
import React, {
|
|
createContext,
|
|
forwardRef,
|
|
useCallback,
|
|
useEffect,
|
|
useImperativeHandle,
|
|
useMemo,
|
|
useRef,
|
|
} from 'react';
|
|
|
|
import { Button, ButtonProps } from './button';
|
|
|
|
type Api = {
|
|
fire: (options?: ConfettiOptions) => void;
|
|
};
|
|
|
|
type Props = React.ComponentPropsWithRef<'canvas'> & {
|
|
options?: ConfettiOptions;
|
|
globalOptions?: ConfettiGlobalOptions;
|
|
manualstart?: boolean;
|
|
children?: ReactNode;
|
|
};
|
|
|
|
export type ConfettiRef = Api | null;
|
|
|
|
const ConfettiContext = createContext<Api>({} as Api);
|
|
|
|
const Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
|
|
const {
|
|
options,
|
|
globalOptions = { resize: true, useWorker: true },
|
|
manualstart = false,
|
|
children,
|
|
...rest
|
|
} = props;
|
|
const instanceRef = useRef<ConfettiInstance | null>(null); // confetti instance
|
|
|
|
const canvasRef = useCallback(
|
|
// https://react.dev/reference/react-dom/components/common#ref-callback
|
|
// https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
|
|
(node: HTMLCanvasElement) => {
|
|
if (node !== null) {
|
|
// <canvas> is mounted => create the confetti instance
|
|
if (instanceRef.current) return; // if not already created
|
|
instanceRef.current = confetti.create(node, {
|
|
...globalOptions,
|
|
resize: true,
|
|
});
|
|
} else {
|
|
// <canvas> is unmounted => reset and destroy instanceRef
|
|
if (instanceRef.current) {
|
|
instanceRef.current.reset();
|
|
instanceRef.current = null;
|
|
}
|
|
}
|
|
},
|
|
[globalOptions],
|
|
);
|
|
|
|
// `fire` is a function that calls the instance() with `opts` merged with `options`
|
|
const fire = useCallback(
|
|
(opts = {}) => instanceRef.current?.({ ...options, ...opts }),
|
|
[options],
|
|
);
|
|
|
|
const api = useMemo(
|
|
() => ({
|
|
fire,
|
|
}),
|
|
[fire],
|
|
);
|
|
|
|
useImperativeHandle(ref, () => api, [api]);
|
|
|
|
useEffect(() => {
|
|
if (!manualstart) {
|
|
fire();
|
|
}
|
|
}, [manualstart, fire]);
|
|
|
|
return (
|
|
<ConfettiContext.Provider value={api}>
|
|
<canvas ref={canvasRef} {...rest} />
|
|
{children}
|
|
</ConfettiContext.Provider>
|
|
);
|
|
});
|
|
|
|
interface ConfettiButtonProps extends ButtonProps {
|
|
options?: ConfettiOptions & ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) {
|
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
const x = rect.left + rect.width / 2;
|
|
const y = rect.top + rect.height / 2;
|
|
confetti({
|
|
...options,
|
|
origin: {
|
|
x: x / window.innerWidth,
|
|
y: y / window.innerHeight,
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Button onClick={handleClick} {...props}>
|
|
{children}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
Confetti.displayName = 'Confetti';
|
|
|
|
export { Confetti, ConfettiButton };
|
|
|
|
export default Confetti;
|