@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.
- package/.turbo/turbo-build.log +7 -0
- package/CHANGELOG.md +12 -0
- package/README.md +15 -0
- package/eslint.config.js +21 -0
- package/package.json +79 -0
- package/rollup.config.js +48 -0
- package/src/clerk/_index.ts +6 -0
- package/src/clerk/actions/_index.ts +2 -0
- package/src/clerk/actions/invitations.ts +78 -0
- package/src/clerk/actions/systemUsers.ts +94 -0
- package/src/clerk/auth.ts +34 -0
- package/src/clerk/clerk.d.ts +105 -0
- package/src/clerk/hasPermission.ts +96 -0
- package/src/clerk/rbacConfig.ts +68 -0
- package/src/clerk/schemas/_index.ts +1 -0
- package/src/clerk/schemas/invitation.ts +17 -0
- package/src/clerk/schemas/systemUser.ts +16 -0
- package/src/clerk/toClientSafeUser.ts +77 -0
- package/src/constants/_index.ts +2 -0
- package/src/constants/defaults.tsx +62 -0
- package/src/constants/pageLimits.ts +2 -0
- package/src/declaration.d.ts +5 -0
- package/src/index.ts +5 -0
- package/src/providers/CMSRootProviders.tsx +13 -0
- package/src/providers/_index.ts +1 -0
- package/src/routes.d.ts +96 -0
- package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.module.css +4 -0
- package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.tsx +16 -0
- package/src/ui/Inputs/_index.ts +1 -0
- package/src/ui/Layout/CMSSecurityLayout.tsx +27 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebar.tsx +39 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarBody.tsx +43 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.module.css +7 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.tsx +59 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.module.css +44 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.tsx +30 -0
- package/src/ui/Layout/CMSSidebar/_index.ts +4 -0
- package/src/ui/Layout/_index.ts +2 -0
- package/src/ui/System/Auth/SignIn/SignIn.module.css +50 -0
- package/src/ui/System/Auth/SignIn/SignIn.tsx +79 -0
- package/src/ui/System/Auth/SignIn/_index.ts +2 -0
- package/src/ui/System/Auth/SignIn/useSignInForm.tsx +42 -0
- package/src/ui/System/Auth/SignUp/SignUp.module.css +48 -0
- package/src/ui/System/Auth/SignUp/SignUp.tsx +138 -0
- package/src/ui/System/Auth/SignUp/_index.ts +2 -0
- package/src/ui/System/Auth/SignUp/useSignUpForm.tsx +54 -0
- package/src/ui/System/Auth/_index.ts +2 -0
- package/src/ui/System/Invitations/InvitationList.tsx +9 -0
- package/src/ui/System/Invitations/InvitationListActions.tsx +167 -0
- package/src/ui/System/Invitations/InvitationListCard.tsx +79 -0
- package/src/ui/System/Invitations/InvitationListPage.tsx +32 -0
- package/src/ui/System/Invitations/InvitationListPagination.tsx +19 -0
- package/src/ui/System/Invitations/_index.ts +5 -0
- package/src/ui/System/Permissions/RoleListCard.tsx +33 -0
- package/src/ui/System/Permissions/RolePermissionsPage.tsx +18 -0
- package/src/ui/System/Permissions/RolePermissionsTable.tsx +36 -0
- package/src/ui/System/Permissions/_index.ts +3 -0
- package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.module.css +5 -0
- package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.tsx +102 -0
- package/src/ui/System/SystemUser/CurrentSystemUserPage.tsx +12 -0
- package/src/ui/System/SystemUser/SystemUserActions.tsx +45 -0
- package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.module.css +6 -0
- package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.tsx +71 -0
- package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.module.css +7 -0
- package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.tsx +114 -0
- package/src/ui/System/SystemUser/SystemUserList.tsx +18 -0
- package/src/ui/System/SystemUser/SystemUserListActions.tsx +17 -0
- package/src/ui/System/SystemUser/SystemUserListCard.tsx +85 -0
- package/src/ui/System/SystemUser/SystemUserListPage.tsx +33 -0
- package/src/ui/System/SystemUser/SystemUserListPagination.tsx +19 -0
- package/src/ui/System/SystemUser/SystemUserPage.tsx +30 -0
- package/src/ui/System/SystemUser/SystemUserPageContent.tsx +54 -0
- package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.module.css +17 -0
- package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.tsx +64 -0
- package/src/ui/System/SystemUser/SystemUserRoleForm/SystemUserRoleForm.tsx +51 -0
- package/src/ui/System/SystemUser/SystemUserTimestamps.tsx +56 -0
- package/src/ui/System/SystemUser/_index.ts +14 -0
- package/src/ui/System/WelcomePage/WelcomePage.module.css +18 -0
- package/src/ui/System/WelcomePage/WelcomePage.tsx +43 -0
- package/src/ui/System/_index.ts +6 -0
- package/src/ui/System/types.ts +7 -0
- package/src/ui/_index.ts +3 -0
- package/src/utils/_index.ts +1 -0
- package/src/utils/proxyFunctions.ts +37 -0
- 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,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‘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,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,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 </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 
|
|
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
|
+
}
|