Skip to content
Merged
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
25 changes: 22 additions & 3 deletions components/dashboard/contract-escrow-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
EscrowStatusTracker,
type EscrowStage,
} from "@/components/dashboard/escrow-status-tracker";
import { ContractAddress, TransactionHash } from "@/components/stellar";
import { networkFromPassphrase } from "@/lib/stellar/explorer";

interface EscrowInfo {
escrow_address: string | null;
Expand All @@ -32,6 +34,8 @@ export function ContractEscrowSummary({
const escrowStage: EscrowStage =
(escrow?.escrow_status as EscrowStage | undefined) ?? "Funded";

const network = networkFromPassphrase(escrow?.network_passphrase);

return (
<div className="space-y-4">
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -61,7 +65,14 @@ export function ContractEscrowSummary({
{escrow.escrow_address && (
<p className="text-xs text-muted-foreground font-mono mt-1 break-all flex items-center gap-1.5">
<Wallet className="h-3.5 w-3.5 shrink-0" />
<span>Address: {escrow.escrow_address}</span>
<span>Address: </span>
<ContractAddress
address={escrow.escrow_address}
network={network}
showCopy
showExplorerLink
size="sm"
/>
</p>
)}
{escrow.network_passphrase && (
Expand Down Expand Up @@ -96,8 +107,16 @@ export function ContractEscrowSummary({
</div>

{escrow.funding_tx_hash && (
<p className="text-[11px] text-muted-foreground font-mono truncate">
Funding Tx: <span className="text-foreground">{escrow.funding_tx_hash}</span>
<p className="text-[11px] text-muted-foreground font-mono flex items-center gap-1.5">
<span>Funding Tx:</span>
<TransactionHash
hash={escrow.funding_tx_hash}
network={network}
showCopy
showExplorerLink
truncateChars={8}
size="sm"
/>
</p>
)}

Expand Down
18 changes: 8 additions & 10 deletions components/freelancer/freelancer-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
CheckCircle2,
Clock,
Copy,
ExternalLink,
Shield,
Star,
TrendingUp,
Expand All @@ -16,6 +15,7 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import type { FreelancerReputationPayload } from '@/lib/reputation'
import { WalletAddress } from '@/components/stellar'

const MOCK_SKILLS = [
'Next.js',
Expand Down Expand Up @@ -275,15 +275,13 @@ export function FreelancerProfile() {
)}
</div>
{walletAddress && (
<a
href={`https://stellar.expert/explorer/public/account/${walletAddress}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-primary"
>
<ExternalLink className="size-3" />
View on Explorer
</a>
<WalletAddress
address={walletAddress}
network="PUBLIC"
showCopy
showExplorerLink
size="sm"
/>
)}
</div>
</div>
Expand Down
109 changes: 109 additions & 0 deletions components/stellar/BlockInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use client'

import { Blocks } from 'lucide-react'
import { cn } from '@/lib/utils'
import { getLedgerUrl } from '@/lib/stellar/explorer'
import { ExplorerLink } from './ExplorerLink'
import type { StellarNetwork } from '@/components/wallet-provider'

export interface BlockInfoProps {
/** The ledger sequence number */
ledgerSeq: number | string
/** The network environment */
network: StellarNetwork
/** Additional CSS classes */
className?: string
/** Size variant */
size?: 'sm' | 'md' | 'lg'
/** Optional label to display before the block info */
label?: string
/** Whether to show the explorer link */
showExplorerLink?: boolean
/** Whether to show the block icon */
showIcon?: boolean
/** Optional timestamp to display alongside the block */
timestamp?: string
}

/**
* Displays block/ledger information with a direct reference link
* to Stellar Explorer.
*
* @example
* ```tsx
* <BlockInfo
* ledgerSeq={12345678}
* network="TESTNET"
* label="Ledger"
* timestamp="2025-10-24T12:00:00Z"
* />
* ```
*/
export function BlockInfo({
ledgerSeq,
network,
className,
size = 'sm',
label,
showExplorerLink = true,
showIcon = true,
timestamp,
}: BlockInfoProps) {
const explorerUrl = showExplorerLink
? getLedgerUrl(ledgerSeq, network)
: undefined

const sizeClasses = {
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base',
}

const formattedTimestamp = timestamp
? new Date(timestamp).toLocaleString()
: undefined

return (
<span
className={cn(
'inline-flex items-center gap-1.5',
sizeClasses[size],
className
)}
>
{/* Block icon */}
{showIcon && (
<Blocks className="h-3.5 w-3.5 text-muted-foreground/60 shrink-0" />
)}

{/* Optional label */}
{label && (
<span className="text-muted-foreground/70 font-sans">{label}:</span>
)}

{/* Explorer link or plain text */}
{explorerUrl ? (
<ExplorerLink
href={explorerUrl}
network={network}
showIcon={true}
size={size}
aria-label={`View ledger ${ledgerSeq} on Stellar Explorer`}
>
<span className="font-mono" title={`Ledger ${ledgerSeq}`}>
#{ledgerSeq}
</span>
</ExplorerLink>
) : (
<span className="font-mono text-muted-foreground">#{ledgerSeq}</span>
)}

{/* Timestamp */}
{formattedTimestamp && (
<span className="text-muted-foreground/60 font-sans">
{formattedTimestamp}
</span>
)}
</span>
)
}
129 changes: 129 additions & 0 deletions components/stellar/ContractAddress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'use client'

import { useState, useCallback } from 'react'
import { Copy, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import {
getContractUrl,
truncateAddress,
copyToClipboard,
} from '@/lib/stellar/explorer'
import { ExplorerLink } from './ExplorerLink'
import type { StellarNetwork } from '@/components/wallet-provider'

export interface ContractAddressProps {
/** The full contract address (typically C... Stellar address) */
address: string
/** The network environment */
network: StellarNetwork
/** Whether to show the copy button */
showCopy?: boolean
/** Whether to show the explorer link */
showExplorerLink?: boolean
/** Additional CSS classes */
className?: string
/** Size variant */
size?: 'sm' | 'md' | 'lg'
/** Optional label to display before the address */
label?: string
/** Callback when the address is copied to clipboard */
onCopy?: () => void
}

/**
* Displays a contract address with a clickable link to Stellar Explorer
* and optional copy-to-clipboard functionality.
*
* @example
* ```tsx
* <ContractAddress
* address="CC1234567890..."
* network="TESTNET"
* showCopy
* showExplorerLink
* />
* ```
*/
export function ContractAddress({
address,
network,
showCopy = true,
showExplorerLink = true,
className,
size = 'sm',
label,
onCopy,
}: ContractAddressProps) {
const [copied, setCopied] = useState(false)

const handleCopy = useCallback(async () => {
const success = await copyToClipboard(address)
if (success) {
setCopied(true)
onCopy?.()
setTimeout(() => setCopied(false), 2000)
}
}, [address, onCopy])

const explorerUrl = showExplorerLink ? getContractUrl(address, network) : undefined

const sizeClasses = {
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base',
}

return (
<span
className={cn(
'inline-flex items-center gap-1.5 font-mono',
sizeClasses[size],
className
)}
>
{/* Optional label */}
{label && (
<span className="text-muted-foreground/70 font-sans">{label}:</span>
)}

{/* Explorer link */}
{explorerUrl ? (
<ExplorerLink
href={explorerUrl}
network={network}
showIcon={true}
size={size}
aria-label={`View contract on Stellar Explorer`}
>
<span title={address}>{truncateAddress(address)}</span>
</ExplorerLink>
) : (
<span className="text-muted-foreground" title={address}>
{truncateAddress(address)}
</span>
)}

{/* Copy to clipboard button */}
{showCopy && (
<button
type="button"
onClick={handleCopy}
className={cn(
'inline-flex items-center justify-center rounded p-0.5 transition-colors',
copied
? 'text-accent hover:text-accent/80'
: 'text-muted-foreground/50 hover:text-muted-foreground'
)}
title={copied ? 'Copied!' : 'Copy contract address to clipboard'}
aria-label={copied ? 'Copied' : 'Copy contract address'}
>
{copied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</button>
)}
</span>
)
}
Loading
Loading