From af2dfa6e7e514f14bf8d12314f3d505142e38746 Mon Sep 17 00:00:00 2001 From: Khoironi Kurnia Syah Date: Sat, 31 Aug 2024 21:08:26 +0800 Subject: [PATCH 1/4] feat: adjust admin dashboard --- next.config.mjs | 8 + .../(private)/dashboard/(admin)/layout.tsx | 29 +++ .../dashboard/(admin)/users/page.tsx | 148 ++++++------- .../{role-form.tsx => role-form-dialog.tsx} | 6 +- .../dashboard/(admin)/users/user-form.tsx | 0 src/app/(private)/dashboard/page.tsx | 8 +- src/app/(private)/layout.tsx | 5 +- src/components/common/container.tsx | 3 + src/components/private/header.tsx | 197 ++++++++++-------- src/components/private/logout-loading.tsx | 17 ++ src/components/public/header.tsx | 12 +- src/components/ui/badge.tsx | 2 +- src/components/ui/button.tsx | 2 +- src/components/ui/input.tsx | 2 +- src/components/ui/select.tsx | 2 +- src/components/ui/textarea.tsx | 2 +- src/lib/actions/roles.action.ts | 25 +++ src/lib/actions/users.action.ts | 16 +- src/lib/auth/index.ts | 3 +- src/lib/auth/session.ts | 5 +- 20 files changed, 306 insertions(+), 186 deletions(-) create mode 100644 src/app/(private)/dashboard/(admin)/layout.tsx rename src/app/(private)/dashboard/(admin)/users/{role-form.tsx => role-form-dialog.tsx} (99%) delete mode 100644 src/app/(private)/dashboard/(admin)/users/user-form.tsx create mode 100644 src/components/common/container.tsx create mode 100644 src/components/private/logout-loading.tsx diff --git a/next.config.mjs b/next.config.mjs index 10aec74..3b3ee17 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,6 +3,14 @@ const nextConfig = { compiler: { removeConsole: process.env.NODE_ENV === 'production', }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.googleusercontent.com', + }, + ], + }, }; export default nextConfig; diff --git a/src/app/(private)/dashboard/(admin)/layout.tsx b/src/app/(private)/dashboard/(admin)/layout.tsx new file mode 100644 index 0000000..9dd4836 --- /dev/null +++ b/src/app/(private)/dashboard/(admin)/layout.tsx @@ -0,0 +1,29 @@ +import { Metadata } from 'next'; +import { redirect } from 'next/navigation'; + +import { checkAdmin } from '@/lib/auth'; + +export const metadata: Metadata = { + title: 'Dashboard | Sainseni Community', + authors: [ + { + name: 'Khoironi Kurnia Syah', + url: 'https://zekhoi.dev', + }, + ], + description: 'Community for community', +}; + +export default async function PrivateLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const { isAdmin } = await checkAdmin(); + + if (!isAdmin) { + return redirect('/dashboard'); + } + + return <>{children}; +} diff --git a/src/app/(private)/dashboard/(admin)/users/page.tsx b/src/app/(private)/dashboard/(admin)/users/page.tsx index 6c06e65..be14ff2 100644 --- a/src/app/(private)/dashboard/(admin)/users/page.tsx +++ b/src/app/(private)/dashboard/(admin)/users/page.tsx @@ -7,7 +7,7 @@ import { useGetRoles } from '@/lib/queries'; import { useGetUsers } from '@/lib/queries/users.query'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Select, @@ -26,7 +26,7 @@ import { } from '@/components/ui/table'; import { useToast } from '@/components/ui/use-toast'; -import RoleFormDialog from './role-form'; +import RoleFormDialog from './role-form-dialog'; export default function UserManagement() { const { toast } = useToast(); @@ -92,92 +92,76 @@ export default function UserManagement() { } return ( -
-
+ +

- User Management + Users

-
- -
- - - User List - - - - - - - - Name - - - Email - - Role + + + +
+ + + + Name + + + Email + + Role + + + + {userData && + userData.map((user) => ( + + + {user.name} + + + {user.email} + + + + - - - {userData && - userData.map((user) => ( - - - {user.name} - - - {user.email} - - - - - - ))} - -
-
-
-
-
-
+ ))} + + + + + ); } diff --git a/src/app/(private)/dashboard/(admin)/users/role-form.tsx b/src/app/(private)/dashboard/(admin)/users/role-form-dialog.tsx similarity index 99% rename from src/app/(private)/dashboard/(admin)/users/role-form.tsx rename to src/app/(private)/dashboard/(admin)/users/role-form-dialog.tsx index 5d933e0..4ca1ae7 100644 --- a/src/app/(private)/dashboard/(admin)/users/role-form.tsx +++ b/src/app/(private)/dashboard/(admin)/users/role-form-dialog.tsx @@ -222,7 +222,7 @@ export default function RoleFormDialog() { role.id, ) } - className='w-1/2 focus-visible:ring-0' + className='w-1/2' /> ) : ( {role.name} @@ -259,7 +259,7 @@ export default function RoleFormDialog() { handleCancelEdit } > - + Cancel @@ -300,7 +300,7 @@ export default function RoleFormDialog() { role.id ? ( ) : ( - + )} Delete{' '} diff --git a/src/app/(private)/dashboard/(admin)/users/user-form.tsx b/src/app/(private)/dashboard/(admin)/users/user-form.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/(private)/dashboard/page.tsx b/src/app/(private)/dashboard/page.tsx index 269aff0..125aa1c 100644 --- a/src/app/(private)/dashboard/page.tsx +++ b/src/app/(private)/dashboard/page.tsx @@ -28,8 +28,8 @@ import { export default function DashboardPage() { return ( -
-
+ <> +
@@ -374,7 +374,7 @@ export default function DashboardPage() {
-
-
+ + ); } diff --git a/src/app/(private)/layout.tsx b/src/app/(private)/layout.tsx index 46685cb..95b3857 100644 --- a/src/app/(private)/layout.tsx +++ b/src/app/(private)/layout.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { checkAdmin, validateRequest } from '@/lib/auth'; +import Container from '@/components/common/container'; import PrivateHeader from '@/components/private/header'; export const metadata: Metadata = { @@ -27,12 +28,12 @@ export default async function PrivateLayout({ return redirect('/auth/signin'); } - const isAdmin = await checkAdmin(); + const { isAdmin } = await checkAdmin(); return (
- {children} + {children}
); } diff --git a/src/components/common/container.tsx b/src/components/common/container.tsx new file mode 100644 index 0000000..0b5dc27 --- /dev/null +++ b/src/components/common/container.tsx @@ -0,0 +1,3 @@ +export default function Container({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/src/components/private/header.tsx b/src/components/private/header.tsx index 78d60cc..b31474c 100644 --- a/src/components/private/header.tsx +++ b/src/components/private/header.tsx @@ -1,12 +1,15 @@ 'use client'; import { CircleUser, Menu, Sparkles } from 'lucide-react'; +import Image from 'next/image'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { useState } from 'react'; import { signOut } from '@/lib/actions/auth.action'; import { DatabaseUserAttributes } from '@/lib/auth'; +import LogoutLoadingScreen from '@/components/private/logout-loading'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -26,14 +29,16 @@ export default function PrivateHeader({ isAdmin = false, userData, }: PrivateHeaderProps) { + const [isLoggingOut, setIsLoggingOut] = useState(false); const navbarMenu = [ { name: 'Dashboard', url: '/dashboard', isAdmin: false }, - { name: 'Projects', url: '/dashboard/projects', isAdmin: false }, - { name: 'Friends', url: '/dashboard/friends', isAdmin: false }, - { name: 'References', url: '/dashboard/references', isAdmin: false }, - { name: 'Events', url: '/dashboard/events', isAdmin: false }, + { name: 'Statistics', url: '/dashboard/statistics', isAdmin: true }, + // { name: 'Projects', url: '/dashboard/projects', isAdmin: false }, + { name: 'Friends', url: '/dashboard/friends', isAdmin: true }, + { name: 'References', url: '/dashboard/references', isAdmin: true }, + // { name: 'Events', url: '/dashboard/events', isAdmin: true }, { name: 'Users', url: '/dashboard/users', isAdmin: true }, - { name: 'Keys', url: '/dashboard/keys', isAdmin: true }, + { name: 'API Keys', url: '/dashboard/keys', isAdmin: true }, ]; const pathname = usePathname(); @@ -45,57 +50,30 @@ export default function PrivateHeader({ return pathname.startsWith(path); }; + const handleSignOut = async () => { + setIsLoggingOut(true); + try { + await signOut(); + } catch { + setIsLoggingOut(false); + } + }; + return ( -
- - - - - - - - - - - - - - - {userData.name} - - Settings - - await signOut()} - > - Sign Out - - - -
+ + + + + + + + + + + + + + + {userData.name} + + + Profile + + + + Sign Out + + + + + {isLoggingOut && } + ); } diff --git a/src/components/private/logout-loading.tsx b/src/components/private/logout-loading.tsx new file mode 100644 index 0000000..338bd9f --- /dev/null +++ b/src/components/private/logout-loading.tsx @@ -0,0 +1,17 @@ +import { Loader2 } from 'lucide-react'; + +export default function LogoutLoadingScreen() { + return ( +
+
+ +

+ Logging out... +

+

+ Please wait while we securely log you out. +

+
+
+ ); +} diff --git a/src/components/public/header.tsx b/src/components/public/header.tsx index acbed4b..65a7664 100644 --- a/src/components/public/header.tsx +++ b/src/components/public/header.tsx @@ -35,7 +35,7 @@ export default function Navbar({ isSigned = false }: NavbarProps) { return (
-
+
{isSigned ? ( <> - + + ) : ( <> diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index ea6cf21..6b62400 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-0 focus:ring-ring focus:ring-offset-0', { variants: { variant: { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 7e65943..fe6726e 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 98a5d8f..b6c260a 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -13,7 +13,7 @@ const Input = React.forwardRef( span]:line-clamp-1', + 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-0 focus:ring-ring focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', className, )} {...props} diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 84ecdc1..ca454f1 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -12,7 +12,7 @@ const Textarea = React.forwardRef( return (