Skip to content

Commit 847005f

Browse files
authoredDec 2, 2024··
Merge pull request #88 from TEDx-SJEC/admin-page
Admin page
2 parents 55cf72f + 005838a commit 847005f

27 files changed

+939
-568
lines changed
 

‎src/app/actions/change-role.ts

+37-6
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,56 @@
11
"use server";
22

33
import prisma from "@/server/db";
4+
import { getServerSideSession } from "@/lib/get-server-session";
45
import { revalidatePath } from "next/cache";
6+
import getErrorMessage from "@/utils/getErrorMessage";
7+
import { UserRole,ADMIN_USERS_PATH } from "@/constants";
58

6-
async function updateUserRole(id: string, role: string) {
9+
async function updateUserRole(id: string, role: UserRole) {
10+
const VALID_ROLES = Object.values(UserRole);
11+
if (!VALID_ROLES.includes(role)) {
12+
throw new Error(`Invalid role: ${role}`);
13+
}
14+
const session = await getServerSideSession();
15+
if (!session || session.user.role !== UserRole.ADMIN) {
16+
throw new Error(`Unauthorized Access...`);
17+
}
718
try {
819
const updatedUser = await prisma.user.update({
920
where: { id },
1021
data: { role },
1122
});
12-
revalidatePath("/admin/users");
23+
revalidatePath(ADMIN_USERS_PATH);
1324
return updatedUser;
1425
} catch (error) {
15-
console.error("Error updating user role:", error);
16-
return null;
26+
console.error("Error updating user role:", getErrorMessage(error));
27+
throw new Error("Failed to update user role. Please try again later.");
1728
}
1829
}
30+
1931
export const makeAdmin = async (userId: string) => {
20-
return await updateUserRole(userId, "ADMIN");
32+
try {
33+
return await updateUserRole(userId, UserRole.ADMIN);
34+
} catch (error) {
35+
console.error("Failed to make user admin:", getErrorMessage(error));
36+
return null;
37+
}
2138
};
2239

2340
export const makeParticipant = async (userId: string) => {
24-
return await updateUserRole(userId, "PARTICIPANT");
41+
try {
42+
return await updateUserRole(userId, UserRole.PARTICIPANT);
43+
} catch (error) {
44+
console.error("Failed to make user participant:", getErrorMessage(error));
45+
return null;
46+
}
47+
};
48+
49+
export const makeCoordinator = async (userId: string) => {
50+
try {
51+
return await updateUserRole(userId, UserRole.COORDINATOR);
52+
} catch (error) {
53+
console.error("Failed to make user coordinator:", getErrorMessage(error));
54+
return null;
55+
}
2556
};

‎src/app/actions/create-coupon-code.ts

+40-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
11
"use server";
2+
import { generateCouponCode } from "@/lib/helper";
23
import prisma from "@/server/db";
4+
import { couponSchema } from "@/utils/zod-schemas";
35

46
export const saveCoupon = async (
5-
coupon: string,
6-
id: string,
7-
discount: string = "20",
7+
coupon: string,
8+
createdById: string,
9+
discount: number = 20,
10+
numberOfCoupons: number = 1
811
) => {
9-
const resp = await prisma.referral.create({
10-
data: {
11-
code: coupon,
12-
isUsed: false,
13-
createdById: id,
14-
discountPercentage: discount,
15-
},
16-
});
12+
try {
13+
const validatCoupon = couponSchema.parse({ coupon, createdById, discount });
14+
15+
// Check if the coupon already exists
16+
const couponExists = await prisma.referral.findFirst({
17+
where: { code: validatCoupon.coupon },
18+
});
19+
if (couponExists) {
20+
throw new Error("Coupon code already exists");
21+
}
22+
23+
// Create coupons
24+
const createCoupon = async (code: string) => {
25+
return prisma.referral.create({
26+
data: {
27+
code,
28+
isUsed: false,
29+
createdById: validatCoupon.createdById,
30+
discountPercentage: validatCoupon.discount.toString(),
31+
},
32+
});
33+
};
34+
35+
const couponCodes =
36+
numberOfCoupons === 1
37+
? [validatCoupon.coupon]
38+
: Array.from({ length: numberOfCoupons }, () => generateCouponCode(10));
39+
40+
const responses = await Promise.all(couponCodes.map(createCoupon));
41+
return responses;
42+
} catch (error) {
43+
console.error("Error creating coupon:", error);
44+
throw new Error("Failed to create coupon. Please try again later.");
45+
}
1746
};

‎src/app/actions/get-payment-count.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use server";
2+
3+
import { getServerSideSession } from "@/lib/get-server-session";
4+
import prisma from "@/server/db";
5+
6+
export default async function getPaymentCount() {
7+
const session = await getServerSideSession();
8+
if (!session) {
9+
return null;
10+
}
11+
12+
const paymentCount = await prisma.payment.count();
13+
14+
return paymentCount;
15+
}

‎src/app/actions/get-user-by-id.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import prisma from "@/server/db"; // Adjust the import based on your structure
22

33
export async function getUserById(userId: string) {
4-
const user = await prisma.user.findUnique({
5-
where: { id: userId },
6-
select: {
7-
id: true,
8-
role: true, // Include any other fields you need
9-
},
10-
});
11-
12-
return user;
4+
try {
5+
const user = await prisma.user.findUnique({
6+
where: { id: userId },
7+
select: {
8+
id: true,
9+
role: true,
10+
},
11+
});
12+
if (!user) {
13+
throw new Error(`User with ID ${userId} not found`);
14+
}
15+
return user;
16+
} catch (error) {
17+
console.error("Error getting user by id:", error);
18+
throw new Error("Failed to get user. Please try again later.");
19+
}
1320
}

‎src/app/actions/get-user-count.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use server";
2+
import prisma from "@/server/db";
3+
import { getServerSideSession } from "@/lib/get-server-session";
4+
5+
export default async function getUserCount() {
6+
const session = await getServerSideSession();
7+
if (!session) {
8+
return null;
9+
}
10+
11+
const userCount = await prisma.user.count();
12+
13+
return userCount;
14+
}

‎src/app/actions/is-allowed-to-access.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
import prisma from "@/server/db";
44

55
export const isAllowedToAccess = async (email: string): Promise<boolean> => {
6+
if (!email) return false;
7+
try {
68
const user = await prisma.sjecUser.findFirst({
7-
where: {
8-
email: email,
9-
},
9+
where: {
10+
email: email,
11+
},
1012
});
1113
return user !== null;
14+
} catch (error) {
15+
console.error("Error getting user by email:", error);
16+
return false;
17+
}
1218
};

‎src/app/actions/submit-form.ts

+27-16
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,37 @@
33
import { getServerSideSession } from "@/lib/get-server-session";
44
import prisma from "@/server/db";
55
import { FormDataInterface } from "@/types";
6+
import { z } from "zod";
7+
8+
const amountSchema = z.number().positive("Amount must be a positive number.");
69

710
export async function submitForm(data: FormDataInterface, amount: number) {
811
const session = await getServerSideSession();
912
if (!session) {
10-
return;
13+
throw new Error("User is not authenticated");
1114
}
15+
const validatedAmount = amountSchema.parse(amount);
16+
17+
const totalAmount = Math.round(validatedAmount + validatedAmount * 0.02);
1218

13-
return await prisma.form.create({
14-
data: {
15-
name: data.name,
16-
usn: data.usn,
17-
email: data.email,
18-
foodPreference: data.foodPreference,
19-
contact: data.phone,
20-
designation: data.designation,
21-
paidAmount: amount,
22-
photo: data.photo,
23-
collegeIdCard: data.idCard,
24-
createdById: session.user.id,
25-
entityName: data.entityName,
26-
},
27-
});
19+
try {
20+
return await prisma.form.create({
21+
data: {
22+
name: data.name,
23+
usn: data.usn,
24+
email: data.email,
25+
foodPreference: data.foodPreference,
26+
contact: data.phone,
27+
designation: data.designation,
28+
paidAmount: totalAmount,
29+
photo: data.photo,
30+
collegeIdCard: data.idCard,
31+
createdById: session.user.id,
32+
entityName: data.entityName,
33+
},
34+
});
35+
} catch (error) {
36+
console.error("Error creating form:", error);
37+
throw new Error("Failed to submit the form. Please try again later.");
38+
}
2839
}

‎src/app/admin/layout.tsx

+50-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { Inter } from "next/font/google";
55
import "./globals.css";
66
import Providers from "@/components/layout/Provider";
77
import { AdminNavbar } from "@/components/admin/Navbar/navbar";
8-
import { useSession } from "next-auth/react";
8+
import { signIn, useSession } from "next-auth/react";
9+
import { useEffect, useState } from "react";
10+
import { tailChase } from "ldrs";
911

1012
const inter = Inter({ subsets: ["latin"] });
1113

@@ -14,22 +16,62 @@ export default function RootLayout({
1416
}: Readonly<{
1517
children: React.ReactNode;
1618
}>) {
17-
const { data: session } = useSession({
19+
const { data: session, status } = useSession({
1820
required: true,
21+
onUnauthenticated: async () => {
22+
await signIn("google");
23+
},
1924
});
2025

26+
if (typeof window !== "undefined") {
27+
tailChase.register();
28+
}
29+
30+
const [isLoading, setIsLoading] = useState(true);
31+
32+
useEffect(() => {
33+
if (status === "loading") {
34+
setIsLoading(true);
35+
} else {
36+
setIsLoading(false);
37+
}
38+
}, [status]);
39+
40+
if (isLoading || status !== "authenticated" || !session) {
41+
// Show the loading spinner if session is loading or not authenticated
42+
return (
43+
<div className="flex flex-col items-center justify-center min-h-screen">
44+
<l-tail-chase
45+
size={"88"}
46+
speed={"1.75"}
47+
color={"#FF0000"}
48+
></l-tail-chase>
49+
</div>
50+
);
51+
}
52+
2153
if (!session) {
2254
return (
23-
<div className="w-screen h-screen flex justify-center items-center">
24-
Unauthorized
55+
<div className="w-screen h-screen flex justify-center items-center bg-black text-gray-200">
56+
<div className="text-center">
57+
<h1 className="text-3xl font-bold mb-2 text-red-500">Unauthorized</h1>
58+
<p className="text-gray-400">
59+
You need to log in to access this page.
60+
</p>
61+
</div>
2562
</div>
2663
);
2764
}
2865

29-
if (session.user.role !== "ADMIN") {
66+
if (session.user.role !== "ADMIN" && session.user.role !== "COORDINATOR") {
3067
return (
31-
<div className="w-screen h-screen flex justify-center items-center">
32-
Forbidden
68+
<div className="w-screen h-screen flex justify-center items-center bg-black text-gray-200">
69+
<div className="text-center">
70+
<h1 className="text-3xl font-bold mb-2 text-red-500">Forbidden</h1>
71+
<p className="text-gray-400">
72+
You do not have the required permissions to view this page.
73+
</p>
74+
</div>
3375
</div>
3476
);
3577
}
@@ -40,7 +82,7 @@ export default function RootLayout({
4082
<Providers>
4183
<div className="flex h-screen overflow-hidden">
4284
<AdminNavbar />
43-
<main className="flex-1 overflow-auto bg-indigo-50">
85+
<main className="ml-16 md:ml-0 flex-1 overflow-y-auto bg-gray-800">
4486
{children}
4587
</main>
4688
</div>

‎src/app/admin/loading.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client"
2+
import React from "react";
3+
4+
const Loading: React.FC = () => {
5+
return (
6+
<div className="flex items-center justify-center h-screen text-red-500">
7+
<div className="loader"></div>
8+
<style jsx>{`
9+
.loader {
10+
border: 8px solid #f3f3f3; /* Light grey */
11+
border-top: 8px solid #3498db; /* Blue */
12+
border-radius: 50%;
13+
width: 60px;
14+
height: 60px;
15+
animation: spin 1s linear infinite;
16+
}
17+
18+
@keyframes spin {
19+
0% {
20+
transform: rotate(0deg);
21+
}
22+
100% {
23+
transform: rotate(360deg);
24+
}
25+
}
26+
`}</style>
27+
</div>
28+
);
29+
};
30+
31+
export default Loading;

‎src/app/admin/page.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
"use client";
22
import { Coupon } from "@/components/admin/code-generation-card";
33
import { useSession } from "next-auth/react";
4+
import Payments from "./payment/page";
45
export default function AdminPage() {
56
const { data: session } = useSession();
67

7-
if (!session || session.user.role != "ADMIN") {
8+
if (
9+
!session ||
10+
(session.user.role != "ADMIN" && session.user.role != "COORDINATOR")
11+
) {
812
return <div>Unauthorized </div>;
913
}
1014

1115
return (
1216
<>
13-
<div className="h-screen flex items-center justify-center">
14-
<Coupon session={session} />
17+
<div className="h-screen flex items-center justify-center bg-gray-800">
18+
{session.user.role === "ADMIN" ? (
19+
<Coupon session={session} />
20+
) : session.user.role === "COORDINATOR" ? (
21+
<div className="h-screen w-screen flex flex-col items-center justify-center bg-white text-2xl font-semibold">
22+
<h1 className="mb-2 text-red-600">Welcome, Coordinator!</h1>
23+
<p className="text-sm text-black">
24+
You have successfully logged in with the Coordinator role.
25+
</p>
26+
</div>
27+
) : null}
1528
</div>
1629
</>
1730
);

‎src/app/admin/payment/page.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { SearchableInfiniteScrollTable } from "@/components/common/searchable-infinite-scroll-table";
1+
import { PaymentCards } from "@/components/common/searchable-infinite-scroll-table";
22
import React from "react";
33

44
export default async function Payments() {
5-
return (
6-
<>
7-
<div className="pt-20 flex min-h-screen w-full flex-col bg-background">
8-
<SearchableInfiniteScrollTable />
9-
</div>
10-
</>
11-
);
5+
return (
6+
<>
7+
<div className="p-8 flex min-h-screen w-full flex-col bg-background">
8+
<PaymentCards />
9+
</div>
10+
</>
11+
);
1212
}

‎src/app/admin/razorpay/[id]/page.tsx

+66-61
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) {
6565
const [loading, setLoading] = useState(true);
6666
const [loadingForButton, setLoadingForButton] = useState(false);
6767

68+
const handleSendEmail = async () => {
69+
if (!paymentData) {
70+
return;
71+
}
72+
setLoadingForButton(true)
73+
await sendEmail(paymentData.id)
74+
setLoadingForButton(false)
75+
}
76+
77+
6878
async function sendEmail(paymentId: string) {
6979
setLoadingForButton(true);
7080
try {
@@ -127,67 +137,62 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) {
127137
}
128138

129139
return (
130-
<div className="w-screen h-screen flex justify-center items-center bg-gray-100 dark:bg-gray-900 p-4">
131-
<Card className="w-full max-w-lg bg-white dark:bg-gray-800 shadow-lg">
132-
<CardHeader>
133-
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">
134-
Payment Data of {paymentData.notes.customerName || "Unknown"}
135-
</CardTitle>
136-
</CardHeader>
137-
<CardContent className="space-y-4">
138-
<Label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
139-
Payment ID
140-
<Input value={paymentData.id || ""} disabled className="mt-1" />
141-
</Label>
142-
<Label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
143-
Amount
144-
<Input
145-
value={new Intl.NumberFormat("en-IN", {
146-
style: "currency",
147-
currency: "INR",
148-
}).format((paymentData.amount ?? 0) / 100)}
149-
disabled
150-
className="mt-1"
151-
/>
152-
</Label>
153-
<Label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
154-
Email
155-
<Input value={paymentData.email || ""} disabled className="mt-1" />
156-
</Label>
157-
<Label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
158-
Phone
159-
<Input
160-
value={paymentData.contact || ""}
161-
disabled
162-
className="mt-1"
163-
/>
164-
</Label>
165-
<Label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
166-
Created At
167-
<Input
168-
value={new Date(paymentData.created_at * 1000).toLocaleString(
169-
undefined,
170-
{
171-
dateStyle: "full",
172-
timeStyle: "long",
173-
},
174-
)}
175-
disabled
176-
className="mt-1"
177-
/>
178-
</Label>
179-
</CardContent>
180-
<CardFooter className="flex justify-end">
181-
<Button
182-
disabled={loadingForButton}
183-
className="bg-blue-600 text-white hover:bg-blue-700"
184-
onClick={async () => await sendEmail(paymentData.id!)}
185-
>
186-
{loadingForButton ? "Loading..." : "Send Email"}
187-
</Button>
188-
</CardFooter>
189-
</Card>
190-
</div>
140+
<div className="flex w-full min-h-screen justify-center items-center bg-background p-4">
141+
<Card className="w-full max-w-lg">
142+
<CardHeader>
143+
<CardTitle className="text-lg font-semibold">
144+
Payment Data of {paymentData.notes.customerName || "Unknown"}
145+
</CardTitle>
146+
</CardHeader>
147+
<CardContent className="space-y-4">
148+
<div className="space-y-2">
149+
<Label htmlFor="payment-id">Payment ID</Label>
150+
<Input id="payment-id" value={paymentData.id || ""} readOnly />
151+
</div>
152+
<div className="space-y-2">
153+
<Label htmlFor="amount">Amount</Label>
154+
<Input
155+
id="amount"
156+
value={new Intl.NumberFormat("en-IN", {
157+
style: "currency",
158+
currency: "INR",
159+
}).format((paymentData.amount ?? 0) / 100)}
160+
readOnly
161+
/>
162+
</div>
163+
<div className="space-y-2">
164+
<Label htmlFor="email">Email</Label>
165+
<Input id="email" value={paymentData.email || ""} readOnly />
166+
</div>
167+
<div className="space-y-2">
168+
<Label htmlFor="phone">Phone</Label>
169+
<Input id="phone" value={paymentData.contact || ""} readOnly />
170+
</div>
171+
<div className="space-y-2">
172+
<Label htmlFor="created-at">Created At</Label>
173+
<Input
174+
id="created-at"
175+
value={new Date(paymentData.created_at * 1000).toLocaleString(
176+
undefined,
177+
{
178+
dateStyle: "full",
179+
timeStyle: "long",
180+
}
181+
)}
182+
readOnly
183+
/>
184+
</div>
185+
</CardContent>
186+
<CardFooter className="flex justify-end">
187+
<Button
188+
disabled={loadingForButton}
189+
onClick={handleSendEmail}
190+
>
191+
{loadingForButton ? "Loading..." : "Send Email"}
192+
</Button>
193+
</CardFooter>
194+
</Card>
195+
</div>
191196
);
192197
}
193198

‎src/app/admin/razorpay/page.tsx

+53-55
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
"use client";
1+
"use client"
2+
3+
import { Button } from "@/components/ui/button"
24
import {
35
Form,
46
FormControl,
@@ -7,70 +9,66 @@ import {
79
FormItem,
810
FormLabel,
911
FormMessage,
10-
} from "@/components/ui/form";
11-
import { Input } from "@/components/ui/input";
12-
import { zodResolver } from "@hookform/resolvers/zod";
13-
import { useRouter } from "next/navigation";
14-
import React from "react";
15-
import { useForm } from "react-hook-form";
16-
import { z } from "zod";
12+
} from "@/components/ui/form"
13+
import { Input } from "@/components/ui/input"
14+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
15+
import { zodResolver } from "@hookform/resolvers/zod"
16+
import { useRouter } from "next/navigation"
17+
import React from "react"
18+
import { useForm } from "react-hook-form"
19+
import { z } from "zod"
1720

1821
const formSchema = z.object({
1922
razorpayPaymentId: z.string().nonempty("Payment ID is required"),
20-
});
23+
})
2124

2225
const FetchRazorpayPaymentData = () => {
2326
const form = useForm<z.infer<typeof formSchema>>({
2427
resolver: zodResolver(formSchema),
25-
});
26-
const router = useRouter();
28+
})
29+
const router = useRouter()
2730

2831
const onSubmit = async (data: z.infer<typeof formSchema>) => {
29-
router.push(`/admin/razorpay/${data.razorpayPaymentId}`);
30-
};
32+
router.push(`/admin/razorpay/${data.razorpayPaymentId}`)
33+
}
3134

3235
return (
33-
<div className="flex w-screen h-screen justify-center items-center flex-col bg-gray-100 dark:bg-gray-900 p-4">
34-
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mt-10">
35-
Search by Razorpay Payment ID
36-
</h2>
37-
<Form {...form}>
38-
<form
39-
onSubmit={form.handleSubmit(onSubmit)}
40-
className="space-y-4 mt-8 bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg w-full max-w-md"
41-
>
42-
<FormField
43-
control={form.control}
44-
name="razorpayPaymentId"
45-
render={({ field }) => (
46-
<FormItem>
47-
<FormLabel className="text-gray-700 dark:text-gray-300">
48-
Razorpay Payment ID:
49-
</FormLabel>
50-
<FormControl>
51-
<Input
52-
placeholder="pay_O69OS3rml4xT8K"
53-
{...field}
54-
className="border-gray-300 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100"
55-
/>
56-
</FormControl>
57-
<FormDescription className="text-sm text-gray-500 dark:text-gray-400">
58-
Search for a payment by its Razorpay Payment ID
59-
</FormDescription>
60-
<FormMessage />
61-
</FormItem>
62-
)}
63-
/>
64-
<button
65-
type="submit"
66-
className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
67-
>
68-
Submit
69-
</button>
70-
</form>
71-
</Form>
36+
<div className="flex lg:w-full min-h-screen justify-center items-center bg-background p-4 pt-0 mt-0">
37+
<Card className="w-full max-w-md">
38+
<CardHeader>
39+
<CardTitle className="text-lg lg:text-2xl font-semibold text-primary">
40+
Search by Razorpay Payment ID
41+
</CardTitle>
42+
</CardHeader>
43+
<CardContent>
44+
<Form {...form}>
45+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
46+
<FormField
47+
control={form.control}
48+
name="razorpayPaymentId"
49+
render={({ field }) => (
50+
<FormItem>
51+
<FormLabel>Razorpay Payment ID</FormLabel>
52+
<FormControl>
53+
<Input placeholder="pay_O69OS3rml4xT8K" {...field} />
54+
</FormControl>
55+
<FormDescription>
56+
Search for a payment by its Razorpay Payment ID
57+
</FormDescription>
58+
<FormMessage />
59+
</FormItem>
60+
)}
61+
/>
62+
<Button type="submit" className="w-full">
63+
Submit
64+
</Button>
65+
</form>
66+
</Form>
67+
</CardContent>
68+
</Card>
7269
</div>
73-
);
74-
};
70+
)
71+
}
72+
73+
export default FetchRazorpayPaymentData
7574

76-
export default FetchRazorpayPaymentData;

‎src/app/admin/verify/page.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const QRCodeScanner = () => {
1515
const userAgent =
1616
navigator.userAgent || navigator.vendor || (window as any).opera;
1717
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
18-
userAgent.toLowerCase(),
18+
userAgent.toLowerCase()
1919
);
2020
};
2121
setIsMobile(checkMobile());
@@ -30,7 +30,7 @@ const QRCodeScanner = () => {
3030
const url = new URL(result);
3131

3232
if (!url.href.startsWith("https://tedxsjec")) {
33-
throw new Error("Invalid QR code . Please scan a valid QR code");
33+
throw new Error("Invalid QR code . Please scan a valid QR code");
3434
}
3535
// Redirect to the scanned URL
3636
router.push(url.toString());
@@ -41,8 +41,14 @@ const QRCodeScanner = () => {
4141
}
4242
};
4343

44+
45+
4446
const handleError = (error: unknown) => {
45-
if (error instanceof Error) {
47+
if (error instanceof DOMException && error.name === "NotAllowedError") {
48+
setError(
49+
"Camera access denied. Please allow camera access to scan QR codes."
50+
);
51+
} else if (error instanceof Error) {
4652
setError("Error accessing camera: " + error.message);
4753
} else {
4854
setError("An unknown error occurred.");
@@ -57,8 +63,8 @@ const QRCodeScanner = () => {
5763
Error
5864
</h1>
5965
<p className="text-center text-gray-700">
60-
This feature is only available on mobile devices. Please access this
61-
page from your smartphone or tablet.
66+
This feature is not available on mobile devices. Please access this
67+
page from your tablet or laptop.
6268
</p>
6369
</div>
6470
</div>

‎src/app/api/users/payment/route.ts

+77-55
Original file line numberDiff line numberDiff line change
@@ -5,63 +5,85 @@ import { NextRequest, NextResponse } from "next/server";
55
export const dynamic = "force-dynamic";
66

77
export async function GET(request: NextRequest) {
8-
const session = await getServerSideSession();
9-
if (!session) {
10-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
11-
}
8+
const session = await getServerSideSession();
9+
if (!session) {
10+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
11+
}
1212

13-
if (session.user?.role !== "ADMIN") {
14-
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
15-
}
16-
const { searchParams } = new URL(request.url);
17-
try {
18-
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10));
19-
const search = searchParams.get("search") || "";
20-
const limit = 10;
13+
if (session.user?.role !== "ADMIN" && session.user?.role !== "COORDINATOR") {
14+
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
15+
}
16+
const { searchParams } = new URL(request.url);
17+
try {
18+
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10));
19+
const search = searchParams.get("search") || "";
20+
const limit = 10;
2121

22-
const [users, totalCount] = await Promise.all([
23-
prisma.payment.findMany({
24-
skip: (page - 1) * limit,
25-
take: limit,
26-
where: {
27-
razorpayPaymentId: {
28-
contains: search,
29-
},
30-
},
31-
include: {
32-
user: {
33-
select: {
34-
name: true,
35-
email: true,
36-
},
37-
},
38-
},
39-
}),
40-
prisma.payment.count({
41-
where: {
42-
razorpayPaymentId: {
43-
contains: search,
44-
},
45-
},
46-
}),
47-
]);
22+
const [users, totalCount] = await Promise.all([
23+
prisma.payment.findMany({
24+
skip: (page - 1) * limit,
25+
take: limit,
26+
where: {
27+
OR: [
28+
// Update to search in multiple fields
29+
{
30+
razorpayPaymentId: {
31+
contains: search,
32+
},
33+
},
34+
{
35+
user: {
36+
name: {
37+
contains: search,
38+
},
39+
},
40+
},
41+
{
42+
user: {
43+
email: {
44+
contains: search,
45+
},
46+
},
47+
},
48+
],
49+
},
50+
include: {
51+
user: {
52+
select: {
53+
name: true,
54+
email: true,
55+
forms: {
56+
select: {
57+
photo: true,
58+
},
59+
take: 1,
60+
},
61+
},
62+
},
63+
},
64+
}),
65+
prisma.payment.count({
66+
where: {
67+
razorpayPaymentId: {
68+
contains: search,
69+
},
70+
},
71+
}),
72+
]);
4873

49-
const totalPages = Math.ceil(totalCount / limit);
74+
const totalPages = Math.ceil(totalCount / limit);
5075

51-
return NextResponse.json({
52-
users,
53-
pagination: {
54-
currentPage: page,
55-
totalCount,
56-
totalPages,
57-
limit,
58-
},
59-
});
60-
} catch (error) {
61-
console.error("Error fetching payment details:", error);
62-
return NextResponse.json(
63-
{ error: "Failed to fetch data" },
64-
{ status: 500 },
65-
);
66-
}
76+
return NextResponse.json({
77+
users,
78+
pagination: {
79+
currentPage: page,
80+
totalCount,
81+
totalPages,
82+
limit,
83+
},
84+
});
85+
} catch (error) {
86+
console.error("Error fetching payment details:", error);
87+
return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 });
88+
}
6789
}

‎src/app/api/users/route.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export async function GET(req: Request) {
99
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
1010
}
1111

12-
if (session.user?.role !== "ADMIN") {
12+
if (session.user?.role !== "ADMIN" && session.user?.role !== "COORDINATOR") {
1313
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
1414
}
1515

‎src/app/auth/signin/signin-page.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export default function SignIn() {
5353
<l-tail-chase size={"88"} speed={"1.75"} color={"#FF0000"}></l-tail-chase>
5454
</div>
5555
) : (
56-
<p>Redirecting...</p>
56+
<div className="flex flex-col items-center">
57+
<l-tail-chase size={"88"} speed={"1.75"} color={"#FF0000"}></l-tail-chase>
58+
<p className="text-red-500 text-center mt-4">Please wait, redirecting...</p>
59+
</div>
60+
5761
)}
5862
</div>
5963
);

‎src/app/auth/signout/page.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,16 @@ export default function Signin() {
3939
></l-tail-chase>
4040
</div>
4141
) : (
42-
<p>Redirecting...</p>
42+
<div className="flex flex-col items-center">
43+
<l-tail-chase
44+
size={"88"}
45+
speed={"1.75"}
46+
color={"#FF0000"}
47+
></l-tail-chase>
48+
<p className="text-red-500 text-center mt-4">
49+
Please wait, redirecting...
50+
</p>
51+
</div>
4352
)}
4453
</div>
4554
);

‎src/components/admin/Navbar/navbar.tsx

+54-44
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { IconType } from "react-icons";
44
import { FiChevronDown, FiChevronsRight, FiUser } from "react-icons/fi";
55
import { RiCoupon3Line } from "react-icons/ri";
66
import { MdPayment } from "react-icons/md";
7-
import { SiTicktick } from "react-icons/si";
8-
import { SiRazorpay } from "react-icons/si";
7+
import { SiTicktick, SiRazorpay } from "react-icons/si";
98
import { motion } from "framer-motion";
109
import Link from "next/link";
10+
import { useSession } from "next-auth/react";
1111

1212
export const AdminNavbar = () => {
1313
return (
14-
<div className="flex bg-indigo-50 h-screen ">
14+
<div className="flex bg-black h-screen text-white fixed z-[999] sm:static sm:z-auto">
1515
<Sidebar />
1616
<NavbarContent />
1717
</div>
@@ -21,34 +21,39 @@ export const AdminNavbar = () => {
2121
const Sidebar = () => {
2222
const [open, setOpen] = useState(true);
2323
const [selected, setSelected] = useState("Dashboard");
24+
const { data: session } = useSession();
2425

2526
return (
2627
<motion.nav
2728
layout
28-
className=" sticky top-0 h-screen shrink-0 border-r border-slate-300 bg-white p-2"
29+
className="sticky top-0 h-screen shrink-0 border-r border-gray-700 bg-gray-900 p-2"
2930
style={{
3031
width: open ? "225px" : "fit-content",
3132
}}
3233
>
3334
<TitleSection open={open} />
3435

3536
<div className="space-y-1">
36-
<Option
37-
Icon={RiCoupon3Line}
38-
title="Coupon"
39-
selected={selected}
40-
setSelected={setSelected}
41-
open={open}
42-
href="/admin"
43-
/>
44-
<Option
45-
Icon={FiUser}
46-
title="Users"
47-
selected={selected}
48-
setSelected={setSelected}
49-
open={open}
50-
href="/admin/users"
51-
/>
37+
{session?.user?.role === "ADMIN" && (
38+
<Option
39+
Icon={RiCoupon3Line}
40+
title="Coupon"
41+
selected={selected}
42+
setSelected={setSelected}
43+
open={open}
44+
href="/admin"
45+
/>
46+
)}
47+
{session?.user?.role === "ADMIN" && (
48+
<Option
49+
Icon={FiUser}
50+
title="Users"
51+
selected={selected}
52+
setSelected={setSelected}
53+
open={open}
54+
href="/admin/users"
55+
/>
56+
)}
5257
<Option
5358
Icon={MdPayment}
5459
title="Payments"
@@ -65,14 +70,16 @@ const Sidebar = () => {
6570
open={open}
6671
href="/admin/verify"
6772
/>
68-
<Option
69-
Icon={SiRazorpay}
70-
title="Razorpay"
71-
selected={selected}
72-
setSelected={setSelected}
73-
open={open}
74-
href="/admin/razorpay"
75-
/>
73+
{session?.user?.role === "ADMIN" && (
74+
<Option
75+
Icon={SiRazorpay}
76+
title="Razorpay"
77+
selected={selected}
78+
setSelected={setSelected}
79+
open={open}
80+
href="/admin/razorpay"
81+
/>
82+
)}
7683
</div>
7784

7885
<ToggleClose open={open} setOpen={setOpen} />
@@ -104,8 +111,8 @@ const Option = ({
104111
onClick={() => setSelected(title)}
105112
className={`relative flex h-10 w-full items-center rounded-md transition-colors ${
106113
selected === title
107-
? "bg-indigo-100 text-indigo-800"
108-
: "text-slate-500 hover:bg-slate-100"
114+
? "bg-red-500 text-white"
115+
: "text-gray-400 hover:bg-gray-800 hover:text-white"
109116
}`}
110117
>
111118
<motion.div
@@ -120,7 +127,7 @@ const Option = ({
120127
initial={{ opacity: 0, y: 12 }}
121128
animate={{ opacity: 1, y: 0 }}
122129
transition={{ delay: 0.125 }}
123-
className="text-xs font-medium"
130+
className="text-sm font-medium"
124131
>
125132
{title}
126133
</motion.span>
@@ -135,7 +142,7 @@ const Option = ({
135142
}}
136143
style={{ y: "-50%" }}
137144
transition={{ delay: 0.5 }}
138-
className="absolute right-2 top-1/2 size-4 rounded bg-indigo-500 text-xs text-white"
145+
className="absolute right-2 top-1/2 size-4 rounded bg-red-500 text-xs text-white"
139146
>
140147
{notifs}
141148
</motion.span>
@@ -147,8 +154,8 @@ const Option = ({
147154

148155
const TitleSection = ({ open }: { open: boolean }) => {
149156
return (
150-
<div className="mb-3 border-b border-slate-300 pb-3">
151-
<div className="flex cursor-pointer items-center justify-between rounded-md transition-colors hover:bg-slate-100">
157+
<div className="mb-3 border-b border-gray-700 pb-3">
158+
<div className="flex cursor-pointer items-center justify-between rounded-md transition-colors hover:bg-gray-800">
152159
<div className="flex items-center gap-2">
153160
<Logo />
154161
{open && (
@@ -158,31 +165,32 @@ const TitleSection = ({ open }: { open: boolean }) => {
158165
animate={{ opacity: 1, y: 0 }}
159166
transition={{ delay: 0.125 }}
160167
>
161-
<span className="block text-xs font-semibold">Tedxsjec</span>
162-
<span className="block text-xs text-slate-500">Admin Page</span>
168+
<span className="block text-md font-semibold text-red-500">
169+
Tedxsjec
170+
</span>
171+
<span className="block text-xs text-gray-400">Admin Page</span>
163172
</motion.div>
164173
)}
165174
</div>
166-
{open && <FiChevronDown className="mr-2" />}
175+
{open && <FiChevronDown className="mr-2 text-gray-400" />}
167176
</div>
168177
</div>
169178
);
170179
};
171180

172181
const Logo = () => {
173-
// Temp logo from https://logoipsum.com/
174182
return (
175183
<motion.div
176184
layout
177-
className="grid size-10 shrink-0 place-content-center rounded-md bg-indigo-600"
185+
className="grid size-10 shrink-0 place-content-center rounded-md bg-red-600"
178186
>
179187
<svg
180188
width="24"
181189
height="auto"
182190
viewBox="0 0 50 39"
183191
fill="none"
184192
xmlns="http://www.w3.org/2000/svg"
185-
className="fill-slate-50"
193+
className="fill-gray-50"
186194
>
187195
<path
188196
d="M16.4992 2H37.5808L22.0816 24.9729H1L16.4992 2Z"
@@ -208,12 +216,12 @@ const ToggleClose = ({
208216
<motion.button
209217
layout
210218
onClick={() => setOpen((pv) => !pv)}
211-
className="absolute bottom-0 left-0 right-0 border-t border-slate-300 transition-colors hover:bg-slate-100"
219+
className="absolute bottom-0 left-0 right-0 border-t border-gray-700 transition-colors hover:bg-gray-800"
212220
>
213221
<div className="flex items-center p-2">
214222
<motion.div
215223
layout
216-
className="grid size-10 place-content-center text-lg"
224+
className="grid size-10 place-content-center text-lg text-gray-400"
217225
>
218226
<FiChevronsRight
219227
className={`transition-transform ${open && "rotate-180"}`}
@@ -225,7 +233,7 @@ const ToggleClose = ({
225233
initial={{ opacity: 0, y: 12 }}
226234
animate={{ opacity: 1, y: 0 }}
227235
transition={{ delay: 0.125 }}
228-
className="text-xs font-medium"
236+
className="text-xs font-medium text-gray-400"
229237
>
230238
Hide
231239
</motion.span>
@@ -235,4 +243,6 @@ const ToggleClose = ({
235243
);
236244
};
237245

238-
const NavbarContent = () => <div className="h-[200vh] w-full"></div>;
246+
const NavbarContent = () => (
247+
<div className="h-[200vh] w-full bg-gray-900"></div>
248+
);

‎src/components/admin/change-role.tsx

+65-24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
"use client";
2-
import { makeAdmin, makeParticipant } from "@/app/actions/change-role";
2+
import {
3+
makeAdmin,
4+
makeCoordinator,
5+
makeParticipant,
6+
} from "@/app/actions/change-role";
37
import { Button } from "@/components/ui/button";
8+
import {
9+
Popover,
10+
PopoverContent,
11+
PopoverTrigger,
12+
} from "@radix-ui/react-popover";
13+
import { ChevronDownIcon } from "lucide-react";
14+
import { useState } from "react";
415

516
export default function ChangeRole({
617
userId,
@@ -9,32 +20,62 @@ export default function ChangeRole({
920
userId: string;
1021
userRole: string;
1122
}) {
12-
async function handleMakeAdmin() {
13-
await makeAdmin(userId);
14-
}
23+
const [currentRole, setCurrentRole] = useState(userRole);
1524

16-
async function handleMakeParticipant() {
17-
await makeParticipant(userId);
25+
async function handleRoleChange(newRole: string) {
26+
switch (newRole) {
27+
case "ADMIN":
28+
await makeAdmin(userId);
29+
break;
30+
case "PARTICIPANT":
31+
await makeParticipant(userId);
32+
break;
33+
case "COORDINATOR":
34+
await makeCoordinator(userId);
35+
break;
36+
default:
37+
break;
38+
}
39+
setCurrentRole(newRole);
1840
}
1941

2042
return (
21-
<div className="">
22-
<Button
23-
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded dark:bg-black ${
24-
userRole === "ADMIN" ? "hidden" : ""
25-
}`}
26-
onClick={handleMakeAdmin}
27-
>
28-
Make Admin
29-
</Button>
30-
<Button
31-
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded dark:bg-black ${
32-
userRole === "PARTICIPANT" ? "hidden" : ""
33-
}`}
34-
onClick={handleMakeParticipant}
35-
>
36-
Make Participant
37-
</Button>
38-
</div>
43+
<Popover>
44+
<PopoverTrigger asChild>
45+
<Button variant="outline" size="sm">
46+
{currentRole}{" "}
47+
<ChevronDownIcon className="w-4 h-4 ml-2 text-muted-foreground" />
48+
</Button>
49+
</PopoverTrigger>
50+
<PopoverContent className="w-auto p-0" align="center">
51+
<div className="flex gap-2 bg-white p-2 flex-col">
52+
<Button
53+
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded ${
54+
currentRole === "ADMIN" ? "hidden" : ""
55+
}`}
56+
onClick={() => handleRoleChange("ADMIN")}
57+
>
58+
Make Admin
59+
</Button>
60+
<Button
61+
type="submit"
62+
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded ${
63+
currentRole === "PARTICIPANT" ? "hidden" : ""
64+
}`}
65+
onClick={() => handleRoleChange("PARTICIPANT")}
66+
>
67+
Make Participant
68+
</Button>
69+
<Button
70+
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded ${
71+
currentRole === "COORDINATOR" ? "hidden" : ""
72+
}`}
73+
onClick={() => handleRoleChange("COORDINATOR")}
74+
>
75+
Make Coordinator
76+
</Button>
77+
</div>
78+
</PopoverContent>
79+
</Popover>
3980
);
4081
}

‎src/components/admin/code-generation-card.tsx

+144-75
Original file line numberDiff line numberDiff line change
@@ -19,91 +19,160 @@ import { type Session as NextAuthSession } from "next-auth";
1919
import CouponGeneratorDialog from "../payment/coupon-generator-dialog";
2020
import { Checkbox } from "../ui/checkbox";
2121
import { useState } from "react";
22+
// import { toast } from "sonner";
23+
import getErrorMessage from "@/utils/getErrorMessage";
24+
import { Copy, Check } from "lucide-react";
25+
import { toast } from "sonner";
2226

2327
export function Coupon({ session }: { session: NextAuthSession }) {
2428
const [discount, setDiscount] = useState("20");
29+
const [numberOfCoupons, setNumberOfCoupons] = useState("1");
2530
const [checked, setChecked] = useState(false);
31+
const [numberOfCouponsChecked, setNumberOfCouponsChecked] = useState(false);
32+
const [disabled, setDisabled] = useState(true);
33+
const [copied, setCopied] = useState(false);
2634
const { data, isPending, isError, error, refetch } = useQuery({
2735
queryKey: ["coupon"],
2836
queryFn: createCouponCode,
2937
enabled: false,
3038
});
3139

40+
const handleCopy = () => {
41+
if (!data) return;
42+
navigator.clipboard.writeText(data).then(() => {
43+
setCopied(true);
44+
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
45+
});
46+
};
47+
48+
const handleGenerateCoupon = async () => {
49+
try {
50+
await saveCoupon(data as string, session.user.id, Number(discount) , Number(numberOfCoupons));
51+
// refetch();
52+
setDisabled(true);
53+
alert("Coupon code saved successfully");
54+
} catch (error) {
55+
alert(getErrorMessage(error));
56+
}
57+
};
58+
3259
return (
33-
<Tabs defaultValue="account" className="w-[400px]">
34-
<TabsList className="grid w-full grid-cols-2">
35-
<TabsTrigger value="account">Create</TabsTrigger>
36-
<TabsTrigger value="password">Store</TabsTrigger>
37-
</TabsList>
38-
<TabsContent value="account">
39-
<Card>
40-
<CardHeader>
41-
<CardTitle>Coupon code</CardTitle>
42-
<CardDescription>
43-
Here you can create the coupon code and add it to the database
44-
</CardDescription>
45-
</CardHeader>
46-
<CardContent className="space-y-2">
47-
<div className="space-y-1">
48-
<Label htmlFor="username">Code</Label>
49-
<Input id="username" disabled value={isPending ? "" : data} />
50-
</div>
51-
<div className="space-y-1">
52-
<Label htmlFor="username">Discount(%)</Label>
53-
<Input
54-
id="discount"
55-
value={discount}
56-
disabled={!checked}
57-
onChange={(e) => {
58-
setDiscount(e.target.value);
59-
}}
60-
/>
61-
<Checkbox
62-
id="discount_terms"
63-
checked={checked}
64-
onClick={() => {
65-
setChecked(!checked);
66-
}}
67-
/>
68-
<label
69-
htmlFor="terms"
70-
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
71-
>
72-
Edit default discount
73-
</label>
74-
</div>
75-
</CardContent>
76-
<CardFooter>
77-
<CouponGeneratorDialog onGenerateCoupon={refetch} />
78-
</CardFooter>
79-
</Card>
80-
</TabsContent>
81-
<TabsContent value="password">
82-
<Card>
83-
<CardHeader>
84-
<CardTitle>Coupon code</CardTitle>
85-
<CardDescription>
86-
You can see the generated coupon code below
87-
</CardDescription>
88-
</CardHeader>
89-
<CardContent className="space-y-2">
90-
<div className="space-y-1">
91-
<Label htmlFor="new">Coupon code</Label>
92-
<Input id="new" type="text" disabled value={data} />
93-
</div>
94-
</CardContent>
95-
<CardFooter>
96-
<Button
97-
disabled={data === undefined ? true : false}
98-
onClick={async () => {
99-
await saveCoupon(data as string, session.user.id, discount);
100-
}}
101-
>
102-
Save coupon
103-
</Button>
104-
</CardFooter>
105-
</Card>
106-
</TabsContent>
107-
</Tabs>
60+
<Tabs defaultValue="account" className="w-[400px]">
61+
<TabsList className="grid w-full grid-cols-2">
62+
<TabsTrigger value="account">Create</TabsTrigger>
63+
<TabsTrigger value="password">Store</TabsTrigger>
64+
</TabsList>
65+
<TabsContent value="account">
66+
<Card>
67+
<CardHeader>
68+
<CardTitle>Coupon code</CardTitle>
69+
<CardDescription>
70+
Here you can create the coupon code and add it to the database
71+
</CardDescription>
72+
</CardHeader>
73+
<CardContent className="space-y-2">
74+
<div className="space-y-1">
75+
<Label htmlFor="username">Code</Label>
76+
<Input id="username" disabled value={isPending ? "" : data} />
77+
</div>
78+
<div className="space-y-1">
79+
<Label htmlFor="username">Discount(%)</Label>
80+
<Input
81+
id="discount"
82+
value={discount}
83+
disabled={!checked}
84+
onChange={(e) => {
85+
setDiscount(e.target.value);
86+
}}
87+
/>
88+
</div>
89+
<div className="flex items-center space-x-2 mt-2">
90+
<Checkbox
91+
id="number_of_coupons"
92+
checked={checked}
93+
onClick={() => {
94+
setChecked(!checked);
95+
}}
96+
/>
97+
<label
98+
htmlFor="terms"
99+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
100+
>
101+
Edit default number of coupons
102+
</label>
103+
</div>
104+
<div className="space-y-1">
105+
<Label htmlFor="username">Number of coupons</Label>
106+
<Input
107+
id="number_of_coupons"
108+
value={numberOfCoupons}
109+
disabled={!numberOfCouponsChecked}
110+
onChange={(e) => {
111+
setNumberOfCoupons(e.target.value);
112+
}}
113+
/>
114+
</div>
115+
<div className="flex items-center space-x-2 mt-2">
116+
<Checkbox
117+
id="discount_terms"
118+
checked={numberOfCouponsChecked}
119+
onClick={() => {
120+
setNumberOfCouponsChecked(!numberOfCouponsChecked);
121+
}}
122+
/>
123+
<label
124+
htmlFor="terms"
125+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
126+
>
127+
Edit default number of coupons
128+
</label>
129+
</div>
130+
</CardContent>
131+
<CardFooter>
132+
<CouponGeneratorDialog
133+
onGenerateCoupon={() => {
134+
refetch();
135+
setDisabled(false);
136+
}}
137+
/>
138+
</CardFooter>
139+
</Card>
140+
</TabsContent>
141+
<TabsContent value="password">
142+
<Card>
143+
<CardHeader>
144+
<CardTitle>Coupon code</CardTitle>
145+
<CardDescription>You can see the generated coupon code below</CardDescription>
146+
</CardHeader>
147+
<CardContent className="space-y-2">
148+
<div className="space-y-1">
149+
<Label htmlFor="new">Coupon code</Label>
150+
<div className="flex items-center space-x-2">
151+
<Input id="new" type="text" disabled value={data} />
152+
<button
153+
onClick={handleCopy}
154+
className="p-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 focus:outline-none"
155+
title={copied ? "Copied!" : "Copy to clipboard"}
156+
>
157+
{copied ? (
158+
<Check size={16} className="text-green-500" />
159+
) : (
160+
<Copy size={16} />
161+
)}
162+
</button>
163+
</div>
164+
</div>
165+
</CardContent>
166+
<CardFooter>
167+
<Button
168+
disabled={data === undefined || isPending || disabled}
169+
onClick={handleGenerateCoupon}
170+
>
171+
Save coupon
172+
</Button>
173+
</CardFooter>
174+
</Card>
175+
</TabsContent>
176+
</Tabs>
108177
);
109178
}

‎src/components/admin/user-list.tsx

+13-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ChevronDownIcon, SearchIcon } from "lucide-react";
1010
import { Input } from "../ui/input";
1111
import debounce from "lodash.debounce";
1212
import ChangeRole from "./change-role";
13+
import getUserCount from "@/app/actions/get-user-count";
1314

1415
export interface User {
1516
id: string;
@@ -31,6 +32,7 @@ const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => {
3132
const [hasMore, setHasMore] = useState(true);
3233
const [searchQuery, setSearchQuery] = useState("");
3334
const loader = useRef<HTMLDivElement | null>(null);
35+
const [totalNumberOfUsers, setTotalNumberOfUsers] = useState(0);
3436

3537
const fetchUsers = useCallback(async (page: number, query: string, isNewSearch: boolean) => {
3638
if (loading) return;
@@ -92,6 +94,14 @@ const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => {
9294
return () => observer.disconnect();
9395
}, [hasMore, loadMoreUsers]);
9496

97+
useEffect(() => {
98+
async function getNumberOfUsers() {
99+
const count = await getUserCount();
100+
setTotalNumberOfUsers(count ?? 0); // Use 0 if count is null
101+
}
102+
getNumberOfUsers();
103+
}, []);
104+
95105
return (
96106
<Card className="w-full">
97107
<div className="flex justify-end gap-2 mt-5 p-5">
@@ -107,7 +117,7 @@ const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => {
107117
</div>
108118
</div>
109119
<CardHeader>
110-
<CardTitle>Users</CardTitle>
120+
<CardTitle>Users {totalNumberOfUsers}</CardTitle>
111121
<CardDescription>Manage user roles and permissions.</CardDescription>
112122
</CardHeader>
113123
<CardContent>
@@ -126,17 +136,8 @@ const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => {
126136
</p>
127137
</div>
128138
</div>
129-
<Popover>
130-
<PopoverTrigger asChild>
131-
<Button variant="outline" size="sm">
132-
{user.role}{" "}
133-
<ChevronDownIcon className="w-4 h-4 ml-2 text-muted-foreground" />
134-
</Button>
135-
</PopoverTrigger>
136-
<PopoverContent className="w-auto p-0" align="center">
137-
<ChangeRole userId={user.id} userRole={user.role} />
138-
</PopoverContent>
139-
</Popover>
139+
140+
<ChangeRole userId={user.id} userRole={user.role} />
140141
</div>
141142
))}
142143
</div>
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,175 @@
11
"use client";
22

33
import { useEffect, useRef, useState, useCallback } from "react";
4-
import {
5-
Table,
6-
TableBody,
7-
TableCell,
8-
TableHead,
9-
TableHeader,
10-
TableRow,
11-
} from "@/components/ui/table";
4+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
126
import { Input } from "@/components/ui/input";
137
import { Loader2, Search } from "lucide-react";
148
import axios from "axios";
159
import debounce from "lodash.debounce";
16-
17-
interface TableData {
18-
user: {
19-
name: string;
20-
email: string;
21-
};
22-
usn?: string;
23-
razorpayPaymentId: string;
24-
contactNumber: string;
25-
amount: number;
10+
import getPaymentCount from "@/app/actions/get-payment-count";
11+
12+
interface PaymentData {
13+
user: {
14+
name: string;
15+
email: string;
16+
image: string;
17+
forms: [{ photo: string }];
18+
};
19+
usn?: string;
20+
razorpayPaymentId: string;
21+
contactNumber: string;
22+
amount: number;
2623
}
2724

28-
export function SearchableInfiniteScrollTable() {
29-
const [data, setData] = useState<TableData[]>([]);
30-
const [filteredData, setFilteredData] = useState<TableData[]>([]);
31-
const [isLoading, setIsLoading] = useState(false);
32-
const [page, setPage] = useState(1);
33-
const [searchTerm, setSearchTerm] = useState("");
34-
const [hasMoreData, setHasMoreData] = useState(true);
35-
const loaderRef = useRef<HTMLDivElement>(null);
36-
const observerRef = useRef<IntersectionObserver | null>(null);
37-
38-
const getPaymentDetails = async (page: number, query: string) => {
39-
if (isLoading || !hasMoreData) return;
40-
41-
setIsLoading(true);
42-
try {
43-
const response = await axios.get(
44-
`/api/users/payment?page=${page}&search=${encodeURIComponent(query)}`,
45-
);
46-
const users = response.data.users;
47-
48-
if (users.length === 0) {
49-
setHasMoreData(false); // No more data to load
50-
}
51-
52-
setData((prevData) => {
53-
const newData = [...prevData, ...users];
54-
// Remove duplicates
55-
const uniqueData = Array.from(
56-
new Map(
57-
newData.map((item) => [item.razorpayPaymentId, item]),
58-
).values(),
59-
);
60-
return uniqueData;
61-
});
62-
setPage((prevPage) => prevPage + 1);
63-
} catch (error) {
64-
console.error("Error fetching payment details:", error);
65-
} finally {
66-
setIsLoading(false);
67-
}
68-
};
69-
70-
const loadMoreData = () => {
71-
if (searchTerm === "") {
72-
getPaymentDetails(page, "");
73-
}
74-
};
75-
76-
const fetchSearchResults = useCallback(async (query: string) => {
77-
setPage(1); // Reset page number
78-
setHasMoreData(true); // Reset hasMoreData
79-
try {
80-
const response = await axios.get(
81-
`/api/users/payment?page=1&search=${encodeURIComponent(query)}`,
82-
);
83-
const users = response.data.users;
84-
setData(users); // Set new data from search
85-
setFilteredData(users); // Set filtered data to the same as new data
86-
} catch (error) {
87-
console.error("Error fetching payment details:", error);
88-
}
89-
}, []);
90-
91-
// eslint-disable-next-line react-hooks/exhaustive-deps
92-
const debouncedFetch = useCallback(
93-
debounce((query: string) => {
94-
fetchSearchResults(query);
95-
}, 500),
96-
[],
97-
);
98-
99-
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
100-
const value = event.target.value;
101-
setSearchTerm(value);
102-
debouncedFetch(value); // Use debounced fetch function
103-
};
104-
105-
useEffect(() => {
106-
loadMoreData(); // Initial load
107-
// eslint-disable-next-line react-hooks/exhaustive-deps
108-
}, []);
109-
110-
useEffect(() => {
111-
const observer = new IntersectionObserver(
112-
(entries) => {
113-
if (entries[0].isIntersecting && !isLoading) {
114-
loadMoreData();
25+
export function PaymentCards() {
26+
const [data, setData] = useState<PaymentData[]>([]);
27+
const [filteredData, setFilteredData] = useState<PaymentData[]>([]);
28+
const [isLoading, setIsLoading] = useState(false);
29+
const [totalNumberOfPayments, setTotalNumberOfPayments] = useState(0);
30+
const [page, setPage] = useState(1);
31+
const [searchTerm, setSearchTerm] = useState("");
32+
const [hasMoreData, setHasMoreData] = useState(true);
33+
const loaderRef = useRef<HTMLDivElement>(null);
34+
const observerRef = useRef<IntersectionObserver | null>(null);
35+
36+
const getPaymentDetails = async (page: number, query: string) => {
37+
if (isLoading || !hasMoreData) return;
38+
39+
setIsLoading(true);
40+
try {
41+
const response = await axios.get(
42+
`/api/users/payment?page=${page}&search=${encodeURIComponent(query)}`
43+
);
44+
const users = response.data.users;
45+
46+
if (users.length === 0) {
47+
setHasMoreData(false);
48+
}
49+
50+
setData((prevData) => {
51+
const newData = [...prevData, ...users];
52+
const uniqueData = Array.from(
53+
new Map(newData.map((item) => [item.razorpayPaymentId, item])).values()
54+
);
55+
return uniqueData;
56+
});
57+
setPage((prevPage) => prevPage + 1);
58+
} catch (error) {
59+
console.error("Error fetching payment details:", error);
60+
} finally {
61+
setIsLoading(false);
11562
}
116-
},
117-
{ threshold: 1.0 },
118-
);
63+
};
64+
65+
const loadMoreData = () => {
66+
if (searchTerm === "") {
67+
getPaymentDetails(page, "");
68+
}
69+
};
70+
71+
const fetchSearchResults = useCallback(async (query: string) => {
72+
setPage(1);
73+
setHasMoreData(true);
74+
try {
75+
const response = await axios.get(`/api/users/payment?page=1&search=${encodeURIComponent(query)}`);
76+
const users = response.data.users;
77+
setData(users);
78+
setFilteredData(users);
79+
} catch (error) {
80+
console.error("Error fetching payment details:", error);
81+
}
82+
}, []);
11983

120-
if (loaderRef.current) {
121-
observer.observe(loaderRef.current);
122-
}
84+
const debouncedFetch = useCallback(
85+
debounce((query: string) => {
86+
fetchSearchResults(query);
87+
}, 500),
88+
[]
89+
);
12390

124-
return () => {
125-
if (loaderRef.current) {
126-
// eslint-disable-next-line react-hooks/exhaustive-deps
127-
observer.unobserve(loaderRef.current);
128-
}
129-
observer.disconnect();
91+
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
92+
const value = event.target.value;
93+
setSearchTerm(value);
94+
debouncedFetch(value);
13095
};
131-
// eslint-disable-next-line react-hooks/exhaustive-deps
132-
}, [isLoading]);
133-
134-
return (
135-
<div className="container mx-auto py-10">
136-
<div className="mb-4 relative">
137-
<Input
138-
type="text"
139-
placeholder="Search..."
140-
value={searchTerm}
141-
onChange={handleSearch}
142-
className="pl-10"
143-
/>
144-
<Search
145-
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
146-
size={20}
147-
/>
148-
</div>
149-
<Table>
150-
<TableHeader>
151-
<TableRow>
152-
<TableHead>Name</TableHead>
153-
<TableHead>Email</TableHead>
154-
<TableHead>Payment ID</TableHead>
155-
<TableHead>Amount</TableHead>
156-
</TableRow>
157-
</TableHeader>
158-
<TableBody>
159-
{(searchTerm ? filteredData : data).map((item, index) => (
160-
<TableRow key={index}>
161-
<TableCell>{item.user.name}</TableCell>
162-
<TableCell>{item.user.email}</TableCell>
163-
<TableCell>{item.razorpayPaymentId}</TableCell>
164-
<TableCell>{item.amount.toFixed(2)}</TableCell>
165-
</TableRow>
166-
))}
167-
</TableBody>
168-
</Table>
169-
{searchTerm === "" && hasMoreData && (
170-
<div ref={loaderRef} className="flex justify-center py-4">
171-
{isLoading && <Loader2 className="h-6 w-6 animate-spin" />}
96+
97+
useEffect(() => {
98+
loadMoreData();
99+
async function getNumberOfPayments() {
100+
const count = await getPaymentCount();
101+
setTotalNumberOfPayments(count ?? 0);
102+
}
103+
getNumberOfPayments();
104+
}, []);
105+
106+
useEffect(() => {
107+
const observer = new IntersectionObserver(
108+
(entries) => {
109+
if (entries[0].isIntersecting && !isLoading) {
110+
loadMoreData();
111+
}
112+
},
113+
{ threshold: 1.0 }
114+
);
115+
116+
if (loaderRef.current) {
117+
observer.observe(loaderRef.current);
118+
}
119+
120+
return () => {
121+
if (loaderRef.current) {
122+
observer.unobserve(loaderRef.current);
123+
}
124+
observer.disconnect();
125+
};
126+
}, [isLoading]);
127+
128+
return (
129+
<div className="container mx-auto py-10">
130+
<h1 className="text-3xl font-bold text-primary py-4">Payments ({totalNumberOfPayments})</h1>
131+
<div className="mb-4 relative">
132+
<Input
133+
type="text"
134+
placeholder="Search..."
135+
value={searchTerm}
136+
onChange={handleSearch}
137+
className="pl-10"
138+
/>
139+
<Search
140+
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
141+
size={20}
142+
/>
143+
</div>
144+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
145+
{(searchTerm ? filteredData : data).map((item, index) => (
146+
<Card key={index} className="overflow-hidden">
147+
<CardHeader className="p-0">
148+
<div className="relative pb-[100%]">
149+
<Avatar className="absolute inset-0 w-full h-full rounded-none">
150+
<AvatarImage
151+
src={item.user.forms?.[0]?.photo || ""}
152+
alt={item.user.name}
153+
className="object-cover"
154+
/>
155+
<AvatarFallback>{item.user.name.charAt(0)}</AvatarFallback>
156+
</Avatar>
157+
</div>
158+
</CardHeader>
159+
<CardContent className="p-4">
160+
<CardTitle className="text-xl mb-2">{item.user.name}</CardTitle>
161+
<p className="text-sm text-muted-foreground mb-1">{item.user.email}</p>
162+
<p className="text-sm text-muted-foreground mb-1">ID: {item.razorpayPaymentId}</p>
163+
<p className="text-sm font-semibold">Amount: ₹{item.amount.toFixed(2)}</p>
164+
</CardContent>
165+
</Card>
166+
))}
167+
</div>
168+
{searchTerm === "" && hasMoreData && (
169+
<div ref={loaderRef} className="flex justify-center py-4">
170+
{isLoading && <Loader2 className="h-6 w-6 animate-spin" />}
171+
</div>
172+
)}
172173
</div>
173-
)}
174-
</div>
175-
);
174+
);
176175
}

‎src/constants/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ export const basePrice = 980.39;
55
export const initialdiscount = 0;
66
export const sjecStudentPrice = 735.29;
77
export const sjecFacultyPrice = 784.31;
8+
export enum UserRole {
9+
ADMIN = "ADMIN",
10+
PARTICIPANT = "PARTICIPANT",
11+
COORDINATOR = "COORDINATOR",
12+
}
13+
14+
export const ADMIN_USERS_PATH = "/admin/users";
815
export const speakers: Speaker[] = [
916
{
1017
id: 1,

‎src/middleware.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,10 @@ export async function middleware(request: NextRequest) {
99
const url = request.nextUrl;
1010

1111
if (url.pathname.startsWith("/admin")) {
12-
if (token?.role !== "ADMIN") {
12+
if (token?.role !== "ADMIN" && token?.role !== "COORDINATOR") {
1313
return NextResponse.redirect(new URL("/", request.url));
1414
}
1515
}
16-
// if (url.pathname.startsWith("/register")) {
17-
// if (token?.role !== "ADMIN") {
18-
// return NextResponse.redirect(new URL("/", request.url));
19-
// }
20-
// }
21-
2216
}
2317

2418
// See "Matching Paths" below to learn more

‎src/types/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ReactNode } from "react";
22

3-
export type UserRoleType = "ADMIN" | "PARTICIPANT";
3+
export type UserRoleType = "ADMIN" | "PARTICIPANT" | "COORDINATOR";
44

55
export interface Speaker {
66
id: number;
@@ -45,7 +45,7 @@ export interface ResendEmailOptions {
4545
}
4646

4747
export interface FormDataInterface {
48-
designation: "student" | "faculty" | "employee";
48+
designation: "student" | "faculty" | "external";
4949
foodPreference: "veg" | "non-veg";
5050
name: string;
5151
email: string;

‎src/utils/zod-schemas.ts

+7
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,10 @@ export const studentFormSchema = z.object({
4444
idCard: z.string().min(1, { message: "ID Card is required for students." }),
4545
photo: z.string().min(1, { message: "Photo is required." }),
4646
});
47+
48+
49+
export const couponSchema = z.object({
50+
coupon: z.string().min(1, { message: "Coupon code is required" }),
51+
createdById: z.string(),
52+
discount: z.number().min(0).max(100).default(20),
53+
});

0 commit comments

Comments
 (0)
Please sign in to comment.