From b8feae8a21ddf53576cd425da68466c1493005ef Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:16:34 +0530 Subject: [PATCH 01/32] feat: enhance form submission with validation and error handling --- src/app/actions/submit-form.ts | 43 +++++++++++++++++++++------------- src/types/index.ts | 4 ++-- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/app/actions/submit-form.ts b/src/app/actions/submit-form.ts index 3d19454..d0634af 100644 --- a/src/app/actions/submit-form.ts +++ b/src/app/actions/submit-form.ts @@ -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."); + } } diff --git a/src/types/index.ts b/src/types/index.ts index 5772a6b..6ad1c05 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { ReactNode } from "react"; -export type UserRoleType = "ADMIN" | "PARTICIPANT"; +export type UserRoleType = "ADMIN" | "PARTICIPANT" ; export interface Speaker { id: number; @@ -33,7 +33,7 @@ export interface ResendEmailOptions { } export interface FormDataInterface { - designation: "student" | "faculty" | "employee"; + designation: "student" | "faculty" | "external"; foodPreference: "veg" | "non-veg"; name: string; email: string; From bb835da58a6c9fa601d7d41a7bd2a1d0342e6a0e Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:29:10 +0530 Subject: [PATCH 02/32] feat: implement user role management with validation and error handling with new role --- src/app/actions/change-role.ts | 50 ++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/app/actions/change-role.ts b/src/app/actions/change-role.ts index b28fea2..b33a0c1 100644 --- a/src/app/actions/change-role.ts +++ b/src/app/actions/change-role.ts @@ -1,25 +1,63 @@ "use server"; import prisma from "@/server/db"; +import { getServerSideSession } from "@/lib/get-server-session"; import { revalidatePath } from "next/cache"; +import getErrorMessage from "@/utils/getErrorMessage"; -async function updateUserRole(id: string, role: string) { +export enum UserRole { + ADMIN = "ADMIN", + PARTICIPANT = "PARTICIPANT", + COORDINATOR = "COORDINATOR", +} + +const ADMIN_USERS_PATH = "/admin/users"; + +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 makeOrganizer = async (userId: string) => { + try { + return await updateUserRole(userId, UserRole.COORDINATOR); + } catch (error) { + console.error("Failed to make user coordinator:", getErrorMessage(error)); + return null; + } }; From dde1f62cb044cbba6719d83539eeb353457fc3da Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:52:35 +0530 Subject: [PATCH 03/32] feat: add coupon validation and error handling in saveCoupon function --- src/app/actions/create-coupon-code.ts | 28 +++++++++++++++++---------- src/utils/zod-schemas.ts | 7 +++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/app/actions/create-coupon-code.ts b/src/app/actions/create-coupon-code.ts index b659ffb..f87b7d0 100644 --- a/src/app/actions/create-coupon-code.ts +++ b/src/app/actions/create-coupon-code.ts @@ -1,17 +1,25 @@ "use server"; import prisma from "@/server/db"; +import { couponSchema } from "@/utils/zod-schemas"; export const saveCoupon = async ( coupon: string, - id: string, - discount: string = "20", + createdById: string, + discount: string = "20" ) => { - const resp = await prisma.referral.create({ - data: { - code: coupon, - isUsed: false, - createdById: id, - discountPercentage: discount, - }, - }); + try { + const validatCoupon = couponSchema.parse({ coupon, createdById, discount }); + const resp = await prisma.referral.create({ + data: { + code: validatCoupon.coupon, + isUsed: false, + createdById: validatCoupon.createdById, + discountPercentage: validatCoupon.discount.toString(), + }, + }); + return resp; + } catch (error) { + console.error("Error creating coupon:", error); + throw new Error("Failed to create coupon. Please try again later."); + } }; diff --git a/src/utils/zod-schemas.ts b/src/utils/zod-schemas.ts index 7ea4bbe..daa1339 100644 --- a/src/utils/zod-schemas.ts +++ b/src/utils/zod-schemas.ts @@ -50,3 +50,10 @@ export const studentFormSchema = z.object({ idCard: z.string().min(1, { message: "ID Card is required for students." }), photo: z.string().min(1, { message: "Photo is required." }), }); + + +export const couponSchema = z.object({ + coupon: z.string().min(1, { message: "Coupon code is required" }), + createdById: z.string(), + discount: z.number().min(0).max(100).default(20), +}); From 2acc9f0343b60caaf33b6ea8e7c2654ebb7b7380 Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:24:35 +0530 Subject: [PATCH 04/32] feat: add coupon existence check and update discount type in saveCoupon function --- src/app/actions/create-coupon-code.ts | 10 +++++++++- src/components/admin/code-generation-card.tsx | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/actions/create-coupon-code.ts b/src/app/actions/create-coupon-code.ts index f87b7d0..ff43411 100644 --- a/src/app/actions/create-coupon-code.ts +++ b/src/app/actions/create-coupon-code.ts @@ -5,10 +5,18 @@ import { couponSchema } from "@/utils/zod-schemas"; export const saveCoupon = async ( coupon: string, createdById: string, - discount: string = "20" + discount: number = 20 ) => { try { const validatCoupon = couponSchema.parse({ coupon, createdById, discount }); + const couponExists = await prisma.referral.findFirst({ + where: { + code: validatCoupon.coupon, + }, + }); + if (couponExists) { + throw new Error("Coupon code already exists"); + } const resp = await prisma.referral.create({ data: { code: validatCoupon.coupon, diff --git a/src/components/admin/code-generation-card.tsx b/src/components/admin/code-generation-card.tsx index 45c5e6b..9b1f618 100644 --- a/src/components/admin/code-generation-card.tsx +++ b/src/components/admin/code-generation-card.tsx @@ -19,6 +19,8 @@ import { type Session as NextAuthSession } from "next-auth"; import CouponGeneratorDialog from "../payment/coupon-generator-dialog"; import { Checkbox } from "../ui/checkbox"; import { useState } from "react"; +// import { toast } from "sonner"; +// import getErrorMessage from "@/utils/getErrorMessage"; export function Coupon({ session }: { session: NextAuthSession }) { const [discount, setDiscount] = useState("20"); @@ -29,6 +31,16 @@ export function Coupon({ session }: { session: NextAuthSession }) { enabled: false, }); + const handleGenerateCoupon = async () => { + try { + await saveCoupon(data as string, session.user.id, Number(discount)); + // refetch(); + // toast.success("Coupon code saved successfully"); + } catch (error) { + // toast.error(getErrorMessage(error)); + } + }; + return ( @@ -94,10 +106,8 @@ export function Coupon({ session }: { session: NextAuthSession }) { From 2d93e9d157de09d510db2848f1e874d3b26d15cb Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:36:33 +0530 Subject: [PATCH 05/32] feat: add clipboard copy functionality for coupon code and improve coupon generation flow --- src/components/admin/code-generation-card.tsx | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/components/admin/code-generation-card.tsx b/src/components/admin/code-generation-card.tsx index 9b1f618..dd6369e 100644 --- a/src/components/admin/code-generation-card.tsx +++ b/src/components/admin/code-generation-card.tsx @@ -20,24 +20,36 @@ import CouponGeneratorDialog from "../payment/coupon-generator-dialog"; import { Checkbox } from "../ui/checkbox"; import { useState } from "react"; // import { toast } from "sonner"; -// import getErrorMessage from "@/utils/getErrorMessage"; +import getErrorMessage from "@/utils/getErrorMessage"; +import { Copy, Check } from "lucide-react"; export function Coupon({ session }: { session: NextAuthSession }) { const [discount, setDiscount] = useState("20"); const [checked, setChecked] = useState(false); + const [disabled, setDisabled] = useState(true); + const [copied, setCopied] = useState(false); const { data, isPending, isError, error, refetch } = useQuery({ queryKey: ["coupon"], queryFn: createCouponCode, enabled: false, }); + const handleCopy = () => { + if (!data) return; + navigator.clipboard.writeText(data).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds + }); + }; + const handleGenerateCoupon = async () => { try { await saveCoupon(data as string, session.user.id, Number(discount)); - // refetch(); - // toast.success("Coupon code saved successfully"); + // refetch(); + setDisabled(true); + alert("Coupon code saved successfully"); } catch (error) { - // toast.error(getErrorMessage(error)); + alert(getErrorMessage(error)); } }; @@ -70,6 +82,8 @@ export function Coupon({ session }: { session: NextAuthSession }) { setDiscount(e.target.value); }} /> + +
- + { + refetch(); + setDisabled(false); + }} + /> @@ -101,12 +120,25 @@ export function Coupon({ session }: { session: NextAuthSession }) {
- +
+ + +
- -
+ + + + + +
+ + + +
+
+
); } diff --git a/src/components/admin/user-list.tsx b/src/components/admin/user-list.tsx index e921fd4..1c8d917 100644 --- a/src/components/admin/user-list.tsx +++ b/src/components/admin/user-list.tsx @@ -2,7 +2,13 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; import axios from "axios"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Button } from "../ui/button"; @@ -12,142 +18,148 @@ import debounce from "lodash.debounce"; import ChangeRole from "./change-role"; export interface User { - id: string; - name: string | null; - email: string | null; - role: string; - image: string | null; + id: string; + name: string | null; + email: string | null; + role: string; + image: string | null; } interface UsersListProps { - initialUsers: User[]; - initialPage: number; + initialUsers: User[]; + initialPage: number; } const UsersList: React.FC = ({ initialUsers, initialPage }) => { - const [userList, setUserList] = useState(initialUsers); - const [currentPage, setCurrentPage] = useState(initialPage); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [searchQuery, setSearchQuery] = useState(""); - const loader = useRef(null); + const [userList, setUserList] = useState(initialUsers); + const [currentPage, setCurrentPage] = useState(initialPage); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const loader = useRef(null); - const fetchUsers = useCallback(async (page: number, query: string, isNewSearch: boolean) => { - if (loading) return; - setLoading(true); - try { - const response = await axios.get(`/api/users?page=${page}&search=${encodeURIComponent(query)}`, { - headers: { "Cache-Control": "no-cache" }, - }); - if (response.data.users.length > 0) { - setUserList((prevUsers) => - isNewSearch ? response.data.users : [...prevUsers, ...response.data.users] - ); - setCurrentPage(page); - setHasMore(response.data.users.length === 10); // Assuming 10 is the page size - } else { - setHasMore(false); - } - } catch (error) { - console.error("Error fetching users:", error); + const fetchUsers = useCallback( + async (page: number, query: string, isNewSearch: boolean) => { + if (loading) return; + setLoading(true); + try { + const response = await axios.get( + `/api/users?page=${page}&search=${encodeURIComponent(query)}`, + { + headers: { "Cache-Control": "no-cache" }, + } + ); + if (response.data.users.length > 0) { + setUserList((prevUsers) => + isNewSearch + ? response.data.users + : [...prevUsers, ...response.data.users] + ); + setCurrentPage(page); + setHasMore(response.data.users.length === 10); // Assuming 10 is the page size + } else { + setHasMore(false); } - setLoading(false); - }, []); + } catch (error) { + console.error("Error fetching users:", error); + } + setLoading(false); + }, + [] + ); - const loadMoreUsers = useCallback(() => { - if (hasMore && !loading) { - fetchUsers(currentPage + 1, searchQuery, false); - } - }, [currentPage, hasMore, searchQuery, fetchUsers, loading]); + const loadMoreUsers = useCallback(() => { + if (hasMore && !loading) { + fetchUsers(currentPage + 1, searchQuery, false); + } + }, [currentPage, hasMore, searchQuery, fetchUsers, loading]); - const debouncedSearch = useCallback( - debounce((query: string) => { - setCurrentPage(1); - setHasMore(true); - fetchUsers(1, query, true); - }, 500), - [fetchUsers] - ); + const debouncedSearch = useCallback( + debounce((query: string) => { + setCurrentPage(1); + setHasMore(true); + fetchUsers(1, query, true); + }, 500), + [fetchUsers] + ); - const handleSearchChange = (e: React.ChangeEvent) => { - const query = e.target.value; - setSearchQuery(query); - debouncedSearch(query); - }; - - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore) { - loadMoreUsers(); - } - }, - { threshold: 1.0 } - ); + const handleSearchChange = (e: React.ChangeEvent) => { + const query = e.target.value; + setSearchQuery(query); + debouncedSearch(query); + }; - if (loader.current) { - observer.observe(loader.current); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + loadMoreUsers(); } + }, + { threshold: 1.0 } + ); + + if (loader.current) { + observer.observe(loader.current); + } - return () => observer.disconnect(); - }, [hasMore, loadMoreUsers]); + return () => observer.disconnect(); + }, [hasMore, loadMoreUsers]); - return ( - -
-
- - + return ( + +
+
+ + +
+
+ + Users {userList.length} + Manage user roles and permissions. + + +
+ {userList.map((user) => ( +
+
+ + + + {user.name ? user.name[0] : "N/A"} + + +
+

+ {user.name || "Unknown"} +

+

+ {user.email || "No email"} +

+
+ +
- - Users - Manage user roles and permissions. - - -
- {userList.map((user) => ( -
-
- - - {user.name ? user.name[0] : "N/A"} - -
-

{user.name || "Unknown"}

-

- {user.email || "No email"} -

-
-
- - - - - - - - -
- ))} -
- {hasMore && ( -
- {loading ? "Loading..." : "Load more"} -
- )} -
- - ); + ))} +
+ {hasMore && ( +
+ {loading ? "Loading..." : "Load more"} +
+ )} +
+
+ ); }; export default UsersList; diff --git a/src/constants/index.ts b/src/constants/index.ts index 8eb5c11..55a6ad0 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -5,6 +5,13 @@ export const basePrice = 980.39; export const initialdiscount = 0; export const sjecStudentPrice = 735.29; export const sjecFacultyPrice = 784.31; +export enum UserRole { + ADMIN = "ADMIN", + PARTICIPANT = "PARTICIPANT", + COORDINATOR = "COORDINATOR", +} + +export const ADMIN_USERS_PATH = "/admin/users"; export const speakers: Speaker[] = [ { id: 1, diff --git a/src/types/index.ts b/src/types/index.ts index 63617de..ecd13bf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { ReactNode } from "react"; -export type UserRoleType = "ADMIN" | "PARTICIPANT" ; +export type UserRoleType = "ADMIN" | "PARTICIPANT" | "COORDINATOR"; export interface Speaker { id: number; From b106b9e650b1dbb7cfceb73010f281843f586840 Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:45:26 +0530 Subject: [PATCH 10/32] style: update admin layout and razorpay page for improved aesthetics and functionality --- src/app/admin/layout.tsx | 2 +- src/app/admin/page.tsx | 2 +- src/app/admin/razorpay/page.tsx | 108 ++++++++++++++++---------------- 3 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 24e3e02..13c2d8b 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -50,7 +50,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index ddcca3d..98cb1ce 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -10,7 +10,7 @@ export default function AdminPage() { return ( <> -
+
diff --git a/src/app/admin/razorpay/page.tsx b/src/app/admin/razorpay/page.tsx index afeab50..5afe394 100644 --- a/src/app/admin/razorpay/page.tsx +++ b/src/app/admin/razorpay/page.tsx @@ -1,4 +1,6 @@ -"use client"; +"use client" + +import { Button } from "@/components/ui/button" import { Form, FormControl, @@ -7,70 +9,66 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useRouter } from "next/navigation"; -import React from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" +import React from "react" +import { useForm } from "react-hook-form" +import { z } from "zod" const formSchema = z.object({ razorpayPaymentId: z.string().nonempty("Payment ID is required"), -}); +}) const FetchRazorpayPaymentData = () => { const form = useForm>({ resolver: zodResolver(formSchema), - }); - const router = useRouter(); + }) + const router = useRouter() const onSubmit = async (data: z.infer) => { - router.push(`/admin/razorpay/${data.razorpayPaymentId}`); - }; + router.push(`/admin/razorpay/${data.razorpayPaymentId}`) + } return ( -
-

- Search by Razorpay Payment ID -

-
- - ( - - - Razorpay Payment ID: - - - - - - Search for a payment by its Razorpay Payment ID - - - - )} - /> - - - +
+ + + + Search by Razorpay Payment ID + + + +
+ + ( + + Razorpay Payment ID + + + + + Search for a payment by its Razorpay Payment ID + + + + )} + /> + + + +
+
- ); -}; + ) +} + +export default FetchRazorpayPaymentData -export default FetchRazorpayPaymentData; From 57f76978d24f6342d447f3a66e68d8f79e8dadb5 Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:39:05 +0530 Subject: [PATCH 11/32] style: enhance payment and razorpay pages with improved layout and email functionality --- src/app/admin/payment/page.tsx | 3 +- src/app/admin/razorpay/[id]/page.tsx | 127 +++++++++--------- .../searchable-infinite-scroll-table.tsx | 3 + 3 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/app/admin/payment/page.tsx b/src/app/admin/payment/page.tsx index c995e17..37c387e 100644 --- a/src/app/admin/payment/page.tsx +++ b/src/app/admin/payment/page.tsx @@ -4,7 +4,8 @@ import React from "react"; export default async function Payments() { return ( <> -
+
+
diff --git a/src/app/admin/razorpay/[id]/page.tsx b/src/app/admin/razorpay/[id]/page.tsx index 2dcd4a0..e3e5ff9 100644 --- a/src/app/admin/razorpay/[id]/page.tsx +++ b/src/app/admin/razorpay/[id]/page.tsx @@ -65,6 +65,16 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) { const [loading, setLoading] = useState(true); const [loadingForButton, setLoadingForButton] = useState(false); + const handleSendEmail = async () => { + if (!paymentData) { + return; + } + setLoadingForButton(true) + await sendEmail(paymentData.id) + setLoadingForButton(false) + } + + async function sendEmail(paymentId: string) { setLoadingForButton(true); try { @@ -127,67 +137,62 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) { } return ( -
- - - - Payment Data of {paymentData.notes.customerName || "Unknown"} - - - - - - - - - - - - - -
+
+ + + + Payment Data of {paymentData.notes.customerName || "Unknown"} + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
); } diff --git a/src/components/common/searchable-infinite-scroll-table.tsx b/src/components/common/searchable-infinite-scroll-table.tsx index 738d63e..2b67817 100644 --- a/src/components/common/searchable-infinite-scroll-table.tsx +++ b/src/components/common/searchable-infinite-scroll-table.tsx @@ -133,6 +133,9 @@ export function SearchableInfiniteScrollTable() { return (
+

+ Payments {filteredData.length > 0 ? `(${filteredData.length})` : `(${data.length})`} +

Date: Fri, 29 Nov 2024 20:12:19 +0530 Subject: [PATCH 12/32] style: improved responsiveness for mobile view --- src/app/admin/layout.tsx | 2 +- src/app/admin/payment/page.tsx | 3 +-- src/components/admin/Navbar/navbar.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 13c2d8b..fbe04ab 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -50,7 +50,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/src/app/admin/payment/page.tsx b/src/app/admin/payment/page.tsx index 37c387e..8650e45 100644 --- a/src/app/admin/payment/page.tsx +++ b/src/app/admin/payment/page.tsx @@ -4,8 +4,7 @@ import React from "react"; export default async function Payments() { return ( <> -
- +
diff --git a/src/components/admin/Navbar/navbar.tsx b/src/components/admin/Navbar/navbar.tsx index 8f08410..ce946f0 100644 --- a/src/components/admin/Navbar/navbar.tsx +++ b/src/components/admin/Navbar/navbar.tsx @@ -10,7 +10,7 @@ import Link from "next/link"; export const AdminNavbar = () => { return ( -
+
From 75278bf827329be012334c92c7b655b32405aabb Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 08:46:11 +0530 Subject: [PATCH 13/32] feat: Add getPaymentCount function --- src/app/actions/get-payment-count.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/app/actions/get-payment-count.ts diff --git a/src/app/actions/get-payment-count.ts b/src/app/actions/get-payment-count.ts new file mode 100644 index 0000000..53e98a4 --- /dev/null +++ b/src/app/actions/get-payment-count.ts @@ -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; +} From 8d064c5d1222348a6ee7019fa7bf6643ecc0d08e Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 08:46:20 +0530 Subject: [PATCH 14/32] feat: Add getUserCount function --- src/app/actions/get-user-count.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/app/actions/get-user-count.ts diff --git a/src/app/actions/get-user-count.ts b/src/app/actions/get-user-count.ts new file mode 100644 index 0000000..2cfd371 --- /dev/null +++ b/src/app/actions/get-user-count.ts @@ -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; +} \ No newline at end of file From 0a0add35be8e5dda94b755e76c643d785c59c44a Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 08:46:51 +0530 Subject: [PATCH 15/32] refactor: Improve error handling in GET payment route --- src/app/api/users/payment/route.ts | 126 ++++++++++++++++------------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/src/app/api/users/payment/route.ts b/src/app/api/users/payment/route.ts index 0e3c476..2653a3f 100644 --- a/src/app/api/users/payment/route.ts +++ b/src/app/api/users/payment/route.ts @@ -5,63 +5,79 @@ import { NextRequest, NextResponse } from "next/server"; export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { - const session = await getServerSideSession(); - if (!session) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } + const session = await getServerSideSession(); + if (!session) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } - if (session.user?.role !== "ADMIN") { - return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); - } - const { searchParams } = new URL(request.url); - try { - const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); - const search = searchParams.get("search") || ""; - const limit = 10; + if (session.user?.role !== "ADMIN") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); + } + const { searchParams } = new URL(request.url); + try { + const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); + const search = searchParams.get("search") || ""; + const limit = 10; - const [users, totalCount] = await Promise.all([ - prisma.payment.findMany({ - skip: (page - 1) * limit, - take: limit, - where: { - razorpayPaymentId: { - contains: search, - }, - }, - include: { - user: { - select: { - name: true, - email: true, - }, - }, - }, - }), - prisma.payment.count({ - where: { - razorpayPaymentId: { - contains: search, - }, - }, - }), - ]); + const [users, totalCount] = await Promise.all([ + prisma.payment.findMany({ + skip: (page - 1) * limit, + take: limit, + where: { + OR: [ + // Update to search in multiple fields + { + razorpayPaymentId: { + contains: search, + }, + }, + { + user: { + name: { + contains: search, + }, + }, + }, + { + user: { + email: { + contains: search, + }, + }, + }, + ], + }, + include: { + user: { + select: { + name: true, + email: true, + }, + }, + }, + }), + prisma.payment.count({ + where: { + razorpayPaymentId: { + contains: search, + }, + }, + }), + ]); - const totalPages = Math.ceil(totalCount / limit); + const totalPages = Math.ceil(totalCount / limit); - return NextResponse.json({ - users, - pagination: { - currentPage: page, - totalCount, - totalPages, - limit, - }, - }); - } catch (error) { - console.error("Error fetching payment details:", error); - return NextResponse.json( - { error: "Failed to fetch data" }, - { status: 500 }, - ); - } + return NextResponse.json({ + users, + pagination: { + currentPage: page, + totalCount, + totalPages, + limit, + }, + }); + } catch (error) { + console.error("Error fetching payment details:", error); + return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 }); + } } From 89d5ca18da4e6ba85d1d7c9e4ec67c1181ed255d Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 08:47:14 +0530 Subject: [PATCH 16/32] add totoal number of user count --- src/components/admin/user-list.tsx | 251 ++++++++++++++--------------- 1 file changed, 120 insertions(+), 131 deletions(-) diff --git a/src/components/admin/user-list.tsx b/src/components/admin/user-list.tsx index 1c8d917..7cab81e 100644 --- a/src/components/admin/user-list.tsx +++ b/src/components/admin/user-list.tsx @@ -2,13 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; import axios from "axios"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "../ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Button } from "../ui/button"; @@ -16,150 +10,145 @@ import { ChevronDownIcon, SearchIcon } from "lucide-react"; import { Input } from "../ui/input"; import debounce from "lodash.debounce"; import ChangeRole from "./change-role"; +import getUserCount from "@/app/actions/get-user-count"; export interface User { - id: string; - name: string | null; - email: string | null; - role: string; - image: string | null; + id: string; + name: string | null; + email: string | null; + role: string; + image: string | null; } interface UsersListProps { - initialUsers: User[]; - initialPage: number; + initialUsers: User[]; + initialPage: number; } const UsersList: React.FC = ({ initialUsers, initialPage }) => { - const [userList, setUserList] = useState(initialUsers); - const [currentPage, setCurrentPage] = useState(initialPage); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [searchQuery, setSearchQuery] = useState(""); - const loader = useRef(null); + const [userList, setUserList] = useState(initialUsers); + const [currentPage, setCurrentPage] = useState(initialPage); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const loader = useRef(null); + const [totalNumberOfUsers, setTotalNumberOfUsers] = useState(0); - const fetchUsers = useCallback( - async (page: number, query: string, isNewSearch: boolean) => { - if (loading) return; - setLoading(true); - try { - const response = await axios.get( - `/api/users?page=${page}&search=${encodeURIComponent(query)}`, - { - headers: { "Cache-Control": "no-cache" }, - } - ); - if (response.data.users.length > 0) { - setUserList((prevUsers) => - isNewSearch - ? response.data.users - : [...prevUsers, ...response.data.users] - ); - setCurrentPage(page); - setHasMore(response.data.users.length === 10); // Assuming 10 is the page size - } else { - setHasMore(false); + const fetchUsers = useCallback(async (page: number, query: string, isNewSearch: boolean) => { + if (loading) return; + setLoading(true); + try { + const response = await axios.get(`/api/users?page=${page}&search=${encodeURIComponent(query)}`, { + headers: { "Cache-Control": "no-cache" }, + }); + if (response.data.users.length > 0) { + setUserList((prevUsers) => + isNewSearch ? response.data.users : [...prevUsers, ...response.data.users] + ); + setCurrentPage(page); + setHasMore(response.data.users.length === 10); // Assuming 10 is the page size + } else { + setHasMore(false); + } + } catch (error) { + console.error("Error fetching users:", error); + } + setLoading(false); + }, []); + + const loadMoreUsers = useCallback(() => { + if (hasMore && !loading) { + fetchUsers(currentPage + 1, searchQuery, false); } - } catch (error) { - console.error("Error fetching users:", error); - } - setLoading(false); - }, - [] - ); + }, [currentPage, hasMore, searchQuery, fetchUsers, loading]); - const loadMoreUsers = useCallback(() => { - if (hasMore && !loading) { - fetchUsers(currentPage + 1, searchQuery, false); - } - }, [currentPage, hasMore, searchQuery, fetchUsers, loading]); + const debouncedSearch = useCallback( + debounce((query: string) => { + setCurrentPage(1); + setHasMore(true); + fetchUsers(1, query, true); + }, 500), + [fetchUsers] + ); - const debouncedSearch = useCallback( - debounce((query: string) => { - setCurrentPage(1); - setHasMore(true); - fetchUsers(1, query, true); - }, 500), - [fetchUsers] - ); + const handleSearchChange = (e: React.ChangeEvent) => { + const query = e.target.value; + setSearchQuery(query); + debouncedSearch(query); + }; - const handleSearchChange = (e: React.ChangeEvent) => { - const query = e.target.value; - setSearchQuery(query); - debouncedSearch(query); - }; + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + loadMoreUsers(); + } + }, + { threshold: 1.0 } + ); - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore) { - loadMoreUsers(); + if (loader.current) { + observer.observe(loader.current); } - }, - { threshold: 1.0 } - ); - if (loader.current) { - observer.observe(loader.current); - } + return () => observer.disconnect(); + }, [hasMore, loadMoreUsers]); - return () => observer.disconnect(); - }, [hasMore, loadMoreUsers]); + useEffect(() => { + async function getNumberOfUsers() { + const count = await getUserCount(); + setTotalNumberOfUsers(count ?? 0); // Use 0 if count is null + } + getNumberOfUsers(); + }, []); - return ( - -
-
- - -
-
- - Users {userList.length} - Manage user roles and permissions. - - -
- {userList.map((user) => ( -
-
- - - - {user.name ? user.name[0] : "N/A"} - - -
-

- {user.name || "Unknown"} -

-

- {user.email || "No email"} -

+ return ( + +
+
+ +
-
- -
- ))} -
- {hasMore && ( -
- {loading ? "Loading..." : "Load more"} -
- )} - - - ); + + Users {totalNumberOfUsers} + Manage user roles and permissions. + + +
+ {userList.map((user) => ( +
+
+ + + {user.name ? user.name[0] : "N/A"} + +
+

{user.name || "Unknown"}

+

+ {user.email || "No email"} +

+
+
+ + +
+ ))} +
+ {hasMore && ( +
+ {loading ? "Loading..." : "Load more"} +
+ )} +
+ + ); }; export default UsersList; From e11596b99a886c22136df31efde30b4166de9b0a Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 08:47:25 +0530 Subject: [PATCH 17/32] add total number of payment count --- .../searchable-infinite-scroll-table.tsx | 312 +++++++++--------- 1 file changed, 153 insertions(+), 159 deletions(-) diff --git a/src/components/common/searchable-infinite-scroll-table.tsx b/src/components/common/searchable-infinite-scroll-table.tsx index 2b67817..cdffdb1 100644 --- a/src/components/common/searchable-infinite-scroll-table.tsx +++ b/src/components/common/searchable-infinite-scroll-table.tsx @@ -1,179 +1,173 @@ "use client"; import { useEffect, useRef, useState, useCallback } from "react"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; import { Loader2, Search } from "lucide-react"; import axios from "axios"; import debounce from "lodash.debounce"; +import getPaymentCount from "@/app/actions/get-payment-count"; interface TableData { - user: { - name: string; - email: string; - }; - usn?: string; - razorpayPaymentId: string; - contactNumber: string; - amount: number; + user: { + name: string; + email: string; + }; + usn?: string; + razorpayPaymentId: string; + contactNumber: string; + amount: number; } export function SearchableInfiniteScrollTable() { - const [data, setData] = useState([]); - const [filteredData, setFilteredData] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [page, setPage] = useState(1); - const [searchTerm, setSearchTerm] = useState(""); - const [hasMoreData, setHasMoreData] = useState(true); - const loaderRef = useRef(null); - const observerRef = useRef(null); - - const getPaymentDetails = async (page: number, query: string) => { - if (isLoading || !hasMoreData) return; - - setIsLoading(true); - try { - const response = await axios.get( - `/api/users/payment?page=${page}&search=${encodeURIComponent(query)}`, - ); - const users = response.data.users; - - if (users.length === 0) { - setHasMoreData(false); // No more data to load - } - - setData((prevData) => { - const newData = [...prevData, ...users]; - // Remove duplicates - const uniqueData = Array.from( - new Map( - newData.map((item) => [item.razorpayPaymentId, item]), - ).values(), - ); - return uniqueData; - }); - setPage((prevPage) => prevPage + 1); - } catch (error) { - console.error("Error fetching payment details:", error); - } finally { - setIsLoading(false); - } - }; - - const loadMoreData = () => { - if (searchTerm === "") { - getPaymentDetails(page, ""); - } - }; - - const fetchSearchResults = useCallback(async (query: string) => { - setPage(1); // Reset page number - setHasMoreData(true); // Reset hasMoreData - try { - const response = await axios.get( - `/api/users/payment?page=1&search=${encodeURIComponent(query)}`, - ); - const users = response.data.users; - setData(users); // Set new data from search - setFilteredData(users); // Set filtered data to the same as new data - } catch (error) { - console.error("Error fetching payment details:", error); - } - }, []); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedFetch = useCallback( - debounce((query: string) => { - fetchSearchResults(query); - }, 500), - [], - ); - - const handleSearch = (event: React.ChangeEvent) => { - const value = event.target.value; - setSearchTerm(value); - debouncedFetch(value); // Use debounced fetch function - }; - - useEffect(() => { - loadMoreData(); // Initial load - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const [data, setData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [totalNumberOfPayments, setTotalNumberOfPayments] = useState(0); + const [page, setPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const [hasMoreData, setHasMoreData] = useState(true); + const loaderRef = useRef(null); + const observerRef = useRef(null); + + const getPaymentDetails = async (page: number, query: string) => { + if (isLoading || !hasMoreData) return; + + setIsLoading(true); + try { + const response = await axios.get( + `/api/users/payment?page=${page}&search=${encodeURIComponent(query)}` + ); + const users = response.data.users; + + if (users.length === 0) { + setHasMoreData(false); // No more data to load + } + + setData((prevData) => { + const newData = [...prevData, ...users]; + // Remove duplicates + const uniqueData = Array.from( + new Map(newData.map((item) => [item.razorpayPaymentId, item])).values() + ); + return uniqueData; + }); + setPage((prevPage) => prevPage + 1); + } catch (error) { + console.error("Error fetching payment details:", error); + } finally { + setIsLoading(false); + } + }; - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !isLoading) { - loadMoreData(); + const loadMoreData = () => { + if (searchTerm === "") { + getPaymentDetails(page, ""); } - }, - { threshold: 1.0 }, + }; + + const fetchSearchResults = useCallback(async (query: string) => { + setPage(1); // Reset page number + setHasMoreData(true); // Reset hasMoreData + try { + const response = await axios.get(`/api/users/payment?page=1&search=${encodeURIComponent(query)}`); + const users = response.data.users; + setData(users); // Set new data from search + setFilteredData(users); // Set filtered data to the same as new data + } catch (error) { + console.error("Error fetching payment details:", error); + } + }, []); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedFetch = useCallback( + debounce((query: string) => { + fetchSearchResults(query); + }, 500), + [] ); - if (loaderRef.current) { - observer.observe(loaderRef.current); - } + const handleSearch = (event: React.ChangeEvent) => { + const value = event.target.value; + setSearchTerm(value); + debouncedFetch(value); // Use debounced fetch function + }; - return () => { - if (loaderRef.current) { + useEffect(() => { + loadMoreData(); // Initial load // eslint-disable-next-line react-hooks/exhaustive-deps - observer.unobserve(loaderRef.current); - } - observer.disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); - - return ( -
-

- Payments {filteredData.length > 0 ? `(${filteredData.length})` : `(${data.length})`} -

-
- - -
- - - - Name - Email - Payment ID - Amount - - - - {(searchTerm ? filteredData : data).map((item, index) => ( - - {item.user.name} - {item.user.email} - {item.razorpayPaymentId} - ₹{item.amount.toFixed(2)} - - ))} - -
- {searchTerm === "" && hasMoreData && ( -
- {isLoading && } + async function getNumberOfPayments() { + const count = await getPaymentCount(); + setTotalNumberOfPayments(count ?? 0); // Use 0 if count is null + } + getNumberOfPayments(); + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !isLoading) { + loadMoreData(); + } + }, + { threshold: 1.0 } + ); + + if (loaderRef.current) { + observer.observe(loaderRef.current); + } + + return () => { + if (loaderRef.current) { + // eslint-disable-next-line react-hooks/exhaustive-deps + observer.unobserve(loaderRef.current); + } + observer.disconnect(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + + return ( +
+

Payments {totalNumberOfPayments}

+
+ + +
+ + + + Name + Email + Payment ID + Amount + + + + {(searchTerm ? filteredData : data).map((item, index) => ( + + {item.user.name} + {item.user.email} + {item.razorpayPaymentId} + ₹{item.amount.toFixed(2)} + + ))} + +
+ {searchTerm === "" && hasMoreData && ( +
+ {isLoading && } +
+ )}
- )} -
- ); + ); } From c3d8b8c4773d7fda6110cd2b2eba2d9d1a6c42c2 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 16:26:43 +0530 Subject: [PATCH 18/32] refactor: Improve error handling and add multiple coupon code creation in saveCoupon function --- src/app/actions/create-coupon-code.ts | 63 ++++++++++++++++----------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/app/actions/create-coupon-code.ts b/src/app/actions/create-coupon-code.ts index ff43411..7053578 100644 --- a/src/app/actions/create-coupon-code.ts +++ b/src/app/actions/create-coupon-code.ts @@ -1,33 +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, - createdById: string, - discount: number = 20 + coupon: string, + createdById: string, + discount: number = 20, + numberOfCoupons: number = 1 ) => { - try { - const validatCoupon = couponSchema.parse({ coupon, createdById, discount }); - const couponExists = await prisma.referral.findFirst({ - where: { - code: validatCoupon.coupon, - }, - }); - if (couponExists) { - throw new Error("Coupon code already exists"); + 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."); } - const resp = await prisma.referral.create({ - data: { - code: validatCoupon.coupon, - isUsed: false, - createdById: validatCoupon.createdById, - discountPercentage: validatCoupon.discount.toString(), - }, - }); - return resp; - } catch (error) { - console.error("Error creating coupon:", error); - throw new Error("Failed to create coupon. Please try again later."); - } }; From 6a5f7e4837e36c9433fb3695d89c69b06ea1d9c4 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 16:26:53 +0530 Subject: [PATCH 19/32] feat: Add Loading component --- src/app/admin/loading.tsx | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/app/admin/loading.tsx diff --git a/src/app/admin/loading.tsx b/src/app/admin/loading.tsx new file mode 100644 index 0000000..effbbb6 --- /dev/null +++ b/src/app/admin/loading.tsx @@ -0,0 +1,31 @@ +"use client" +import React from "react"; + +const Loading: React.FC = () => { + return ( +
+
+ +
+ ); +}; + +export default Loading; From 1f84930dee4ae43ed8d6f69e73d2f7ef21b62eef Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 30 Nov 2024 16:27:26 +0530 Subject: [PATCH 20/32] feat : bulk coupon code creation --- src/components/admin/code-generation-card.tsx | 217 ++++++++++-------- 1 file changed, 122 insertions(+), 95 deletions(-) diff --git a/src/components/admin/code-generation-card.tsx b/src/components/admin/code-generation-card.tsx index dd6369e..f09d288 100644 --- a/src/components/admin/code-generation-card.tsx +++ b/src/components/admin/code-generation-card.tsx @@ -22,10 +22,13 @@ import { useState } from "react"; // import { toast } from "sonner"; import getErrorMessage from "@/utils/getErrorMessage"; import { Copy, Check } from "lucide-react"; +import { toast } from "sonner"; export function Coupon({ session }: { session: NextAuthSession }) { const [discount, setDiscount] = useState("20"); + const [numberOfCoupons, setNumberOfCoupons] = useState("1"); const [checked, setChecked] = useState(false); + const [numberOfCouponsChecked, setNumberOfCouponsChecked] = useState(false); const [disabled, setDisabled] = useState(true); const [copied, setCopied] = useState(false); const { data, isPending, isError, error, refetch } = useQuery({ @@ -44,108 +47,132 @@ export function Coupon({ session }: { session: NextAuthSession }) { const handleGenerateCoupon = async () => { try { - await saveCoupon(data as string, session.user.id, Number(discount)); + await saveCoupon(data as string, session.user.id, Number(discount) , Number(numberOfCoupons)); // refetch(); setDisabled(true); - alert("Coupon code saved successfully"); + alert("Coupon code saved successfully"); } catch (error) { alert(getErrorMessage(error)); } }; return ( - - - Create - Store - - - - - Coupon code - - Here you can create the coupon code and add it to the database - - - -
- - -
-
- - { - setDiscount(e.target.value); - }} - /> -
-
- { - setChecked(!checked); - }} - /> - -
-
- - { - refetch(); - setDisabled(false); - }} - /> - -
-
- - - - Coupon code - - You can see the generated coupon code below - - - -
- -
- - -
-
-
- - - -
-
-
+ + + Create + Store + + + + + Coupon code + + Here you can create the coupon code and add it to the database + + + +
+ + +
+
+ + { + setDiscount(e.target.value); + }} + /> +
+
+ { + setChecked(!checked); + }} + /> + +
+
+ + { + setNumberOfCoupons(e.target.value); + }} + /> +
+
+ { + setNumberOfCouponsChecked(!numberOfCouponsChecked); + }} + /> + +
+
+ + { + refetch(); + setDisabled(false); + }} + /> + +
+
+ + + + Coupon code + You can see the generated coupon code below + + +
+ +
+ + +
+
+
+ + + +
+
+
); } From 9fd0964d8b2956c37c1c0ebf51fcc6dc03e16250 Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:23:21 +0530 Subject: [PATCH 21/32] feat: Add loading state and support for COORDINATOR role in admin layout and middleware --- src/app/admin/layout.tsx | 11 +++++++++-- src/middleware.ts | 10 +++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index fbe04ab..a42bc52 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -6,6 +6,7 @@ import "./globals.css"; import Providers from "@/components/layout/Provider"; import { AdminNavbar } from "@/components/admin/Navbar/navbar"; import { useSession } from "next-auth/react"; +import Loading from "./loading"; const inter = Inter({ subsets: ["latin"] }); @@ -14,10 +15,16 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const { data: session } = useSession({ + const { data: session, status : sessionStatus } = useSession({ required: true, }); + if (sessionStatus === "loading") { + return ( + + ); + } + if (!session) { return (
@@ -31,7 +38,7 @@ export default function RootLayout({ ); } - if (session.user.role !== "ADMIN") { + if (session.user.role !== "ADMIN" && session.user.role !== "COORDINATOR") { return (
diff --git a/src/middleware.ts b/src/middleware.ts index 4c5cbfc..d4611ae 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,11 +13,11 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(new URL("/", request.url)); } } - // if (url.pathname.startsWith("/register")) { - // if (token?.role !== "ADMIN") { - // return NextResponse.redirect(new URL("/", request.url)); - // } - // } + if (url.pathname.startsWith("/register")) { + if (token?.role !== "ADMIN" && token?.role !== "COORDINATOR") { + return NextResponse.redirect(new URL("/", request.url)); + } + } } From b3ee87059f835761ca48a72a2c390084fa010fbf Mon Sep 17 00:00:00 2001 From: Joywin Bennis <107112207+joywin2003@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:42:12 +0530 Subject: [PATCH 22/32] feat: rendering nav items based on role and some style imporvements --- src/app/admin/loading.tsx | 2 +- src/app/admin/page.tsx | 3 +- src/components/admin/Navbar/navbar.tsx | 64 +++++++++++-------- .../searchable-infinite-scroll-table.tsx | 2 +- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/app/admin/loading.tsx b/src/app/admin/loading.tsx index effbbb6..9410ac2 100644 --- a/src/app/admin/loading.tsx +++ b/src/app/admin/loading.tsx @@ -3,7 +3,7 @@ import React from "react"; const Loading: React.FC = () => { return ( -
+