diff --git a/src/components/common/CreatorCard.tsx b/src/components/common/CreatorCard.tsx index e03a4ea8..aa67ffc9 100644 --- a/src/components/common/CreatorCard.tsx +++ b/src/components/common/CreatorCard.tsx @@ -3,13 +3,16 @@ import { useAccount } from 'wagmi'; import { Button } from '@/components/ui/button'; import type { Course } from '@/services/course.service'; import { cn } from '@/lib/utils'; -import { ShoppingCart, Wallet, Link as LinkIcon } from 'lucide-react'; +import { ShoppingCart, Link as LinkIcon } from 'lucide-react'; import toast from 'react-hot-toast'; import showToast from '@/utils/toast.util'; import TransactionRetryNotice from '@/components/common/TransactionRetryNotice'; import CardMetaRow from '@/components/common/CardMetaRow'; import VerifiedBadge from '@/components/common/VerifiedBadge'; -import CompactEmptyWalletState from '@/components/common/CompactEmptyWalletState'; +import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar'; +import WalletConnectCalloutBanner from '@/components/common/WalletConnectCalloutBanner'; +import CreatorSocialLinksList from '@/components/common/CreatorSocialLinksList'; +import TransactionStatusIcon from '@/components/common/TransactionStatusIcon'; interface CreatorCardProps { creator: Course; @@ -19,7 +22,7 @@ interface CreatorCardProps { const CreatorCard: React.FC = ({ creator, className }) => { const { isConnected } = useAccount(); const [transactionState, setTransactionState] = useState< - 'idle' | 'submitting' | 'failed' + 'idle' | 'submitting' | 'failed' | 'success' >('idle'); const hasFailedOnceRef = useRef(false); @@ -37,18 +40,21 @@ const CreatorCard: React.FC = ({ creator, className }) => { } hasFailedOnceRef.current = false; - setTransactionState('idle'); + setTransactionState('success'); showToast.transactionSuccess( 'Purchase Successful!', `You successfully bought a key for ${creator.title}` ); + + window.setTimeout(() => { + setTransactionState('idle'); + }, 1800); }, 1500); }; const handleBuy = () => { if (!isConnected) { toast.error('Please connect your wallet to purchase keys', { - icon: , duration: 4000, }); return; @@ -65,10 +71,10 @@ const CreatorCard: React.FC = ({ creator, className }) => { )} >
- {creator.title}
@@ -94,9 +100,21 @@ const CreatorCard: React.FC = ({ creator, className }) => { Handle } - value={creator.socialHandle ? `@${creator.socialHandle}` : 'No public handle'} - valueTitle={creator.socialHandle ? `@${creator.socialHandle}` : undefined} - valueClassName={creator.socialHandle ? 'text-white/75' : 'italic text-white/35'} + value={ + creator.socialHandle + ? `@${creator.socialHandle}` + : 'No public handle' + } + valueTitle={ + creator.socialHandle + ? `@${creator.socialHandle}` + : undefined + } + valueClassName={ + creator.socialHandle + ? 'text-white/75' + : 'italic text-white/35' + } /> = ({ creator, className }) => { valueClassName="font-grotesque text-base font-black text-amber-400" />
+
@@ -115,24 +137,30 @@ const CreatorCard: React.FC = ({ creator, className }) => { disabled={transactionState === 'submitting'} className={cn( 'rounded-xl font-bold cursor-pointer ', - !isConnected && - 'border-white/10 hover:bg-white/5' + !isConnected && 'border-white/10 hover:bg-white/5' )} > + {transactionState === 'success' && ( + + )} + {transactionState === 'submitting' && ( + + )} + {transactionState === 'failed' && ( + + )} - {transactionState === 'submitting' ? 'Processing...' : 'Buy Key'} + {transactionState === 'submitting' + ? 'Processing...' + : transactionState === 'success' + ? 'Completed' + : transactionState === 'failed' + ? 'Retry Purchase' + : 'Buy Key'}
- {!isConnected && ( -
-
- - Wallet Required -
- -
- )} + {!isConnected && } {transactionState === 'failed' && ( { + const cleanName = name.trim(); + if (!cleanName) { + return 'NA'; + } + + const parts = cleanName.split(/\s+/).filter(Boolean); + if (parts.length === 1) { + return parts[0].slice(0, 2).toUpperCase(); + } + + return `${parts[0][0] ?? ''}${parts[1][0] ?? ''}`.toUpperCase(); +}; + +const CreatorInitialsAvatar: React.FC = ({ + name, + imageSrc, + className, + imageClassName, +}) => { + const initials = getInitials(name); + + if (!imageSrc) { + return ( +
+ {initials} +
+ ); + } + + return ( + {name} + ); +}; + +export default CreatorInitialsAvatar; diff --git a/src/components/common/CreatorSocialLinksList.tsx b/src/components/common/CreatorSocialLinksList.tsx new file mode 100644 index 00000000..64854324 --- /dev/null +++ b/src/components/common/CreatorSocialLinksList.tsx @@ -0,0 +1,88 @@ +import type { ComponentType } from 'react'; +import { + Globe, + Instagram, + Link as LinkIcon, + Twitter, + Youtube, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface CreatorSocialLinksListProps { + handle?: string; + className?: string; +} + +interface SocialLinkItem { + id: 'x' | 'instagram' | 'youtube' | 'website'; + label: string; + url: string; + Icon: ComponentType<{ className?: string }>; +} + +const CreatorSocialLinksList: React.FC = ({ + handle, + className, +}) => { + if (!handle) { + return ( +
+ No social links yet +
+ ); + } + + const normalized = handle.replace(/^@/, '').trim(); + const links: SocialLinkItem[] = [ + { + id: 'x', + label: 'X', + url: `https://x.com/${normalized}`, + Icon: Twitter, + }, + { + id: 'instagram', + label: 'Instagram', + url: `https://instagram.com/${normalized}`, + Icon: Instagram, + }, + { + id: 'youtube', + label: 'YouTube', + url: `https://youtube.com/@${normalized}`, + Icon: Youtube, + }, + { + id: 'website', + label: 'Website', + url: `https://${normalized}.link`, + Icon: Globe, + }, + ]; + + return ( + + ); +}; + +export default CreatorSocialLinksList; diff --git a/src/components/common/TransactionStatusIcon.tsx b/src/components/common/TransactionStatusIcon.tsx new file mode 100644 index 00000000..05cc3b84 --- /dev/null +++ b/src/components/common/TransactionStatusIcon.tsx @@ -0,0 +1,65 @@ +import { CheckCircle2, Clock3, XCircle } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +type TransactionStatus = 'success' | 'pending' | 'failed'; + +interface TransactionStatusIconProps { + status: TransactionStatus; + className?: string; +} + +const statusStyles: Record = { + success: 'bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-300/35', + pending: 'bg-amber-500/15 text-amber-300 ring-1 ring-amber-300/35', + failed: 'bg-red-500/15 text-red-300 ring-1 ring-red-300/35', +}; + +const TransactionStatusIcon: React.FC = ({ + status, + className, +}) => { + if (status === 'success') { + return ( + + + + ); + } + + if (status === 'pending') { + return ( + + + + ); + } + + return ( + + + + ); +}; + +export default TransactionStatusIcon; diff --git a/src/components/common/WalletConnectCalloutBanner.tsx b/src/components/common/WalletConnectCalloutBanner.tsx new file mode 100644 index 00000000..84afdfb4 --- /dev/null +++ b/src/components/common/WalletConnectCalloutBanner.tsx @@ -0,0 +1,42 @@ +import { Wallet } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import ConnectWalletButton from '@/components/common/ConnectWalletButton'; + +interface WalletConnectCalloutBannerProps { + title?: string; + description?: string; + className?: string; +} + +const WalletConnectCalloutBanner: React.FC = ({ + title = 'Wallet connection required', + description = 'Connect your wallet to continue with creator key purchases and on-chain actions.', + className, +}) => { + return ( +
+
+
+
+ + Wallet required +
+

+ {title} +

+

{description}

+
+
+ +
+
+
+ ); +}; + +export default WalletConnectCalloutBanner;