mirror_match/components/AdminUserList.tsx
ilia 9640627972
Some checks failed
CI / skip-ci-check (pull_request) Successful in 1m19s
CI / lint-and-type-check (pull_request) Failing after 1m37s
CI / test (pull_request) Successful in 2m16s
CI / build (pull_request) Failing after 1m46s
CI / secret-scanning (pull_request) Successful in 1m20s
CI / dependency-scan (pull_request) Successful in 1m27s
CI / sast-scan (pull_request) Successful in 2m29s
CI / workflow-summary (pull_request) Successful in 1m18s
feat: Add photo management features, duplicate detection, attempt limits, and admin deletion
- Add duplicate photo detection (file hash and URL checking)
- Add max attempts per photo with UI counter
- Simplify penalty system (auto-enable when points > 0)
- Prevent scores from going below 0
- Add admin photo deletion functionality
- Improve navigation with always-visible logout
- Prevent users from guessing their own photos
2026-01-02 14:57:30 -05:00

141 lines
4.8 KiB
TypeScript

"use client"
import { useState } from "react"
interface User {
id: string
name: string
email: string
role: string
points: number
createdAt: Date
}
export default function AdminUserList({ users }: { users: User[] }) {
const [resettingUserId, setResettingUserId] = useState<string | null>(null)
const [newPassword, setNewPassword] = useState<Record<string, string>>({})
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
const handleResetPassword = async (userId: string) => {
const password = newPassword[userId]
if (!password || password.length < 6) {
setError("Password must be at least 6 characters")
return
}
setError("")
setResettingUserId(userId)
try {
const response = await fetch(`/api/admin/users/${userId}/reset-password`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
})
const data = await response.json()
if (!response.ok) {
setError(data.error || "Failed to reset password")
} else {
setSuccess("Password reset successfully!")
setNewPassword({ ...newPassword, [userId]: "" })
setTimeout(() => {
setSuccess("")
}, 3000)
}
} catch {
setError("An error occurred. Please try again.")
} finally {
setResettingUserId(null)
}
}
return (
<div className="space-y-4">
{error && (
<div className="rounded-md bg-red-50 p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{success && (
<div className="rounded-md bg-green-50 p-4">
<p className="text-sm text-green-800">{success}</p>
</div>
)}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Points
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{user.name}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{user.email}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<span
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
user.role === "ADMIN"
? "bg-purple-100 text-purple-800"
: "bg-gray-100 text-gray-800"
}`}
>
{user.role}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{user.points}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<div className="flex items-center space-x-2">
<input
type="password"
placeholder="New password"
value={newPassword[user.id] || ""}
onChange={(e) =>
setNewPassword({ ...newPassword, [user.id]: e.target.value })
}
className="px-2 py-1 border border-gray-300 rounded text-xs w-32 bg-white text-gray-900"
/>
<button
onClick={() => handleResetPassword(user.id)}
disabled={resettingUserId === user.id}
className="px-3 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600 disabled:opacity-50"
>
{resettingUserId === user.id ? "Resetting..." : "Reset"}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}