-
-
Notifications
You must be signed in to change notification settings - Fork 3
Enhance admin dashboard with settings and analytics features #20
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
base: master
Are you sure you want to change the base?
Changes from all commits
374dbe5
e6f3b06
598e95f
9c20b0e
2c01a6e
4a32c07
e622c5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "$schema": "https://json.schemastore.org/claude-code-settings.json", | ||
| "env": { | ||
| "GIT_AUTHOR_NAME": "Shivam Kumar", | ||
| "GIT_COMMITTER_NAME": "Shivam Kumar", | ||
| "GIT_AUTHOR_EMAIL": "csautomaytic@gmail.com", | ||
| "GIT_COMMITTER_EMAIL": "csautomaytic@gmail.com" | ||
| }, | ||
| "includeCoAuthoredBy": false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,32 @@ | ||
| SESSION_SECRET=your-secure-secret | ||
| FORCE_HTTPS=false # if HTTPS issues | ||
| COOKIE_DOMAIN=yourdomain.com # if domain issues | ||
| # Database Configuration (Required) | ||
| DATABASE_URL=postgresql://user:password@host:port/database?sslmode=require | ||
|
|
||
| # Server Configuration | ||
| PORT=3000 | ||
| NODE_ENV=development | ||
|
|
||
| # Session Configuration (Required in production) | ||
| SESSION_SECRET=your-secure-random-secret-change-this-in-production | ||
| FORCE_HTTPS=false # Set to true in production with HTTPS | ||
| COOKIE_DOMAIN= # Optional: Set for multi-subdomain support (e.g., .yourdomain.com) | ||
|
|
||
| # Email Configuration (SMTP) | ||
| SMTP_HOST=smtp.gmail.com | ||
| SMTP_PORT=587 | ||
| SMTP_SECURE=false # true for 465, false for other ports | ||
| SMTP_USER=your-email@gmail.com | ||
| SMTP_PASS=your-app-specific-password | ||
| SMTP_FROM=noreply@linkboard.com # Optional: defaults to SMTP_USER | ||
| APP_URL=http://localhost:3000 # Used in email links | ||
|
|
||
| # Optional: Error Tracking with Sentry | ||
| # Leave empty to disable Sentry (app works perfectly without it) | ||
| SENTRY_DSN= # Your Sentry DSN from sentry.io | ||
| SENTRY_ENVIRONMENT=production # Optional: defaults to NODE_ENV | ||
| SENTRY_TRACES_SAMPLE_RATE=0.1 # Optional: 10% of transactions (0.0 to 1.0) | ||
| SENTRY_PROFILES_SAMPLE_RATE=0.1 # Optional: 10% of profiles (0.0 to 1.0) | ||
|
|
||
| # Optional: Redis Caching | ||
| # Leave empty to disable Redis caching (app works perfectly without it) | ||
| REDIS_URL= # redis://localhost:6379 or redis://user:pass@host:port | ||
| REDIS_CACHE_TTL=3600 # Optional: Cache TTL in seconds (default: 3600 = 1 hour) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,10 @@ externalPort = 80 | |
| localPort = 37849 | ||
| externalPort = 3000 | ||
|
|
||
| [[ports]] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Port mapping Prompt for AI agents |
||
| localPort = 41423 | ||
| externalPort = 3001 | ||
|
|
||
| [env] | ||
| PORT = "5000" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,125 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Admin Dashboard Layout Component | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Professional sidebar layout for admin dashboard | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Link, useLocation } from "wouter"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| LayoutDashboard, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Users, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| FileText, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| LogOut, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Menu, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| X, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Settings | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "lucide-react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from "@/components/ui/button"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| interface AdminLayoutProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| children: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export function AdminLayout({ children }: AdminLayoutProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [location] = useLocation(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [sidebarOpen, setSidebarOpen] = useState(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Every admin page opens with the mobile drawer and backdrop covering its content, requiring an extra close action before the dashboard can be used. Initialize the drawer closed; Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const navigation = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { name: "Overview", href: "/admin", icon: LayoutDashboard }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { name: "Users", href: "/admin/users", icon: Users }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { name: "Profiles", href: "/admin/profiles", icon: FileText }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { name: "Settings", href: "/admin/settings", icon: Settings }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleLogout = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await fetch("/api/auth/logout", { method: "POST" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A failed logout still redirects to Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = "/login"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Logout error:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="min-h-screen bg-gray-50"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Mobile sidebar toggle */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="lg:hidden fixed top-0 left-0 right-0 bg-white border-b z-50 px-4 py-3 flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 className="text-xl font-bold text-gray-900">Admin Dashboard</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| size="icon" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The mobile menu control has no accessible name, so screen-reader users cannot tell whether it opens or closes navigation. Add a state-specific Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setSidebarOpen(!sidebarOpen)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {sidebarOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Sidebar */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <aside | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`fixed inset-y-0 left-0 z-40 w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| sidebarOpen ? "translate-x-0" : "-translate-x-full" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } lg:translate-x-0 mt-14 lg:mt-0`} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col h-full"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Logo */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between px-6 py-5 border-b border-gray-200"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 className="text-2xl font-bold text-gray-900">LinkBoard</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="px-2 py-1 text-xs font-medium text-blue-600 bg-blue-50 rounded"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Admin | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Navigation */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {navigation.map((item) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const Icon = item.icon; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const isActive = location === item.href; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Link key={item.name} href={item.href}> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| isActive | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "bg-blue-50 text-blue-600" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : "text-gray-700 hover:bg-gray-50 hover:text-gray-900" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Icon className="w-5 h-5 mr-3" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {item.name} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+79
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In wouter 3.3.5, the Link component's rendering behavior regarding child elements is determined by the presence of the asChild prop [1][2]. Default Behavior By default, the Link component wraps all its children in a standard HTML tag [1][3][4]. This is the standard mode used for simple navigation links [1]. asChild Behavior When the asChild prop is provided, the Link component does not wrap its children in an tag [1][3]. Instead, it passes navigation props (such as href and, depending on implementation, other attributes) directly to the single child element provided [1][4]. Key Considerations for asChild: 1. Valid Children: The asChild mode requires a single valid React element child [5][4]. Passing multiple children or text nodes directly without a wrapping element will not function as intended and may trigger errors or unexpected behavior [5][4]. 2. Prop Forwarding: When using asChild, ensure the child component is capable of receiving and applying the props injected by Link (e.g., href, onClick) [2][6]. If you are using a custom component as a child, it must be designed to accept these attributes for navigation to work [2]. 3. Nesting: While Link supports nesting within routes, the asChild behavior itself is independent of route nesting contexts [3][7]. It is a mechanism for component composition rather than routing structure [5]. If you need to render a custom component (like a button or a styled link) as the navigation element, the asChild pattern is the correct approach to avoid invalid HTML structures (such as nested tags) [3][2]. Citations:
🏁 Script executed: set -euo pipefail
printf '\n== package files ==\n'
git ls-files 'package.json' 'client/package.json' 'pnpm-lock.yaml' 'package-lock.json' 'yarn.lock' | sed 's#^`#-` #'
printf '\n== AdminLayout references ==\n'
rg -n '"wouter"|from '\''wouter'\''|from "wouter"|<Link|asChild' client/src/components/admin/AdminLayout.tsx client/package.json package.json 2>/dev/null || true
printf '\n== AdminLayout excerpt ==\n'
cat -n client/src/components/admin/AdminLayout.tsx | sed -n '1,170p'Repository: WizzyWeb/LinkBoard Length of output: 5289 🏁 Script executed: set -euo pipefail
echo '== package.json =='
if [ -f package.json ]; then
cat -n package.json | sed -n '1,220p'
fi
echo
echo '== client/package.json =='
if [ -f client/package.json ]; then
cat -n client/package.json | sed -n '1,220p'
fi
echo
echo '== AdminLayout first 120 lines =='
cat -n client/src/components/admin/AdminLayout.tsx | sed -n '1,120p'Repository: WizzyWeb/LinkBoard Length of output: 10154 Remove the inner 🤖 Prompt for AI Agents
Comment on lines
+79
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Each sidebar navigation item renders nested anchor elements, creating invalid interactive markup and unreliable keyboard/click behavior. Put the styling and contents directly on Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </nav> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Logout */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-4 border-t border-gray-200"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full justify-start" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleLogout} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <LogOut className="w-4 h-4 mr-2" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Logout | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </aside> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Main content */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="lg:pl-64 pt-14 lg:pt-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <main className="p-6 lg:p-8"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Mobile sidebar backdrop */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {sidebarOpen && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className="fixed inset-0 bg-black bg-opacity-50 z-30 lg:hidden" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setSidebarOpen(false)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /** | ||
| * Admin Panel Component | ||
| * Displays admin controls and user list in the dashboard | ||
| */ | ||
|
|
||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Badge } from "@/components/ui/badge"; | ||
| import { | ||
| Table, | ||
| TableBody, | ||
| TableCell, | ||
| TableHead, | ||
| TableHeader, | ||
| TableRow, | ||
| } from "@/components/ui/table"; | ||
| import { Link } from "wouter"; | ||
| import { Users, Shield, ExternalLink, ChevronRight, FileText, Link as LinkIcon } from "lucide-react"; | ||
|
|
||
| export function AdminPanel() { | ||
| // Fetch admin stats | ||
| const { data: statsData } = useQuery({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Dashboard load makes a complete Prompt for AI agents |
||
| queryKey: ["/api/admin/stats"], | ||
| queryFn: async () => { | ||
| const res = await fetch("/api/admin/stats"); | ||
| if (!res.ok) throw new Error("Failed to fetch"); | ||
| return res.json(); | ||
| }, | ||
| retry: false, | ||
| }); | ||
|
|
||
| // Fetch all users | ||
| const { data, isLoading, error } = useQuery({ | ||
| queryKey: ["/api/admin/users", 1], | ||
| queryFn: async () => { | ||
| const res = await fetch(`/api/admin/users?page=1&limit=10`); | ||
| if (!res.ok) throw new Error("Failed to fetch"); | ||
| return res.json(); | ||
| }, | ||
| retry: false, | ||
| }); | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <Card className="border-red-200 bg-red-50"> | ||
| <CardContent className="pt-6"> | ||
| <p className="text-red-600 text-sm"> | ||
| Unable to load admin data. Please check your permissions. | ||
| </p> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| const users = data?.users || []; | ||
| const pagination = data?.pagination || { total: 0 }; | ||
|
|
||
| return ( | ||
| <Card className="border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50"> | ||
| <CardHeader> | ||
| <div className="flex items-center justify-between"> | ||
| <div className="flex items-center gap-2"> | ||
| <Shield className="h-5 w-5 text-blue-600" /> | ||
| <CardTitle className="text-xl">Admin Panel</CardTitle> | ||
| <Badge variant="default" className="bg-blue-600"> | ||
| Admin | ||
| </Badge> | ||
| </div> | ||
| <Link href="/admin"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The navigation controls render a button inside a link, which is invalid nested interactive content and produces unreliable keyboard/screen-reader semantics. Render the link as the button via Prompt for AI agents |
||
| <Button variant="outline" size="sm" className="flex items-center gap-2"> | ||
| Full Admin Dashboard | ||
| <ChevronRight className="h-4 w-4" /> | ||
| </Button> | ||
| </Link> | ||
| </div> | ||
| </CardHeader> | ||
| <CardContent> | ||
| {isLoading ? ( | ||
| <div className="text-center py-8"> | ||
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div> | ||
| <p className="text-gray-500 text-sm">Loading users...</p> | ||
| </div> | ||
| ) : ( | ||
| <> | ||
| <div className="mb-4"> | ||
| <p className="text-sm text-gray-600"> | ||
| Total Registered Users: <span className="font-semibold text-gray-900">{pagination.total}</span> | ||
| </p> | ||
| </div> | ||
|
|
||
| {users.length === 0 ? ( | ||
| <p className="text-gray-500 text-sm text-center py-4">No users found</p> | ||
| ) : ( | ||
| <div className="rounded-md border bg-white"> | ||
| <Table> | ||
| <TableHeader> | ||
| <TableRow> | ||
| <TableHead>User</TableHead> | ||
| <TableHead>Email</TableHead> | ||
| <TableHead>Status</TableHead> | ||
| <TableHead>Role</TableHead> | ||
| <TableHead>Joined</TableHead> | ||
| </TableRow> | ||
| </TableHeader> | ||
| <TableBody> | ||
| {users.map((user: any) => ( | ||
| <TableRow key={user.id}> | ||
| <TableCell className="font-medium"> | ||
| {user.firstName && user.lastName | ||
| ? `${user.firstName} ${user.lastName}` | ||
| : "—"} | ||
| </TableCell> | ||
| <TableCell className="text-sm">{user.email}</TableCell> | ||
| <TableCell> | ||
| {user.isEmailVerified ? ( | ||
| <Badge variant="default" className="bg-green-100 text-green-800 text-xs"> | ||
| Verified | ||
| </Badge> | ||
| ) : ( | ||
| <Badge variant="secondary" className="text-xs"> | ||
| Unverified | ||
| </Badge> | ||
| )} | ||
| </TableCell> | ||
| <TableCell> | ||
| {user.isAdmin ? ( | ||
| <Badge variant="default" className="bg-blue-100 text-blue-800 text-xs"> | ||
| Admin | ||
| </Badge> | ||
| ) : ( | ||
| <Badge variant="outline" className="text-xs"> | ||
| User | ||
| </Badge> | ||
| )} | ||
| </TableCell> | ||
| <TableCell className="text-sm text-gray-500"> | ||
| {new Date(user.createdAt).toLocaleDateString()} | ||
| </TableCell> | ||
| </TableRow> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| </div> | ||
| )} | ||
|
|
||
| {pagination.total > 10 && ( | ||
| <div className="mt-4 text-center"> | ||
| <Link href="/admin/users"> | ||
| <Button variant="outline" size="sm"> | ||
| View All {pagination.total} Users | ||
| <ExternalLink className="h-4 w-4 ml-2" /> | ||
| </Button> | ||
| </Link> | ||
| </div> | ||
| )} | ||
| </> | ||
| )} | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not force a personal Git identity in shared repository settings.
These environment variables make agent-created commits from every contributor appear authored and committed by the named individual, and publish their email in repository config. Remove this block and keep author identity in each developer’s local Git/Claude configuration.
Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents