Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin page #88

Merged
merged 38 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5058a1b
Merge branch 'main' of https://github.com/TEDx-SJEC/website
joywin2003 Nov 25, 2024
b8feae8
feat: enhance form submission with validation and error handling
joywin2003 Nov 25, 2024
bb835da
feat: implement user role management with validation and error handli…
joywin2003 Nov 25, 2024
dde1f62
feat: add coupon validation and error handling in saveCoupon function
joywin2003 Nov 25, 2024
2acc9f0
feat: add coupon existence check and update discount type in saveCoup…
joywin2003 Nov 25, 2024
2d93e9d
feat: add clipboard copy functionality for coupon code and improve co…
joywin2003 Nov 26, 2024
1766ff7
feat: enhance user retrieval functions with error handling and valida…
joywin2003 Nov 26, 2024
a16636b
Merge branch 'main' of https://github.com/TEDx-SJEC/website into admi…
joywin2003 Nov 27, 2024
10685e8
style: update admin navbar colors and layout for improved aesthetics
joywin2003 Nov 27, 2024
ce8857c
style: enhance unauthorized and forbidden page messages with improved…
joywin2003 Nov 27, 2024
50b5691
feat: add COORDINATOR role and update role management functionality
joywin2003 Nov 27, 2024
b106b9e
style: update admin layout and razorpay page for improved aesthetics …
joywin2003 Nov 27, 2024
57f7697
style: enhance payment and razorpay pages with improved layout and em…
joywin2003 Nov 28, 2024
35c0fd3
Merge branch 'main' of https://github.com/TEDx-SJEC/website into admi…
joywin2003 Nov 29, 2024
f8c8ae2
style: improved responsiveness for mobile view
joywin2003 Nov 29, 2024
5984e31
Merge branch 'main' of https://github.com/TEDx-SJEC/website into admi…
Vyshnav001 Nov 29, 2024
75278bf
feat: Add getPaymentCount function
Vyshnav001 Nov 30, 2024
8d064c5
feat: Add getUserCount function
Vyshnav001 Nov 30, 2024
0a0add3
refactor: Improve error handling in GET payment route
Vyshnav001 Nov 30, 2024
89d5ca1
add totoal number of user count
Vyshnav001 Nov 30, 2024
e11596b
add total number of payment count
Vyshnav001 Nov 30, 2024
c3d8b8c
refactor: Improve error handling and add multiple coupon code creatio…
Vyshnav001 Nov 30, 2024
6a5f7e4
feat: Add Loading component
Vyshnav001 Nov 30, 2024
1f84930
feat : bulk coupon code creation
Vyshnav001 Nov 30, 2024
543c4ba
Merge branch 'main' of https://github.com/TEDx-SJEC/website into admi…
Vyshnav001 Nov 30, 2024
9fd0964
feat: Add loading state and support for COORDINATOR role in admin lay…
joywin2003 Dec 1, 2024
7408750
Merge branch 'admin-page' of https://github.com/TEDx-SJEC/website int…
joywin2003 Dec 1, 2024
b3ee870
feat: rendering nav items based on role and some style imporvements
joywin2003 Dec 1, 2024
cd34edc
feat: enhance admin layout with loading state and Google sign-in, upd…
joywin2003 Dec 1, 2024
5b92f9c
refactor: added an error message when camera is not allowed
joywin2003 Dec 1, 2024
13e1237
style: added redirecting animation
joywin2003 Dec 1, 2024
760dd83
refactor: clean up signout page layout and improve middleware role ch…
joywin2003 Dec 1, 2024
e8a6150
style: added a welcome page when coordinator logs in
joywin2003 Dec 1, 2024
baaed92
refactor:removed users and added verify in admin nav
joywin2003 Dec 1, 2024
ac12483
refactor: allow COORDINATOR role access in payment routes
joywin2003 Dec 2, 2024
1d76809
Refactor payment page to use PaymentCards component
Vyshnav001 Dec 2, 2024
553b750
Refactor payment route to include user's photo in the response
Vyshnav001 Dec 2, 2024
005838a
Refactor searchable infinite scroll table to use payment cards component
Vyshnav001 Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions src/app/actions/change-role.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
"use server";

import prisma from "@/server/db";
import { getServerSideSession } from "@/lib/get-server-session";
import { revalidatePath } from "next/cache";
import getErrorMessage from "@/utils/getErrorMessage";
import { UserRole,ADMIN_USERS_PATH } from "@/constants";

async function updateUserRole(id: string, role: string) {
async function updateUserRole(id: string, role: UserRole) {
const VALID_ROLES = Object.values(UserRole);
if (!VALID_ROLES.includes(role)) {
throw new Error(`Invalid role: ${role}`);
}
const session = await getServerSideSession();
if (!session || session.user.role !== UserRole.ADMIN) {
throw new Error(`Unauthorized Access...`);
}
try {
const updatedUser = await prisma.user.update({
where: { id },
data: { role },
});
revalidatePath("/admin/users");
revalidatePath(ADMIN_USERS_PATH);
return updatedUser;
} catch (error) {
console.error("Error updating user role:", error);
return null;
console.error("Error updating user role:", getErrorMessage(error));
throw new Error("Failed to update user role. Please try again later.");
}
}

export const makeAdmin = async (userId: string) => {
return await updateUserRole(userId, "ADMIN");
try {
return await updateUserRole(userId, UserRole.ADMIN);
} catch (error) {
console.error("Failed to make user admin:", getErrorMessage(error));
return null;
}
};

export const makeParticipant = async (userId: string) => {
return await updateUserRole(userId, "PARTICIPANT");
try {
return await updateUserRole(userId, UserRole.PARTICIPANT);
} catch (error) {
console.error("Failed to make user participant:", getErrorMessage(error));
return null;
}
};

export const makeCoordinator = async (userId: string) => {
try {
return await updateUserRole(userId, UserRole.COORDINATOR);
} catch (error) {
console.error("Failed to make user coordinator:", getErrorMessage(error));
return null;
}
};
51 changes: 40 additions & 11 deletions src/app/actions/create-coupon-code.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
"use server";
import { generateCouponCode } from "@/lib/helper";
import prisma from "@/server/db";
import { couponSchema } from "@/utils/zod-schemas";

export const saveCoupon = async (
coupon: string,
id: string,
discount: string = "20",
coupon: string,
createdById: string,
discount: number = 20,
numberOfCoupons: number = 1
) => {
const resp = await prisma.referral.create({
data: {
code: coupon,
isUsed: false,
createdById: id,
discountPercentage: discount,
},
});
try {
const validatCoupon = couponSchema.parse({ coupon, createdById, discount });

// Check if the coupon already exists
const couponExists = await prisma.referral.findFirst({
where: { code: validatCoupon.coupon },
});
if (couponExists) {
throw new Error("Coupon code already exists");
}

// Create coupons
const createCoupon = async (code: string) => {
return prisma.referral.create({
data: {
code,
isUsed: false,
createdById: validatCoupon.createdById,
discountPercentage: validatCoupon.discount.toString(),
},
});
};

const couponCodes =
numberOfCoupons === 1
? [validatCoupon.coupon]
: Array.from({ length: numberOfCoupons }, () => generateCouponCode(10));

const responses = await Promise.all(couponCodes.map(createCoupon));
return responses;
} catch (error) {
console.error("Error creating coupon:", error);
throw new Error("Failed to create coupon. Please try again later.");
}
};
15 changes: 15 additions & 0 deletions src/app/actions/get-payment-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use server";

import { getServerSideSession } from "@/lib/get-server-session";
import prisma from "@/server/db";

export default async function getPaymentCount() {
const session = await getServerSideSession();
if (!session) {
return null;
}

const paymentCount = await prisma.payment.count();

return paymentCount;
}
25 changes: 16 additions & 9 deletions src/app/actions/get-user-by-id.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import prisma from "@/server/db"; // Adjust the import based on your structure

export async function getUserById(userId: string) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
role: true, // Include any other fields you need
},
});

return user;
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
role: true,
},
});
if (!user) {
throw new Error(`User with ID ${userId} not found`);
}
return user;
} catch (error) {
console.error("Error getting user by id:", error);
throw new Error("Failed to get user. Please try again later.");
}
}
14 changes: 14 additions & 0 deletions src/app/actions/get-user-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use server";
import prisma from "@/server/db";
import { getServerSideSession } from "@/lib/get-server-session";

export default async function getUserCount() {
const session = await getServerSideSession();
if (!session) {
return null;
}

const userCount = await prisma.user.count();

return userCount;
}
12 changes: 9 additions & 3 deletions src/app/actions/is-allowed-to-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import prisma from "@/server/db";

export const isAllowedToAccess = async (email: string): Promise<boolean> => {
if (!email) return false;
try {
const user = await prisma.sjecUser.findFirst({
where: {
email: email,
},
where: {
email: email,
},
});
return user !== null;
} catch (error) {
console.error("Error getting user by email:", error);
return false;
}
};
43 changes: 27 additions & 16 deletions src/app/actions/submit-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@
import { getServerSideSession } from "@/lib/get-server-session";
import prisma from "@/server/db";
import { FormDataInterface } from "@/types";
import { z } from "zod";

const amountSchema = z.number().positive("Amount must be a positive number.");

export async function submitForm(data: FormDataInterface, amount: number) {
const session = await getServerSideSession();
if (!session) {
return;
throw new Error("User is not authenticated");
}
const validatedAmount = amountSchema.parse(amount);

const totalAmount = Math.round(validatedAmount + validatedAmount * 0.02);

return await prisma.form.create({
data: {
name: data.name,
usn: data.usn,
email: data.email,
foodPreference: data.foodPreference,
contact: data.phone,
designation: data.designation,
paidAmount: amount,
photo: data.photo,
collegeIdCard: data.idCard,
createdById: session.user.id,
entityName: data.entityName,
},
});
try {
return await prisma.form.create({
data: {
name: data.name,
usn: data.usn,
email: data.email,
foodPreference: data.foodPreference,
contact: data.phone,
designation: data.designation,
paidAmount: totalAmount,
photo: data.photo,
collegeIdCard: data.idCard,
createdById: session.user.id,
entityName: data.entityName,
},
});
} catch (error) {
console.error("Error creating form:", error);
throw new Error("Failed to submit the form. Please try again later.");
}
}
58 changes: 50 additions & 8 deletions src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { Inter } from "next/font/google";
import "./globals.css";
import Providers from "@/components/layout/Provider";
import { AdminNavbar } from "@/components/admin/Navbar/navbar";
import { useSession } from "next-auth/react";
import { signIn, useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { tailChase } from "ldrs";

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

Expand All @@ -14,22 +16,62 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const { data: session } = useSession({
const { data: session, status } = useSession({
required: true,
onUnauthenticated: async () => {
await signIn("google");
},
});

if (typeof window !== "undefined") {
tailChase.register();
}

const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
if (status === "loading") {
setIsLoading(true);
} else {
setIsLoading(false);
}
}, [status]);

if (isLoading || status !== "authenticated" || !session) {
// Show the loading spinner if session is loading or not authenticated
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<l-tail-chase
size={"88"}
speed={"1.75"}
color={"#FF0000"}
></l-tail-chase>
</div>
);
}

if (!session) {
return (
<div className="w-screen h-screen flex justify-center items-center">
Unauthorized
<div className="w-screen h-screen flex justify-center items-center bg-black text-gray-200">
<div className="text-center">
<h1 className="text-3xl font-bold mb-2 text-red-500">Unauthorized</h1>
<p className="text-gray-400">
You need to log in to access this page.
</p>
</div>
</div>
);
}

if (session.user.role !== "ADMIN") {
if (session.user.role !== "ADMIN" && session.user.role !== "COORDINATOR") {
return (
<div className="w-screen h-screen flex justify-center items-center">
Forbidden
<div className="w-screen h-screen flex justify-center items-center bg-black text-gray-200">
<div className="text-center">
<h1 className="text-3xl font-bold mb-2 text-red-500">Forbidden</h1>
<p className="text-gray-400">
You do not have the required permissions to view this page.
</p>
</div>
</div>
);
}
Expand All @@ -40,7 +82,7 @@ export default function RootLayout({
<Providers>
<div className="flex h-screen overflow-hidden">
<AdminNavbar />
<main className="flex-1 overflow-auto bg-indigo-50">
<main className="ml-16 md:ml-0 flex-1 overflow-y-auto bg-gray-800">
{children}
</main>
</div>
Expand Down
31 changes: 31 additions & 0 deletions src/app/admin/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client"
import React from "react";

const Loading: React.FC = () => {
return (
<div className="flex items-center justify-center h-screen text-red-500">
<div className="loader"></div>
<style jsx>{`
.loader {
border: 8px solid #f3f3f3; /* Light grey */
border-top: 8px solid #3498db; /* Blue */
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`}</style>
</div>
);
};

export default Loading;
Loading
Loading