feat: add admin page with reject/approve functionalities

This commit is contained in:
sirin 2024-10-23 16:18:12 +07:00
parent 3fecafcf77
commit 446f8cce48
8 changed files with 316 additions and 1 deletions

View File

@ -11,6 +11,7 @@
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",

View File

@ -11,6 +11,9 @@ dependencies:
'@radix-ui/react-avatar':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-checkbox':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
@ -119,6 +122,9 @@ dependencies:
stripe:
specifier: ^17.1.0
version: 17.2.0
sweetalert2:
specifier: ^11.14.3
version: 11.14.4
tailwind-merge:
specifier: ^2.5.2
version: 2.5.2
@ -151,6 +157,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18
version: 18.3.0
'@types/react-select-country-list':
specifier: ^2.2.3
version: 2.2.3
eslint:
specifier: ^8
version: 8.57.0
@ -668,6 +677,33 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
@ -1847,6 +1883,10 @@ packages:
dependencies:
'@types/react': 18.3.4
/@types/react-select-country-list@2.2.3:
resolution: {integrity: sha512-nffcYOwuun+5B0EWqubK+amHpPdK9Xj20xkLYNqYrzmESd8FnpLwHsS79ClLAWA9y+icVA8gWPkbwBp1gpjSwA==}
dev: true
/@types/react@18.3.4:
resolution: {integrity: sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==}
dependencies:
@ -5690,6 +5730,10 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
/sweetalert2@11.14.4:
resolution: {integrity: sha512-8QMzjxCuinwm18EK5AtYvuhP+lRMRxTWVXy8om9wGlULsXSI4TD29kyih3VYrSXMMBlD4EShFvNC7slhTC7j0w==}
dev: false
/tailwind-merge@2.5.2:
resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==}
dev: false

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,104 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Check, X } from "lucide-react";
import { useState } from "react";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { rejectBusiness, approveBusiness } from "@/lib/data/applicationMutate";
import toast from "react-hot-toast";
interface BusinessActionButtonsProps {
businessApplicationId: number;
}
export function BusinessActionButtons({ businessApplicationId }: BusinessActionButtonsProps) {
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectOpen, setIsRejectOpen] = useState(false);
const [isApproveOpen, setIsApproveOpen] = useState(false);
const onRejectBusiness = async () => {
try {
setIsRejectLoading(true);
const client = createSupabaseClient();
const { error } = await rejectBusiness(client, businessApplicationId);
if (error) throw error;
toast.success("Rejected successfully");
window.location.reload();
} catch (error) {
toast.error("Failed to reject business");
console.error("Failed to reject business:", error);
} finally {
setIsRejectLoading(false);
setIsRejectOpen(false);
}
};
const onApproveBusiness = async () => {
try {
setIsApproveLoading(true);
const client = createSupabaseClient();
const { error } = await approveBusiness(client, businessApplicationId);
if (error) throw error;
toast.success("Approved successfully");
window.location.reload();
} catch (error) {
toast.error("Failed to approve business");
console.error("Failed to approve business:", error);
} finally {
setIsApproveLoading(false);
setIsApproveOpen(false);
}
};
return (
<div className="flex flex-col gap-1">
<Dialog open={isApproveOpen} onOpenChange={setIsApproveOpen}>
<DialogTrigger asChild>
<Check className="border-[2px] border-black dark:border-white rounded-md hover:bg-primary cursor-pointer" />
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Approve this Business</DialogTitle>
<DialogDescription>Are you sure that you will approve this business?</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="submit" variant="default" onClick={onApproveBusiness} disabled={isApproveLoading}>
{isApproveLoading ? "Approving..." : "Approve"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isRejectOpen} onOpenChange={setIsRejectOpen}>
<DialogTrigger asChild>
<X className="border-[2px] border-black dark:border-white rounded-md hover:bg-destructive cursor-pointer" />
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Reject this Business</DialogTitle>
<DialogDescription>Are you sure that you will reject this business?</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="submit" variant="destructive" onClick={onRejectBusiness} disabled={isRejectLoading}>
{isRejectLoading ? "Rejecting..." : "Reject"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

109
src/app/admin/page.tsx Normal file
View File

@ -0,0 +1,109 @@
import { getUserRole } from "@/lib/data/userQuery";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import { redirect } from "next/navigation";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import Link from "next/link";
import { FolderOpenDot } from "lucide-react";
import { getAllBusinessApplicationQuery } from "@/lib/data/applicationQuery";
import { BusinessActionButtons } from "./BusinessActionButtons";
export default async function AdminPage() {
const client = createSupabaseClient();
const { data: userData, error: userDataError } = await client.auth.getUser();
if (userDataError) {
redirect("/");
}
const uid = userData.user!.id;
const { data: roleData, error: roleDataError } = await getUserRole(client, uid);
if (roleDataError) {
redirect("/");
}
if (roleData!.role != "admin") {
redirect("/");
}
const { data: businessApplicationData, error: businessApplicationError } =
await getAllBusinessApplicationQuery(client);
if (businessApplicationError) {
console.log(businessApplicationError);
}
return (
<div className="container max-w-screen-xl">
<div className="flex my-4">
<Table className="border-2 border-border rounded-md">
<TableCaption>A list of business applications.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Business Name</TableHead>
<TableHead>User Account</TableHead>
<TableHead>Pitch Deck URL</TableHead>
<TableHead>Is In US?</TableHead>
<TableHead>Is For Sale?</TableHead>
<TableHead>Generate Revenue</TableHead>
<TableHead>Community Size</TableHead>
<TableHead>Money raised to date</TableHead>
<TableHead>Location</TableHead>
<TableHead>Project</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{businessApplicationData && businessApplicationData.length > 0 ? (
businessApplicationData.map((application) => (
<TableRow key={application.id}>
<TableCell>{application.business_name}</TableCell>
<TableCell>
<Link href={`/profile/${application.user_id}`} className="text-blue-500 hover:text-blue-600">
{application.username}
</Link>
</TableCell>
<TableCell>
{application.pitch_deck_url && (
<Link href={application.pitch_deck_url} className="text-blue-500 hover:text-blue-600">
{application.pitch_deck_url}
</Link>
)}
</TableCell>
<TableCell>
<Checkbox checked={application.is_in_us} disabled />
</TableCell>
<TableCell>
<Checkbox checked={application.is_for_sale} disabled />
</TableCell>
<TableCell>
<Checkbox checked={application.is_generating_revenue} disabled />
</TableCell>
<TableCell>{application.community_size}</TableCell>
<TableCell>{application.money_raised_to_date}</TableCell>
<TableCell>{application.location}</TableCell>
<TableCell>
{application.project_application_id && (
<Link href={`/admin/project/${application.project_application_id}`}>
<FolderOpenDot className="border-[2px] border-black dark:border-white rounded-md hover:bg-gray-400 w-full cursor-pointer" />
</Link>
)}
</TableCell>
<TableCell>
<BusinessActionButtons businessApplicationId={application.id} />
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={11} className="text-center h-24 text-muted-foreground">
No business applications found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,23 @@
import { SupabaseClient } from "@supabase/supabase-js";
export async function rejectBusiness(
client: SupabaseClient,
businessApplicationId: Number,
) {
return client.from("business_application")
.update({
status: "reject",
})
.eq("id", businessApplicationId);
}
export async function approveBusiness(
client: SupabaseClient,
businessApplicationId: Number,
) {
return client.from("business_application")
.update({
status: "approve",
})
.eq("id", businessApplicationId);
}

View File

@ -20,4 +20,8 @@ async function getUserProfile(client: SupabaseClient, userId: string) {
}
}
export { getUserProfile };
function getUserRole(client: SupabaseClient, userId: string) {
return client.from("user_role").select("role").eq("user_id", userId).single();
}
export { getUserProfile, getUserRole };