there is issue in deleteing the media give proper code in react typescript dont use nextjs ad tailwindcss update given code .dashboardContainer { display: flex; flex-direction: column; gap: 1rem;}.controlsContainer { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; align-items: center; padding: 0 1.5rem;}.filterButtons { display: flex; gap: 0.5rem;}.filterButton { background-color: #f0f2f5; color: #555; border: 1px solid #e0e0e0; padding: 0.6rem 1.2rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; white-space: nowrap;}.filterButton:hover { background-color: #e9ecef; border-color: #d0d0d0;}.filterButton.activeFilter { background-color: #4b49ac; color: #fff; border-color: #4b49ac; box-shadow: 0 2px 8px rgba(75, 73, 172, 0.2);}.filterButton.activeFilter:hover { background-color: #3a388a; border-color: #3a388a;}.sortDropdown { position: relative;}.select { background-color: #fff; border: 1px solid black; padding: 0.5rem 1rem 0.5rem 1.2rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; color: #555; cursor: pointer; outline: none; transition: border-color 0.2s ease;}.select:hover { border-color: #b0b0b0;}.select:focus { border-color: #4b49ac; box-shadow: 0 0 0 2px rgba(75, 73, 172, 0.2);}.tableContainer { background-color: #fff; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); overflow: hidden;}.tableHeader { display: grid; grid-template-columns: 2fr 2.5fr 1fr 1fr 0.8fr; background-color: #f8f9fa; padding: 1rem 1.5rem; border-bottom: 2px solid #e9ecef; font-weight: 600; color: #374151; font-size: 0.9rem; align-items: center;}.headerCell { padding: 0.2rem 0.5rem;}.tableRow { display: grid; grid-template-columns: 2fr 2.5fr 1fr 1fr 0.8fr; padding: 1rem; border-bottom: 1px solid #f0f2f5; align-items: center; transition: background-color 0.15s ease-in-out;}.tableRow:last-child { border-bottom: none;}.tableRow:hover { background-color: #f8fafc;}.cell { padding: 0.5rem; color: #333; font-size: 0.95rem; display: flex; flex-direction: column; gap: 0.2rem;}.progressCell { min-width: 200px;}.campaignTitle { font-weight: 600; color: #222; margin-bottom: 0.1rem;}.campaignDescriptionTruncated { font-size: 0.85rem; color: #666; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis;}.addressContainer { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.3rem;}.addressItem { display: flex; align-items: center; gap: 0.5rem; background-color: #f8f9fa; padding: 0.3rem 0.5rem; border-radius: 4px; border: 1px solid #e9ecef;}.addressText { font-size: 0.8rem; color: #666; flex: 1;}.mapButton { background: none; border: none; cursor: pointer; padding: 0.2rem; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; color: #4b49ac;}.mapButton:hover { background-color: #e9ecef; color: #3a388a;}.modalAddressItem { display: flex; align-items: center; justify-content: space-between; gap: 1rem; background-color: #f8f9fa; padding: 0.6rem; border-radius: 6px; border: 1px solid #e9ecef; margin-bottom: 0.5rem;}.modalAddressText { font-size: 0.9rem; color: #555; flex: 1;}.modalMapButton { background-color: #4b49ac; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 0.3rem; transition: all 0.2s ease;}.modalMapButton:hover { background-color: #3a388a;}.ngoTags { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;}.ngoTag { background-color: #e9ecef; color: #555; padding: 0.3rem 0.7rem; border-radius: 6px; font-size: 0.75rem; font-weight: 500;}.progressAmounts { font-weight: 500; color: #444; font-size: 0.9rem; white-space: nowrap;}.progressBarWrapper { background-color: #e0e0e0; border-radius: 5px; height: 8px; overflow: hidden; width: 100%;}.progressBar { height: 100%; background: linear-gradient(to right, #4b49ac, #6a68d6); border-radius: 5px; transition: width 0.5s ease-out; box-shadow: 0 0 5px rgba(75, 73, 172, 0.4);}.progressPercentage { font-size: 0.85rem; color: #555; text-align: right; font-weight: 600;}.statusBadge { display: inline-block; padding: 0.4rem 0.8rem; border-radius: 20px; font-size: 0.8rem; font-weight: 600; text-transform: capitalize; width: fit-content;}.statusBadge.active { background-color: #e6f7ee; color: #10b981;}.statusBadge.completed { background-color: #eff6ff; color: #3b82f6;}.statusBadge.pending { background-color: #fef3c7; color: #f59e0b;}.virtualBadge { margin-top: 0.5rem; display: inline-block;}.virtualBadge span { background-color: #e8f4fd; color: #1e40af; padding: 0.3rem 0.8rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; border: 1px solid #bfdbfe;}.modalVirtualBadge { background-color: #e8f4fd; color: #1e40af; padding: 0.4rem 1rem; border-radius: 18px; font-size: 0.85rem; font-weight: 600; border: 1px solid #bfdbfe; display: inline-flex; align-items: center;}.actionIcons { display: flex; gap: 0.5rem; justify-content: flex-end;}.iconButton { background: none; border: none; cursor: pointer; padding: 0.5rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; color: #666;}.iconButton:hover { background-color: #f0f2f5; color: #4b49ac;}.noCampaigns { padding: 2rem; text-align: center; color: #666; font-size: 1rem;}.modalMediaGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem;}.modalMediaItem { position: relative; border-radius: 8px; overflow: hidden; border: 1px solid #e0e4ff; background-color: #f9faff; transition: transform 0.2s ease, box-shadow 0.2s ease;}.modalMediaItem:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);}.modalMediaOverlay { position: absolute; top: 0.5rem; left: 0.5rem; background-color: rgba(0, 0, 0, 0.7); color: white; padding: 0.3rem 0.6rem; border-radius: 4px; font-size: 0.75rem; font-weight: 500; display: flex; align-items: center; gap: 0.3rem; z-index: 1;}.modalMediaIcon { font-size: 0.8rem;}.modalMediaType { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.5px;}.modalMediaImage { width: 100%; height: 150px; object-fit: cover; cursor: pointer; transition: opacity 0.2s ease;}.modalMediaImage:hover { opacity: 0.9;}.modalMediaVideo { width: 100%; height: 150px; object-fit: cover; border-radius: 0;}.loadingContainer { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px; gap: 1.5rem;}.loadingSpinner { width: 50px; height: 50px; border: 5px solid #f0f2f5; border-top-color: #4b49ac; border-radius: 50%; animation: spin 1s linear infinite;}.loadingText { color: #4b49ac; font-size: 1.1rem; font-weight: 500; animation: pulse 1.5s ease-in-out infinite;}.loadingDots { display: flex; gap: 0.5rem;}.loadingDot { width: 10px; height: 10px; background-color: #4b49ac; border-radius: 50%; opacity: 0.3; animation: bounce 1.4s infinite ease-in-out;}.loadingDot:nth-child(1) { animation-delay: 0s;}.loadingDot:nth-child(2) { animation-delay: 0.2s;}.loadingDot:nth-child(3) { animation-delay: 0.4s;}@keyframes spin { to { transform: rotate(360deg); }}@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; }}@keyframes bounce { 0%, 100% { transform: translateY(0); opacity: 0.3; } 50% { transform: translateY(-10px); opacity: 1; }}.error { color: #dc3545; background-color: #f8d7da; padding: 1.5rem; border-radius: 8px; text-align: center; border: 1px solid #f5c6cb;}.retryButton { background-color: #4b49ac; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; margin-top: 1rem;}.paginationContainer { display: flex; justify-content: center; align-items: center; padding: 1.5rem; gap: 0.5rem; border-top: 1px solid #f0f2f5;}.paginationButton { background: #f0f2f5; border: none; border-radius: 6px; padding: 0.5rem 0.75rem; cursor: pointer; display: flex; align-items: center; transition: all 0.2s ease;}.paginationButton:hover:not(:disabled) { background: #e0e0e0;}.paginationButton:disabled { opacity: 0.5; cursor: not-allowed;}.pageNumbers { display: flex; gap: 0.5rem;}.pageNumber { background: #f0f2f5; border: none; border-radius: 6px; padding: 0.5rem 0.75rem; min-width: 36px; cursor: pointer; transition: all 0.2s ease;}.pageNumber:hover { background: #e0e0e0;}.pageNumber.activePage { background: #4b49ac; color: white;}.searchContainer { position: relative; flex-grow: 1; min-width: 250px;}.searchAndSortContainer { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;}.searchIcon { position: absolute; left: 1rem; top: 50%; transform: translateY(-50%); color: #666;}.searchBar { width: 100%; padding: 0.5rem 1rem 0.5rem 2.5rem; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9rem; transition: all 0.2s ease;}.searchBar:focus { outline: none; border-color: #4b49ac; box-shadow: 0 0 0 2px rgba(75, 73, 172, 0.2);}.deleteDialogOverlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;}.deleteDialog { background-color: white; padding: 2rem; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);}.deleteDialog h3 { margin-top: 0; color: #333;}.deleteDialog p { margin-bottom: 2rem; color: #555;}.dialogButtons { display: flex; justify-content: flex-end; gap: 1rem;}.cancelButton { padding: 0.5rem 1rem; border: 1px solid #ddd; border-radius: 4px; background-color: #f5f5f5; cursor: pointer; transition: all 0.2s ease;}.cancelButton:hover { background-color: #e5e5e5;}.deleteButton { padding: 0.5rem 1rem; border: 1px solid #dc3545; border-radius: 4px; background-color: #dc3545; color: white; cursor: pointer; transition: all 0.2s ease;}.deleteButton:hover { background-color: #c82333; border-color: #c82333;}.modalOverlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 1000; backdrop-filter: blur(4px); animation: fadeIn 0.3s ease-out;}.modalContent { background-color: white; padding: 2rem; border-radius: 12px; max-width: 1200px; width: 90%; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); position: relative; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; animation: slideUp 0.3s ease-out; border: 1px solid rgba(255, 255, 255, 0.1);}.modalHeader { display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 2px solid rgba(75, 73, 172, 0.15);}.modalTitle { font-size: 1.2rem; font-weight: 600; color: #4b49ac; margin: 0;}.modalCloseButton { background: none; border: none; font-size: 1.6rem; cursor: pointer; color: #888; padding: 0.2rem; border-radius: 50%; transition: all 0.2s ease; line-height: 1; width: 2.2rem; height: 2.2rem; display: flex; align-items: center; justify-content: center;}.modalCloseButton:hover { background-color: #f5f5ff; color: #4b49ac; transform: rotate(90deg);}.modalBody { display: flex; flex-direction: column; gap: 0.8rem; font-size: 0.95rem; color: #555;}.modalDetailValue { line-height: 1.5;}.modalDetailValue div { margin: 2px 0;}.modalDetailValue span.percentage { color: #555; font-size: 0.8rem;}.modalDetailRow { display: grid; grid-template-columns: 160px 1fr; gap: 0.75rem; align-items: baseline; padding: 0.5rem 0; border-bottom: 2px solid rgba(0, 0, 0, 0.05);}.modalDetailLabel { font-weight: 600; color: #333; font-size: 0.9rem;}.modalDetailValue { color: #555; word-break: break-word; font-size: 0.9rem;}.modalBadge { align-items: center; background-color: #f0f2ff; color: #4b49ac; padding: 0.3rem 0.8rem; border-radius: 18px; font-size: 0.8rem; font-weight: 500; border: 1px solid rgba(75, 73, 172, 0.2);}.modalSectionTitle { font-size: 1rem; font-weight: 500; color: #4b49ac; margin-top: 1rem; margin-bottom: 0.35rem; padding-bottom: 0.5rem; border-bottom: 2px solid rgba(75, 73, 172, 0.1);}.modalList { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem;}.modalListItem { background-color: #f9faff; border: 1px solid #e0e4ff; border-radius: 8px; padding: 0.8rem 1rem; font-size: 0.9rem; color: #444;}.modalListItemLabel { font-weight: 600; color: #4b49ac; margin-bottom: 0.2rem;}.modalNoData { font-style: italic; color: #999; padding: 0.4rem 0.8rem; background: #f9f9f9; border-radius: 6px; display: inline-block;}.modalImages { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.8rem;}.modalImage { width: 100px; height: 100px; object-fit: cover; border-radius: 8px; border: 1px solid #eee;}.modalDateGoalGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 0.8rem;}.modalDateGoalGrid .modalDetailItem { background: #f9faff; border-radius: 8px; padding: 0.8rem; border: 1px solid #e0e4ff;}.modalNgoList { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0.8rem;}.modalNgoListItem { background-color: #f9faff; border: 1px solid #e0e4ff; border-radius: 8px; padding: 0.8rem; font-size: 0.9rem;}.modalDistributionTable { max-height: 400px; overflow-y: auto; border: 1px solid #e0e4ff; border-radius: 8px; margin-top: 0.5rem;}.modalDistributionTableHeader { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr 0.8fr; background-color: #4b49ac; font-weight: 600; color: white; font-size: 0.85rem; position: sticky; top: 0; z-index: 1;}.modalDistributionTableHeaderCell { padding: 0.8rem;}.modalDistributionTableRow { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr 0.8fr;}.modalDistributionTableCell { padding: 0.5rem; font-size: 0.85rem; border: 1px solid #e0e4ff;}.editForm { display: flex; flex-direction: column; gap: 1.5rem;}.formGroup { display: flex; flex-direction: column; gap: 0.5rem;}.formRow { display: flex; gap: 1rem;}.formLabel { font-weight: 500; color: #555; font-size: 0.9rem;}.formInput, .formTextarea { padding: 0.75rem; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; transition: all 0.2s ease;}.formInput:focus, .formTextarea:focus { outline: none; border-color: #4b49ac; box-shadow: 0 0 0 2px rgba(75, 73, 172, 0.2);}.formTextarea { min-height: 120px; resize: vertical;}.formButtons { display: flex; justify-content: flex-end; gap: 1rem; margin-top: 1rem;}.submitButton { padding: 0.75rem 1.5rem; background-color: #4b49ac; color: white; border: none; border-radius: 6px; font-weight: 500; cursor: pointer; transition: all 0.2s ease;}.submitButton:hover { background-color: #3a388a;}.submitButton:disabled { background-color: #a5a4d8; cursor: not-allowed;}.editMediaSection { margin-top: 1.5rem; border-top: 1px solid #eee; padding-top: 1.5rem;}.editMediaSectionTitle { font-size: 1rem; font-weight: 600; color: #4b49ac; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;}.editMediaSectionTitle svg { font-size: 1.1rem;}.mediaUploadGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem; margin: 1rem 0;}.mediaUploadItem { position: relative; border-radius: 8px; overflow: hidden; border: 1px solid #e0e4ff; background-color: #f9faff; aspect-ratio: 1/1; transition: all 0.2s ease;}.mediaUploadItem:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(75, 73, 172, 0.1);}.mediaPreviewImage { width: 100%; height: 100%; object-fit: cover;}.mediaPreviewVideo { width: 100%; height: 100%; object-fit: cover;}.mediaDeleteButton { position: absolute; top: 0.3rem; right: 0.3rem; width: 1.5rem; height: 1.5rem; background-color: rgba(220, 53, 69, 0.9); color: white; border: none; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.9rem; font-weight: bold; transition: all 0.2s ease;}.mediaDeleteButton:hover { background-color: #c82333; transform: scale(1.1);}.mediaTypeBadge { position: absolute; bottom: 0.3rem; left: 0.3rem; background-color: rgba(0, 0, 0, 0.7); color: white; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; font-weight: 500;}.uploadControls { display: flex; flex-direction: column; gap: 1rem; margin-top: 1.5rem;}.uploadButton { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; background-color: #4b49ac; color: white; border: none; border-radius: 6px; font-size: 0.9rem; cursor: pointer; transition: all 0.2s ease; width: fit-content;}.uploadButton:hover { background-color: #3a388a;}.uploadButton input[type="file"] { display: none;}.mediaUploadNotice { font-size: 0.8rem; color: #666; margin-top: 0.5rem; font-style: italic;}@media (max-width: 768px) { .controlsContainer { flex-direction: column; align-items: stretch; padding: 1rem; } .filterButtons { flex-direction: column; width: 100%; } .filterButton { width: 100%; text-align: center; } .sortDropdown { width: 100%; } .select { width: 100%; } .tableHeader, .tableRow { grid-template-columns: 1fr; padding: 1rem; } .tableHeader { display: none; } .tableRow { border: 1px solid #e0e0e0; margin-bottom: 1rem; border-radius: 8px; gap: 1rem; } .cell { width: 100%; padding: 0.5rem 0; border-bottom: 1px dashed #eee; } .cell:last-child { border-bottom: none; } .actionIcons { justify-content: flex-start; } .addressItem { flex-direction: column; align-items: flex-start; gap: 0.3rem; } .modalAddressItem { flex-direction: column; align-items: flex-start; gap: 0.5rem; } .modalMediaGrid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.8rem; } .modalMediaImage, .modalMediaVideo { height: 120px; } .virtualBadge { margin-top: 0.3rem; } .virtualBadge span { font-size: 0.7rem; padding: 0.25rem 0.6rem; } .modalVirtualBadge { font-size: 0.8rem; padding: 0.35rem 0.8rem; } .modalContent { padding: 1.5rem; width: 95%; } .modalTitle { font-size: 1.3rem; } .modalDetailRow { grid-template-columns: 1fr; gap: 0.3rem; } .modalDateGoalGrid, .modalNgoList { grid-template-columns: 1fr; gap: 0.6rem; } .modalDistributionTableRow { grid-template-columns: 1fr; } .modalDistributionTableCell { padding: 0.6rem; } .formRow { flex-direction: column; gap: 1rem; } .mediaUploadGrid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); } .editMediaSection { margin-top: 1rem; padding-top: 1rem; }}@media (max-width: 480px) { .modalContent { padding: 1.25rem; } .modalHeader { gap: 0.8rem; padding-bottom: 0.8rem; } .modalImage { width: 80px; height: 80px; } .mediaUploadGrid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 0.8rem; } .uploadControls { flex-direction: column; align-items: flex-start; }}@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; }}@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; }}/* Add these new styles to your existing CSS */.mediaDeleteButton:disabled { opacity: 0.6; cursor: not-allowed;}.uploadButton input:disabled { cursor: not-allowed;}.formInput:disabled,.formTextarea:disabled { background-color: #f8f9fa; cursor: not-allowed; opacity: 0.6;}/* Loading state for media operations */.mediaUploadItem.deleting { opacity: 0.5; pointer-events: none;}.mediaUploadItem.deleting::after { content: "Deleting..."; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;}/* Error message styling */.error { color: #dc3545; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 12px; border-radius: 4px; margin: 10px 0;}/* Success message styling */.success { color: #155724; background-color: #d4edda; border: 1px solid #c3e6cb; padding: 12px; border-radius: 4px; margin: 10px 0;}/* Media operation status */.mediaOperationStatus { position: fixed; top: 20px; right: 20px; background: #007bff; color: white; padding: 12px 20px; border-radius: 4px; z-index: 1000; animation: slideIn 0.3s ease-out;}@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; }}.mediaOperationStatus.error { background: #dc3545;}.mediaOperationStatus.success { background: #28a745;}/* Responsive adjustments */@media (max-width: 768px) { .uploadControls { flex-direction: column; gap: 10px; }}"use client"import type React from "react"import { useState, useMemo, useEffect, useCallback } from "react"import axios, { type AxiosResponse } from "axios"import { FiEye, FiEdit2, FiTrash2, FiChevronLeft, FiChevronRight, FiSearch, FiMapPin, FiPlay, FiImage, FiPlus } from "react-icons/fi"import styles from "./CampaignTable.module.css"interface CampaignAddress { city: string location: string}interface ParticipatingNgo { ngo_id: string; ngo_name: string; custom_plat_percent: string;}interface CampaignAgent { agent_id: string agent_name: string}interface NgoWiseDistribution { ngoId: string ngoName: string campaignAmount: number ngoShare: number platformShare: number platPercent: string}interface ApiCampaign { _id: string campaign_name: string description: string goal_amount: number raised_amount: number start_date: string end_date: string status: string campaign_mode: string campaign_address?: CampaignAddress[]}interface FullApiCampaign { _id: string campaign_title: string campaign_description: string campaign_image?: string[] campaign_video?: string[] goal_amount: number raised_amount: number start_date: string end_date: string campaign_category: string[] status: string ngoWiseDistribution?: NgoWiseDistribution[] participating_ngos?: ParticipatingNgo[] campaign_mode: string campaign_address?: CampaignAddress[] campaign_agents?: CampaignAgent[]}interface Campaign { _id: string campaign_name: string description: string campaign_image: string[] campaign_video: string[] goal_amount: number raised_amount: number start_date: string end_date: string campaign_category: string[] ngoWiseDistribution: NgoWiseDistribution[] status: string campaign_mode: string campaign_address: CampaignAddress[] campaign_agents: CampaignAgent[] participating_ngos: ParticipatingNgo[]}interface DeleteConfirmation { show: boolean campaignId: string | null campaignName: string}interface EditCampaignForm { campaign_title: string goal_amount: number end_date: string start_date: string campaign_description: string campaign_image: string[] campaign_video: string[] newImages: File[] newVideos: File[] deletedImages: string[] deletedVideos: string[]}type FilterStatus = "all" | "active" | "completed"type SortOrder = "newest" | "progress_asc" | "progress_desc"type CampaignStatus = "Active" | "Completed"const API_BASE_URL = import.meta.env.VITE_HOST_URLconst getAuthToken = (): string | null => { return localStorage.getItem("adminToken") || sessionStorage.getItem("adminToken")}const getAuthConfig = () => { const token = getAuthToken() return { headers: { Authorization: `Bearer ${token}`, }, }}const transformCampaignData = (apiCampaign: ApiCampaign | FullApiCampaign): Campaign => { const isFullCampaign = "campaign_title" in apiCampaign return { _id: apiCampaign._id, campaign_name: isFullCampaign ? (apiCampaign as FullApiCampaign).campaign_title : (apiCampaign as ApiCampaign).campaign_name, description: isFullCampaign ? (apiCampaign as FullApiCampaign).campaign_description : (apiCampaign as ApiCampaign).description, campaign_image: (apiCampaign as FullApiCampaign).campaign_image || [], campaign_video: (apiCampaign as FullApiCampaign).campaign_video || [], goal_amount: apiCampaign.goal_amount, raised_amount: apiCampaign.raised_amount, start_date: apiCampaign.start_date, end_date: apiCampaign.end_date, campaign_category: (apiCampaign as FullApiCampaign).campaign_category || ["General"], ngoWiseDistribution: (apiCampaign as FullApiCampaign).ngoWiseDistribution || [], status: apiCampaign.status, campaign_mode: apiCampaign.campaign_mode, campaign_address: apiCampaign.campaign_address || [], campaign_agents: (apiCampaign as FullApiCampaign).campaign_agents || [], participating_ngos: (apiCampaign as FullApiCampaign).participating_ngos || [], }}const formatCurrency = (amount: number): string => { return `₹${amount.toLocaleString("en-IN")}`}const calculateProgress = (raised: number, goal: number): number => { return goal > 0 ? (raised / goal) * 100 : 0}const parseDate = (dateStr: string): Date => { const date = new Date(dateStr) if (!isNaN(date.getTime())) return date const [year, month, day] = dateStr.split("-").map(Number) if (year && month && day) { return new Date(year, month - 1, day) } return new Date()}const getDaysLeft = (endDate: string): string => { try { const end = parseDate(endDate) const now = new Date() now.setHours(0, 0, 0, 0) end.setHours(0, 0, 0, 0) const diffTime = end.getTime() - now.getTime() const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? "s" : ""} left` if (diffDays === 0) return "Ends today" return "Ended" } catch (error) { console.error("Error parsing date for days left:", error) return "Invalid date" }}const openGoogleMaps = (address: CampaignAddress) => { const query = encodeURIComponent(`${address.location}, ${address.city}`) const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${query}` window.open(googleMapsUrl, "_blank")}const fetchCampaigns = async (filterStatus: FilterStatus): Promise<ApiCampaign[] | FullApiCampaign[]> => { const config = getAuthConfig() let apiResponse: AxiosResponse<{ success: boolean; campaigns: ApiCampaign[] | FullApiCampaign[]; count: number }> try { switch (filterStatus) { case "active": apiResponse = await axios.get<{ success: boolean; campaigns: ApiCampaign[]; count: number }>( `${API_BASE_URL}/admin/get-active-campaigns`, config, ) break case "completed": apiResponse = await axios.get<{ success: boolean; campaigns: ApiCampaign[]; count: number }>( `${API_BASE_URL}/admin/get-completed-campaigns`, config, ) break default: apiResponse = await axios.get<{ success: boolean; campaigns: FullApiCampaign[]; count: number }>( `${API_BASE_URL}/admin/get-all-campaigns`, config, ) break } if (!apiResponse.data.success) { throw new Error("Failed to fetch campaigns") } return apiResponse.data.campaigns || [] } catch (error) { console.error("Error fetching campaigns:", error) if (axios.isAxiosError(error) && error.response?.status === 404) { return [] } throw error }}const deleteCampaign = async (campaignId: string): Promise<void> => { try { await axios.delete(`${API_BASE_URL}/admin/delete-campaign/${campaignId}`, getAuthConfig()) } catch (error) { console.error("Error deleting campaign:", error) throw error }}const deleteCampaignMedia = async (campaignId: string, mediaUrls: string[]): Promise<void> => { try { const response = await axios.delete( `${API_BASE_URL}/admin/delete-campaign-media/${campaignId}`, { ...getAuthConfig(), data: { mediaUrls } } ) if (!response.data.success) { throw new Error(response.data.message || "Failed to delete media") } } catch (error) { console.error("Error deleting campaign media:", error) throw error }}// Main Componentconst CampaignTable: React.FC = () => { const [campaigns, setCampaigns] = useState<Campaign[]>([]) const [loading, setLoading] = useState<boolean>(true) const [error, setError] = useState<string | null>(null) const [filterStatus, setFilterStatus] = useState<FilterStatus>("all") const [sortOrder, setSortOrder] = useState<SortOrder>("newest") const [currentPage, setCurrentPage] = useState<number>(1) const [searchTerm, setSearchTerm] = useState<string>("") const [deleteConfirmation, setDeleteConfirmation] = useState<DeleteConfirmation>({ show: false, campaignId: null, campaignName: "", }) const [selectedCampaign, setSelectedCampaign] = useState<Campaign | null>(null) const [isModalOpen, setIsModalOpen] = useState<boolean>(false) const [editModalOpen, setEditModalOpen] = useState<boolean>(false) const [editingCampaign, setEditingCampaign] = useState<Campaign | null>(null) const [editForm, setEditForm] = useState<EditCampaignForm>({ campaign_title: '', goal_amount: 0, end_date: '', start_date: '', campaign_description: '', campaign_image: [], campaign_video: [], newImages: [], newVideos: [], deletedImages: [], deletedVideos: [] }) const [deletingMedia, setDeletingMedia] = useState<boolean>(false) const campaignsPerPage = 10 const loadCampaigns = useCallback(async () => { setLoading(true) setError(null) try { const apiCampaigns = await fetchCampaigns(filterStatus) const transformedCampaigns = apiCampaigns.map(transformCampaignData) setCampaigns(transformedCampaigns) } catch (err) { console.error("Error fetching campaigns:", err) const errorMessage = axios.isAxiosError(err) ? err.response?.data?.message || err.message : "Failed to load campaigns. Please try again." setError(errorMessage) } finally { setLoading(false) } }, [filterStatus]) useEffect(() => { loadCampaigns() }, [loadCampaigns]) const handleDeleteClick = useCallback((campaignId: string, campaignName: string) => { setDeleteConfirmation({ show: true, campaignId, campaignName, }) }, []) const handleDeleteConfirm = useCallback(async () => { if (!deleteConfirmation.campaignId) return try { setLoading(true) await deleteCampaign(deleteConfirmation.campaignId) setCampaigns((prev) => prev.filter((c) => c._id !== deleteConfirmation.campaignId)) setDeleteConfirmation({ show: false, campaignId: null, campaignName: "" }) } catch (err) { console.error("Error deleting campaign:", err) setError("Failed to delete campaign. Please try again.") } finally { setLoading(false) } }, [deleteConfirmation.campaignId]) const handleDeleteCancel = useCallback(() => { setDeleteConfirmation({ show: false, campaignId: null, campaignName: "" }) }, []) const handleViewClick = useCallback((campaign: Campaign) => { setSelectedCampaign(campaign) setIsModalOpen(true) }, []) const handleCloseModal = useCallback(() => { setIsModalOpen(false) setSelectedCampaign(null) }, []) const handleFilterChange = useCallback((newFilter: FilterStatus) => { setFilterStatus(newFilter) setCurrentPage(1) }, []) const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { setSearchTerm(e.target.value) setCurrentPage(1) }, []) const handleSortChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => { setSortOrder(e.target.value as SortOrder) }, []) const handleEditClick = useCallback((campaign: Campaign) => { setEditingCampaign(campaign) setEditForm({ campaign_title: campaign.campaign_name, goal_amount: campaign.goal_amount, end_date: campaign.end_date, start_date: campaign.start_date, campaign_description: campaign.description, campaign_image: campaign.campaign_image || [], campaign_video: campaign.campaign_video || [], newImages: [], newVideos: [], deletedImages: [], deletedVideos: [] }) setEditModalOpen(true)}, []) const handleEditFormChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = e.target setEditForm(prev => ({ ...prev, [name]: name === 'goal_amount' ? Number(value) : value })) }, []) const handleImageUpload = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files) { const files = Array.from(e.target.files) setEditForm(prev => ({ ...prev, newImages: [...prev.newImages, ...files] })) } }, []) const handleVideoUpload = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files) { const files = Array.from(e.target.files) setEditForm(prev => ({ ...prev, newVideos: [...prev.newVideos, ...files] })) } }, []) // Update your media delete handlersconst handleDeleteImage = useCallback(async (imageUrl: string) => { try { setDeletingMedia(true) await deleteCampaignMedia(editingCampaign?._id || '', [imageUrl]) setEditForm(prev => ({ ...prev, campaign_image: prev.campaign_image.filter(img => img !== imageUrl), deletedImages: [...prev.deletedImages, imageUrl] })) } catch (error) { console.error("Error deleting image:", error) setError("Failed to delete image. Please try again.") } finally { setDeletingMedia(false) }}, [editingCampaign])const handleDeleteVideo = useCallback(async (videoUrl: string) => { try { setDeletingMedia(true) await deleteCampaignMedia(editingCampaign?._id || '', [videoUrl]) setEditForm(prev => ({ ...prev, campaign_video: prev.campaign_video.filter(vid => vid !== videoUrl), deletedVideos: [...prev.deletedVideos, videoUrl] })) } catch (error) { console.error("Error deleting video:", error) setError("Failed to delete video. Please try again.") } finally { setDeletingMedia(false) }}, [editingCampaign]) const handleDeleteNewImage = useCallback((index: number) => { setEditForm(prev => { const newImages = [...prev.newImages] newImages.splice(index, 1) return { ...prev, newImages } }) }, []) const handleDeleteNewVideo = useCallback((index: number) => { setEditForm(prev => { const newVideos = [...prev.newVideos] newVideos.splice(index, 1) return { ...prev, newVideos } }) }, [])const handleEditSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault() if (!editingCampaign) return try { setLoading(true) // First handle media deletions if any if (editForm.deletedImages.length > 0 || editForm.deletedVideos.length > 0) { await deleteCampaignMedia( editingCampaign._id, [...editForm.deletedImages, ...editForm.deletedVideos] ) } // Then proceed with the rest of the updates const formData = new FormData() formData.append('campaign_title', editForm.campaign_title) formData.append('goal_amount', editForm.goal_amount.toString()) formData.append('end_date', editForm.end_date) formData.append('start_date', editForm.start_date) formData.append('campaign_description', editForm.campaign_description) editForm.newImages.forEach((file) => { formData.append('campaign_images', file) }) editForm.newVideos.forEach((file) => { formData.append('campaign_videos', file) }) const config = { ...getAuthConfig(), headers: { ...getAuthConfig().headers, 'Content-Type': 'multipart/form-data' } } const response = await axios.patch( `${API_BASE_URL}/admin/update-campaign/${editingCampaign._id}`, formData, config ) if (response.data.success) { loadCampaigns() setEditModalOpen(false) } } catch (error) { console.error("Error updating campaign:", error) setError("Failed to update campaign. Please try again.") } finally { setLoading(false) }}, [editingCampaign, editForm, loadCampaigns]) const filteredAndSortedCampaigns = useMemo(() => { let currentCampaigns = [...campaigns] if (filterStatus !== "all") { currentCampaigns = currentCampaigns.filter((campaign) => { const backendStatus = campaign.status.toLowerCase() return backendStatus === filterStatus }) } if (searchTerm.trim() !== "") { const searchLower = searchTerm.toLowerCase() currentCampaigns = currentCampaigns.filter( (campaign) => campaign.campaign_name.toLowerCase().includes(searchLower) || campaign.description.toLowerCase().includes(searchLower), ) } currentCampaigns.sort((a, b) => { switch (sortOrder) { case "newest": return parseDate(b.start_date).getTime() - parseDate(a.start_date).getTime() case "progress_asc": return calculateProgress(a.raised_amount, a.goal_amount) - calculateProgress(b.raised_amount, b.goal_amount) case "progress_desc": return calculateProgress(b.raised_amount, b.goal_amount) - calculateProgress(a.raised_amount, a.goal_amount) default: return 0 } }) return currentCampaigns }, [campaigns, filterStatus, sortOrder, searchTerm]) // Pagination logic const totalPages = Math.ceil(filteredAndSortedCampaigns.length / campaignsPerPage) const indexOfLastCampaign = currentPage * campaignsPerPage const indexOfFirstCampaign = indexOfLastCampaign - campaignsPerPage const currentCampaigns = filteredAndSortedCampaigns.slice(indexOfFirstCampaign, indexOfLastCampaign) const paginate = useCallback((pageNumber: number) => setCurrentPage(pageNumber), []) const nextPage = useCallback(() => setCurrentPage((prev) => Math.min(prev + 1, totalPages)), [totalPages]) const prevPage = useCallback(() => setCurrentPage((prev) => Math.max(prev - 1, 1)), []) if (loading) { return ( <div className={styles.loadingContainer}> <div className={styles.loadingSpinner}></div> <div className={styles.loadingText}>Loading campaigns</div> <div className={styles.loadingDots}> <div className={styles.loadingDot}></div> <div className={styles.loadingDot}></div> <div className={styles.loadingDot}></div> </div> </div> ) } if (error) { return ( <div className={`${styles.dashboardContainer} ${styles.error}`}> <p>{error}</p> <button onClick={loadCampaigns} className={styles.retryButton}> Retry </button> </div> ) } return ( <div className={styles.dashboardContainer}> <div className={styles.controlsContainer}> <div className={styles.filterButtons}> <button className={`${styles.filterButton} ${filterStatus === "all" ? styles.activeFilter : ""}`} onClick={() => handleFilterChange("all")} > All Campaigns </button> <button className={`${styles.filterButton} ${filterStatus === "active" ? styles.activeFilter : ""}`} onClick={() => handleFilterChange("active")} > Active Campaigns </button> <button className={`${styles.filterButton} ${filterStatus === "completed" ? styles.activeFilter : ""}`} onClick={() => handleFilterChange("completed")} > Completed Campaigns </button> </div> <div className={styles.searchAndSortContainer}> <div className={styles.searchContainer}> <FiSearch className={styles.searchIcon} /> <input type="text" placeholder="Search campaigns..." className={styles.searchBar} value={searchTerm} onChange={handleSearchChange} /> </div> <div className={styles.sortDropdown}> <select id="sort-select" className={styles.select} value={sortOrder} onChange={handleSortChange}> <option value="newest">Sort by: Newest</option> <option value="progress_desc">Sort by: Progress (High to Low)</option> <option value="progress_asc">Sort by: Progress (Low to High)</option> </select> </div> </div> </div> <div className={styles.tableContainer}> <div className={styles.tableHeader}> <div className={styles.headerCell}>CAMPAIGN</div> <div className={styles.headerCell}>PROGRESS</div> <div className={styles.headerCell}>DURATION</div> <div className={styles.headerCell}>STATUS</div> <div className={styles.headerCell}>ACTIONS</div> </div> {currentCampaigns.length === 0 ? ( <div className={styles.noCampaigns}> {filterStatus === "all" ? "No campaigns found." : `No ${filterStatus} campaigns found.`} </div> ) : ( currentCampaigns.map((campaign) => { const progress = calculateProgress(campaign.raised_amount, campaign.goal_amount) const daysLeftText = getDaysLeft(campaign.end_date) const displayStatus = campaign.status.toLowerCase() as CampaignStatus return ( <div key={campaign._id} className={styles.tableRow}> <div className={styles.cell}> <div className={styles.campaignTitle}>{campaign.campaign_name}</div> <div className={styles.campaignDescriptionTruncated}>{campaign.description}</div> {campaign.campaign_mode?.toLowerCase() === "virtual" ? ( <div className={styles.virtualBadge}> <span>Virtual Campaign</span> </div> ) : ( <div className={styles.virtualBadge}> <span>Face-to-Face Campaign</span> </div> )} </div> <div className={styles.cell}> <div className={styles.progressAmounts}> {formatCurrency(campaign.raised_amount)} / {formatCurrency(campaign.goal_amount)} </div> <div className={styles.progressBarWrapper}> <div className={styles.progressBar} style={{ width: `${Math.min(progress, 100)}%` }}></div> </div> <div className={styles.progressPercentage}>{progress.toFixed(1)}%</div> </div> <div className={styles.cell}>{daysLeftText}</div> <div className={styles.cell}> <span className={`${styles.statusBadge} ${styles[displayStatus]}`}>{campaign.status}</span> </div> <div className={styles.cell}> <div className={styles.actionIcons}> <button className={styles.iconButton} aria-label="View campaign" onClick={() => handleViewClick(campaign)} > <FiEye size={20} /> </button> <button className={styles.iconButton} aria-label="Edit campaign" onClick={() => handleEditClick(campaign)} > <FiEdit2 size={20} /> </button> <button className={styles.iconButton} aria-label="Delete campaign" onClick={() => handleDeleteClick(campaign._id, campaign.campaign_name)} > <FiTrash2 size={20} /> </button> </div> </div> </div> ) }) )} {/* Pagination Controls */} {filteredAndSortedCampaigns.length > campaignsPerPage && ( <div className={styles.paginationContainer}> <button onClick={prevPage} disabled={currentPage === 1} className={styles.paginationButton}> <FiChevronLeft size={18} /> </button> <div className={styles.pageNumbers}> {Array.from({ length: totalPages }, (_, i) => i + 1).map((number) => ( <button key={number} onClick={() => paginate(number)} className={`${styles.pageNumber} ${currentPage === number ? styles.activePage : ""}`} > {number} </button> ))} </div> <button onClick={nextPage} disabled={currentPage === totalPages} className={styles.paginationButton}> <FiChevronRight size={18} /> </button> </div> )} </div> {/* Delete Confirmation Dialog */} {deleteConfirmation.show && ( <div className={styles.deleteDialogOverlay}> <div className={styles.deleteDialog}> <h3>Confirm Deletion</h3> <p>Are you sure you want to delete the campaign "{deleteConfirmation.campaignName}"?</p> <div className={styles.dialogButtons}> <button className={styles.cancelButton} onClick={handleDeleteCancel}> Cancel </button> <button className={styles.deleteButton} onClick={handleDeleteConfirm}> Delete </button> </div> </div> </div> )} {/* Edit Modal */} {editModalOpen && editingCampaign && ( <div className={styles.modalOverlay}> <div className={styles.modalContent}> <div className={styles.modalHeader}> <h3 className={styles.modalTitle}>Edit Campaign: {editingCampaign.campaign_name}</h3> <button className={styles.modalCloseButton} onClick={() => setEditModalOpen(false)} disabled={loading} > × </button> </div> <form onSubmit={handleEditSubmit} className={styles.editForm}> <div className={styles.formGroup}> <label className={styles.formLabel}>Campaign Name</label> <input type="text" name="campaign_title" value={editForm.campaign_title} onChange={handleEditFormChange} className={styles.formInput} required /> </div> <div className={styles.formGroup}> <label className={styles.formLabel}>Description</label> <textarea name="campaign_description" value={editForm.campaign_description} onChange={handleEditFormChange} className={styles.formTextarea} required /> </div> <div className={styles.formRow}> <div className={styles.formGroup}> <label className={styles.formLabel}>Goal Amount (₹)</label> <input type="number" name="goal_amount" value={editForm.goal_amount} onChange={handleEditFormChange} className={styles.formInput} required min="1" /> </div> <div className={styles.formGroup}> <label className={styles.formLabel}>Start Date</label> <input type="date" name="start_date" value={editForm.start_date} onChange={handleEditFormChange} className={styles.formInput} required /> </div> <div className={styles.formGroup}> <label className={styles.formLabel}>End Date</label> <input type="date" name="end_date" value={editForm.end_date} onChange={handleEditFormChange} className={styles.formInput} required /> </div> </div> <div className={styles.editMediaSection}> {/* Campaign Images Section */} <h4 className={styles.editMediaSectionTitle}> <FiImage /> Campaign Images </h4> {editForm.campaign_image.length > 0 || editForm.newImages.length > 0 ? ( <div className={styles.mediaUploadGrid}> {/* Existing images */} {editForm.campaign_image.map((imageUrl, index) => ( <div key={`existing-image-${index}`} className={styles.mediaUploadItem}> <button type="button" className={styles.mediaDeleteButton} onClick={() => handleDeleteImage(imageUrl)} disabled={deletingMedia}> {deletingMedia ? '...' : '×'}</button> <img src={imageUrl || "/placeholder.svg"} alt={`Campaign image ${index + 1}`} className={styles.mediaPreviewImage} /> <span className={styles.mediaTypeBadge}>Image</span> </div> ))} {/* New images */} {editForm.newImages.map((file, index) => ( <div key={`new-image-${index}`} className={styles.mediaUploadItem}> <button type="button" className={styles.mediaDeleteButton} onClick={() => handleDeleteNewImage(index)} > × </button> <img src={URL.createObjectURL(file) || "/placeholder.svg"} alt={`New image ${index + 1}`} className={styles.mediaPreviewImage} /> <span className={styles.mediaTypeBadge}>New</span> </div> ))} </div> ) : ( <p className={styles.mediaUploadNotice}>No images uploaded yet</p> )} <div className={styles.uploadControls}> <label className={styles.uploadButton}> <FiPlus /> Add Images <input type="file" accept="image/*" multiple onChange={handleImageUpload} /> </label> </div> {/* Campaign Videos Section */} <h4 className={styles.editMediaSectionTitle}> <FiPlay /> Campaign Videos </h4> {editForm.campaign_video.length > 0 || editForm.newVideos.length > 0 ? ( <div className={styles.mediaUploadGrid}> {/* Existing videos */} {editForm.campaign_video.map((videoUrl, index) => ( <div key={`existing-video-${index}`} className={styles.mediaUploadItem}> <button type="button" className={styles.mediaDeleteButton} onClick={() => handleDeleteVideo(videoUrl)} > × </button> <video src={videoUrl} className={styles.mediaPreviewVideo} controls preload="metadata" /> <span className={styles.mediaTypeBadge}>Video</span> </div> ))} {/* New videos */} {editForm.newVideos.map((file, index) => ( <div key={`new-video-${index}`} className={styles.mediaUploadItem}> <button type="button" className={styles.mediaDeleteButton} onClick={() => handleDeleteNewVideo(index)} > × </button> <video src={URL.createObjectURL(file)} className={styles.mediaPreviewVideo} controls preload="metadata" /> <span className={styles.mediaTypeBadge}>New</span> </div> ))} </div> ) : ( <p className={styles.mediaUploadNotice}>No videos uploaded yet</p> )} <div className={styles.uploadControls}> <label className={styles.uploadButton}> <FiPlus /> Add Videos <input type="file" accept="video/*" multiple onChange={handleVideoUpload} /> </label> </div> </div> <div className={styles.formButtons}> <button type="button" className={styles.cancelButton} onClick={() => setEditModalOpen(false)} disabled={loading} > Cancel </button> <button type="submit" className={styles.submitButton} disabled={loading} > {loading ? 'Saving...' : 'Save Changes'} </button> </div> </form> </div> </div> )} {/* Campaign Details Modal */} {isModalOpen && selectedCampaign && ( <div className={styles.modalOverlay}> <div className={styles.modalContent}> <div className={styles.modalHeader}> <h3 className={styles.modalTitle}> {selectedCampaign.campaign_name} - {selectedCampaign._id} </h3> <button className={styles.modalCloseButton} onClick={handleCloseModal}> × </button> </div> <div className={styles.modalBody}> <div className={styles.modalDetailRow}> <span className={styles.modalDetailLabel}>Description:</span> <span className={styles.modalDetailValue}>{selectedCampaign.description}</span> </div> {selectedCampaign.campaign_mode?.toLowerCase() === "virtual" ? ( <div className={styles.modalDetailRow}> <span className={styles.modalDetailLabel}>Address:</span> <span className={styles.modalDetailValue}> <span className={styles.modalVirtualBadge}>Online — no address </span> </span> </div> ) : ( selectedCampaign.campaign_address && selectedCampaign.campaign_address.length > 0 && ( <div className={styles.modalDetailRow}> <span className={styles.modalDetailLabel}>Campaign Address:</span> <div className={styles.modalDetailValue}> {selectedCampaign.campaign_address.map((address, index) => ( <div key={index} className={styles.modalAddressItem}> <span className={styles.modalAddressText}> {address.location}, {address.city} </span> <button className={styles.modalMapButton} onClick={() => openGoogleMaps(address)} title="View on Google Maps" > <FiMapPin size={16} /> View on Maps </button> </div> ))} </div> </div> ) )} {selectedCampaign.campaign_category.length > 0 && ( <div className={styles.modalDetailRow}> <span className={styles.modalDetailLabel}>Categories:</span> <span className={styles.modalDetailValue}> {selectedCampaign.campaign_category.map((category, index) => ( <span key={index} className={styles.modalBadge}> {category} {index < selectedCampaign.campaign_category.length - 1 ? ", " : ""} </span> ))} </span> </div> )} {selectedCampaign.campaign_agents.length > 0 && ( <div className={styles.modalDetailRow}> <span className={styles.modalDetailLabel}>Agents:</span> <span className={styles.modalDetailValue}> {selectedCampaign.campaign_agents.map((agent, index) => ( <span key={agent.agent_id} className={styles.modalBadge}> {agent.agent_name} {index < selectedCampaign.campaign_agents.length - 1 ? ', ' : ''} </span> ))} </span> </div> )} {selectedCampaign.participating_ngos?.length > 0 && ( <div className={styles.modalDetailRow}> <span className={styles.modalDetailLabel}>Participating NGOs:</span> <span className={styles.modalDetailValue}> {selectedCampaign.participating_ngos .map(ngo => `${ngo.ngo_name} (NGO Share:${100 - (parseInt(ngo.custom_plat_percent) || 0)}%)`) .join(', ')} </span> </div> )} {/* Campaign Media Section */} {(selectedCampaign.campaign_image.length > 0 || selectedCampaign.campaign_video.length > 0) && ( <> <h4 className={styles.modalSectionTitle}>Campaign Media:</h4> <div className={styles.modalMediaGrid}> {/* Campaign Images */} {selectedCampaign.campaign_image.map((imageUrl, index) => ( <div key={`image-${index}`} className={styles.modalMediaItem}> <div className={styles.modalMediaOverlay}> <FiImage className={styles.modalMediaIcon} /> <span className={styles.modalMediaType}>Image</span> </div> <img src={imageUrl || "/placeholder.svg"} alt={`Campaign image ${index + 1}`} className={styles.modalMediaImage} onClick={() => window.open(imageUrl, '_blank')} /> </div> ))} {/* Campaign Videos */} {selectedCampaign.campaign_video.map((videoUrl, index) => ( <div key={`video-${index}`} className={styles.modalMediaItem}> <div className={styles.modalMediaOverlay}> <FiPlay className={styles.modalMediaIcon} /> <span className={styles.modalMediaType}>Video</span> </div> <video src={videoUrl} className={styles.modalMediaVideo} controls preload="metadata" /> </div> ))} </div> </> )} <h4 className={styles.modalSectionTitle}>Campaign Dates & Goal:</h4> <div className={styles.modalDateGoalGrid}> <div className={styles.modalDetailItem}> <span className={styles.modalDetailLabel}>Start Date:</span> <span className={styles.modalDetailValue}>{selectedCampaign.start_date}</span> </div> <div className={styles.modalDetailItem}> <span className={styles.modalDetailLabel}>End Date:</span> <span className={styles.modalDetailValue}>{selectedCampaign.end_date}</span> </div> <div className={styles.modalDetailItem}> <span className={styles.modalDetailLabel}>Goal Amount:</span> <span className={styles.modalDetailValue}>{formatCurrency(selectedCampaign.goal_amount)}</span> </div> <div className={styles.modalDetailItem}> <span className={styles.modalDetailLabel}>Raised Amount:</span> <span className={styles.modalDetailValue}>{formatCurrency(selectedCampaign.raised_amount)}</span> </div> </div> {selectedCampaign.ngoWiseDistribution.length > 0 && ( <> <h4 className={styles.modalSectionTitle}>NGO Wise Distribution:</h4> <div className={styles.modalDistributionTable}> <div className={styles.modalDistributionTableHeader}> <div className={styles.modalDistributionTableHeaderCell}>NGO Name</div> <div className={styles.modalDistributionTableHeaderCell}>Campaign Amount</div> <div className={styles.modalDistributionTableHeaderCell}>NGO Share</div> <div className={styles.modalDistributionTableHeaderCell}>Platform Share</div> <div className={styles.modalDistributionTableHeaderCell}>Plat Percent</div> </div> {selectedCampaign.ngoWiseDistribution.map((dist, index) => ( <div key={`${dist.ngoId}-${index}`} className={styles.modalDistributionTableRow}> <div className={styles.modalDistributionTableCell}>{dist.ngoName}</div> <div className={styles.modalDistributionTableCell}>{formatCurrency(dist.campaignAmount)}</div> <div className={styles.modalDistributionTableCell}>{formatCurrency(dist.ngoShare)}</div> <div className={styles.modalDistributionTableCell}>{formatCurrency(dist.platformShare)}</div> <div className={styles.modalDistributionTableCell}>{dist.platPercent}</div> </div> ))} </div> </> )} </div> </div> </div> )} </div> )}export default CampaignTableCampaigntable.tsx:255 Error deleting campaign media: Error: Deleted 0 of 1 items at deleteCampaignMedia (Campaigntable.tsx:252:13) at async Campaigntable.tsx:420:5deleteCampaignMedia @ Campaigntable.tsx:255await in deleteCampaignMedia(anonymous) @ Campaigntable.tsx:420onClick @ Campaigntable.tsx:855executeDispatch @ react-dom_client.js?v=ae019817:11736runWithFiberInDEV @ react-dom_client.js?v=ae019817:1485processDispatchQueue @ react-dom_client.js?v=ae019817:11772(anonymous) @ react-dom_client.js?v=ae019817:12182batchedUpdates$1 @ react-dom_client.js?v=ae019817:2628dispatchEventForPluginEventSystem @ react-dom_client.js?v=ae019817:11877dispatchEvent @ react-dom_client.js?v=ae019817:14792dispatchDiscreteEvent @ react-dom_client.js?v=ae019817:14773<button>exports.jsxDEV @ react_jsx-dev-runtime.js?v=ae019817:250(anonymous) @ Campaigntable.tsx:852CampaignTable @ Campaigntable.tsx:850react-stack-bottom-frame @ react-dom_client.js?v=ae019817:17424renderWithHooksAgain @ react-dom_client.js?v=ae019817:4281renderWithHooks @ react-dom_client.js?v=ae019817:4217updateFunctionComponent @ react-dom_client.js?v=ae019817:6619beginWork @ react-dom_client.js?v=ae019817:7654runWithFiberInDEV @ react-dom_client.js?v=ae019817:1485performUnitOfWork @ react-dom_client.js?v=ae019817:10868workLoopSync @ react-dom_client.js?v=ae019817:10728renderRootSync @ react-dom_client.js?v=ae019817:10711performWorkOnRoot @ react-dom_client.js?v=ae019817:10330performSyncWorkOnRoot @ react-dom_client.js?v=ae019817:11635flushSyncWorkAcrossRoots_impl @ react-dom_client.js?v=ae019817:11536processRootScheduleInMicrotask @ react-dom_client.js?v=ae019817:11558(anonymous) @ react-dom_client.js?v=ae019817:11649Understand this errorCampaigntable.tsx:427 Error deleting image: Error: Deleted 0 of 1 items at deleteCampaignMedia (Campaigntable.tsx:252:13) at async Campaigntable.tsx:420:5 this is the error getting { "mediaUrls":["https://noblegivingstorage.blob.core.windows.net/campaign-assets/campaign-images/campaign_6892f3bee359387cc421d2711754560879450noblelogo.png?sv=2025-01-05&se=2025-08-07T12%3A27%3A49Z&sr=b&sp=r&sig=D%2Fqk66MEYYTEls9iMob51RAl1I9UyQ68HScGp1djFjU%3D"]} api is working fine frontend issue aara hai mediaurl array main hai
Thanks for the full snippet and the error details. The “Deleted 0 of 1 items” message typically happens because the API receives a different identifier than what it expects for the blob you’re trying to delete. Two frontend issues I see: 1) You’re sending a signed SAS URL with query parameters. Many backends store the clean URL or the blob path only, so they can’t match the SAS variant. 2) You’re deleting immediately on click and also trying to delete again on Save (handleEditSubmit), causing double-delete attempts. Below are minimal, safe changes in plain React + TypeScript (no Next.js, no Tailwind) to fix both issues: What changes we’ll make - Normalize media URLs before sending to the API (strip query string and decode). - Use axios.request({ method: 'delete', ... }) with a JSON body and explicit headers to avoid any axios.delete(version)-with-data quirks. - Track per-item deleting state so you don’t block all delete buttons at once and you can show proper UI state. - Do not re-delete on Save if you already deleted on click. Replace or add the following code 1) Add a normalizer for media URLs (strip query params) Put this near your other utility functions: ```ts // Normalize a media URL so it matches what backend stores. // 1) Strip query string (e.g. SAS token) // 2) Decode URI // If your backend expects a path only (no origin), you can adjust here. const toDeletableMediaUrl = (raw: string): string => { try { const u = new URL(raw); // absolute URL without query/hash const noQuery = `${u.origin}${u.pathname}`; return decodeURI(noQuery); } catch { // Fallback if it's not a full URL return decodeURI(raw.split("?")[0]); } } ``` 2) Update deleteCampaignMedia to always send normalized URLs and use axios.request Replace your deleteCampaignMedia with this: ```ts const deleteCampaignMedia = async (campaignId: string, mediaUrls: string[]): Promise<void> => { try { const cleaned = mediaUrls.map(toDeletableMediaUrl); const headers = { ...(getAuthConfig().headers || {}), "Content-Type": "application/json", Accept: "application/json", }; const response = await axios.request({ method: "delete", url: `${API_BASE_URL}/admin/delete-campaign-media/${campaignId}`, headers, data: { mediaUrls: cleaned }, // body MUST be on "data" for axios }); if (!response.data?.success) { throw new Error(response.data?.message || "Failed to delete media"); } } catch (error) { console.error("Error deleting campaign media:", error); throw error; } }; ``` Note: If your backend expects only the blob path (e.g., campaign-assets/campaign-images/file.png), modify toDeletableMediaUrl to return just u.pathname.slice(1) instead of origin + pathname. 3) Track per-item delete state instead of a single global boolean Add this state (replace your deletingMedia boolean and usages): ```ts const [deletingMediaMap, setDeletingMediaMap] = useState<Record<string, boolean>>({}); const setDeletingFor = (url: string, value: boolean) => setDeletingMediaMap((m) => ({ ...m, [url]: value })); ``` 4) Update your media delete handlers to: - Call the API once on click - Update local state - Do NOT push into deletedImages/deletedVideos (to avoid double-delete on Save) Replace both handlers: ```ts const handleDeleteImage = useCallback(async (imageUrl: string) => { if (!editingCampaign?._id) return; try { setDeletingFor(imageUrl, true); await deleteCampaignMedia(editingCampaign._id, [imageUrl]); // normalized inside setEditForm((prev) => ({ ...prev, campaign_image: prev.campaign_image.filter((img) => img !== imageUrl), // NOTE: do NOT add to deletedImages here; we already deleted on click })); } catch (error) { console.error("Error deleting image:", error); setError("Failed to delete image. Please try again."); } finally { setDeletingFor(imageUrl, false); } }, [editingCampaign]); const handleDeleteVideo = useCallback(async (videoUrl: string) => { if (!editingCampaign?._id) return; try { setDeletingFor(videoUrl, true); await deleteCampaignMedia(editingCampaign._id, [videoUrl]); // normalized inside setEditForm((prev) => ({ ...prev, campaign_video: prev.campaign_video.filter((vid) => vid !== videoUrl), // NOTE: do NOT add to deletedVideos here; we already deleted on click })); } catch (error) { console.error("Error deleting video:", error); setError("Failed to delete video. Please try again."); } finally { setDeletingFor(videoUrl, false); } }, [editingCampaign]); ``` 5) Make sure buttons reflect per-item loading state and the item shows a deleting style Adjust the delete buttons and containers for existing media: Images (existing): ```tsx {editForm.campaign_image.map((imageUrl, index) => { const isDeleting = !!deletingMediaMap[imageUrl]; return ( <div key={`existing-image-${index}`} className={`${styles.mediaUploadItem} ${isDeleting ? styles.deleting : ""}`} > <button type="button" className={styles.mediaDeleteButton} onClick={() => handleDeleteImage(imageUrl)} disabled={isDeleting} title={isDeleting ? "Deleting..." : "Delete"} > {isDeleting ? "…" : "×"} </button> <img src={imageUrl || "/placeholder.svg"} alt={`Campaign image ${index + 1}`} className={styles.mediaPreviewImage} /> <span className={styles.mediaTypeBadge}>Image</span> </div> ); })} ``` Videos (existing): ```tsx {editForm.campaign_video.map((videoUrl, index) => { const isDeleting = !!deletingMediaMap[videoUrl]; return ( <div key={`existing-video-${index}`} className={`${styles.mediaUploadItem} ${isDeleting ? styles.deleting : ""}`} > <button type="button" className={styles.mediaDeleteButton} onClick={() => handleDeleteVideo(videoUrl)} disabled={isDeleting} title={isDeleting ? "Deleting..." : "Delete"} > {isDeleting ? "…" : "×"} </button> <video src={videoUrl} className={styles.mediaPreviewVideo} controls preload="metadata" /> <span className={styles.mediaTypeBadge}>Video</span> </div> ); })} ``` 6) Remove the re-delete step from handleEditSubmit You already deleted on click, so this block should be removed from handleEditSubmit: ```ts // REMOVE this entire block // if (editForm.deletedImages.length > 0 || editForm.deletedVideos.length > 0) { // await deleteCampaignMedia( // editingCampaign._id, // [...editForm.deletedImages, ...editForm.deletedVideos] // ) // } ``` If you prefer to delete only on Save rather than on click: - Don’t call deleteCampaignMedia in handleDeleteImage/handleDeleteVideo. - Instead, push URLs into deletedImages/deletedVideos arrays. - Keep the deletion block in handleEditSubmit and send the normalized URLs there using toDeletableMediaUrl. Why this fixes your error - The API likely stores the media without SAS parameters; sending the clean URL/path allows the backend to match the item, so it won’t return “Deleted 0 of 1 items”. - Avoiding a second deletion attempt prevents 0-of-N errors after the first succeeds. - Using axios.request with method "delete" ensures the JSON body is sent consistently across axios versions, with explicit Content-Type headers. If your backend expects a path-only identifier Change the normalizer like this: ```ts const toDeletableMediaUrl = (raw: string): string => { try { const u = new URL(raw); // Return "campaign-assets/campaign-images/filename.ext" return decodeURI(u.pathname.replace(/^\/+/, "")); } catch { return decodeURI(raw.split("?")[0]).replace(/^\/+/, ""); } }; ``` Let me know whether your API expects: - absolute URL without query, or - path-only (container/folder/filename), or - just filename. I’ll adjust the normalizer accordingly. Actions
CampaignTable.tsx
CampaignTable.module.css
Notes
Want me to flip the normalizer to send path-only and push deletes to Save instead of immediate?