@startsimpli/ui 0.4.4 → 0.4.6

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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,4 @@
1
+ export { ProfileForm } from './profile-form'
2
+ export type { ProfileFormProps, ProfileFormValues } from './profile-form'
3
+ export { ChangePasswordForm } from './change-password-form'
4
+ export type { ChangePasswordFormProps } from './change-password-form'
@@ -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
+ }
@@ -73,3 +73,6 @@ export * from './loading'
73
73
 
74
74
  // Wizard / StepIndicator
75
75
  export * from './wizard'
76
+
77
+ // Account (profile + password forms)
78
+ export * from './account'
@@ -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])