@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,79 @@
1
+ "use client";
2
+
3
+ import { useUser } from "@clerk/nextjs";
4
+ import {
5
+ Button,
6
+ Card,
7
+ CubicsUILogo,
8
+ PasswordInput,
9
+ TextInput,
10
+ PoweredByBanner,
11
+ } from "@studiocubics/ui";
12
+ import { useRouter } from "next/navigation";
13
+ import { type ChangeEvent, useEffect } from "react";
14
+ import styles from "./SignIn.module.css";
15
+ import { useSignInForm } from "./useSignInForm";
16
+ import Link from "next/link";
17
+
18
+ export function SignIn() {
19
+ const { user } = useUser();
20
+ const router = useRouter();
21
+ const { formState, setFormState, pending, error, handleSubmit } =
22
+ useSignInForm();
23
+
24
+ useEffect(() => {
25
+ if (user?.id) {
26
+ router.push("/dashboard");
27
+ }
28
+ }, [user]);
29
+
30
+ function handleFormStateChange(e: ChangeEvent<HTMLInputElement>) {
31
+ const target = e.currentTarget as HTMLInputElement;
32
+ setFormState({ ...formState, [target.id]: target.value });
33
+ }
34
+
35
+ return (
36
+ <Card size="lg" className={styles.root}>
37
+ <div className={styles.body}>
38
+ <CubicsUILogo width={"120px"} />
39
+ <header className={styles.header}>
40
+ <h1>Sign in to your Account</h1>
41
+ <p>Welcome Back! Lets get started.</p>
42
+ </header>
43
+ <form onSubmit={handleSubmit} className={styles.form}>
44
+ <div id="clerk-captcha" />
45
+
46
+ <TextInput
47
+ label={"Email Address"}
48
+ id="email"
49
+ value={formState.email}
50
+ onChange={handleFormStateChange}
51
+ required
52
+ fullWidth
53
+ type="email"
54
+ />
55
+ <PasswordInput
56
+ label={"Password"}
57
+ id="password"
58
+ value={formState.password}
59
+ onChange={handleFormStateChange}
60
+ required
61
+ fullWidth
62
+ disabled={pending}
63
+ />
64
+ <Button type="submit" variant="contained">
65
+ Confirm
66
+ </Button>
67
+ {error && <p className={styles.error}>{error}</p>}
68
+ </form>
69
+ </div>
70
+ <div className={styles.footer}>
71
+ <p>
72
+ Dont have an account?{" "}
73
+ <Link href={"/auth/requestAccount"}>Request</Link> one from an admin
74
+ </p>
75
+ <PoweredByBanner />
76
+ </div>
77
+ </Card>
78
+ );
79
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./SignIn";
2
+ export * from "./useSignInForm";
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { useSignIn } from "@clerk/nextjs";
4
+ import { type SubmitEvent, useState } from "react";
5
+
6
+ export function useSignInForm() {
7
+ const clerkSignIn = useSignIn();
8
+ const [formState, setFormState] = useState({
9
+ email: "",
10
+ password: "",
11
+ });
12
+ const [pending, setPending] = useState(false);
13
+ const [error, setError] = useState("");
14
+
15
+ async function handleSubmit(e: SubmitEvent) {
16
+ e.preventDefault();
17
+ const { email, password } = formState;
18
+ if (!clerkSignIn.isLoaded) return;
19
+
20
+ try {
21
+ setPending(true);
22
+ const signInAttempt = await clerkSignIn.signIn.create({
23
+ identifier: email,
24
+ password,
25
+ });
26
+ if (signInAttempt.status === "complete") {
27
+ await clerkSignIn.setActive({
28
+ session: signInAttempt.createdSessionId,
29
+ });
30
+ } else {
31
+ throw new Error("Sign In failed to complete");
32
+ }
33
+ } catch (err) {
34
+ console.error(err);
35
+ if (err instanceof Error) setError(err.message);
36
+ } finally {
37
+ setPending(false);
38
+ }
39
+ }
40
+
41
+ return { formState, setFormState, pending, error, handleSubmit };
42
+ }
@@ -0,0 +1,48 @@
1
+ .root,
2
+ .body,
3
+ .header,
4
+ .form,
5
+ .footer {
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+ .root {
10
+ width: clamp(350px, 50vw, 550px);
11
+ align-items: center;
12
+ gap: var(--spacing-gap-7);
13
+ & p {
14
+ font-size: var(--fs-body2);
15
+ text-align: center;
16
+ }
17
+ }
18
+ .body {
19
+ width: 100%;
20
+ align-items: center;
21
+ gap: var(--spacing-gap-3);
22
+ }
23
+ .header {
24
+ align-items: center;
25
+ text-align: center;
26
+ gap: var(--spacing-gap);
27
+ & > h1 {
28
+ font-size: var(--fs-h3);
29
+ }
30
+ & > p {
31
+ font-size: var(--fs-body2);
32
+ color: var(--color-on-background-faint);
33
+ }
34
+ }
35
+ .form {
36
+ width: 100%;
37
+ gap: var(--spacing-gap-2);
38
+ }
39
+ .footer {
40
+ width: 100%;
41
+ padding: var(--spacing-gap-3);
42
+ align-items: center;
43
+ border-top: 1px solid var(--color-outline);
44
+ }
45
+ .error {
46
+ font-size: 2rem;
47
+ color: var(--color-error);
48
+ }
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import { useUser } from "@clerk/nextjs";
4
+ import {
5
+ Card,
6
+ CubicsUILogo,
7
+ TextInput,
8
+ PasswordInput,
9
+ Button,
10
+ PoweredByBanner,
11
+ } from "@studiocubics/ui";
12
+ import Link from "next/link";
13
+ import { useRouter } from "next/navigation";
14
+ import { useState, useEffect, type ChangeEvent } from "react";
15
+ import styles from "./SignUp.module.css";
16
+ import { useSignUpForm } from "./useSignUpForm";
17
+
18
+ export function SignUp() {
19
+ const { user } = useUser();
20
+ const router = useRouter();
21
+ const { ticket, setFormState, formState, pending, error, handleSubmit } =
22
+ useSignUpForm();
23
+ const [step, setStep] = useState(0);
24
+
25
+ // Handle signed-in users visiting this page
26
+ // This will also redirect the user once they finish the sign-up process
27
+ useEffect(() => {
28
+ if (user?.id) {
29
+ router.push("/dashboard");
30
+ }
31
+ }, [user]);
32
+
33
+ // If there is no invitation ticket, restrict access to this page
34
+ if (!ticket) {
35
+ return (
36
+ <Card size="lg" className={styles.root}>
37
+ <p>
38
+ Looks like you haven&lsquo;t been invited!
39
+ <br /> Please <Link href={"/auth/requestAccount"}>request</Link> an
40
+ invitation from an admin.
41
+ </p>
42
+ </Card>
43
+ );
44
+ }
45
+
46
+ function handleFormStateChange(e: ChangeEvent<HTMLInputElement>) {
47
+ setFormState({ ...formState, [e.target.id]: e.target.value });
48
+ }
49
+
50
+ const Step0 = (
51
+ <>
52
+ <TextInput
53
+ label={"First Name"}
54
+ id="firstName"
55
+ value={formState.firstName}
56
+ onChange={handleFormStateChange}
57
+ required
58
+ fullWidth
59
+ />
60
+ <TextInput
61
+ label={"Last Name"}
62
+ id="lastName"
63
+ value={formState.lastName}
64
+ onChange={handleFormStateChange}
65
+ required
66
+ fullWidth
67
+ />
68
+ <Button
69
+ type="button"
70
+ variant="contained"
71
+ onClick={() => setStep(1)}
72
+ fullWidth
73
+ disabled={!formState.firstName || !formState.lastName}
74
+ >
75
+ Continue
76
+ </Button>
77
+ </>
78
+ );
79
+ const Step1 = (
80
+ <>
81
+ <PasswordInput
82
+ label={"Password"}
83
+ id="password"
84
+ value={formState.password}
85
+ onChange={handleFormStateChange}
86
+ required
87
+ fullWidth
88
+ disabled={pending}
89
+ disableStrengthMeter={false}
90
+ />
91
+ <PasswordInput
92
+ label={"Confirm Password"}
93
+ id="confirmPassword"
94
+ value={formState.confirmPassword}
95
+ onChange={handleFormStateChange}
96
+ required
97
+ fullWidth
98
+ disabled={pending}
99
+ />
100
+ <Button disabled={pending} variant="contained" type="submit" fullWidth>
101
+ Register
102
+ </Button>
103
+ </>
104
+ );
105
+ return (
106
+ <Card size="lg" className={styles.root}>
107
+ <div className={styles.body}>
108
+ <CubicsUILogo width={"120px"} />
109
+ <header className={styles.header}>
110
+ <h1>Create your account</h1>
111
+ <p>Welcome! Please fill in the details to get started.</p>
112
+ </header>
113
+ <form onSubmit={handleSubmit} className={styles.form}>
114
+ <div id="clerk-captcha" />
115
+ {step == 0 && Step0}
116
+ {step == 1 && Step1}
117
+ {step > 0 && (
118
+ <Button
119
+ disabled={pending}
120
+ type="button"
121
+ onClick={() => setStep(step - 1)}
122
+ fullWidth
123
+ >
124
+ Go back
125
+ </Button>
126
+ )}
127
+ {error && <p className={styles.error}>{error}</p>}
128
+ </form>
129
+ </div>
130
+ <div className={styles.footer}>
131
+ <p>
132
+ Already have an account? <Link href={"/auth/signIn/"}>Sign In</Link>
133
+ </p>
134
+ <PoweredByBanner />
135
+ </div>
136
+ </Card>
137
+ );
138
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./SignUp";
2
+ export * from "./useSignUpForm";
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { useSignUp } from "@clerk/nextjs";
4
+ import { useSearchParams } from "next/navigation";
5
+ import { type SubmitEvent, useState } from "react";
6
+
7
+ export function useSignUpForm() {
8
+ const clerkSignUp = useSignUp();
9
+ const [formState, setFormState] = useState({
10
+ firstName: "",
11
+ lastName: "",
12
+ password: "",
13
+ confirmPassword: "",
14
+ });
15
+ const [pending, setPending] = useState(false);
16
+ const [error, setError] = useState("");
17
+
18
+ const ticket = useSearchParams().get("__clerk_ticket");
19
+
20
+ async function handleSubmit(e: SubmitEvent) {
21
+ e.preventDefault();
22
+ const { firstName, lastName, password, confirmPassword } = formState;
23
+ if (!clerkSignUp.isLoaded) return;
24
+ if (!ticket) return;
25
+ if (password != confirmPassword) {
26
+ return setError("Passwords dont match!");
27
+ }
28
+
29
+ try {
30
+ setPending(true);
31
+ const signUpAttempt = await clerkSignUp.signUp.create({
32
+ strategy: "ticket",
33
+ ticket,
34
+ firstName,
35
+ lastName,
36
+ password,
37
+ });
38
+ if (signUpAttempt.status === "complete") {
39
+ await clerkSignUp.setActive({
40
+ session: signUpAttempt.createdSessionId,
41
+ });
42
+ } else {
43
+ throw new Error("Sign Up failed to complete");
44
+ }
45
+ } catch (err) {
46
+ console.error(err);
47
+ if (err instanceof Error) setError(err.message);
48
+ } finally {
49
+ setPending(false);
50
+ }
51
+ }
52
+
53
+ return { formState, setFormState, pending, ticket, error, handleSubmit };
54
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./SignIn/_index";
2
+ export * from "./SignUp/_index";
@@ -0,0 +1,9 @@
1
+ import { invitationListReadAction } from "../../../clerk/actions/invitations";
2
+ import { InvitationListCard } from "./InvitationListCard";
3
+
4
+ export async function InvitationList(params: ClerkInvitationListParams) {
5
+ const invitationList = await invitationListReadAction(params);
6
+ return invitationList.data.map((inv) => (
7
+ <InvitationListCard key={inv.id} invitation={inv} />
8
+ ));
9
+ }
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ import { useDisclosure } from "@studiocubics/hooks";
4
+ import {
5
+ Button,
6
+ Dialog,
7
+ Switch,
8
+ TextInput,
9
+ ConfirmationForm,
10
+ Select,
11
+ } from "@studiocubics/ui";
12
+ import { Ban, Eye, EyeClosed, UserPlus } from "lucide-react";
13
+ import Link from "next/link";
14
+ import type { Invitation } from "@clerk/nextjs/server";
15
+ import { initialiseForm, toCapitalised } from "@studiocubics/utils";
16
+ import { useActionState, useEffect } from "react";
17
+ import {
18
+ invitationDeleteAction,
19
+ invitationCreateAction,
20
+ } from "../../../clerk/actions/invitations";
21
+ import { roles } from "../../../clerk/rbacConfig";
22
+
23
+ export function InvitationListActions({
24
+ status,
25
+ }: {
26
+ status: ClerkInvitationListParams["status"];
27
+ }) {
28
+ const isShowRevoked = status == "revoked";
29
+ return (
30
+ <>
31
+ <Link href={`?status=${!isShowRevoked ? "revoked" : ""}`}>
32
+ <Button
33
+ startIcon={isShowRevoked ? <Eye /> : <EyeClosed />}
34
+ size="sm"
35
+ color={isShowRevoked ? "primary" : undefined}
36
+ >
37
+ {isShowRevoked ? "Hide" : "Show"} Revoked
38
+ </Button>
39
+ </Link>
40
+ <InvitationCreateDialog />
41
+ </>
42
+ );
43
+ }
44
+ export function InvitationRevokeDialog({ id }: { id: Invitation["id"] }) {
45
+ const { open, handleOpen, handleStrictClose, handleClose } = useDisclosure();
46
+ const [state, action, pending] = useActionState(
47
+ invitationDeleteAction.bind(null, id),
48
+ initialiseForm(),
49
+ );
50
+ useEffect(() => {
51
+ if (state.success) {
52
+ handleClose();
53
+ }
54
+ }, [state]);
55
+ return (
56
+ <>
57
+ <Button
58
+ color="error"
59
+ type="submit"
60
+ startIcon={<Ban />}
61
+ onClick={handleOpen}
62
+ disabled={pending}
63
+ >
64
+ Revoke Invitation
65
+ </Button>
66
+ <Dialog open={open} onClose={handleStrictClose}>
67
+ <ConfirmationForm
68
+ variant="danger"
69
+ onCancel={handleClose}
70
+ action={action}
71
+ formTitle="Revoke Invitation"
72
+ confirmText="Revoke"
73
+ disabled={pending}
74
+ >
75
+ Are you sure you want to revoke this invitation?
76
+ </ConfirmationForm>
77
+ </Dialog>
78
+ </>
79
+ );
80
+ }
81
+ const initialInvitationCreateState = initialiseForm(
82
+ "emailAddress",
83
+ "expiresInDays",
84
+ "role",
85
+ );
86
+ export type InvitationCreateState = typeof initialInvitationCreateState;
87
+ export function InvitationCreateDialog() {
88
+ const { open, handleOpen, handleStrictClose, handleClose } = useDisclosure();
89
+ const [state, action, pending] = useActionState<
90
+ InvitationCreateState,
91
+ FormData
92
+ >(invitationCreateAction, initialInvitationCreateState);
93
+ useEffect(() => {
94
+ if (state.success) {
95
+ handleClose();
96
+ }
97
+ }, [state]);
98
+ return (
99
+ <>
100
+ <Button
101
+ size="sm"
102
+ startIcon={<UserPlus />}
103
+ variant="contained"
104
+ onClick={handleOpen}
105
+ >
106
+ Invite New
107
+ </Button>
108
+ <Dialog open={open} onClose={handleStrictClose}>
109
+ <ConfirmationForm
110
+ onCancel={handleClose}
111
+ action={action}
112
+ formTitle="Invite New"
113
+ disabled={pending}
114
+ >
115
+ <TextInput
116
+ required
117
+ type="email"
118
+ name="emailAddress"
119
+ label="Email Address"
120
+ disabled={pending}
121
+ error={state.fieldErrors?.emailAddress}
122
+ />
123
+ <TextInput
124
+ type="number"
125
+ name="expiresInDays"
126
+ label="Expires in Days"
127
+ endIcon={<strong>Days&nbsp;&nbsp;</strong>}
128
+ defaultValue={30}
129
+ disabled={pending}
130
+ error={state.fieldErrors?.expiresInDays}
131
+ />
132
+ <Switch
133
+ name="ignoreExisting"
134
+ label="Ignore Existing Invites"
135
+ disabled={pending}
136
+ />
137
+ <Switch
138
+ name="notify"
139
+ value={true}
140
+ label="Notify with email"
141
+ disabled={pending}
142
+ />
143
+ <Select
144
+ name="role"
145
+ label="Select Role to assign user on sign up"
146
+ disabled={pending}
147
+ error={state.fieldErrors?.role}
148
+ >
149
+ {roles.map((r) => (
150
+ <option key={r} value={r}>
151
+ {toCapitalised(r)}
152
+ </option>
153
+ ))}
154
+ </Select>
155
+ </ConfirmationForm>
156
+ </Dialog>
157
+ </>
158
+ );
159
+ }
160
+
161
+ // emailAddress: string;
162
+ // expiresInDays?: number;
163
+ // ignoreExisting?: boolean;
164
+ // notify?: boolean;
165
+ // publicMetadata?: UserPublicMetadata;
166
+ // redirectUrl?: string;
167
+ // templateSlug?: "invitation" | "waitlist_invitation";
@@ -0,0 +1,79 @@
1
+ import type { Invitation } from "@clerk/nextjs/server";
2
+ import {
3
+ AccordionItem,
4
+ Chip,
5
+ CollectionItemCard,
6
+ CopyableText,
7
+ LabeledValue,
8
+ SectionWrapper,
9
+ Tooltip,
10
+ } from "@studiocubics/ui";
11
+ import { formatDate, relativeTime, toCapitalised } from "@studiocubics/utils";
12
+ import { InvitationRevokeDialog } from "./InvitationListActions";
13
+ import { ThemedMonacoEditor } from "../../Inputs/ThemedMonacoEditor/ThemedMonacoEditor";
14
+
15
+ export function InvitationListCard({ invitation }: { invitation: Invitation }) {
16
+ return (
17
+ <AccordionItem
18
+ summary={
19
+ <CollectionItemCard
20
+ id={invitation.id}
21
+ title={invitation.emailAddress}
22
+ subtitle={
23
+ <>
24
+ Invited&thinsp;
25
+ <Tooltip>
26
+ <span>{relativeTime(invitation.createdAt)}</span>
27
+ </Tooltip>
28
+ </>
29
+ }
30
+ chip={toCapitalised(invitation.status)}
31
+ description={<span></span>}
32
+ />
33
+ }
34
+ name="InvitationListCard"
35
+ >
36
+ <SectionWrapper title={"Basic Details"} noBorders>
37
+ <LabeledValue label={"Id"}>{invitation.id}</LabeledValue>
38
+ <LabeledValue label={"Email Address"}>
39
+ {invitation.emailAddress}
40
+ </LabeledValue>
41
+ <LabeledValue label={"Status"}>
42
+ <Chip
43
+ color={
44
+ invitation.status == "accepted"
45
+ ? "primary"
46
+ : invitation.status == "pending"
47
+ ? "secondary"
48
+ : "error"
49
+ }
50
+ >
51
+ <strong>{toCapitalised(invitation.status)}</strong>
52
+ </Chip>
53
+ </LabeledValue>
54
+ </SectionWrapper>
55
+ <SectionWrapper title={"Timestamps"}>
56
+ <LabeledValue label={"Created At"}>
57
+ {formatDate(invitation.createdAt)}
58
+ </LabeledValue>
59
+ <LabeledValue label={"Updated At"}>
60
+ {formatDate(invitation.updatedAt)}
61
+ </LabeledValue>
62
+ </SectionWrapper>
63
+ <SectionWrapper title={"Invite Link"}>
64
+ <CopyableText>{invitation.url ?? "No link found!"}</CopyableText>
65
+ </SectionWrapper>
66
+ <SectionWrapper title={"Public Metadata"}>
67
+ <ThemedMonacoEditor
68
+ options={{ readOnly: true }}
69
+ height="10vh"
70
+ defaultLanguage="json"
71
+ defaultValue={JSON.stringify(invitation.publicMetadata)}
72
+ />
73
+ </SectionWrapper>
74
+ {invitation.status == "pending" && (
75
+ <InvitationRevokeDialog id={invitation.id} />
76
+ )}
77
+ </AccordionItem>
78
+ );
79
+ }
@@ -0,0 +1,32 @@
1
+ import { INVITATIONS_PAGE_LIMIT as limit } from "../../../constants/pageLimits";
2
+ import { PageLayoutPagination } from "@studiocubics/ui";
3
+ import { InvitationListPagination } from "./InvitationListPagination";
4
+ import { InvitationList } from "./InvitationList";
5
+ import type { SecurityPageProps } from "../types";
6
+ import { InvitationListActions } from "./InvitationListActions";
7
+
8
+ export async function InvitationListPage({
9
+ searchParams,
10
+ securityLinks,
11
+ }: SecurityPageProps) {
12
+ const sp = await searchParams;
13
+ const status = sp.status as ClerkInvitationListParams["status"];
14
+ const page = Number(sp.page ?? 1);
15
+ const offset = (page - 1) * limit;
16
+ const params = {
17
+ limit,
18
+ offset,
19
+ status,
20
+ };
21
+
22
+ return (
23
+ <PageLayoutPagination
24
+ size="sm"
25
+ title={securityLinks[3]?.children}
26
+ actions={<InvitationListActions status={status} />}
27
+ paginationComponent={<InvitationListPagination page={page} {...params} />}
28
+ >
29
+ <InvitationList {...params} />
30
+ </PageLayoutPagination>
31
+ );
32
+ }
@@ -0,0 +1,19 @@
1
+ import { NextSSRPagination } from "@studiocubics/next";
2
+ import { invitationListReadAction } from "../../../clerk/actions/invitations";
3
+
4
+ export async function InvitationListPagination({
5
+ page,
6
+ ...params
7
+ }: {
8
+ page: number;
9
+ limit: number;
10
+ } & ClerkInvitationListParams) {
11
+ const invitationList = await invitationListReadAction(params);
12
+ return (
13
+ <NextSSRPagination
14
+ page={page}
15
+ limit={params.limit}
16
+ total={invitationList.totalCount}
17
+ />
18
+ );
19
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./InvitationList";
2
+ export * from "./InvitationListActions";
3
+ export * from "./InvitationListCard";
4
+ export * from "./InvitationListPage";
5
+ export * from "./InvitationListPagination";