Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { StellarProvider } from './context/StellarContext'
import { NetworkSwitcher } from './components/NetworkSwitcher'
import { LanguageSwitcher } from './components/LanguageSwitcher'
import { FundbotButton } from './components/FundbotButton'
import { CopyButton } from './components/CopyButton'
import { useWallet } from './hooks/useWallet'
import { truncateAddress, formatXLM } from './utils/formatting'
import { NavBar } from './components/NavBar'
Expand Down Expand Up @@ -124,11 +125,9 @@ function AppContent() {
<div className="flex items-center gap-3">
<FundbotButton />
<div className="text-right">
<div
className="text-sm font-medium text-gray-900 dark:text-gray-100"
title={wallet.address ?? undefined}
>
{wallet.address && truncateAddress(wallet.address)}
<div className="inline-flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-gray-100">
<span title={wallet.address ?? undefined}>{wallet.address && truncateAddress(wallet.address)}</span>
{wallet.address && <CopyButton value={wallet.address} ariaLabel="Copy wallet address" className="text-gray-400" />}
</div>
<Button onClick={handleDisconnect} variant="secondary" size="sm">
{t('wallet.disconnect')}
Expand All @@ -152,7 +151,10 @@ function AppContent() {

{wallet.isConnected && wallet.address && (
<div className="sm:hidden text-xs text-gray-600 dark:text-gray-400 truncate" title={wallet.address}>
{truncateAddress(wallet.address)}
<div className="inline-flex items-center gap-2">
<span>{truncateAddress(wallet.address)}</span>
<CopyButton value={wallet.address} ariaLabel="Copy wallet address" className="text-gray-400" />
</div>
{wallet.balance && <span className="ml-2">{formatXLM(wallet.balance)}</span>}
</div>
)}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
<button
onClick={handleCopy}
aria-label={copied ? 'Copied!' : ariaLabel}
title={copied ? 'Copied!' : ariaLabel}
className={`text-gray-400 hover:text-gray-600 ${defaultClasses} ${className}`}
type="button"
>
<span className="sr-only">{copied ? 'Copied to clipboard' : ariaLabel}</span>
{copied ? (
// Checkmark icon (from AddressDisplay)
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Expand Down
35 changes: 20 additions & 15 deletions frontend/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,30 @@ export const TokenDashboard: React.FC = () => {
key={token.address}
className="p-3 border rounded text-sm flex items-center justify-between gap-2 hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors dark:bg-slate-800 dark:border-slate-700"
>
<Link
to={`/tokens/${token.address}`}
className="flex-1 min-w-0 hover:underline"
title={`View ${token.name} details`}
>
<span className="font-medium">{token.name}</span>
<span className="ml-2 text-gray-500 font-mono">({token.symbol})</span>
<div
className="text-xs text-gray-400 mt-0.5 font-mono truncate"
title={token.address}
<div className="flex-1 min-w-0">
<Link
to={`/tokens/${token.address}`}
className="block hover:underline"
title={`View ${token.name} details`}
>
{formatAddress(token.address)}
</div>
<span className="font-medium">{token.name}</span>
<span className="ml-2 text-gray-500 font-mono">({token.symbol})</span>
<div
className="text-xs text-gray-400 mt-0.5 font-mono truncate"
title={token.address}
>
{formatAddress(token.address)}
</div>
</Link>
{token.creator && (
<div className="text-xs text-gray-400 font-mono truncate" title={token.creator}>
Creator: {formatAddress(token.creator)}
<div className="mt-1 inline-flex items-center gap-1 text-xs text-gray-400 font-mono w-full">
<span className="truncate block max-w-full" title={token.creator}>
Creator: {formatAddress(token.creator)}
</span>
<CopyButton value={token.creator} ariaLabel="Copy creator address" className="text-gray-400" />
</div>
)}
</Link>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<CopyButton value={token.address} ariaLabel="Copy token address" />
<a
Expand Down
21 changes: 12 additions & 9 deletions frontend/src/components/TokenDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,18 @@ export const TokenDetail: React.FC = () => {
<dt className="text-gray-500 dark:text-gray-400">Creator</dt>
<dd className="flex items-center gap-1 font-mono text-xs break-all text-gray-900 dark:text-gray-100 mt-1">
{token.creator ? (
<a
href={stellarExplorerUrl('account', token.creator, network)}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-500 hover:underline"
title={token.creator}
>
{formatAddress(token.creator)}
</a>
<>
<a
href={stellarExplorerUrl('account', token.creator, network)}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-500 hover:underline"
title={token.creator}
>
{formatAddress(token.creator)}
</a>
<CopyButton value={token.creator} ariaLabel="Copy creator address" className="text-gray-400" />
</>
) : '—'}
</dd>
</div>
Expand Down
22 changes: 13 additions & 9 deletions frontend/src/components/TransactionHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import React from 'react';
import { useTransactionHistory } from '../hooks/useTransactionHistory';
import { CopyButton } from './CopyButton'

interface TransactionHistoryProps {
publicKey?: string;
Expand Down Expand Up @@ -98,15 +99,18 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({
</span>
</td>
<td className="px-4 py-2">
<a
href={`https://stellar.expert/explorer/public/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
aria-label={`View transaction ${tx.hash} on Stellar Explorer`}
>
View
</a>
<div className="flex items-center gap-2">
<a
href={`https://stellar.expert/explorer/public/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
aria-label={`View transaction ${tx.hash} on Stellar Explorer`}
>
View
</a>
<CopyButton value={tx.hash} ariaLabel="Copy transaction hash" className="text-gray-400" />
</div>
</td>
</tr>
))}
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/components/TransactionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'
import { stellarService } from '../services/stellar'
import { Spinner } from './UI/Spinner'
import { CopyButton } from './CopyButton'

export interface TransactionStatusProps {
txHash: string
Expand Down Expand Up @@ -96,15 +97,19 @@ export const TransactionStatus: React.FC<TransactionStatusProps> = ({
</svg>
</div>
<span className="font-bold text-lg text-gray-800">Transaction Successful</span>
<a
href={`https://stellar.expert/explorer/testnet/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-mono text-blue-500 hover:text-blue-700 underline truncate max-w-full px-4"
title={txHash}
>
{txHash.slice(0, 8)}...{txHash.slice(-8)}
</a>
<div className="inline-flex items-center gap-2">
<a
href={`https://stellar.expert/explorer/testnet/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-mono text-blue-500 hover:text-blue-700 underline truncate max-w-full px-4"
title={txHash}
aria-label="View transaction on Stellar Explorer"
>
{txHash.slice(0, 8)}...{txHash.slice(-8)}
</a>
<CopyButton value={txHash} ariaLabel="Copy transaction hash" className="text-gray-400" />
</div>
</div>
)}

Expand Down
13 changes: 6 additions & 7 deletions frontend/src/components/WalletConnectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useWalletContext } from '../context/WalletContext'
import { truncateAddress, formatXLM } from '../utils/formatting'
import { Button } from './UI/Button'
import { Spinner } from './UI/Spinner'
import { CopyButton } from './CopyButton'

export const WalletConnectButton: React.FC = () => {
const { wallet, isConnecting, isInstalled, connect, disconnect } = useWalletContext()
Expand Down Expand Up @@ -35,13 +36,11 @@ export const WalletConnectButton: React.FC = () => {
if (wallet.isConnected && wallet.address) {
return (
<div className="inline-flex items-center gap-3">
<div className="flex flex-col items-end">
<span
className="font-mono text-sm text-gray-700 dark:text-gray-300"
title={wallet.address}
>
{truncateAddress(wallet.address)}
</span>
<div className="flex flex-col items-end gap-1">
<div className="inline-flex items-center gap-2 font-mono text-sm text-gray-700 dark:text-gray-300">
<span title={wallet.address}>{truncateAddress(wallet.address)}</span>
<CopyButton value={wallet.address} ariaLabel="Copy wallet address" className="text-gray-400" />
</div>
{wallet.balance !== undefined ? (
<span className="text-xs text-gray-500 dark:text-gray-400">
{formatXLM(wallet.balance)}
Expand Down
49 changes: 24 additions & 25 deletions frontend/src/hooks/useClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,43 @@ export const useClipboard = (resetDelay = 2000) => {
const [copied, setCopied] = useState(false)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const fallbackCopy = (text: string) => {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
textArea.style.top = '0'
document.body.appendChild(textArea)

textArea.focus()
textArea.select()

const successful = document.execCommand?.('copy')
document.body.removeChild(textArea)

return Boolean(successful)
}

const copy = useCallback(
async (text: string) => {
if (!text) return

try {
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}

// Try modern navigator.clipboard API first
let copiedSuccessfully = false

if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
setCopied(true)
} else {
// Fallback to execCommand for older browsers or non-secure contexts
const textArea = document.createElement('textarea')
textArea.value = text

// Ensure textarea is not visible but part of DOM
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
textArea.style.top = '0'
document.body.appendChild(textArea)

textArea.focus()
textArea.select()

const successful = document.execCommand('copy')
document.body.removeChild(textArea)

if (successful) {
setCopied(true)
}
// Fallback copy silently failed — nothing to surface to the user
copiedSuccessfully = true
} else if (typeof document !== 'undefined') {
copiedSuccessfully = fallbackCopy(text)
}

// Reset copied state after delay
setCopied(copiedSuccessfully)

timeoutRef.current = setTimeout(() => {
setCopied(false)
timeoutRef.current = null
Expand Down
Loading