src/features/account/profile-dialogs.tsx
Rama main · réplica de lectura del repositorio original.
import { KeyRound, Mail, Upload, UserRound } from 'lucide-react'
import type { FormEvent } from 'react'
import { Field, TextInput } from '@/components/product-form'
import { Button } from '@/components/ui/button'
import { Status } from '@/components/ui/status'
import { tt } from '@/lib/i18n'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './account-drawer'
import type { UserProfile } from './types'
type ProfileDialog = 'avatar' | 'displayName' | 'username' | 'email' | 'password' | null
export function ProfileDialogs({
avatarPreview,
changeEmail,
changePassword,
closeDialog,
confirmPassword,
currentPassword,
dialog,
displayName,
email,
emailOtp,
emailStep,
newPassword,
passwordError,
revokeSessions = true,
setRevokeSessions,
profile,
saveProfile,
setConfirmPassword,
setCurrentPassword,
setDisplayName,
setEmail,
setEmailOtp,
setEmailStep,
setNewPassword,
setUsername,
uploadAvatar,
username,
}: {
avatarPreview: string
changeEmail: (event: FormEvent) => void
changePassword: (event: FormEvent) => void
closeDialog: () => void
confirmPassword: string
currentPassword: string
dialog: ProfileDialog
displayName: string
email: string
emailOtp: string
emailStep: 'request' | 'confirm'
newPassword: string
passwordError: string | null
revokeSessions?: boolean
setRevokeSessions?: (value: boolean) => void
profile: UserProfile
saveProfile: (event: FormEvent) => void
setConfirmPassword: (value: string) => void
setCurrentPassword: (value: string) => void
setDisplayName: (value: string) => void
setEmail: (value: string) => void
setEmailOtp: (value: string) => void
setEmailStep: (value: 'request' | 'confirm') => void
setNewPassword: (value: string) => void
setUsername: (value: string) => void
uploadAvatar: (file: File | undefined) => void
username: string
}) {
return (
<>
<ProfileImageDialog
avatarPreview={avatarPreview}
closeDialog={closeDialog}
dialog={dialog}
displayName={displayName}
saveProfile={saveProfile}
setDisplayName={setDisplayName}
uploadAvatar={uploadAvatar}
/>
<UsernameDialog
closeDialog={closeDialog}
dialog={dialog}
saveProfile={saveProfile}
setUsername={setUsername}
username={username}
/>
<EmailDialog
changeEmail={changeEmail}
closeDialog={closeDialog}
dialog={dialog}
email={email}
emailOtp={emailOtp}
emailStep={emailStep}
setEmail={setEmail}
setEmailOtp={setEmailOtp}
setEmailStep={setEmailStep}
/>
<PasswordDialog
changePassword={changePassword}
closeDialog={closeDialog}
confirmPassword={confirmPassword}
currentPassword={currentPassword}
dialog={dialog}
newPassword={newPassword}
passwordError={passwordError}
revokeSessions={revokeSessions}
setRevokeSessions={setRevokeSessions}
profile={profile}
setConfirmPassword={setConfirmPassword}
setCurrentPassword={setCurrentPassword}
setNewPassword={setNewPassword}
/>
</>
)
}
function ProfileImageDialog({
avatarPreview,
closeDialog,
dialog,
displayName,
saveProfile,
setDisplayName,
uploadAvatar,
}: {
avatarPreview: string
closeDialog: () => void
dialog: ProfileDialog
displayName: string
saveProfile: (event: FormEvent) => void
setDisplayName: (value: string) => void
uploadAvatar: (file: File | undefined) => void
}) {
return (
<Dialog onOpenChange={(open) => !open && closeDialog()} open={dialog === 'avatar' || dialog === 'displayName'}>
<DialogContent>
<form onSubmit={saveProfile}>
<DialogHeader>
<DialogTitle>{dialog === 'avatar' ? tt('Change avatar') : tt('Edit display name')}</DialogTitle>
<DialogDescription>{tt('Update the profile shown across trusted applications.')}</DialogDescription>
</DialogHeader>
<div className="dialogFormBody formStack">
{dialog === 'avatar' ? (
<div className="avatarUploadControl">
{avatarPreview ? (
<img alt="" className="assetPreview" src={avatarPreview} width="56" height="56" />
) : (
<div className="assetPreview" aria-hidden="true">
<UserRound size={28} />
</div>
)}
<div className="avatarUploadMeta">
<span className="avatarUploadLabel">{tt('Avatar image')}</span>
<span className="avatarUploadHelp">{tt('PNG, JPEG, or WebP up to 2 MB.')}</span>
<button
className="avatarUploadButton"
onClick={() => document.getElementById('account-avatar-upload')?.click()}
type="button"
>
<Upload size={16} /> {tt('Upload image')}
</button>
<input
accept="image/png,image/jpeg,image/webp"
aria-label={tt('Avatar image')}
className="visuallyHidden"
id="account-avatar-upload"
name="avatar"
onChange={(event) => uploadAvatar(event.currentTarget.files?.[0])}
tabIndex={-1}
type="file"
/>
</div>
</div>
) : (
<Field label={tt('Display name')}>
<TextInput
autoComplete="name"
name="display-name"
onChange={(event) => setDisplayName(event.target.value)}
required
value={displayName}
/>
</Field>
)}
</div>
<DialogFooter>
<Button onClick={closeDialog} type="button" variant="secondary">
{tt('Cancel')}
</Button>
<Button type="submit">{dialog === 'avatar' ? tt('Save avatar') : tt('Save display name')}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
function UsernameDialog({
closeDialog,
dialog,
saveProfile,
setUsername,
username,
}: {
closeDialog: () => void
dialog: ProfileDialog
saveProfile: (event: FormEvent) => void
setUsername: (value: string) => void
username: string
}) {
return (
<Dialog onOpenChange={(open) => !open && closeDialog()} open={dialog === 'username'}>
<DialogContent>
<form onSubmit={saveProfile}>
<DialogHeader>
<DialogTitle>{tt('Edit username')}</DialogTitle>
<DialogDescription>{tt('Choose the username associated with this hosted account.')}</DialogDescription>
</DialogHeader>
<div className="dialogFormBody formStack">
<Field label={tt('Username')}>
<TextInput
autoComplete="username"
name="username"
onChange={(event) => setUsername(event.target.value)}
value={username}
/>
</Field>
</div>
<DialogFooter>
<Button onClick={closeDialog} type="button" variant="secondary">
{tt('Cancel')}
</Button>
<Button type="submit" variant="secondary">
{tt('Save identifiers')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
function EmailDialog({
changeEmail,
closeDialog,
dialog,
email,
emailOtp,
emailStep,
setEmail,
setEmailOtp,
setEmailStep,
}: {
changeEmail: (event: FormEvent) => void
closeDialog: () => void
dialog: ProfileDialog
email: string
emailOtp: string
emailStep: 'request' | 'confirm'
setEmail: (value: string) => void
setEmailOtp: (value: string) => void
setEmailStep: (value: 'request' | 'confirm') => void
}) {
return (
<Dialog onOpenChange={(open) => !open && closeDialog()} open={dialog === 'email'}>
<DialogContent>
<form onSubmit={changeEmail}>
<DialogHeader>
<DialogTitle>{tt('Change email')}</DialogTitle>
<DialogDescription>
{emailStep === 'request'
? tt('A verification code will be sent to the new email address.')
: tt('Enter the verification code sent to {{email}}.', { email })}
</DialogDescription>
</DialogHeader>
<div className="dialogFormBody formStack">
{emailStep === 'request' ? (
<Field label={tt('Email')}>
<TextInput
autoComplete="email"
name="email"
onChange={(event) => setEmail(event.target.value)}
required
type="email"
value={email}
/>
</Field>
) : (
<Field label={tt('Verification code')}>
<TextInput
autoComplete="one-time-code"
inputMode="numeric"
name="email-otp"
onChange={(event) => setEmailOtp(event.target.value)}
required
value={emailOtp}
/>
</Field>
)}
</div>
<DialogFooter>
<Button onClick={closeDialog} type="button" variant="secondary">
{tt('Cancel')}
</Button>
{emailStep === 'confirm' ? (
<Button
onClick={() => {
setEmailStep('request')
setEmailOtp('')
}}
type="button"
variant="secondary"
>
{tt('Back')}
</Button>
) : null}
<Button type="submit" variant="secondary">
<Mail size={18} />
{emailStep === 'request' ? tt('Send code') : tt('Verify code')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
function PasswordDialog({
changePassword,
closeDialog,
confirmPassword,
currentPassword,
dialog,
newPassword,
passwordError,
revokeSessions = true,
setRevokeSessions,
profile,
setConfirmPassword,
setCurrentPassword,
setNewPassword,
}: {
changePassword: (event: FormEvent) => void
closeDialog: () => void
confirmPassword: string
currentPassword: string
dialog: ProfileDialog
newPassword: string
passwordError: string | null
revokeSessions?: boolean
setRevokeSessions?: (value: boolean) => void
profile: UserProfile
setConfirmPassword: (value: string) => void
setCurrentPassword: (value: string) => void
setNewPassword: (value: string) => void
}) {
return (
<Dialog onOpenChange={(open) => !open && closeDialog()} open={dialog === 'password'}>
<DialogContent>
<form onSubmit={changePassword}>
<DialogHeader>
<DialogTitle>{tt('Change password')}</DialogTitle>
<DialogDescription>
{tt('Rotates the hosted sign-in password and revokes other sessions.')}
</DialogDescription>
</DialogHeader>
<div className="dialogFormBody formStack">
{passwordError ? <Status tone="error">{passwordError}</Status> : null}
<input autoComplete="username" hidden name="username" readOnly type="text" value={profile.email} />
<Field label={tt('Current password')}>
<TextInput
autoComplete="current-password"
name="current-password"
onChange={(event) => setCurrentPassword(event.target.value)}
required
type="password"
value={currentPassword}
/>
</Field>
<Field label={tt('New password')}>
<TextInput
autoComplete="new-password"
minLength={8}
name="new-password"
onChange={(event) => setNewPassword(event.target.value)}
required
type="password"
value={newPassword}
/>
</Field>
<Field label={tt('Confirm new password')}>
<TextInput
autoComplete="new-password"
minLength={8}
name="confirm-password"
onChange={(event) => setConfirmPassword(event.target.value)}
required
type="password"
value={confirmPassword}
/>
</Field>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={revokeSessions}
onChange={(event) => setRevokeSessions?.(event.target.checked)}
/>
{tt('Sign out other sessions after changing password')}
</label>
</div>
<DialogFooter>
<Button onClick={closeDialog} type="button" variant="secondary">
{tt('Cancel')}
</Button>
<Button type="submit" variant="secondary">
<KeyRound size={18} /> {tt('Change password')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}