@lukoweb/apitogo-module-user-panel 0.2.23 → 0.2.24

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.
@@ -1,279 +1,279 @@
1
- import type { ResolvedProfileContent } from "@lukoweb/apitogo";
2
- import { Head, Link } from "@lukoweb/apitogo/components";
3
- import { useAuth } from "@lukoweb/apitogo/hooks";
4
- import { Button } from "@lukoweb/apitogo/ui/Button.js";
5
- import {
6
- Card,
7
- CardContent,
8
- CardHeader,
9
- CardTitle,
10
- } from "@lukoweb/apitogo/ui/Card.js";
11
- import { Input } from "@lukoweb/apitogo/ui/Input.js";
12
- import { Label } from "@lukoweb/apitogo/ui/Label.js";
13
- import { useEffect, useState } from "react";
14
- import {
15
- useAccountMe,
16
- useChangeAccountPassword,
17
- useDeleteAccount,
18
- useUpdateAccountProfile,
19
- } from "../hooks/useAccountSettings.js";
20
-
21
- export type SettingsPageProps = {
22
- content: ResolvedProfileContent;
23
- };
24
-
25
- export const SettingsPage = ({ content }: SettingsPageProps) => {
26
- const auth = useAuth();
27
- const { data: account, isLoading } = useAccountMe();
28
- const updateProfile = useUpdateAccountProfile();
29
- const changePassword = useChangeAccountPassword();
30
- const deleteAccountMutation = useDeleteAccount();
31
-
32
- const [displayName, setDisplayName] = useState("");
33
- const [currentPassword, setCurrentPassword] = useState("");
34
- const [newPassword, setNewPassword] = useState("");
35
- const [deletePassword, setDeletePassword] = useState("");
36
- const [deleteConfirm, setDeleteConfirm] = useState("");
37
- const [profileMessage, setProfileMessage] = useState<string | null>(null);
38
- const [passwordMessage, setPasswordMessage] = useState<string | null>(null);
39
- const [deleteMessage, setDeleteMessage] = useState<string | null>(null);
40
-
41
- const title = content.title ?? "Settings";
42
- const hasNativePassword = account?.hasNativePassword === true;
43
- const email = account?.email ?? auth.profile?.email ?? "";
44
-
45
- useEffect(() => {
46
- if (account?.displayName !== undefined) {
47
- setDisplayName(account.displayName ?? "");
48
- } else if (auth.profile?.name) {
49
- setDisplayName(auth.profile.name);
50
- }
51
- }, [account?.displayName, auth.profile?.name]);
52
-
53
- if (!auth.isAuthenticated) {
54
- return (
55
- <div className="mx-auto max-w-[760px] px-8 py-7">
56
- <Head>
57
- <title>{title}</title>
58
- </Head>
59
- <h1 className="mb-2 text-[1.6rem] font-bold tracking-[-0.01em]">
60
- {title}
61
- </h1>
62
- <p className="text-muted-foreground">
63
- {auth.isAuthEnabled
64
- ? "Sign in to manage your account settings."
65
- : "Configure authentication to enable account settings."}
66
- </p>
67
- {auth.isAuthEnabled ? (
68
- <Button asChild className="mt-4">
69
- <Link to="/signin">Sign in</Link>
70
- </Button>
71
- ) : null}
72
- </div>
73
- );
74
- }
75
-
76
- return (
77
- <div className="mx-auto max-w-[760px] px-8 py-7">
78
- <Head>
79
- <title>{title}</title>
80
- </Head>
81
-
82
- <h1 className="mb-6 text-[1.6rem] font-bold tracking-[-0.01em]">
83
- {title}
84
- </h1>
85
-
86
- <div className="grid gap-5">
87
- <Card>
88
- <CardHeader>
89
- <CardTitle>Profile</CardTitle>
90
- </CardHeader>
91
- <CardContent className="space-y-4">
92
- <div className="grid gap-4 sm:grid-cols-2">
93
- <div className="space-y-2">
94
- <Label htmlFor="settings-display-name">Full name</Label>
95
- <Input
96
- id="settings-display-name"
97
- value={displayName}
98
- onChange={(event) => setDisplayName(event.target.value)}
99
- disabled={isLoading || updateProfile.isPending}
100
- />
101
- </div>
102
- <div className="space-y-2">
103
- <Label htmlFor="settings-email">Email</Label>
104
- <Input
105
- id="settings-email"
106
- type="email"
107
- value={email}
108
- readOnly
109
- disabled
110
- />
111
- </div>
112
- </div>
113
- {profileMessage ? (
114
- <p className="text-sm text-muted-foreground">{profileMessage}</p>
115
- ) : null}
116
- <Button
117
- size="sm"
118
- disabled={isLoading || updateProfile.isPending}
119
- onClick={() => {
120
- setProfileMessage(null);
121
- updateProfile.mutate(displayName, {
122
- onSuccess: () => setProfileMessage("Profile saved."),
123
- onError: (error) =>
124
- setProfileMessage(
125
- error instanceof Error
126
- ? error.message
127
- : "Unable to save profile.",
128
- ),
129
- });
130
- }}
131
- >
132
- Save changes
133
- </Button>
134
- </CardContent>
135
- </Card>
136
-
137
- {hasNativePassword ? (
138
- <Card>
139
- <CardHeader>
140
- <CardTitle>Password</CardTitle>
141
- </CardHeader>
142
- <CardContent className="space-y-4">
143
- <div className="grid gap-4 sm:grid-cols-2">
144
- <div className="space-y-2">
145
- <Label htmlFor="settings-current-password">
146
- Current password
147
- </Label>
148
- <Input
149
- id="settings-current-password"
150
- type="password"
151
- value={currentPassword}
152
- onChange={(event) => setCurrentPassword(event.target.value)}
153
- autoComplete="current-password"
154
- />
155
- </div>
156
- <div className="space-y-2">
157
- <Label htmlFor="settings-new-password">New password</Label>
158
- <Input
159
- id="settings-new-password"
160
- type="password"
161
- value={newPassword}
162
- onChange={(event) => setNewPassword(event.target.value)}
163
- autoComplete="new-password"
164
- />
165
- </div>
166
- </div>
167
- {passwordMessage ? (
168
- <p className="text-sm text-muted-foreground">
169
- {passwordMessage}
170
- </p>
171
- ) : null}
172
- <Button
173
- variant="outline"
174
- size="sm"
175
- disabled={changePassword.isPending}
176
- onClick={() => {
177
- setPasswordMessage(null);
178
- changePassword.mutate(
179
- { currentPassword, newPassword },
180
- {
181
- onSuccess: () => {
182
- setCurrentPassword("");
183
- setNewPassword("");
184
- setPasswordMessage("Password updated.");
185
- },
186
- onError: (error) =>
187
- setPasswordMessage(
188
- error instanceof Error
189
- ? error.message
190
- : "Unable to update password.",
191
- ),
192
- },
193
- );
194
- }}
195
- >
196
- Update password
197
- </Button>
198
- </CardContent>
199
- </Card>
200
- ) : null}
201
-
202
- <Card className="border-destructive/40">
203
- <CardHeader>
204
- <CardTitle className="text-destructive">Danger zone</CardTitle>
205
- </CardHeader>
206
- <CardContent className="space-y-4">
207
- <p className="text-sm text-muted-foreground">
208
- Deleting your account removes all keys, usage history, and billing
209
- data. This cannot be undone.
210
- </p>
211
- {hasNativePassword ? (
212
- <div className="max-w-sm space-y-2">
213
- <Label htmlFor="settings-delete-password">
214
- Confirm with password
215
- </Label>
216
- <Input
217
- id="settings-delete-password"
218
- type="password"
219
- value={deletePassword}
220
- onChange={(event) => setDeletePassword(event.target.value)}
221
- autoComplete="current-password"
222
- />
223
- </div>
224
- ) : (
225
- <div className="max-w-sm space-y-2">
226
- <Label htmlFor="settings-delete-confirm">
227
- Type DELETE to confirm
228
- </Label>
229
- <Input
230
- id="settings-delete-confirm"
231
- value={deleteConfirm}
232
- onChange={(event) => setDeleteConfirm(event.target.value)}
233
- autoComplete="off"
234
- />
235
- </div>
236
- )}
237
- {deleteMessage ? (
238
- <p className="text-sm text-muted-foreground">{deleteMessage}</p>
239
- ) : null}
240
- <Button
241
- variant="outline"
242
- size="sm"
243
- className="border-destructive text-destructive hover:bg-destructive/10"
244
- disabled={
245
- deleteAccountMutation.isPending ||
246
- (hasNativePassword
247
- ? !deletePassword
248
- : deleteConfirm !== "DELETE")
249
- }
250
- onClick={() => {
251
- if (
252
- !window.confirm(
253
- "Delete your account permanently? This cannot be undone.",
254
- )
255
- ) {
256
- return;
257
- }
258
- setDeleteMessage(null);
259
- deleteAccountMutation.mutate(
260
- hasNativePassword ? deletePassword : undefined,
261
- {
262
- onError: (error) =>
263
- setDeleteMessage(
264
- error instanceof Error
265
- ? error.message
266
- : "Unable to delete account.",
267
- ),
268
- },
269
- );
270
- }}
271
- >
272
- Delete account
273
- </Button>
274
- </CardContent>
275
- </Card>
276
- </div>
277
- </div>
278
- );
279
- };
1
+ import type { ResolvedProfileContent } from "@lukoweb/apitogo";
2
+ import { Head, Link } from "@lukoweb/apitogo/components";
3
+ import { useAuth } from "@lukoweb/apitogo/hooks";
4
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@lukoweb/apitogo/ui/Card.js";
11
+ import { Input } from "@lukoweb/apitogo/ui/Input.js";
12
+ import { Label } from "@lukoweb/apitogo/ui/Label.js";
13
+ import { useEffect, useState } from "react";
14
+ import {
15
+ useAccountMe,
16
+ useChangeAccountPassword,
17
+ useDeleteAccount,
18
+ useUpdateAccountProfile,
19
+ } from "../hooks/useAccountSettings.js";
20
+
21
+ export type SettingsPageProps = {
22
+ content: ResolvedProfileContent;
23
+ };
24
+
25
+ export const SettingsPage = ({ content }: SettingsPageProps) => {
26
+ const auth = useAuth();
27
+ const { data: account, isLoading } = useAccountMe();
28
+ const updateProfile = useUpdateAccountProfile();
29
+ const changePassword = useChangeAccountPassword();
30
+ const deleteAccountMutation = useDeleteAccount();
31
+
32
+ const [displayName, setDisplayName] = useState("");
33
+ const [currentPassword, setCurrentPassword] = useState("");
34
+ const [newPassword, setNewPassword] = useState("");
35
+ const [deletePassword, setDeletePassword] = useState("");
36
+ const [deleteConfirm, setDeleteConfirm] = useState("");
37
+ const [profileMessage, setProfileMessage] = useState<string | null>(null);
38
+ const [passwordMessage, setPasswordMessage] = useState<string | null>(null);
39
+ const [deleteMessage, setDeleteMessage] = useState<string | null>(null);
40
+
41
+ const title = content.title ?? "Settings";
42
+ const hasNativePassword = account?.hasNativePassword === true;
43
+ const email = account?.email ?? auth.profile?.email ?? "";
44
+
45
+ useEffect(() => {
46
+ if (account?.displayName !== undefined) {
47
+ setDisplayName(account.displayName ?? "");
48
+ } else if (auth.profile?.name) {
49
+ setDisplayName(auth.profile.name);
50
+ }
51
+ }, [account?.displayName, auth.profile?.name]);
52
+
53
+ if (!auth.isAuthenticated) {
54
+ return (
55
+ <div className="mx-auto max-w-[760px] px-8 py-7">
56
+ <Head>
57
+ <title>{title}</title>
58
+ </Head>
59
+ <h1 className="mb-2 text-[1.6rem] font-bold tracking-[-0.01em]">
60
+ {title}
61
+ </h1>
62
+ <p className="text-muted-foreground">
63
+ {auth.isAuthEnabled
64
+ ? "Sign in to manage your account settings."
65
+ : "Configure authentication to enable account settings."}
66
+ </p>
67
+ {auth.isAuthEnabled ? (
68
+ <Button asChild className="mt-4">
69
+ <Link to="/signin">Sign in</Link>
70
+ </Button>
71
+ ) : null}
72
+ </div>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <div className="mx-auto max-w-[760px] px-8 py-7">
78
+ <Head>
79
+ <title>{title}</title>
80
+ </Head>
81
+
82
+ <h1 className="mb-6 text-[1.6rem] font-bold tracking-[-0.01em]">
83
+ {title}
84
+ </h1>
85
+
86
+ <div className="grid gap-5">
87
+ <Card>
88
+ <CardHeader>
89
+ <CardTitle>Profile</CardTitle>
90
+ </CardHeader>
91
+ <CardContent className="space-y-4">
92
+ <div className="grid gap-4 sm:grid-cols-2">
93
+ <div className="space-y-2">
94
+ <Label htmlFor="settings-display-name">Full name</Label>
95
+ <Input
96
+ id="settings-display-name"
97
+ value={displayName}
98
+ onChange={(event) => setDisplayName(event.target.value)}
99
+ disabled={isLoading || updateProfile.isPending}
100
+ />
101
+ </div>
102
+ <div className="space-y-2">
103
+ <Label htmlFor="settings-email">Email</Label>
104
+ <Input
105
+ id="settings-email"
106
+ type="email"
107
+ value={email}
108
+ readOnly
109
+ disabled
110
+ />
111
+ </div>
112
+ </div>
113
+ {profileMessage ? (
114
+ <p className="text-sm text-muted-foreground">{profileMessage}</p>
115
+ ) : null}
116
+ <Button
117
+ size="sm"
118
+ disabled={isLoading || updateProfile.isPending}
119
+ onClick={() => {
120
+ setProfileMessage(null);
121
+ updateProfile.mutate(displayName, {
122
+ onSuccess: () => setProfileMessage("Profile saved."),
123
+ onError: (error) =>
124
+ setProfileMessage(
125
+ error instanceof Error
126
+ ? error.message
127
+ : "Unable to save profile.",
128
+ ),
129
+ });
130
+ }}
131
+ >
132
+ Save changes
133
+ </Button>
134
+ </CardContent>
135
+ </Card>
136
+
137
+ {hasNativePassword ? (
138
+ <Card>
139
+ <CardHeader>
140
+ <CardTitle>Password</CardTitle>
141
+ </CardHeader>
142
+ <CardContent className="space-y-4">
143
+ <div className="grid gap-4 sm:grid-cols-2">
144
+ <div className="space-y-2">
145
+ <Label htmlFor="settings-current-password">
146
+ Current password
147
+ </Label>
148
+ <Input
149
+ id="settings-current-password"
150
+ type="password"
151
+ value={currentPassword}
152
+ onChange={(event) => setCurrentPassword(event.target.value)}
153
+ autoComplete="current-password"
154
+ />
155
+ </div>
156
+ <div className="space-y-2">
157
+ <Label htmlFor="settings-new-password">New password</Label>
158
+ <Input
159
+ id="settings-new-password"
160
+ type="password"
161
+ value={newPassword}
162
+ onChange={(event) => setNewPassword(event.target.value)}
163
+ autoComplete="new-password"
164
+ />
165
+ </div>
166
+ </div>
167
+ {passwordMessage ? (
168
+ <p className="text-sm text-muted-foreground">
169
+ {passwordMessage}
170
+ </p>
171
+ ) : null}
172
+ <Button
173
+ variant="outline"
174
+ size="sm"
175
+ disabled={changePassword.isPending}
176
+ onClick={() => {
177
+ setPasswordMessage(null);
178
+ changePassword.mutate(
179
+ { currentPassword, newPassword },
180
+ {
181
+ onSuccess: () => {
182
+ setCurrentPassword("");
183
+ setNewPassword("");
184
+ setPasswordMessage("Password updated.");
185
+ },
186
+ onError: (error) =>
187
+ setPasswordMessage(
188
+ error instanceof Error
189
+ ? error.message
190
+ : "Unable to update password.",
191
+ ),
192
+ },
193
+ );
194
+ }}
195
+ >
196
+ Update password
197
+ </Button>
198
+ </CardContent>
199
+ </Card>
200
+ ) : null}
201
+
202
+ <Card className="border-destructive/40">
203
+ <CardHeader>
204
+ <CardTitle className="text-destructive">Danger zone</CardTitle>
205
+ </CardHeader>
206
+ <CardContent className="space-y-4">
207
+ <p className="text-sm text-muted-foreground">
208
+ Deleting your account removes all keys, usage history, and billing
209
+ data. This cannot be undone.
210
+ </p>
211
+ {hasNativePassword ? (
212
+ <div className="max-w-sm space-y-2">
213
+ <Label htmlFor="settings-delete-password">
214
+ Confirm with password
215
+ </Label>
216
+ <Input
217
+ id="settings-delete-password"
218
+ type="password"
219
+ value={deletePassword}
220
+ onChange={(event) => setDeletePassword(event.target.value)}
221
+ autoComplete="current-password"
222
+ />
223
+ </div>
224
+ ) : (
225
+ <div className="max-w-sm space-y-2">
226
+ <Label htmlFor="settings-delete-confirm">
227
+ Type DELETE to confirm
228
+ </Label>
229
+ <Input
230
+ id="settings-delete-confirm"
231
+ value={deleteConfirm}
232
+ onChange={(event) => setDeleteConfirm(event.target.value)}
233
+ autoComplete="off"
234
+ />
235
+ </div>
236
+ )}
237
+ {deleteMessage ? (
238
+ <p className="text-sm text-muted-foreground">{deleteMessage}</p>
239
+ ) : null}
240
+ <Button
241
+ variant="outline"
242
+ size="sm"
243
+ className="border-destructive text-destructive hover:bg-destructive/10"
244
+ disabled={
245
+ deleteAccountMutation.isPending ||
246
+ (hasNativePassword
247
+ ? !deletePassword
248
+ : deleteConfirm !== "DELETE")
249
+ }
250
+ onClick={() => {
251
+ if (
252
+ !window.confirm(
253
+ "Delete your account permanently? This cannot be undone.",
254
+ )
255
+ ) {
256
+ return;
257
+ }
258
+ setDeleteMessage(null);
259
+ deleteAccountMutation.mutate(
260
+ hasNativePassword ? deletePassword : undefined,
261
+ {
262
+ onError: (error) =>
263
+ setDeleteMessage(
264
+ error instanceof Error
265
+ ? error.message
266
+ : "Unable to delete account.",
267
+ ),
268
+ },
269
+ );
270
+ }}
271
+ >
272
+ Delete account
273
+ </Button>
274
+ </CardContent>
275
+ </Card>
276
+ </div>
277
+ </div>
278
+ );
279
+ };
@@ -1,18 +1,18 @@
1
- import type { ComponentType, ReactNode } from "react";
2
- import { isValidElement } from "react";
3
-
4
- export const renderModuleElement = (
5
- component: ComponentType | ReactNode | undefined,
6
- fallback: ReactNode,
7
- ) => {
8
- if (!component) {
9
- return fallback;
10
- }
11
-
12
- if (isValidElement(component)) {
13
- return component;
14
- }
15
-
16
- const Component = component as ComponentType;
17
- return <Component />;
18
- };
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import { isValidElement } from "react";
3
+
4
+ export const renderModuleElement = (
5
+ component: ComponentType | ReactNode | undefined,
6
+ fallback: ReactNode,
7
+ ) => {
8
+ if (!component) {
9
+ return fallback;
10
+ }
11
+
12
+ if (isValidElement(component)) {
13
+ return component;
14
+ }
15
+
16
+ const Component = component as ComponentType;
17
+ return <Component />;
18
+ };