Skip to content
Merged
6 changes: 1 addition & 5 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
<HeaderCard bounty={bounty} />
<DescriptionCard description={bounty.description} />
{!isCancelled && pool && <EscrowDetailPanel poolId={bountyId} />}
<RefundStatusTracker
bountyId={bountyId}
isCancelled={isCancelled}
/>
<RefundStatusTracker bountyId={bountyId} isCancelled={isCancelled} />
<BountyDetailSubmissionsCard bounty={bounty} />
</div>

Expand All @@ -107,4 +104,3 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
</div>
);
}

9 changes: 2 additions & 7 deletions components/bounty-detail/bounty-detail-sidebar-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
AlertDialogFooter,
AlertDialogCancel,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";

Check warning on line 25 in components/bounty-detail/bounty-detail-sidebar-cta.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'toast' is defined but never used

import { BountyFieldsFragment } from "@/lib/graphql/generated";
import { StatusBadge, TypeBadge } from "./bounty-badges";
Expand Down Expand Up @@ -237,9 +237,7 @@
onClick={handleCancel}
disabled={!cancelReason.trim() || isCancelling}
>
{isCancelling && (
<Loader2 className="mr-2 size-4 animate-spin" />
)}
{isCancelling && <Loader2 className="mr-2 size-4 animate-spin" />}
Cancel Bounty & Refund
</Button>
</AlertDialogFooter>
Expand Down Expand Up @@ -346,9 +344,7 @@
onClick={handleCancel}
disabled={!cancelReason.trim() || isCancelling}
>
{isCancelling && (
<Loader2 className="mr-2 size-4 animate-spin" />
)}
{isCancelling && <Loader2 className="mr-2 size-4 animate-spin" />}
Cancel & Refund
</Button>
</AlertDialogFooter>
Expand All @@ -357,4 +353,3 @@
</div>
);
}

27 changes: 20 additions & 7 deletions components/bounty-detail/bounty-detail-submissions-card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use client";

import { useState, useEffect } from "react";
import { Loader2, DollarSign } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand All @@ -11,17 +9,20 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { BountySubmissionType } from "@/lib/graphql/generated";
import { TransactionLink } from "@/components/ui/stellar-link";
import { Textarea } from "@/components/ui/textarea";
import { useSubmissionDraft } from "@/hooks/use-submission-draft";
import {
useSubmitToBounty,
useReviewSubmission,
useMarkSubmissionPaid,
useReviewSubmission,
useSubmitToBounty,
} from "@/hooks/use-submission-mutations";
import { authClient } from "@/lib/auth-client";
import { useSubmissionDraft } from "@/hooks/use-submission-draft";
import { BountySubmissionType } from "@/lib/graphql/generated";
import { DollarSign, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";

interface ExtendedUser {
id: string;
Expand Down Expand Up @@ -321,6 +322,18 @@ export function BountyDetailSubmissionsCard({
Paid on {new Date(submission.paidAt).toLocaleDateString()}
</div>
)}

{submission.rewardTransactionHash && (
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-400">Transaction:</span>
<TransactionLink
value={submission.rewardTransactionHash}
maxLength={10}
showCopy={true}
className="text-primary"
/>
</div>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
))}
</div>
Expand Down
221 changes: 221 additions & 0 deletions components/ui/stellar-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
"use client";

import React, { useState } from "react";
import { Copy, ExternalLink, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
getTransactionUrl,
getAccountUrl,
getContractUrl,
getStellarNetwork,
StellarNetwork,
isValidStellarAddress,
isValidStellarTxHash,
isValidStellarContractId,
} from "@/lib/utils/stellar-explorer";

export type StellarLinkType = "transaction" | "account" | "contract";

interface StellarLinkProps {
/** The value to link to (transaction hash, address, or contract ID) */
value: string;
/** The type of link */
type: StellarLinkType;
/** Optional network override */
network?: StellarNetwork;
/** Optional explorer override */
explorer?: string;
/** Maximum characters to display before truncating */
maxLength?: number;
/** Whether to show the copy button */
showCopy?: boolean;
/** Whether to show the external link icon */
showExternalIcon?: boolean;
/** Custom CSS classes */
className?: string;
/** Custom link text (overrides truncation) */
linkText?: string;
/** Tooltip text prefix */
tooltipPrefix?: string;
}

export function StellarLink({
value,
type,
network,
explorer = "stellar.expert",
maxLength = 12,
showCopy = true,
showExternalIcon = true,
className,
linkText,
tooltipPrefix,
}: StellarLinkProps) {
const [copied, setCopied] = useState(false);
const [copyError, setCopyError] = useState(false);

// Validate the input
const isValid = React.useMemo(() => {
switch (type) {
case "transaction":
return isValidStellarTxHash(value);
case "account":
return isValidStellarAddress(value);
case "contract":
return isValidStellarContractId(value);
default:
return false;
}
}, [value, type]);

// Generate the appropriate URL
const url = React.useMemo(() => {
if (!isValid) return "";

try {
const detectedNetwork = network || getStellarNetwork();

switch (type) {
case "transaction":
return getTransactionUrl(value, detectedNetwork, explorer);
case "account":
return getAccountUrl(value, detectedNetwork, explorer);
case "contract":
return getContractUrl(value, detectedNetwork, explorer);
default:
return "";
}
} catch (error) {
console.error("Error generating Stellar URL:", error);
return "";
}
}, [value, type, network, explorer, isValid]);

// Truncate the display value
const displayValue = React.useMemo(() => {
if (linkText) return linkText;
if (!value) return "";
if (value.length <= maxLength) return value;

const start = value.slice(0, Math.ceil(maxLength / 2));
const end = value.slice(-Math.floor(maxLength / 2));
return `${start}...${end}`;
}, [value, maxLength, linkText]);

// Copy to clipboard handler
const handleCopy = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();

try {
await navigator.clipboard.writeText(value);
setCopied(true);
setCopyError(false);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error("Failed to copy:", error);
setCopyError(true);
setTimeout(() => setCopyError(false), 2000);
}
};

// Generate tooltip text
const tooltipText = React.useMemo(() => {
if (!isValid) return `Invalid ${type}`;
const prefix = tooltipPrefix || `View ${type}`;
const networkText = network || getStellarNetwork();
return `${prefix} on ${networkText} • ${explorer}`;
}, [type, network, explorer, isValid, tooltipPrefix, value]);

Check warning on line 135 in components/ui/stellar-link.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

React Hook React.useMemo has an unnecessary dependency: 'value'. Either exclude it or remove the dependency array

if (!value || !isValid) {
return (
<span
className={cn("text-muted-foreground font-mono text-sm", className)}
>
{displayValue || "Invalid"}
</span>
);
}

return (
<TooltipProvider>
<div className={cn("inline-flex items-center gap-1", className)}>
<Tooltip>
<TooltipTrigger asChild>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className={cn(
"inline-flex items-center gap-1 text-primary hover:text-primary/80",
"transition-colors duration-200",
"font-mono text-sm",
"hover:underline",
"focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-1",
)}
>
<span>{displayValue}</span>
{showExternalIcon && (
<ExternalLink className="h-3 w-3 shrink-0" />
)}
</a>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{tooltipText}</p>
</TooltipContent>
</Tooltip>

{showCopy && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
onClick={handleCopy}
className={cn(
"h-6 w-6 shrink-0",
copied && "text-green-600",
copyError && "text-red-600",
)}
aria-label={`Copy ${type} value`}
>
{copied ? (
<Check className="h-3 w-3" />
) : copyError ? (
<span className="text-xs">!</span>
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{copied ? "Copied!" : copyError ? "Failed" : `Copy ${type}`}
</p>
</TooltipContent>
</Tooltip>
)}
</div>
</TooltipProvider>
);
}

// Convenience components for specific types
export function TransactionLink(props: Omit<StellarLinkProps, "type">) {
return <StellarLink {...props} type="transaction" />;
}

export function AccountLink(props: Omit<StellarLinkProps, "type">) {
return <StellarLink {...props} type="account" />;
}

export function ContractLink(props: Omit<StellarLinkProps, "type">) {
return <StellarLink {...props} type="contract" />;
}
Loading
Loading