diff --git a/src/components/common/CreatorCard.tsx b/src/components/common/CreatorCard.tsx index f530242a..a02e8662 100644 --- a/src/components/common/CreatorCard.tsx +++ b/src/components/common/CreatorCard.tsx @@ -7,6 +7,8 @@ import { ShoppingCart, Link as LinkIcon, TrendingUp } from 'lucide-react'; import toast from 'react-hot-toast'; import showToast from '@/utils/toast.util'; import TransactionRetryNotice from '@/components/common/TransactionRetryNotice'; +import TransactionFailureDrawer from '@/components/common/TransactionFailureDrawer'; +import type { TransactionFailureDetails } from '@/components/common/TransactionFailureDrawer'; import CardMetaRow from '@/components/common/CardMetaRow'; import VerifiedBadge from '@/components/common/VerifiedBadge'; import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar'; @@ -30,6 +32,10 @@ const CreatorCard: React.FC = ({ creator, className }) => { const [transactionState, setTransactionState] = useState< 'idle' | 'submitting' | 'failed' | 'success' >('idle'); + const [failureDrawerOpen, setFailureDrawerOpen] = useState(false); + const [failureDetails, setFailureDetails] = useState({ + errorMessage: '', + }); const hasFailedOnceRef = useRef(false); const runPurchaseAttempt = () => { @@ -42,6 +48,18 @@ const CreatorCard: React.FC = ({ creator, className }) => { if (!hasFailedOnceRef.current) { hasFailedOnceRef.current = true; setTransactionState('failed'); + setFailureDetails({ + errorMessage: 'Transaction failed: Insufficient balance to complete the purchase.', + errorCode: 'ERR_INSUFFICIENT_BALANCE', + txHash: '0xabcd1234...failed', + developerDetails: { + requiredAmount: '0.05 ETH', + availableBalance: '0.02 ETH', + gasEstimate: '0.001 ETH', + }, + timestamp: Date.now(), + }); + setFailureDrawerOpen(true); return; } @@ -234,6 +252,14 @@ const CreatorCard: React.FC = ({ creator, className }) => { onRetry={runPurchaseAttempt} /> )} + + setFailureDrawerOpen(false)} + /> ); }; diff --git a/src/components/common/CreatorOnboardingForm.tsx b/src/components/common/CreatorOnboardingForm.tsx new file mode 100644 index 00000000..77023c7b --- /dev/null +++ b/src/components/common/CreatorOnboardingForm.tsx @@ -0,0 +1,145 @@ +import React, { useState, useEffect } from 'react'; +import { FormInput } from './FormInput'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export interface CreatorOnboardingFormData { + name: string; + email: string; + bio: string; + category: string; +} + +export interface CreatorOnboardingFormProps { + onSubmit?: (data: CreatorOnboardingFormData) => void; + initialData?: Partial; + className?: string; +} + +export const CreatorOnboardingForm: React.FC< + CreatorOnboardingFormProps +> = ({ onSubmit, initialData, className }) => { + const [formData, setFormData] = useState({ + name: initialData?.name || '', + email: initialData?.email || '', + bio: initialData?.bio || '', + category: initialData?.category || '', + }); + + const [isDirty, setIsDirty] = useState(false); + const [touched, setTouched] = useState>({}); + + const initialDataRef = React.useRef(formData); + + useEffect(() => { + initialDataRef.current = { + name: initialData?.name || '', + email: initialData?.email || '', + bio: initialData?.bio || '', + category: initialData?.category || '', + }; + setFormData(initialDataRef.current); + setIsDirty(false); + }, [initialData]); + + useEffect(() => { + const hasChanged = JSON.stringify(formData) !== JSON.stringify(initialDataRef.current); + setIsDirty(hasChanged); + }, [formData]); + + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isDirty) { + e.preventDefault(); + e.returnValue = ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [isDirty]); + + const handleChange = (field: keyof CreatorOnboardingFormData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + setTouched(prev => ({ ...prev, [field]: true })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit?.(formData); + setIsDirty(false); + initialDataRef.current = { ...formData }; + }; + + const handleReset = () => { + if (isDirty && !confirm('Discard unsaved changes?')) { + return; + } + setFormData(initialDataRef.current); + setTouched({}); + setIsDirty(false); + }; + + return ( +
+ handleChange('name', value)} + placeholder="Your creator name" + required + touched={touched.name} + /> + + handleChange('email', value)} + placeholder="your@email.com" + required + touched={touched.email} + /> + + handleChange('bio', value)} + placeholder="Tell us about yourself..." + touched={touched.bio} + rows={4} + /> + + handleChange('category', value)} + placeholder="e.g., Art, Music, Tech" + touched={touched.category} + /> + +
+ + +
+ + {isDirty && ( +
+ You have unsaved changes. They will be lost if you leave this page. +
+ )} + + ); +}; + +export default CreatorOnboardingForm; diff --git a/src/components/common/ProfileTabPill.tsx b/src/components/common/ProfileTabPill.tsx index 6340dcf4..8a18899e 100644 --- a/src/components/common/ProfileTabPill.tsx +++ b/src/components/common/ProfileTabPill.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; @@ -67,6 +67,7 @@ export interface ProfileTabPillGroupProps { activeTab: string; onTabChange: (value: string) => void; className?: string; + enableHashRouting?: boolean; } export const ProfileTabPillGroup: React.FC = ({ @@ -74,7 +75,40 @@ export const ProfileTabPillGroup: React.FC = ({ activeTab, onTabChange, className, + enableHashRouting = false, }) => { + useEffect(() => { + if (!enableHashRouting) return; + + const handleHashChange = () => { + const hash = window.location.hash.slice(1); + const validTab = tabs.find(tab => tab.value === hash); + if (validTab && hash !== activeTab) { + onTabChange(hash); + } + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, [enableHashRouting, tabs, activeTab, onTabChange]); + + const handleTabClick = (value: string) => { + onTabChange(value); + if (enableHashRouting) { + window.location.hash = value; + } + }; + + useEffect(() => { + if (!enableHashRouting) return; + + const hash = window.location.hash.slice(1); + const validTab = tabs.find(tab => tab.value === hash); + if (validTab && hash !== activeTab) { + onTabChange(hash); + } + }, [enableHashRouting, tabs, activeTab, onTabChange]); + return (