mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-17 13:07:34 +02:00
* refactor: consistent use of displayName vs walletFileName * refactor: Settings component from jsx to tsx * refactor: Wallets component from jsx to tsx * refactor: Onboarding component from jsx to tsx * refactor: ScheduleProgress component from jsx to tsx * chore(i18n): consistent use of global.errors.reason_unknown * refactor: CreateWallet component from jsx to tsx * refactor: BitcoinQR from jsx to tsx * refactor: Receive component from jsx to tsx * refactor: Earn component from jsx to tsx * dev(regtest): switch to python:3.11-slim-bookworm as base image see https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1484#issuecomment-1751814593 * chore(dev): do not log error when fetch was aborted
129 lines
3 KiB
TypeScript
129 lines
3 KiB
TypeScript
import { ReactNode, PropsWithChildren, useState, useEffect, useRef } from 'react'
|
|
|
|
const copyToClipboard = (
|
|
text: string,
|
|
fallbackInputField: HTMLInputElement,
|
|
errorMessage?: string,
|
|
): Promise<boolean> => {
|
|
const copyToClipboardFallback = (
|
|
inputField: HTMLInputElement,
|
|
errorMessage = 'Cannot copy value to clipboard',
|
|
): Promise<boolean> =>
|
|
new Promise((resolve, reject) => {
|
|
inputField.select()
|
|
const success = document.execCommand && document.execCommand('copy')
|
|
inputField.blur()
|
|
success ? resolve(success) : reject(new Error(errorMessage))
|
|
})
|
|
|
|
// The `navigator.clipboard` API might not be available, e.g. on sites served over HTTP.
|
|
if (!navigator.clipboard) {
|
|
return copyToClipboardFallback(fallbackInputField)
|
|
}
|
|
|
|
return navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => true)
|
|
.catch((e: Error) => {
|
|
if (fallbackInputField) {
|
|
return copyToClipboardFallback(fallbackInputField, errorMessage)
|
|
} else {
|
|
throw e
|
|
}
|
|
})
|
|
}
|
|
|
|
interface CopyableProps {
|
|
value: string
|
|
onSuccess?: () => void
|
|
onError?: (e: Error) => void
|
|
className?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
function Copyable({
|
|
value,
|
|
onSuccess,
|
|
onError,
|
|
className,
|
|
children,
|
|
disabled,
|
|
...props
|
|
}: PropsWithChildren<CopyableProps>) {
|
|
const valueFallbackInputRef = useRef(null)
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
{...props}
|
|
disabled={disabled}
|
|
className={className}
|
|
onClick={() => copyToClipboard(value, valueFallbackInputRef.current!).then(onSuccess, onError)}
|
|
>
|
|
{children}
|
|
</button>
|
|
<input
|
|
readOnly
|
|
aria-hidden
|
|
ref={valueFallbackInputRef}
|
|
value={value}
|
|
style={{
|
|
position: 'absolute',
|
|
left: '-9999px',
|
|
top: '-9999px',
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
interface CopyButtonProps extends CopyableProps {
|
|
text: ReactNode
|
|
successText?: ReactNode
|
|
successTextTimeout?: number
|
|
disabled?: boolean
|
|
}
|
|
|
|
export function CopyButton({
|
|
value,
|
|
onSuccess,
|
|
onError,
|
|
text,
|
|
successText = text,
|
|
successTextTimeout = 1_500,
|
|
className,
|
|
disabled,
|
|
...props
|
|
}: CopyButtonProps) {
|
|
const [showValueCopiedConfirmation, setShowValueCopiedConfirmation] = useState(false)
|
|
const [valueCopiedFlag, setValueCopiedFlag] = useState(0)
|
|
|
|
useEffect(() => {
|
|
if (valueCopiedFlag < 1) return
|
|
|
|
setShowValueCopiedConfirmation(true)
|
|
const timer = setTimeout(() => {
|
|
setShowValueCopiedConfirmation(false)
|
|
}, successTextTimeout)
|
|
|
|
return () => clearTimeout(timer)
|
|
}, [valueCopiedFlag, successTextTimeout])
|
|
|
|
return (
|
|
<Copyable
|
|
{...props}
|
|
disabled={disabled}
|
|
className={`btn ${className || ''}`}
|
|
value={value}
|
|
onError={onError}
|
|
onSuccess={() => {
|
|
setValueCopiedFlag((current) => current + 1)
|
|
onSuccess && onSuccess()
|
|
}}
|
|
>
|
|
<div className="d-flex align-items-center justify-content-center">
|
|
{showValueCopiedConfirmation ? successText : text}
|
|
</div>
|
|
</Copyable>
|
|
)
|
|
}
|