OpChan/src/pages/ProfilePage.tsx

508 lines
17 KiB
TypeScript
Raw Normal View History

2025-09-05 12:53:15 +05:30
import { useState, useEffect } from 'react';
import { useAuth, useUserActions, useForumActions } from '@/hooks';
2025-09-05 13:41:37 +05:30
import { useAuth as useAuthContext } from '@/contexts/useAuth';
2025-09-05 12:53:15 +05:30
import { useUserDisplay } from '@/hooks';
2025-09-05 14:03:29 +05:30
import { DelegationFullStatus } from '@/lib/delegation';
2025-09-05 12:53:15 +05:30
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { WalletWizard } from '@/components/ui/wallet-wizard';
import {
Loader2,
Wallet,
Hash,
User,
Shield,
CheckCircle,
AlertTriangle,
XCircle,
} from 'lucide-react';
import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
import { useToast } from '@/hooks/use-toast';
export default function ProfilePage() {
const { updateProfile } = useUserActions();
const { refreshData } = useForumActions();
const { toast } = useToast();
// Get current user from auth context for the address
2025-09-05 13:41:37 +05:30
const { currentUser } = useAuth();
const { getDelegationStatus } = useAuthContext();
2025-09-05 14:03:29 +05:30
const [delegationInfo, setDelegationInfo] =
useState<DelegationFullStatus | null>(null);
2025-09-05 12:53:15 +05:30
const address = currentUser?.address;
2025-09-05 14:03:29 +05:30
// Load delegation status
useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
2025-09-05 12:53:15 +05:30
// Get comprehensive user information from the unified hook
const userInfo = useUserDisplay(address || '');
const [isEditing, setIsEditing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [callSign, setCallSign] = useState(currentUser?.callSign || '');
const [displayPreference, setDisplayPreference] = useState(
currentUser?.displayPreference || EDisplayPreference.WALLET_ADDRESS
);
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
// Update local state when user data changes
useEffect(() => {
if (currentUser) {
setCallSign(currentUser.callSign || '');
setDisplayPreference(
currentUser.displayPreference || EDisplayPreference.WALLET_ADDRESS
);
}
}, [currentUser]);
if (!currentUser) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
Please connect your wallet to view your profile.
</div>
</CardContent>
</Card>
</div>
);
}
const handleSave = async () => {
if (!callSign.trim()) {
toast({
title: 'Invalid Input',
description: 'Call sign cannot be empty.',
variant: 'destructive',
});
return;
}
// Basic validation for call sign
if (callSign.length < 3 || callSign.length > 20) {
toast({
title: 'Invalid Call Sign',
description: 'Call sign must be between 3 and 20 characters.',
variant: 'destructive',
});
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(callSign)) {
toast({
title: 'Invalid Call Sign',
description:
'Call sign can only contain letters, numbers, underscores, and hyphens.',
variant: 'destructive',
});
return;
}
setIsSubmitting(true);
try {
const success = await updateProfile({
callSign: callSign.trim(),
displayPreference,
});
if (success) {
await refreshData();
setIsEditing(false);
toast({
title: 'Profile Updated',
description: 'Your profile has been updated successfully.',
});
}
} catch {
toast({
title: 'Update Failed',
description: 'Failed to update profile. Please try again.',
variant: 'destructive',
});
} finally {
setIsSubmitting(false);
}
};
const handleCancel = () => {
setCallSign(currentUser.callSign || '');
setDisplayPreference(currentUser.displayPreference);
setIsEditing(false);
};
const getVerificationIcon = () => {
switch (userInfo.verificationLevel) {
2025-09-05 13:41:37 +05:30
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
2025-09-05 12:53:15 +05:30
return <CheckCircle className="h-4 w-4 text-green-500" />;
2025-09-05 13:41:37 +05:30
case EVerificationStatus.WALLET_CONNECTED:
2025-09-05 12:53:15 +05:30
return <Shield className="h-4 w-4 text-blue-500" />;
2025-09-05 13:41:37 +05:30
case EVerificationStatus.WALLET_UNCONNECTED:
2025-09-05 12:53:15 +05:30
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
default:
return <XCircle className="h-4 w-4 text-red-500" />;
}
};
const getVerificationText = () => {
switch (userInfo.verificationLevel) {
2025-09-05 13:41:37 +05:30
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
return 'Owns ENS or Ordinal';
case EVerificationStatus.WALLET_CONNECTED:
return 'Connected Wallet';
case EVerificationStatus.WALLET_UNCONNECTED:
return 'Unconnected Wallet';
2025-09-05 12:53:15 +05:30
default:
return 'Unknown';
}
};
const getVerificationColor = () => {
switch (userInfo.verificationLevel) {
2025-09-05 13:41:37 +05:30
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
2025-09-05 12:53:15 +05:30
return 'bg-green-100 text-green-800 border-green-200';
2025-09-05 13:41:37 +05:30
case EVerificationStatus.WALLET_CONNECTED:
2025-09-05 12:53:15 +05:30
return 'bg-blue-100 text-blue-800 border-blue-200';
2025-09-05 13:41:37 +05:30
case EVerificationStatus.WALLET_UNCONNECTED:
2025-09-05 12:53:15 +05:30
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
return (
<div className="container mx-auto px-4 py-8 max-w-2xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Profile
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Wallet Information */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Wallet className="h-4 w-4" />
Wallet Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Address
</Label>
<div className="mt-1 font-mono text-sm bg-muted px-3 py-2 rounded-md break-all">
{currentUser.address}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Network
</Label>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline" className="capitalize">
{currentUser.walletType}
</Badge>
</div>
</div>
</div>
</div>
<Separator />
{/* Identity Information */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Hash className="h-4 w-4" />
Identity
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
ENS Name
</Label>
<div className="mt-1 text-sm">
{currentUser.ensDetails?.ensName || 'N/A'}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Current Display Name
</Label>
<div className="mt-1 text-sm font-medium">
{userInfo.displayName}
</div>
</div>
</div>
</div>
<Separator />
{/* Editable Profile Fields */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Profile Settings</h3>
<div className="space-y-4">
<div>
<Label htmlFor="callSign" className="text-sm font-medium">
Call Sign
</Label>
{isEditing ? (
<Input
id="callSign"
value={callSign}
onChange={e => setCallSign(e.target.value)}
placeholder="Enter your call sign"
className="mt-1"
disabled={isSubmitting}
/>
) : (
<div className="mt-1 text-sm bg-muted px-3 py-2 rounded-md">
{userInfo.callSign || currentUser.callSign || 'Not set'}
</div>
)}
<p className="mt-1 text-xs text-muted-foreground">
3-20 characters, letters, numbers, underscores, and hyphens
only
</p>
</div>
<div>
<Label
htmlFor="displayPreference"
className="text-sm font-medium"
>
Display Preference
</Label>
{isEditing ? (
<Select
value={displayPreference}
onValueChange={value =>
setDisplayPreference(value as EDisplayPreference)
}
disabled={isSubmitting}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={EDisplayPreference.CALL_SIGN}>
Call Sign (when available)
</SelectItem>
<SelectItem value={EDisplayPreference.WALLET_ADDRESS}>
Wallet Address
</SelectItem>
</SelectContent>
</Select>
) : (
<div className="mt-1 text-sm bg-muted px-3 py-2 rounded-md">
{(userInfo.displayPreference || displayPreference) ===
EDisplayPreference.CALL_SIGN
? 'Call Sign (when available)'
: 'Wallet Address'}
</div>
)}
</div>
</div>
</div>
<Separator />
{/* Verification Status */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Shield className="h-4 w-4" />
Verification Status
</h3>
<div className="flex items-center gap-3">
{getVerificationIcon()}
<Badge className={getVerificationColor()}>
{getVerificationText()}
</Badge>
</div>
</div>
<Separator />
{/* Delegation Details */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<CheckCircle className="h-4 w-4" />
Key Delegation
</h3>
<div className="space-y-4">
{/* Delegation Status */}
<div className="flex items-center gap-3">
2025-09-05 13:41:37 +05:30
<Badge
2025-09-05 14:03:29 +05:30
variant={delegationInfo?.isValid ? 'default' : 'secondary'}
2025-09-05 13:41:37 +05:30
className={
2025-09-05 14:03:29 +05:30
delegationInfo?.isValid
2025-09-05 13:41:37 +05:30
? 'bg-green-600 hover:bg-green-700'
: ''
}
2025-09-05 12:53:15 +05:30
>
2025-09-05 14:03:29 +05:30
{delegationInfo?.isValid ? 'Active' : 'Inactive'}
2025-09-05 12:53:15 +05:30
</Badge>
2025-09-05 14:03:29 +05:30
{delegationInfo?.isValid && delegationInfo?.timeRemaining && (
2025-09-05 12:53:15 +05:30
<span className="text-sm text-muted-foreground">
{delegationInfo.timeRemaining} remaining
</span>
)}
2025-09-05 14:03:29 +05:30
{!delegationInfo?.isValid && (
2025-09-05 13:41:37 +05:30
<Badge
variant="outline"
className="text-yellow-600 border-yellow-600"
>
2025-09-05 12:53:15 +05:30
Renewal Recommended
</Badge>
)}
2025-09-05 14:03:29 +05:30
{!delegationInfo?.isValid && (
2025-09-05 13:41:37 +05:30
<Badge variant="destructive">Expired</Badge>
2025-09-05 12:53:15 +05:30
)}
</div>
{/* Delegation Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Browser Public Key
</Label>
<div className="mt-1 text-sm font-mono bg-muted px-3 py-2 rounded-md break-all">
2025-09-05 13:41:37 +05:30
{currentUser.browserPubKey
? `${currentUser.browserPubKey.slice(0, 12)}...${currentUser.browserPubKey.slice(-8)}`
: 'Not delegated'}
2025-09-05 12:53:15 +05:30
</div>
</div>
2025-09-05 13:41:37 +05:30
2025-09-05 12:53:15 +05:30
<div>
<Label className="text-sm font-medium text-muted-foreground">
Delegation Signature
</Label>
<div className="mt-1 text-sm">
{currentUser.delegationSignature === 'valid' ? (
2025-09-05 13:41:37 +05:30
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
2025-09-05 12:53:15 +05:30
Valid
</Badge>
) : (
'Not signed'
)}
</div>
</div>
{currentUser.delegationExpiry && (
<div>
<Label className="text-sm font-medium text-muted-foreground">
Expires At
</Label>
<div className="mt-1 text-sm">
{new Date(currentUser.delegationExpiry).toLocaleString()}
</div>
</div>
)}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Last Updated
</Label>
<div className="mt-1 text-sm">
2025-09-05 13:41:37 +05:30
{currentUser.lastChecked
? new Date(currentUser.lastChecked).toLocaleString()
: 'Never'}
2025-09-05 12:53:15 +05:30
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Can Delegate
</Label>
<div className="mt-1 text-sm">
2025-09-05 14:03:29 +05:30
{delegationInfo?.hasDelegation ? (
2025-09-05 13:41:37 +05:30
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
2025-09-05 12:53:15 +05:30
Yes
</Badge>
) : (
2025-09-05 13:41:37 +05:30
<Badge
variant="outline"
className="text-red-600 border-red-600"
>
2025-09-05 12:53:15 +05:30
No
</Badge>
)}
</div>
</div>
</div>
{/* Delegation Actions */}
2025-09-05 14:03:29 +05:30
{delegationInfo?.hasDelegation && (
2025-09-05 12:53:15 +05:30
<div className="pt-2">
2025-09-05 13:41:37 +05:30
<Button
variant="outline"
2025-09-05 12:53:15 +05:30
size="sm"
onClick={() => setWalletWizardOpen(true)}
>
2025-09-05 14:03:29 +05:30
{delegationInfo?.isValid
2025-09-05 13:41:37 +05:30
? 'Renew Delegation'
: 'Delegate Key'}
2025-09-05 12:53:15 +05:30
</Button>
</div>
)}
</div>
</div>
<Separator />
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4">
{isEditing ? (
<>
<Button
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSubmitting}>
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save Changes
</Button>
</>
) : (
<Button onClick={() => setIsEditing(true)}>Edit Profile</Button>
)}
</div>
</CardContent>
</Card>
{/* Wallet Wizard */}
<WalletWizard
open={walletWizardOpen}
onOpenChange={setWalletWizardOpen}
onComplete={() => setWalletWizardOpen(false)}
/>
</div>
);
}