@startsimpli/ui 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Button } from '../ui/button'
|
|
5
|
+
import { Input } from '../ui/input'
|
|
6
|
+
import { Label } from '../ui/label'
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
8
|
+
import { Loader2 } from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
export interface ChangePasswordFormProps {
|
|
11
|
+
onSubmit: (values: {
|
|
12
|
+
old_password: string
|
|
13
|
+
new_password: string
|
|
14
|
+
new_password_confirm: string
|
|
15
|
+
}) => Promise<void>
|
|
16
|
+
/** Called after a successful password change (e.g. to sign out) */
|
|
17
|
+
onSuccess?: () => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ChangePasswordForm({ onSubmit, onSuccess, disabled }: ChangePasswordFormProps) {
|
|
22
|
+
const [oldPassword, setOldPassword] = useState('')
|
|
23
|
+
const [newPassword, setNewPassword] = useState('')
|
|
24
|
+
const [confirmPassword, setConfirmPassword] = useState('')
|
|
25
|
+
const [saving, setSaving] = useState(false)
|
|
26
|
+
const [error, setError] = useState<string | null>(null)
|
|
27
|
+
const [success, setSuccess] = useState(false)
|
|
28
|
+
|
|
29
|
+
const isValid =
|
|
30
|
+
oldPassword.length > 0 &&
|
|
31
|
+
newPassword.length >= 8 &&
|
|
32
|
+
newPassword === confirmPassword
|
|
33
|
+
|
|
34
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
setError(null)
|
|
37
|
+
setSuccess(false)
|
|
38
|
+
|
|
39
|
+
if (newPassword !== confirmPassword) {
|
|
40
|
+
setError('New passwords do not match.')
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setSaving(true)
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await onSubmit({
|
|
48
|
+
old_password: oldPassword,
|
|
49
|
+
new_password: newPassword,
|
|
50
|
+
new_password_confirm: confirmPassword,
|
|
51
|
+
})
|
|
52
|
+
setSuccess(true)
|
|
53
|
+
setOldPassword('')
|
|
54
|
+
setNewPassword('')
|
|
55
|
+
setConfirmPassword('')
|
|
56
|
+
onSuccess?.()
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(err instanceof Error ? err.message : 'Failed to change password')
|
|
59
|
+
} finally {
|
|
60
|
+
setSaving(false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Card>
|
|
66
|
+
<CardHeader>
|
|
67
|
+
<CardTitle>Change password</CardTitle>
|
|
68
|
+
<CardDescription>
|
|
69
|
+
Update your password. You will need to sign in again after changing it.
|
|
70
|
+
</CardDescription>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent>
|
|
73
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
74
|
+
<div className="space-y-2">
|
|
75
|
+
<Label htmlFor="cp-old-password">Current password</Label>
|
|
76
|
+
<Input
|
|
77
|
+
id="cp-old-password"
|
|
78
|
+
type="password"
|
|
79
|
+
value={oldPassword}
|
|
80
|
+
onChange={(e) => setOldPassword(e.target.value)}
|
|
81
|
+
disabled={disabled || saving}
|
|
82
|
+
autoComplete="current-password"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="space-y-2">
|
|
87
|
+
<Label htmlFor="cp-new-password">New password</Label>
|
|
88
|
+
<Input
|
|
89
|
+
id="cp-new-password"
|
|
90
|
+
type="password"
|
|
91
|
+
value={newPassword}
|
|
92
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
93
|
+
disabled={disabled || saving}
|
|
94
|
+
autoComplete="new-password"
|
|
95
|
+
/>
|
|
96
|
+
{newPassword.length > 0 && newPassword.length < 8 && (
|
|
97
|
+
<p className="text-xs text-muted-foreground">
|
|
98
|
+
Must be at least 8 characters.
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
<Label htmlFor="cp-confirm-password">Confirm new password</Label>
|
|
105
|
+
<Input
|
|
106
|
+
id="cp-confirm-password"
|
|
107
|
+
type="password"
|
|
108
|
+
value={confirmPassword}
|
|
109
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
110
|
+
disabled={disabled || saving}
|
|
111
|
+
autoComplete="new-password"
|
|
112
|
+
/>
|
|
113
|
+
{confirmPassword.length > 0 && newPassword !== confirmPassword && (
|
|
114
|
+
<p className="text-xs text-destructive">Passwords do not match.</p>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{error && (
|
|
119
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{success && (
|
|
123
|
+
<p className="text-sm text-green-600">Password changed successfully.</p>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<Button type="submit" disabled={disabled || saving || !isValid}>
|
|
127
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
128
|
+
Change password
|
|
129
|
+
</Button>
|
|
130
|
+
</form>
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Button } from '../ui/button'
|
|
5
|
+
import { Input } from '../ui/input'
|
|
6
|
+
import { Label } from '../ui/label'
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
8
|
+
import { Loader2 } from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
export interface ProfileFormValues {
|
|
11
|
+
firstName: string
|
|
12
|
+
lastName: string
|
|
13
|
+
email: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProfileFormProps {
|
|
17
|
+
initialValues: ProfileFormValues
|
|
18
|
+
onSubmit: (values: { first_name: string; last_name: string }) => Promise<void>
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProfileForm({ initialValues, onSubmit, disabled }: ProfileFormProps) {
|
|
23
|
+
const [firstName, setFirstName] = useState(initialValues.firstName)
|
|
24
|
+
const [lastName, setLastName] = useState(initialValues.lastName)
|
|
25
|
+
const [saving, setSaving] = useState(false)
|
|
26
|
+
const [error, setError] = useState<string | null>(null)
|
|
27
|
+
const [success, setSuccess] = useState(false)
|
|
28
|
+
|
|
29
|
+
const hasChanges =
|
|
30
|
+
firstName !== initialValues.firstName || lastName !== initialValues.lastName
|
|
31
|
+
|
|
32
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
setError(null)
|
|
35
|
+
setSuccess(false)
|
|
36
|
+
setSaving(true)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await onSubmit({ first_name: firstName.trim(), last_name: lastName.trim() })
|
|
40
|
+
setSuccess(true)
|
|
41
|
+
setTimeout(() => setSuccess(false), 3000)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
setError(err instanceof Error ? err.message : 'Failed to update profile')
|
|
44
|
+
} finally {
|
|
45
|
+
setSaving(false)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader>
|
|
52
|
+
<CardTitle>Profile</CardTitle>
|
|
53
|
+
<CardDescription>Update your personal information.</CardDescription>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent>
|
|
56
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
57
|
+
<div className="space-y-2">
|
|
58
|
+
<Label htmlFor="profile-email">Email</Label>
|
|
59
|
+
<Input
|
|
60
|
+
id="profile-email"
|
|
61
|
+
type="email"
|
|
62
|
+
value={initialValues.email}
|
|
63
|
+
disabled
|
|
64
|
+
className="bg-muted"
|
|
65
|
+
/>
|
|
66
|
+
<p className="text-xs text-muted-foreground">
|
|
67
|
+
Email cannot be changed.
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="grid grid-cols-2 gap-4">
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
<Label htmlFor="profile-first-name">First name</Label>
|
|
74
|
+
<Input
|
|
75
|
+
id="profile-first-name"
|
|
76
|
+
value={firstName}
|
|
77
|
+
onChange={(e) => setFirstName(e.target.value)}
|
|
78
|
+
disabled={disabled || saving}
|
|
79
|
+
placeholder="First name"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<Label htmlFor="profile-last-name">Last name</Label>
|
|
84
|
+
<Input
|
|
85
|
+
id="profile-last-name"
|
|
86
|
+
value={lastName}
|
|
87
|
+
onChange={(e) => setLastName(e.target.value)}
|
|
88
|
+
disabled={disabled || saving}
|
|
89
|
+
placeholder="Last name"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{error && (
|
|
95
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{success && (
|
|
99
|
+
<p className="text-sm text-green-600">Profile updated.</p>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<Button type="submit" disabled={disabled || saving || !hasChanges}>
|
|
103
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
104
|
+
Save changes
|
|
105
|
+
</Button>
|
|
106
|
+
</form>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
)
|
|
110
|
+
}
|
package/src/components/index.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -81,7 +81,8 @@ export function formatCurrency(amount: number): string {
|
|
|
81
81
|
/**
|
|
82
82
|
* Get initials from a display name (up to 2 characters)
|
|
83
83
|
*/
|
|
84
|
-
export function getInitials(name: string): string {
|
|
84
|
+
export function getInitials(name: string | null | undefined): string {
|
|
85
|
+
if (!name) return '?'
|
|
85
86
|
return name
|
|
86
87
|
.split(' ')
|
|
87
88
|
.map((word) => word[0])
|