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

Payment #41

Closed
wants to merge 11 commits into from
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 9 additions & 7 deletions src/app/admin/payment/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="pt-20 flex min-h-screen w-full flex-col bg-background">
<SearchableInfiniteScrollTable />
</div>
</>
);
const totalPayments = await prisma.payment.count();
return (
<>
<div className="pt-20 flex min-h-screen w-full flex-col bg-background">
<SearchableInfiniteScrollTable totalPayments={totalPayments} />
</div>
</>
);
}
61 changes: 33 additions & 28 deletions src/app/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[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,
});
const totalNumberOfUsers = await prisma.user.count();
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}
totalNumberOfUsers={totalNumberOfUsers}
/>
</div>
</>
);
}
176 changes: 89 additions & 87 deletions src/components/admin/user-list.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -23,14 +23,17 @@ export interface User {
interface UsersListProps {
initialUsers: User[];
initialPage: number;
totalNumberOfUsers: number;
}

export const dynamic = "force-dynamic";
const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => {

const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage, totalNumberOfUsers }) => {
const [userList, setUserList] = useState<User[]>(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<HTMLDivElement | null>(null);

const fetchUsers = async (page: number, query: string) => {
Expand All @@ -39,7 +42,9 @@ const UsersList: React.FC<UsersListProps> = ({ 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);
Expand All @@ -49,116 +54,113 @@ const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => {
}
setLoading(false);
};

const loadMoreUsers = useCallback(() => {
if (hasMore) {
fetchUsers(currentPage + 1, searchQuery);
}
// 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<HTMLInputElement>) => {
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 (
<>
<div className="flex flex-1 overflow-hidden">
<main className="container grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 mt-5">
<Card className="w-full">
<div className="flex justify-end gap-2 mt-5 p-5">
<div className="flex flex-1 overflow-hidden">
<main className="container grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 mt-5">
<Card className="w-full">
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage user roles and permissions.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-6">
<Badge variant="secondary" className="text-sm">
Total Users: {totalNumberOfUsers}
</Badge>
<div className="relative">
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search users..."
className="pl-8 sm:w-[200px] md:w-[300px]"
value={searchQuery}
onChange={handleSearchChange} // Handle search input change
onChange={handleSearchChange}
aria-label="Search users"
/>
</div>
</div>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage user roles and permissions.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
{userList.map((user) => (
<div
key={user.id}
className="grid grid-cols-[1fr_auto] items-center gap-4"
>
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage src={user.image || "/placeholder-user.jpg"} />
<AvatarFallback>
{user.name ? user.name[0] : "N/A"}
</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium">
{user.name || "Unknown"}
</p>
<p className="text-sm text-muted-foreground">
{user.email || "No email"}
</p>
</div>
<div className="grid gap-4">
{userList.map((user) => (
<div key={user.id} className="grid grid-cols-[1fr_auto] items-center gap-4">
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage
src={user.image || "/placeholder-user.jpg"}
alt={user.name || "User avatar"}
/>
<AvatarFallback>{user.name ? user.name[0] : "U"}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium">{user.name || "Unknown"}</p>
<p className="text-sm text-muted-foreground">
{user.email || "No email"}
</p>
</div>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
{user.role}{" "}
<ChevronDownIcon className="w-4 h-4 ml-2 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<ChangeRole userId={user.id} userRole={user.role} />
</PopoverContent>
</Popover>
</div>
))}
</div>
{hasMore && (
<div ref={loader} className="text-center">
{loading ? "Loading..." : "Load more"}
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
{user.role}{" "}
<ChevronDown className="w-4 h-4 ml-2 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<ChangeRole userId={user.id} userRole={user.role} />
</PopoverContent>
</Popover>
</div>
)}
</CardContent>
</Card>
</main>
</div>
</>
))}
</div>
{hasMore && (
<div ref={loader} className="text-center mt-4">
{loading ? "Loading..." : "Load more"}
</div>
)}
</CardContent>
</Card>
</main>
</div>
);
};

Expand Down
Loading
Loading