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
2 changes: 2 additions & 0 deletions src/components/common/CreatorOnboardingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const CreatorOnboardingForm: React.FC<
placeholder="Tell us about yourself..."
touched={touched.bio}
rows={4}
maxLength={200}
showCharacterCount={true}
/>

<FormInput
Expand Down
152 changes: 93 additions & 59 deletions src/components/common/CreatorProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Copy, Check, Share2 } from 'lucide-react';
import showToast from '@/utils/toast.util';
import appendUtmParams from '@/utils/utm.utils';
Expand Down Expand Up @@ -33,6 +34,15 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
className,
}) => {
const [copied, setCopied] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);

useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);

// Display-normalised handle; raw `handle` is preserved for any equality /
// URL construction the caller might do via the prop.
Expand Down Expand Up @@ -75,75 +85,99 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
return (
<div
className={cn(
'flex flex-col gap-6 md:flex-row md:items-end md:justify-between',
'sticky top-0 z-30 -mx-6 px-6 py-4 transition-all duration-300 md:-mx-12 md:px-12',
isScrolled
? 'bg-slate-950/80 shadow-lg backdrop-blur-md py-3'
: 'bg-transparent',
className
)}
>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:gap-6">
<div
className={cn(
'size-24 overflow-hidden border-4 border-white/10 shadow-xl md:size-32',
CREATOR_CARD_MEDIA_RADIUS_CLASS
)}
role="img"
aria-labelledby="creator-profile-name"
>
<CreatorInitialsAvatar name={name} creatorId={creatorId} imageSrc={avatarUrl} />
</div>
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2 overflow-hidden">
<h1
id="creator-profile-name"
className="truncate font-grotesque text-3xl font-black tracking-tight text-white md:text-4xl"
>
{name}
</h1>
{isVerified && (
<div className="shrink-0">
<VerifiedBadge verified={true} />
<div className="mx-auto max-w-7xl flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-4 md:gap-6">
<motion.div
animate={{
scale: isScrolled ? 0.6 : 1,
}}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className={cn(
'overflow-hidden border-2 border-white/10 shadow-xl shrink-0',
isScrolled ? 'size-12 md:size-16' : 'size-24 md:size-32',
CREATOR_CARD_MEDIA_RADIUS_CLASS
)}
>
<CreatorInitialsAvatar name={name} creatorId={creatorId} imageSrc={avatarUrl} />
</motion.div>
<div className="min-w-0 space-y-0.5">
<div className="flex items-center gap-2 overflow-hidden">
<motion.h1
id="creator-profile-name"
animate={{ fontSize: isScrolled ? '1.25rem' : '1.875rem' }}
className={cn(
"truncate font-grotesque font-black tracking-tight text-white transition-all duration-300",
isScrolled ? "text-xl md:text-2xl" : "text-3xl md:text-4xl"
)}
>
{name}
</motion.h1>
{isVerified && (
<div className="shrink-0">
<VerifiedBadge verified={true} />
</div>
)}
</div>
{!isScrolled ? (
<div className="animate-in fade-in slide-in-from-top-1 duration-300">
<p
className={cn(
'font-jakarta text-lg text-white/50',
CREATOR_PROFILE_SUBTITLE_WRAP_CLASS_NAME
)}
>
{displayHandle || `@${handle}`}
</p>
<CreatorBio
bio={bio}
variant="profile"
collapsible
className="mt-2 max-w-md"
/>
</div>
) : (
<p className="font-jakarta text-xs text-white/50 truncate">
{displayHandle || `@${handle}`}
</p>
)}
</div>
<p
</div>

<div className={cn(
"flex items-center gap-3 transition-transform duration-300",
isScrolled ? "scale-90" : "scale-100"
)}>
<Button
onClick={handleShare}
variant="outline"
className={cn(
'font-jakarta text-lg text-white/50',
CREATOR_PROFILE_SUBTITLE_WRAP_CLASS_NAME
"rounded-xl border-white/10 bg-white/5 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10 active:scale-95",
isScrolled ? "h-9 px-3 text-xs" : "h-11 px-4 text-sm"
)}
>
{displayHandle || `@${handle}`}
</p>
{/* #315: profile bio auto-collapses with a Show more / less
toggle once long enough. Short bios render unchanged. */}
<CreatorBio
bio={bio}
variant="profile"
collapsible
className="mt-2 max-w-md"
/>
{copied ? (
<Check className="mr-2 size-4 text-emerald-400" />
) : canNativeShare ? (
<Share2 className="mr-2 size-4 text-amber-500" />
) : (
<Copy className="mr-2 size-4 text-amber-500" />
)}
<span className="hidden sm:inline">
{copied ? 'Copied!' : canNativeShare ? 'Share Profile' : 'Copy Profile Link'}
</span>
<span className="sm:hidden">
{copied ? 'Copied' : canNativeShare ? 'Share' : 'Copy'}
</span>
</Button>
</div>
</div>

<div className="flex items-center gap-3">
<Button
onClick={handleShare}
variant="outline"
className="h-11 rounded-xl border-white/10 bg-white/5 px-4 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10 active:scale-95"
>
{copied ? (
<Check className="mr-2 size-4 text-emerald-400" />
) : canNativeShare ? (
<Share2 className="mr-2 size-4 text-amber-500" />
) : (
<Copy className="mr-2 size-4 text-amber-500" />
)}
<span className="hidden sm:inline">
{copied ? 'Copied!' : canNativeShare ? 'Share Profile' : 'Copy Profile Link'}
</span>
<span className="sm:hidden">
{copied ? 'Copied' : canNativeShare ? 'Share' : 'Copy'}
</span>
</Button>
</div>
</div>
);
};
Expand Down
39 changes: 29 additions & 10 deletions src/components/common/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface FormInputProps {
suffix?: React.ReactNode;
// Optional wrapper className for the input container
wrapperClassName?: string;
showCharacterCount?: boolean;
}

export const FormInput: React.FC<FormInputProps> = ({
Expand All @@ -42,6 +43,7 @@ export const FormInput: React.FC<FormInputProps> = ({
prefix,
suffix,
wrapperClassName = '',
showCharacterCount = false,
}) => {
// Local display state is used only for number inputs so we can
// show formatted (comma separated) values while keeping the
Expand Down Expand Up @@ -198,16 +200,33 @@ export const FormInput: React.FC<FormInputProps> = ({
</label>

{renderInputWithElements()}

{hasError && (
<p
id={`${inputId}-error`}
className="text-sm text-red-600"
role="alert"
>
{error}
</p>
)}

<div className="flex justify-between items-start gap-2">
<div className="flex-1">
{hasError && (
<p
id={`${inputId}-error`}
className="text-sm text-red-600"
role="alert"
>
{error}
</p>
)}
</div>
{showCharacterCount && maxLength && (
<div
className={cn(
"text-xs font-medium tabular-nums",
maxLength - String(value).length < 20
? "text-amber-500"
: "text-gray-400"
)}
aria-live="polite"
>
{String(value).length} / {maxLength}
</div>
)}
</div>
</div>
);
};
Expand Down
34 changes: 34 additions & 0 deletions src/components/common/PendingTxModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import CircularSpinner from '@/components/common/CircularSpinnerProps';
import { cn } from '@/lib/utils';
import TransactionHashRow from '@/components/common/TransactionHashRow';
import { getConfirmationStatus, CONFIRMATION_THRESHOLDS } from '@/utils/transaction.utils';
import { Badge } from '@/components/ui/badge';

Check failure on line 15 in src/components/common/PendingTxModal.tsx

View workflow job for this annotation

GitHub Actions / verify

Cannot find module '@/components/ui/badge' or its corresponding type declarations.
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';

Check failure on line 16 in src/components/common/PendingTxModal.tsx

View workflow job for this annotation

GitHub Actions / verify

Module '"@/components/ui/tooltip"' has no exported member 'TooltipTrigger'.

Check failure on line 16 in src/components/common/PendingTxModal.tsx

View workflow job for this annotation

GitHub Actions / verify

Module '"@/components/ui/tooltip"' has no exported member 'TooltipProvider'.

Check failure on line 16 in src/components/common/PendingTxModal.tsx

View workflow job for this annotation

GitHub Actions / verify

Module '"@/components/ui/tooltip"' has no exported member 'TooltipContent'.

export interface PendingTxModalProps {
open: boolean;
Expand All @@ -30,6 +33,8 @@
label: string;
onClick: () => void;
};
/** Optional block confirmation count */
confirmations?: number;
}

const PendingTxModal: React.FC<PendingTxModalProps> = ({
Expand All @@ -42,6 +47,7 @@
explorerUrl,
blockDismissal = false,
action,
confirmations,
}) => {
const handleOpenChange = (next: boolean) => {
if (!next && blockDismissal && isLoading) return;
Expand Down Expand Up @@ -93,6 +99,34 @@
<DialogDescription className="text-center">
{description}
</DialogDescription>

{confirmations !== undefined && (
<div className="mt-4 flex flex-col items-center gap-2">
<TooltipProvider>
<Tooltip>

Check failure on line 106 in src/components/common/PendingTxModal.tsx

View workflow job for this annotation

GitHub Actions / verify

Property 'content' is missing in type '{ children: Element[]; }' but required in type 'TooltipProps'.
<TooltipTrigger asChild>
<Badge
variant={confirmations >= CONFIRMATION_THRESHOLDS.CONFIRMED ? "success" : "secondary"}
className={cn(
"px-3 py-1 text-xs font-bold uppercase tracking-wider",
confirmations === 0 && "bg-slate-500/20 text-slate-400 border-slate-500/30",
confirmations > 0 && confirmations < CONFIRMATION_THRESHOLDS.CONFIRMED && "bg-amber-500/20 text-amber-300 border-amber-500/30",
confirmations >= CONFIRMATION_THRESHOLDS.CONFIRMED && "bg-emerald-500/20 text-emerald-400 border-emerald-500/30"
)}
>
{getConfirmationStatus(confirmations)}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>{confirmations} block confirmations</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<p className="text-[10px] text-white/40 uppercase tracking-widest font-medium">
{confirmations} confirmations
</p>
</div>
)}
</DialogHeader>

{txHash && (
Expand Down
5 changes: 3 additions & 2 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import NetworkMismatchBanner from '@/components/common/NetworkMismatchBanner';
import StellarConnectionQualityBadge from '@/components/common/StellarConnectionQualityBadge';
import { useNetworkMismatch } from '@/hooks/useNetworkMismatch';
import showToast from '@/utils/toast.util';
import { getSignatureErrorMessage } from '@/utils/errorHandling.utils';
import { formatCompactNumber, formatNumber } from '@/utils/numberFormat.utils';
import PrecisionModeToggle, {
type PrecisionMode,
Expand Down Expand Up @@ -478,9 +479,9 @@ function LandingPage() {
: `Holdings refreshed: -${formatNumber(amount)} keys.`
);
setTradeDialogOpen(false);
} catch {
} catch (error) {
setFeaturedHoldings(previousHoldings);
showToast.error('Trade failed. Holdings have been restored.');
showToast.error(getSignatureErrorMessage(error));
} finally {
setTradeSubmitting(false);
}
Expand Down
55 changes: 55 additions & 0 deletions src/utils/errorHandling.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Wallet and Signature error handling utilities
*/

/**
* Standard wallet error types and their corresponding user-friendly messages
*/
export const WALLET_ERROR_COPY = {
SIGNATURE_REJECTED: "Signature request was declined. Please try again when you're ready to confirm.",
SIGNATURE_FAILED: "The signature request failed. Please ensure your wallet is unlocked and try again.",
GENERIC_TRANSACTION_FAILED: "Transaction failed. Please check your balance or connection and try again.",
};

interface WalletError {
message?: string;
code?: number | string;
}

/**
* Determines if an error is a user rejection of a signature or transaction
*
* @param error - The error object to check
* @returns boolean
*/
export function isUserRejection(error: unknown): boolean {
if (!error || typeof error !== 'object') return false;

const err = error as WalletError;
// Check common error codes and names across different wallets/libraries
const errorMessage = (err.message || '').toLowerCase();
const errorCode = err.code;

return (
errorCode === 4001 || // EIP-1193 userRejectedRequest
errorCode === 'ACTION_REJECTED' || // ethers.js
errorMessage.includes('user rejected') ||
errorMessage.includes('declined') ||
errorMessage.includes('cancelled')
);
}

/**
* Gets a human-readable error message for a wallet signature failure
*
* @param error - The error object
* @returns Targeted error message
*/
export function getSignatureErrorMessage(error: unknown): string {
if (isUserRejection(error)) {
return WALLET_ERROR_COPY.SIGNATURE_REJECTED;
}

// For other failures that aren't explicit rejections
return WALLET_ERROR_COPY.SIGNATURE_FAILED;
}
Loading
Loading