Compare commits
2 commits
df8918beac
...
e0122c2c6b
| Author | SHA1 | Date | |
|---|---|---|---|
| e0122c2c6b | |||
| 8b13aa45fe |
2 changed files with 638 additions and 0 deletions
173
frontend/src/components/auth/export-passport-dialog.tsx
Executable file
173
frontend/src/components/auth/export-passport-dialog.tsx
Executable file
|
|
@ -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<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
465
frontend/src/components/auth/user-profile-dialog.tsx
Executable file
465
frontend/src/components/auth/user-profile-dialog.tsx
Executable file
|
|
@ -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<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