Skip to content

Commit

Permalink
Merge pull request #19 from TEDx-SJEC/admin
Browse files Browse the repository at this point in the history
Admin
  • Loading branch information
joywin2003 authored Oct 1, 2024
2 parents 032b704 + ba965c2 commit 9e04593
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 48 deletions.
11 changes: 11 additions & 0 deletions src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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";

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

Expand All @@ -12,6 +13,16 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const { data: session } = useSession({
required: true,
});
if (!session) {
return <div>Unauthorized</div>;
}

if (session.user.role !== "ADMIN") {
return <div>Forbidden</div>;
}
return (
<html lang="en">
<body className={inter.className}>
Expand Down
12 changes: 12 additions & 0 deletions src/app/admin/payment/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SearchableInfiniteScrollTable } from "@/components/searchable-infinite-scroll-table";
import React from "react";

export default async function Payments() {
return (
<>
<div className="pt-20 flex min-h-screen w-full flex-col bg-background">
<SearchableInfiniteScrollTable />
</div>
</>
);
}
57 changes: 29 additions & 28 deletions src/app/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import UsersList from "@/components/Admin/user-list";
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: "[email protected]",
role: "PARTICIPANT",
image: "https://i.pravatar.cc/300?img=1",
},
];
}
return (
<>
<div className="pt-20 flex min-h-screen w-full flex-col bg-background">
<UsersList initialUsers={initialUserData} initialPage={1} />
</div>
</>
);
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: "[email protected]",
role: "PARTICIPANT",
image: "https://i.pravatar.cc/300?img=1",
},
];
}
return (
<>
<div className="pt-20 flex min-h-screen w-full flex-col bg-background">
<UsersList initialUsers={initialUserData} initialPage={1} />
</div>
</>
);
}
35 changes: 17 additions & 18 deletions src/app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import { getServerSideSession } from "@/lib/get-server-session";
import { getServerSession } from "next-auth";
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";

const f = createUploadthing();

const auth = async (req: Request) => {
const session = await getServerSideSession();
if (session && session.user) {
return session.user;
} else return null;
const session = await getServerSession();

return session?.user;
};

export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: "4MB" } })
.middleware(async ({ req }) => {
console.log("Middleware for imageUploader", req.url);
const user = await auth(req);
imageUploader: f({ image: { maxFileSize: "4MB" } })
.middleware(async ({ req }) => {
console.log("Middleware for imageUploader", req.url);
const user = await auth(req);

if (!user) throw new UploadThingError("Unauthorized");
if (!user) throw new UploadThingError("Unauthorized");

return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
// console.log("Upload complete for userId:", metadata.userId);
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
// console.log("Upload complete for userId:", metadata.userId);

// console.log("file url", file.url);
// console.log("file url", file.url);

// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
return { uploadedBy: metadata.userId };
}),
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
return { uploadedBy: metadata.userId };
}),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
13 changes: 11 additions & 2 deletions src/components/Admin/Navbar/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import React, { Dispatch, SetStateAction, useState } from "react";
import { IconType } from "react-icons";
import { FiChevronDown, FiChevronsRight, FiUser } from "react-icons/fi";
import { RiCoupon3Line } from "react-icons/ri";
import { MdPayment } from "react-icons/md";
import { motion } from "framer-motion";
import Link from "next/link";

export const AdminNavbar = () => {
return (
<div className="flex bg-indigo-50 h-screen">
<div className="flex bg-indigo-50 h-screen ">
<Sidebar />
<NavbarContent />
</div>
Expand All @@ -22,7 +23,7 @@ const Sidebar = () => {
return (
<motion.nav
layout
className="sticky top-0 bottom-0 h-screen shrink-0 border-r border-slate-300 bg-white p-2"
className=" sticky top-0 h-screen shrink-0 border-r border-slate-300 bg-white p-2"
style={{
width: open ? "225px" : "fit-content",
}}
Expand All @@ -46,6 +47,14 @@ const Sidebar = () => {
open={open}
href="/admin/users"
/>
<Option
Icon={MdPayment}
title="Payments"
selected={selected}
setSelected={setSelected}
open={open}
href="/admin/payment"
/>
</div>

<ToggleClose open={open} setOpen={setOpen} />
Expand Down
132 changes: 132 additions & 0 deletions src/components/searchable-infinite-scroll-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { Loader2, Search } from "lucide-react";

interface TableData {
name: string;
email: string;
usn: string;
paymentId: string;
contactNumber: string;
amount: number;
}

function generateMockData(count: number): TableData[] {
return Array.from({ length: count }, (_, i) => ({
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
usn: `USN${(1000 + i).toString().padStart(4, "0")}`,
paymentId: `PAY${(10000 + i).toString().padStart(5, "0")}`,
contactNumber: `+1 ${Math.floor(1000000000 + Math.random() * 9000000000)}`,
amount: Math.floor(10 + Math.random() * 990),
}));
}

export function SearchableInfiniteScrollTable() {
const [data, setData] = useState<TableData[]>([]);
const [filteredData, setFilteredData] = useState<TableData[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const loaderRef = useRef(null);

const loadMoreData = () => {
setIsLoading(true);
setTimeout(() => {
const newData = generateMockData(20);
setData((prevData) => [...prevData, ...newData]);
setPage((prevPage) => prevPage + 1);
setIsLoading(false);
}, 1000); // Simulating API delay
};

useEffect(() => {
loadMoreData();
}, []);

useEffect(() => {
const filtered = data.filter((item) =>
Object.values(item).some((value) =>
value.toString().toLowerCase().includes(searchTerm.toLowerCase())
)
);
setFilteredData(filtered);
}, [data, searchTerm]);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading && searchTerm === "") {
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();
};
}, [isLoading, searchTerm]);

const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
};

return (
<div className="container mx-auto py-10">
<div className="mb-4 relative">
<Input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearch}
className="pl-10"
/>
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>USN</TableHead>
<TableHead>Payment ID</TableHead>
<TableHead>Contact Number</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.map((item, index) => (
<TableRow key={index}>
<TableCell>{item.name}</TableCell>
<TableCell>{item.email}</TableCell>
<TableCell>{item.usn}</TableCell>
<TableCell>{item.paymentId}</TableCell>
<TableCell>{item.contactNumber}</TableCell>
<TableCell>${item.amount.toFixed(2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{searchTerm === "" && (
<div ref={loaderRef} className="flex justify-center py-4">
{isLoading && <Loader2 className="h-6 w-6 animate-spin" />}
</div>
)}
</div>
);
}
Loading

0 comments on commit 9e04593

Please sign in to comment.