Skip to content

Commit ed1c0a7

Browse files
Admin dashboard!! (#387)
* updated dependency * made baseline * frontend landing page * Revert "updated dependency (#384)" This reverts commit bc38882. * updated redirection * refactored header * feat: report schema * db changes * fixed styling * rough draft of admin dashboard * created role * made add user work * updated auth * added generate file * fixed logic for landing page login * updated image placement * fixed styling * merge conflicts * added toast * removed unnecessary * cleaned up code * removed untracked files * test * add dev role * fix auth bug * admin dashboard scaffolding * db changes * format * added revokedAt field * format --------- Co-authored-by: Michael Song <msong@hometap.com> Co-authored-by: Michael Song <song.mich@northeastern.edu> Co-authored-by: Michael Song <64758303+songmichael11@users.noreply.github.com>
1 parent f8e1d1c commit ed1c0a7

File tree

30 files changed

+3723
-25
lines changed

30 files changed

+3723
-25
lines changed

apps/web/src/app/(pages)/(landing)/page.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import { signIn } from "@cooper/auth";
12
import Image from "next/image";
23
import LoginButton from "~/app/_components/auth/login-button";
4+
import { AdminAccessToast } from "~/app/_components/landing/admin-access-toast";
35

46
const textOptions = [
57
"Insights on interviews, pay, and job experience",
68
"Side-by-side comparison view of up to three jobs",
79
"Anonymous reviews to protect identities",
810
];
11+
912
export default function Landing() {
1013
return (
1114
<div className="flex w-full flex-col bg-cooper-cream-100 lg:flex-row overflow-auto lg:overflow-hidden h-full flex-1">
15+
<AdminAccessToast />
1216
<div className="lg:w-[43%] flex flex-col pl-16 pr-28 justify-center pt-2 lg:pt-0">
1317
<div className="flex w-fit flex-row items-center gap-2">
1418
<div className="text-cooper-blue-800 text-[40px] leading-[48px] font-semibold">
@@ -18,9 +22,21 @@ export default function Landing() {
1822
</div>
1923
<div className="w-fit pt-8">
2024
<LoginButton />
21-
<div className="text-cooper-gray-600 text-md pb-6 pt-4 w-fit">
25+
<div className="text-cooper-gray-600 text-md pt-4 w-fit">
2226
Log in with husky.neu.edu email to access reviews
2327
</div>
28+
<form>
29+
<button
30+
type="submit"
31+
formAction={async () => {
32+
"use server";
33+
await signIn("googleAdmin", { redirectTo: "/roles" });
34+
}}
35+
className="text-cooper-gray-600 font-bold text-md pb-6 pt-2 w-fit cursor-pointer hover:underline"
36+
>
37+
Or continue as admin / coordinator
38+
</button>
39+
</form>
2440
<hr />
2541
</div>
2642

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function AdminDashboardPage() {
2+
return (
3+
<div className="flex h-full w-full items-center justify-center flex-col">
4+
Dashboard yay
5+
</div>
6+
);
7+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import { UserRole } from "@cooper/db/schema";
4+
import Link from "next/link";
5+
import { useRouter } from "next/navigation";
6+
7+
import { api } from "~/trpc/react";
8+
9+
export default function AdminLayout({
10+
children,
11+
}: {
12+
children: React.ReactNode;
13+
}) {
14+
const router = useRouter();
15+
16+
const {
17+
data: session,
18+
isLoading: sessionLoading,
19+
error: _sessionError,
20+
} = api.auth.getSession.useQuery();
21+
22+
if (
23+
!sessionLoading &&
24+
session?.user.role &&
25+
session.user.role !== UserRole.ADMIN &&
26+
session.user.role !== UserRole.DEVELOPER
27+
) {
28+
router.replace("/404");
29+
return null;
30+
}
31+
32+
return (
33+
<div className="flex w-full flex-col">
34+
<div className="border-b bg-white px-6 py-3">
35+
<div className="flex gap-4 text-sm">
36+
<Link href="/admin/dashboard">Dashboard</Link>
37+
<Link href="/admin/user-manager">User Manager</Link>
38+
</div>
39+
</div>
40+
{children}
41+
</div>
42+
);
43+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function AdminUserManagerPage() {
2+
return (
3+
<div className="flex h-full w-full items-center justify-center flex-col">
4+
User Manager yay
5+
</div>
6+
);
7+
}

apps/web/src/app/(pages)/(protected)/review-form/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import dayjs from "dayjs";
2121
import { Form } from "node_modules/@cooper/ui/src/form";
2222
import { PaySection } from "~/app/_components/form/sections/pay-section";
2323
import { api } from "~/trpc/react";
24+
import { UserRole } from "node_modules/@cooper/db/src/schema/misc";
2425

2526
const filter = new Filter();
2627

@@ -264,6 +265,14 @@ export default function ReviewForm() {
264265
return null;
265266
}
266267

268+
if (
269+
session.user.role &&
270+
session.user.role !== UserRole.STUDENT &&
271+
session.user.role !== UserRole.DEVELOPER
272+
) {
273+
router.replace("/404");
274+
}
275+
267276
// if (submitted) {
268277
// if (validForm) {
269278
// return <SubmissionConfirmation />;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
import { UserRole, UserRoleType } from "@cooper/db/schema";
6+
import { cn, useCustomToast } from "@cooper/ui";
7+
import { Button } from "@cooper/ui/button";
8+
import { Input } from "@cooper/ui/input";
9+
10+
import { Select } from "~/app/_components/themed/onboarding/select";
11+
import { api } from "~/trpc/react";
12+
13+
export function CreateUserForm() {
14+
const { toast } = useCustomToast();
15+
const [selectedRole, setSelectedRole] = useState<string>("");
16+
const [email, setEmail] = useState("");
17+
18+
const createUser = api.user.create.useMutation({
19+
onSuccess: () => {
20+
toast.success("User added successfully.");
21+
setEmail("");
22+
setSelectedRole("");
23+
},
24+
onError: (error) => {
25+
toast.error(error.message);
26+
},
27+
});
28+
29+
return (
30+
<div className="flex flex-row">
31+
<Input
32+
className={cn(
33+
"border-cooper-gray-150 h-9 border-[1px] pl-5 w-[30%] text-sm text-cooper-gray-400",
34+
)}
35+
placeholder="Type email here"
36+
value={email}
37+
onChange={(e) => setEmail(e.target.value)}
38+
/>
39+
<div className="w-[10%]">
40+
<Select
41+
options={[
42+
{ value: UserRole.ADMIN, label: "Admin" },
43+
{ value: UserRole.COORDINATOR, label: "Co-op advisor" },
44+
]}
45+
className="border-cooper-gray-150 h-10 text-sm"
46+
value={selectedRole}
47+
placeholder="Select"
48+
onChange={(e) => {
49+
setSelectedRole(e.target.value);
50+
}}
51+
/>
52+
</div>
53+
<Button
54+
type="button"
55+
className="bg-cooper-gray-550 hover:bg-cooper-gray-600 rounded-lg border-none px-8 py-3 text-lg font-semibold text-white"
56+
onClick={() => {
57+
if (!email || !selectedRole) return;
58+
createUser.mutate({ email, role: selectedRole as UserRoleType });
59+
}}
60+
>
61+
Submit
62+
</Button>
63+
</div>
64+
);
65+
}

apps/web/src/app/_components/companies/company-card-preview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function CompanyCardPreview({
3737
return (
3838
<Card
3939
className={cn(
40-
"outline-cooper-gray-150 flex h-fit flex-col justify-between rounded-lg outline outline-[0.75px]",
40+
"outline-cooper-gray-150 flex h-fit flex-col justify-between rounded-lg outline outline-[0.75px] hover:cursor-pointer hover:bg-cooper-gray-200",
4141
className,
4242
)}
4343
>

apps/web/src/app/_components/header/header-layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import ProfileButton from "../profile/profile-button";
88
/**
99
* This should be used when placing content under the header, standardizes how children are placed under a header.
1010
* @param param0 Children to pass into the layout
11-
* @returns A layout component that standardizes the distance from the header
11+
* @returns A layout component that standardizes the distance from the header.
1212
*/
1313
export default async function HeaderLayout({
1414
children,

apps/web/src/app/_components/header/header.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { handleSignOut } from "../auth/actions";
1818
import CooperLogo from "../cooper-logo";
1919
import MobileHeaderButton from "./mobile-header-button";
2020
import { Session } from "@cooper/auth";
21+
import { UserRole } from "node_modules/@cooper/db/src/schema/misc";
2122

2223
interface HeaderProps {
2324
auth: React.ReactNode;
@@ -32,6 +33,9 @@ export default function Header({ auth, loggedIn }: HeaderProps) {
3233
const [isOpen, setIsOpen] = useState(false);
3334
const session = api.auth.getSession.useQuery();
3435
const utils = api.useUtils();
36+
const isStudentOrDeveloper =
37+
session.data?.user.role === UserRole.STUDENT ||
38+
session.data?.user.role === UserRole.DEVELOPER;
3539

3640
if (isOpen) {
3741
return (
@@ -101,6 +105,15 @@ export default function Header({ auth, loggedIn }: HeaderProps) {
101105
Log Out
102106
</button>
103107
</DropdownMenuLabel>
108+
<DropdownMenuSeparator />
109+
<DropdownMenuLabel className="text-center">
110+
<Link
111+
href="/admin/dashboard"
112+
onClick={() => setIsOpen(false)}
113+
>
114+
Admin
115+
</Link>
116+
</DropdownMenuLabel>
104117
</DropdownMenuContent>
105118
</DropdownMenu>
106119
) : (
@@ -136,7 +149,7 @@ export default function Header({ auth, loggedIn }: HeaderProps) {
136149
>
137150
Submit Feedback or Bug Reports
138151
</Link>
139-
{session.data && loggedIn && (
152+
{session.data && loggedIn && isStudentOrDeveloper && (
140153
<div className="flex items-center gap-8">
141154
<Link href="/review-form">
142155
<Button className="hover:border-cooper-yellow-700 hover:bg-cooper-yellow-700 h-9 rounded-lg border-none border-cooper-yellow-500 bg-cooper-yellow-500 px-3 py-2 text-sm font-semibold text-white">
@@ -151,7 +164,7 @@ export default function Header({ auth, loggedIn }: HeaderProps) {
151164

152165
{/* Mobile: when logged in show + and hamburger; when logged out show only login button */}
153166
<div className="justify-right mr-2 flex flex-shrink grid-cols-2 items-center gap-2 md:hidden">
154-
{session.data ? (
167+
{session.data && loggedIn && isStudentOrDeveloper ? (
155168
<>
156169
<Link href="/review-form">
157170
<Button className="hover:border-cooper-yellow-700 hover:bg-cooper-yellow-700 h-9 rounded-lg border-none border-cooper-yellow-500 bg-cooper-yellow-500 px-3 py-2 text-sm font-semibold text-white">
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { Suspense, useEffect, useRef } from "react";
4+
import { useRouter, useSearchParams } from "next/navigation";
5+
6+
import { useCustomToast } from "@cooper/ui";
7+
8+
const CLEAR_URL_AFTER_MS = 5000;
9+
10+
const MESSAGE = "You don't have access as an admin or coordinator.";
11+
12+
function AdminAccessToastInner() {
13+
const searchParams = useSearchParams();
14+
const router = useRouter();
15+
const { toast } = useCustomToast();
16+
const shownRef = useRef(false);
17+
18+
const error = searchParams.get("error");
19+
console.log("[AdminAccessToast] render", { error });
20+
21+
useEffect(() => {
22+
if (!error || shownRef.current) return;
23+
shownRef.current = true;
24+
console.log("[AdminAccessToast] scheduling toast", { error });
25+
26+
console.log("[AdminAccessToast] showing toast", { error });
27+
toast.error(MESSAGE);
28+
29+
const clearUrlId = window.setTimeout(() => {
30+
console.log("[AdminAccessToast] clearing url", { error });
31+
router.replace("/", { scroll: false });
32+
}, CLEAR_URL_AFTER_MS);
33+
34+
return () => {
35+
console.log("[AdminAccessToast] cleanup", { error });
36+
window.clearTimeout(clearUrlId);
37+
};
38+
// Intentionally depend only on `error`.
39+
// `toast` is not referentially stable (new object each render) and would cancel the timeout.
40+
// eslint-disable-next-line react-hooks/exhaustive-deps
41+
}, [error]);
42+
43+
return null;
44+
}
45+
46+
export function AdminAccessToast() {
47+
return (
48+
<Suspense fallback={null}>
49+
<AdminAccessToastInner />
50+
</Suspense>
51+
);
52+
}

0 commit comments

Comments
 (0)