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
26 changes: 26 additions & 0 deletions src/components/common/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +32,10 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
const [transactionState, setTransactionState] = useState<
'idle' | 'submitting' | 'failed' | 'success'
>('idle');
const [failureDrawerOpen, setFailureDrawerOpen] = useState(false);
const [failureDetails, setFailureDetails] = useState<TransactionFailureDetails>({
errorMessage: '',
});
const hasFailedOnceRef = useRef(false);

const runPurchaseAttempt = () => {
Expand All @@ -42,6 +48,18 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ 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;
}

Expand Down Expand Up @@ -234,6 +252,14 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
onRetry={runPurchaseAttempt}
/>
)}

<TransactionFailureDrawer
open={failureDrawerOpen}
onOpenChange={setFailureDrawerOpen}
failureDetails={failureDetails}
onRetry={runPurchaseAttempt}
onDismiss={() => setFailureDrawerOpen(false)}
/>
</div>
);
};
Expand Down
145 changes: 145 additions & 0 deletions src/components/common/CreatorOnboardingForm.tsx
Original file line number Diff line number Diff line change
@@ -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<CreatorOnboardingFormData>;
className?: string;
}

export const CreatorOnboardingForm: React.FC<
CreatorOnboardingFormProps
> = ({ onSubmit, initialData, className }) => {
const [formData, setFormData] = useState<CreatorOnboardingFormData>({
name: initialData?.name || '',
email: initialData?.email || '',
bio: initialData?.bio || '',
category: initialData?.category || '',
});

const [isDirty, setIsDirty] = useState(false);
const [touched, setTouched] = useState<Record<string, boolean>>({});

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 (
<form onSubmit={handleSubmit} className={cn('space-y-6', className)}>
<FormInput
label="Creator Name"
value={formData.name}
onChange={value => handleChange('name', value)}
placeholder="Your creator name"
required
touched={touched.name}
/>

<FormInput
label="Email"
type="email"
value={formData.email}
onChange={value => handleChange('email', value)}
placeholder="your@email.com"
required
touched={touched.email}
/>

<FormInput
label="Bio"
type="textarea"
value={formData.bio}
onChange={value => handleChange('bio', value)}
placeholder="Tell us about yourself..."
touched={touched.bio}
rows={4}
/>

<FormInput
label="Category"
value={formData.category}
onChange={value => handleChange('category', value)}
placeholder="e.g., Art, Music, Tech"
touched={touched.category}
/>

<div className="flex gap-3 pt-4">
<Button type="submit" className="flex-1">
Save Profile
</Button>
<Button
type="button"
variant="outline"
onClick={handleReset}
disabled={!isDirty}
>
Discard
</Button>
</div>

{isDirty && (
<div className="rounded-lg bg-amber-500/10 border border-amber-500/30 px-4 py-3 text-sm text-amber-100">
You have unsaved changes. They will be lost if you leave this page.
</div>
)}
</form>
);
};

export default CreatorOnboardingForm;
38 changes: 36 additions & 2 deletions src/components/common/ProfileTabPill.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -67,14 +67,48 @@ export interface ProfileTabPillGroupProps {
activeTab: string;
onTabChange: (value: string) => void;
className?: string;
enableHashRouting?: boolean;
}

export const ProfileTabPillGroup: React.FC<ProfileTabPillGroupProps> = ({
tabs,
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 (
<nav
role="tablist"
Expand All @@ -88,7 +122,7 @@ export const ProfileTabPillGroup: React.FC<ProfileTabPillGroupProps> = ({
aria-selected={activeTab === tab.value}
isActive={activeTab === tab.value}
icon={tab.icon}
onClick={() => onTabChange(tab.value)}
onClick={() => handleTabClick(tab.value)}
>
{tab.label}
</ProfileTabPill>
Expand Down
Loading
Loading