@carlonicora/nextjs-jsonapi 1.66.0 → 1.68.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 (63) hide show
  1. package/dist/{BlockNoteEditor-KCJMA6LW.mjs → BlockNoteEditor-6FDECIS2.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-GQM2TZG2.js → BlockNoteEditor-DXHROT4C.js} +14 -14
  3. package/dist/{BlockNoteEditor-GQM2TZG2.js.map → BlockNoteEditor-DXHROT4C.js.map} +1 -1
  4. package/dist/billing/index.js +346 -346
  5. package/dist/billing/index.mjs +3 -3
  6. package/dist/{chunk-NVXYOQFW.js → chunk-37KYO2UD.js} +20 -5
  7. package/dist/chunk-37KYO2UD.js.map +1 -0
  8. package/dist/{chunk-QIFM4G7T.js → chunk-ELTHSXBI.js} +1476 -1298
  9. package/dist/chunk-ELTHSXBI.js.map +1 -0
  10. package/dist/{chunk-4E74ZTRT.mjs → chunk-H4ZS3R76.mjs} +2606 -2428
  11. package/dist/chunk-H4ZS3R76.mjs.map +1 -0
  12. package/dist/{chunk-35GWVOYZ.mjs → chunk-IOMDNRX5.mjs} +20 -5
  13. package/dist/{chunk-35GWVOYZ.mjs.map → chunk-IOMDNRX5.mjs.map} +1 -1
  14. package/dist/{chunk-OQRBY22T.js → chunk-WOJIRXIP.js} +11 -11
  15. package/dist/{chunk-OQRBY22T.js.map → chunk-WOJIRXIP.js.map} +1 -1
  16. package/dist/{chunk-UXGPZZ6V.mjs → chunk-WVTBEVAL.mjs} +2 -2
  17. package/dist/client/index.js +4 -4
  18. package/dist/client/index.mjs +3 -3
  19. package/dist/components/index.d.mts +29 -7
  20. package/dist/components/index.d.ts +29 -7
  21. package/dist/components/index.js +8 -4
  22. package/dist/components/index.js.map +1 -1
  23. package/dist/components/index.mjs +9 -5
  24. package/dist/contexts/index.d.mts +1 -1
  25. package/dist/contexts/index.d.ts +1 -1
  26. package/dist/contexts/index.js +4 -4
  27. package/dist/contexts/index.mjs +3 -3
  28. package/dist/core/index.d.mts +2 -2
  29. package/dist/core/index.d.ts +2 -2
  30. package/dist/core/index.js +2 -2
  31. package/dist/core/index.mjs +1 -1
  32. package/dist/index.d.mts +2 -2
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +3 -3
  35. package/dist/index.mjs +2 -2
  36. package/dist/{s3.service-XchHd3ii.d.mts → s3.service-CHOTwfWA.d.mts} +7 -0
  37. package/dist/{s3.service-DIR6Su9B.d.ts → s3.service-N1g0piXD.d.ts} +7 -0
  38. package/dist/server/index.d.mts +1 -1
  39. package/dist/server/index.d.ts +1 -1
  40. package/dist/server/index.js +3 -3
  41. package/dist/server/index.mjs +1 -1
  42. package/package.json +1 -1
  43. package/src/components/EditableAvatar.tsx +175 -0
  44. package/src/components/index.ts +1 -0
  45. package/src/features/company/components/forms/CompanyEditor.tsx +1 -3
  46. package/src/features/role/components/forms/FormRoles.tsx +5 -4
  47. package/src/features/user/components/containers/AllUsersListContainer.tsx +36 -0
  48. package/src/features/user/components/containers/UserContainer.tsx +10 -13
  49. package/src/features/user/components/containers/UsersListContainer.tsx +15 -24
  50. package/src/features/user/components/containers/index.ts +1 -0
  51. package/src/features/user/components/details/UserContent.tsx +92 -0
  52. package/src/features/user/components/details/index.ts +1 -1
  53. package/src/features/user/components/forms/UserEditor.tsx +233 -233
  54. package/src/features/user/components/lists/CompanyUsersList.tsx +3 -1
  55. package/src/features/user/contexts/UserContext.tsx +1 -6
  56. package/src/features/user/data/user.service.ts +18 -0
  57. package/src/features/user/data/user.ts +3 -4
  58. package/dist/chunk-4E74ZTRT.mjs.map +0 -1
  59. package/dist/chunk-NVXYOQFW.js.map +0 -1
  60. package/dist/chunk-QIFM4G7T.js.map +0 -1
  61. package/src/features/user/components/details/UserDetails.tsx +0 -74
  62. /package/dist/{BlockNoteEditor-KCJMA6LW.mjs.map → BlockNoteEditor-6FDECIS2.mjs.map} +0 -0
  63. /package/dist/{chunk-UXGPZZ6V.mjs.map → chunk-WVTBEVAL.mjs.map} +0 -0
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunkNVXYOQFWjs = require('../chunk-NVXYOQFW.js');
18
+ var _chunk37KYO2UDjs = require('../chunk-37KYO2UD.js');
19
19
  require('../chunk-LXKSUWAV.js');
20
20
  require('../chunk-IBS6NI7D.js');
21
21
 
@@ -86,7 +86,7 @@ var ServerSession = class {
86
86
  if (!rawModules) return false;
87
87
  const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
88
88
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
89
- return _chunkNVXYOQFWjs.checkPermissionsFromServer.call(void 0, {
89
+ return _chunk37KYO2UDjs.checkPermissionsFromServer.call(void 0, {
90
90
  module: params.module,
91
91
  action: params.action,
92
92
  data: params.data,
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
296
296
 
297
297
 
298
298
 
299
- exports.ServerAuthService = _chunkNVXYOQFWjs.AuthService; exports.ServerCompanyService = _chunkNVXYOQFWjs.CompanyService; exports.ServerContentService = _chunkNVXYOQFWjs.ContentService; exports.ServerFeatureService = _chunkNVXYOQFWjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkNVXYOQFWjs.NotificationService; exports.ServerPushService = _chunkNVXYOQFWjs.PushService; exports.ServerRoleService = _chunkNVXYOQFWjs.RoleService; exports.ServerS3Service = _chunkNVXYOQFWjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkNVXYOQFWjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
299
+ exports.ServerAuthService = _chunk37KYO2UDjs.AuthService; exports.ServerCompanyService = _chunk37KYO2UDjs.CompanyService; exports.ServerContentService = _chunk37KYO2UDjs.ContentService; exports.ServerFeatureService = _chunk37KYO2UDjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk37KYO2UDjs.NotificationService; exports.ServerPushService = _chunk37KYO2UDjs.PushService; exports.ServerRoleService = _chunk37KYO2UDjs.RoleService; exports.ServerS3Service = _chunk37KYO2UDjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk37KYO2UDjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
300
300
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-35GWVOYZ.mjs";
18
+ } from "../chunk-IOMDNRX5.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.66.0",
3
+ "version": "1.68.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { Avatar, AvatarFallback, AvatarImage } from "../shadcnui";
4
+ import { useCurrentUserContext } from "../contexts";
5
+ import { S3Interface } from "../features/s3/data/s3.interface";
6
+ import { S3Service } from "../features/s3/data/s3.service";
7
+ import { ModuleWithPermissions } from "../permissions";
8
+ import { errorToast } from "./errors/errorToast";
9
+ import { PencilIcon, Trash2Icon } from "lucide-react";
10
+ import { useTranslations } from "next-intl";
11
+ import { useCallback, useRef, useState } from "react";
12
+ import { cn } from "../utils/cn";
13
+
14
+ type EditableAvatarProps = {
15
+ entityId: string;
16
+ module: ModuleWithPermissions;
17
+ image?: string;
18
+ fallback: string;
19
+ alt: string;
20
+ patchImage: (imageKey: string) => Promise<void>;
21
+ className?: string;
22
+ fallbackClassName?: string;
23
+ };
24
+
25
+ export function EditableAvatar({
26
+ entityId,
27
+ module,
28
+ image,
29
+ fallback,
30
+ alt,
31
+ patchImage,
32
+ className,
33
+ fallbackClassName,
34
+ }: EditableAvatarProps) {
35
+ const { company } = useCurrentUserContext();
36
+ const t = useTranslations();
37
+ const fileInputRef = useRef<HTMLInputElement>(null);
38
+
39
+ // Optimistic state: null means "use the prop", string means "override"
40
+ const [optimisticImage, setOptimisticImage] = useState<string | null>(null);
41
+ const [isUploading, setIsUploading] = useState(false);
42
+
43
+ const displayImage = optimisticImage ?? image;
44
+
45
+ const generateS3Key = useCallback(
46
+ (file: File) => {
47
+ const ext = file.type.split("/").pop() ?? "";
48
+ const ts = new Date().toISOString().replace(/[-:T]/g, "").split(".")[0];
49
+ return `companies/${company!.id}/${module.name}/${entityId}/${entityId}.${ts}.${ext}`;
50
+ },
51
+ [company, module.name, entityId],
52
+ );
53
+
54
+ const handleFile = useCallback(
55
+ async (file: File) => {
56
+ if (isUploading) return;
57
+ if (!company) return;
58
+
59
+ const previousImage = image;
60
+ const previewUrl = URL.createObjectURL(file);
61
+ setOptimisticImage(previewUrl);
62
+ setIsUploading(true);
63
+
64
+ try {
65
+ const s3Key = generateS3Key(file);
66
+
67
+ const s3: S3Interface = await S3Service.getPreSignedUrl({
68
+ key: s3Key,
69
+ contentType: file.type,
70
+ isPublic: true,
71
+ });
72
+
73
+ const uploadResponse = await fetch(s3.url, {
74
+ method: "PUT",
75
+ headers: s3.headers,
76
+ body: file,
77
+ });
78
+
79
+ if (!uploadResponse.ok) {
80
+ throw new Error(`S3 upload failed: ${uploadResponse.status}`);
81
+ }
82
+
83
+ await patchImage(s3Key);
84
+ setOptimisticImage(null);
85
+ } catch (error) {
86
+ setOptimisticImage(previousImage ?? null);
87
+ errorToast({ title: t("generic.errors.update"), error });
88
+ } finally {
89
+ URL.revokeObjectURL(previewUrl);
90
+ setIsUploading(false);
91
+ }
92
+ },
93
+ [company, generateS3Key, image, isUploading, patchImage, t],
94
+ );
95
+
96
+ const handleRemove = useCallback(async () => {
97
+ if (isUploading) return;
98
+
99
+ const previousImage = image;
100
+ setOptimisticImage("");
101
+ setIsUploading(true);
102
+
103
+ try {
104
+ await patchImage("");
105
+ setOptimisticImage(null);
106
+ } catch (error) {
107
+ setOptimisticImage(previousImage ?? null);
108
+ errorToast({ title: t("generic.errors.update"), error });
109
+ } finally {
110
+ setIsUploading(false);
111
+ }
112
+ }, [image, isUploading, patchImage, t]);
113
+
114
+ const handleFileInputChange = useCallback(
115
+ (e: React.ChangeEvent<HTMLInputElement>) => {
116
+ const file = e.target.files?.[0];
117
+ if (file) handleFile(file);
118
+ e.target.value = "";
119
+ },
120
+ [handleFile],
121
+ );
122
+
123
+ const handleDrop = useCallback(
124
+ (e: React.DragEvent) => {
125
+ e.preventDefault();
126
+ const file = e.dataTransfer.files?.[0];
127
+ if (file && file.type.startsWith("image/")) {
128
+ handleFile(file);
129
+ }
130
+ },
131
+ [handleFile],
132
+ );
133
+
134
+ const handleDragOver = useCallback((e: React.DragEvent) => {
135
+ e.preventDefault();
136
+ }, []);
137
+
138
+ return (
139
+ <div className={cn("group relative", className)} onDrop={handleDrop} onDragOver={handleDragOver}>
140
+ <Avatar className="h-full w-full">
141
+ {displayImage ? <AvatarImage src={displayImage} alt={alt} /> : null}
142
+ <AvatarFallback className={fallbackClassName}>{fallback}</AvatarFallback>
143
+ </Avatar>
144
+
145
+ {/* Hover overlay */}
146
+ <div
147
+ className={cn(
148
+ "absolute inset-0 flex items-center justify-center gap-x-2 rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100",
149
+ isUploading && "opacity-100",
150
+ )}
151
+ >
152
+ <button
153
+ type="button"
154
+ onClick={() => fileInputRef.current?.click()}
155
+ disabled={isUploading}
156
+ className="rounded-full p-2 text-white hover:bg-white/20 disabled:opacity-50"
157
+ >
158
+ <PencilIcon className="h-4 w-4" />
159
+ </button>
160
+ {displayImage && (
161
+ <button
162
+ type="button"
163
+ onClick={handleRemove}
164
+ disabled={isUploading}
165
+ className="rounded-full p-2 text-white hover:bg-white/20 disabled:opacity-50"
166
+ >
167
+ <Trash2Icon className="h-4 w-4" />
168
+ </button>
169
+ )}
170
+ </div>
171
+
172
+ <input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleFileInputChange} />
173
+ </div>
174
+ );
175
+ }
@@ -1,4 +1,5 @@
1
1
  export { EntityAvatar } from "./EntityAvatar";
2
+ export { EditableAvatar } from "./EditableAvatar";
2
3
  export { TableCellAvatar } from "./TableCellAvatar";
3
4
  export { getInitials } from "../utils/getInitials";
4
5
  export * from "./containers";
@@ -195,9 +195,7 @@ function CompanyEditorInternal({
195
195
  fiscal_data: fiscalRef.current ? JSON.stringify(fiscalRef.current.getData()) : undefined,
196
196
  };
197
197
 
198
- const updatedCompany = company
199
- ? await CompanyService.update(payload)
200
- : await CompanyService.create(payload);
198
+ const updatedCompany = company ? await CompanyService.update(payload) : await CompanyService.create(payload);
201
199
 
202
200
  // Refresh user context after company changes
203
201
  const fullUser = await UserService.findFullUser();
@@ -22,7 +22,7 @@ export function FormRoles({ form, id, name, roles }: FormRolesProps) {
22
22
  <div className="flex w-full flex-col">
23
23
  <FormFieldWrapper form={form} name={id} label={name}>
24
24
  {(field) => (
25
- <div>
25
+ <div className="flex w-full flex-col gap-y-1">
26
26
  {roles
27
27
  .filter((role: RoleInterface) => role.isSelectable)
28
28
  .sort((a: RoleInterface, b: RoleInterface) => a.name.localeCompare(b.name))
@@ -30,9 +30,10 @@ export function FormRoles({ form, id, name, roles }: FormRolesProps) {
30
30
  if (role.requiredFeature && !hasAccesToFeature(role.requiredFeature.id)) return null;
31
31
 
32
32
  return (
33
- <div key={role.id}>
33
+ <div key={role.id} className="flex w-full items-center">
34
34
  <Checkbox
35
- defaultChecked={(field.value as string[]).some((roleId: string) => roleId === role.id)}
35
+ id={`role-${role.id}`}
36
+ checked={(field.value as string[]).some((roleId: string) => roleId === role.id)}
36
37
  onCheckedChange={(checked) => {
37
38
  if (checked) {
38
39
  form.setValue(id, [...(field.value as string[]), role.id]);
@@ -46,7 +47,7 @@ export function FormRoles({ form, id, name, roles }: FormRolesProps) {
46
47
  />
47
48
  <Tooltip>
48
49
  <TooltipTrigger>
49
- <FieldLabel className="ml-3 font-normal">
50
+ <FieldLabel htmlFor={`role-${role.id}`} className="ml-3 cursor-pointer font-normal">
50
51
  {t(`role.roles`, { role: role.id.replaceAll(`-`, ``) })}
51
52
  </FieldLabel>
52
53
  </TooltipTrigger>
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { CompanyUsersList, Tab, TabsContainer } from "../../../../components";
5
+ import { Modules } from "../../../../core";
6
+ import { Action } from "../../../../permissions";
7
+ import { useCurrentUserContext } from "../../contexts";
8
+ import { UserInterface } from "../../data";
9
+
10
+ function AllUsersListContainerInternal() {
11
+ const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
12
+ const t = useTranslations();
13
+
14
+ if (!hasPermissionToModule({ module: Modules.User, action: Action.Delete })) return <CompanyUsersList />;
15
+
16
+ const tabs: Tab[] = [
17
+ {
18
+ label: t(`entities.users`, { count: 2 }),
19
+ content: <CompanyUsersList />,
20
+ modules: [Modules.Company],
21
+ action: Action.Read,
22
+ },
23
+ {
24
+ label: t(`user.deleted`),
25
+ content: <CompanyUsersList isDeleted={true} />,
26
+ modules: [Modules.Company],
27
+ action: Action.Update,
28
+ },
29
+ ];
30
+
31
+ return <TabsContainer tabs={tabs} />;
32
+ }
33
+
34
+ export function AllUsersListContainer() {
35
+ return <AllUsersListContainerInternal />;
36
+ }
@@ -1,23 +1,20 @@
1
1
  "use client";
2
2
 
3
- import { useTranslations } from "next-intl";
3
+ import { RoundPageContainer, Tab } from "@/components";
4
+ import { Modules } from "@/core";
4
5
  import { useUserContext } from "../../contexts";
5
- import { UserDetails } from "../details";
6
+ import { UserContent } from "../details";
6
7
 
7
8
  export function UserContainer() {
8
9
  const { user } = useUserContext();
9
10
  if (!user) return null;
10
11
 
11
- const _t = useTranslations();
12
+ const tabs: Tab[] = [
13
+ {
14
+ label: "Details",
15
+ content: <UserContent user={user} />,
16
+ },
17
+ ];
12
18
 
13
- return (
14
- <div className="flex w-full gap-x-4">
15
- <div className="w-2xl flex h-[calc(100vh-theme(spacing.20))] flex-col justify-between border-r pr-4">
16
- <div className="flex h-full overflow-y-auto">
17
- <UserDetails user={user} />
18
- </div>
19
- </div>
20
- <div className="flex w-full flex-col gap-y-4"></div>
21
- </div>
22
- );
19
+ return <RoundPageContainer module={Modules.User} tabs={tabs} />;
23
20
  }
@@ -1,36 +1,27 @@
1
1
  "use client";
2
2
 
3
- import { useTranslations } from "next-intl";
4
- import { CompanyUsersList, Tab, TabsContainer } from "../../../../components";
3
+ import { CompanyUsersList, RoundPageContainer } from "../../../../components";
5
4
  import { Modules } from "../../../../core";
6
5
  import { Action } from "../../../../permissions";
7
6
  import { useCurrentUserContext } from "../../contexts";
8
7
  import { UserInterface } from "../../data";
9
8
 
10
- function UsersListContainerInternal() {
11
- const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
12
- const t = useTranslations();
9
+ type UsersListContainerProps = {
10
+ fullWidth?: boolean;
11
+ };
13
12
 
14
- if (!hasPermissionToModule({ module: Modules.User, action: Action.Delete })) return <CompanyUsersList />;
13
+ function UsersListContainerInternal({ fullWidth }: UsersListContainerProps) {
14
+ return (
15
+ <RoundPageContainer module={Modules.User} fullWidth={fullWidth}>
16
+ <CompanyUsersList fullWidth={fullWidth} />
17
+ </RoundPageContainer>
18
+ );
19
+ }
15
20
 
16
- const tabs: Tab[] = [
17
- {
18
- label: t(`entities.users`, { count: 2 }),
19
- content: <CompanyUsersList />,
20
- modules: [Modules.Company],
21
- action: Action.Read,
22
- },
23
- {
24
- label: t(`user.deleted`),
25
- content: <CompanyUsersList isDeleted={true} />,
26
- modules: [Modules.Company],
27
- action: Action.Update,
28
- },
29
- ];
21
+ export function UsersListContainer({ fullWidth }: UsersListContainerProps) {
22
+ const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
30
23
 
31
- return <TabsContainer tabs={tabs} />;
32
- }
24
+ if (!hasPermissionToModule({ module: Modules.User, action: Action.Read })) return null;
33
25
 
34
- export function UsersListContainer() {
35
- return <UsersListContainerInternal />;
26
+ return <UsersListContainerInternal fullWidth={fullWidth} />;
36
27
  }
@@ -1,3 +1,4 @@
1
+ export * from "./AllUsersListContainer";
1
2
  export * from "./UserContainer";
2
3
  export * from "./UserIndexContainer";
3
4
  export * from "./UsersListContainer";
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { AttributeElement } from "../../../../components/contents";
5
+ import { EditableAvatar } from "../../../../components/EditableAvatar";
6
+ import { getInitials } from "../../../../utils/getInitials";
7
+ import { Modules } from "../../../../core";
8
+ import { usePageUrlGenerator } from "../../../../hooks";
9
+ import { Badge, Link } from "../../../../shadcnui";
10
+ import { RoleInterface } from "../../../role";
11
+ import { UserInterface } from "../../data";
12
+ import { UserService } from "../../data/user.service";
13
+ import { useUserContext } from "../../contexts";
14
+ import { BriefcaseIcon, MailIcon, PhoneIcon } from "lucide-react";
15
+
16
+ type UserContentProps = {
17
+ user: UserInterface;
18
+ };
19
+
20
+ export function UserContent({ user }: UserContentProps) {
21
+ const t = useTranslations();
22
+ const generateUrl = usePageUrlGenerator();
23
+ const { setUser } = useUserContext();
24
+
25
+ const hasBio = !!user.bio;
26
+
27
+ return (
28
+ <div className="flex flex-col gap-y-8">
29
+ {/* Hero Section */}
30
+ <div className="flex items-start gap-x-6">
31
+ <EditableAvatar
32
+ entityId={user.id}
33
+ module={Modules.User}
34
+ image={user.avatar}
35
+ fallback={getInitials(user.name)}
36
+ alt={user.name}
37
+ patchImage={async (imageKey) => {
38
+ const updated = await UserService.patchAvatar({ id: user.id, avatar: imageKey });
39
+ setUser(updated);
40
+ }}
41
+ className="h-24 w-24"
42
+ fallbackClassName="text-2xl"
43
+ />
44
+ <div className="flex flex-col gap-y-2">
45
+ {user.roles && user.roles.length > 0 && (
46
+ <div className="flex flex-wrap gap-2">
47
+ {user.roles.map((role: RoleInterface) => (
48
+ <Link key={role.id} href={generateUrl({ page: Modules.Role, id: role.id })}>
49
+ <Badge variant="default">{t(`role.roles`, { role: role.id.replaceAll(`-`, ``) })}</Badge>
50
+ </Link>
51
+ ))}
52
+ </div>
53
+ )}
54
+ {user.isDeleted ? (
55
+ <div>
56
+ <Badge variant="destructive">{t(`user.errors.deleted`)}</Badge>
57
+ </div>
58
+ ) : (
59
+ <>
60
+ {!user.isActivated && (
61
+ <div>
62
+ <Badge variant="destructive">{t(`user.errors.inactive`)}</Badge>
63
+ </div>
64
+ )}
65
+ </>
66
+ )}
67
+ {user.title && (
68
+ <div className="text-muted-foreground flex items-center gap-x-2 text-sm">
69
+ <BriefcaseIcon className="h-4 w-4 shrink-0" />
70
+ {user.title}
71
+ </div>
72
+ )}
73
+ {user.email && (
74
+ <div className="text-muted-foreground flex items-center gap-x-2 text-sm">
75
+ <MailIcon className="h-4 w-4 shrink-0" />
76
+ {user.email}
77
+ </div>
78
+ )}
79
+ {user.phone && (
80
+ <div className="text-muted-foreground flex items-center gap-x-2 text-sm">
81
+ <PhoneIcon className="h-4 w-4 shrink-0" />
82
+ {user.phone}
83
+ </div>
84
+ )}
85
+ </div>
86
+ </div>
87
+
88
+ {/* Bio Section */}
89
+ {hasBio && <AttributeElement title={t(`user.fields.bio.label`)} value={user.bio} />}
90
+ </div>
91
+ );
92
+ }
@@ -1,3 +1,3 @@
1
- export * from "./UserDetails";
1
+ export * from "./UserContent";
2
2
  export * from "./UserIndexDetails";
3
3
  export * from "./UserStandaloneDetails";