diff options
Diffstat (limited to 'jb-ui/src/components/ui/confetti.tsx')
| -rw-r--r-- | jb-ui/src/components/ui/confetti.tsx | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/jb-ui/src/components/ui/confetti.tsx b/jb-ui/src/components/ui/confetti.tsx new file mode 100644 index 0000000..e0d353a --- /dev/null +++ b/jb-ui/src/components/ui/confetti.tsx @@ -0,0 +1,146 @@ +import type { ReactNode } from "react" +import React, { + createContext, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react" +import type { + GlobalOptions as ConfettiGlobalOptions, + CreateTypes as ConfettiInstance, + Options as ConfettiOptions, +} from "canvas-confetti" +import confetti from "canvas-confetti" + +import { Button } from "@/components/ui/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) + +// Define component first +const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => { + const { + options, + globalOptions = { resize: true, useWorker: true }, + manualstart = false, + children, + ...rest + } = props + const instanceRef = useRef<ConfettiInstance | null>(null) + + const canvasRef = useCallback( + (node: HTMLCanvasElement) => { + if (node !== null) { + if (instanceRef.current) return + instanceRef.current = confetti.create(node, { + ...globalOptions, + resize: true, + }) + } else { + if (instanceRef.current) { + instanceRef.current.reset() + instanceRef.current = null + } + } + }, + [globalOptions] + ) + + const fire = useCallback( + async (opts = {}) => { + try { + await instanceRef.current?.({ ...options, ...opts }) + } catch (error) { + console.error("Confetti error:", error) + } + }, + [options] + ) + + const api = useMemo( + () => ({ + fire, + }), + [fire] + ) + + useImperativeHandle(ref, () => api, [api]) + + useEffect(() => { + if (!manualstart) { + ;(async () => { + try { + await fire() + } catch (error) { + console.error("Confetti effect error:", error) + } + })() + } + }, [manualstart, fire]) + + return ( + <ConfettiContext.Provider value={api}> + <canvas ref={canvasRef} {...rest} /> + {children} + </ConfettiContext.Provider> + ) +}) + +// Set display name immediately +ConfettiComponent.displayName = "Confetti" + +// Export as Confetti +export const Confetti = ConfettiComponent + +interface ConfettiButtonProps extends React.ComponentProps<"button"> { + options?: ConfettiOptions & + ConfettiGlobalOptions & { canvas?: HTMLCanvasElement } +} + +const ConfettiButtonComponent = ({ + options, + children, + ...props +}: ConfettiButtonProps) => { + const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => { + try { + const rect = event.currentTarget.getBoundingClientRect() + const x = rect.left + rect.width / 2 + const y = rect.top + rect.height / 2 + await confetti({ + ...options, + origin: { + x: x / window.innerWidth, + y: y / window.innerHeight, + }, + }) + } catch (error) { + console.error("Confetti button error:", error) + } + } + + return ( + <Button onClick={handleClick} {...props}> + {children} + </Button> + ) +} + +ConfettiButtonComponent.displayName = "ConfettiButton" + +export const ConfettiButton = ConfettiButtonComponent |
