From b6a2b4e275e00577e8f6b619836da03d124cc3e9 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Tue, 15 Jul 2025 19:42:03 +0530 Subject: [PATCH 01/12] feat: Enhance text color for better readability in KYCContent and BadgeForm components --- src/components/Admin/dashboardContent/KYCContent.tsx | 2 +- src/components/Admin/dashboardContent/badge/BadgeForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Admin/dashboardContent/KYCContent.tsx b/src/components/Admin/dashboardContent/KYCContent.tsx index 07f58fa5..fc514f53 100644 --- a/src/components/Admin/dashboardContent/KYCContent.tsx +++ b/src/components/Admin/dashboardContent/KYCContent.tsx @@ -347,7 +347,7 @@ export default function KYCContent() { } return ( -
+

Admin Dashboard - KYC

diff --git a/src/components/Admin/dashboardContent/badge/BadgeForm.tsx b/src/components/Admin/dashboardContent/badge/BadgeForm.tsx index 1905fe75..47120714 100644 --- a/src/components/Admin/dashboardContent/badge/BadgeForm.tsx +++ b/src/components/Admin/dashboardContent/badge/BadgeForm.tsx @@ -203,7 +203,7 @@ const BadgeForm = ({ onBadgeAdded }: BadgeFormProps) => { }; return ( -
+

Add New Badge

{formError && ( From 455a32083d6e4a6f730b46a524365f2e4ecbb6f3 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 13:21:42 +0530 Subject: [PATCH 02/12] feat: Add admin management features including create, edit, and filter modals - Implemented AdminCreateModal for creating new admin users with validation. - Added AdminEditModal for editing existing admin details with email validation. - Introduced AdminFilters component for searching and filtering admins by role. - Created AdminTable component to display admin details with action buttons for edit and delete. - Added Toast component for displaying success, error, and warning messages. - Updated DashboardContent to include suspended users metric. --- src/app/api/admin/dashboard/route.ts | 6 +- .../AdminManagementContent.tsx | 1295 +++-------------- .../dashboardContent/DashboardContent.tsx | 11 +- .../adminManager/AdminCreateModal.tsx | 268 ++++ .../adminManager/AdminEditModal.tsx | 279 ++++ .../adminManager/AdminFilters.tsx | 67 + .../adminManager/AdminTable.tsx | 172 +++ .../dashboardContent/adminManager/Toast.tsx | 70 + 8 files changed, 1105 insertions(+), 1063 deletions(-) create mode 100644 src/components/Admin/dashboardContent/adminManager/AdminCreateModal.tsx create mode 100644 src/components/Admin/dashboardContent/adminManager/AdminEditModal.tsx create mode 100644 src/components/Admin/dashboardContent/adminManager/AdminFilters.tsx create mode 100644 src/components/Admin/dashboardContent/adminManager/AdminTable.tsx create mode 100644 src/components/Admin/dashboardContent/adminManager/Toast.tsx diff --git a/src/app/api/admin/dashboard/route.ts b/src/app/api/admin/dashboard/route.ts index 0a3f9649..83ac4935 100644 --- a/src/app/api/admin/dashboard/route.ts +++ b/src/app/api/admin/dashboard/route.ts @@ -4,13 +4,14 @@ import Session from "@/lib/models/sessionSchema"; import SkillList from "@/lib/models/skillListing"; import SkillListing from "@/lib/models/skillListing"; import SkillMatch from "@/lib/models/skillMatch"; +import SuspendedUser from "@/lib/models/suspendedUserSchema"; import { NextResponse } from "next/server"; export async function GET() { try { await connect(); // Connect to your MongoDB - const totalUsers = await User.countDocuments(); + const totalUsers = await User.countDocuments({ isDeleted: false }); const sessions = await Session.countDocuments(); const skillsRequested = await SkillList.countDocuments(); const skillsOffered = await SkillListing.countDocuments(); @@ -20,6 +21,8 @@ export async function GET() { const userIds = await Session.distinct("userId"); const activeUsers = userIds.length; + const suspendedUsers = await SuspendedUser.countDocuments(); + const popularSkillDoc = await SkillList.aggregate([ { $group: { _id: "$skillName", count: { $sum: 1 } } }, { $sort: { count: -1 } }, @@ -88,6 +91,7 @@ export async function GET() { totalUsers, activeUsers, sessions, + suspendedUsers, popularSkill, skillsOffered, skillsRequested, diff --git a/src/components/Admin/dashboardContent/AdminManagementContent.tsx b/src/components/Admin/dashboardContent/AdminManagementContent.tsx index debb1221..0436c61c 100644 --- a/src/components/Admin/dashboardContent/AdminManagementContent.tsx +++ b/src/components/Admin/dashboardContent/AdminManagementContent.tsx @@ -1,34 +1,39 @@ "use client"; -import React, { useState, useEffect } from "react"; -import { - Users, - Plus, - Edit, - Trash2, - Shield, - ShieldCheck, - Eye, - EyeOff, - Search, - Filter, - AlertCircle, - CheckCircle, - XCircle, -} from "lucide-react"; - -// Define interfaces +// ─── TOP-LEVEL CONSTANTS (no magic strings) ──────────────────────────────────── + +// API endpoints +const API_CREATE_ADMIN = "/api/admin/create-admin"; +const API_MANAGE_ADMIN = "/api/admin/manage-admin"; + +// Role identifiers +const ROLE_SUPER_ADMIN = "super_admin"; +const ROLE_ADMIN = "admin"; +const ROLE_ALL = "all"; + +// Confirmation prompts +const CONFIRM_DELETE_ADMIN_MESSAGE = + "Are you sure you want to delete this admin? This action cannot be undone."; + +// ─── IMPORTS ────────────────────────────────────────────────────────────────── +import React, { useEffect, useState } from "react"; +import Toast, { ToastType } from "./adminManager/Toast"; +import AdminFilters from "./adminManager/AdminFilters"; +import AdminTable from "./adminManager/AdminTable"; +import AdminCreateModal from "./adminManager/AdminCreateModal"; +import AdminEditModal from "./adminManager/AdminEditModal"; +import { Plus, Shield, ShieldCheck } from "lucide-react"; + +// ─── DATA SHAPES ────────────────────────────────────────────────────────────── +// TypeScript interfaces to enforce structure of our data interface Admin { _id: string; username: string; email: string; role: string; permissions: string[]; - status: string; createdAt: string; - createdBy?: { - username: string; - }; + createdBy?: { username: string }; } interface CreateAdminData { @@ -41,15 +46,25 @@ interface CreateAdminData { interface UpdateAdminData { adminId: string; - username?: string; - email?: string; - password?: string; - role?: string; - permissions?: string[]; - status?: string; + username: string; + email: string; + password: string; + role: string; + permissions: string[]; } -// Available permissions +interface EmailValidation { + isValid: boolean; + message: string; + isChecking: boolean; +} + +interface AdminManagementContentProps { + currentAdminRole?: string; +} + +// ─── PERMISSIONS LIST ───────────────────────────────────────────────────────── +// Central list of all assignable permissions const AVAILABLE_PERMISSIONS = [ { key: "manage_admins", @@ -76,6 +91,11 @@ const AVAILABLE_PERMISSIONS = [ label: "Manage System", description: "System configuration and settings", }, + { + key: "manage_suspended_users", + label: "Manage Suspended Users", + description: "View, suspend or reactivate user accounts", + }, { key: "manage_verification", label: "Manage Verification", @@ -91,6 +111,11 @@ const AVAILABLE_PERMISSIONS = [ label: "Manage Forum Reports", description: "Review and moderate forum post reports", }, + { + key: "manage_success_stories", + label: "Manage Success Stories", + description: "Create, edit, and delete success stories", + }, { key: "view_dashboard", label: "View Dashboard", @@ -98,94 +123,22 @@ const AVAILABLE_PERMISSIONS = [ }, ]; -// Toast notification component -const Toast = ({ - message, - type, - onClose, -}: { - message: string; - type: "success" | "error" | "warning"; - onClose: () => void; -}) => { - const bgColor = - type === "success" - ? "bg-green-500" - : type === "error" - ? "bg-red-500" - : "bg-yellow-500"; - const icon = - type === "success" ? ( - - ) : type === "error" ? ( - - ) : ( - - ); - - useEffect(() => { - const timer = setTimeout(onClose, 5000); - return () => clearTimeout(timer); - }, [onClose]); - - return ( -
- {icon} - {message} - -
- ); -}; - -interface AdminManagementContentProps { - currentAdminRole?: string; -} - -const AdminManagementContent: React.FC = ({ +// ─── COMPONENT ──────────────────────────────────────────────────────────────── +export default function AdminManagementContent({ currentAdminRole, -}) => { +}: AdminManagementContentProps) { + // ─── STATE HOOKS ──────────────────────────────────────────────────────────── const [admins, setAdmins] = useState([]); const [loading, setLoading] = useState(true); const [showCreateForm, setShowCreateForm] = useState(false); const [showEditForm, setShowEditForm] = useState(false); const [selectedAdmin, setSelectedAdmin] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); - const [filterRole, setFilterRole] = useState("all"); - const [filterStatus, setFilterStatus] = useState("all"); - const [toast, setToast] = useState<{ - message: string; - type: "success" | "error" | "warning"; - } | null>(null); - - // Email validation states - const [emailValidation, setEmailValidation] = useState({ - isValid: false, - message: "", - isChecking: false, - }); - const [updateEmailValidation, setUpdateEmailValidation] = useState({ - isValid: false, - message: "", - isChecking: false, - }); - // Check if current admin is a super admin - const isCurrentSuperAdmin = currentAdminRole === "super_admin"; - - // Form states const [createForm, setCreateForm] = useState({ username: "", email: "", password: "", - role: "admin", + role: ROLE_ADMIN, permissions: [], }); @@ -194,53 +147,75 @@ const AdminManagementContent: React.FC = ({ username: "", email: "", password: "", - role: "", + role: ROLE_ADMIN, permissions: [], - status: "", }); + const [searchTerm, setSearchTerm] = useState(""); + const [filterRole, setFilterRole] = useState(ROLE_ALL); const [showPassword, setShowPassword] = useState(false); - // Email validation functions - const validateEmailFormat = ( - email: string - ): { isValid: boolean; message: string } => { - if (!email.trim()) { - return { isValid: false, message: "Email is required" }; - } + // For displaying temporary feedback to the user + const [toast, setToast] = useState<{ message: string; type: ToastType } | null>( + null + ); - // Check email length - if (email.length > 254) { - return { - isValid: false, - message: "Email is too long (max 254 characters)", - }; + // Email validation states for create vs. update flows + const [emailValidation, setEmailValidation] = useState({ + isValid: false, + message: "", + isChecking: false, + }); + const [updateEmailValidation, setUpdateEmailValidation] = + useState({ + isValid: false, + message: "", + isChecking: false, + }); + + // Debounced search term to avoid rapid filtering on every keystroke + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + + // Shortcut boolean for super‐admin checks + const isCurrentSuperAdmin = currentAdminRole === ROLE_SUPER_ADMIN; + + // ─── DATA FETCHING ─────────────────────────────────────────────────────────── + const fetchAdmins = async () => { + try { + const response = await fetch(API_CREATE_ADMIN, { + method: "GET", + credentials: "include", + }); + if (!response.ok) throw new Error("Failed to fetch admins"); + const data = await response.json(); + setAdmins(data.admins || []); + } catch { + setToast({ message: "Failed to load admins", type: "error" }); + } finally { + setLoading(false); } + }; - // Enhanced email regex pattern + // ─── EMAIL VALIDATION HELPERS ──────────────────────────────────────────────── + const validateEmailFormat = (email: string) => { + if (!email.trim()) return { isValid: false, message: "Email is required" }; + if (email.length > 254) + return { isValid: false, message: "Email is too long (max 254 characters)" }; + + // Standard RFC-compatible regex const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - - if (!emailRegex.test(email.trim())) { + if (!emailRegex.test(email.trim())) return { isValid: false, message: "Please enter a valid email address" }; - } - // Check for common domains and format issues - const domain = email.split("@")[1]; - if (domain) { - // Check if domain has at least one dot - if (!domain.includes(".")) { - return { - isValid: false, - message: "Email domain must contain a valid extension", - }; - } - - // Check domain length - if (domain.length > 253) { - return { isValid: false, message: "Email domain is too long" }; - } - } + const domain = email.split("@")[1] || ""; + if (!domain.includes(".")) + return { + isValid: false, + message: "Email domain must contain a valid extension", + }; + if (domain.length > 253) + return { isValid: false, message: "Email domain is too long" }; return { isValid: true, message: "Email format is valid" }; }; @@ -248,69 +223,40 @@ const AdminManagementContent: React.FC = ({ const checkEmailUniqueness = async ( email: string, excludeAdminId?: string - ): Promise<{ isUnique: boolean; message: string }> => { + ) => { try { - // Check against existing admins - const existingAdmin = admins.find( - (admin) => - admin.email.toLowerCase() === email.toLowerCase() && - admin._id !== excludeAdminId + const existing = admins.find( + (a) => + a.email.toLowerCase() === email.toLowerCase() && + a._id !== excludeAdminId ); - - if (existingAdmin) { - return { - isUnique: false, - message: "This email is already used by another admin", - }; - } - - // You could also check against user collection here if needed - // For now, we'll rely on server-side validation for complete check - + if (existing) + return { isUnique: false, message: "This email is already used by another admin" }; return { isUnique: true, message: "Email is available" }; - } catch (error) { + } catch { return { isUnique: false, message: "Unable to verify email uniqueness" }; } }; + // ─── CREATE / UPDATE EMAIL VALIDATION FLOWS ───────────────────────────────── const validateEmailForCreate = async (email: string) => { setEmailValidation({ isValid: false, message: "", isChecking: true }); - - // First check format const formatCheck = validateEmailFormat(email); if (!formatCheck.isValid) { - setEmailValidation({ - isValid: false, - message: formatCheck.message, - isChecking: false, - }); + setEmailValidation({ ...formatCheck, isChecking: false }); return; } - - // Then check uniqueness const uniquenessCheck = await checkEmailUniqueness(email); - setEmailValidation({ - isValid: uniquenessCheck.isUnique, - message: uniquenessCheck.message, - isChecking: false, - }); + setEmailValidation({ isValid: uniquenessCheck.isUnique, message: uniquenessCheck.message, isChecking: false }); }; const validateEmailForUpdate = async (email: string, adminId: string) => { setUpdateEmailValidation({ isValid: false, message: "", isChecking: true }); - - // First check format const formatCheck = validateEmailFormat(email); if (!formatCheck.isValid) { - setUpdateEmailValidation({ - isValid: false, - message: formatCheck.message, - isChecking: false, - }); + setUpdateEmailValidation({ ...formatCheck, isChecking: false }); return; } - - // Then check uniqueness (excluding current admin) const uniquenessCheck = await checkEmailUniqueness(email, adminId); setUpdateEmailValidation({ isValid: uniquenessCheck.isUnique, @@ -319,212 +265,68 @@ const AdminManagementContent: React.FC = ({ }); }; - // Fetch all admins - const fetchAdmins = async () => { - try { - const response = await fetch("/api/admin/create-admin", { - method: "GET", - credentials: "include", - }); - - if (!response.ok) { - throw new Error("Failed to fetch admins"); - } - - const data = await response.json(); - setAdmins(data.admins || []); - } catch (error) { - console.error("Error fetching admins:", error); - setToast({ message: "Failed to load admins", type: "error" }); - } finally { - setLoading(false); - } - }; - - // Create new admin + // ─── CRUD OPERATIONS ───────────────────────────────────────────────────────── const createAdmin = async (e: React.FormEvent) => { e.preventDefault(); - - // Client-side validation - if (createForm.username.trim().length < 4) { - setToast({ - message: "Username must be at least 4 characters long", - type: "error", - }); - return; - } - - // Enhanced email validation - const emailFormatCheck = validateEmailFormat(createForm.email); - if (!emailFormatCheck.isValid) { - setToast({ - message: emailFormatCheck.message, - type: "error", - }); - return; - } - - // Check email uniqueness - const emailUniquenessCheck = await checkEmailUniqueness(createForm.email); - if (!emailUniquenessCheck.isUnique) { - setToast({ - message: emailUniquenessCheck.message, - type: "error", - }); - return; - } - - if (createForm.password.length < 8) { - setToast({ - message: "Password must be at least 8 characters long", - type: "error", - }); - return; - } - - const passwordRegex = /^(?=.*\d)/; - if (!passwordRegex.test(createForm.password)) { - setToast({ - message: "Password must contain at least one number", - type: "error", - }); - return; - } - try { - const response = await fetch("/api/admin/create-admin", { + const response = await fetch(API_CREATE_ADMIN, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(createForm), }); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Failed to create admin"); - } - + if (!response.ok) throw new Error(data.message || "Failed to create admin"); setToast({ message: "New admin created successfully", type: "success" }); setShowCreateForm(false); - setCreateForm({ - username: "", - email: "", - password: "", - role: "admin", - permissions: [], - }); - // Reset email validation state + setCreateForm({ username: "", email: "", password: "", role: ROLE_ADMIN, permissions: [] }); setEmailValidation({ isValid: false, message: "", isChecking: false }); fetchAdmins(); } catch (error: any) { - console.error("Error creating admin:", error); - setToast({ - message: error.message || "Failed to create admin", - type: "error", - }); + setToast({ message: error.message || "Failed to create admin", type: "error" }); } }; - // Update admin const updateAdmin = async (e: React.FormEvent) => { e.preventDefault(); - - // Email validation if email is being changed - if (updateForm.email && updateForm.email !== selectedAdmin?.email) { - const emailFormatCheck = validateEmailFormat(updateForm.email); - if (!emailFormatCheck.isValid) { - setToast({ - message: emailFormatCheck.message, - type: "error", - }); - return; - } - - const emailUniquenessCheck = await checkEmailUniqueness( - updateForm.email, - updateForm.adminId - ); - if (!emailUniquenessCheck.isUnique) { - setToast({ - message: emailUniquenessCheck.message, - type: "error", - }); - return; - } - } - try { - const response = await fetch("/api/admin/manage-admin", { + const response = await fetch(API_MANAGE_ADMIN, { method: "PUT", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(updateForm), }); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Failed to update admin"); - } - + if (!response.ok) throw new Error(data.message || "Failed to update admin"); setToast({ message: "Admin updated successfully", type: "success" }); setShowEditForm(false); setSelectedAdmin(null); - // Reset email validation state - setUpdateEmailValidation({ - isValid: false, - message: "", - isChecking: false, - }); + setUpdateEmailValidation({ isValid: false, message: "", isChecking: false }); fetchAdmins(); } catch (error: any) { - console.error("Error updating admin:", error); - setToast({ - message: error.message || "Failed to update admin", - type: "error", - }); + setToast({ message: error.message || "Failed to update admin", type: "error" }); } }; - // Delete admin const deleteAdmin = async (adminId: string) => { - if ( - !confirm( - "Are you sure you want to delete this admin? This action cannot be undone." - ) - ) { - return; - } + // Ask user for confirmation before destructive action + if (!confirm(CONFIRM_DELETE_ADMIN_MESSAGE)) return; try { - const response = await fetch(`/api/admin/manage-admin?id=${adminId}`, { + const response = await fetch(`${API_MANAGE_ADMIN}?id=${adminId}`, { method: "DELETE", credentials: "include", }); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Failed to delete admin"); - } - + if (!response.ok) throw new Error(data.message || "Failed to delete admin"); setToast({ message: "Admin deleted successfully", type: "success" }); fetchAdmins(); } catch (error: any) { - console.error("Error deleting admin:", error); - setToast({ - message: error.message || "Failed to delete admin", - type: "error", - }); + setToast({ message: error.message || "Failed to delete admin", type: "error" }); } }; - // Handle edit form + // ─── HANDLERS & HELPERS ───────────────────────────────────────────────────── const handleEdit = (admin: Admin) => { setSelectedAdmin(admin); setUpdateForm({ @@ -534,105 +336,81 @@ const AdminManagementContent: React.FC = ({ password: "", role: admin.role, permissions: admin.permissions, - status: admin.status, - }); - // Reset email validation state - setUpdateEmailValidation({ - isValid: false, - message: "", - isChecking: false, }); + setUpdateEmailValidation({ isValid: false, message: "", isChecking: false }); setShowEditForm(true); }; - // Handle permission toggle - const togglePermission = (permission: string, isCreate: boolean = false) => { - if (isCreate) { - const newPermissions = createForm.permissions.includes(permission) - ? createForm.permissions.filter((p) => p !== permission) - : [...createForm.permissions, permission]; - setCreateForm({ ...createForm, permissions: newPermissions }); - } else { - const newPermissions = updateForm.permissions!.includes(permission) - ? updateForm.permissions!.filter((p) => p !== permission) - : [...updateForm.permissions!, permission]; - setUpdateForm({ ...updateForm, permissions: newPermissions }); - } + // Toggle a permission on create vs update forms + const togglePermission = (permission: string, isCreate = false) => { + const form = isCreate ? createForm : updateForm; + const has = form.permissions.includes(permission); + const newPerms = has + ? form.permissions.filter((p) => p !== permission) + : [...form.permissions, permission]; + + if (isCreate) setCreateForm({ ...createForm, permissions: newPerms }); + else setUpdateForm({ ...updateForm, permissions: newPerms }); }; - // Set default permissions based on role - const setDefaultPermissions = (role: string, isCreate: boolean = false) => { - const defaultPermissions = - role === "super_admin" + // Auto-assign default permissions based on selected role + const setDefaultPermissions = (role: string, isCreate = false) => { + const defaultKeys = + role === ROLE_SUPER_ADMIN ? AVAILABLE_PERMISSIONS.map((p) => p.key) - : AVAILABLE_PERMISSIONS.filter((p) => p.key !== "manage_admins").map( - (p) => p.key - ); + : AVAILABLE_PERMISSIONS.filter((p) => p.key !== "manage_admins").map((p) => p.key); - if (isCreate) { - setCreateForm({ ...createForm, role, permissions: defaultPermissions }); - } else { - setUpdateForm({ ...updateForm, role, permissions: defaultPermissions }); - } + if (isCreate) setCreateForm({ ...createForm, role, permissions: defaultKeys }); + else setUpdateForm({ ...updateForm, role, permissions: defaultKeys }); }; - // Filter admins + // ─── FILTERED & DEBOUNCED SEARCH ───────────────────────────────────────────── const filteredAdmins = admins.filter((admin) => { + const text = debouncedSearchTerm.toLowerCase(); const matchesSearch = - admin.username - .toLowerCase() - .includes(debouncedSearchTerm.toLowerCase()) || - admin.email.toLowerCase().includes(debouncedSearchTerm.toLowerCase()); - const matchesRole = filterRole === "all" || admin.role === filterRole; - const matchesStatus = - filterStatus === "all" || admin.status === filterStatus; - return matchesSearch && matchesRole && matchesStatus; + admin.username.toLowerCase().includes(text) || + admin.email.toLowerCase().includes(text); + const matchesRole = filterRole === ROLE_ALL || admin.role === filterRole; + return matchesSearch && matchesRole; }); - // Debounce search term + // Debounce search input for smoother typing UX useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm); - }, 300); - + const timer = setTimeout(() => setDebouncedSearchTerm(searchTerm), 300); return () => clearTimeout(timer); }, [searchTerm]); - // Debounce email validation for create form + // Validate email on create form with a slight delay useEffect(() => { if (createForm.email.trim()) { - const timer = setTimeout(() => { - validateEmailForCreate(createForm.email.trim()); - }, 500); - + const timer = setTimeout(() => validateEmailForCreate(createForm.email.trim()), 500); return () => clearTimeout(timer); - } else { - setEmailValidation({ isValid: false, message: "", isChecking: false }); } + setEmailValidation({ isValid: false, message: "", isChecking: false }); }, [createForm.email]); - // Debounce email validation for update form + // Validate email on update form when email or id changes useEffect(() => { - if (updateForm.email && updateForm.email.trim() && updateForm.adminId) { - const timer = setTimeout(() => { - validateEmailForUpdate(updateForm.email!.trim(), updateForm.adminId); - }, 500); - + if (updateForm.email && updateForm.adminId) { + const timer = setTimeout(() => validateEmailForUpdate(updateForm.email.trim(), updateForm.adminId), 500); return () => clearTimeout(timer); - } else { - setUpdateEmailValidation({ - isValid: false, - message: "", - isChecking: false, - }); } + setUpdateEmailValidation({ isValid: false, message: "", isChecking: false }); }, [updateForm.email, updateForm.adminId]); + // Initial data load useEffect(() => { fetchAdmins(); }, []); + // Reset create permissions whenever role changes + useEffect(() => { + setDefaultPermissions(createForm.role, true); + }, [createForm.role]); + + // ─── RENDER ────────────────────────────────────────────────────────────────── if (loading) { + // Show spinner while fetching return (
@@ -641,16 +419,14 @@ const AdminManagementContent: React.FC = ({ ); } - // Show unauthorized message if user is not a super admin if (!isCurrentSuperAdmin) { + // Restrict access for non-super admins return (
-

- Unauthorized -

+

Unauthorized

You don't have permission to access admin management.

@@ -665,30 +441,27 @@ const AdminManagementContent: React.FC = ({ return (
- {/* Toast notification */} + {/* Toast notifications */} {toast && (
- setToast(null)} - /> + setToast(null)} />
)} - {/* Header */} + {/* Header and Create button */}

Admin Management

-

- Manage administrator accounts and permissions -

+

Manage administrator accounts and permissions

- {/* Filters */} -
-
-
-
- - setSearchTerm(e.target.value)} - className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
-
- - - -
-
-
- - {/* Admins List */} -
-
- - - - - - - - - - - - {filteredAdmins.length === 0 ? ( - - - - ) : ( - filteredAdmins.map((admin) => ( - - - - - - - - )) - )} - -
- Admin Details - - Role & Status - - Permissions - - Created - - Actions -
- -

No admins found

-

- Try adjusting your search or filter criteria -

-
-
-
- {admin.username} -
-
- {admin.email} -
-
-
-
- {admin.role === "super_admin" ? ( - - ) : ( - - )} -
-
- {admin.role === "super_admin" - ? "Super Admin" - : "Admin"} -
-
- {admin.status.charAt(0).toUpperCase() + - admin.status.slice(1)} -
-
-
-
-
- {admin.permissions.slice(0, 3).map((permission) => ( - - {AVAILABLE_PERMISSIONS.find( - (p) => p.key === permission - )?.label || permission} - - ))} - {admin.permissions.length > 3 && ( - - +{admin.permissions.length - 3} more - - )} -
-
-
-
- {new Date(admin.createdAt).toLocaleDateString()} -
- {admin.createdBy && ( -
- by {admin.createdBy.username} -
- )} -
-
-
- - -
-
-
-
- - {/* Create Admin Modal */} + {/* Filters and table */} + + + + {/* Create and Edit modals */} {showCreateForm && ( -
-
-

Create New Admin

-
-
-
- - - setCreateForm({ ...createForm, username: e.target.value }) - } - placeholder="At least 4 characters" - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - minLength={4} - /> -

- Minimum 4 characters -

-
-
- -
- - setCreateForm({ ...createForm, email: e.target.value }) - } - placeholder="admin@example.com" - className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:border-transparent ${ - createForm.email.trim() === "" - ? "border-gray-300 focus:ring-blue-500" - : emailValidation.isChecking - ? "border-yellow-300 focus:ring-yellow-500" - : emailValidation.isValid - ? "border-green-300 focus:ring-green-500" - : "border-red-300 focus:ring-red-500" - }`} - required - /> - {emailValidation.isChecking && ( -
-
-
- )} - {!emailValidation.isChecking && createForm.email.trim() && ( -
- {emailValidation.isValid ? ( - - ) : ( - - )} -
- )} -
-

- {emailValidation.isChecking - ? "Validating email..." - : emailValidation.message || - "Valid email address required"} -

-
-
- -
- -
- - setCreateForm({ ...createForm, password: e.target.value }) - } - placeholder="Minimum 8 characters" - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - minLength={8} - /> - -
-

- Minimum 8 characters, at least 1 number -

-
- -
- - -

- Super admins have full system access -

-
- -
- -
- {createForm.role === "super_admin" ? ( - // Super Admin: Show all permissions as read-only list with checkmarks -
-

- Super admins have access to all system permissions: -

- {AVAILABLE_PERMISSIONS.map((permission) => ( -
- -
- - {permission.label} - - - {permission.description} - -
-
- ))} -
- ) : ( - // Regular Admin: Show permissions as read-only list (excluding manage_admins) -
-

- Regular admins have all permissions except admin - management: -

- {AVAILABLE_PERMISSIONS.filter( - (p) => p.key !== "manage_admins" - ).map((permission) => ( -
- -
- - {permission.label} - - - {permission.description} - -
-
- ))} -
- -
- - Manage Admins - - - Not available for regular admins - -
-
-
- )} -
-
- -
- - -
-
-
-
+ setShowCreateForm(false)} + availablePermissions={AVAILABLE_PERMISSIONS} + showPassword={showPassword} + setShowPassword={setShowPassword} + /> )} - - {/* Edit Admin Modal */} {showEditForm && selectedAdmin && ( -
-
-

- Edit Admin: {selectedAdmin.username} -

-
-
-
- - - setUpdateForm({ ...updateForm, username: e.target.value }) - } - placeholder="At least 4 characters" - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - minLength={4} - /> -
-
- -
- - setUpdateForm({ ...updateForm, email: e.target.value }) - } - placeholder="admin@example.com" - className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:border-transparent ${ - updateForm.email === selectedAdmin.email - ? "border-gray-300 focus:ring-blue-500" - : updateEmailValidation.isChecking - ? "border-yellow-300 focus:ring-yellow-500" - : updateEmailValidation.isValid - ? "border-green-300 focus:ring-green-500" - : "border-red-300 focus:ring-red-500" - }`} - required - /> - {updateEmailValidation.isChecking && ( -
-
-
- )} - {!updateEmailValidation.isChecking && - updateForm.email !== selectedAdmin.email && ( -
- {updateEmailValidation.isValid ? ( - - ) : ( - - )} -
- )} -
-

- {updateForm.email === selectedAdmin.email - ? "Current email address" - : updateEmailValidation.isChecking - ? "Validating email..." - : updateEmailValidation.message || - "Valid email address required"} -

-
-
- -
- -
- - setUpdateForm({ ...updateForm, password: e.target.value }) - } - placeholder="Leave blank to keep current password" - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - -
-

- Minimum 8 characters, at least 1 number if provided -

-
- -
-
- - -
-
- - -
-
- -
- -
- {updateForm.role === "super_admin" ? ( - // Super Admin: Show all permissions as read-only list with checkmarks -
-

- Super admins have access to all system permissions: -

- {AVAILABLE_PERMISSIONS.map((permission) => ( -
- -
- - {permission.label} - - - {permission.description} - -
-
- ))} -
- ) : ( - // Regular Admin: Show permissions as read-only list (excluding manage_admins) -
-

- Regular admins have all permissions except admin - management: -

- {AVAILABLE_PERMISSIONS.filter( - (p) => p.key !== "manage_admins" - ).map((permission) => ( -
- -
- - {permission.label} - - - {permission.description} - -
-
- ))} -
- -
- - Manage Admins - - - Not available for regular admins - -
-
-
- )} -
-
- -
- - -
-
-
-
+ setShowEditForm(false)} + emailValidation={updateEmailValidation} + availablePermissions={AVAILABLE_PERMISSIONS} + showPassword={showPassword} + setShowPassword={setShowPassword} + /> )}
); -}; - -export default AdminManagementContent; +} diff --git a/src/components/Admin/dashboardContent/DashboardContent.tsx b/src/components/Admin/dashboardContent/DashboardContent.tsx index 2a2416a1..a7689102 100644 --- a/src/components/Admin/dashboardContent/DashboardContent.tsx +++ b/src/components/Admin/dashboardContent/DashboardContent.tsx @@ -97,6 +97,7 @@ const DASHBOARD_API_URL = "/api/admin/dashboard"; interface DashboardData { totalUsers: number; sessions: number; + suspendedUsers: number; newUsersThisWeek: number; skillsOffered: number; skillsRequested: number; @@ -661,12 +662,10 @@ export default function DashboardContent() { value={filteredMetrics.newUsers.toString()} /> 0 - ? `${Math.round((data.matches / data.skillsRequested) * 100)}%` - : "0%" - } + + title="No of Suspended Users" + value={data.suspendedUsers.toString()} + /> void; + onSubmit: (e: React.FormEvent) => void; + onClose: () => void; + availablePermissions: Permission[]; + showPassword: boolean; + setShowPassword: (val: boolean) => void; +} + +// ─── COMPONENT ──────────────────────────────────────────────────────────────── +export default function AdminCreateModal({ + form, + onChange, + onSubmit, + onClose, + availablePermissions, + showPassword, + setShowPassword, +}: AdminCreateModalProps) { + // Trimmed email for validation + const email = form.email.trim(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + // null = untouched, true/false = validity + const isEmailValid = email === "" ? null : emailRegex.test(email); + + // Password strength: at least 8 chars, one uppercase, one digit + const pwd = form.password; + const pwdRegex = /^(?=.*[A-Z])(?=.*\d).{8,}$/; + const isPasswordValid = pwd === "" ? null : pwdRegex.test(pwd); + + return ( +
+ {/* Modal backdrop and container */} +
+

Create New Admin

+
+ {/* ── Username & Email ───────────────────────────────────────── */} +
+ {/* Username input */} +
+ + onChange({ ...form, username: e.target.value })} + placeholder="At least 4 characters" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" + required + minLength={4} + /> +
+ {/* Email input with live validation */} +
+ +
+ onChange({ ...form, email: e.target.value })} + placeholder="admin@example.com" + className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 ${ + email === "" + ? "border-gray-300" + : isEmailValid + ? "border-green-300" + : "border-red-300" + }`} + required + /> + {email && ( +
+ {isEmailValid ? ( + + ) : ( + + )} +
+ )} +
+ {email && ( +

+ {isEmailValid + ? "Valid email format" + : "Please enter a valid email address"} +

+ )} +
+
+ + {/* ── Password Input with Visibility Toggle ──────────────────────── */} +
+ +
+ onChange({ ...form, password: e.target.value })} + placeholder="Min 8 chars, 1 uppercase, 1 digit" + className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 ${ + pwd === "" + ? "border-gray-300" + : isPasswordValid + ? "border-green-300" + : "border-red-300" + }`} + required + minLength={8} + /> + +
+ {pwd && ( +

+ {isPasswordValid + ? "Strong password" + : "Use 8+ chars, including uppercase & digit"} +

+ )} +
+ + {/* ── Role Selector ──────────────────────────────────────────────── */} +
+ + +
+ + {/* ── Permissions Display ───────────────────────────────────────── */} +
+ +
+ {form.role === ROLE_SUPER_ADMIN ? ( + // Super admins have all permissions granted +
+ {availablePermissions.map(perm => ( +
+ +
+
+ {perm.label} +
+
+ {perm.description} +
+
+
+ ))} +
+ ) : ( + // Regular admins see all except the manage_admins permission +
+ {availablePermissions + .filter(p => p.key !== "manage_admins") + .map(perm => ( +
+ +
+
+ {perm.label} +
+
+ {perm.description} +
+
+
+ ))} + {/* Explicitly indicate that manage_admins is unavailable */} +
+ +
+
+ Manage Admins +
+
+ Not available for regular admins +
+
+
+
+ )} +
+
+ + {/* ── Action Buttons ──────────────────────────────────────────────── */} +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Admin/dashboardContent/adminManager/AdminEditModal.tsx b/src/components/Admin/dashboardContent/adminManager/AdminEditModal.tsx new file mode 100644 index 00000000..c3990fd6 --- /dev/null +++ b/src/components/Admin/dashboardContent/adminManager/AdminEditModal.tsx @@ -0,0 +1,279 @@ +"use client"; + +// ─── TOP-LEVEL CONSTANTS ────────────────────────────────────────────────────── +// Role identifiers used throughout the modal +const ROLE_SUPER_ADMIN = "super_admin"; +const ROLE_ADMIN = "admin"; + +// ─── IMPORTS ────────────────────────────────────────────────────────────────── +import { CheckCircle, Eye, EyeOff, XCircle } from "lucide-react"; + +// ─── TYPE DEFINITIONS ───────────────────────────────────────────────────────── +// Defines the structure of a permission item +interface Permission { + key: string; + label: string; + description: string; +} + +// Payload for updating an admin +interface UpdateAdminData { + adminId: string; + username: string; + email: string; + password: string; + role: string; + permissions: string[]; +} + +// Tracks email validation state during edit +interface EmailValidation { + isValid: boolean; + message: string; + isChecking: boolean; +} + +// Props accepted by the AdminEditModal component +interface AdminEditModalProps { + selectedAdmin: UpdateAdminData; + form: UpdateAdminData; + onChange: (data: UpdateAdminData) => void; + onSubmit: (e: React.FormEvent) => void; + onClose: () => void; + emailValidation: EmailValidation; + availablePermissions: Permission[]; + showPassword: boolean; + setShowPassword: (val: boolean) => void; +} + +// ─── COMPONENT ──────────────────────────────────────────────────────────────── +export default function AdminEditModal({ + selectedAdmin, + form, + onChange, + onSubmit, + onClose, + emailValidation, + availablePermissions, + showPassword, + setShowPassword, +}: AdminEditModalProps) { + return ( +
+ {/* Modal container */} +
+ {/* Header */} +

+ Edit Admin: {selectedAdmin.username} +

+ + {/* Form starts here */} +
+ {/* Username & Email fields */} +
+ {/* Username input */} +
+ + + onChange({ ...form, username: e.target.value }) + } + placeholder="At least 4 characters" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + minLength={4} + /> +
+ + {/* Email input with validation UI */} +
+ +
+ onChange({ ...form, email: e.target.value })} + placeholder="admin@example.com" + className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:border-transparent ${ + form.email === selectedAdmin.email + ? "border-gray-300 focus:ring-blue-500" + : emailValidation.isChecking + ? "border-yellow-300 focus:ring-yellow-500" + : emailValidation.isValid + ? "border-green-300 focus:ring-green-500" + : "border-red-300 focus:ring-red-500" + }`} + required + /> + {/* Spinner while checking email */} + {emailValidation.isChecking && ( +
+
+
+ )} + {/* Success or error icon once checking completes */} + {!emailValidation.isChecking && + form.email !== selectedAdmin.email && ( +
+ {emailValidation.isValid ? ( + + ) : ( + + )} +
+ )} +
+ {/* Validation message below email */} +

+ {form.email === selectedAdmin.email + ? "Current email address" + : emailValidation.isChecking + ? "Validating email..." + : emailValidation.message || + "Valid email address required"} +

+
+
+ + {/* Password field with visibility toggle */} +
+ +
+ + onChange({ ...form, password: e.target.value }) + } + placeholder="Leave blank to keep current password" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + +
+
+ + {/* Role selector */} +
+
+ + +
+
+ + {/* Permissions list */} +
+ +
+ {form.role === ROLE_SUPER_ADMIN ? ( + // Super admin sees all permissions pre-checked +
+ {availablePermissions.map(permission => ( +
+ +
+ + {permission.label} + + + {permission.description} + +
+
+ ))} +
+ ) : ( + // Regular admin can toggle each permission +
+ {availablePermissions.map(permission => ( +
+ { + const has = form.permissions.includes(permission.key); + const updated = has + ? form.permissions.filter(p => p !== permission.key) + : [...form.permissions, permission.key]; + onChange({ ...form, permissions: updated }); + }} + className="mr-2" + /> +
+ + {permission.label} + + + {permission.description} + +
+
+ ))} +
+ )} +
+
+ + {/* Action buttons */} +
+ + +
+
+
+
+ ); +} diff --git a/src/components/Admin/dashboardContent/adminManager/AdminFilters.tsx b/src/components/Admin/dashboardContent/adminManager/AdminFilters.tsx new file mode 100644 index 00000000..69a68b13 --- /dev/null +++ b/src/components/Admin/dashboardContent/adminManager/AdminFilters.tsx @@ -0,0 +1,67 @@ +"use client"; + +// ─── TOP-LEVEL CONSTANTS ──────────────────────────────────────────────── +// Role identifiers used in filters +const ROLE_SUPER_ADMIN = "super_admin"; +const ROLE_ADMIN = "admin"; +const ROLE_ALL = "all"; + +// ─── IMPORTS ──────────────────────────────────────────────────────────── +import { Filter, Search } from "lucide-react"; + +// ─── PROPS DEFINITION ──────────────────────────────────────────────────── +// Defines the shape of props passed into the filter component +interface AdminFiltersProps { + searchTerm: string; + onSearchChange: (value: string) => void; + filterRole: string; + onRoleChange: (value: string) => void; +} + +// ─── COMPONENT ──────────────────────────────────────────────────────────── +export default function AdminFilters({ + searchTerm, + onSearchChange, + filterRole, + onRoleChange, +}: AdminFiltersProps) { + return ( +
+ {/* Wrapper for search input and role dropdown */} +
+ {/* Search box */} +
+
+ {/* Search icon inside input */} + + onSearchChange(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + {/* Role filter dropdown */} +
+ {/* Filter icon next to dropdown */} + + +
+
+
+ ); +} diff --git a/src/components/Admin/dashboardContent/adminManager/AdminTable.tsx b/src/components/Admin/dashboardContent/adminManager/AdminTable.tsx new file mode 100644 index 00000000..1bf40495 --- /dev/null +++ b/src/components/Admin/dashboardContent/adminManager/AdminTable.tsx @@ -0,0 +1,172 @@ +"use client"; + +// ─── TOP-LEVEL CONSTANTS ────────────────────────────────────────────────────── +// Role identifier for Super Admin checks +const ROLE_SUPER_ADMIN = "super_admin"; + +// ─── IMPORTS ────────────────────────────────────────────────────────────────── +import { Edit, Shield, ShieldCheck, Trash2 } from "lucide-react"; + +// ─── TYPE DEFINITIONS ───────────────────────────────────────────────────────── +// Represents an administrator account +interface Admin { + _id: string; + username: string; + email: string; + role: string; + permissions: string[]; + createdAt: string; + createdBy?: { username: string }; +} + +// Represents a single permission option +interface Permission { + key: string; + label: string; + description: string; +} + +// Props accepted by the AdminTable component +interface AdminTableProps { + admins: Admin[]; + availablePermissions: Permission[]; + onEdit: (admin: Admin) => void; + onDelete: (id: string) => void; +} + +// ─── COMPONENT ──────────────────────────────────────────────────────────────── +export default function AdminTable({ + admins, + availablePermissions, + onEdit, + onDelete, +}: AdminTableProps) { + return ( +
+ {/* Responsive wrapper for horizontal scrolling */} +
+ + {/* Table headings */} + + + + + + + + + + {/* Table body with data rows */} + + {admins.map(admin => ( + + {/* Username and email column */} + + + {/* Role column with icon */} + + + {/* Permissions column: show first 3 with “+N more” */} + + + {/* Created date and creator */} + + + {/* Action buttons for edit and delete */} + + + ))} + + {/* Fallback row when there are no admins */} + {admins.length === 0 && ( + + + + )} + +
+ Admin Details + + Role + + Permissions + + Created + + Actions +
+
+
+ {admin.username} +
+
{admin.email}
+
+
+
+ {admin.role === ROLE_SUPER_ADMIN ? ( + + ) : ( + + )} + + {admin.role === ROLE_SUPER_ADMIN ? "Super Admin" : "Admin"} + +
+
+
+ {admin.permissions.slice(0, 3).map(permissionKey => ( + + { + availablePermissions.find(p => p.key === permissionKey) + ?.label || permissionKey + } + + ))} + {admin.permissions.length > 3 && ( + + +{admin.permissions.length - 3} more + + )} +
+
+
+
+ {new Date(admin.createdAt).toLocaleDateString()} +
+ {admin.createdBy && ( +
+ by {admin.createdBy.username} +
+ )} +
+
+
+ + +
+
+ No admins found +
+
+
+ ); +} diff --git a/src/components/Admin/dashboardContent/adminManager/Toast.tsx b/src/components/Admin/dashboardContent/adminManager/Toast.tsx new file mode 100644 index 00000000..40fbb051 --- /dev/null +++ b/src/components/Admin/dashboardContent/adminManager/Toast.tsx @@ -0,0 +1,70 @@ +"use client"; + +// ─── CONSTANTS ─────────────────────────────────────────────────────────────── +// Toast types +const TOAST_SUCCESS: ToastType = "success"; +const TOAST_ERROR: ToastType = "error"; +const TOAST_WARNING: ToastType = "warning"; + +// Duration before auto-dismiss (in milliseconds) +const TOAST_DURATION_MS = 5000; + +// ─── IMPORTS ───────────────────────────────────────────────────────────────── +import { useEffect } from "react"; +import { AlertCircle, CheckCircle, XCircle } from "lucide-react"; + +// ─── TYPES ─────────────────────────────────────────────────────────────────── +export type ToastType = "success" | "error" | "warning"; + +interface ToastProps { + message: string; + type: ToastType; + onClose: () => void; +} + +// ─── COMPONENT ──────────────────────────────────────────────────────────────── +export default function Toast({ message, type, onClose }: ToastProps) { + // Determine background color based on toast type + const bgColor = + type === TOAST_SUCCESS + ? "bg-green-500" + : type === TOAST_ERROR + ? "bg-red-500" + : "bg-yellow-500"; + + // Select appropriate icon for each toast type + const icon = + type === TOAST_SUCCESS ? ( + + ) : type === TOAST_ERROR ? ( + + ) : ( + + ); + + // Automatically dismiss toast after TOAST_DURATION_MS + useEffect(() => { + const timer = setTimeout(onClose, TOAST_DURATION_MS); + return () => clearTimeout(timer); + }, [onClose]); + + return ( +
+ {/* Toast icon */} + {icon} + + {/* Toast message */} + {message} + + {/* Manual close button */} + +
+ ); +} From 30ef38796ce0365693d5eac0ce48f77e6bb72bc0 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 16:02:14 +0530 Subject: [PATCH 03/12] feat: Refactor SuspendedUsersContent component to improve structure and add constants --- .../dashboardContent/SuspendedUsersContent.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx b/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx index da5f599d..4a36b67e 100644 --- a/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx +++ b/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx @@ -1,6 +1,17 @@ -// Suspended Users page in admin dashboard -// This page displays a list of suspended users with search and unsuspend functionality -import React, { useEffect, useState, useMemo, useCallback } from "react"; +"use client"; + +// ─── TOP-LEVEL CONSTANTS ───────────────────────────────────────────────────── +// Number of users to show per page +const USERS_PER_PAGE = 10; +// Delay for debounced search (milliseconds) +const DEBOUNCE_DELAY = 300; + +import React, { + useEffect, + useState, + useMemo, + useCallback, +} from "react"; import { debounce } from "lodash-es"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; From 40d481a6d97a9f838dadf8c5b75027dcb453188e Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 16:04:35 +0530 Subject: [PATCH 04/12] feat: Refactor SuspendedUsersContent component for improved readability and structure --- .../SuspendedUsersContent.tsx | 531 +++++++----------- 1 file changed, 193 insertions(+), 338 deletions(-) diff --git a/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx b/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx index 4a36b67e..e0eaa53f 100644 --- a/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx +++ b/src/components/Admin/dashboardContent/SuspendedUsersContent.tsx @@ -16,7 +16,7 @@ import { debounce } from "lodash-es"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -// Types +// ─── TYPE DEFINITIONS ───────────────────────────────────────────────────────── interface SuspendedUser { _id: string; firstName?: string; @@ -27,7 +27,6 @@ interface SuspendedUser { avatar?: string; originalCreatedAt?: string; suspendedAt?: string; - suspendedBy?: string; suspensionReason?: string; suspensionNotes?: string; originalUserId?: string; @@ -65,85 +64,74 @@ interface DeleteModalProps { userName?: string; } -// Constants -const USERS_PER_PAGE = 10; -const DEBOUNCE_DELAY = 300; +// ─── HELPER FUNCTIONS ───────────────────────────────────────────────────────── +// Get initials from first and last name +const getInitials = (first: string, last: string): string => + `${(first[0] || "").toUpperCase()}${(last[0] || "").toUpperCase()}`; -// Helper functions -const getInitials = (firstName: string, lastName: string): string => { - const first = firstName || ""; - const last = lastName || ""; - return `${first[0] || ""}${last[0] || ""}`.toUpperCase(); -}; +// Format a 10-digit phone number as (123) 456-7890 +const formatPhoneNumber = (phone: string): string => + phone.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3"); -const formatPhoneNumber = (phone: string): string => { - if (!phone) return ""; - return phone.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3"); -}; +// Convert ISO date string to locale date +const formatDate = (iso: string): string => + new Date(iso).toLocaleDateString(); -const formatDate = (dateString: string): string => { - if (!dateString) return ""; - return new Date(dateString).toLocaleDateString(); -}; +// Convert ISO date string to locale date and time +const formatDateTime = (iso: string): string => + new Date(iso).toLocaleString(); -const formatDateTime = (dateString: string): string => { - if (!dateString) return ""; - return new Date(dateString).toLocaleString(); -}; +// ─── SUBCOMPONENTS ──────────────────────────────────────────────────────────── -// Components -const LoadingSkeleton: React.FC<{ count?: number }> = ({ - count = USERS_PER_PAGE, -}) => ( +// Skeleton loader while data is fetching +const LoadingSkeleton: React.FC<{ count?: number }> = ({ count = USERS_PER_PAGE }) => (
{[...Array(count)].map((_, i) => (
-
+
-
-
+
+
-
+
))}
); +// Error message display const ErrorMessage: React.FC<{ message: string }> = ({ message }) => ( -
- {message} +
+ {message}
); +// Empty state when no suspended users const EmptyState: React.FC = () => ( -
- 🚫 No suspended users found. +
+ No suspended users found.
); -const SearchInput: React.FC = ({ - value, - onChange, - onClear, -}) => ( +// Search input with clear button +const SearchInput: React.FC = ({ value, onChange, onClear }) => (
onChange(e.target.value)} + className="w-full pl-10 pr-10 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 transition" value={value} + onChange={e => onChange(e.target.value)} aria-label="Search suspended users" /> )}
); -const Pagination: React.FC = ({ - currentPage, - totalPages, - onPageChange, -}) => { +// Pagination controls with previous/next and page numbers +const Pagination: React.FC = ({ currentPage, totalPages, onPageChange }) => { const getPageNumbers = () => { - const pages = []; - const maxVisiblePages = 5; - let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); - - if (endPage - startPage + 1 < maxVisiblePages) { - startPage = Math.max(1, endPage - maxVisiblePages + 1); + const pages: number[] = []; + const maxVisible = 5; + let start = Math.max(1, currentPage - Math.floor(maxVisible / 2)); + let end = Math.min(totalPages, start + maxVisible - 1); + if (end - start + 1 < maxVisible) { + start = Math.max(1, end - maxVisible + 1); } - - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - + for (let i = start; i <= end; i++) pages.push(i); return pages; }; @@ -203,13 +178,12 @@ const Pagination: React.FC = ({ - - {getPageNumbers().map((page) => ( + {getPageNumbers().map(page => ( ))} - ); +// Button to delete permanently const DeleteButton: React.FC<{ onClick: () => void; label: string }> = ({ onClick, label, }) => ( ); +// Card layout for mobile view const SuspendedUserCard: React.FC = ({ user, onUnsuspend, onDelete, }) => ( -
+

- {user.firstName || "Unknown"} {user.lastName || "User"} + {user.firstName || "Unknown"} {user.lastName || ""} Suspended

- Email: {user.email || "N/A"} + Email: {user.email || "N/A"}

- Phone:{" "} - {user.phone ? formatPhoneNumber(user.phone) : "N/A"} + Phone: {user.phone ? formatPhoneNumber(user.phone) : "N/A"}

- Title: {user.title || "N/A"} -

-

- Reason:{" "} - {user.suspensionReason || "N/A"} -

-

- Originally joined:{" "} - {user.originalCreatedAt ? formatDate(user.originalCreatedAt) : "N/A"} + Reason: {user.suspensionReason || "N/A"}

- Suspended:{" "} - {user.suspendedAt ? formatDateTime(user.suspendedAt) : "N/A"} + Suspended at: {user.suspendedAt ? formatDateTime(user.suspendedAt) : "N/A"}

- {user.suspensionNotes && ( -

- Notes: {user.suspensionNotes} -

- )}
-
+
onUnsuspend(user._id)} - label={`Unsuspend ${user.firstName || "Unknown"} ${user.lastName || "User"}`} + label="Unsuspend user" /> onDelete(user._id)} - label={`Permanently delete ${user.firstName || "Unknown"} ${user.lastName || "User"}`} + label="Delete user" />
); +// Table row for desktop view const SuspendedUserTableRow: React.FC<{ user: SuspendedUser; - onUnsuspend: (userId: string) => void; - onDelete: (userId: string) => void; + onUnsuspend: (id: string) => void; + onDelete: (id: string) => void; }> = ({ user, onUnsuspend, onDelete }) => ( - - + + - - {user.firstName || "Unknown"} {user.lastName || "User"} + + {user.firstName || ""} {user.lastName || ""} Suspended - {user.email || "N/A"} - - {user.phone ? formatPhoneNumber(user.phone) : "N/A"} - - {user.title || "N/A"} - {user.suspensionReason || "N/A"} - - {user.originalCreatedAt ? formatDate(user.originalCreatedAt) : "N/A"} - - - {user.suspendedAt ? formatDateTime(user.suspendedAt) : "N/A"} - - -
- onUnsuspend(user._id)} - label={`Unsuspend ${user.firstName || "Unknown"} ${user.lastName || "User"}`} - /> - onDelete(user._id)} - label={`Permanently delete ${user.firstName || "Unknown"} ${user.lastName || "User"}`} - /> -
+ {user.email || "N/A"} + {user.phone ? formatPhoneNumber(user.phone) : "N/A"} + {user.title || "N/A"} + {user.suspensionReason || "N/A"} + {user.originalCreatedAt ? formatDate(user.originalCreatedAt) : "N/A"} + {user.suspendedAt ? formatDateTime(user.suspendedAt) : "N/A"} + + onUnsuspend(user._id)} label="Unsuspend" /> + onDelete(user._id)} label="Delete" /> ); +// Table layout for desktop const SuspendedUserTable: React.FC = ({ users, onUnsuspend, @@ -422,39 +344,21 @@ const SuspendedUserTable: React.FC = ({ return (
- + - - - - - - - - - + + + + + + + + + - {users.map((user) => ( + {users.map(user => ( = ({ ); }; +// Delete confirmation modal const DeleteModal: React.FC = ({ isOpen, onClose, @@ -475,32 +380,27 @@ const DeleteModal: React.FC = ({ userName, }) => { if (!isOpen) return null; - return ( -
+
-

+

Confirm Permanent Deletion

- Are you sure you want to permanently delete{" "} - {userName ? `suspended user ${userName}` : "this suspended user"}? - This action cannot be undone and all their data will be lost forever. + Permanently delete {userName || "this user"}? This cannot be undone.

@@ -508,182 +408,139 @@ const DeleteModal: React.FC = ({ ); }; +// ─── MAIN COMPONENT ─────────────────────────────────────────────────────────── const SuspendedUsersContent: React.FC = () => { - const [suspendedUsers, setSuspendedUsers] = useState([]); + const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [showModal, setShowModal] = useState(false); - const [userToDelete, setUserToDelete] = useState(null); + const [toDelete, setToDelete] = useState(null); - // Fetch suspended users + // Fetch data on mount useEffect(() => { - const fetchSuspendedUsers = async () => { + async function fetchData() { setLoading(true); setError(null); try { const res = await fetch("/api/suspended-users"); if (!res.ok) throw new Error("Failed to fetch suspended users"); const data = await res.json(); - setSuspendedUsers(data.data || []); + setUsers(data.data || []); } catch (err) { - const error = err as Error; - setError(error.message); - toast.error(error.message, { position: "top-right" }); + setError((err as Error).message); + toast.error((err as Error).message); } finally { setLoading(false); } - }; - - fetchSuspendedUsers(); + } + fetchData(); }, []); - // Handle unsuspend user - const handleUnsuspend = async (userId: string) => { - const user = suspendedUsers.find((u) => u._id === userId); + // Unsuspend handler + const handleUnsuspend = async (id: string) => { + const user = users.find(u => u._id === id); if (!user) return; - - const confirmUnsuspend = window.confirm( - `Are you sure you want to unsuspend ${user.firstName || "Unknown"} ${user.lastName || "User"}? They will be able to access their account again.` - ); - - if (!confirmUnsuspend) return; - + if (!confirm(`Unsuspend ${user.firstName} ${user.lastName}?`)) return; try { - const res = await fetch(`/api/suspended-users?userId=${userId}`, { - method: "PATCH", - }); - - if (!res.ok) throw new Error("Failed to unsuspend user"); - - setSuspendedUsers((prev) => prev.filter((u) => u._id !== userId)); - toast.success( - `User ${user.firstName || "Unknown"} ${user.lastName || "User"} has been unsuspended successfully and can now login with their original credentials.`, - { - position: "top-right", - autoClose: 5000, // Show for 5 seconds - } - ); + const res = await fetch(`/api/suspended-users?userId=${id}`, { method: "PATCH" }); + if (!res.ok) throw new Error("Failed to unsuspend"); + setUsers(prev => prev.filter(u => u._id !== id)); + toast.success("User unsuspended"); } catch (err) { - const error = err as Error; - toast.error(error.message, { position: "top-right" }); + toast.error((err as Error).message); } }; - // Handle delete suspended user + // Delete handler const handleDelete = async () => { - if (!userToDelete) return; - + if (!toDelete) return; try { - const res = await fetch( - `/api/suspended-users?userId=${userToDelete._id}`, - { - method: "DELETE", - } - ); - - if (!res.ok) throw new Error("Failed to delete suspended user"); - - setSuspendedUsers((prev) => - prev.filter((u) => u._id !== userToDelete._id) - ); - toast.success( - `Suspended user ${userToDelete.firstName || "Unknown"} ${userToDelete.lastName || "User"} deleted permanently`, - { - position: "top-right", - } - ); + const res = await fetch(`/api/suspended-users?userId=${toDelete._id}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error("Failed to delete"); + setUsers(prev => prev.filter(u => u._id !== toDelete._id)); + toast.success("User deleted"); } catch (err) { - const error = err as Error; - toast.error(error.message, { position: "top-right" }); + toast.error((err as Error).message); } finally { setShowModal(false); - setUserToDelete(null); + setToDelete(null); } }; - // Debounced search - const debouncedSearchHandler = useMemo( + // Open delete modal + const requestDelete = useCallback( + (id: string) => { + const user = users.find(u => u._id === id); + if (user) { + setToDelete(user); + setShowModal(true); + } + }, + [users] + ); + + // Debounced search setup + const debouncedSearch = useMemo( () => debounce((value: string) => { - setSearch(value); + setSearch(value.toLowerCase()); setPage(1); }, DEBOUNCE_DELAY), [] ); const handleSearchChange = useCallback( - (value: string) => { - debouncedSearchHandler(value); - }, - [debouncedSearchHandler] + (value: string) => debouncedSearch(value), + [debouncedSearch] ); const clearSearch = useCallback(() => { + debouncedSearch.cancel(); setSearch(""); setPage(1); - debouncedSearchHandler.cancel(); - }, [debouncedSearchHandler]); + }, [debouncedSearch]); // Filter and paginate users - const filteredUsers = useMemo(() => { - const searchTerm = search.toLowerCase(); - return suspendedUsers.filter( - (user) => - (user.firstName || "").toLowerCase().includes(searchTerm) || - (user.lastName || "").toLowerCase().includes(searchTerm) || - (user.email || "").toLowerCase().includes(searchTerm) || - (user.title || "").toLowerCase().includes(searchTerm) || - (user.suspensionReason || "").toLowerCase().includes(searchTerm) - ); - }, [suspendedUsers, search]); - - const totalPages = Math.ceil(filteredUsers.length / USERS_PER_PAGE) || 1; - const paginatedUsers = useMemo(() => { - return filteredUsers.slice( - (page - 1) * USERS_PER_PAGE, - page * USERS_PER_PAGE - ); - }, [filteredUsers, page]); - - // Reset page if it's out of bounds after filtering + const filtered = useMemo( + () => + users.filter(u => + [u.firstName, u.lastName, u.email, u.title, u.suspensionReason] + .join(" ") + .toLowerCase() + .includes(search) + ), + [users, search] + ); + + const totalPages = Math.max(1, Math.ceil(filtered.length / USERS_PER_PAGE)); + + const paginated = useMemo( + () => filtered.slice((page - 1) * USERS_PER_PAGE, page * USERS_PER_PAGE), + [filtered, page] + ); + + // Reset page if out of range useEffect(() => { if (page > totalPages) setPage(1); - }, [totalPages, page]); + }, [page, totalPages]); // Clean up debounce on unmount - useEffect(() => { - return () => { - debouncedSearchHandler.cancel(); - }; - }, [debouncedSearchHandler]); - - const handleDeleteClick = useCallback( - (userId: string) => { - const user = suspendedUsers.find((u) => u._id === userId); - if (user) { - setUserToDelete(user); - setShowModal(true); - } - }, - [suspendedUsers] - ); + useEffect(() => () => debouncedSearch.cancel(), [debouncedSearch]); return (
- {/* Header */} -
+ {/* Header with count and search */} +
-

- Suspended Users -

-

- {filteredUsers.length}{" "} - {filteredUsers.length === 1 ? "suspended user" : "suspended users"}{" "} - found +

Suspended Users

+

+ {filtered.length} user{filtered.length !== 1 && "s"} found

{ />
- {/* Content */} + {/* Content area */}
- {/* Desktop: Table */} + {/* Desktop table */}
{error ? ( ) : ( )}
- {/* Mobile: Cards */} + {/* Mobile cards */}
{loading ? ( ) : error ? ( - ) : paginatedUsers.length === 0 ? ( + ) : paginated.length === 0 ? ( ) : ( - paginatedUsers.map((user) => ( + paginated.map(user => ( )) )}
- {/* Pagination */} - {filteredUsers.length > USERS_PER_PAGE && ( -
+ {/* Pagination controls */} + {filtered.length > USERS_PER_PAGE && ( +
{
)} - {/* Delete Confirmation Modal */} + {/* Delete confirmation modal */} setShowModal(false)} onConfirm={handleDelete} userName={ - userToDelete - ? `${userToDelete.firstName} ${userToDelete.lastName}` - : undefined + toDelete ? `${toDelete.firstName} ${toDelete.lastName}` : undefined } />
From 078fc22107bed039a8ffb9fe257262fc42483ea6 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 16:16:23 +0530 Subject: [PATCH 05/12] feat: Update AdminSidebar to use NotebookTabs icon for Forum Reports --- src/components/Admin/AdminSidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Admin/AdminSidebar.tsx b/src/components/Admin/AdminSidebar.tsx index 9d3788f9..101b6aa2 100644 --- a/src/components/Admin/AdminSidebar.tsx +++ b/src/components/Admin/AdminSidebar.tsx @@ -17,6 +17,7 @@ import { Shield, Star, UserX, + NotebookTabs, } from "lucide-react"; import clsx from "clsx"; // Utility for conditional class names @@ -88,7 +89,7 @@ const navItems = [ { id: "forum-reports", label: "Forum Reports", - icon: Flag, + icon: NotebookTabs, permission: "manage_forum_reports", }, ]; From fbade216292d5aa7a65f21119f4cacb9f8feeff5 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 22:01:44 +0530 Subject: [PATCH 06/12] feat: Refactor reporting system with enhanced filtering and email handling - Updated GET request in reports route to use .lean() for better performance. - Improved AdminReports component by integrating Filters and ReportsTable components for better UI organization. - Enhanced error handling in fetchReports function. - Added userId field to KYCSchema for better user tracking. - Created Filters component for unified search, status filtering, and sorting. - Developed ReportsTable component to handle report display and actions. - Introduced types for AdminReport and related entities for better type safety. - Implemented format utility functions for consistent data formatting across the application. --- src/app/api/admin/reports/route.ts | 2 +- .../dashboardContent/ReportingContent.tsx | 671 +++++------------- .../dashboardContent/reporting/Filters.tsx | 69 ++ .../reporting/ReportDetailsModal.tsx | 0 .../reporting/ReportsTable.tsx | 296 ++++++++ .../Admin/dashboardContent/reporting/types.ts | 119 ++++ src/lib/models/KYCSchema.ts | 6 + src/utils/format.ts | 26 + 8 files changed, 677 insertions(+), 512 deletions(-) create mode 100644 src/components/Admin/dashboardContent/reporting/Filters.tsx create mode 100644 src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx create mode 100644 src/components/Admin/dashboardContent/reporting/ReportsTable.tsx create mode 100644 src/components/Admin/dashboardContent/reporting/types.ts create mode 100644 src/utils/format.ts diff --git a/src/app/api/admin/reports/route.ts b/src/app/api/admin/reports/route.ts index f2f09615..b046aaa2 100644 --- a/src/app/api/admin/reports/route.ts +++ b/src/app/api/admin/reports/route.ts @@ -20,7 +20,7 @@ export async function GET(req: Request) { } // Check if we can fetch without population first - const reportsBasic = await ReportInSession.find({}).limit(1); + const reportsBasic = await ReportInSession.find({}).limit(1).lean(); console.log( "Basic report sample:", JSON.stringify(reportsBasic[0], null, 2) diff --git a/src/components/Admin/dashboardContent/ReportingContent.tsx b/src/components/Admin/dashboardContent/ReportingContent.tsx index f7138aec..4b13fbe5 100644 --- a/src/components/Admin/dashboardContent/ReportingContent.tsx +++ b/src/components/Admin/dashboardContent/ReportingContent.tsx @@ -3,6 +3,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import toast from "react-hot-toast"; +import { Filters } from "./reporting/Filters"; +import { ReportsTable } from "./reporting/ReportsTable"; +import type { EmailFlow } from "./reporting/types"; + import { Mail, @@ -241,44 +245,23 @@ export default function AdminReports() { }; const fetchReports = async () => { - try { - setLoading(true); - const response = await fetch("/api/admin/reports"); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - console.log("Fetched reports data:", data); // Debug log - console.log("Sample report structure:", data[0]); // Debug log - - // More detailed debugging - if (data.length > 0) { - const sample = data[0]; - console.log("Report ID:", sample._id); - console.log("Reported By:", sample.reportedBy); - console.log("Reported User:", sample.reportedUser); - console.log("Session ID:", sample.sessionId); - console.log("Reason:", sample.reason); - console.log("Description:", sample.description); - console.log("Status:", sample.status); - console.log("Created At:", sample.createdAt); - } - - setReports(Array.isArray(data) ? data : []); - setError(null); - } catch (err) { - console.error("Error fetching reports:", err); - setError( - err instanceof Error - ? err.message - : "An error occurred while fetching reports" - ); - } finally { - setLoading(false); + try { + setLoading(true); + const res = await fetch("/api/admin/reports"); + if (!res.ok) { + const errorText = await res.text(); + console.error("🔴 API /admin/reports error body:", errorText); + throw new Error(`HTTP error! status: ${res.status}`); } - }; + const data = await res.json(); + setReports(data); + } catch (err) { + console.error("Error fetching reports:", err); + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } +}; const handleSendNotification = async (reportId: string) => { if ( @@ -561,23 +544,31 @@ SkillSwapHub Admin Team`; reportId: string, resolution: "warn_reported" | "warn_reporter" | "dismiss" ) => { - // For warning actions, we use the email client instead of backend API if (resolution === "warn_reported" || resolution === "warn_reporter") { - const report = reports.find((r) => r._id === reportId); - if (report) { - const userType = - resolution === "warn_reported" ? "reported" : "reporter"; - openWarningEmailClient(report, userType); - - const message = - resolution === "warn_reported" - ? "Warning email opened for reported user. Please send the email and then mark the report as resolved." - : "Warning email opened for reporting user. Please send the email and then mark the report as resolved."; - - alert(message); - } - return; - } + // map your underscore‐style resolution to the hyphen‐style EmailFlow + const flow: EmailFlow = + resolution === "warn_reported" ? "warn-reported" : "warn-reporter"; + + // 1) Look up the full AdminReport object by its ID +const rpt = reports.find((r) => r._id === reportId); +if (!rpt) { + console.warn("Could not find report with id", reportId); + return; +} + +// 2) Call the email‐flow helper with the real report object +openWarningEmailClient(rpt, flow); + +// 3) Let the admin know which warning was opened +alert( + flow === "warn-reported" + ? "Warning email opened for reported user…" + : "Warning email opened for reporting user…" +); +return; + +} + // Only dismiss action calls the backend API const resolutionMessages = { @@ -640,41 +631,45 @@ SkillSwapHub Admin Team`; } }; - const openWarningEmailClient = ( - report: AdminReport, - userType: "reporter" | "reported" - ) => { - const email = - userType === "reporter" - ? report.reportedBy?.email - : report.reportedUser?.email; - - if (!email) { - alert( - `No email address available for the ${userType === "reporter" ? "reporting" : "reported"} user.` - ); - return; - } - - const reporterName = formatName( - report.reportedBy?.firstName, - report.reportedBy?.lastName - ); - const reportedName = formatName( - report.reportedUser?.firstName, - report.reportedUser?.lastName - ); - const sessionTitle = getSessionTitle(report.sessionId); - const reason = formatReason(report.reason); - - let subject = ""; - let body = ""; - - if (userType === "reported") { - subject = `Warning: Platform Rules Violation - Report #${report._id.slice(-8)}`; + /** Kick off any of the four email flows: + * - initial-reporter + * - initial-reported + * - warn-reporter + * - warn-reported + */ +const openWarningEmailClient = ( + report: AdminReport, + flow: EmailFlow +) => { + const reporterEmail = report.reportedBy?.email; + const reportedEmail = report.reportedUser?.email; + const reporterName = formatName( + report.reportedBy?.firstName, + report.reportedBy?.lastName + ); + const reportedName = formatName( + report.reportedUser?.firstName, + report.reportedUser?.lastName + ); + const sessionTitle = getSessionTitle(report.sessionId); + const reason = formatReason(report.reason); + + let email: string | undefined; + let subject = ""; + let body = ""; + + switch (flow) { + case "initial-reporter": + email = reporterEmail; + subject = `Investigation Required – Report #${report._id.slice(-8)}`; + body = `Dear ${reporterName},\n\nThank you for your report about ${reportedName}. We’ve started investigating the reported user. Please reply with any additional details or evidence you might have.\n\nBest,\nAdmin Team`; + break; + case "initial-reported": + email = reportedEmail; + subject = `Investigation Notice – Report #${report._id.slice(-8)}`; body = `Dear ${reportedName}, -Following our investigation of a report filed against you, we have determined that your behavior during a session violated our platform guidelines. +We are writing to inform you that a report has been filed regarding your interaction on our platform. Report Details: - Session: ${sessionTitle} @@ -683,45 +678,40 @@ Report Details: - Report ID: ${report._id} - Date: ${formatDate(report.createdAt)} -This serves as an official warning. Please review our community guidelines and ensure your future interactions comply with our platform standards. - -Repeated violations may result in account restrictions or suspension. - -If you believe this warning was issued in error, please reply to this email with your explanation within 7 days. - -Best regards, -SkillSwapHub Admin Team`; - } else { - subject = `Warning: False Complaint Filed - Report #${report._id.slice(-8)}`; - body = `Dear ${reporterName}, - -Following our investigation of the report you filed, we have determined that your complaint appears to be unfounded or exaggerated. - -Report Details: -- Session: ${sessionTitle} -- Reported user: ${reportedName} -- Reason claimed: ${reason} -- Report ID: ${report._id} -- Date: ${formatDate(report.createdAt)} - -Our investigation found insufficient evidence to support your claims. Filing false or misleading reports undermines our platform's integrity and wastes administrative resources. - -This serves as an official warning. Please ensure that future reports are accurate and made in good faith. +As part of our investigation process, we would like to hear your side of the story. Please reply to this email with: -Repeated false reporting may result in account restrictions. +1. Your account of what happened during the session +2. Any relevant context or explanations +3. Any evidence or screenshots that support your version of events -If you have additional evidence to support your original claim, please reply to this email within 7 days. +You have 3 business days to respond to this email. We are committed to conducting a fair and thorough investigation. Best regards, SkillSwapHub Admin Team`; - } + break; + case "warn-reporter": + email = reporterEmail; + subject = `Warning: False Complaint – Report #${report._id.slice(-8)}`; + body = `Dear ${reporterName},\n\nOur investigation found insufficient evidence to support your report. Please refrain from filing false reports.\n\nBest,\nAdmin Team`; + break; + case "warn-reported": + email = reportedEmail; + subject = `Warning: Violation – Report #${report._id.slice(-8)}`; + body = `Dear ${reportedName},\n\nWe confirmed your behavior violated our guidelines. This is an official warning.\n\nBest,\nAdmin Team`; + break; + } - // Create mailto link - const mailtoLink = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + if (!email) { + alert("No email address available for this user."); + return; + } - // Open the default email client - window.location.href = mailtoLink; - }; + window.location.href = [ + `mailto:${encodeURIComponent(email)}`, + `?subject=${encodeURIComponent(subject)}`, + `&body=${encodeURIComponent(body)}`, + ].join(""); +}; useEffect(() => { fetchReports(); @@ -871,6 +861,15 @@ SkillSwapHub Admin Team`; const statusCounts = getStatusCounts(); + // new: options for our Filters dropdown + const statusOptions = [ + { value: "all", label: "All", count: statusCounts.all }, + { value: "pending", label: "Pending", count: statusCounts.pending }, + { value: "under_review", label: "Under Review", count: statusCounts.under_review }, + { value: "resolved", label: "Resolved", count: statusCounts.resolved }, + { value: "dismissed", label: "Dismissed", count: statusCounts.dismissed }, + ]; + // Toggle sort direction const toggleSortDirection = () => { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); @@ -971,397 +970,47 @@ SkillSwapHub Admin Team`;
- {/* Search Bar */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors" - /> - {searchQuery && ( - - )} -
-
- - {/* Filter Dropdown */} -
-
-
-
-

- Filter by Status: -

- -
-
- -
- {/* Sort direction toggle button */} - -
-
-
- - {filteredReports.length === 0 ? ( -
- -

- {reports.length === 0 - ? "No Reports Found" - : searchQuery - ? `No Results Found` - : `No ${formatStatus(statusFilter)} Reports`} -

-

- {reports.length === 0 - ? "All user reports will appear here when submitted" - : searchQuery - ? `No reports match your search "${searchQuery}". Try adjusting your search terms or filters.` - : `No reports with status "${formatStatus(statusFilter)}" found. Try selecting a different filter.`} -

- {reports.length === 0 && ( -
- - -
- )} -
- ) : ( -
-
- Avatar - - Name - - Email - - Phone - - Title - - Reason - - Originally Joined - - Suspended - - Actions - AvatarNameEmailPhoneTitleReasonJoinedSuspendedActions
- - - - - - - - - - - - {filteredReports.map((report) => ( - - - - - - - - - ))} - -
Reporting UserReported UserReasonStatus -
- Created At - -
-
Actions
-
-
-
- {formatName( - report.reportedBy?.firstName, - report.reportedBy?.lastName - )} -
-
- {report.reportedBy?.email || "No email available"} -
-
- - - {/* download evidence */} - {report.evidenceFiles.length > 0 && ( -
- - {report.evidenceFiles.length > 1 && ( - - {report.evidenceFiles.length} - - )} -
- )} -
-
-
-
-
- {formatName( - report.reportedUser?.firstName, - report.reportedUser?.lastName - )} -
-
- {report.reportedUser?.email || - "No email available"} -
-
- -
-
-
- {formatReason(report.reason)} -
-
-
- - {formatStatus(report.status)} - -
-
-
- {formatDate(report.createdAt)} -
-
-
- - - {report.status === "pending" && ( - - )} - - {report.status === "under_review" && ( - <> - - - - - - )} - - {report.status === "resolved" && ( - - )} -
-
-
- )} + + {/* unified search + status + sort UI */} + + + +{filteredReports.length === 0 ? ( +
+ {/* …your existing “no results” UI… */} +
+) : ( + +)} @@ -1565,7 +1214,7 @@ SkillSwapHub Admin Team`; +
+ ); +} diff --git a/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx b/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Admin/dashboardContent/reporting/ReportsTable.tsx b/src/components/Admin/dashboardContent/reporting/ReportsTable.tsx new file mode 100644 index 00000000..70125b15 --- /dev/null +++ b/src/components/Admin/dashboardContent/reporting/ReportsTable.tsx @@ -0,0 +1,296 @@ +// src/components/Admin/dashboardContent/reporting/ReportsTable.tsx + +import React from "react"; +import { + Mail, + Loader2, + AlertOctagon, + ShieldX, + X, + CheckCircle, + Download, + Eye, + SortAsc, + SortDesc, +} from "lucide-react"; +import type { ReportsTableProps } from "./types"; + +export function ReportsTable({ + reports, + sortDirection, + onToggleSort, + downloading, + onDownloadEvidence, + sendingEmails, + onSendNotification, + onSendNotificationToReporter, + onSendNotificationToReported, + onOpenWarningEmail, + resolvingReport, + onResolve, + onMarkResolved, + onViewDetails, + formatName, + formatReason, + formatDate, + getStatusColor, +}: ReportsTableProps) { + return ( +
+ + + + + + + + + + + + + {reports.map((report) => ( + + {/* Reporting User */} + + + {/* Reported User */} + + + {/* Reason */} + + + {/* Status */} + + + {/* Created At */} + + + {/* Actions */} + + + ))} + +
Reporting UserReported UserReasonStatus +
+ Created At + +
+
Actions
+
+
+
+ {formatName( + report.reportedBy?.firstName, + report.reportedBy?.lastName + )} +
+
+ {report.reportedBy?.email ?? "No email"} +
+
+ {/* ask the reporter for more info */} + +
+
+
+
+
+ {formatName( + report.reportedUser?.firstName, + report.reportedUser?.lastName + )} +
+
+ {report.reportedUser?.email ?? "No email"} +
+
+ {/* ask the reported user for their side */} + +
+
+
+ {formatReason(report.reason)} +
+
+ + {report.status + .replace(/_/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase())} + + +
+ {formatDate(report.createdAt)} +
+
+
+ {/* view details */} + + + {report.status === "pending" && ( + + )} + + {report.status === "under_review" && ( + <> + {/* warn reported user */} + + {/* warn reporting user */} + + {/* dismiss */} + + {/* mark solved */} + + + )} + + {report.status === "resolved" && ( + + )} + + {/* download evidence */} + {report.evidenceFiles.length > 0 && ( +
+ + {report.evidenceFiles.length > 1 && ( + + {report.evidenceFiles.length} + + )} +
+ )} +
+
+
+ ); +} diff --git a/src/components/Admin/dashboardContent/reporting/types.ts b/src/components/Admin/dashboardContent/reporting/types.ts new file mode 100644 index 00000000..5611dec6 --- /dev/null +++ b/src/components/Admin/dashboardContent/reporting/types.ts @@ -0,0 +1,119 @@ +// src/components/Admin/dashboardContent/reporting/types.ts + +/** + * A user who either reported or was reported. + */ +export interface AdminReportUser { + _id: string; + firstName: string; + lastName: string; + email: string; +} + +/** + * The session in which the report was filed. + */ +export interface AdminReportSession { + _id: string; + user1Id: string; + user2Id: string; + skill1Id: string; + skill2Id: string; + descriptionOfService1: string; + descriptionOfService2: string; + startDate: string; + status: string; +} + +/** + * The shape of a report. + */ +export interface AdminReport { + _id: string; + sessionId: AdminReportSession | null; + reportedBy: AdminReportUser | null; + reportedUser: AdminReportUser | null; + + reason: + | "not_submitting_work" + | "not_responsive" + | "poor_quality_work" + | "inappropriate_behavior" + | "not_following_session_terms" + | "other"; + + description: string; + evidenceFiles: string[]; + + // Auto‐collected + reportedUserLastActive?: string; + reportedUserWorksCount: number; + reportingUserWorksCount: number; + reportedUserWorksDetails: { + workId: string; + submissionDate: string; + status: "pending" | "accepted" | "rejected"; + }[]; + reportingUserWorksDetails: { + workId: string; + submissionDate: string; + status: "pending" | "accepted" | "rejected"; + }[]; + + status: "pending" | "under_review" | "resolved" | "dismissed"; + adminResponse?: string; + adminId?: string; + resolvedAt?: string; + createdAt: string; + updatedAt: string; +} + +/** + * All four email‐flow modes: + * • initial-reporter – ask the reporter for more info + * • initial-reported – ask the reported user for their side + * • warn-reporter – warn the reporter (“false complaint”) + * • warn-reported – warn the reported user (“violation”) + */ +export type EmailFlow = + | "initial-reporter" + | "initial-reported" + | "warn-reporter" + | "warn-reported"; + +/** + * Props for the ReportsTable component. + */ +export interface ReportsTableProps { + /** Which reports to render */ + reports: AdminReport[]; + + /** "asc" or "desc" */ + sortDirection: "asc" | "desc"; + onToggleSort: () => void; + + downloading: Record; + onDownloadEvidence: (fileUrl: string, reportId: string) => Promise; + + sendingEmails: string | null; + onSendNotification: (reportId: string) => Promise; + onSendNotificationToReporter: (reportId: string) => Promise; + onSendNotificationToReported: (reportId: string) => Promise; + + /** Kick off any of the four email flows */ + onOpenWarningEmail: (report: AdminReport, flow: EmailFlow) => void; + + resolvingReport: string | null; + onResolve: ( + reportId: string, + resolution: "dismiss" + ) => Promise; + onMarkResolved: (reportId: string) => Promise; + + onViewDetails: (report: AdminReport) => void; + + formatName: (first: string | undefined, last: string | undefined) => string; + formatReason: (reason: string) => string; + formatDate: (dateString: string) => string; + getStatusColor: (status: AdminReport["status"]) => string; +} diff --git a/src/lib/models/KYCSchema.ts b/src/lib/models/KYCSchema.ts index 3dfec83c..b429f27c 100644 --- a/src/lib/models/KYCSchema.ts +++ b/src/lib/models/KYCSchema.ts @@ -8,6 +8,12 @@ const KYCSchema = new mongoose.Schema({ // National Identity Card number nic: { type: String, required: true }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + // User ID or email of the person submitting KYC recipient: { type: String, required: true }, diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 00000000..d39c1272 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,26 @@ +// src/utils/format.ts + +export function formatName(firstName?: string, lastName?: string): string { + if (!firstName && !lastName) return "(Unknown)"; + return [firstName, lastName].filter(Boolean).join(" "); +} + +export function formatStatus(status?: string): string { + if (!status) return "Unknown"; + switch (status) { + case "pending": return "Pending"; + case "under_review": return "Under Review"; + case "resolved": return "Resolved"; + default: return status[0].toUpperCase() + status.slice(1); + } +} + +export function formatDate(date?: string): string { + if (!date) return ""; + return new Date(date).toLocaleString(); +} + +export function formatReason(reason?: string): string { + if (!reason) return "N/A"; + return reason[0].toUpperCase() + reason.slice(1); +} From d32d23936597080de5ede1bb9a1c099759c1098e Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 22:14:44 +0530 Subject: [PATCH 07/12] feat: Add hasInstallScript property to package-lock.json --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index f0b71a79..2f679b9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "skill-swap-hub", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "^3.744.0", "@aws-sdk/s3-request-presigner": "^3.744.0", From d4127a99d0ddd408854fd6191effdf6286105207 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Wed, 16 Jul 2025 23:51:11 +0530 Subject: [PATCH 08/12] feat: Add ReportDetailsModal component and enhance report filtering functionality --- .../dashboardContent/ReportingContent.tsx | 651 +++++------------- .../dashboardContent/reporting/Filters.tsx | 2 +- .../reporting/ReportDetailsModal.tsx | 203 ++++++ 3 files changed, 362 insertions(+), 494 deletions(-) diff --git a/src/components/Admin/dashboardContent/ReportingContent.tsx b/src/components/Admin/dashboardContent/ReportingContent.tsx index 4b13fbe5..cb966d86 100644 --- a/src/components/Admin/dashboardContent/ReportingContent.tsx +++ b/src/components/Admin/dashboardContent/ReportingContent.tsx @@ -1,12 +1,11 @@ import React, { useState, useEffect, useMemo } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import toast from "react-hot-toast"; import { Filters } from "./reporting/Filters"; import { ReportsTable } from "./reporting/ReportsTable"; import type { EmailFlow } from "./reporting/types"; - +import { ReportDetailsModal } from "./reporting/ReportDetailsModal"; import { Mail, @@ -245,23 +244,23 @@ export default function AdminReports() { }; const fetchReports = async () => { - try { - setLoading(true); - const res = await fetch("/api/admin/reports"); - if (!res.ok) { - const errorText = await res.text(); - console.error("🔴 API /admin/reports error body:", errorText); - throw new Error(`HTTP error! status: ${res.status}`); + try { + setLoading(true); + const res = await fetch("/api/admin/reports"); + if (!res.ok) { + const errorText = await res.text(); + console.error("🔴 API /admin/reports error body:", errorText); + throw new Error(`HTTP error! status: ${res.status}`); + } + const data = await res.json(); + setReports(data); + } catch (err) { + console.error("Error fetching reports:", err); + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); } - const data = await res.json(); - setReports(data); - } catch (err) { - console.error("Error fetching reports:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } -}; + }; const handleSendNotification = async (reportId: string) => { if ( @@ -404,84 +403,6 @@ export default function AdminReports() { } }; - const openEmailClient = ( - email: string | undefined, - userType: "reporter" | "reported", - report: AdminReport - ) => { - if (!email) { - alert("No email address available for this user."); - return; - } - - const reporterName = formatName( - report.reportedBy?.firstName, - report.reportedBy?.lastName - ); - const reportedName = formatName( - report.reportedUser?.firstName, - report.reportedUser?.lastName - ); - const sessionTitle = getSessionTitle(report.sessionId); - const reason = formatReason(report.reason); - - let subject = ""; - let body = ""; - - if (userType === "reporter") { - subject = `Investigation Required - Report #${report._id.slice(-8)}`; - body = `Dear ${reporterName}, - -Thank you for reporting an incident on our platform. We are following up on your report regarding ${reportedName}. - -Report Details: -- Session: ${sessionTitle} -- Reason: ${reason} -- Report ID: ${report._id} -- Date: ${formatDate(report.createdAt)} - -We would appreciate if you could provide any additional information that might help us investigate this matter thoroughly. Please reply to this email with: - -1. Any additional details about the incident -2. Screenshots or evidence if available -3. Any other relevant information - -We take all reports seriously and will investigate this matter within 3 business days. - -Best regards, -SkillSwapHub Admin Team`; - } else { - subject = `Investigation Notice - Report #${report._id.slice(-8)}`; - body = `Dear ${reportedName}, - -We are writing to inform you that a report has been filed regarding your interaction on our platform. - -Report Details: -- Session: ${sessionTitle} -- Reported by: ${reporterName} -- Reason: ${reason} -- Report ID: ${report._id} -- Date: ${formatDate(report.createdAt)} - -As part of our investigation process, we would like to hear your side of the story. Please reply to this email with: - -1. Your account of what happened during the session -2. Any relevant context or explanations -3. Any evidence or screenshots that support your version of events - -You have 3 business days to respond to this email. We are committed to conducting a fair and thorough investigation. - -Best regards, -SkillSwapHub Admin Team`; - } - - // Create mailto link - const mailtoLink = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; - - // Open the default email client - window.location.href = mailtoLink; - }; - const markAsResolved = async (reportId: string) => { if ( !confirm( @@ -545,30 +466,28 @@ SkillSwapHub Admin Team`; resolution: "warn_reported" | "warn_reporter" | "dismiss" ) => { if (resolution === "warn_reported" || resolution === "warn_reporter") { - // map your underscore‐style resolution to the hyphen‐style EmailFlow - const flow: EmailFlow = - resolution === "warn_reported" ? "warn-reported" : "warn-reporter"; - - // 1) Look up the full AdminReport object by its ID -const rpt = reports.find((r) => r._id === reportId); -if (!rpt) { - console.warn("Could not find report with id", reportId); - return; -} - -// 2) Call the email‐flow helper with the real report object -openWarningEmailClient(rpt, flow); - -// 3) Let the admin know which warning was opened -alert( - flow === "warn-reported" - ? "Warning email opened for reported user…" - : "Warning email opened for reporting user…" -); -return; + // map your underscore‐style resolution to the hyphen‐style EmailFlow + const flow: EmailFlow = + resolution === "warn_reported" ? "warn-reported" : "warn-reporter"; + + // 1) Look up the full AdminReport object by its ID + const rpt = reports.find((r) => r._id === reportId); + if (!rpt) { + console.warn("Could not find report with id", reportId); + return; + } -} + // 2) Call the email‐flow helper with the real report object + openWarningEmailClient(rpt, flow); + // 3) Let the admin know which warning was opened + alert( + flow === "warn-reported" + ? "Warning email opened for reported user…" + : "Warning email opened for reporting user…" + ); + return; + } // Only dismiss action calls the backend API const resolutionMessages = { @@ -613,7 +532,10 @@ return; setReports((prevReports) => prevReports.map((report) => report._id === reportId - ? { ...report, status: "resolved" as const } + ? { + ...report, + status: resolution === "dismiss" ? "dismissed" : "resolved", + } : report ) ); @@ -632,42 +554,39 @@ return; }; /** Kick off any of the four email flows: - * - initial-reporter - * - initial-reported - * - warn-reporter - * - warn-reported - */ -const openWarningEmailClient = ( - report: AdminReport, - flow: EmailFlow -) => { - const reporterEmail = report.reportedBy?.email; - const reportedEmail = report.reportedUser?.email; - const reporterName = formatName( - report.reportedBy?.firstName, - report.reportedBy?.lastName - ); - const reportedName = formatName( - report.reportedUser?.firstName, - report.reportedUser?.lastName - ); - const sessionTitle = getSessionTitle(report.sessionId); - const reason = formatReason(report.reason); - - let email: string | undefined; - let subject = ""; - let body = ""; - - switch (flow) { - case "initial-reporter": - email = reporterEmail; - subject = `Investigation Required – Report #${report._id.slice(-8)}`; - body = `Dear ${reporterName},\n\nThank you for your report about ${reportedName}. We’ve started investigating the reported user. Please reply with any additional details or evidence you might have.\n\nBest,\nAdmin Team`; - break; - case "initial-reported": - email = reportedEmail; - subject = `Investigation Notice – Report #${report._id.slice(-8)}`; - body = `Dear ${reportedName}, + * - initial-reporter + * - initial-reported + * - warn-reporter + * - warn-reported + */ + const openWarningEmailClient = (report: AdminReport, flow: EmailFlow) => { + const reporterEmail = report.reportedBy?.email; + const reportedEmail = report.reportedUser?.email; + const reporterName = formatName( + report.reportedBy?.firstName, + report.reportedBy?.lastName + ); + const reportedName = formatName( + report.reportedUser?.firstName, + report.reportedUser?.lastName + ); + const sessionTitle = getSessionTitle(report.sessionId); + const reason = formatReason(report.reason); + + let email: string | undefined; + let subject = ""; + let body = ""; + + switch (flow) { + case "initial-reporter": + email = reporterEmail; + subject = `Investigation Required – Report #${report._id.slice(-8)}`; + body = `Dear ${reporterName},\n\nThank you for your report about ${reportedName}. We’ve started investigating the reported user. Please reply with any additional details or evidence you might have.\n\nBest,\nAdmin Team`; + break; + case "initial-reported": + email = reportedEmail; + subject = `Investigation Notice – Report #${report._id.slice(-8)}`; + body = `Dear ${reportedName}, We are writing to inform you that a report has been filed regarding your interaction on our platform. @@ -688,30 +607,30 @@ You have 3 business days to respond to this email. We are committed to conductin Best regards, SkillSwapHub Admin Team`; - break; - case "warn-reporter": - email = reporterEmail; - subject = `Warning: False Complaint – Report #${report._id.slice(-8)}`; - body = `Dear ${reporterName},\n\nOur investigation found insufficient evidence to support your report. Please refrain from filing false reports.\n\nBest,\nAdmin Team`; - break; - case "warn-reported": - email = reportedEmail; - subject = `Warning: Violation – Report #${report._id.slice(-8)}`; - body = `Dear ${reportedName},\n\nWe confirmed your behavior violated our guidelines. This is an official warning.\n\nBest,\nAdmin Team`; - break; - } + break; + case "warn-reporter": + email = reporterEmail; + subject = `Warning: False Complaint – Report #${report._id.slice(-8)}`; + body = `Dear ${reporterName},\n\nOur investigation found insufficient evidence to support your report. Please refrain from filing false reports.\n\nBest,\nAdmin Team`; + break; + case "warn-reported": + email = reportedEmail; + subject = `Warning: Violation – Report #${report._id.slice(-8)}`; + body = `Dear ${reportedName},\n\nWe confirmed your behavior violated our guidelines. This is an official warning.\n\nBest,\nAdmin Team`; + break; + } - if (!email) { - alert("No email address available for this user."); - return; - } + if (!email) { + alert("No email address available for this user."); + return; + } - window.location.href = [ - `mailto:${encodeURIComponent(email)}`, - `?subject=${encodeURIComponent(subject)}`, - `&body=${encodeURIComponent(body)}`, - ].join(""); -}; + window.location.href = [ + `mailto:${encodeURIComponent(email)}`, + `?subject=${encodeURIComponent(subject)}`, + `&body=${encodeURIComponent(body)}`, + ].join(""); + }; useEffect(() => { fetchReports(); @@ -861,13 +780,17 @@ SkillSwapHub Admin Team`; const statusCounts = getStatusCounts(); - // new: options for our Filters dropdown - const statusOptions = [ - { value: "all", label: "All", count: statusCounts.all }, - { value: "pending", label: "Pending", count: statusCounts.pending }, - { value: "under_review", label: "Under Review", count: statusCounts.under_review }, - { value: "resolved", label: "Resolved", count: statusCounts.resolved }, - { value: "dismissed", label: "Dismissed", count: statusCounts.dismissed }, + // new: options for our Filters dropdown + const statusOptions = [ + { value: "all", label: "All", count: statusCounts.all }, + { value: "pending", label: "Pending", count: statusCounts.pending }, + { + value: "under_review", + label: "Under Review", + count: statusCounts.under_review, + }, + { value: "resolved", label: "Resolved", count: statusCounts.resolved }, + { value: "dismissed", label: "Dismissed", count: statusCounts.dismissed }, ]; // Toggle sort direction @@ -970,319 +893,61 @@ SkillSwapHub Admin Team`;
- - {/* unified search + status + sort UI */} - - - -{filteredReports.length === 0 ? ( -
- {/* …your existing “no results” UI… */} -
-) : ( - -)} + {/* unified search + status + sort UI */} + + + {filteredReports.length === 0 ? ( +
+ {/* …your existing “no results” UI… */} +
+ ) : ( + + )}
- {/* Report Details Modal */} {selectedReport && ( -
-
-
-
-

- Report Details -

- -
-
- -
-
-
-

- 👤 Reporting User -

-
-

- {formatName( - selectedReport.reportedBy?.firstName, - selectedReport.reportedBy?.lastName - )} -

-

- 📧{" "} - {selectedReport.reportedBy?.email || "No email available"} -

-
-
- -
-

- 🚨 Reported User -

-
-

- {formatName( - selectedReport.reportedUser?.firstName, - selectedReport.reportedUser?.lastName - )} -

-

- 📧{" "} - {selectedReport.reportedUser?.email || - "No email available"} -

-
-
-
- -
-

- 🔄 Session Details -

-
-

- {getSessionTitle(selectedReport.sessionId)} -

-
-

- Session ID:{" "} - {selectedReport.sessionId?._id || "Not Available"} -

- {selectedReport.sessionId?.descriptionOfService1 && ( -

- Service 1:{" "} - {selectedReport.sessionId.descriptionOfService1} -

- )} - {selectedReport.sessionId?.descriptionOfService2 && ( -

- Service 2:{" "} - {selectedReport.sessionId.descriptionOfService2} -

- )} - {!selectedReport.sessionId && ( -

- ⚠️ Session data not available or not populated -

- )} -
-
-
- -
-

- ⚠️ Report Reason -

-

- {formatReason(selectedReport.reason)} -

-
- -
-

- 📝 Detailed Description -

-
-

- {selectedReport.description} -

-
-
- - {/* Evidence Files Section */} - {selectedReport.evidenceFiles && - selectedReport.evidenceFiles.length > 0 && ( -
-

- 📎 Evidence Files -

-
- {selectedReport.evidenceFiles.map((fileUrl, index) => ( -
-
-
- -
-
-

- Evidence File {index + 1} -

-

- {fileUrl.split("/").pop() || "Unknown file"} -

-
-
- -
- ))} -
-
- )} - -
-
-

- Current Status -

- - {formatStatus(selectedReport.status)} - -
-
-

- 📅 Report Submitted -

-

- {formatDate(selectedReport.createdAt)} -

-
-
- - {/* Resolution Actions */} - {selectedReport.status === "under_review" && ( -
-

- 🎯 Resolve Report -

-

- Choose the appropriate resolution based on your - investigation: -

-
- - - -
- -
-
- )} - - {selectedReport.status === "resolved" && ( -
-

- - Report Resolved -

-

- This report has been successfully resolved and appropriate - action has been taken. -

-
- )} -
-
-
+ setSelectedReport(null)} + onResolve={resolveReport} + onMarkResolved={markAsResolved} + openWarningEmail={openWarningEmailClient} + downloadEvidence={downloadEvidence} + downloading={downloading} + formatName={formatName} + formatDate={formatDate} + formatReason={formatReason} + getSessionTitle={getSessionTitle} + getStatusColor={getStatusColor} + /> )}
); diff --git a/src/components/Admin/dashboardContent/reporting/Filters.tsx b/src/components/Admin/dashboardContent/reporting/Filters.tsx index c4ef3d5d..4ed4fcd6 100644 --- a/src/components/Admin/dashboardContent/reporting/Filters.tsx +++ b/src/components/Admin/dashboardContent/reporting/Filters.tsx @@ -29,7 +29,7 @@ export function Filters({ onSearchChange(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors" diff --git a/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx b/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx index e69de29b..ec07536d 100644 --- a/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx +++ b/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx @@ -0,0 +1,203 @@ +// src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + AlertOctagon, + ShieldX, + X, + CheckCircle, + Download, + Loader2, +} from "lucide-react"; +import type { AdminReport, EmailFlow } from "./types"; + +interface ReportDetailsModalProps { + report: AdminReport; + onClose: () => void; + onResolve: ( + id: string, + resolution: "dismiss" | "warn_reported" | "warn_reporter" + ) => void; + onMarkResolved: (id: string) => void; + openWarningEmail: (r: AdminReport, flow: EmailFlow) => void; + downloadEvidence: (url: string, id: string) => Promise; + downloading: Record; + formatName: (first?: string, last?: string) => string; + formatDate: (s: string) => string; + formatReason: (r: string) => string; + getSessionTitle: (s: AdminReport["sessionId"]) => string; + getStatusColor: (status: AdminReport["status"]) => string; +} +export function ReportDetailsModal({ + report, + onClose, + onResolve, + onMarkResolved, + openWarningEmail, + downloadEvidence, + downloading, + formatName, + formatDate, + formatReason, + getSessionTitle, + getStatusColor, +}: ReportDetailsModalProps) { + return ( +
+
+
+
+

Report Details

+

+ Report ID: {report._id} +

+
+ +
+ +
+ {/* Reporting & Reported users */} +
+ {/* Reporting User */} +
+

👤 Reporting User

+

+ {formatName( + report.reportedBy?.firstName, + report.reportedBy?.lastName + )} +

+

+ 📧 {report.reportedBy?.email || "No email"} +

+
+ {/* Reported User */} +
+

🚨 Reported User

+

+ {formatName( + report.reportedUser?.firstName, + report.reportedUser?.lastName + )} +

+

+ 📧 {report.reportedUser?.email || "No email"} +

+
+
+ + {/* Session Details */} +
+

🔄 Session Details

+

{getSessionTitle(report.sessionId)}

+
+

+ Session ID: {report.sessionId?._id || "N/A"} +

+ {report.sessionId?.descriptionOfService1 && ( +

+ Service 1:{" "} + {report.sessionId.descriptionOfService1} +

+ )} + {report.sessionId?.descriptionOfService2 && ( +

+ Service 2:{" "} + {report.sessionId.descriptionOfService2} +

+ )} +
+
+ + {/* Reason, Description, Evidence */} +
+

⚠️ Report Reason

+

{formatReason(report.reason)}

+
+
+

📝 Detailed Description

+
{report.description}
+
+ {report.evidenceFiles.length > 0 && ( +
+

📎 Evidence Files

+
    + {report.evidenceFiles.map((url, i) => ( +
  • +
    +

    Evidence File {i + 1}

    +

    + {url.split("/").pop()} +

    +
    + +
  • + ))} +
+
+ )} + + {/* Status + Resolve Actions */} +
+ + {report.status.replace(/_/g, " ")} + + {report.status === "under_review" ? ( +
+ + + + +
+ ) : ( + + Submitted {formatDate(report.createdAt)} + + )} +
+
+
+
+ ); +} From c96d763e0cee69f1584e2590982d778af1f90ef9 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Thu, 17 Jul 2025 11:35:42 +0530 Subject: [PATCH 09/12] feat: Refactor ReportDetailsModal to use string constants for user-visible text --- .../dashboardContent/ReportingContent.tsx | 16 +--- .../reporting/ReportDetailsModal.tsx | 80 +++++++++++++------ 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/src/components/Admin/dashboardContent/ReportingContent.tsx b/src/components/Admin/dashboardContent/ReportingContent.tsx index cb966d86..2fc9a7f6 100644 --- a/src/components/Admin/dashboardContent/ReportingContent.tsx +++ b/src/components/Admin/dashboardContent/ReportingContent.tsx @@ -7,21 +7,7 @@ import { ReportsTable } from "./reporting/ReportsTable"; import type { EmailFlow } from "./reporting/types"; import { ReportDetailsModal } from "./reporting/ReportDetailsModal"; -import { - Mail, - Loader2, - AlertCircle, - RefreshCw, - Eye, - CheckCircle, - Download, - X, - ShieldX, - AlertOctagon, - Search, - SortAsc, - SortDesc, -} from "lucide-react"; +import { Mail, Loader2, AlertCircle, RefreshCw } from "lucide-react"; // Local interfaces to ensure TypeScript compatibility interface AdminReportUser { _id: string; diff --git a/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx b/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx index ec07536d..81ed148e 100644 --- a/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx +++ b/src/components/Admin/dashboardContent/reporting/ReportDetailsModal.tsx @@ -12,6 +12,29 @@ import { } from "lucide-react"; import type { AdminReport, EmailFlow } from "./types"; +// String constants for all user-visible text +const MODAL_TITLE = "Report Details"; +const REPORT_ID_LABEL = "Report ID:"; +const CLOSE_BUTTON_TEXT = "✕ Close"; +const REPORTING_USER_TITLE = "👤 Reporting User"; +const REPORTED_USER_TITLE = "🚨 Reported User"; +const SESSION_DETAILS_TITLE = "🔄 Session Details"; +const REPORT_REASON_TITLE = "⚠️ Report Reason"; +const DESCRIPTION_TITLE = "📝 Detailed Description"; +const EVIDENCE_FILES_TITLE = "📎 Evidence Files"; +const NO_EMAIL_FALLBACK = "No email"; +const SESSION_ID_LABEL = "Session ID:"; +const SERVICE_1_LABEL = "Service 1:"; +const SERVICE_2_LABEL = "Service 2:"; +const SESSION_ID_FALLBACK = "N/A"; +const EVIDENCE_FILE_PREFIX = "Evidence File "; +const WARN_REPORTED_BUTTON = "Warn Reported"; +const WARN_REPORTER_BUTTON = "Warn Reporter"; +const DISMISS_BUTTON = "Dismiss"; +const RESOLVE_BUTTON = "Resolve"; +const SUBMITTED_PREFIX = "Submitted "; +const EMAIL_ICON = "📧 "; + interface ReportDetailsModalProps { report: AdminReport; onClose: () => void; @@ -29,6 +52,7 @@ interface ReportDetailsModalProps { getSessionTitle: (s: AdminReport["sessionId"]) => string; getStatusColor: (status: AdminReport["status"]) => string; } + export function ReportDetailsModal({ report, onClose, @@ -46,24 +70,27 @@ export function ReportDetailsModal({ return (
+ + {/* HEADER - Modal title and close button */}
-

Report Details

+

{MODAL_TITLE}

- Report ID: {report._id} + {REPORT_ID_LABEL} {report._id}

- {/* Reporting & Reported users */} + + {/* USERS SECTION - Information about reporting and reported users */}
{/* Reporting User */}
-

👤 Reporting User

+

{REPORTING_USER_TITLE}

{formatName( report.reportedBy?.firstName, @@ -71,12 +98,13 @@ export function ReportDetailsModal({ )}

- 📧 {report.reportedBy?.email || "No email"} + {EMAIL_ICON}{report.reportedBy?.email || NO_EMAIL_FALLBACK}

+ {/* Reported User */}
-

🚨 Reported User

+

{REPORTED_USER_TITLE}

{formatName( report.reportedUser?.firstName, @@ -84,46 +112,48 @@ export function ReportDetailsModal({ )}

- 📧 {report.reportedUser?.email || "No email"} + {EMAIL_ICON}{report.reportedUser?.email || NO_EMAIL_FALLBACK}

- {/* Session Details */} + {/* SESSION DETAILS - Information about the session where the report occurred */}
-

🔄 Session Details

+

{SESSION_DETAILS_TITLE}

{getSessionTitle(report.sessionId)}

- Session ID: {report.sessionId?._id || "N/A"} + {SESSION_ID_LABEL} {report.sessionId?._id || SESSION_ID_FALLBACK}

{report.sessionId?.descriptionOfService1 && (

- Service 1:{" "} + {SERVICE_1_LABEL}{" "} {report.sessionId.descriptionOfService1}

)} {report.sessionId?.descriptionOfService2 && (

- Service 2:{" "} + {SERVICE_2_LABEL}{" "} {report.sessionId.descriptionOfService2}

)}
- {/* Reason, Description, Evidence */} + {/* REPORT CONTENT - Reason, description, and evidence files */}
-

⚠️ Report Reason

+

{REPORT_REASON_TITLE}

{formatReason(report.reason)}

+
-

📝 Detailed Description

+

{DESCRIPTION_TITLE}

{report.description}
+ {report.evidenceFiles.length > 0 && (
-

📎 Evidence Files

+

{EVIDENCE_FILES_TITLE}

    {report.evidenceFiles.map((url, i) => (
  • -

    Evidence File {i + 1}

    +

    {EVIDENCE_FILE_PREFIX}{i + 1}

    {url.split("/").pop()}

    @@ -154,7 +184,7 @@ export function ReportDetailsModal({
)} - {/* Status + Resolve Actions */} + {/* FOOTER - Status badge and action buttons */}
{report.status.replace(/_/g, " ")} @@ -166,33 +196,33 @@ export function ReportDetailsModal({ size="sm" variant="outline" > - Warn Reported + {WARN_REPORTED_BUTTON}
) : ( - Submitted {formatDate(report.createdAt)} + {SUBMITTED_PREFIX}{formatDate(report.createdAt)} )}
@@ -200,4 +230,4 @@ export function ReportDetailsModal({
); -} +} \ No newline at end of file From ef0cd49d5d826a6c2a8c8a657d38bd9755430f16 Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Thu, 17 Jul 2025 13:13:20 +0530 Subject: [PATCH 10/12] feat: Remove "Engagement and Activity Badges" from badge categories and criteria options --- src/app/badge/page.tsx | 1 - src/components/Admin/dashboardContent/badge/BadgeForm.tsx | 1 - src/components/Admin/dashboardContent/badge/badgeHelpers.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/src/app/badge/page.tsx b/src/app/badge/page.tsx index 9cb6592c..e29bd3da 100644 --- a/src/app/badge/page.tsx +++ b/src/app/badge/page.tsx @@ -20,7 +20,6 @@ const CATEGORIES = [ "All", "Achievement Milestone Badges", "Specific Badges", - "Engagement and Activity Badges", "Exclusive Recognition Badges", ]; diff --git a/src/components/Admin/dashboardContent/badge/BadgeForm.tsx b/src/components/Admin/dashboardContent/badge/BadgeForm.tsx index 47120714..5d1035f6 100644 --- a/src/components/Admin/dashboardContent/badge/BadgeForm.tsx +++ b/src/components/Admin/dashboardContent/badge/BadgeForm.tsx @@ -5,7 +5,6 @@ import Image from "next/image"; import { criteriaOptions, validateBadgeInput, - handleImageFileChange, uploadBadgeImage, API_ENDPOINTS, validateImageFile, diff --git a/src/components/Admin/dashboardContent/badge/badgeHelpers.ts b/src/components/Admin/dashboardContent/badge/badgeHelpers.ts index 89099fb2..29788e44 100644 --- a/src/components/Admin/dashboardContent/badge/badgeHelpers.ts +++ b/src/components/Admin/dashboardContent/badge/badgeHelpers.ts @@ -27,7 +27,6 @@ export interface Badge { export const criteriaOptions = [ "Achievement Milestone Badges", "Specific Badges", - "Engagement and Activity Badges", "Exclusive Recognition Badges", ]; From f0f5ac67a072063e11fa10a328d082ddf6e7be1e Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Thu, 17 Jul 2025 21:16:55 +0530 Subject: [PATCH 11/12] feat: Enhance badge name validation to require a minimum length of 3 characters --- .../Admin/dashboardContent/badge/badgeHelpers.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/Admin/dashboardContent/badge/badgeHelpers.ts b/src/components/Admin/dashboardContent/badge/badgeHelpers.ts index 29788e44..bdf61dba 100644 --- a/src/components/Admin/dashboardContent/badge/badgeHelpers.ts +++ b/src/components/Admin/dashboardContent/badge/badgeHelpers.ts @@ -125,6 +125,13 @@ export const validateBadgeInput = ( errorMessage: "Badge name must be less than 50 characters", }; } + + if (name.trim().length < 3) { + return { + isValid: false, + errorMessage: "Badge name must be at least 3 characters long.", + }; + } if (!description || description.trim().length === 0) { return { isValid: false, errorMessage: "Description is required" }; } From f8d71b303cdda03395eb5c481bf858fe1b049b5c Mon Sep 17 00:00:00 2001 From: RENULUCSHMI PRAKASAN Date: Fri, 18 Jul 2025 21:44:17 +0530 Subject: [PATCH 12/12] chore: update next and related dependencies to version 15.4.1 and sharp to version 0.34.3 --- package-lock.json | 570 +++++++++++++++++++++++++++++++++++++++------- package.json | 2 +- 2 files changed, 492 insertions(+), 80 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1eb79eeb..72a0f643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "mongodb": "^6.13.0", "mongoose": "^8.10.1", "natural": "^8.1.0", - "next": "^15.2.4", + "next": "^15.4.1", "next-auth": "^4.24.11", "next-connect": "^1.0.0", "next-themes": "^0.4.4", @@ -1719,8 +1719,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", "license": "MIT", "optional": true, "dependencies": { @@ -2091,8 +2092,409 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", "cpu": [ "x64" ], @@ -2726,7 +3128,9 @@ } }, "node_modules/@next/env": { - "version": "15.3.1", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.1.tgz", + "integrity": "sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2738,12 +3142,13 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", - "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.1.tgz", + "integrity": "sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2753,12 +3158,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", - "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.1.tgz", + "integrity": "sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2768,12 +3174,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", - "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.1.tgz", + "integrity": "sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2783,12 +3190,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", - "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.1.tgz", + "integrity": "sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2798,12 +3206,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", - "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.1.tgz", + "integrity": "sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2813,12 +3222,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", - "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.1.tgz", + "integrity": "sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2828,12 +3238,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", - "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.1.tgz", + "integrity": "sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2843,7 +3254,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.1", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.1.tgz", + "integrity": "sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==", "cpu": [ "x64" ], @@ -6029,15 +6442,6 @@ "dev": true, "license": "MIT" }, - "node_modules/busboy": { - "version": "1.6.0", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/cacheable-lookup": { "version": "5.0.4", "license": "MIT", @@ -6387,6 +6791,8 @@ }, "node_modules/color": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", "optional": true, "dependencies": { @@ -6413,6 +6819,8 @@ }, "node_modules/color-string": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", "optional": true, "dependencies": { @@ -6951,6 +7359,8 @@ }, "node_modules/detect-libc": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "optional": true, "engines": { @@ -9217,6 +9627,8 @@ }, "node_modules/is-arrayish": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT", "optional": true }, @@ -11525,13 +11937,13 @@ } }, "node_modules/next": { - "version": "15.3.1", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.4.1.tgz", + "integrity": "sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==", "license": "MIT", "dependencies": { - "@next/env": "15.3.1", - "@swc/counter": "0.1.3", + "@next/env": "15.4.1", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -11543,19 +11955,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.1", - "@next/swc-darwin-x64": "15.3.1", - "@next/swc-linux-arm64-gnu": "15.3.1", - "@next/swc-linux-arm64-musl": "15.3.1", - "@next/swc-linux-x64-gnu": "15.3.1", - "@next/swc-linux-x64-musl": "15.3.1", - "@next/swc-win32-arm64-msvc": "15.3.1", - "@next/swc-win32-x64-msvc": "15.3.1", - "sharp": "^0.34.1" + "@next/swc-darwin-arm64": "15.4.1", + "@next/swc-darwin-x64": "15.4.1", + "@next/swc-linux-arm64-gnu": "15.4.1", + "@next/swc-linux-arm64-musl": "15.4.1", + "@next/swc-linux-x64-gnu": "15.4.1", + "@next/swc-linux-x64-musl": "15.4.1", + "@next/swc-win32-arm64-msvc": "15.4.1", + "@next/swc-win32-x64-msvc": "15.4.1", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -13395,14 +13807,16 @@ } }, "node_modules/sharp": { - "version": "0.34.1", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" + "detect-libc": "^2.0.4", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -13411,26 +13825,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", - "@img/sharp-libvips-darwin-arm64": "1.1.0", - "@img/sharp-libvips-darwin-x64": "1.1.0", - "@img/sharp-libvips-linux-arm": "1.1.0", - "@img/sharp-libvips-linux-arm64": "1.1.0", - "@img/sharp-libvips-linux-ppc64": "1.1.0", - "@img/sharp-libvips-linux-s390x": "1.1.0", - "@img/sharp-libvips-linux-x64": "1.1.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", - "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" } }, "node_modules/shebang-command": { @@ -13524,6 +13940,8 @@ }, "node_modules/simple-swizzle": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", "optional": true, "dependencies": { @@ -13778,12 +14196,6 @@ "version": "1.0.3", "license": "MIT" }, - "node_modules/streamsearch": { - "version": "1.1.0", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", diff --git a/package.json b/package.json index 9ed63573..db0467ce 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "mongodb": "^6.13.0", "mongoose": "^8.10.1", "natural": "^8.1.0", - "next": "^15.2.4", + "next": "^15.4.1", "next-auth": "^4.24.11", "next-connect": "^1.0.0", "next-themes": "^0.4.4",