Skip to content

Commit d8bb564

Browse files
authored
Merge pull request #44 from GTBitsOfGood/invite-user-form
invite user and create account front end implementation
2 parents e8799bc + bc70f11 commit d8bb564

20 files changed

+1535
-3
lines changed

public/assets/blue_arrow.svg

+3
Loading

public/assets/progress_checkmark.svg

+3
Loading

src/app/account_management/page.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
import AccountManagementScreen from "@/screens/AccountManagementScreen";
44

5-
export default AccountManagementScreen;
5+
export default function AccountManagementPage() {
6+
return <AccountManagementScreen />;
7+
}

src/app/api/invites/route.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const schema = zfd
3030
{
3131
message: "Partner details are required for PARTNER user type",
3232
path: ["partnerDetails"],
33-
},
33+
}
3434
);
3535

3636
/**
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client";
2+
3+
import NavBarLayout from "@/components/NavBarLayout";
4+
import { usePathname } from "next/navigation";
5+
import { Open_Sans } from "next/font/google";
6+
const openSans = Open_Sans({
7+
subsets: ["latin"],
8+
display: "swap",
9+
});
10+
11+
export default function RootLayout({
12+
children,
13+
}: {
14+
children: React.ReactNode;
15+
}) {
16+
const pathname = usePathname();
17+
const hideNav = pathname.startsWith("/create-partner-account");
18+
19+
return (
20+
<html lang="en" className={openSans.className}>
21+
<body>
22+
{hideNav ? (
23+
//if the path starts with /create-partner-account, render children ONLY
24+
children
25+
) : (
26+
//else wrap everything in NavBarLayout
27+
<NavBarLayout>{children}</NavBarLayout>
28+
)}
29+
</body>
30+
</html>
31+
);
32+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use client";
2+
3+
import CreatePartnerAccountScreen from "@/screens/CreatePartnerAccountScreen";
4+
5+
export default function CreatePartnerAccountPage() {
6+
return <CreatePartnerAccountScreen />;
7+
}

src/components/InviteUserForm.tsx

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { UserType } from "@prisma/client";
5+
import { Open_Sans } from "next/font/google";
6+
import { useRouter } from "next/navigation";
7+
8+
const openSans = Open_Sans({
9+
subsets: ["latin"],
10+
weight: ["400", "600", "700"],
11+
});
12+
13+
interface InviteUserFormProps {
14+
closeModal: () => void;
15+
onSubmit: (role: UserType) => void;
16+
}
17+
18+
export default function InviteUserForm({
19+
closeModal,
20+
onSubmit,
21+
}: InviteUserFormProps) {
22+
const router = useRouter();
23+
const [name, setName] = useState("");
24+
const [email, setEmail] = useState("");
25+
const [role, setRole] = useState<UserType | "">("");
26+
const [dropdownOpen, setDropdownOpen] = useState(false);
27+
const [errorMessage, setErrorMessage] = useState("");
28+
const [success, setSuccess] = useState(false);
29+
30+
const handleSubmit = async (event: React.FormEvent) => {
31+
event.preventDefault();
32+
33+
if (!name.trim() || !email.trim() || !role) {
34+
setErrorMessage("All fields are required.");
35+
return;
36+
}
37+
38+
//if the role is PARTNER, go to account creation without sending an invite
39+
if (role === "PARTNER") {
40+
closeModal();
41+
router.push(
42+
`/create-partner-account?name=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`
43+
);
44+
return;
45+
}
46+
47+
//otherwise, send invite immediately
48+
try {
49+
const response = await fetch("/api/invites", {
50+
method: "POST",
51+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
52+
body: new URLSearchParams({
53+
email,
54+
name,
55+
userType: role.toUpperCase(),
56+
}).toString(),
57+
});
58+
59+
if (!response.ok) throw new Error("Couldn't send invite.");
60+
61+
setSuccess(true);
62+
onSubmit(role as UserType);
63+
} catch {
64+
setErrorMessage("Couldn't send invite. Please try again.");
65+
}
66+
};
67+
68+
return (
69+
<div
70+
className={`fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 ${openSans.className}`}
71+
>
72+
<div className="bg-white p-6 rounded-[16px] w-[450px] relative">
73+
{success ? (
74+
<>
75+
<h2 className="text-[24px] font-bold text-[#22070B] mb-4">
76+
Add new account
77+
</h2>
78+
<button
79+
className="text-[24px] absolute top-4 right-4 mr-3 text-[#22070B]"
80+
onClick={closeModal}
81+
>
82+
83+
</button>
84+
<p className="text-[16px] text-[#22070B]/70 mb-4">
85+
An email has been sent to finalize account creation. This account
86+
is currently pending.
87+
</p>
88+
<p className="text-[16px] text-[#22070B]/70 mb-8">
89+
You can view the current status from the account management page.
90+
</p>
91+
<div className="flex justify-center mb-8">
92+
<img
93+
src="/assets/blue_arrow.svg"
94+
alt="Success"
95+
className="w-30 h-30"
96+
/>
97+
</div>
98+
99+
<button
100+
className="bg-mainRed text-white px-6 py-3 rounded-[4px] font-semibold w-full"
101+
onClick={closeModal}
102+
>
103+
Back to account management
104+
</button>
105+
</>
106+
) : (
107+
<>
108+
<h2 className="text-[24px] font-bold text-[#22070B] mb-4">
109+
Add new account
110+
</h2>
111+
<button
112+
className="absolute top-4 right-4 text-[#22070B]"
113+
onClick={closeModal}
114+
>
115+
116+
</button>
117+
<form onSubmit={handleSubmit}>
118+
<label className="block text-[16px] text-[#22070B] mb-2">
119+
Name
120+
</label>
121+
<input
122+
type="text"
123+
className="w-full p-3 border bg-[#F9F9F9] text-[16px] text-[#22070B] rounded-[4px] mb-5"
124+
placeholder="Name"
125+
value={name}
126+
onChange={(e) => setName(e.target.value)}
127+
required
128+
/>
129+
130+
<label className="block text-[16px] text-[#22070B] mb-2">
131+
Email
132+
</label>
133+
<input
134+
type="email"
135+
className="w-full p-3 border bg-[#F9F9F9] text-[16px] text-[#22070B] rounded-[4px] mb-5"
136+
placeholder="Email"
137+
value={email}
138+
onChange={(e) => setEmail(e.target.value)}
139+
required
140+
/>
141+
142+
<label className="block text-[16px] text-[#22070B] mb-2">
143+
Role
144+
</label>
145+
<div className="relative w-full">
146+
<button
147+
type="button"
148+
className="w-full p-3 border bg-[#F9F9F9] text-[16px] text-[#22070B] rounded-[4px] flex justify-between items-center"
149+
onClick={() => setDropdownOpen(!dropdownOpen)}
150+
>
151+
{role ? role : "Select a role"}
152+
<svg
153+
className="w-4 h-4 text-[#6B7280]"
154+
viewBox="0 0 24 24"
155+
xmlns="http://www.w3.org/2000/svg"
156+
>
157+
<path
158+
strokeLinecap="round"
159+
strokeLinejoin="round"
160+
d="M19 9l-7 7-7-7"
161+
/>
162+
</svg>
163+
</button>
164+
{dropdownOpen && (
165+
<div className="w-full bg-white border rounded-[4px] mt-1">
166+
{["SUPER_ADMIN", "ADMIN", "STAFF", "PARTNER"].map(
167+
(option) => (
168+
<div
169+
key={option}
170+
className="p-3 text-[16px] text-[#22070B] bg-[#F9F9F9] hover:bg-[#22070B]/10 cursor-pointer"
171+
onClick={() => {
172+
setRole(option as UserType);
173+
setDropdownOpen(false);
174+
}}
175+
>
176+
{option}
177+
</div>
178+
)
179+
)}
180+
</div>
181+
)}
182+
</div>
183+
184+
{errorMessage && (
185+
<p className="text-red-500 text-sm mt-2">{errorMessage}</p>
186+
)}
187+
188+
<div className="flex justify-between mt-6">
189+
<button
190+
type="button"
191+
className="border text-mainRed px-6 py-3 rounded-[4px] font-semibold"
192+
onClick={closeModal}
193+
>
194+
Cancel
195+
</button>
196+
<button
197+
type="submit"
198+
className="bg-mainRed text-white px-6 py-3 rounded-[4px] font-semibold"
199+
>
200+
{role === "PARTNER" ? "Next" : "Send invite link"}
201+
</button>
202+
</div>
203+
</form>
204+
</>
205+
)}
206+
</div>
207+
</div>
208+
);
209+
}

src/screens/AccountManagementScreen.tsx

+25-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useEffect, useState } from "react";
44
import { DotsThree, MagnifyingGlass, Plus } from "@phosphor-icons/react";
55
import { CgSpinner } from "react-icons/cg";
66
import { User, UserType } from "@prisma/client";
7+
import { useRouter } from "next/navigation";
8+
import InviteUserForm from "@/components/InviteUserForm";
79

810
enum UserFilterKey {
911
ALL = "All",
@@ -58,6 +60,18 @@ export default function AccountManagementScreen() {
5860
setFilteredUsers(users.filter(filterMap[type]));
5961
};
6062

63+
const router = useRouter();
64+
const [isInviteModalOpen, setInviteModalOpen] = useState(false);
65+
66+
const handleInviteSubmit = (role: UserType) => {
67+
if (role === "PARTNER") {
68+
setInviteModalOpen(false);
69+
router.push("/create-partner-account");
70+
} else {
71+
console.log("Sending invite link for", role);
72+
}
73+
};
74+
6175
return (
6276
<>
6377
<h1 className="text-2xl font-semibold">Account Management</h1>
@@ -73,7 +87,10 @@ export default function AccountManagementScreen() {
7387
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg bg-gray-100 focus:outline-none focus:border-gray-400"
7488
/>
7589
</div>
76-
<button className="flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-lg font-medium hover:bg-red-600 transition">
90+
<button
91+
className="flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-lg font-medium hover:bg-red-600 transition"
92+
onClick={() => setInviteModalOpen(true)}
93+
>
7794
<Plus size={18} /> Add account
7895
</button>
7996
</div>
@@ -142,6 +159,13 @@ export default function AccountManagementScreen() {
142159
</table>
143160
</div>
144161
)}
162+
163+
{isInviteModalOpen && (
164+
<InviteUserForm
165+
closeModal={() => setInviteModalOpen(false)}
166+
onSubmit={handleInviteSubmit}
167+
/>
168+
)}
145169
</>
146170
);
147171
}

0 commit comments

Comments
 (0)