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": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.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-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
|
|||||||
@ -11,6 +11,9 @@ dependencies:
|
|||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.0
|
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)
|
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':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.2
|
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)
|
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:
|
stripe:
|
||||||
specifier: ^17.1.0
|
specifier: ^17.1.0
|
||||||
version: 17.2.0
|
version: 17.2.0
|
||||||
|
sweetalert2:
|
||||||
|
specifier: ^11.14.3
|
||||||
|
version: 11.14.4
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.5.2
|
specifier: ^2.5.2
|
||||||
version: 2.5.2
|
version: 2.5.2
|
||||||
@ -151,6 +157,9 @@ devDependencies:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18
|
specifier: ^18
|
||||||
version: 18.3.0
|
version: 18.3.0
|
||||||
|
'@types/react-select-country-list':
|
||||||
|
specifier: ^2.2.3
|
||||||
|
version: 2.2.3
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8
|
specifier: ^8
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
@ -668,6 +677,33 @@ packages:
|
|||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
dev: false
|
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):
|
/@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==}
|
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1847,6 +1883,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.3.4
|
'@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:
|
/@types/react@18.3.4:
|
||||||
resolution: {integrity: sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==}
|
resolution: {integrity: sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5690,6 +5730,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
/sweetalert2@11.14.4:
|
||||||
|
resolution: {integrity: sha512-8QMzjxCuinwm18EK5AtYvuhP+lRMRxTWVXy8om9wGlULsXSI4TD29kyih3VYrSXMMBlD4EShFvNC7slhTC7j0w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tailwind-merge@2.5.2:
|
/tailwind-merge@2.5.2:
|
||||||
resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==}
|
resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==}
|
||||||
dev: false
|
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