diff --git a/frontend/src/components/auth/export-passport-dialog.tsx b/frontend/src/components/auth/export-passport-dialog.tsx new file mode 100755 index 0000000..c03fc95 --- /dev/null +++ b/frontend/src/components/auth/export-passport-dialog.tsx @@ -0,0 +1,173 @@ +'use client'; + +import React, { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { PassportBrowserIO } from '@/lib/wasm-browser'; + +interface ExportPassportDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + passportId: string | null; +} + +export function ExportPassportDialog({ + open, + onOpenChange, + passportId +}: ExportPassportDialogProps) { + const [formData, setFormData] = useState({ + password: '', + confirmPassword: '', + filename: '' + }); + const [isExporting, setIsExporting] = useState(false); + + const handleFormChange = (field: keyof typeof formData) => (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [field]: e.target.value + })); + }; + + const handleExportSubmit = async () => { + if (!passportId) { + alert('No passport found to export'); + return; + } + + // Validate form + if (!formData.password) { + alert('Password is required'); + return; + } + + if (formData.password !== formData.confirmPassword) { + alert('Passwords do not match'); + return; + } + + if (!formData.filename) { + alert('Filename is required'); + return; + } + + // Ensure filename has .spf extension + let filename = formData.filename; + if (!filename.toLowerCase().endsWith('.spf')) { + filename += '.spf'; + } + + setIsExporting(true); + try { + // Export the passport using browser I/O operations + await PassportBrowserIO.exportPassport(passportId, formData.password, filename); + + // Reset form and close dialog + setFormData({ + password: '', + confirmPassword: '', + filename: '' + }); + onOpenChange(false); + } catch (error) { + console.error('Failed to export passport:', error); + alert('Failed to export passport. Please try again.'); + } finally { + setIsExporting(false); + } + }; + + const handleClose = () => { + // Reset form when closing + setFormData({ + password: '', + confirmPassword: '', + filename: '' + }); + onOpenChange(false); + }; + + return ( + + + + Export Passport + + Enter a password to encrypt the exported passport file and choose a filename. + + + +
+
+ + +
+ +
+ + + {formData.password !== formData.confirmPassword && formData.confirmPassword && ( +

Passwords do not match

+ )} +
+ +
+ + +

+ File will be saved with .spf extension +

+
+
+ + +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/auth/user-profile-dialog.tsx b/frontend/src/components/auth/user-profile-dialog.tsx new file mode 100755 index 0000000..91ccce2 --- /dev/null +++ b/frontend/src/components/auth/user-profile-dialog.tsx @@ -0,0 +1,465 @@ +'use client'; + +import React, { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import type { UserIdentity, UserProfile, UserPreferences } from '@/lib/auth/types'; + +interface UserProfileDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + mode: 'create' | 'edit'; + profile?: UserProfile | null; + onSubmit: (identity: UserIdentity, preferences: UserPreferences, showDateOfBirth: boolean, hubDid?: string) => Promise; + isLoading: boolean; +} + +export function UserProfileDialog({ + open, + onOpenChange, + mode, + profile, + onSubmit, + isLoading +}: UserProfileDialogProps) { + const [activeTab, setActiveTab] = useState<'identity' | 'preferences'>('identity'); + + // Form fields for identity + const [identityData, setIdentityData] = useState>({ + handle: '', + display_name: '', + first_name: '', + last_name: '', + email: '', + avatar_url: '', + bio: '', + }); + + // Form fields for preferences + const [preferencesData, setPreferencesData] = useState>({ + theme: 'light', + language: 'en', + notifications_enabled: true, + privacy_level: 'standard', + auto_sync: false, + }); + + const [showDateOfBirth, setShowDateOfBirth] = useState(false); + const [hubDid, setHubDid] = useState(''); + + // Initialize form with profile data when in edit mode + React.useEffect(() => { + if (mode === 'edit' && profile) { + setIdentityData({ + handle: profile.identity.handle || '', + display_name: profile.identity.display_name || '', + first_name: profile.identity.first_name || '', + last_name: profile.identity.last_name || '', + email: profile.identity.email || '', + avatar_url: profile.identity.avatar_url || '', + bio: profile.identity.bio || '', + }); + setPreferencesData({ + theme: profile.preferences.theme || 'light', + language: profile.preferences.language || 'en', + notifications_enabled: profile.preferences.notifications_enabled ?? true, + privacy_level: profile.preferences.privacy_level || 'standard', + auto_sync: profile.preferences.auto_sync ?? false, + }); + setShowDateOfBirth(profile.show_date_of_birth || false); + setHubDid(profile.hub_did || ''); + } else { + // Reset form for create mode + setIdentityData({ + handle: '', + display_name: '', + first_name: '', + last_name: '', + email: '', + avatar_url: '', + bio: '', + }); + setPreferencesData({ + theme: 'light', + language: 'en', + notifications_enabled: true, + privacy_level: 'standard', + auto_sync: false, + }); + setShowDateOfBirth(false); + setHubDid(''); + } + }, [mode, profile, open]); + + const handleIdentityChange = (field: keyof UserIdentity) => (e: React.ChangeEvent) => { + setIdentityData(prev => ({ + ...prev, + [field]: e.target.value + })); + }; + + const handlePreferenceChange = (field: keyof UserPreferences) => (e: React.ChangeEvent) => { + const value = e.target.type === 'checkbox' ? (e.target as HTMLInputElement).checked : e.target.value; + setPreferencesData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleSubmit = async () => { + if (!identityData.display_name) { + alert('Display name is required'); + return; + } + + await onSubmit( + identityData as UserIdentity, + preferencesData as UserPreferences, + showDateOfBirth, + hubDid || undefined + ); + }; + + const handleClose = () => { + onOpenChange(false); + }; + + const isFormValid = identityData.display_name && identityData.display_name.trim() !== ''; + + return ( + + + + + {mode === 'create' ? 'Create New User Profile' : `Edit Profile: ${profile?.identity.display_name || 'Unnamed Profile'}`} + + + {mode === 'create' + ? 'Create a new user profile with identity information and preferences.' + : 'Update the user profile information and preferences.' + } + + + + {/* Tab Navigation */} +
+ +
+ +
+ {/* Identity Tab */} + {activeTab === 'identity' && ( +
+
+

Basic Information

+
+
+ + +

+ This is how others will see you +

+
+ +
+ + +

+ A unique identifier for your profile +

+
+
+ + {/* Hub DID Field - Full Width on its own line */} +
+ + setHubDid(e.target.value)} + className="w-full" + /> +

+ The DID of the hub this profile is affiliated with (leave empty for default profile) +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +

+ URL to your profile picture +

+
+
+
+ +
+

Additional Information

+
+ + +

+ A brief description about yourself +

+
+
+ +
+

Privacy Settings

+
+ setShowDateOfBirth(e.target.checked)} + className="h-4 w-4 text-blue-600" + /> + +
+

+ When enabled, your date of birth will be visible to others +

+
+
+ )} + + {/* Preferences Tab */} + {activeTab === 'preferences' && ( +
+
+

Appearance

+
+
+ + +
+ +
+ + +
+
+
+ +
+

Notifications

+
+
+
+ +

+ Receive notifications for important updates +

+
+ handlePreferenceChange('notifications_enabled')(e)} + className="h-4 w-4 text-blue-600" + /> +
+
+
+ +
+

Privacy & Security

+
+
+ + +

+ Controls who can see your profile information +

+
+ +
+
+ +

+ Automatically sync your profile across devices +

+
+ handlePreferenceChange('auto_sync')(e)} + className="h-4 w-4 text-blue-600" + /> +
+
+
+
+ )} +
+ + +
+
+ {activeTab === 'preferences' && ( + + )} + {activeTab === 'identity' && ( + + )} +
+
+ + +
+
+
+
+
+ ); +} \ No newline at end of file