Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f7c768b
feat(admin): add admin dashboard with side navigation - dummy content
mosesintech Nov 7, 2024
56fd1f6
feat(admin): add users table in admin dashboard
mosesintech Nov 7, 2024
4db4ef7
feat(admin): add real user data to users table in admin dash
mosesintech Nov 7, 2024
5287e43
feat(admin): add all user columns and update design
mosesintech Nov 12, 2024
4c03827
feat(admin): add filters for users table in admin dash
mosesintech Nov 12, 2024
e12a6d6
feat(admin): add filter by user role - unfinished
mosesintech Nov 13, 2024
8cf7696
feat(admin): add promote-user function to promote users to mod by admin
mosesintech Nov 15, 2024
3347392
feat(admin): add makeAdmin function to promote mods to admins by admins
mosesintech Nov 15, 2024
4fe510b
feat(admin): add user role demotions
mosesintech Nov 15, 2024
45a5d0a
feat(admin): add toast messages on promote/demote user
mosesintech Nov 15, 2024
ab59cb7
feat(admin): add delete-user admin action
mosesintech Nov 16, 2024
91128fb
feat(admin): add ban-user mod action to admin dashboard
mosesintech Nov 16, 2024
a1777b9
feat(admin): add guard so admins cannot demote themselves
mosesintech Nov 16, 2024
04b40e2
feat(admin): add color to user row if banned in admin dashboard
mosesintech Nov 16, 2024
0175e53
fix(admin): remove link to saint page from users table in admin dash
mosesintech Nov 26, 2024
17a4d54
feat(mod): added unban-user mod action to admin dashboard
mosesintech Nov 26, 2024
b881e62
chore(cron): add todo note for future cron job: unban users
mosesintech Nov 26, 2024
7a74a4f
feat(admin): remove unused tab from admin dashboard nav
mosesintech Nov 26, 2024
115a2e0
fix(admin): hide admin dashboard behind auth
mosesintech Feb 6, 2025
1c5a843
fix(admin): update admin user table filters
mosesintech Feb 6, 2025
0661662
refactor(admin): fix types in users table
mosesintech Feb 6, 2025
0b4bf6c
feat(admin): add feature to ban user until specified date
mosesintech Feb 10, 2025
fccd832
fix(admin): invalidate users table data when user is banned/unbanned
mosesintech Feb 10, 2025
216e091
fix(admin): invalidate user table data when promote/demote/delete users
mosesintech Feb 10, 2025
eb10576
refactor(admin): clean up ActionsColumn - needs more work
mosesintech Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,495 changes: 5,760 additions & 4,735 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/app/[[...paths]]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TRPCReactProvider } from "~/trpc/react";
import { sourceSansPro, synaxisHeader } from "../fonts";
import HeaderMenu from "~/components/layout/header-menu";
import Footer from "~/components/layout/footer";
import { Toaster } from "~/components/ui/toaster";
import "~/styles/globals.css";

export default function RootLayout({
Expand All @@ -16,7 +17,10 @@ export default function RootLayout({
>
<div className="mx-auto">
<HeaderMenu />
<TRPCReactProvider>{children}</TRPCReactProvider>
<TRPCReactProvider>
{children}
<Toaster />
</TRPCReactProvider>
<Footer />
</div>
</body>
Expand Down
47 changes: 47 additions & 0 deletions src/components/domain/admin/admin-actions/delete-user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useToast } from "~/components/ui/use-toast";
import { Button } from "~/components/ui/button";
import { api } from "~/trpc/react";

interface PromoteUserProps {
userId: string;
username: string;
}

export function DeleteUser(props: PromoteUserProps) {
const { userId, username } = props;
const { toast } = useToast();
const utils = api.useUtils();

const { mutate, isLoading } = api.user.delete.useMutation({
onSuccess: async (_data, _variables) => {
await utils.user.list.invalidate();
toast({
title: `Success`,
description: `${username} has been deleted.`,
});
},
onError: (e) => {
toast({
title: `Error`,
variant: "destructive",
description: `${e.message}`,
});
},
});

function handleClick() {
return mutate({ userId });
}

return (
<>
<Button
size={"sm"}
variant={isLoading ? "disabled" : "default"} // TODO: Change variant to RED-CAUTION
onClick={handleClick}
>
{isLoading ? "Deleting..." : "DELETE USER"}
</Button>
</>
);
}
83 changes: 83 additions & 0 deletions src/components/domain/admin/admin-actions/demote-user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useToast } from "~/components/ui/use-toast";
import { Button } from "~/components/ui/button";
import { USER_ROLES } from "~/lib/constants";
import { api } from "~/trpc/react";

interface PromoteUserProps {
userId: string;
username: string;
currentUserRole: USER_ROLES;
}

export function DemoteUser(props: PromoteUserProps) {
const { userId, username, currentUserRole } = props;
const { toast } = useToast();
const utils = api.useUtils();

const { mutate: demoteFromMod, isLoading: userDemoteIsLoading } =
api.user.demoteUserFromMod.useMutation({
onSuccess: async (_data, _variables) => {
await utils.user.list.invalidate();
toast({
title: `Success`,
description: `${username} has been demoted to a regular user.`,
});
},
onError: (e) => {
toast({
title: `Error`,
variant: "destructive",
description: `${e.message}`,
});
},
});
const { mutate: demoteToMod, isLoading: modDemoteIsLoading } =
api.user.makeMod.useMutation({
onSuccess: async (_data, _variables) => {
await utils.user.list.invalidate();
toast({
title: `Success`,
description: `${username} has been demoted to a moderator.`,
});
},
onError: (e) => {
toast({
title: `Error`,
variant: "destructive",
description: `${e.message}`,
});
},
});

function handleClick() {
switch (currentUserRole) {
case USER_ROLES.MODERATOR:
return demoteFromMod({ userId });
case USER_ROLES.ADMINISTRATOR:
return demoteToMod({ userId });
default:
break;
}
}

const buttonText =
currentUserRole === USER_ROLES.ADMINISTRATOR
? "Demote User to Moderator"
: "Demote User from Moderator";

return (
<>
<Button
size={"sm"}
variant={
userDemoteIsLoading || modDemoteIsLoading ? "disabled" : "default"
}
onClick={handleClick}
>
{userDemoteIsLoading || modDemoteIsLoading
? "Promoting..."
: buttonText}
</Button>
</>
);
}
83 changes: 83 additions & 0 deletions src/components/domain/admin/admin-actions/promote-user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useToast } from "~/components/ui/use-toast";
import { Button } from "~/components/ui/button";
import { USER_ROLES } from "~/lib/constants";
import { api } from "~/trpc/react";

interface PromoteUserProps {
userId: string;
username: string;
currentUserRole: USER_ROLES;
}

export function PromoteUser(props: PromoteUserProps) {
const { userId, username, currentUserRole } = props;
const { toast } = useToast();
const utils = api.useUtils();

const { mutate: promoteToMod, isLoading: modPromoteIsLoading } =
api.user.makeMod.useMutation({
onSuccess: async (_data, _variables) => {
await utils.user.list.invalidate();
toast({
title: `Success`,
description: `${username} is now a moderator!`,
});
},
onError: (e) => {
toast({
title: `Error`,
variant: "destructive",
description: `${e.message}`,
});
},
});
const { mutate: promoteToAdmin, isLoading: adminPromoteIsLoading } =
api.user.makeAdmin.useMutation({
onSuccess: async (_data, _variables) => {
await utils.user.list.invalidate();
toast({
title: `Success`,
description: `${username} is now an administrator!`,
});
},
onError: (e) => {
toast({
title: `Error`,
variant: "destructive",
description: `${e.message}`,
});
},
});

function handleClick() {
switch (currentUserRole) {
case USER_ROLES.USER:
return promoteToMod({ userId });
case USER_ROLES.MODERATOR:
return promoteToAdmin({ userId });
default:
break;
}
}

const buttonText =
currentUserRole === USER_ROLES.USER
? "Promote User to Moderator"
: "Make User an Administrator";

return (
<>
<Button
size={"sm"}
variant={
modPromoteIsLoading || adminPromoteIsLoading ? "disabled" : "default"
}
onClick={handleClick}
>
{modPromoteIsLoading || adminPromoteIsLoading
? "Promoting..."
: buttonText}
</Button>
</>
);
}
68 changes: 68 additions & 0 deletions src/components/domain/admin/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import React, { useState } from "react";
import { Separator } from "~/components/ui/separator";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuList,
} from "~/components/ui/navigation-menu";
import AllUsers from "./users";

type View = "users" | "account";

export default function AdminDashboard() {
const [view, setView] = useState<View>("users");

const views: { title: string; view: View }[] = [
{
title: "Users",
view: "users",
},
// {
// title: "Account",
// view: "account",
// },
];

return (
<main className="h-full min-h-screen w-full text-neutral-900">
<div className="container flex h-full flex-col items-start justify-center gap-4 px-4 py-4 ">
<h1
className={`font-synaxisHeader text-5xl font-extrabold tracking-tight text-primary-gold-600 sm:text-[5rem]`}
>
<span className="text-secondary-red-600">Admin</span> Dashboard
</h1>

<p className="w-1/2">
Handle admin-level tasks all from this dashboard.
</p>

<Separator className="w-full border border-neutral-400" />

<div className="flex h-full w-full flex-row items-start justify-normal gap-4">
<NavigationMenu>
<NavigationMenuList className="flex w-64 flex-col items-start justify-center">
{views.map((item) => {
return (
<React.Fragment key={item.view}>
<NavigationMenuItem className="w-full" key={item.view}>
<span
className={`${item.view === view ? "bg-neutral-200" : ""} text-md block cursor-pointer select-none space-y-1 rounded-md p-3 font-medium leading-none no-underline outline-none transition-colors focus:bg-neutral-100`}
onClick={() => setView(item.view)}
>
{item.title}
</span>
</NavigationMenuItem>
</React.Fragment>
);
})}
</NavigationMenuList>
</NavigationMenu>

{view === "users" ? <AllUsers /> : <AllUsers />}
</div>
</div>
</main>
);
}
Loading