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
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
155 changes: 93 additions & 62 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 All @@ -8,7 +9,6 @@ import VerifiedBadge from '@/components/common/VerifiedBadge';
import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar';
import CreatorBio from '@/components/common/CreatorBio';
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';
import CreatorHandleHoverCard from '@/components/common/CreatorHandleHoverCard';
import { CREATOR_CARD_MEDIA_RADIUS_CLASS } from '@/utils/creatorCardTokens';

interface CreatorProfileHeaderProps {
Expand All @@ -34,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 @@ -76,77 +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"
)}
>
<CreatorHandleHoverCard handle={displayHandle || `@${handle}`}>
{displayHandle || `@${handle}`}
</CreatorHandleHoverCard>
</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
39 changes: 39 additions & 0 deletions src/components/common/PendingTxModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ import { Button } from '@/components/ui/button';
import CircularSpinner from '@/components/common/CircularSpinnerProps';
import { cn } from '@/lib/utils';
import TransactionHashRow from '@/components/common/TransactionHashRow';
import {
getConfirmationStatus,
getConfirmationTone,
} from '@/utils/transaction.utils';
import { Tooltip } from '@/components/ui/tooltip';

const confirmationToneClasses = {
neutral: 'border-slate-500/30 bg-slate-500/20 text-slate-400',
warning: 'border-amber-500/30 bg-amber-500/20 text-amber-300',
success: 'border-emerald-500/30 bg-emerald-500/20 text-emerald-400',
} as const;

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

const PendingTxModal: React.FC<PendingTxModalProps> = ({
Expand All @@ -42,12 +55,20 @@ const PendingTxModal: React.FC<PendingTxModalProps> = ({
explorerUrl,
blockDismissal = false,
action,
confirmations,
}) => {
const handleOpenChange = (next: boolean) => {
if (!next && blockDismissal && isLoading) return;
onOpenChange?.(next);
};

const confirmationStatus =
confirmations !== undefined ? getConfirmationStatus(confirmations) : undefined;
const confirmationTone =
confirmationStatus !== undefined
? getConfirmationTone(confirmationStatus)
: undefined;

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
Expand Down Expand Up @@ -93,6 +114,24 @@ const PendingTxModal: React.FC<PendingTxModalProps> = ({
<DialogDescription className="text-center">
{description}
</DialogDescription>

{confirmationStatus !== undefined && confirmationTone !== undefined && (
<div className="mt-4 flex flex-col items-center gap-2">
<Tooltip content={`${confirmations} block confirmations`}>
<span
className={cn(
'inline-flex items-center rounded-full border px-3 py-1 text-xs font-bold uppercase tracking-wider',
confirmationToneClasses[confirmationTone]
)}
>
{confirmationStatus}
</span>
</Tooltip>
<p className="text-[10px] font-medium uppercase tracking-widest text-white/40">
{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 @@ -31,6 +31,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 @@ -518,9 +519,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
Loading
Loading