diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6f85d8..3b459e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,4 +66,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: build - path: build # Adjust this if your build output is in a different directory + path: build # Adjust this if your build output is in a different directory \ No newline at end of file diff --git a/src/app/admin/payment/page.tsx b/src/app/admin/payment/page.tsx index c995e17..f7082f1 100644 --- a/src/app/admin/payment/page.tsx +++ b/src/app/admin/payment/page.tsx @@ -1,12 +1,14 @@ import { SearchableInfiniteScrollTable } from "@/components/common/searchable-infinite-scroll-table"; +import prisma from "@/server/db"; import React from "react"; export default async function Payments() { - return ( - <> -
- -
- - ); + const totalPayments = await prisma.payment.count(); + return ( + <> +
+ +
+ + ); } diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 897dc52..8ec201e 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -3,32 +3,37 @@ import prisma from "@/server/db"; import React from "react"; export default async function Users() { - let initialUserData = await prisma.user.findMany({ - select: { - id: true, - name: true, - email: true, - role: true, - image: true, - }, - take: 10, - }); - if (initialUserData === null) { - initialUserData = [ - { - id: "1", - name: "Test name", - email: "testEmail@gmail.com", - role: "PARTICIPANT", - image: "https://i.pravatar.cc/300?img=1", - }, - ]; - } - return ( - <> -
- -
- - ); + let initialUserData = await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + role: true, + image: true, + }, + take: 10, + }); + const totalNumberOfUsers = await prisma.user.count(); + if (initialUserData === null) { + initialUserData = [ + { + id: "1", + name: "Test name", + email: "testEmail@gmail.com", + role: "PARTICIPANT", + image: "https://i.pravatar.cc/300?img=1", + }, + ]; + } + return ( + <> +
+ +
+ + ); } diff --git a/src/components/admin/user-list.tsx b/src/components/admin/user-list.tsx index 75232bd..3ad74a2 100644 --- a/src/components/admin/user-list.tsx +++ b/src/components/admin/user-list.tsx @@ -1,14 +1,14 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import axios from "axios"; -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"; -import { ChevronDownIcon, SearchIcon } from "lucide-react"; -import { Input } from "../ui/input"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { ChevronDown, Search } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; import debounce from "lodash.debounce"; import ChangeRole from "./change-role"; @@ -23,14 +23,17 @@ export interface User { interface UsersListProps { initialUsers: User[]; initialPage: number; + totalNumberOfUsers: number; } + export const dynamic = "force-dynamic"; -const UsersList: React.FC = ({ initialUsers, initialPage }) => { + +const UsersList: React.FC = ({ initialUsers, initialPage, totalNumberOfUsers }) => { const [userList, setUserList] = useState(initialUsers); const [currentPage, setCurrentPage] = useState(initialPage); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); - const [searchQuery, setSearchQuery] = useState(""); // Search query state + const [searchQuery, setSearchQuery] = useState(""); const loader = useRef(null); const fetchUsers = async (page: number, query: string) => { @@ -39,7 +42,9 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => { try { const response = await axios.get(`/api/users?page=${page}&search=${encodeURIComponent(query)}`); if (response.data.users.length > 0) { - setUserList((prevUsers) => [...prevUsers, ...response.data.users]); + setUserList((prevUsers) => + page === 1 ? response.data.users : [...prevUsers, ...response.data.users] + ); setCurrentPage(page); } else { setHasMore(false); @@ -49,6 +54,7 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => { } setLoading(false); }; + const loadMoreUsers = useCallback(() => { if (hasMore) { fetchUsers(currentPage + 1, searchQuery); @@ -56,109 +62,105 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPage, hasMore, searchQuery]); + // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedFetchUsers = useCallback( - debounce(async (query: string) => { - setCurrentPage(1); // Reset page number - setHasMore(true); // Reset hasMore - try { - const response = await axios.get(`/api/users?page=1&search=${encodeURIComponent(query)}`); - setUserList(response.data.users); - } catch (error) { - console.error("Error fetching users:", error); - } - }, 500), + debounce((query: string) => { + setCurrentPage(1); + setHasMore(true); + fetchUsers(1, query); + }, 300), [] ); + const handleSearchChange = (e: React.ChangeEvent) => { const query = e.target.value; setSearchQuery(query); - debouncedFetchUsers(query); // Use debounced fetch function + debouncedFetchUsers(query); }; - // Observe scroll and load more users when scrolled to the bottom useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + loadMoreUsers(); + } + }, + { threshold: 1.0 } + ); + if (loader.current) { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore) { - loadMoreUsers(); - } - }, - { threshold: 1.0 } - ); observer.observe(loader.current); - return () => observer.disconnect(); } - }, [loader.current, hasMore, loadMoreUsers]); + + return () => observer.disconnect(); + }, [hasMore, loadMoreUsers]); return ( - <> -
-
- -
+
+
+ + + Users + Manage user roles and permissions. + + +
+ + Total Users: {totalNumberOfUsers} +
- +
- - Users - Manage user roles and permissions. - - -
- {userList.map((user) => ( -
-
- - - - {user.name ? user.name[0] : "N/A"} - - -
-

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

-

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

-
+
+ {userList.map((user) => ( +
+
+ + + {user.name ? user.name[0] : "U"} + +
+

{user.name || "Unknown"}

+

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

- - - - - - - -
- ))} -
- {hasMore && ( -
- {loading ? "Loading..." : "Load more"} + + + + + + + +
- )} - - -
-
- + ))} +
+ {hasMore && ( +
+ {loading ? "Loading..." : "Load more"} +
+ )} + +
+
+
); }; diff --git a/src/components/common/registration-form.tsx b/src/components/common/registration-form.tsx index 6ee30b4..4ce64c7 100644 --- a/src/components/common/registration-form.tsx +++ b/src/components/common/registration-form.tsx @@ -6,30 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { getPrice } from "@/app/actions/get-price"; import { toast } from "sonner"; @@ -44,401 +31,389 @@ import { PaymentLoading } from "../payment/payment-loading"; import { PaymentSuccessfulComponent } from "../payment/payment-successful"; declare global { - interface Window { - Razorpay: any; - } + interface Window { + Razorpay: any; + } } interface ResponseInterface { - orderId?: string; - status: number; - error?: string; + orderId?: string; + status: number; + error?: string; } interface RazorpayResponse { - razorpay_order_id: string; - razorpay_payment_id: string; - razorpay_signature: string; + razorpay_order_id: string; + razorpay_payment_id: string; + razorpay_signature: string; } -export const baseSchema = z.object({ - designation: z.enum(["student", "faculty", "employee"]), - name: z.string().min(2, { message: "Name must be at least 2 characters." }), - email: z.string().email({ message: "Invalid email address." }), - phone: z - .string() - .regex(/^\d{10}$/, { message: "Phone number must be 10 digits." }), - photo: z.string(), - couponCode: z.string().optional(), +const baseSchema = z.object({ + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email"), + phone: z.string().min(10, "Phone number must be at least 10 digits"), + photo: z.string().min(1, "Photo is required"), + designation: z.enum(["student", "faculty", "employee"]), }); +const studentSchema = baseSchema.extend({ + usn: z.string().min(1, "USN is required"), + idCard: z.string().min(1, "ID Card is required"), +}); + +const facultyOrEmployeeSchema = baseSchema; + export interface FormDataInterface { - designation: "student" | "faculty" | "employee"; - name: string; - email: string; - phone: string; - photo: string; - idCard?: string; - usn?: string; - amount: string; - couponCode?: string; + designation: "student" | "faculty" | "employee"; + name: string; + email: string; + phone: string; + photo: string; + idCard?: string; + usn?: string; + amount: string; + couponCode?: string; } -export const studentSchema = baseSchema.extend({ - usn: z.string().min(1, { message: "USN is required for students." }), - idCard: z.string(), -}); - export default function RegistrationForm() { - const [step, setStep] = useState(1); - const [pricing, setPricing] = useState({ - basePrice: basePrice, - discountAmount: initialdiscount, - finalPrice: basePrice, - }); - const [isProcessing, setIsProcessing] = useState(false); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); + const [step, setStep] = useState(1); + const [pricing, setPricing] = useState({ + basePrice: basePrice, + discountAmount: initialdiscount, + finalPrice: basePrice, + }); + const [isProcessing, setIsProcessing] = useState(false); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); - const { data: session } = useSession(); + const { data: session } = useSession(); - const form = useForm< - z.infer - >({ - resolver: zodResolver(baseSchema), - defaultValues: { - designation: "student", - name: "", - email: "", - phone: "", - couponCode: "", - }, - }); + const form = useForm< + z.infer & { + couponCode?: string; + } + >({ + resolver: zodResolver(facultyOrEmployeeSchema), + defaultValues: { + name: "", + email: "", + phone: "", + photo: "", + idCard: "", + usn: "", + designation: "student", + couponCode: "", + }, + }); - // const handleSubmitForm = async () => { - // const data = form.getValues(); - // const resp = await submitForm(data as FormDataInterface); - // }; + // const handleSubmitForm = async () => { + // const data = form.getValues(); + // const resp = await submitForm(data as FormDataInterface); + // }; - const handlePayment = async () => { - setIsProcessing(true); - const couponCode = form.getValues("couponCode"); - try { - const response = await fetch("/api/create-order", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amount: pricing.finalPrice }), - }); - const data: Promise = response.json(); + const handlePayment = async () => { + setIsProcessing(true); + const couponCode = form.getValues("couponCode"); + try { + const response = await fetch("/api/create-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ amount: pricing.finalPrice }), + }); + const data: Promise = response.json(); - const options = { - key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, - amount: pricing.finalPrice * 100, - currency: "INR", - name: "Test Name", - description: "Test Transaction", - order_id: (await data).orderId, - handler: async (response: RazorpayResponse) => { - const resp = await fetch("/api/verify-order", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - orderId: response.razorpay_order_id, - razorpayPaymentId: response.razorpay_payment_id, - razorpaySignature: response.razorpay_signature, - amount: pricing.finalPrice, - }), - }); + const options = { + key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, + amount: pricing.finalPrice * 100, + currency: "INR", + name: "St Joseph Engineering College", + description: "Test Transaction", + order_id: (await data).orderId, + handler: async (response: RazorpayResponse) => { + const resp = await fetch("/api/verify-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + orderId: response.razorpay_order_id, + razorpayPaymentId: response.razorpay_payment_id, + razorpaySignature: response.razorpay_signature, + amount: pricing.finalPrice, + }), + }); - const data = await resp.json(); - console.log(data); - if (data.isOk) { - await invalidateCouponCode(couponCode ?? "", session!); - const formResponse = form.getValues(); - await submitForm( - formResponse as FormDataInterface, - pricing.finalPrice - ); - setIsProcessing(false); - setSuccess(true); - } else { - setIsProcessing(false); - alert("Payment failed"); - } - }, - // change to dynamic - notes: { - customerName: form.getValues("name"), - customerEmail: session?.user.email, - customerContact: form.getValues("phone"), - }, - prefill: { - name: form.getValues("name"), - email: session?.user.email, - constact: form.getValues("phone"), - }, - theme: { - color: "#3399cc", - }, - }; - const rzp1 = new window.Razorpay(options); - rzp1.open(); - } catch (error) { - toast.error(`Some error ${error}`); - } - }; + const data = await resp.json(); + console.log(data); + if (data.isOk) { + await invalidateCouponCode(couponCode ?? "", session!); + const formResponse = form.getValues(); + await submitForm(formResponse as FormDataInterface, pricing.finalPrice); + setIsProcessing(false); + setSuccess(true); + } else { + setIsProcessing(false); + alert("Payment failed"); + } + }, + // change to dynamic + notes: { + customerName: form.getValues("name"), + customerEmail: session?.user.email, + customerContact: form.getValues("phone"), + }, + prefill: { + name: form.getValues("name"), + email: session?.user.email, + constact: form.getValues("phone"), + }, + theme: { + color: "#3399cc", + }, + }; + const rzp1 = new window.Razorpay(options); + rzp1.open(); + } catch (error) { + toast.error(`Some error ${error}`); + } + }; - const onSubmit = async ( - values: z.infer< - typeof studentSchema | typeof baseSchema | typeof baseSchema - > - ) => { - await handlePayment(); - // Handle form submission here - }; + const onSubmit = async (values: z.infer) => { + await handlePayment(); + // Handle form submission here + }; - const verifyCoupon = async () => { - const couponCode = form.getValues("couponCode"); - try { - const { basePrice, discountAmount, finalPrice } = await getPrice( - couponCode - ); - setPricing({ basePrice, discountAmount, finalPrice }); - toast.success("Coupon applied successfully"); - } catch (e) { - console.error(e); - const message = getErrorMessage(e); - toast.error(`${message}`); - } - }; + const verifyCoupon = async () => { + const couponCode = form.getValues("couponCode"); + try { + const { basePrice, discountAmount, finalPrice } = await getPrice(couponCode); + setPricing({ basePrice, discountAmount, finalPrice }); + toast.success("Coupon applied successfully"); + } catch (e) { + console.error(e); + const message = getErrorMessage(e); + toast.error(`${message}`); + } + }; - const handleNext = async () => { - let isValid = false; - if (step === 1) { - isValid = await form.trigger(["designation"]); - } else if (step === 2) { - const designation = form.getValues("designation"); - if (designation === "student") { - isValid = await form.trigger([ - "name", - "email", - "phone", - "usn", - "idCard", - "photo", - ]); - } else if (designation === "faculty" || designation === "employee") { - isValid = await form.trigger(["name", "email", "phone", "photo"]); - } - } + const handleNext = async () => { + let isValid = false; + + if (step === 1) { + isValid = await form.trigger(["designation"]); + } else if (step === 2) { + const designation = form.getValues("designation"); + + // Trigger different validations based on the designation + if (designation === "student") { + isValid = await form.trigger(["name", "email", "phone", "usn", "idCard", "photo"]); + } else if (designation === "faculty" || designation === "employee") { + isValid = await form.trigger(["name", "email", "phone", "photo"]); + } + } - if (isValid) { - setStep(step + 1); + if (isValid) { + setStep(step + 1); + } + }; + if (isProcessing) { + return ( +
+ +
+ ); + } + if (success) { + return ( +
+ +
+ ); } - }; - if (isProcessing) { - return ( -
- -
- ); - } - if (success) { - return ( -
- -
- ); - } - return ( - -