-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from TEDx-SJEC/admin
Admin
- Loading branch information
Showing
9 changed files
with
817 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
"use server"; | ||
|
||
import prisma from "@/server/db"; | ||
import { revalidatePath } from "next/cache"; | ||
|
||
async function updateUserRole(id: string, role: string) { | ||
try { | ||
const updatedUser = await prisma.user.update({ | ||
where: { id }, | ||
data: { role }, | ||
}); | ||
revalidatePath("/admin/users"); | ||
return updatedUser; | ||
} catch (error) { | ||
console.error("Error updating user role:", error); | ||
return null; | ||
} | ||
} | ||
export const makeAdmin = async (userId: string) => { | ||
return await updateUserRole(userId, "ADMIN"); | ||
}; | ||
|
||
export const makeParticipant = async (userId: string) => { | ||
return await updateUserRole(userId, "PARTICIPANT"); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import UsersList from "@/components/Admin/user-list"; | ||
import prisma from "@/server/db"; | ||
|
||
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> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import prisma from "@/server/db"; | ||
|
||
import { NextResponse } from "next/server"; | ||
export async function GET(req: Request) { | ||
const { searchParams } = new URL(req.url); | ||
const page = parseInt(searchParams.get("page") || "1"); | ||
const search = searchParams.get("search") || ""; | ||
const limit = 10; | ||
|
||
const users = await prisma.user.findMany({ | ||
skip: (page - 1) * limit, | ||
take: limit, | ||
where: { | ||
name: { | ||
contains: search, | ||
}, | ||
}, | ||
}); | ||
return NextResponse.json({ users }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
"use client"; | ||
import { makeAdmin, makeParticipant } from "@/app/actions/change-role"; | ||
import { Button } from "@/components/ui/button"; | ||
|
||
export default function ChangeRole({ userId, userRole }: { userId: string; userRole: string }) { | ||
async function handleMakeAdmin() { | ||
await makeAdmin(userId); | ||
} | ||
|
||
async function handleMakeParticipant() { | ||
await makeParticipant(userId); | ||
} | ||
|
||
return ( | ||
<div className=""> | ||
<Button | ||
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded dark:bg-black ${ | ||
userRole === "ADMIN" ? "hidden" : "" | ||
}`} | ||
onClick={handleMakeAdmin} | ||
> | ||
Make Admin | ||
</Button> | ||
<Button | ||
className={`bg-foreground hover:bg-muted-foreground font-bold py-2 px-4 rounded dark:bg-black ${ | ||
userRole === "PARTICIPANT" ? "hidden" : "" | ||
}`} | ||
onClick={handleMakeParticipant} | ||
> | ||
Make Participant | ||
</Button> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/* eslint-disable react-hooks/exhaustive-deps */ | ||
"use client"; | ||
|
||
import { 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 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; | ||
} | ||
|
||
interface UsersListProps { | ||
initialUsers: User[]; | ||
initialPage: number; | ||
} | ||
export const dynamic = "force-dynamic"; | ||
const UsersList: React.FC<UsersListProps> = ({ initialUsers, initialPage }) => { | ||
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 loader = useRef<HTMLDivElement | null>(null); | ||
|
||
const fetchUsers = async (page: number, query: string) => { | ||
if (loading) return; | ||
setLoading(true); | ||
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]); | ||
setCurrentPage(page); | ||
} else { | ||
setHasMore(false); | ||
} | ||
} catch (error) { | ||
console.error("Error fetching users:", error); | ||
} | ||
setLoading(false); | ||
}; | ||
const loadMoreUsers = useCallback(() => { | ||
if (hasMore) { | ||
fetchUsers(currentPage + 1, searchQuery); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [currentPage, hasMore, searchQuery]); | ||
|
||
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), | ||
[] | ||
); | ||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const query = e.target.value; | ||
setSearchQuery(query); | ||
debouncedFetchUsers(query); // Use debounced fetch function | ||
}; | ||
|
||
// Observe scroll and load more users when scrolled to the bottom | ||
useEffect(() => { | ||
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 ( | ||
<> | ||
<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="relative"> | ||
<SearchIcon 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 | ||
/> | ||
</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> | ||
<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"} | ||
</div> | ||
)} | ||
</CardContent> | ||
</Card> | ||
</main> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
export default UsersList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
"use client" | ||
|
||
import * as React from "react" | ||
import * as AvatarPrimitive from "@radix-ui/react-avatar" | ||
|
||
import { cn } from "@/lib/utils" | ||
|
||
const Avatar = React.forwardRef< | ||
React.ElementRef<typeof AvatarPrimitive.Root>, | ||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> | ||
>(({ className, ...props }, ref) => ( | ||
<AvatarPrimitive.Root | ||
ref={ref} | ||
className={cn( | ||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
)) | ||
Avatar.displayName = AvatarPrimitive.Root.displayName | ||
|
||
const AvatarImage = React.forwardRef< | ||
React.ElementRef<typeof AvatarPrimitive.Image>, | ||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> | ||
>(({ className, ...props }, ref) => ( | ||
<AvatarPrimitive.Image | ||
ref={ref} | ||
className={cn("aspect-square h-full w-full", className)} | ||
{...props} | ||
/> | ||
)) | ||
AvatarImage.displayName = AvatarPrimitive.Image.displayName | ||
|
||
const AvatarFallback = React.forwardRef< | ||
React.ElementRef<typeof AvatarPrimitive.Fallback>, | ||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> | ||
>(({ className, ...props }, ref) => ( | ||
<AvatarPrimitive.Fallback | ||
ref={ref} | ||
className={cn( | ||
"flex h-full w-full items-center justify-center rounded-full bg-muted", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
)) | ||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName | ||
|
||
export { Avatar, AvatarImage, AvatarFallback } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"use client" | ||
|
||
import * as React from "react" | ||
import * as PopoverPrimitive from "@radix-ui/react-popover" | ||
|
||
import { cn } from "@/lib/utils" | ||
|
||
const Popover = PopoverPrimitive.Root | ||
|
||
const PopoverTrigger = PopoverPrimitive.Trigger | ||
|
||
const PopoverAnchor = PopoverPrimitive.Anchor | ||
|
||
const PopoverContent = React.forwardRef< | ||
React.ElementRef<typeof PopoverPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> | ||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( | ||
<PopoverPrimitive.Portal> | ||
<PopoverPrimitive.Content | ||
ref={ref} | ||
align={align} | ||
sideOffset={sideOffset} | ||
className={cn( | ||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
</PopoverPrimitive.Portal> | ||
)) | ||
PopoverContent.displayName = PopoverPrimitive.Content.displayName | ||
|
||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } |