@studiocubics/cms 0.0.1

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 (85) hide show
  1. package/.turbo/turbo-build.log +7 -0
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +15 -0
  4. package/eslint.config.js +21 -0
  5. package/package.json +79 -0
  6. package/rollup.config.js +48 -0
  7. package/src/clerk/_index.ts +6 -0
  8. package/src/clerk/actions/_index.ts +2 -0
  9. package/src/clerk/actions/invitations.ts +78 -0
  10. package/src/clerk/actions/systemUsers.ts +94 -0
  11. package/src/clerk/auth.ts +34 -0
  12. package/src/clerk/clerk.d.ts +105 -0
  13. package/src/clerk/hasPermission.ts +96 -0
  14. package/src/clerk/rbacConfig.ts +68 -0
  15. package/src/clerk/schemas/_index.ts +1 -0
  16. package/src/clerk/schemas/invitation.ts +17 -0
  17. package/src/clerk/schemas/systemUser.ts +16 -0
  18. package/src/clerk/toClientSafeUser.ts +77 -0
  19. package/src/constants/_index.ts +2 -0
  20. package/src/constants/defaults.tsx +62 -0
  21. package/src/constants/pageLimits.ts +2 -0
  22. package/src/declaration.d.ts +5 -0
  23. package/src/index.ts +5 -0
  24. package/src/providers/CMSRootProviders.tsx +13 -0
  25. package/src/providers/_index.ts +1 -0
  26. package/src/routes.d.ts +96 -0
  27. package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.module.css +4 -0
  28. package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.tsx +16 -0
  29. package/src/ui/Inputs/_index.ts +1 -0
  30. package/src/ui/Layout/CMSSecurityLayout.tsx +27 -0
  31. package/src/ui/Layout/CMSSidebar/CMSSidebar.tsx +39 -0
  32. package/src/ui/Layout/CMSSidebar/CMSSidebarBody.tsx +43 -0
  33. package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.module.css +7 -0
  34. package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.tsx +59 -0
  35. package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.module.css +44 -0
  36. package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.tsx +30 -0
  37. package/src/ui/Layout/CMSSidebar/_index.ts +4 -0
  38. package/src/ui/Layout/_index.ts +2 -0
  39. package/src/ui/System/Auth/SignIn/SignIn.module.css +50 -0
  40. package/src/ui/System/Auth/SignIn/SignIn.tsx +79 -0
  41. package/src/ui/System/Auth/SignIn/_index.ts +2 -0
  42. package/src/ui/System/Auth/SignIn/useSignInForm.tsx +42 -0
  43. package/src/ui/System/Auth/SignUp/SignUp.module.css +48 -0
  44. package/src/ui/System/Auth/SignUp/SignUp.tsx +138 -0
  45. package/src/ui/System/Auth/SignUp/_index.ts +2 -0
  46. package/src/ui/System/Auth/SignUp/useSignUpForm.tsx +54 -0
  47. package/src/ui/System/Auth/_index.ts +2 -0
  48. package/src/ui/System/Invitations/InvitationList.tsx +9 -0
  49. package/src/ui/System/Invitations/InvitationListActions.tsx +167 -0
  50. package/src/ui/System/Invitations/InvitationListCard.tsx +79 -0
  51. package/src/ui/System/Invitations/InvitationListPage.tsx +32 -0
  52. package/src/ui/System/Invitations/InvitationListPagination.tsx +19 -0
  53. package/src/ui/System/Invitations/_index.ts +5 -0
  54. package/src/ui/System/Permissions/RoleListCard.tsx +33 -0
  55. package/src/ui/System/Permissions/RolePermissionsPage.tsx +18 -0
  56. package/src/ui/System/Permissions/RolePermissionsTable.tsx +36 -0
  57. package/src/ui/System/Permissions/_index.ts +3 -0
  58. package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.module.css +5 -0
  59. package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.tsx +102 -0
  60. package/src/ui/System/SystemUser/CurrentSystemUserPage.tsx +12 -0
  61. package/src/ui/System/SystemUser/SystemUserActions.tsx +45 -0
  62. package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.module.css +6 -0
  63. package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.tsx +71 -0
  64. package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.module.css +7 -0
  65. package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.tsx +114 -0
  66. package/src/ui/System/SystemUser/SystemUserList.tsx +18 -0
  67. package/src/ui/System/SystemUser/SystemUserListActions.tsx +17 -0
  68. package/src/ui/System/SystemUser/SystemUserListCard.tsx +85 -0
  69. package/src/ui/System/SystemUser/SystemUserListPage.tsx +33 -0
  70. package/src/ui/System/SystemUser/SystemUserListPagination.tsx +19 -0
  71. package/src/ui/System/SystemUser/SystemUserPage.tsx +30 -0
  72. package/src/ui/System/SystemUser/SystemUserPageContent.tsx +54 -0
  73. package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.module.css +17 -0
  74. package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.tsx +64 -0
  75. package/src/ui/System/SystemUser/SystemUserRoleForm/SystemUserRoleForm.tsx +51 -0
  76. package/src/ui/System/SystemUser/SystemUserTimestamps.tsx +56 -0
  77. package/src/ui/System/SystemUser/_index.ts +14 -0
  78. package/src/ui/System/WelcomePage/WelcomePage.module.css +18 -0
  79. package/src/ui/System/WelcomePage/WelcomePage.tsx +43 -0
  80. package/src/ui/System/_index.ts +6 -0
  81. package/src/ui/System/types.ts +7 -0
  82. package/src/ui/_index.ts +3 -0
  83. package/src/utils/_index.ts +1 -0
  84. package/src/utils/proxyFunctions.ts +37 -0
  85. package/tsconfig.json +32 -0
@@ -0,0 +1,33 @@
1
+ import {
2
+ AccordionItem,
3
+ CollectionItemCard,
4
+ SectionWrapper,
5
+ } from "@studiocubics/ui";
6
+ import { RolePermissionsTable } from "./RolePermissionsTable";
7
+ import type { Role, RoleDoc } from "../../../clerk/rbacConfig";
8
+
9
+ export interface RoleCardProps {
10
+ role: Role;
11
+ rbacConfig: RoleDoc;
12
+ }
13
+
14
+ export function RoleListCard(props: RoleCardProps) {
15
+ const { role, rbacConfig } = props;
16
+
17
+ return (
18
+ <AccordionItem
19
+ name="RoleCard"
20
+ summary={
21
+ <CollectionItemCard
22
+ title={role}
23
+ chip={rbacConfig.isSystem ? "System User" : ""}
24
+ description={rbacConfig.desc}
25
+ />
26
+ }
27
+ >
28
+ <SectionWrapper title={`${role}'s Permissions`} noBorders>
29
+ <RolePermissionsTable {...props} />
30
+ </SectionWrapper>
31
+ </AccordionItem>
32
+ );
33
+ }
@@ -0,0 +1,18 @@
1
+ import { PageLayoutPagination, type TabProps } from "@studiocubics/ui";
2
+ import { RoleListCard } from "./RoleListCard";
3
+ import { RBAC_CONFIG } from "../../../clerk/rbacConfig";
4
+
5
+ export function RolePermissionsPage({
6
+ securityLinks,
7
+ }: {
8
+ securityLinks: TabProps[];
9
+ }) {
10
+ return (
11
+ <PageLayoutPagination size="sm" title={securityLinks[1]?.children}>
12
+ {(Object.keys(RBAC_CONFIG) as (keyof typeof RBAC_CONFIG)[]).map((k) => {
13
+ const item = RBAC_CONFIG[k];
14
+ return <RoleListCard key={k} role={k} rbacConfig={item} />;
15
+ })}
16
+ </PageLayoutPagination>
17
+ );
18
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { Check, X } from "lucide-react";
4
+ import type { RoleCardProps } from "./RoleListCard";
5
+ import { useCallback, useMemo } from "react";
6
+ import { Table, type TableData } from "@studiocubics/ui";
7
+
8
+ export function RolePermissionsTable({ rbacConfig }: RoleCardProps) {
9
+ const permissions = rbacConfig.permissions;
10
+ const icon = useCallback((check?: boolean) => {
11
+ return check ? <Check /> : <X />;
12
+ }, []);
13
+ const tableData: TableData = useMemo(
14
+ () => [
15
+ ["", "create", "delete", "read", "update"],
16
+ ...permissions.map((p) => [
17
+ p.resource,
18
+ icon(p.actions.create),
19
+ icon(p.actions.delete),
20
+ icon(p.actions.read),
21
+ icon(p.actions.update),
22
+ ]),
23
+ ],
24
+ [permissions],
25
+ );
26
+
27
+ if (!permissions || !permissions.length)
28
+ return (
29
+ <p>
30
+ This is a root system user role, all resource actions are permitted to
31
+ anyone with this role. Make sure to always have atleast one admin user.
32
+ </p>
33
+ );
34
+
35
+ return <Table data={tableData} />;
36
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./RoleListCard";
2
+ export * from "./RolePermissionsPage";
3
+ export * from "./RolePermissionsTable";
@@ -0,0 +1,5 @@
1
+ .title {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--spacing-gap);
5
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useClerk, useUser } from "@clerk/nextjs";
4
+ import { useAnchorElement, useDisclosure } from "@studiocubics/hooks";
5
+ import {
6
+ Chip,
7
+ ConfirmationForm,
8
+ Dialog,
9
+ IdentityDisplay,
10
+ type IdentityDisplayProps,
11
+ List,
12
+ ListItem,
13
+ Popover,
14
+ Skeleton,
15
+ } from "@studiocubics/ui";
16
+ import { toCapitalised } from "@studiocubics/utils";
17
+ import { LogOut, UserCog } from "lucide-react";
18
+ import type { SubmitEvent } from "react";
19
+ import styles from "./CurrentSystemUserButton.module.css";
20
+ import type { Role } from "../../../../clerk/rbacConfig";
21
+
22
+ export function CurrentSystemUserButton(
23
+ props: { clickable?: boolean } & Omit<IdentityDisplayProps, "profileName">,
24
+ ) {
25
+ const user = useUser();
26
+
27
+ const { clickable = true, ...rest } = props;
28
+ const { open, anchorEl, handleClick, handleClose } = useAnchorElement();
29
+
30
+ if (!user.isLoaded) return <Skeleton height={78} />;
31
+
32
+ return (
33
+ <>
34
+ <IdentityDisplay
35
+ onClick={clickable ? handleClick : undefined}
36
+ profileImage={user.user?.imageUrl ?? ""}
37
+ role={user.user?.primaryEmailAddress?.emailAddress}
38
+ {...rest}
39
+ profileName={
40
+ <div className={styles.title}>
41
+ <h4>{user.user?.fullName}</h4>{" "}
42
+ <Chip size="sm" color="primary">
43
+ <strong>
44
+ {toCapitalised(user.user?.publicMetadata.role as Role)}
45
+ </strong>
46
+ </Chip>
47
+ </div>
48
+ }
49
+ />
50
+ {clickable && (
51
+ <Popover
52
+ anchorEl={anchorEl}
53
+ open={open}
54
+ onClose={handleClose}
55
+ anchorPosition={{ vertical: "top", horizontal: "right" }}
56
+ transformOrigin={{ vertical: "bottom", horizontal: "left" }}
57
+ >
58
+ <List className="surfaceContainer">
59
+ <ListItem
60
+ startIcon={<UserCog />}
61
+ href="/dashboard/security/account"
62
+ >
63
+ Account Settings
64
+ </ListItem>
65
+ <Logout />
66
+ </List>
67
+ </Popover>
68
+ )}
69
+ </>
70
+ );
71
+ }
72
+ export function Logout() {
73
+ const { open, handleClose, handleStrictClose, handleOpen } = useDisclosure();
74
+ const clerk = useClerk();
75
+ async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
76
+ e.preventDefault();
77
+ await clerk.signOut();
78
+ }
79
+ return (
80
+ <>
81
+ <ListItem
82
+ className={styles.logoutButton}
83
+ onClick={handleOpen}
84
+ // disabled
85
+ startIcon={<LogOut />}
86
+ color="error"
87
+ >
88
+ Log out
89
+ </ListItem>
90
+ <Dialog open={open} onClose={handleStrictClose}>
91
+ <ConfirmationForm
92
+ variant="danger"
93
+ formTitle="Logout"
94
+ onCancel={handleClose}
95
+ onSubmit={handleSubmit}
96
+ >
97
+ Are you sure you want to sign out of this account?
98
+ </ConfirmationForm>
99
+ </Dialog>
100
+ </>
101
+ );
102
+ }
@@ -0,0 +1,12 @@
1
+ import { currentUser } from "@clerk/nextjs/server";
2
+ import { redirect } from "next/navigation";
3
+ import { SystemUserPageContent } from "./SystemUserPageContent";
4
+ import { auth } from "../../../clerk/auth";
5
+
6
+ export async function CurrentSystemUserPage() {
7
+ const user = await currentUser();
8
+ const session = await auth();
9
+ const allowEdit = await session.hasPermission("systemUsers", "update");
10
+ if (!user) redirect("/auth/signIn/");
11
+ return <SystemUserPageContent user={user} allowEdit={allowEdit} />;
12
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import type { User } from "@clerk/nextjs/server";
4
+ import { useDisclosure } from "@studiocubics/hooks";
5
+ import {
6
+ ConfirmationForm,
7
+ Dialog,
8
+ ListItem,
9
+ type ListItemProps,
10
+ TextInput,
11
+ } from "@studiocubics/ui";
12
+ import { Trash } from "lucide-react";
13
+
14
+ export function SystemUserDeleteListItem(
15
+ props: ListItemProps & { uid: User["id"]; fullName?: User["fullName"] },
16
+ ) {
17
+ const { uid, fullName, ...rest } = props;
18
+ const { open, handleOpen, handleStrictClose, handleClose } = useDisclosure();
19
+ return (
20
+ <>
21
+ <ListItem
22
+ {...rest}
23
+ color="error"
24
+ startIcon={<Trash />}
25
+ onClick={handleOpen}
26
+ >
27
+ Delete User
28
+ </ListItem>
29
+ <Dialog open={open} onClose={handleStrictClose}>
30
+ <ConfirmationForm
31
+ formTitle="Delete System User"
32
+ variant="danger"
33
+ onCancel={handleClose}
34
+ >
35
+ Are you sure you want to delete {fullName ? `${fullName}'s` : "this"}{" "}
36
+ system account?
37
+ <br />
38
+ All information will be deleted, this action is irreversible and
39
+ cannot be undone.
40
+ <TextInput fullWidth />
41
+ </ConfirmationForm>
42
+ </Dialog>
43
+ </>
44
+ );
45
+ }
@@ -0,0 +1,6 @@
1
+ .root {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ align-items: flex-start;
5
+ width: 100%;
6
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { Button, IdentityDisplay, TransitionAnimation } from "@studiocubics/ui";
4
+ import styles from "./SystemUserDetails.module.css";
5
+ import { Edit } from "lucide-react";
6
+ import { useDisclosure } from "@studiocubics/hooks";
7
+ import { type User } from "@clerk/nextjs/server";
8
+ import { SystemUserDetailsForm } from "../SystemUserDetailsForm/SystemUserDetailsForm";
9
+
10
+ interface SystemUserDetailsProps {
11
+ id: User["id"];
12
+ fullName: User["fullName"];
13
+ firstName: User["firstName"];
14
+ lastName?: User["lastName"];
15
+ imageUrl: User["imageUrl"];
16
+ emailAddress?: NonNullable<User["primaryEmailAddress"]>["emailAddress"];
17
+ editable?: boolean;
18
+ }
19
+ export function SystemUserDetails({
20
+ id,
21
+ fullName,
22
+ firstName,
23
+ lastName,
24
+ imageUrl,
25
+ emailAddress,
26
+ editable,
27
+ }: SystemUserDetailsProps) {
28
+ const { open, handleOpen, handleClose } = useDisclosure();
29
+ return (
30
+ <>
31
+ <TransitionAnimation
32
+ in={open}
33
+ transformOrigin={"top right"}
34
+ mountOnly
35
+ unmountOnExit
36
+ >
37
+ <SystemUserDetailsForm
38
+ imageUrl={imageUrl}
39
+ firstName={firstName}
40
+ lastName={lastName}
41
+ onClose={handleClose}
42
+ />
43
+ </TransitionAnimation>
44
+ <TransitionAnimation
45
+ in={!open}
46
+ transformOrigin={"top"}
47
+ mountOnly
48
+ unmountOnExit
49
+ >
50
+ <div className={styles.root}>
51
+ <IdentityDisplay
52
+ profileName={fullName}
53
+ profileImage={imageUrl}
54
+ role={
55
+ <p>
56
+ {emailAddress}
57
+ <br />
58
+ <strong>{id}</strong>
59
+ </p>
60
+ }
61
+ />
62
+ {editable && (
63
+ <Button size="sm" startIcon={<Edit />} onClick={handleOpen}>
64
+ Edit Details
65
+ </Button>
66
+ )}
67
+ </div>
68
+ </TransitionAnimation>
69
+ </>
70
+ );
71
+ }
@@ -0,0 +1,7 @@
1
+ .form {
2
+ flex: 1;
3
+ }
4
+ .formRow {
5
+ display: flex;
6
+ gap: var(--spacing-gap);
7
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import type { User } from "@clerk/nextjs/server";
4
+ import {
5
+ type CardProps,
6
+ Card,
7
+ ConfirmationForm,
8
+ IdentityDisplay,
9
+ Button,
10
+ TextInput,
11
+ toast,
12
+ } from "@studiocubics/ui";
13
+ import styles from "./SystemUserDetailsForm.module.css";
14
+ import { initialiseForm } from "@studiocubics/utils";
15
+ import { systemUserDetailsUpdateAction } from "../../../../clerk/_index";
16
+ import { useActionState, useEffect, useRef, useState } from "react";
17
+
18
+ const initialSystemUserDetailsUpdateState = initialiseForm(
19
+ "firstName",
20
+ "lastName",
21
+ "imageUrl",
22
+ );
23
+ export type SystemUserDetailsUpdateState =
24
+ typeof initialSystemUserDetailsUpdateState;
25
+
26
+ export function SystemUserDetailsForm({
27
+ imageUrl,
28
+ firstName,
29
+ lastName,
30
+ onClose,
31
+ ...rest
32
+ }: {
33
+ imageUrl?: User["imageUrl"];
34
+ firstName?: User["firstName"];
35
+ lastName?: User["lastName"];
36
+ onClose(): void;
37
+ } & CardProps) {
38
+ const [state, action, pending] = useActionState(
39
+ systemUserDetailsUpdateAction.bind(null, "hello"),
40
+ initialSystemUserDetailsUpdateState,
41
+ );
42
+ const [imagePreview, setImagePreview] = useState(imageUrl);
43
+ const [inputImage, setInputImage] = useState<File>();
44
+ const imageInputRef = useRef<HTMLInputElement | null>(null);
45
+
46
+ useEffect(() => {
47
+ if (inputImage) setImagePreview(URL.createObjectURL(inputImage));
48
+ }, [inputImage]);
49
+
50
+ return (
51
+ <Card fullWidth {...rest}>
52
+ <ConfirmationForm
53
+ formTitle="Edit Details"
54
+ onCancel={onClose}
55
+ action={action}
56
+ disabled={pending}
57
+ >
58
+ <IdentityDisplay
59
+ profileImage={imagePreview}
60
+ profileName={
61
+ <div className={styles.formRow}>
62
+ <Button
63
+ size="sm"
64
+ variant="outlined"
65
+ disabled={pending}
66
+ onClick={() => imageInputRef.current?.click()}
67
+ >
68
+ Upload
69
+ </Button>
70
+ <input
71
+ type="file"
72
+ accept="image/jpg, image/png"
73
+ ref={imageInputRef}
74
+ name={"imageFile"}
75
+ onChange={(e) =>
76
+ e.target?.files && setInputImage(e.target.files[0])
77
+ }
78
+ hidden
79
+ disabled={pending}
80
+ />
81
+ <Button
82
+ size="sm"
83
+ color="error"
84
+ onClick={() => toast("hello")}
85
+ disabled={pending}
86
+ >
87
+ Remove
88
+ </Button>
89
+ </div>
90
+ }
91
+ role={"Recommended size 1:1, up to 10MB."}
92
+ />
93
+ <div className={styles.formRow}>
94
+ <TextInput
95
+ fullWidth
96
+ label="First Name"
97
+ defaultValue={firstName!}
98
+ name="firstName"
99
+ error={state.fieldErrors?.firstName}
100
+ disabled={pending}
101
+ />
102
+ <TextInput
103
+ fullWidth
104
+ label="Last Name"
105
+ defaultValue={lastName!}
106
+ name="lastName"
107
+ error={state.fieldErrors?.lastName}
108
+ disabled={pending}
109
+ />
110
+ </div>
111
+ </ConfirmationForm>
112
+ </Card>
113
+ );
114
+ }
@@ -0,0 +1,18 @@
1
+ import { systemUserListReadAction } from "../../../clerk/actions/systemUsers";
2
+ import type { Role } from "../../../clerk/rbacConfig";
3
+ import { SystemUserListCard } from "./SystemUserListCard";
4
+
5
+ export async function SystemUserList(params: ClerkUserListParams) {
6
+ const systemUserList = await systemUserListReadAction(params);
7
+ return systemUserList.data.map((user) => (
8
+ <SystemUserListCard
9
+ key={user.id}
10
+ id={user.id}
11
+ fullName={user.fullName}
12
+ imageUrl={user.imageUrl}
13
+ emailAddress={user.primaryEmailAddress?.emailAddress}
14
+ role={user.publicMetadata.role as Role}
15
+ lastSignInAt={user.lastSignInAt}
16
+ />
17
+ ));
18
+ }
@@ -0,0 +1,17 @@
1
+ import { Button } from "@studiocubics/ui";
2
+ import { Search, ChartNoAxesGantt } from "lucide-react";
3
+ import { InvitationCreateDialog } from "../Invitations/InvitationListActions";
4
+
5
+ export function SystemUserListActions() {
6
+ return (
7
+ <>
8
+ <InvitationCreateDialog />
9
+ <Button size="sm" square>
10
+ <Search />
11
+ </Button>
12
+ <Button size="sm" square>
13
+ <ChartNoAxesGantt />
14
+ </Button>
15
+ </>
16
+ );
17
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import type { User } from "@clerk/nextjs/server";
4
+ import {
5
+ CollectionItemCard,
6
+ CollectionItemCardActions,
7
+ type DocumentAction,
8
+ List,
9
+ ListItem,
10
+ } from "@studiocubics/ui";
11
+ import { toCapitalised } from "@studiocubics/utils";
12
+ import {
13
+ BrickWallShield,
14
+ EllipsisVertical,
15
+ History,
16
+ OctagonX,
17
+ } from "lucide-react";
18
+ import Link from "next/link";
19
+ import { SystemUserDeleteListItem } from "./SystemUserActions";
20
+ import type { Route } from "next";
21
+ import type { Role } from "../../../clerk/rbacConfig";
22
+
23
+ export interface SystemUserListCardProps {
24
+ id: User["id"];
25
+ fullName: User["fullName"];
26
+ imageUrl: User["imageUrl"];
27
+ emailAddress?: NonNullable<User["primaryEmailAddress"]>["emailAddress"];
28
+ role?: Role;
29
+ lastSignInAt?: User["lastActiveAt"];
30
+ }
31
+
32
+ export function SystemUserListCard(props: SystemUserListCardProps) {
33
+ const { id, fullName, imageUrl, emailAddress, role, lastSignInAt } = props;
34
+ const actions: DocumentAction[] = [
35
+ {
36
+ actionType: "link",
37
+ icon: <BrickWallShield />,
38
+ label: "View Sessions",
39
+ href: `/dashboard/security/systemUsers/${id}/sessions`,
40
+ prefetch: true,
41
+ // @ts-expect-error
42
+ // This is dumb polymorphism garbage!
43
+ as: Link,
44
+ },
45
+ {
46
+ actionType: "popover",
47
+ icon: <EllipsisVertical />,
48
+ popoverChildren: (
49
+ <List className="surfaceContainer">
50
+ <SystemUserDeleteListItem uid={id} fullName={fullName} />
51
+ <ListItem
52
+ {...{
53
+ startIcon: <OctagonX />,
54
+ children: "Ban User",
55
+ color: "error",
56
+ }}
57
+ />
58
+ </List>
59
+ ),
60
+ },
61
+ ];
62
+
63
+ return (
64
+ <CollectionItemCard
65
+ as={Link}
66
+ href={`/dashboard/security/systemUsers/${id}` as Route}
67
+ title={fullName ?? "Name is missing"}
68
+ thumbnail={<img src={imageUrl} alt={fullName ?? ""} />}
69
+ subtitle={emailAddress}
70
+ chip={toCapitalised(role)}
71
+ description={
72
+ lastSignInAt ? (
73
+ <>
74
+ <History size={16} />
75
+ &ensp;Last Active&ensp;
76
+ <strong>{new Date(lastSignInAt).toDateString()}</strong>
77
+ </>
78
+ ) : (
79
+ "No activity found!"
80
+ )
81
+ }
82
+ footer={<CollectionItemCardActions actions={actions} />}
83
+ />
84
+ );
85
+ }
@@ -0,0 +1,33 @@
1
+ import { SYSTEM_USERS_PAGE_LIMIT as limit } from "../../../constants/pageLimits";
2
+ import type { SecurityPageProps } from "../types";
3
+ import { PageLayoutPagination } from "@studiocubics/ui";
4
+ import { SystemUserListActions } from "./SystemUserListActions";
5
+ import { SystemUserList } from "./SystemUserList";
6
+ import { SystemUserListPagination } from "./SystemUserListPagination";
7
+
8
+ export async function SystemUserListPage({
9
+ searchParams,
10
+ securityLinks,
11
+ }: SecurityPageProps) {
12
+ const sp = await searchParams;
13
+ const page = Number(sp.page ?? 1);
14
+ const orderBy = String(sp.orderBy ?? "created_at");
15
+ const query = String(sp.query ?? "");
16
+ const offset = (page - 1) * limit;
17
+ const params = {
18
+ limit,
19
+ offset,
20
+ orderBy,
21
+ query,
22
+ };
23
+ return (
24
+ <PageLayoutPagination
25
+ actions={<SystemUserListActions />}
26
+ size="sm"
27
+ title={securityLinks[2]?.children}
28
+ paginationComponent={<SystemUserListPagination page={page} {...params} />}
29
+ >
30
+ <SystemUserList {...params} />
31
+ </PageLayoutPagination>
32
+ );
33
+ }
@@ -0,0 +1,19 @@
1
+ import { NextSSRPagination } from "@studiocubics/next";
2
+ import { systemUserListReadAction } from "../../../clerk/actions/systemUsers";
3
+
4
+ export async function SystemUserListPagination({
5
+ page,
6
+ ...params
7
+ }: {
8
+ page: number;
9
+ limit: number;
10
+ } & ClerkUserListParams) {
11
+ const systemUsersList = await systemUserListReadAction(params);
12
+ return (
13
+ <NextSSRPagination
14
+ page={page}
15
+ limit={params.limit}
16
+ total={systemUsersList.totalCount}
17
+ />
18
+ );
19
+ }
@@ -0,0 +1,30 @@
1
+ import { Breadcrumbs, PageTitle } from "@studiocubics/ui";
2
+ import { SystemUserPageContent } from "./SystemUserPageContent";
3
+ import Link from "next/link";
4
+ import { auth } from "../../../clerk/auth";
5
+ import { systemUserReadAction } from "../../../clerk/actions/systemUsers";
6
+ export async function SystemUserPage({
7
+ params,
8
+ }: PageProps<"/dashboard/security/systemUsers/[userId]">) {
9
+ const userId = (await params).userId;
10
+ const session = await auth();
11
+ const allowEdit = await session.hasPermission("systemUsers", "update");
12
+ const user = await systemUserReadAction(userId);
13
+ return (
14
+ <>
15
+ <PageTitle
16
+ size="sm"
17
+ title={
18
+ <Breadcrumbs defaultActive={1}>
19
+ <h4>
20
+ <Link href={"/dashboard/security/systemUsers"}>System Users</Link>
21
+ </h4>
22
+ <h4>{user.fullName}&apos;s Account</h4>
23
+ </Breadcrumbs>
24
+ }
25
+ noBorders
26
+ />
27
+ <SystemUserPageContent user={user} allowEdit={allowEdit} />
28
+ </>
29
+ );
30
+ }