mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 13:34:06 +01:00
feat: add admin page with reject/approve functionalities
This commit is contained in:
parent
3fecafcf77
commit
446f8cce48
@ -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",
|
||||
|
||||
@ -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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
104
src/app/admin/BusinessActionButtons.tsx
Normal file
104
src/app/admin/BusinessActionButtons.tsx
Normal 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
109
src/app/admin/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal 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 }
|
||||
23
src/lib/data/applicationMutate.ts
Normal file
23
src/lib/data/applicationMutate.ts
Normal 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);
|
||||
}
|
||||
@ -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 };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user