diff --git a/src/components/AdminWalletsModal.js b/src/components/AdminWalletsModal.js
new file mode 100644
index 0000000..d166c61
--- /dev/null
+++ b/src/components/AdminWalletsModal.js
@@ -0,0 +1,327 @@
+import React, { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { useTranslation } from 'react-i18next';
+import {
+ XCircleIcon,
+ PlusIcon,
+ TrashIcon,
+ ShieldCheckIcon,
+ UserCircleIcon,
+} from '@heroicons/react/24/outline';
+
+const AdminWalletsModal = ({
+ isOpen,
+ onClose,
+ getAdminWallets,
+ addAdminWallet,
+ deleteAdminWallet,
+ hasPermission,
+ showToast,
+}) => {
+ const { t } = useTranslation();
+ const [wallets, setWallets] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [actionLoading, setActionLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Form state for adding new wallet
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [newWallet, setNewWallet] = useState({
+ wallet: '',
+ name: '',
+ role: 'admin',
+ });
+
+ // Load wallets on mount
+ useEffect(() => {
+ if (isOpen) {
+ loadWallets();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isOpen]);
+
+ const loadWallets = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await getAdminWallets();
+ setWallets(response.data || response || []);
+ } catch (err) {
+ setError(err.message || t('admin.error_loading_wallets') || 'Error loading wallets');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAddWallet = async (e) => {
+ e.preventDefault();
+ if (!newWallet.wallet.trim()) return;
+
+ // Validate wallet address format
+ if (!/^0x[a-fA-F0-9]{40}$/.test(newWallet.wallet.trim())) {
+ showToast.error(t('admin.invalid_wallet_format') || 'Invalid wallet address format');
+ return;
+ }
+
+ setActionLoading(true);
+ try {
+ await addAdminWallet({
+ wallet: newWallet.wallet.trim().toLowerCase(),
+ name: newWallet.name.trim() || undefined,
+ role: newWallet.role,
+ });
+ showToast.success(t('admin.wallet_added') || 'Wallet added successfully');
+ setNewWallet({ wallet: '', name: '', role: 'admin' });
+ setShowAddForm(false);
+ await loadWallets();
+ } catch (err) {
+ showToast.error(err.message || t('admin.error_adding_wallet') || 'Error adding wallet');
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleDeleteWallet = async (walletId, walletAddress) => {
+ if (!window.confirm(
+ t('admin.confirm_delete_wallet', { wallet: walletAddress }) ||
+ `Are you sure you want to remove ${walletAddress} as admin?`
+ )) {
+ return;
+ }
+
+ setActionLoading(true);
+ try {
+ await deleteAdminWallet(walletId);
+ showToast.success(t('admin.wallet_deleted') || 'Wallet removed successfully');
+ await loadWallets();
+ } catch (err) {
+ showToast.error(err.message || t('admin.error_deleting_wallet') || 'Error removing wallet');
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const formatAddress = (addr) => {
+ if (!addr) return '';
+ return addr.slice(0, 6) + '...' + addr.slice(-4);
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+ e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+
+ {t('admin.manage_wallets') || 'Manage Admin Wallets'}
+
+
+
+
+ {t('admin.manage_wallets_desc') || 'Add or remove wallets that can manage bounties'}
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+
+
{t('common.loading') || 'Loading...'}
+
+ ) : error ? (
+
+
+
{error}
+
+
+ ) : (
+ <>
+ {/* Wallets List */}
+
+ {wallets.length === 0 ? (
+
+
+
{t('admin.no_wallets') || 'No admin wallets configured'}
+
+ ) : (
+ wallets.map((wallet) => (
+
+
+
+
+
+
+
+
+ {formatAddress(wallet.wallet)}
+
+
+ {wallet.role === 'superadmin' ? 'Super Admin' : 'Admin'}
+
+ {wallet.isInitial && (
+
+ {t('admin.protected') || 'Protected'}
+
+ )}
+
+ {wallet.name && (
+
{wallet.name}
+ )}
+
+
+
+ {/* Delete button - only if not initial/protected */}
+ {!wallet.isInitial && hasPermission('manage_admins') && (
+
+ )}
+
+ ))
+ )}
+
+
+ {/* Add Wallet Form */}
+ {hasPermission('manage_admins') && (
+ <>
+ {!showAddForm ? (
+
+ ) : (
+
+
+
+ setNewWallet({ ...newWallet, wallet: e.target.value })}
+ placeholder="0x..."
+ className="w-full px-4 py-2 rounded-lg border border-ultraviolet-darker/20 bg-background-lighter focus:border-ultraviolet focus:ring-1 focus:ring-ultraviolet outline-none text-text-primary font-mono text-sm"
+ required
+ />
+
+
+
+
+ setNewWallet({ ...newWallet, name: e.target.value })}
+ placeholder="e.g., John Doe"
+ className="w-full px-4 py-2 rounded-lg border border-ultraviolet-darker/20 bg-background-lighter focus:border-ultraviolet focus:ring-1 focus:ring-ultraviolet outline-none text-text-primary text-sm"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ )}
+ >
+ )}
+
+
+
+ );
+};
+
+export default AdminWalletsModal;
diff --git a/src/components/BountyForm.js b/src/components/BountyForm.js
index aa6da81..947bac7 100644
--- a/src/components/BountyForm.js
+++ b/src/components/BountyForm.js
@@ -1,27 +1,64 @@
-import React, { useState } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
+import { ChevronDownIcon } from '@heroicons/react/24/outline';
+
+// Tokens disponibles para recompensas (el label de CUSTOM se traduce dinámicamente)
+const REWARD_TOKENS = [
+ { symbol: 'USDC', label: 'USDC', icon: '💵' },
+ { symbol: 'UVD', label: '$UVD', icon: '🟣' },
+ { symbol: 'AVAX', label: 'AVAX', icon: '🔺' },
+ { symbol: 'POL', label: 'POL', icon: '🟪' },
+ { symbol: 'SOL', label: 'SOL', icon: '◎' },
+ { symbol: 'ETH', label: 'ETH', icon: 'Ξ' },
+ { symbol: 'CUSTOM', labelKey: 'bountyForm.custom_token_label', icon: '✏️' },
+];
const BountyForm = ({ onSubmit, loading, error }) => {
const { t } = useTranslation();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
- const [reward, setReward] = useState('');
+ const [rewardAmount, setRewardAmount] = useState('');
+ const [selectedToken, setSelectedToken] = useState(REWARD_TOKENS[0]); // USDC por defecto
+ const [customToken, setCustomToken] = useState('');
+ const [showTokenDropdown, setShowTokenDropdown] = useState(false);
const [endDate, setEndDate] = useState('');
+ const dropdownRef = useRef(null);
+
+ // Cerrar dropdown al hacer clic fuera
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setShowTokenDropdown(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
const resetForm = () => {
setTitle('');
setDescription('');
- setReward('');
+ setRewardAmount('');
+ setSelectedToken(REWARD_TOKENS[0]);
+ setCustomToken('');
setEndDate('');
};
+ // Construir el reward string completo (ej: "100 USDC" o "50 UVD")
+ const getRewardString = () => {
+ const amount = rewardAmount.trim();
+ if (!amount) return '';
+ const tokenSymbol = selectedToken.symbol === 'CUSTOM' ? customToken.trim() : selectedToken.symbol;
+ return tokenSymbol ? `${amount} ${tokenSymbol}` : amount;
+ };
+
const handleSubmit = async (e) => {
e.preventDefault();
try {
await onSubmit({
title,
description,
- reward,
+ reward: getRewardString(),
endDate: endDate || null,
});
// Reset form on successful submission
@@ -31,6 +68,14 @@ const BountyForm = ({ onSubmit, loading, error }) => {
}
};
+ const handleTokenSelect = (token) => {
+ setSelectedToken(token);
+ setShowTokenDropdown(false);
+ if (token.symbol !== 'CUSTOM') {
+ setCustomToken('');
+ }
+ };
+
return (