@blocklet/ui-react 2.12.15 → 2.12.17

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.
@@ -57,6 +57,10 @@ export declare enum StatusEnum {
57
57
  OffSick = "off_sick",
58
58
  WorkingRemotely = "working_remotely"
59
59
  }
60
+ export type UserPhoneProps = {
61
+ country: string;
62
+ phoneNumber: string;
63
+ };
60
64
  export type UserMetadata = {
61
65
  bio?: string;
62
66
  location?: string;
@@ -70,7 +74,7 @@ export type UserMetadata = {
70
74
  links?: UserMetadataLink[];
71
75
  cover?: string;
72
76
  email?: string;
73
- phone?: string;
77
+ phone?: UserPhoneProps;
74
78
  };
75
79
  export type User = UserPublicInfo & {
76
80
  role: string;
@@ -18,6 +18,7 @@ interface EditableFieldProps {
18
18
  verified?: boolean;
19
19
  errorMsg?: string;
20
20
  canEdit?: boolean;
21
+ renderValue?: (value: string) => React.ReactNode;
21
22
  }
22
- declare function EditableField({ value, onChange, onValueValidate, errorMsg, editable, component, placeholder, rows, maxLength, icon, label, children, tooltip, inline, style, verified, canEdit, }: EditableFieldProps): JSX.Element | null;
23
+ declare function EditableField({ value, onChange, onValueValidate, errorMsg, editable, component, placeholder, rows, maxLength, icon, label, children, tooltip, inline, style, verified, canEdit, renderValue, }: EditableFieldProps): JSX.Element | null;
23
24
  export default EditableField;
@@ -43,7 +43,8 @@ function EditableField({
43
43
  inline = true,
44
44
  style = {},
45
45
  verified = false,
46
- canEdit = true
46
+ canEdit = true,
47
+ renderValue
47
48
  }) {
48
49
  const { locale } = useLocaleContext();
49
50
  const t = useMemoizedFn((key, data = {}) => {
@@ -146,7 +147,7 @@ function EditableField({
146
147
  whiteSpace: "pre-wrap",
147
148
  ...inline ? { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } : {}
148
149
  },
149
- children: value
150
+ children: renderValue ? renderValue(value) : value
150
151
  }
151
152
  ),
152
153
  verified && /* @__PURE__ */ jsx(VerifiedIcon, { color: "success", style: { fontSize: 16, width: 16, marginLeft: 4, flexShrink: 0 } })
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps } from '@mui/material';
2
- export default function UserCenter({ children, notLoginContent, currentTab, contentProps, disableAutoRedirect, hideFooter, headerProps, footerProps, userDid, stickySidebar, embed, }: {
2
+ export default function UserCenter({ children, notLoginContent, currentTab, contentProps, disableAutoRedirect, hideFooter, headerProps, footerProps, userDid, stickySidebar, embed, onlyProfile, }: {
3
3
  readonly children: any;
4
4
  readonly notLoginContent: any;
5
5
  readonly currentTab: string;
@@ -12,4 +12,5 @@ export default function UserCenter({ children, notLoginContent, currentTab, cont
12
12
  readonly userDid?: string;
13
13
  readonly stickySidebar?: boolean;
14
14
  readonly embed?: boolean;
15
+ readonly onlyProfile?: boolean;
15
16
  }): import("react").JSX.Element | null;
@@ -70,7 +70,9 @@ export default function UserCenter({
70
70
  footerProps = {},
71
71
  userDid = void 0,
72
72
  stickySidebar = false,
73
- embed = false
73
+ embed = false,
74
+ onlyProfile = false
75
+ // 只显示 profile 页面,用于 ArcSphere 只需要显示 Profile 的内容
74
76
  }) {
75
77
  const { locale } = useLocaleContext();
76
78
  const isMobile = useMobile({ key: "md" });
@@ -386,6 +388,26 @@ export default function UserCenter({
386
388
  userCenterTabs.length === 0 && emptyContent
387
389
  ] });
388
390
  }
391
+ if (onlyProfile) {
392
+ return /* @__PURE__ */ jsx(ContentWrapper, { display: "flex", flexDirection: isMobile ? "column" : "row", children: /* @__PURE__ */ jsx(
393
+ UserBasicInfo,
394
+ {
395
+ isMobile,
396
+ order: isMobile ? 1 : "unset",
397
+ isMyself,
398
+ switchPassport: handleSwitchPassport,
399
+ switchProfile: session.switchProfile,
400
+ user: userState.data,
401
+ showFullDid: false,
402
+ onlyProfile,
403
+ sx: {
404
+ padding: !isMobile ? "40px 24px 24px 40px" : "16px 0 0 0",
405
+ ...!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {},
406
+ boxSizing: "content-box"
407
+ }
408
+ }
409
+ ) });
410
+ }
389
411
  return /* @__PURE__ */ jsxs(ContentWrapper, { display: "flex", flexDirection: isMobile ? "column" : "row", children: [
390
412
  /* @__PURE__ */ jsxs(Box, { flex: "1", className: "user-center-tabs", order: isMobile ? 2 : "unset", children: [
391
413
  userCenterTabs.length > 0 && currentTab ? /* @__PURE__ */ jsxs(
@@ -412,7 +434,7 @@ export default function UserCenter({
412
434
  ".MuiTabs-flexContainer": {
413
435
  gap: 3,
414
436
  ".MuiButtonBase-root": {
415
- padding: "40px 4px 32px 4px",
437
+ padding: isMobile ? "16px 4px" : "40px 4px 32px 4px",
416
438
  fontSize: 16
417
439
  },
418
440
  ".MuiTab-root": {
@@ -453,7 +475,7 @@ export default function UserCenter({
453
475
  user: userState.data,
454
476
  showFullDid: false,
455
477
  sx: {
456
- padding: !isMobile ? "40px 24px 24px 40px" : "24px 0",
478
+ padding: !isMobile ? "40px 24px 24px 40px" : "16px 0 0 0",
457
479
  ...!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {},
458
480
  boxSizing: "content-box"
459
481
  }
@@ -1,5 +1,6 @@
1
1
  import type { User, UserMetadata } from '../../../@types';
2
- export default function UserMetadataComponent({ isMyself, user, onSave, }: {
2
+ export default function UserMetadataComponent({ isMyself, user, onSave, isMobile, }: {
3
+ isMobile: boolean;
3
4
  isMyself: boolean;
4
5
  user: User;
5
6
  onSave: (v: UserMetadata) => void;
@@ -7,12 +7,13 @@ import Backdrop from "@mui/material/Backdrop";
7
7
  import styled from "@emotion/styled";
8
8
  import { joinURL } from "ufo";
9
9
  import Button from "@arcblock/ux/lib/Button";
10
+ import PhoneInput, { validatePhoneNumber } from "@arcblock/ux/lib/PhoneInput";
10
11
  import cloneDeep from "lodash/cloneDeep";
11
12
  import { useCreation, useMemoizedFn, useReactive } from "ahooks";
12
13
  import { useMemo, useRef, useState, memo, forwardRef, useEffect, lazy } from "react";
13
14
  import { translate } from "@arcblock/ux/lib/Locale/util";
14
15
  import isEmail from "validator/lib/isEmail";
15
- import isMobilePhone from "validator/lib/isMobilePhone";
16
+ import { temp as colors } from "@arcblock/ux/lib/Colors";
16
17
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
17
18
  import { useBrowser } from "@arcblock/react-hooks";
18
19
  import { translations } from "../../libs/locales.js";
@@ -52,7 +53,8 @@ BackdropWrap.displayName = "BackdropWrap";
52
53
  export default function UserMetadataComponent({
53
54
  isMyself,
54
55
  user,
55
- onSave
56
+ onSave,
57
+ isMobile
56
58
  }) {
57
59
  const [editable, setEditable] = useState(false);
58
60
  const [visible, setVisible] = useState(false);
@@ -82,9 +84,34 @@ export default function UserMetadataComponent({
82
84
  user?.metadata ? cloneDeep(user.metadata) : {
83
85
  joinedAt: user?.createdAt,
84
86
  email: user?.email,
85
- phone: user?.phone
87
+ phone: {
88
+ country: "cn",
89
+ phoneNumber: user?.phone ?? ""
90
+ }
86
91
  }
87
92
  );
93
+ const phoneValue = useCreation(() => {
94
+ const phone = metadata.phone ?? user?.phone ?? {
95
+ country: "cn",
96
+ phone: ""
97
+ };
98
+ if (typeof phone === "string") {
99
+ return {
100
+ country: "cn",
101
+ phone
102
+ };
103
+ }
104
+ if (typeof phone === "object") {
105
+ return {
106
+ country: phone.country,
107
+ phone: phone.phoneNumber
108
+ };
109
+ }
110
+ return {
111
+ country: "cn",
112
+ phone: ""
113
+ };
114
+ }, [metadata.phone, user?.phone]);
88
115
  const onChange = (v, field) => {
89
116
  metadata[field] = v;
90
117
  };
@@ -155,159 +182,223 @@ export default function UserMetadataComponent({
155
182
  setVisible(false);
156
183
  };
157
184
  const renderEdit = (editing, mode = "self") => {
158
- return /* @__PURE__ */ jsxs(MetadataInfo, { pt: 2, display: "flex", flexDirection: "column", children: [
159
- /* @__PURE__ */ jsx(
160
- EditableField,
161
- {
162
- value: metadata.bio ?? "",
163
- onChange: (value) => onChange(value, "bio"),
164
- editable: editing,
165
- placeholder: "Bio",
166
- component: "textarea",
167
- inline: false,
168
- rows: 3,
169
- label: t("profile.bio"),
170
- maxLength: bioMaxLength,
171
- style: {
172
- ...editing ? { marginBottom: 8 } : {}
173
- }
174
- }
175
- ),
176
- !editing && isMyself ? /* @__PURE__ */ jsx(
177
- Button,
178
- {
179
- size: "large",
180
- variant: "outlined",
181
- sx: {
182
- ...defaultButtonStyle,
183
- mb: 2,
184
- mt: 2,
185
- height: "40px"
186
- },
187
- onClick: onEdit,
188
- fullWidth: true,
189
- children: t("profile.editProfile")
190
- }
191
- ) : null,
192
- /* @__PURE__ */ jsx(
193
- EditableField,
194
- {
195
- value: metadata.location ?? "",
196
- onChange: (value) => onChange(value, "location"),
197
- editable: editing,
198
- placeholder: "Location",
199
- label: t("profile.location"),
200
- icon: /* @__PURE__ */ jsx(LocationIcon, { ...iconSize })
201
- }
202
- ),
203
- /* @__PURE__ */ jsx(
204
- EditableField,
205
- {
206
- value: metadata.timezone || currentTimezone,
207
- onChange: (value) => onChange(value, "timezone"),
208
- editable: editing,
209
- placeholder: "timezone",
210
- icon: /* @__PURE__ */ jsx(TimezoneIcon, { ...iconSize }),
211
- label: t("profile.timezone"),
212
- tooltip: /* @__PURE__ */ jsxs("p", { style: { display: "flex", margin: 0 }, children: [
213
- /* @__PURE__ */ jsx("span", { style: { marginRight: "4px" }, children: t("profile.localTime") }),
214
- /* @__PURE__ */ jsx(Clock, { timezone: metadata.timezone, locale })
215
- ] }),
216
- children: /* @__PURE__ */ jsx(
217
- TimezoneSelect,
185
+ return /* @__PURE__ */ jsxs(
186
+ MetadataInfo,
187
+ {
188
+ pt: 2,
189
+ display: "flex",
190
+ flexDirection: "column",
191
+ justifyContent: "space-between",
192
+ alignItems: "flex-start",
193
+ gap: !isMobile ? "16px" : "4px",
194
+ children: [
195
+ /* @__PURE__ */ jsx(
196
+ EditableField,
197
+ {
198
+ value: metadata.bio ?? "",
199
+ onChange: (value) => onChange(value, "bio"),
200
+ editable: editing,
201
+ placeholder: "Bio",
202
+ component: "textarea",
203
+ inline: false,
204
+ rows: 3,
205
+ label: t("profile.bio"),
206
+ maxLength: bioMaxLength,
207
+ style: {
208
+ ...editing ? { marginBottom: 8 } : {}
209
+ }
210
+ }
211
+ ),
212
+ !editing && isMyself ? /* @__PURE__ */ jsx(
213
+ Button,
214
+ {
215
+ size: isMobile ? "small" : "large",
216
+ variant: "outlined",
217
+ sx: {
218
+ ...defaultButtonStyle,
219
+ mb: !isMobile ? 2 : "4px",
220
+ mt: !isMobile ? 2 : "4px",
221
+ height: !isMobile ? "40px" : "32px"
222
+ },
223
+ onClick: onEdit,
224
+ fullWidth: true,
225
+ children: t("profile.editProfile")
226
+ }
227
+ ) : null,
228
+ /* @__PURE__ */ jsx(
229
+ EditableField,
230
+ {
231
+ value: metadata.location ?? "",
232
+ onChange: (value) => onChange(value, "location"),
233
+ editable: editing,
234
+ placeholder: "Location",
235
+ label: t("profile.location"),
236
+ icon: /* @__PURE__ */ jsx(LocationIcon, { ...iconSize })
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsx(
240
+ EditableField,
218
241
  {
219
242
  value: metadata.timezone || currentTimezone,
220
243
  onChange: (value) => onChange(value, "timezone"),
221
- disabled: !editing
244
+ editable: editing,
245
+ placeholder: "timezone",
246
+ icon: /* @__PURE__ */ jsx(TimezoneIcon, { ...iconSize }),
247
+ label: t("profile.timezone"),
248
+ tooltip: /* @__PURE__ */ jsxs("p", { style: { display: "flex", margin: 0 }, children: [
249
+ /* @__PURE__ */ jsx("span", { style: { marginRight: "4px" }, children: t("profile.localTime") }),
250
+ /* @__PURE__ */ jsx(Clock, { timezone: metadata.timezone, locale })
251
+ ] }),
252
+ children: /* @__PURE__ */ jsx(
253
+ TimezoneSelect,
254
+ {
255
+ value: metadata.timezone || currentTimezone,
256
+ onChange: (value) => onChange(value, "timezone"),
257
+ disabled: !editing
258
+ }
259
+ )
222
260
  }
223
- )
224
- }
225
- ),
226
- /* @__PURE__ */ jsx(
227
- EditableField,
228
- {
229
- value: metadata.email ?? user?.email ?? "",
230
- editable: editing,
231
- canEdit: !emailVerified,
232
- verified: emailVerified,
233
- placeholder: "Email",
234
- icon: /* @__PURE__ */ jsx(EmailIcon, { ...iconSize }),
235
- label: t("profile.email"),
236
- onChange: (value) => onChange(value, "email"),
237
- errorMsg: validateMsg.email,
238
- onValueValidate: (value) => {
239
- let msg = "";
240
- if (!!value && !isEmail(value)) {
241
- msg = t("profile.emailInvalid");
261
+ ),
262
+ /* @__PURE__ */ jsx(
263
+ EditableField,
264
+ {
265
+ value: metadata.email ?? user?.email ?? "",
266
+ editable: editing,
267
+ canEdit: !emailVerified,
268
+ verified: emailVerified,
269
+ placeholder: "Email",
270
+ icon: /* @__PURE__ */ jsx(EmailIcon, { ...iconSize }),
271
+ label: t("profile.email"),
272
+ onChange: (value) => onChange(value, "email"),
273
+ errorMsg: validateMsg.email,
274
+ renderValue: (value) => /* @__PURE__ */ jsx(
275
+ "a",
276
+ {
277
+ href: `mailto:${value}`,
278
+ style: {
279
+ color: "inherit",
280
+ textDecoration: "none"
281
+ },
282
+ children: value
283
+ }
284
+ ),
285
+ onValueValidate: (value) => {
286
+ let msg = "";
287
+ if (!!value && !isEmail(value)) {
288
+ msg = t("profile.emailInvalid");
289
+ }
290
+ validateMsg.email = msg;
291
+ }
242
292
  }
243
- validateMsg.email = msg;
244
- }
245
- }
246
- ),
247
- /* @__PURE__ */ jsx(
248
- EditableField,
249
- {
250
- value: metadata.phone ?? user?.phone ?? "",
251
- editable: editing,
252
- canEdit: !phoneVerified,
253
- verified: phoneVerified,
254
- placeholder: "Phone",
255
- icon: /* @__PURE__ */ jsx(PhoneIcon, { ...iconSize }),
256
- onChange: (value) => onChange(value, "phone"),
257
- label: t("profile.phone"),
258
- errorMsg: validateMsg.phone,
259
- onValueValidate: (value) => {
260
- let msg = "";
261
- if (!!value && !isMobilePhone(value)) {
262
- msg = t("profile.phoneInvalid");
293
+ ),
294
+ /* @__PURE__ */ jsx(
295
+ EditableField,
296
+ {
297
+ value: phoneValue.phone,
298
+ editable: editing,
299
+ canEdit: !phoneVerified,
300
+ verified: phoneVerified,
301
+ placeholder: "Phone",
302
+ icon: /* @__PURE__ */ jsx(PhoneIcon, { ...iconSize }),
303
+ onChange: (value) => onChange(value, "phone"),
304
+ label: t("profile.phone"),
305
+ renderValue: () => {
306
+ return /* @__PURE__ */ jsx(PhoneInput, { value: phoneValue, preview: true });
307
+ },
308
+ children: /* @__PURE__ */ jsx(
309
+ PhoneInput,
310
+ {
311
+ variant: "outlined",
312
+ className: "editable-field",
313
+ InputProps: {
314
+ sx: { backgroundColor: "transparent" },
315
+ placeholder: "Phone"
316
+ },
317
+ value: phoneValue,
318
+ error: !!validateMsg.phone,
319
+ helperText: validateMsg.phone,
320
+ sx: {
321
+ width: "100%",
322
+ ".MuiOutlinedInput-root": {
323
+ "&:hover": {
324
+ fieldset: {
325
+ borderColor: colors.dividerColor
326
+ }
327
+ },
328
+ "&.Mui-focused": {
329
+ fieldset: {
330
+ borderColor: colors.dividerColor
331
+ }
332
+ }
333
+ },
334
+ fieldset: {
335
+ borderColor: colors.dividerColor
336
+ }
337
+ },
338
+ onChange: (value) => {
339
+ const isValid = validatePhoneNumber(value.phone);
340
+ if (!isValid) {
341
+ validateMsg.phone = t("profile.phoneInvalid");
342
+ } else {
343
+ validateMsg.phone = "";
344
+ }
345
+ onChange(
346
+ {
347
+ country: value.country,
348
+ phoneNumber: value.phone
349
+ },
350
+ "phone"
351
+ );
352
+ }
353
+ }
354
+ )
263
355
  }
264
- validateMsg.phone = msg;
265
- }
266
- }
267
- ),
268
- /* @__PURE__ */ jsx(LinkPreviewInput, { editable: editing, links, onChange: handleLinksChange }),
269
- editing && isMyself ? /* @__PURE__ */ jsxs(
270
- Box,
271
- {
272
- display: "flex",
273
- gap: 1,
274
- style: { width: "100%" },
275
- justifyContent: "flex-end",
276
- flexDirection: mode === "drawer" ? "column" : "row",
277
- children: [
278
- /* @__PURE__ */ jsx(
279
- Button,
280
- {
281
- fullWidth: mode === "drawer",
282
- size: "small",
283
- variant: "outlined",
284
- sx: { ...defaultButtonStyle, minWidth: "54px" },
285
- onClick: onCancel,
286
- children: t("common.cancel")
287
- }
288
- ),
289
- /* @__PURE__ */ jsx(
290
- Button,
291
- {
292
- fullWidth: mode === "drawer",
293
- size: "small",
294
- disabled: !!validateMsg.email || !!validateMsg.phone,
295
- variant: "outlined",
296
- sx: {
297
- ...primaryButtonStyle,
298
- minWidth: "54px",
299
- "&.Mui-disabled": {
300
- backgroundColor: "rgba(0, 0, 0, 0.12)"
356
+ ),
357
+ /* @__PURE__ */ jsx(LinkPreviewInput, { editable: editing, links, onChange: handleLinksChange }),
358
+ editing && isMyself ? /* @__PURE__ */ jsxs(
359
+ Box,
360
+ {
361
+ display: "flex",
362
+ gap: 1,
363
+ style: { width: "100%" },
364
+ justifyContent: "flex-end",
365
+ flexDirection: mode === "drawer" ? "column" : "row",
366
+ children: [
367
+ /* @__PURE__ */ jsx(
368
+ Button,
369
+ {
370
+ fullWidth: mode === "drawer",
371
+ size: "small",
372
+ variant: "outlined",
373
+ sx: { ...defaultButtonStyle, minWidth: "54px" },
374
+ onClick: onCancel,
375
+ children: t("common.cancel")
301
376
  }
302
- },
303
- onClick: handleSave,
304
- children: t("common.save")
305
- }
306
- )
307
- ]
308
- }
309
- ) : null
310
- ] });
377
+ ),
378
+ /* @__PURE__ */ jsx(
379
+ Button,
380
+ {
381
+ fullWidth: mode === "drawer",
382
+ size: "small",
383
+ disabled: !!validateMsg.email || !!validateMsg.phone,
384
+ variant: "outlined",
385
+ sx: {
386
+ ...primaryButtonStyle,
387
+ minWidth: "54px",
388
+ "&.Mui-disabled": {
389
+ backgroundColor: "rgba(0, 0, 0, 0.12)"
390
+ }
391
+ },
392
+ onClick: handleSave,
393
+ children: t("common.save")
394
+ }
395
+ )
396
+ ]
397
+ }
398
+ ) : null
399
+ ]
400
+ }
401
+ );
311
402
  };
312
403
  return /* @__PURE__ */ jsxs(Fragment, { children: [
313
404
  renderEdit(editable),
@@ -374,9 +465,6 @@ export default function UserMetadataComponent({
374
465
  ] });
375
466
  }
376
467
  const MetadataInfo = styled(Box)`
377
- gap: 16px;
378
- justify-content: space-between;
379
- align-items: flex-start;
380
468
  width: 100%;
381
469
 
382
470
  .MuiOutlinedInput-root {
@@ -70,11 +70,8 @@ export function TimezoneSelect({ value, onChange, disabled = false, mode = "self
70
70
  size: "small",
71
71
  fullWidth: true,
72
72
  onChange: (event) => setSearchText(event.target.value),
73
- onKeyDown: (e) => {
74
- if (e.key !== "Escape") {
75
- e.stopPropagation();
76
- }
77
- },
73
+ onClick: (e) => e.stopPropagation(),
74
+ onKeyDown: (e) => e.stopPropagation(),
78
75
  sx: {
79
76
  marginTop: "8px",
80
77
  "& .MuiOutlinedInput-root": {
@@ -1,6 +1,6 @@
1
1
  import type { BoxProps } from '@mui/material';
2
2
  import type { User } from '../../../@types';
3
- export default function UserBasicInfo({ user, isMyself, showFullDid, switchPassport, switchProfile, isMobile, ...rest }: {
3
+ export default function UserBasicInfo({ user, isMyself, showFullDid, switchPassport, switchProfile, isMobile, onlyProfile, ...rest }: {
4
4
  user: User;
5
5
  isMyself?: boolean;
6
6
  showFullDid?: boolean;
@@ -8,4 +8,5 @@ export default function UserBasicInfo({ user, isMyself, showFullDid, switchPassp
8
8
  switchProfile: () => void;
9
9
  size?: number;
10
10
  isMobile?: boolean;
11
+ onlyProfile?: boolean;
11
12
  } & BoxProps): import("react").JSX.Element | null;
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { Box, Divider, Typography } from "@mui/material";
2
+ import { Box, Divider, Typography, IconButton, Collapse } from "@mui/material";
3
3
  import Avatar from "@arcblock/ux/lib/Avatar";
4
4
  import DID from "@arcblock/ux/lib/DID";
5
5
  import { useMemoizedFn } from "ahooks";
@@ -10,6 +10,8 @@ import { useEffect, useState } from "react";
10
10
  import Toast from "@arcblock/ux/lib/Toast";
11
11
  import { temp as colors } from "@arcblock/ux/lib/Colors";
12
12
  import { parseURL, joinURL } from "ufo";
13
+ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
14
+ import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
13
15
  import { translations } from "../../libs/locales.js";
14
16
  import { formatAxiosError } from "../../libs/utils.js";
15
17
  import { currentTimezone, getStatusDuration, isValidUrl } from "./utils.js";
@@ -25,6 +27,7 @@ export default function UserBasicInfo({
25
27
  switchPassport,
26
28
  switchProfile,
27
29
  isMobile = false,
30
+ onlyProfile = false,
28
31
  ...rest
29
32
  }) {
30
33
  const { locale } = useLocaleContext();
@@ -32,9 +35,13 @@ export default function UserBasicInfo({
32
35
  const t = useMemoizedFn((key, data = {}) => {
33
36
  return translate(translations, key, locale, "en", data);
34
37
  });
38
+ const [expanded, setExpanded] = useState(!isMobile || onlyProfile);
35
39
  useEffect(() => {
36
40
  setUserStatus(user?.metadata?.status);
37
41
  }, [user]);
42
+ useEffect(() => {
43
+ setExpanded(!isMobile || onlyProfile);
44
+ }, [isMobile, onlyProfile]);
38
45
  const onUpdateUserStatus = async (v) => {
39
46
  if (!isMyself) {
40
47
  return;
@@ -57,6 +64,9 @@ export default function UserBasicInfo({
57
64
  Toast.error(formatAxiosError(err));
58
65
  }
59
66
  };
67
+ const toggleExpand = () => {
68
+ setExpanded(!expanded);
69
+ };
60
70
  if (!user) {
61
71
  return null;
62
72
  }
@@ -184,11 +194,38 @@ export default function UserBasicInfo({
184
194
  }
185
195
  )
186
196
  ] }),
187
- /* @__PURE__ */ jsx(UserMetadataComponent, { isMyself, user, onSave }),
197
+ /* @__PURE__ */ jsx(UserMetadataComponent, { isMobile, isMyself, user, onSave }),
188
198
  isMyself ? /* @__PURE__ */ jsxs(Fragment, { children: [
189
- /* @__PURE__ */ jsx(Divider, { sx: { my: 3, borderColor: colors.dividerColor } }),
190
- /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", fontSize: "14px", mb: 2, children: t("profile.justForYou") }),
191
- /* @__PURE__ */ jsx(UserInfo, { user, isMySelf: isMyself })
199
+ /* @__PURE__ */ jsx(Divider, { sx: { my: isMobile ? 1 : 3, borderColor: colors.dividerColor } }),
200
+ isMobile && !onlyProfile ? /* @__PURE__ */ jsx(
201
+ Box,
202
+ {
203
+ sx: {
204
+ display: "flex",
205
+ justifyContent: "center",
206
+ mb: 0
207
+ },
208
+ children: /* @__PURE__ */ jsx(
209
+ IconButton,
210
+ {
211
+ size: "small",
212
+ onClick: toggleExpand,
213
+ sx: {
214
+ backgroundColor: colors.backgroundsBgField,
215
+ "&:hover": {
216
+ backgroundColor: colors.backgroundsBgField,
217
+ opacity: 0.8
218
+ }
219
+ },
220
+ children: expanded ? /* @__PURE__ */ jsx(KeyboardArrowUpIcon, {}) : /* @__PURE__ */ jsx(KeyboardArrowDownIcon, {})
221
+ }
222
+ )
223
+ }
224
+ ) : null,
225
+ /* @__PURE__ */ jsxs(Collapse, { in: expanded, timeout: "auto", children: [
226
+ /* @__PURE__ */ jsx(Typography, { component: "p", color: "text.secondary", fontSize: "14px", mb: 2, children: t("profile.justForYou") }),
227
+ /* @__PURE__ */ jsx(UserInfo, { user, isMySelf: isMyself })
228
+ ] })
192
229
  ] }) : null
193
230
  ]
194
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "2.12.15",
3
+ "version": "2.12.17",
4
4
  "description": "Some useful front-end web components that can be used in Blocklets.",
5
5
  "keywords": [
6
6
  "react",
@@ -33,10 +33,10 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@abtnode/constant": "^1.16.39",
36
- "@arcblock/bridge": "^2.12.15",
37
- "@arcblock/react-hooks": "^2.12.15",
36
+ "@arcblock/bridge": "^2.12.17",
37
+ "@arcblock/react-hooks": "^2.12.17",
38
38
  "@arcblock/ws": "^1.19.15",
39
- "@blocklet/did-space-react": "^1.0.30",
39
+ "@blocklet/did-space-react": "^1.0.31",
40
40
  "@iconify-icons/logos": "^1.2.36",
41
41
  "@iconify-icons/material-symbols": "^1.2.58",
42
42
  "@iconify/react": "^4.1.1",
@@ -87,5 +87,5 @@
87
87
  "jest": "^29.7.0",
88
88
  "unbuild": "^2.0.0"
89
89
  },
90
- "gitHead": "16a4f35c677fc2ca2118801dc62984b71734df70"
90
+ "gitHead": "de1814f4c2eca44000845ce0db42e9d30d487e80"
91
91
  }
@@ -66,6 +66,11 @@ export enum StatusEnum {
66
66
  WorkingRemotely = 'working_remotely',
67
67
  }
68
68
 
69
+ export type UserPhoneProps = {
70
+ country: string;
71
+ phoneNumber: string;
72
+ };
73
+
69
74
  export type UserMetadata = {
70
75
  bio?: string;
71
76
  location?: string;
@@ -80,7 +85,7 @@ export type UserMetadata = {
80
85
  cover?: string;
81
86
  // 这两个字段是 User, 方便数据更新,在保存时同步
82
87
  email?: string;
83
- phone?: string;
88
+ phone?: UserPhoneProps;
84
89
  };
85
90
 
86
91
  export type User = UserPublicInfo & {
@@ -25,6 +25,7 @@ interface EditableFieldProps {
25
25
  verified?: boolean;
26
26
  errorMsg?: string;
27
27
  canEdit?: boolean;
28
+ renderValue?: (value: string) => React.ReactNode;
28
29
  }
29
30
 
30
31
  const inputFieldStyle = {
@@ -65,6 +66,7 @@ function EditableField({
65
66
  style = {},
66
67
  verified = false,
67
68
  canEdit = true,
69
+ renderValue,
68
70
  }: EditableFieldProps) {
69
71
  const { locale } = useLocaleContext();
70
72
  const t = useMemoizedFn((key, data = {}) => {
@@ -160,7 +162,7 @@ function EditableField({
160
162
  whiteSpace: 'pre-wrap',
161
163
  ...(inline ? { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } : {}),
162
164
  }}>
163
- {value}
165
+ {renderValue ? renderValue(value) : value}
164
166
  </Typography>
165
167
  {verified && (
166
168
  <VerifiedIcon color="success" style={{ fontSize: 16, width: 16, marginLeft: 4, flexShrink: 0 }} />
@@ -81,6 +81,7 @@ export default function UserCenter({
81
81
  userDid = undefined,
82
82
  stickySidebar = false,
83
83
  embed = false,
84
+ onlyProfile = false, // 只显示 profile 页面,用于 ArcSphere 只需要显示 Profile 的内容
84
85
  }: {
85
86
  readonly children: any;
86
87
  readonly notLoginContent: any;
@@ -95,6 +96,7 @@ export default function UserCenter({
95
96
  readonly userDid?: string;
96
97
  readonly stickySidebar?: boolean;
97
98
  readonly embed?: boolean;
99
+ readonly onlyProfile?: boolean;
98
100
  }) {
99
101
  const { locale } = useLocaleContext();
100
102
  const isMobile = useMobile({ key: 'md' });
@@ -456,6 +458,28 @@ export default function UserCenter({
456
458
  );
457
459
  }
458
460
 
461
+ if (onlyProfile) {
462
+ return (
463
+ <ContentWrapper display="flex" flexDirection={isMobile ? 'column' : 'row'}>
464
+ <UserBasicInfo
465
+ isMobile={isMobile}
466
+ order={isMobile ? 1 : 'unset'}
467
+ isMyself={isMyself}
468
+ switchPassport={handleSwitchPassport}
469
+ switchProfile={session.switchProfile}
470
+ user={userState.data as User}
471
+ showFullDid={false}
472
+ onlyProfile={onlyProfile}
473
+ sx={{
474
+ padding: !isMobile ? '40px 24px 24px 40px' : '16px 0 0 0',
475
+ ...(!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {}),
476
+ boxSizing: 'content-box',
477
+ }}
478
+ />
479
+ </ContentWrapper>
480
+ );
481
+ }
482
+
459
483
  return (
460
484
  <ContentWrapper display="flex" flexDirection={isMobile ? 'column' : 'row'}>
461
485
  <Box flex="1" className="user-center-tabs" order={isMobile ? 2 : 'unset'}>
@@ -479,7 +503,7 @@ export default function UserCenter({
479
503
  '.MuiTabs-flexContainer': {
480
504
  gap: 3,
481
505
  '.MuiButtonBase-root': {
482
- padding: '40px 4px 32px 4px',
506
+ padding: isMobile ? '16px 4px' : '40px 4px 32px 4px',
483
507
  fontSize: 16,
484
508
  },
485
509
  '.MuiTab-root': {
@@ -516,7 +540,7 @@ export default function UserCenter({
516
540
  user={userState.data as User}
517
541
  showFullDid={false}
518
542
  sx={{
519
- padding: !isMobile ? '40px 24px 24px 40px' : '24px 0',
543
+ padding: !isMobile ? '40px 24px 24px 40px' : '16px 0 0 0',
520
544
  ...(!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {}),
521
545
  boxSizing: 'content-box',
522
546
  }}
@@ -9,18 +9,19 @@ import Backdrop, { BackdropProps } from '@mui/material/Backdrop';
9
9
  import styled from '@emotion/styled';
10
10
  import { joinURL } from 'ufo';
11
11
  import Button from '@arcblock/ux/lib/Button';
12
+ import PhoneInput, { PhoneValue, validatePhoneNumber } from '@arcblock/ux/lib/PhoneInput';
12
13
  import cloneDeep from 'lodash/cloneDeep';
13
14
 
14
15
  import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
15
16
  import { useMemo, useRef, useState, memo, forwardRef, useEffect, lazy } from 'react';
16
17
  import { translate } from '@arcblock/ux/lib/Locale/util';
17
18
  import isEmail from 'validator/lib/isEmail';
18
- import isMobilePhone from 'validator/lib/isMobilePhone';
19
+ import { temp as colors } from '@arcblock/ux/lib/Colors';
19
20
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
20
21
 
21
22
  import { useBrowser } from '@arcblock/react-hooks';
22
23
  import { translations } from '../../libs/locales';
23
- import type { User, UserMetadata } from '../../../@types';
24
+ import type { User, UserMetadata, UserPhoneProps } from '../../../@types';
24
25
  import EditableField from '../editable-field';
25
26
  import { LinkPreviewInput } from './link-preview-input';
26
27
  import { currentTimezone, defaultButtonStyle, primaryButtonStyle } from './utils';
@@ -64,7 +65,9 @@ export default function UserMetadataComponent({
64
65
  isMyself,
65
66
  user,
66
67
  onSave,
68
+ isMobile,
67
69
  }: {
70
+ isMobile: boolean;
68
71
  isMyself: boolean;
69
72
  user: User;
70
73
  onSave: (v: UserMetadata) => void;
@@ -104,11 +107,40 @@ export default function UserMetadataComponent({
104
107
  : {
105
108
  joinedAt: user?.createdAt,
106
109
  email: user?.email,
107
- phone: user?.phone,
110
+ phone: {
111
+ country: 'cn',
112
+ phoneNumber: user?.phone ?? '',
113
+ },
108
114
  }
109
115
  );
110
116
 
111
- const onChange = (v: any, field: keyof UserMetadata | 'email') => {
117
+ const phoneValue = useCreation((): PhoneValue => {
118
+ const phone = metadata.phone ??
119
+ user?.phone ?? {
120
+ country: 'cn',
121
+ phone: '',
122
+ };
123
+
124
+ if (typeof phone === 'string') {
125
+ return {
126
+ country: 'cn',
127
+ phone,
128
+ };
129
+ }
130
+ if (typeof phone === 'object') {
131
+ return {
132
+ country: phone.country,
133
+ phone: (phone as UserPhoneProps).phoneNumber,
134
+ };
135
+ }
136
+
137
+ return {
138
+ country: 'cn',
139
+ phone: '',
140
+ };
141
+ }, [metadata.phone, user?.phone]);
142
+
143
+ const onChange = (v: any, field: keyof UserMetadata | 'email' | 'phone') => {
112
144
  metadata[field] = v;
113
145
  };
114
146
 
@@ -186,7 +218,13 @@ export default function UserMetadataComponent({
186
218
 
187
219
  const renderEdit = (editing: boolean, mode: 'drawer' | 'self' = 'self') => {
188
220
  return (
189
- <MetadataInfo pt={2} display="flex" flexDirection="column">
221
+ <MetadataInfo
222
+ pt={2}
223
+ display="flex"
224
+ flexDirection="column"
225
+ justifyContent="space-between"
226
+ alignItems="flex-start"
227
+ gap={!isMobile ? '16px' : '4px'}>
190
228
  <EditableField
191
229
  value={metadata.bio ?? ''}
192
230
  onChange={(value) => onChange(value, 'bio')}
@@ -203,13 +241,13 @@ export default function UserMetadataComponent({
203
241
  />
204
242
  {!editing && isMyself ? (
205
243
  <Button
206
- size="large"
244
+ size={isMobile ? 'small' : 'large'}
207
245
  variant="outlined"
208
246
  sx={{
209
247
  ...defaultButtonStyle,
210
- mb: 2,
211
- mt: 2,
212
- height: '40px',
248
+ mb: !isMobile ? 2 : '4px',
249
+ mt: !isMobile ? 2 : '4px',
250
+ height: !isMobile ? '40px' : '32px',
213
251
  }}
214
252
  onClick={onEdit}
215
253
  fullWidth>
@@ -255,6 +293,16 @@ export default function UserMetadataComponent({
255
293
  label={t('profile.email')}
256
294
  onChange={(value) => onChange(value, 'email')}
257
295
  errorMsg={validateMsg.email}
296
+ renderValue={(value) => (
297
+ <a
298
+ href={`mailto:${value}`}
299
+ style={{
300
+ color: 'inherit',
301
+ textDecoration: 'none',
302
+ }}>
303
+ {value}
304
+ </a>
305
+ )}
258
306
  onValueValidate={(value) => {
259
307
  let msg = '';
260
308
  if (!!value && !isEmail(value)) {
@@ -265,7 +313,7 @@ export default function UserMetadataComponent({
265
313
  />
266
314
 
267
315
  <EditableField
268
- value={metadata.phone ?? user?.phone ?? ''}
316
+ value={phoneValue.phone}
269
317
  editable={editing}
270
318
  canEdit={!phoneVerified}
271
319
  verified={phoneVerified}
@@ -273,15 +321,54 @@ export default function UserMetadataComponent({
273
321
  icon={<PhoneIcon {...iconSize} />}
274
322
  onChange={(value) => onChange(value, 'phone')}
275
323
  label={t('profile.phone')}
276
- errorMsg={validateMsg.phone}
277
- onValueValidate={(value) => {
278
- let msg = '';
279
- if (!!value && !isMobilePhone(value)) {
280
- msg = t('profile.phoneInvalid');
281
- }
282
- validateMsg.phone = msg;
283
- }}
284
- />
324
+ renderValue={() => {
325
+ return <PhoneInput value={phoneValue} preview />;
326
+ }}>
327
+ <PhoneInput
328
+ variant="outlined"
329
+ className="editable-field"
330
+ InputProps={{
331
+ sx: { backgroundColor: 'transparent' },
332
+ placeholder: 'Phone',
333
+ }}
334
+ value={phoneValue}
335
+ error={!!validateMsg.phone}
336
+ helperText={validateMsg.phone}
337
+ sx={{
338
+ width: '100%',
339
+ '.MuiOutlinedInput-root': {
340
+ '&:hover': {
341
+ fieldset: {
342
+ borderColor: colors.dividerColor,
343
+ },
344
+ },
345
+ '&.Mui-focused': {
346
+ fieldset: {
347
+ borderColor: colors.dividerColor,
348
+ },
349
+ },
350
+ },
351
+ fieldset: {
352
+ borderColor: colors.dividerColor,
353
+ },
354
+ }}
355
+ onChange={(value: any) => {
356
+ const isValid = validatePhoneNumber(value.phone);
357
+ if (!isValid) {
358
+ validateMsg.phone = t('profile.phoneInvalid');
359
+ } else {
360
+ validateMsg.phone = '';
361
+ }
362
+ onChange(
363
+ {
364
+ country: value.country,
365
+ phoneNumber: value.phone,
366
+ },
367
+ 'phone'
368
+ );
369
+ }}
370
+ />
371
+ </EditableField>
285
372
 
286
373
  <LinkPreviewInput editable={editing} links={links} onChange={handleLinksChange} />
287
374
  {editing && isMyself ? (
@@ -374,9 +461,6 @@ export default function UserMetadataComponent({
374
461
  );
375
462
  }
376
463
  const MetadataInfo = styled(Box)`
377
- gap: 16px;
378
- justify-content: space-between;
379
- align-items: flex-start;
380
464
  width: 100%;
381
465
 
382
466
  .MuiOutlinedInput-root {
@@ -82,11 +82,8 @@ export function TimezoneSelect({ value, onChange, disabled = false, mode = 'self
82
82
  size="small"
83
83
  fullWidth
84
84
  onChange={(event) => setSearchText(event.target.value)}
85
- onKeyDown={(e) => {
86
- if (e.key !== 'Escape') {
87
- e.stopPropagation();
88
- }
89
- }}
85
+ onClick={(e) => e.stopPropagation()}
86
+ onKeyDown={(e) => e.stopPropagation()}
90
87
  sx={{
91
88
  marginTop: '8px',
92
89
  '& .MuiOutlinedInput-root': {
@@ -1,4 +1,4 @@
1
- import { Box, Divider, Typography } from '@mui/material';
1
+ import { Box, Divider, Typography, IconButton, Collapse } from '@mui/material';
2
2
  import type { BoxProps } from '@mui/material';
3
3
  import Avatar from '@arcblock/ux/lib/Avatar';
4
4
  import DID from '@arcblock/ux/lib/DID';
@@ -11,6 +11,8 @@ import Toast from '@arcblock/ux/lib/Toast';
11
11
  import { temp as colors } from '@arcblock/ux/lib/Colors';
12
12
  import type { AxiosError } from 'axios';
13
13
  import { parseURL, joinURL } from 'ufo';
14
+ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
15
+ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
14
16
 
15
17
  import { translations } from '../../libs/locales';
16
18
  import type { User, UserMetadata } from '../../../@types';
@@ -29,6 +31,7 @@ export default function UserBasicInfo({
29
31
  switchPassport,
30
32
  switchProfile,
31
33
  isMobile = false,
34
+ onlyProfile = false,
32
35
  ...rest
33
36
  }: {
34
37
  user: User;
@@ -38,6 +41,7 @@ export default function UserBasicInfo({
38
41
  switchProfile: () => void;
39
42
  size?: number;
40
43
  isMobile?: boolean;
44
+ onlyProfile?: boolean;
41
45
  } & BoxProps) {
42
46
  const { locale } = useLocaleContext();
43
47
  const [userStatus, setUserStatus] = useState<UserMetadata['status']>(undefined);
@@ -45,10 +49,17 @@ export default function UserBasicInfo({
45
49
  return translate(translations, key, locale, 'en', data);
46
50
  });
47
51
 
52
+ const [expanded, setExpanded] = useState(!isMobile || onlyProfile);
53
+
48
54
  useEffect(() => {
49
55
  setUserStatus(user?.metadata?.status);
50
56
  }, [user]);
51
57
 
58
+ // Add effect to handle mobile state changes
59
+ useEffect(() => {
60
+ setExpanded(!isMobile || onlyProfile);
61
+ }, [isMobile, onlyProfile]);
62
+
52
63
  const onUpdateUserStatus = async (v: UserMetadata['status']) => {
53
64
  if (!isMyself) {
54
65
  return;
@@ -72,6 +83,10 @@ export default function UserBasicInfo({
72
83
  }
73
84
  };
74
85
 
86
+ const toggleExpand = () => {
87
+ setExpanded(!expanded);
88
+ };
89
+
75
90
  if (!user) {
76
91
  return null;
77
92
  }
@@ -187,14 +202,37 @@ export default function UserBasicInfo({
187
202
  <DID did={user.did} showQrcode copyable compact={!showFullDid} responsive={!showFullDid} locale={locale} />
188
203
  </Box>
189
204
  </Box>
190
- <UserMetadataComponent isMyself={isMyself} user={user} onSave={onSave} />
205
+ <UserMetadataComponent isMobile={isMobile} isMyself={isMyself} user={user} onSave={onSave} />
191
206
  {isMyself ? (
192
207
  <>
193
- <Divider sx={{ my: 3, borderColor: colors.dividerColor }} />
194
- <Typography component="p" color="text.secondary" fontSize="14px" mb={2}>
195
- {t('profile.justForYou')}
196
- </Typography>
197
- <UserInfo user={user} isMySelf={isMyself} />
208
+ <Divider sx={{ my: isMobile ? 1 : 3, borderColor: colors.dividerColor }} />
209
+ {isMobile && !onlyProfile ? (
210
+ <Box
211
+ sx={{
212
+ display: 'flex',
213
+ justifyContent: 'center',
214
+ mb: 0,
215
+ }}>
216
+ <IconButton
217
+ size="small"
218
+ onClick={toggleExpand}
219
+ sx={{
220
+ backgroundColor: colors.backgroundsBgField,
221
+ '&:hover': {
222
+ backgroundColor: colors.backgroundsBgField,
223
+ opacity: 0.8,
224
+ },
225
+ }}>
226
+ {expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
227
+ </IconButton>
228
+ </Box>
229
+ ) : null}
230
+ <Collapse in={expanded} timeout="auto">
231
+ <Typography component="p" color="text.secondary" fontSize="14px" mb={2}>
232
+ {t('profile.justForYou')}
233
+ </Typography>
234
+ <UserInfo user={user} isMySelf={isMyself} />
235
+ </Collapse>
198
236
  </>
199
237
  ) : null}
200
238
  </Box>