@contentgrowth/content-auth 0.4.9 → 0.5.0

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.
Files changed (28) hide show
  1. package/README.md +68 -2
  2. package/dist/{chunk-CTASTCWI.js → chunk-3HNFZJ7S.js} +6 -6
  3. package/dist/chunk-CBNSTIX6.js +17 -0
  4. package/dist/chunk-F2G7XJIZ.js +17 -0
  5. package/dist/frontend/astro.d.ts +6 -0
  6. package/dist/frontend/astro.js +9 -0
  7. package/dist/frontend/client.d.ts +54 -18
  8. package/dist/frontend/clients/astro-client.d.ts +3134 -0
  9. package/dist/frontend/clients/astro-client.js +9 -0
  10. package/dist/frontend/clients/vue-client.d.ts +3520 -0
  11. package/dist/frontend/clients/vue-client.js +9 -0
  12. package/dist/frontend/components/astro/astro/AuthForm.astro +389 -0
  13. package/dist/frontend/components/astro/astro/ForgotPasswordForm.astro +127 -0
  14. package/dist/frontend/components/astro/astro/Organization.astro +254 -0
  15. package/dist/frontend/components/astro/astro/PasswordChanger.astro +128 -0
  16. package/dist/frontend/components/astro/astro/ProfileEditor.astro +133 -0
  17. package/dist/frontend/components/astro/astro/ResetPasswordForm.astro +172 -0
  18. package/dist/frontend/components/vue/vue/AuthForm.vue +337 -0
  19. package/dist/frontend/components/vue/vue/ForgotPasswordForm.vue +108 -0
  20. package/dist/frontend/components/vue/vue/Organization.vue +215 -0
  21. package/dist/frontend/components/vue/vue/PasswordChanger.vue +115 -0
  22. package/dist/frontend/components/vue/vue/ProfileEditor.vue +112 -0
  23. package/dist/frontend/components/vue/vue/ResetPasswordForm.vue +150 -0
  24. package/dist/frontend/index.js +1 -1
  25. package/dist/frontend/vue.d.ts +12 -0
  26. package/dist/frontend/vue.js +23 -0
  27. package/dist/index.js +1 -1
  28. package/package.json +32 -6
@@ -0,0 +1,254 @@
1
+ ---
2
+ // CreateOrganizationForm Component
3
+ export interface CreateOrganizationFormProps {
4
+ baseUrl?: string;
5
+ className?: string;
6
+ }
7
+
8
+ // OrganizationSwitcher Component
9
+ export interface OrganizationSwitcherProps {
10
+ baseUrl?: string;
11
+ currentOrgId?: string;
12
+ className?: string;
13
+ }
14
+
15
+ // InviteMemberForm Component
16
+ export interface InviteMemberFormProps {
17
+ baseUrl?: string;
18
+ className?: string;
19
+ }
20
+
21
+ export type Props =
22
+ | { component: 'create'; props?: CreateOrganizationFormProps }
23
+ | { component: 'switcher'; props?: OrganizationSwitcherProps }
24
+ | { component: 'invite'; props?: InviteMemberFormProps };
25
+
26
+ const { component, props = {} } = Astro.props;
27
+ const baseUrl = (props as any).baseUrl || '';
28
+ const className = (props as any).className || '';
29
+ const currentOrgId = (props as any).currentOrgId || '';
30
+ ---
31
+
32
+ {component === 'create' && (
33
+ <form class={`ca-form ${className}`} data-create-org-form data-base-url={baseUrl}>
34
+ <h3 class="ca-subtitle">Create Organization</h3>
35
+ <div class="ca-input-group">
36
+ <label class="ca-label">Organization Name</label>
37
+ <input
38
+ type="text"
39
+ class="ca-input"
40
+ name="name"
41
+ required
42
+ placeholder="Acme Corp"
43
+ data-name-input
44
+ />
45
+ </div>
46
+ <div class="ca-input-group">
47
+ <label class="ca-label">Slug</label>
48
+ <input
49
+ type="text"
50
+ class="ca-input"
51
+ name="slug"
52
+ required
53
+ placeholder="acme-corp"
54
+ data-slug-input
55
+ />
56
+ </div>
57
+ <div class="ca-error" data-error-message style="display: none;"></div>
58
+ <div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
59
+ Organization created!
60
+ </div>
61
+ <button type="submit" class="ca-button" data-submit-button>
62
+ Create Organization
63
+ </button>
64
+ </form>
65
+ )}
66
+
67
+ {component === 'switcher' && (
68
+ <div class={`ca-org-switcher ${className}`} data-org-switcher data-base-url={baseUrl} data-current-org-id={currentOrgId}>
69
+ <label class="ca-label">Select Organization</label>
70
+ <select class="ca-select" data-org-select disabled>
71
+ <option value="">Loading...</option>
72
+ </select>
73
+ </div>
74
+ )}
75
+
76
+ {component === 'invite' && (
77
+ <form class={`ca-form ${className}`} data-invite-member-form data-base-url={baseUrl}>
78
+ <h3 class="ca-subtitle">Invite Member</h3>
79
+ <div class="ca-input-group">
80
+ <label class="ca-label">Email Address</label>
81
+ <input
82
+ type="email"
83
+ class="ca-input"
84
+ name="email"
85
+ required
86
+ placeholder="colleague@example.com"
87
+ />
88
+ </div>
89
+ <div class="ca-input-group">
90
+ <label class="ca-label">Role</label>
91
+ <select class="ca-select" name="role">
92
+ <option value="member">Member</option>
93
+ <option value="admin">Admin</option>
94
+ <option value="owner">Owner</option>
95
+ </select>
96
+ </div>
97
+ <div class="ca-error" data-error-message style="display: none;"></div>
98
+ <div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
99
+ Invitation sent!
100
+ </div>
101
+ <button type="submit" class="ca-button" data-submit-button>
102
+ Send Invite
103
+ </button>
104
+ </form>
105
+ )}
106
+
107
+ <script>
108
+ import { createClient } from '../../clients/astro-client';
109
+
110
+ // Create Organization Form
111
+ document.querySelectorAll('[data-create-org-form]').forEach((form) => {
112
+ const baseUrl = form.getAttribute('data-base-url') || undefined;
113
+ const client = createClient(baseUrl);
114
+
115
+ const nameInput = form.querySelector('[data-name-input]') as HTMLInputElement;
116
+ const slugInput = form.querySelector('[data-slug-input]') as HTMLInputElement;
117
+ const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
118
+ const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
119
+ const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
120
+
121
+ // Auto-slugify
122
+ nameInput?.addEventListener('input', () => {
123
+ if (!slugInput.value || slugInput.dataset.autoSlug !== 'false') {
124
+ slugInput.value = nameInput.value.toLowerCase().replace(/\s+/g, '-');
125
+ slugInput.dataset.autoSlug = 'true';
126
+ }
127
+ });
128
+
129
+ slugInput?.addEventListener('input', () => {
130
+ slugInput.dataset.autoSlug = 'false';
131
+ });
132
+
133
+ const showError = (message: string) => {
134
+ errorDiv.textContent = message;
135
+ errorDiv.style.display = 'block';
136
+ successDiv.style.display = 'none';
137
+ };
138
+
139
+ const showSuccess = () => {
140
+ successDiv.style.display = 'block';
141
+ errorDiv.style.display = 'none';
142
+ };
143
+
144
+ const setLoading = (loading: boolean) => {
145
+ submitBtn.disabled = loading;
146
+ submitBtn.textContent = loading ? 'Creating...' : 'Create Organization';
147
+ };
148
+
149
+ form.addEventListener('submit', async (e) => {
150
+ e.preventDefault();
151
+
152
+ const formData = new FormData(form as HTMLFormElement);
153
+ const name = formData.get('name') as string;
154
+ const slug = formData.get('slug') as string;
155
+
156
+ setLoading(true);
157
+
158
+ try {
159
+ const result = await client.organization.create({ name, slug });
160
+ if (result.error) throw result.error;
161
+ (form as HTMLFormElement).reset();
162
+ showSuccess();
163
+ } catch (err: any) {
164
+ showError(err.message || 'Failed to create organization');
165
+ } finally {
166
+ setLoading(false);
167
+ }
168
+ });
169
+ });
170
+
171
+ // Organization Switcher
172
+ document.querySelectorAll('[data-org-switcher]').forEach(async (container) => {
173
+ const baseUrl = container.getAttribute('data-base-url') || undefined;
174
+ const currentOrgId = container.getAttribute('data-current-org-id') || '';
175
+ const client = createClient(baseUrl);
176
+ const select = container.querySelector('[data-org-select]') as HTMLSelectElement;
177
+
178
+ try {
179
+ const { data } = await client.organization.list({});
180
+ if (data && data.length > 0) {
181
+ select.innerHTML = '<option value="" disabled>Select an organization</option>';
182
+ data.forEach((org: any) => {
183
+ const option = document.createElement('option');
184
+ option.value = org.id;
185
+ option.textContent = org.name;
186
+ if (org.id === currentOrgId) option.selected = true;
187
+ select.appendChild(option);
188
+ });
189
+ select.disabled = false;
190
+ } else {
191
+ select.innerHTML = '<option value="" disabled>No organizations</option>';
192
+ }
193
+ } catch (err) {
194
+ select.innerHTML = '<option value="" disabled>Failed to load</option>';
195
+ }
196
+
197
+ select?.addEventListener('change', async () => {
198
+ const orgId = select.value;
199
+ if (!orgId) return;
200
+ await client.organization.setActive({ organizationId: orgId });
201
+ window.dispatchEvent(new CustomEvent('org-switched', { detail: { orgId } }));
202
+ });
203
+ });
204
+
205
+ // Invite Member Form
206
+ document.querySelectorAll('[data-invite-member-form]').forEach((form) => {
207
+ const baseUrl = form.getAttribute('data-base-url') || undefined;
208
+ const client = createClient(baseUrl);
209
+
210
+ const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
211
+ const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
212
+ const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
213
+
214
+ const showError = (message: string) => {
215
+ errorDiv.textContent = message;
216
+ errorDiv.style.display = 'block';
217
+ successDiv.style.display = 'none';
218
+ };
219
+
220
+ const showSuccess = () => {
221
+ successDiv.style.display = 'block';
222
+ errorDiv.style.display = 'none';
223
+ };
224
+
225
+ const setLoading = (loading: boolean) => {
226
+ submitBtn.disabled = loading;
227
+ submitBtn.textContent = loading ? 'Sending Invite...' : 'Send Invite';
228
+ };
229
+
230
+ form.addEventListener('submit', async (e) => {
231
+ e.preventDefault();
232
+
233
+ const formData = new FormData(form as HTMLFormElement);
234
+ const email = formData.get('email') as string;
235
+ const role = formData.get('role') as string;
236
+
237
+ setLoading(true);
238
+
239
+ try {
240
+ const result = await client.organization.inviteMember({
241
+ email,
242
+ role: role as any
243
+ });
244
+ if (result.error) throw result.error;
245
+ (form as HTMLFormElement).reset();
246
+ showSuccess();
247
+ } catch (err: any) {
248
+ showError(err.message || 'Invitation failed');
249
+ } finally {
250
+ setLoading(false);
251
+ }
252
+ });
253
+ });
254
+ </script>
@@ -0,0 +1,128 @@
1
+ ---
2
+ export interface Props {
3
+ baseUrl?: string;
4
+ className?: string;
5
+ }
6
+
7
+ const { baseUrl, className = '' } = Astro.props;
8
+ ---
9
+
10
+ <form class={`ca-form ${className}`} data-password-changer data-base-url={baseUrl}>
11
+ <div class="ca-input-group">
12
+ <label class="ca-label" for="current-password">Current Password</label>
13
+ <input
14
+ id="current-password"
15
+ type="password"
16
+ class="ca-input"
17
+ name="currentPassword"
18
+ required
19
+ />
20
+ </div>
21
+
22
+ <div class="ca-input-group">
23
+ <label class="ca-label" for="new-password">New Password</label>
24
+ <input
25
+ id="new-password"
26
+ type="password"
27
+ class="ca-input"
28
+ name="newPassword"
29
+ minlength={8}
30
+ required
31
+ />
32
+ </div>
33
+
34
+ <div class="ca-input-group">
35
+ <label class="ca-label" for="confirm-password">Confirm New Password</label>
36
+ <input
37
+ id="confirm-password"
38
+ type="password"
39
+ class="ca-input"
40
+ name="confirmPassword"
41
+ minlength={8}
42
+ required
43
+ />
44
+ </div>
45
+
46
+ <div class="ca-error" data-error-message style="display: none;"></div>
47
+ <div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
48
+ Password updated successfully!
49
+ </div>
50
+
51
+ <button type="submit" class="ca-button" data-submit-button>
52
+ Update Password
53
+ </button>
54
+ </form>
55
+
56
+ <script>
57
+ import { createClient } from '../../clients/astro-client';
58
+
59
+ document.querySelectorAll('[data-password-changer]').forEach((form) => {
60
+ const baseUrl = form.getAttribute('data-base-url') || undefined;
61
+ const client = createClient(baseUrl);
62
+
63
+ const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
64
+ const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
65
+ const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
66
+
67
+ const showError = (message: string) => {
68
+ errorDiv.textContent = message;
69
+ errorDiv.style.display = 'block';
70
+ successDiv.style.display = 'none';
71
+ };
72
+
73
+ const showSuccess = () => {
74
+ successDiv.style.display = 'block';
75
+ errorDiv.style.display = 'none';
76
+ };
77
+
78
+ const hideMessages = () => {
79
+ errorDiv.style.display = 'none';
80
+ successDiv.style.display = 'none';
81
+ };
82
+
83
+ const setLoading = (loading: boolean) => {
84
+ submitBtn.disabled = loading;
85
+ submitBtn.textContent = loading ? 'Updating...' : 'Update Password';
86
+ };
87
+
88
+ form.addEventListener('submit', async (e) => {
89
+ e.preventDefault();
90
+ hideMessages();
91
+
92
+ const formData = new FormData(form as HTMLFormElement);
93
+ const currentPassword = formData.get('currentPassword') as string;
94
+ const newPassword = formData.get('newPassword') as string;
95
+ const confirmPassword = formData.get('confirmPassword') as string;
96
+
97
+ if (newPassword !== confirmPassword) {
98
+ showError("New passwords don't match");
99
+ return;
100
+ }
101
+
102
+ setLoading(true);
103
+
104
+ try {
105
+ const res = await client.changePassword({
106
+ currentPassword,
107
+ newPassword
108
+ });
109
+
110
+ if (res?.error) {
111
+ throw new Error(res.error.message);
112
+ }
113
+
114
+ // Clear form on success
115
+ (form as HTMLFormElement).reset();
116
+ showSuccess();
117
+ } catch (err: any) {
118
+ if (err?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND' || err?.message?.includes('Credential account not found')) {
119
+ showError("You are logged in via a social provider (e.g. GitHub, Google) and do not have a password set.");
120
+ } else {
121
+ showError(err.message || 'Failed to change password');
122
+ }
123
+ } finally {
124
+ setLoading(false);
125
+ }
126
+ });
127
+ });
128
+ </script>
@@ -0,0 +1,133 @@
1
+ ---
2
+ export interface Props {
3
+ baseUrl?: string;
4
+ defaultName?: string;
5
+ defaultImage?: string;
6
+ className?: string;
7
+ }
8
+
9
+ const {
10
+ baseUrl,
11
+ defaultName = '',
12
+ defaultImage = '',
13
+ className = ''
14
+ } = Astro.props;
15
+ ---
16
+
17
+ <form class={`ca-form ${className}`} data-profile-editor data-base-url={baseUrl}>
18
+ <div class="ca-input-group">
19
+ <label class="ca-label" for="profile-name">Name</label>
20
+ <input
21
+ id="profile-name"
22
+ type="text"
23
+ class="ca-input"
24
+ name="name"
25
+ placeholder="Your Name"
26
+ value={defaultName}
27
+ />
28
+ </div>
29
+
30
+ <div class="ca-input-group">
31
+ <label class="ca-label" for="profile-image">Avatar URL</label>
32
+ <div style="display: flex; gap: 10px; align-items: center;">
33
+ <input
34
+ id="profile-image"
35
+ type="url"
36
+ class="ca-input"
37
+ name="image"
38
+ placeholder="https://example.com/avatar.jpg"
39
+ value={defaultImage}
40
+ style="flex: 1;"
41
+ />
42
+ <div
43
+ data-avatar-preview
44
+ style={`width: 40px; height: 40px; border-radius: 50%; overflow: hidden; flex-shrink: 0; border: 1px solid #eee; display: ${defaultImage ? 'block' : 'none'};`}
45
+ >
46
+ <img
47
+ src={defaultImage}
48
+ alt="Preview"
49
+ style="width: 100%; height: 100%; object-fit: cover;"
50
+ data-avatar-img
51
+ />
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="ca-error" data-error-message style="display: none;"></div>
57
+ <div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
58
+ Profile updated successfully!
59
+ </div>
60
+
61
+ <button type="submit" class="ca-button" data-submit-button>
62
+ Save Profile
63
+ </button>
64
+ </form>
65
+
66
+ <script>
67
+ import { createClient } from '../../clients/astro-client';
68
+
69
+ document.querySelectorAll('[data-profile-editor]').forEach((form) => {
70
+ const baseUrl = form.getAttribute('data-base-url') || undefined;
71
+ const client = createClient(baseUrl);
72
+
73
+ const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
74
+ const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
75
+ const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
76
+ const imageInput = form.querySelector('#profile-image') as HTMLInputElement;
77
+ const avatarPreview = form.querySelector('[data-avatar-preview]') as HTMLDivElement;
78
+ const avatarImg = form.querySelector('[data-avatar-img]') as HTMLImageElement;
79
+
80
+ // Update avatar preview on input change
81
+ imageInput?.addEventListener('input', () => {
82
+ const url = imageInput.value;
83
+ if (url) {
84
+ avatarImg.src = url;
85
+ avatarPreview.style.display = 'block';
86
+ avatarImg.onerror = () => { avatarPreview.style.display = 'none'; };
87
+ } else {
88
+ avatarPreview.style.display = 'none';
89
+ }
90
+ });
91
+
92
+ const showError = (message: string) => {
93
+ errorDiv.textContent = message;
94
+ errorDiv.style.display = 'block';
95
+ successDiv.style.display = 'none';
96
+ };
97
+
98
+ const showSuccess = () => {
99
+ successDiv.style.display = 'block';
100
+ errorDiv.style.display = 'none';
101
+ };
102
+
103
+ const hideMessages = () => {
104
+ errorDiv.style.display = 'none';
105
+ successDiv.style.display = 'none';
106
+ };
107
+
108
+ const setLoading = (loading: boolean) => {
109
+ submitBtn.disabled = loading;
110
+ submitBtn.textContent = loading ? 'Saving...' : 'Save Profile';
111
+ };
112
+
113
+ form.addEventListener('submit', async (e) => {
114
+ e.preventDefault();
115
+ hideMessages();
116
+
117
+ const formData = new FormData(form as HTMLFormElement);
118
+ const name = formData.get('name') as string;
119
+ const image = formData.get('image') as string;
120
+
121
+ setLoading(true);
122
+
123
+ try {
124
+ await client.updateUser({ name, image });
125
+ showSuccess();
126
+ } catch (err: any) {
127
+ showError(err.message || 'Failed to update profile');
128
+ } finally {
129
+ setLoading(false);
130
+ }
131
+ });
132
+ });
133
+ </script>
@@ -0,0 +1,172 @@
1
+ ---
2
+ export interface Props {
3
+ token?: string | null;
4
+ baseUrl?: string;
5
+ backToLoginUrl?: string;
6
+ title?: string;
7
+ width?: 'default' | 'compact' | 'wide';
8
+ }
9
+
10
+ const {
11
+ token,
12
+ baseUrl,
13
+ backToLoginUrl,
14
+ title = 'Set New Password',
15
+ width = 'default'
16
+ } = Astro.props;
17
+
18
+ const widthClass = width === 'compact' ? 'ca-width-compact' : width === 'wide' ? 'ca-width-wide' : 'ca-width-default';
19
+ const hasToken = !!token;
20
+ ---
21
+
22
+ <div class={`ca-container ${widthClass}`} data-reset-password-form data-base-url={baseUrl} data-token={token || ''}>
23
+ <h2 class="ca-title">{title}</h2>
24
+
25
+ {!hasToken ? (
26
+ <!-- Missing/Invalid Token State -->
27
+ <div class="ca-error-message">
28
+ <svg class="ca-error-icon" viewBox="0 0 24 24" width="48" height="48">
29
+ <circle cx="12" cy="12" r="10" fill="#EF4444" />
30
+ <path d="M12 8v4M12 16h.01" stroke="white" stroke-width="2" stroke-linecap="round" />
31
+ </svg>
32
+ <h3 class="ca-error-title">Invalid or Missing Token</h3>
33
+ <p class="ca-error-text">
34
+ The password reset link is invalid or has expired.
35
+ Please request a new password reset.
36
+ </p>
37
+ </div>
38
+ ) : (
39
+ <>
40
+ <p class="ca-subtitle">Enter your new password below.</p>
41
+
42
+ <form class="ca-form" data-form-element>
43
+ <div class="ca-input-group">
44
+ <label class="ca-label" for="password">New Password</label>
45
+ <input
46
+ id="password"
47
+ type="password"
48
+ class="ca-input"
49
+ name="password"
50
+ placeholder="At least 8 characters"
51
+ minlength={8}
52
+ required
53
+ />
54
+ </div>
55
+
56
+ <div class="ca-input-group">
57
+ <label class="ca-label" for="confirmPassword">Confirm Password</label>
58
+ <input
59
+ id="confirmPassword"
60
+ type="password"
61
+ class="ca-input"
62
+ name="confirmPassword"
63
+ placeholder="Confirm your password"
64
+ required
65
+ />
66
+ </div>
67
+
68
+ <div class="ca-error" data-error-message style="display: none;"></div>
69
+
70
+ <button type="submit" class="ca-button" data-submit-button>
71
+ Reset Password
72
+ </button>
73
+ </form>
74
+ </>
75
+ )}
76
+
77
+ {backToLoginUrl && (
78
+ <div class="ca-footer" data-footer>
79
+ <a href={backToLoginUrl} class="ca-link">{hasToken ? 'Back to Sign In' : 'Back to Sign In'}</a>
80
+ </div>
81
+ )}
82
+
83
+ <!-- Success state (hidden initially) -->
84
+ <div data-success-state style="display: none;">
85
+ <div class="ca-success-message">
86
+ <svg class="ca-success-icon" viewBox="0 0 24 24" width="48" height="48">
87
+ <circle cx="12" cy="12" r="10" fill="#10B981" />
88
+ <path d="M8 12l2.5 2.5L16 9" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
89
+ </svg>
90
+ <h3 class="ca-success-title">Password Reset Successful</h3>
91
+ <p class="ca-success-text">
92
+ Your password has been successfully reset. You can now sign in with your new password.
93
+ </p>
94
+ </div>
95
+ {backToLoginUrl && (
96
+ <div class="ca-footer">
97
+ <a href={backToLoginUrl} class="ca-link">Sign In</a>
98
+ </div>
99
+ )}
100
+ </div>
101
+ </div>
102
+
103
+ <script>
104
+ import { createClient } from '../../clients/astro-client';
105
+
106
+ document.querySelectorAll('[data-reset-password-form]').forEach((container) => {
107
+ const baseUrl = container.getAttribute('data-base-url') || undefined;
108
+ const token = container.getAttribute('data-token');
109
+ const client = createClient(baseUrl);
110
+
111
+ if (!token) return; // No token, form is not shown
112
+
113
+ const form = container.querySelector('[data-form-element]') as HTMLFormElement;
114
+ const submitBtn = container.querySelector('[data-submit-button]') as HTMLButtonElement;
115
+ const errorDiv = container.querySelector('[data-error-message]') as HTMLDivElement;
116
+ const successState = container.querySelector('[data-success-state]') as HTMLDivElement;
117
+ const formElements = container.querySelectorAll('.ca-form, .ca-subtitle');
118
+ const footer = container.querySelector('[data-footer]') as HTMLDivElement;
119
+
120
+ const showError = (message: string) => {
121
+ errorDiv.textContent = message;
122
+ errorDiv.style.display = 'block';
123
+ };
124
+
125
+ const hideError = () => {
126
+ errorDiv.style.display = 'none';
127
+ };
128
+
129
+ const setLoading = (loading: boolean) => {
130
+ submitBtn.disabled = loading;
131
+ submitBtn.textContent = loading ? 'Resetting...' : 'Reset Password';
132
+ };
133
+
134
+ form?.addEventListener('submit', async (e) => {
135
+ e.preventDefault();
136
+ hideError();
137
+
138
+ const formData = new FormData(form);
139
+ const password = formData.get('password') as string;
140
+ const confirmPassword = formData.get('confirmPassword') as string;
141
+
142
+ if (password !== confirmPassword) {
143
+ showError('Passwords do not match');
144
+ return;
145
+ }
146
+
147
+ if (password.length < 8) {
148
+ showError('Password must be at least 8 characters');
149
+ return;
150
+ }
151
+
152
+ setLoading(true);
153
+
154
+ try {
155
+ const { error } = await client.resetPassword({
156
+ token,
157
+ newPassword: password
158
+ });
159
+ if (error) throw error;
160
+
161
+ // Show success state
162
+ formElements.forEach(el => (el as HTMLElement).style.display = 'none');
163
+ if (footer) footer.style.display = 'none';
164
+ successState.style.display = 'block';
165
+ } catch (err: any) {
166
+ showError(err.message || 'Failed to reset password');
167
+ } finally {
168
+ setLoading(false);
169
+ }
170
+ });
171
+ });
172
+ </script>