Instantly share code, notes, and snippets.
Last active
April 28, 2025 00:33
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save mnixry/2e29f83e009bd36e070eb0c13b09d950 to your computer and use it in GitHub Desktop.
Loading & Refresh Button component for Shadcn UI & Lucide Icons.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useState } from 'react'; | |
import { Loader2Icon } from 'lucide-react'; | |
import { useInterval } from 'react-use'; | |
import { cn } from '@/lib/utils'; | |
import { Button } from '@/components/ui/button'; | |
export const LoadingButtonPrimitive: React.FC< | |
React.ComponentPropsWithRef<typeof Button> & { loading?: boolean } | |
> = ({ loading, disabled, className, children, ...props }) => { | |
return ( | |
<Button | |
disabled={loading || disabled} | |
className={cn( | |
'disabled:pointer-events-auto', | |
loading ? 'disabled:cursor-wait' : 'disabled:cursor-not-allowed', | |
className | |
)} | |
{...props} | |
> | |
{children} | |
</Button> | |
); | |
}; | |
interface LoadingButtonProps { | |
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void; | |
leftSection?: React.ReactNode; | |
leftSectionLoading?: React.ReactNode; | |
childrenLoading?: React.ReactNode; | |
} | |
export const LoadingButton: React.FC< | |
Omit<React.ComponentPropsWithRef<typeof LoadingButtonPrimitive>, keyof LoadingButtonProps> & | |
LoadingButtonProps | |
> = ({ | |
onClick: onClickProp, | |
leftSection, | |
leftSectionLoading, | |
childrenLoading, | |
children, | |
...props | |
}) => { | |
const [loading, setLoading] = useState(false); | |
const onClick = useCallback( | |
async (e: React.MouseEvent<HTMLButtonElement>) => { | |
e.stopPropagation(); | |
try { | |
setLoading(true); | |
await onClickProp?.(e); | |
} finally { | |
setLoading(false); | |
} | |
}, | |
[onClickProp] | |
); | |
return ( | |
<LoadingButtonPrimitive onClick={onClick} loading={loading} {...props}> | |
{loading | |
? (leftSectionLoading ?? <Loader2Icon className="mr-2 size-4 animate-spin" />) | |
: leftSection} | |
{loading && childrenLoading ? childrenLoading : children} | |
</LoadingButtonPrimitive> | |
); | |
}; | |
const ConfirmInnerDefault: React.FC<{ remaining?: number }> = ({ remaining }) => { | |
return <span>Proceed? {typeof remaining === 'number' ? `(${remaining}s)` : ''}</span>; | |
}; | |
interface DestructiveLoadingButtonProps extends LoadingButtonProps { | |
confirmInner?: React.ComponentType<React.ComponentProps<typeof ConfirmInnerDefault>>; | |
} | |
export const DestructiveLoadingButton: React.FC< | |
Omit< | |
React.ComponentPropsWithRef<typeof LoadingButtonPrimitive>, | |
keyof DestructiveLoadingButtonProps | |
> & | |
DestructiveLoadingButtonProps | |
> = ({ | |
onClick: onClickProp, | |
leftSection, | |
leftSectionLoading, | |
childrenLoading, | |
confirmInner: ConfirmInner = ConfirmInnerDefault, | |
children: childrenProp, | |
variant, | |
disabled, | |
...props | |
}) => { | |
const [loading, setLoading] = useState(false); | |
const [confirming, setConfirming] = useState(false); | |
const [protectiveDisabled, setProtectiveDisabled] = useState<number>(); | |
useInterval( | |
() => { | |
setProtectiveDisabled((prev) => | |
typeof prev === 'number' && prev - 1 > 0 ? prev - 1 : undefined | |
); | |
}, | |
typeof protectiveDisabled === 'number' ? 1000 : null | |
); | |
const onClick = useCallback( | |
async (e: React.MouseEvent<HTMLButtonElement>) => { | |
e.stopPropagation(); | |
if (!confirming) { | |
setConfirming(true); | |
setProtectiveDisabled(3); | |
setTimeout(() => setConfirming(false), 10 * 1000); | |
return; | |
} | |
try { | |
setConfirming(false); | |
setLoading(true); | |
await onClickProp?.(e); | |
} finally { | |
setLoading(false); | |
} | |
}, | |
[onClickProp, confirming] | |
); | |
const children = loading ? ( | |
<> | |
{leftSectionLoading ?? <Loader2Icon className="mr-2 size-4 animate-spin" />}\ | |
{childrenLoading ?? childrenProp} | |
</> | |
) : confirming ? ( | |
<> | |
{leftSection} | |
<ConfirmInner remaining={protectiveDisabled} /> | |
</> | |
) : ( | |
<> | |
{leftSection} | |
{childrenProp} | |
</> | |
); | |
return ( | |
<LoadingButtonPrimitive | |
loading={loading} | |
variant={confirming ? 'destructive' : variant} | |
disabled={disabled || typeof protectiveDisabled === 'number'} | |
onClick={onClick} | |
{...props} | |
> | |
{children} | |
</LoadingButtonPrimitive> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useState } from 'react'; | |
import { RefreshCw } from 'lucide-react'; | |
import { LoadingButtonPrimitive } from '@/components/loading-button'; | |
export const RefreshButton: React.FC< | |
React.ComponentProps<typeof LoadingButtonPrimitive> & { | |
isRefreshing?: boolean; | |
} | |
> = ({ children, isRefreshing, ...rest }) => { | |
const [isAnimating, setIsAnimating] = useState(false); | |
const [rotation, setRotation] = useState(0); | |
const onTransitionEnd = useCallback(() => { | |
if (isRefreshing) { | |
setIsAnimating(true); | |
setRotation((prev) => prev + 360); | |
} else { | |
setIsAnimating(false); | |
} | |
}, [isRefreshing, setRotation]); | |
useEffect(() => { | |
onTransitionEnd(); | |
}, [onTransitionEnd]); | |
return ( | |
<LoadingButtonPrimitive variant="ghost" size="icon" loading={isAnimating} {...rest}> | |
<RefreshCw | |
className="size-4 duration-1000 transition-transform" | |
style={{ | |
transform: `rotate(${rotation}deg)`, | |
}} | |
onTransitionEnd={onTransitionEnd} | |
/> | |
{children} | |
</LoadingButtonPrimitive> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment