Compare commits
No commits in common. "e0122c2c6b18f8b737645f04f0af38413adfa462" and "df8918beac6e7d2e99d3c805c9c90f1bafd82cc2" have entirely different histories.
e0122c2c6b
...
df8918beac
2 changed files with 0 additions and 638 deletions
|
|
@ -1,173 +0,0 @@
|
|||
'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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export Passport</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a password to encrypt the exported passport file and choose a filename.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="text-sm font-medium">
|
||||
Password *
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
value={formData.password}
|
||||
onChange={handleFormChange('password')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="confirmPassword" className="text-sm font-medium">
|
||||
Confirm Password *
|
||||
</label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleFormChange('confirmPassword')}
|
||||
required
|
||||
/>
|
||||
{formData.password !== formData.confirmPassword && formData.confirmPassword && (
|
||||
<p className="text-sm text-red-600">Passwords do not match</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="filename" className="text-sm font-medium">
|
||||
Filename *
|
||||
</label>
|
||||
<Input
|
||||
id="filename"
|
||||
placeholder="Enter filename"
|
||||
value={formData.filename}
|
||||
onChange={handleFormChange('filename')}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
File will be saved with .spf extension
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-end space-x-2 w-full">
|
||||
<Button variant="outline" onClick={handleClose} disabled={isExporting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExportSubmit}
|
||||
disabled={
|
||||
isExporting ||
|
||||
!formData.password ||
|
||||
!formData.confirmPassword ||
|
||||
!formData.filename ||
|
||||
formData.password !== formData.confirmPassword
|
||||
}
|
||||
>
|
||||
{isExporting ? 'Exporting...' : 'Export'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,465 +0,0 @@
|
|||
'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<void>;
|
||||
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<Partial<UserIdentity>>({
|
||||
handle: '',
|
||||
display_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
bio: '',
|
||||
});
|
||||
|
||||
// Form fields for preferences
|
||||
const [preferencesData, setPreferencesData] = useState<Partial<UserPreferences>>({
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
notifications_enabled: true,
|
||||
privacy_level: 'standard',
|
||||
auto_sync: false,
|
||||
});
|
||||
|
||||
const [showDateOfBirth, setShowDateOfBirth] = useState(false);
|
||||
const [hubDid, setHubDid] = useState<string>('');
|
||||
|
||||
// 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<HTMLInputElement>) => {
|
||||
setIdentityData(prev => ({
|
||||
...prev,
|
||||
[field]: e.target.value
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePreferenceChange = (field: keyof UserPreferences) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{mode === 'create' ? 'Create New User Profile' : `Edit Profile: ${profile?.identity.display_name || 'Unnamed Profile'}`}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{mode === 'create'
|
||||
? 'Create a new user profile with identity information and preferences.'
|
||||
: 'Update the user profile information and preferences.'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('identity')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'identity'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Identity Information
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('preferences')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'preferences'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Preferences & Settings
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="py-4">
|
||||
{/* Identity Tab */}
|
||||
{activeTab === 'identity' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Basic Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="display_name" className="text-sm font-medium">
|
||||
Display Name *
|
||||
</Label>
|
||||
<Input
|
||||
id="display_name"
|
||||
placeholder="Your display name"
|
||||
value={identityData.display_name}
|
||||
onChange={handleIdentityChange('display_name')}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
This is how others will see you
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="handle" className="text-sm font-medium">
|
||||
Handle
|
||||
</Label>
|
||||
<Input
|
||||
id="handle"
|
||||
placeholder="Your unique handle"
|
||||
value={identityData.handle}
|
||||
onChange={handleIdentityChange('handle')}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
A unique identifier for your profile
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hub DID Field - Full Width on its own line */}
|
||||
<div className="space-y-2 mt-6">
|
||||
<Label htmlFor="hub_did" className="text-sm font-medium">
|
||||
Hub DID
|
||||
</Label>
|
||||
<Input
|
||||
id="hub_did"
|
||||
placeholder="did:example:123456789"
|
||||
value={hubDid}
|
||||
onChange={(e) => setHubDid(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
The DID of the hub this profile is affiliated with (leave empty for default profile)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="first_name" className="text-sm font-medium">
|
||||
First Name
|
||||
</Label>
|
||||
<Input
|
||||
id="first_name"
|
||||
placeholder="First name"
|
||||
value={identityData.first_name}
|
||||
onChange={handleIdentityChange('first_name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="last_name" className="text-sm font-medium">
|
||||
Last Name
|
||||
</Label>
|
||||
<Input
|
||||
id="last_name"
|
||||
placeholder="Last name"
|
||||
value={identityData.last_name}
|
||||
onChange={handleIdentityChange('last_name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
value={identityData.email}
|
||||
onChange={handleIdentityChange('email')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="avatar_url" className="text-sm font-medium">
|
||||
Avatar URL
|
||||
</Label>
|
||||
<Input
|
||||
id="avatar_url"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
value={identityData.avatar_url}
|
||||
onChange={handleIdentityChange('avatar_url')}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
URL to your profile picture
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Additional Information</h3>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio" className="text-sm font-medium">
|
||||
Bio
|
||||
</Label>
|
||||
<Input
|
||||
id="bio"
|
||||
placeholder="Tell us about yourself..."
|
||||
value={identityData.bio}
|
||||
onChange={handleIdentityChange('bio')}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
A brief description about yourself
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Privacy Settings</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="show_date_of_birth"
|
||||
checked={showDateOfBirth}
|
||||
onChange={(e) => setShowDateOfBirth(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
<Label htmlFor="show_date_of_birth" className="text-sm">
|
||||
Show Date of Birth in Profile
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
When enabled, your date of birth will be visible to others
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preferences Tab */}
|
||||
{activeTab === 'preferences' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Appearance</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="theme" className="text-sm font-medium">
|
||||
Theme
|
||||
</Label>
|
||||
<select
|
||||
id="theme"
|
||||
value={preferencesData.theme}
|
||||
onChange={handlePreferenceChange('theme')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="auto">Auto</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language" className="text-sm font-medium">
|
||||
Language
|
||||
</Label>
|
||||
<select
|
||||
id="language"
|
||||
value={preferencesData.language}
|
||||
onChange={handlePreferenceChange('language')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="ja">Japanese</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Notifications</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="notifications_enabled" className="text-sm font-medium">
|
||||
Enable Notifications
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500">
|
||||
Receive notifications for important updates
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="notifications_enabled"
|
||||
checked={preferencesData.notifications_enabled}
|
||||
onChange={(e) => handlePreferenceChange('notifications_enabled')(e)}
|
||||
className="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Privacy & Security</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="privacy_level" className="text-sm font-medium">
|
||||
Privacy Level
|
||||
</Label>
|
||||
<select
|
||||
id="privacy_level"
|
||||
value={preferencesData.privacy_level}
|
||||
onChange={handlePreferenceChange('privacy_level')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="public">Public</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="private">Private</option>
|
||||
</select>
|
||||
<p className="text-xs text-gray-500">
|
||||
Controls who can see your profile information
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="auto_sync" className="text-sm font-medium">
|
||||
Auto Sync
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500">
|
||||
Automatically sync your profile across devices
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="auto_sync"
|
||||
checked={preferencesData.auto_sync}
|
||||
onChange={(e) => handlePreferenceChange('auto_sync')(e)}
|
||||
className="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="flex space-x-2">
|
||||
{activeTab === 'preferences' && (
|
||||
<Button variant="outline" onClick={() => setActiveTab('identity')}>
|
||||
← Back to Identity
|
||||
</Button>
|
||||
)}
|
||||
{activeTab === 'identity' && (
|
||||
<Button variant="outline" onClick={() => setActiveTab('preferences')}>
|
||||
Continue to Preferences →
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading || !isFormValid}
|
||||
>
|
||||
{isLoading
|
||||
? (mode === 'create' ? 'Creating...' : 'Updating...')
|
||||
: (mode === 'create' ? 'Create Profile' : 'Update Profile')
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue