Compare commits
	
		
			9 Commits
		
	
	
		
			refactor/n
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a1d56bfdd0 | ||
|  | ed9fa61365 | ||
|  | 8f20913f71 | ||
|  | 57714fac9b | ||
|  | 9eee78fa91 | ||
|  | 4b4ade2bfa | ||
|  | 364cdd3b60 | ||
|  | 799f7cfe09 | ||
|  | 2117638305 | 
| @ -15,7 +15,7 @@ const targetNetwork = ref(DEFAULT_NETWORK); | ||||
| const web3Onboard = init({ | ||||
|   wallets: [injected], | ||||
|   chains: Object.values(Networks).map((network) => ({ | ||||
|     id: network.id, | ||||
|     id: `0x${network.id.toString(16)}`, | ||||
|     token: network.nativeCurrency.symbol, | ||||
|     label: network.name, | ||||
|     rpcUrl: network.rpcUrls.default.http[0], | ||||
|  | ||||
| @ -94,6 +94,10 @@ const getValidDeposits = async ( | ||||
| 
 | ||||
|   // remove doubles from sellers list
 | ||||
|   const depositData = await depositLogs.json(); | ||||
|   if (!depositData.data) { | ||||
|     console.error("Error fetching deposit logs"); | ||||
|     return []; | ||||
|   } | ||||
|   const depositAddeds = depositData.data.depositAddeds; | ||||
|   const uniqueSellers = depositAddeds.reduce( | ||||
|     (acc: Record<Address, boolean>, deposit: any) => { | ||||
|  | ||||
| @ -233,17 +233,17 @@ const handleSubmit = async (e: Event): Promise<void> => { | ||||
|           <div class="flex gap-2"> | ||||
|             <img | ||||
|               alt="Rootstock image" | ||||
|               src="@/assets/rootstock.svg?url" | ||||
|               src="@/assets/networks/rootstock.svg?url" | ||||
|               width="24" | ||||
|               height="24" | ||||
|               v-if=" | ||||
|                 selectedDeposits && | ||||
|                 selectedDeposits.find((d) => d.network == Networks.rootstockTestnet) | ||||
|                 selectedDeposits.find((d) => d.network == Networks.rootstock) | ||||
|               " | ||||
|             /> | ||||
|             <img | ||||
|               alt="Ethereum image" | ||||
|               src="@/assets/ethereum.svg?url" | ||||
|               src="@/assets/networks/ethereum.svg?url" | ||||
|               width="24" | ||||
|               height="24" | ||||
|               v-if=" | ||||
|  | ||||
							
								
								
									
										73
									
								
								src/components/Explorer/AnalyticsCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/components/Explorer/AnalyticsCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| <script setup lang="ts"> | ||||
| interface Props { | ||||
|   title: string; | ||||
|   value: string; | ||||
|   change?: string; | ||||
|   changeType?: 'positive' | 'negative' | 'neutral'; | ||||
|   icon?: string; | ||||
|   loading?: boolean; | ||||
| } | ||||
| 
 | ||||
| const props = withDefaults(defineProps<Props>(), { | ||||
|   changeType: 'neutral', | ||||
|   loading: false | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div class="analytics-card"> | ||||
|     <div class="analytics-content"> | ||||
|       <div v-if="loading" class="analytics-value"> | ||||
|         <div class="animate-pulse bg-gray-300 h-8 w-16 rounded"></div> | ||||
|       </div> | ||||
|       <div v-else class="analytics-value">{{ value }}</div> | ||||
|       <div class="analytics-title">{{ title }}</div> | ||||
|       <div v-if="change && !loading" class="analytics-change" :class="`change-${changeType}`"> | ||||
|         {{ change }} | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-if="icon && !loading" class="analytics-icon"> | ||||
|       <img :src="icon" :alt="`${title} icon`" class="w-8 h-8" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .analytics-card { | ||||
|   @apply bg-white rounded-lg border border-gray-200 p-6 flex items-center justify-between shadow-lg; | ||||
| } | ||||
| 
 | ||||
| .analytics-content { | ||||
|   @apply flex flex-col; | ||||
| } | ||||
| 
 | ||||
| .analytics-value { | ||||
|   @apply text-2xl font-bold text-amber-400 mb-1 break-words overflow-hidden; | ||||
|   word-break: break-all; | ||||
|   max-width: 100%; | ||||
| } | ||||
| 
 | ||||
| .analytics-title { | ||||
|   @apply text-sm text-gray-900 mb-1; | ||||
| } | ||||
| 
 | ||||
| .analytics-change { | ||||
|   @apply text-xs font-medium; | ||||
| } | ||||
| 
 | ||||
| .change-positive { | ||||
|   @apply text-green-600; | ||||
| } | ||||
| 
 | ||||
| .change-negative { | ||||
|   @apply text-red-600; | ||||
| } | ||||
| 
 | ||||
| .change-neutral { | ||||
|   @apply text-gray-600; | ||||
| } | ||||
| 
 | ||||
| .analytics-icon { | ||||
|   @apply flex-shrink-0; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										279
									
								
								src/components/Explorer/TransactionTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								src/components/Explorer/TransactionTable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,279 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| interface Transaction { | ||||
|   id: string; | ||||
|   type: 'deposit' | 'lock' | 'release' | 'return'; | ||||
|   timestamp: string; | ||||
|   seller?: string; | ||||
|   buyer?: string | null; | ||||
|   amount: string; | ||||
|   token: string; | ||||
|   blockNumber: string; | ||||
|   transactionHash: string; | ||||
| } | ||||
| 
 | ||||
| interface Props { | ||||
|   transactions: Transaction[]; | ||||
|   networkExplorerUrl: string; | ||||
| } | ||||
| 
 | ||||
| const props = defineProps<Props>(); | ||||
| 
 | ||||
| const copyFeedback = ref<{ [key: string]: boolean }>({}); | ||||
| const copyFeedbackTimeout = ref<{ [key: string]: NodeJS.Timeout | null }>({}); | ||||
| 
 | ||||
| const getTransactionTypeInfo = (type: string) => { | ||||
|   const typeMap = { | ||||
|     deposit: { label: 'Depósito', status: 'completed' as const }, | ||||
|     lock: { label: 'Bloqueio', status: 'open' as const }, | ||||
|     release: { label: 'Liberação', status: 'completed' as const }, | ||||
|     return: { label: 'Retorno', status: 'expired' as const } | ||||
|   }; | ||||
|   return typeMap[type as keyof typeof typeMap] || { label: type, status: 'pending' as const }; | ||||
| }; | ||||
| 
 | ||||
| const getTransactionTypeColor = (type: string) => { | ||||
|   const colorMap = { | ||||
|     deposit: 'text-emerald-600', | ||||
|     lock: 'text-amber-600',  | ||||
|     release: 'text-emerald-600', | ||||
|     return: 'text-gray-600' | ||||
|   }; | ||||
|   return colorMap[type as keyof typeof colorMap] || 'text-gray-600'; | ||||
| }; | ||||
| 
 | ||||
| const formatAddress = (address: string) => { | ||||
|   return `${address.slice(0, 6)}...${address.slice(-4)}`; | ||||
| }; | ||||
| 
 | ||||
| const formatAmount = (amount: string, decimals: number = 18): string => { | ||||
|   const num = parseFloat(amount) / Math.pow(10, decimals); | ||||
|   return num.toString(); | ||||
| }; | ||||
| 
 | ||||
| const getExplorerUrl = (txHash: string) => { | ||||
|   return `${props.networkExplorerUrl}/tx/${txHash}`; | ||||
| }; | ||||
| 
 | ||||
| const copyToClipboard = async (address: string, key: string) => { | ||||
|   if (!address) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     await navigator.clipboard.writeText(address); | ||||
| 
 | ||||
|     if (copyFeedbackTimeout.value[key]) { | ||||
|       clearTimeout(copyFeedbackTimeout.value[key]!); | ||||
|     } | ||||
| 
 | ||||
|     copyFeedback.value[key] = true; | ||||
| 
 | ||||
|     copyFeedbackTimeout.value[key] = setTimeout(() => { | ||||
|       copyFeedback.value[key] = false; | ||||
|     }, 2000); | ||||
|   } catch (error) { | ||||
|     console.error('Error copying to clipboard:', error); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="hidden lg:block overflow-x-auto"> | ||||
|       <table class="w-full"> | ||||
|         <thead> | ||||
|           <tr class="border-b border-gray-200"> | ||||
|             <th class="text-left py-3 px-4 text-gray-700 font-medium">Horário</th> | ||||
|             <th class="text-left py-3 px-4 text-gray-700 font-medium">Tipo</th> | ||||
|             <th class="text-left py-3 px-4 text-gray-700 font-medium">Participantes</th> | ||||
|             <th class="text-left py-3 px-4 text-gray-700 font-medium">Valor</th> | ||||
|             <th class="text-left py-3 px-4 text-gray-700 font-medium">Bloco</th> | ||||
|             <th class="text-left py-3 px-4 text-gray-700 font-medium">Ações</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr  | ||||
|             v-for="transaction in transactions"  | ||||
|             :key="transaction.id" | ||||
|             class="border-b border-gray-100 hover:bg-gray-50 transition-colors" | ||||
|           > | ||||
|             <td class="py-4 px-4"> | ||||
|               <div class="text-sm text-gray-600">{{ transaction.timestamp }}</div> | ||||
|             </td> | ||||
|             <td class="py-4 px-4"> | ||||
|               <span  | ||||
|                 :class="getTransactionTypeColor(transaction.type)" | ||||
|                 class="text-sm font-medium" | ||||
|               > | ||||
|                 {{ getTransactionTypeInfo(transaction.type).label }} | ||||
|               </span> | ||||
|             </td> | ||||
|             <td class="py-4 px-4"> | ||||
|               <div class="space-y-1"> | ||||
|                 <div v-if="transaction.seller" class="text-sm"> | ||||
|                   <span class="text-gray-600">Vendedor: </span> | ||||
|                   <div class="relative inline-block"> | ||||
|                     <span  | ||||
|                       class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors" | ||||
|                       @click="copyToClipboard(transaction.seller, `seller-${transaction.id}`)" | ||||
|                       title="Copiar" | ||||
|                     > | ||||
|                       {{ formatAddress(transaction.seller) }} | ||||
|                     </span> | ||||
|                     <transition name="fade"> | ||||
|                       <span | ||||
|                         v-if="copyFeedback[`seller-${transaction.id}`]" | ||||
|                         class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10" | ||||
|                       > | ||||
|                         Copiado! | ||||
|                       </span> | ||||
|                     </transition> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div v-if="transaction.buyer" class="text-sm"> | ||||
|                   <span class="text-gray-600">Comprador: </span> | ||||
|                   <div class="relative inline-block"> | ||||
|                     <span  | ||||
|                       class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors" | ||||
|                       @click="copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)" | ||||
|                       title="Copiar" | ||||
|                     > | ||||
|                       {{ formatAddress(transaction.buyer) }} | ||||
|                     </span> | ||||
|                     <transition name="fade"> | ||||
|                       <span | ||||
|                         v-if="copyFeedback[`buyer-${transaction.id}`]" | ||||
|                         class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10" | ||||
|                       > | ||||
|                         Copiado! | ||||
|                       </span> | ||||
|                     </transition> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="py-4 px-4"> | ||||
|               <div class="text-sm font-semibold text-emerald-600"> | ||||
|                 {{ formatAmount(transaction.amount, 18) }} BRZ | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="py-4 px-4"> | ||||
|               <div class="text-sm text-gray-600 font-mono"> | ||||
|                 #{{ transaction.blockNumber }} | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="py-4 px-4"> | ||||
|               <a | ||||
|                 :href="getExplorerUrl(transaction.transactionHash)" | ||||
|                 target="_blank" | ||||
|                 rel="noopener noreferrer" | ||||
|                 class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors" | ||||
|               > | ||||
|                 Explorador | ||||
|               </a> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Mobile Cards --> | ||||
|     <div class="lg:hidden space-y-4"> | ||||
|       <div  | ||||
|         v-for="transaction in transactions"  | ||||
|         :key="transaction.id" | ||||
|         class="bg-gray-50 rounded-lg p-4 border border-gray-200" | ||||
|       > | ||||
|         <div class="flex items-center justify-between mb-3"> | ||||
|           <span  | ||||
|             :class="getTransactionTypeColor(transaction.type)" | ||||
|             class="text-sm font-medium" | ||||
|           > | ||||
|             {{ getTransactionTypeInfo(transaction.type).label }} | ||||
|           </span> | ||||
|           <div class="text-sm text-gray-600">{{ transaction.timestamp }}</div> | ||||
|         </div> | ||||
|          | ||||
|         <div class="space-y-2 mb-4"> | ||||
|           <div v-if="transaction.seller" class="text-sm"> | ||||
|             <span class="text-gray-600">Vendedor: </span> | ||||
|             <div class="relative inline-block"> | ||||
|               <span  | ||||
|                 class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors" | ||||
|                 @click="copyToClipboard(transaction.seller, `seller-${transaction.id}`)" | ||||
|                 title="Copiar" | ||||
|               > | ||||
|                 {{ formatAddress(transaction.seller) }} | ||||
|               </span> | ||||
|               <transition name="fade"> | ||||
|                 <span | ||||
|                   v-if="copyFeedback[`seller-${transaction.id}`]" | ||||
|                   class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10" | ||||
|                 > | ||||
|                   Copiado! | ||||
|                 </span> | ||||
|               </transition> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div v-if="transaction.buyer" class="text-sm"> | ||||
|             <span class="text-gray-600">Comprador: </span> | ||||
|             <div class="relative inline-block"> | ||||
|               <span  | ||||
|                 class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors" | ||||
|                 @click="copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)" | ||||
|                 title="Copiar" | ||||
|               > | ||||
|                 {{ formatAddress(transaction.buyer) }} | ||||
|               </span> | ||||
|               <transition name="fade"> | ||||
|                 <span | ||||
|                   v-if="copyFeedback[`buyer-${transaction.id}`]" | ||||
|                   class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10" | ||||
|                 > | ||||
|                   Copiado! | ||||
|                 </span> | ||||
|               </transition> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="text-sm"> | ||||
|             <span class="text-gray-600">Valor: </span> | ||||
|               <span class="font-semibold text-emerald-600">{{ formatAmount(transaction.amount, 18) }} BRZ</span> | ||||
|             </div> | ||||
|           <div class="text-sm"> | ||||
|             <span class="text-gray-600">Bloco: </span> | ||||
|             <span class="text-gray-900 font-mono">#{{ transaction.blockNumber }}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         <a | ||||
|           :href="getExplorerUrl(transaction.transactionHash)" | ||||
|           target="_blank" | ||||
|           rel="noopener noreferrer" | ||||
|           class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors" | ||||
|         > | ||||
|           Ver no Explorador | ||||
|         </a> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Empty State --> | ||||
|     <div v-if="transactions.length === 0" class="text-center py-12"> | ||||
|       <div class="text-gray-500 text-lg mb-2">📭</div> | ||||
|       <p class="text-gray-600">Nenhuma transação encontrada</p> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
|   transition: opacity 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
|   opacity: 0; | ||||
| } | ||||
| </style> | ||||
| @ -90,13 +90,13 @@ const handleInputEvent = (event: any): void => { | ||||
|           <div class="flex gap-2"> | ||||
|             <img | ||||
|               alt="Polygon image" | ||||
|               src="@/assets/polygon.svg?url" | ||||
|               src="@/assets/networks/polygon.svg?url" | ||||
|               width="24" | ||||
|               height="24" | ||||
|             /> | ||||
|             <img | ||||
|               alt="Ethereum image" | ||||
|               src="@/assets/ethereum.svg?url" | ||||
|               src="@/assets/networks/ethereum.svg?url" | ||||
|               width="24" | ||||
|               height="24" | ||||
|             /> | ||||
|  | ||||
| @ -155,6 +155,19 @@ onClickOutside(infoMenuRef, () => { | ||||
|           > | ||||
|             <div class="mt-2"> | ||||
|               <div class="bg-white rounded-md z-10 -left-36 w-52"> | ||||
|                 <RouterLink | ||||
|                   :to="'/explore'" | ||||
|                   class="menu-button gap-2 px-4 rounded-md cursor-pointer" | ||||
|                 > | ||||
|                   <span | ||||
|                     class="text-gray-900 py-4 text-end font-semibold text-sm whitespace-nowrap" | ||||
|                   > | ||||
|                     Explorar Transações | ||||
|                   </span> | ||||
|                 </RouterLink> | ||||
|                 <div class="w-full flex justify-center"> | ||||
|                   <hr class="w-4/5" /> | ||||
|                 </div> | ||||
|                 <div class="menu-button gap-2 px-4 rounded-md cursor-pointer"> | ||||
|                   <span | ||||
|                     class="text-gray-900 py-4 text-end font-semibold text-sm" | ||||
| @ -412,6 +425,9 @@ onClickOutside(infoMenuRef, () => { | ||||
|           <div class="w-full flex justify-center"> | ||||
|             <hr class="w-4/5" /> | ||||
|           </div> | ||||
|           <div class="w-full flex justify-center"> | ||||
|             <hr class="w-4/5" /> | ||||
|           </div> | ||||
|           <div class="menu-button" @click="closeMenu()"> | ||||
|             <RouterLink to="/manage_bids" class="redirect_button"> | ||||
|               Gerenciar Ofertas | ||||
|  | ||||
| @ -33,7 +33,7 @@ const props = withDefaults( | ||||
| } | ||||
| 
 | ||||
| .form-card:not(.no-border) { | ||||
|   @apply border-y-10; | ||||
|   @apply border-y; | ||||
| } | ||||
| 
 | ||||
| .form-card.full-width { | ||||
|  | ||||
							
								
								
									
										475
									
								
								src/composables/useGraphQL.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										475
									
								
								src/composables/useGraphQL.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,475 @@ | ||||
| import { NetworkConfig } from '@/model/NetworkEnum'; | ||||
| import { ref, computed, type Ref } from 'vue'; | ||||
| import { isTestnetEnvironment } from '@/config/networks'; | ||||
| import { sepolia, rootstock, rootstockTestnet } from "viem/chains"; | ||||
| 
 | ||||
| export interface Transaction { | ||||
|   id: string; | ||||
|   type: 'deposit' | 'lock' | 'release' | 'return'; | ||||
|   timestamp: string; | ||||
|   blockTimestamp: string; | ||||
|   seller?: string; | ||||
|   buyer?: string | null; | ||||
|   amount: string; | ||||
|   token: string; | ||||
|   blockNumber: string; | ||||
|   transactionHash: string; | ||||
| } | ||||
| 
 | ||||
| export interface AnalyticsData { | ||||
|   totalVolume: string; | ||||
|   totalTransactions: string; | ||||
|   totalLocks: string; | ||||
|   totalDeposits: string; | ||||
|   totalReleases: string; | ||||
| } | ||||
| 
 | ||||
| export function useGraphQL(network: Ref<NetworkConfig>) { | ||||
|   const searchAddress = ref(''); | ||||
|   const selectedType = ref('all'); | ||||
|   const loading = ref(false); | ||||
|   const error = ref<string | null>(null); | ||||
|   const analyticsLoading = ref(false); | ||||
|    | ||||
|   const transactionsData = ref<Transaction[]>([]); | ||||
|   const analyticsData = ref<AnalyticsData>({ | ||||
|     totalVolume: '0', | ||||
|     totalTransactions: '0', | ||||
|     totalLocks: '0', | ||||
|     totalDeposits: '0', | ||||
|     totalReleases: '0' | ||||
|   }); | ||||
| 
 | ||||
|   const getGraphQLUrl = () => { | ||||
|     const rskNetworkName = isTestnetEnvironment() ? rootstockTestnet.name : rootstock.name;   | ||||
|      | ||||
|     switch (network.value.name) { | ||||
|       case sepolia.name: | ||||
|         return import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL!; | ||||
|       case rskNetworkName: | ||||
|         return import.meta.env.VITE_RSK_SUBGRAPH_URL!; | ||||
|       default: | ||||
|         throw new Error(`Unsupported network: ${network.value.name}`); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const executeQuery = async (query: string, variables: any = {}) => { | ||||
|     const url = getGraphQLUrl(); | ||||
|      | ||||
|     try { | ||||
|       const response = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { | ||||
|           'Content-Type': 'application/json', | ||||
|         }, | ||||
|         body: JSON.stringify({ | ||||
|           query, | ||||
|           variables, | ||||
|         }), | ||||
|       }); | ||||
| 
 | ||||
|       if (!response.ok) { | ||||
|         throw new Error(`HTTP error! status: ${response.status}`); | ||||
|       } | ||||
| 
 | ||||
|       const data = await response.json(); | ||||
|        | ||||
|       if (data.errors) { | ||||
|         throw new Error(data.errors[0]?.message || 'GraphQL error'); | ||||
|       } | ||||
| 
 | ||||
|       return data.data; | ||||
|     } catch (err) { | ||||
|       console.error('GraphQL query error:', err); | ||||
|       throw err; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const fetchAllActivity = async () => { | ||||
|     loading.value = true; | ||||
|     error.value = null; | ||||
| 
 | ||||
|     const query = ` | ||||
|       query GetAllActivity($first: Int = 50) { | ||||
|         depositAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           seller | ||||
|           token | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         depositWithdrawns(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           seller | ||||
|           token | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         lockAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           buyer | ||||
|           lockID | ||||
|           seller | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         lockReleaseds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           buyer | ||||
|           lockId | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         lockReturneds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           buyer | ||||
|           lockId | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|       } | ||||
|     `;
 | ||||
| 
 | ||||
|     try { | ||||
|       const data = await executeQuery(query, { first: 50 }); | ||||
|       transactionsData.value = processActivityData(data); | ||||
|     } catch (err) { | ||||
|       error.value = err instanceof Error ? err.message : 'Failed to fetch transactions'; | ||||
|     } finally { | ||||
|       loading.value = false; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const fetchUserActivity = async (userAddress: string) => { | ||||
|     loading.value = true; | ||||
|     error.value = null; | ||||
| 
 | ||||
|     const query = ` | ||||
|       query GetUserActivity($userAddress: String!, $first: Int = 50) { | ||||
|         depositAddeds(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           seller | ||||
|           token | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         depositWithdrawns(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           seller | ||||
|           token | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         lockAddeds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           buyer | ||||
|           lockID | ||||
|           seller | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         lockReleaseds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           buyer | ||||
|           lockId | ||||
|           amount | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|         lockReturneds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") { | ||||
|           id | ||||
|           buyer | ||||
|           lockId | ||||
|           blockNumber | ||||
|           blockTimestamp | ||||
|           transactionHash | ||||
|         } | ||||
|       } | ||||
|     `;
 | ||||
| 
 | ||||
|     try { | ||||
|       const data = await executeQuery(query, { userAddress, first: 50 }); | ||||
|       transactionsData.value = processActivityData(data); | ||||
|     } catch (err) { | ||||
|       error.value = err instanceof Error ? err.message : 'Failed to fetch user transactions'; | ||||
|     } finally { | ||||
|       loading.value = false; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const clearData = () => { | ||||
|     transactionsData.value = []; | ||||
|     analyticsData.value = { | ||||
|       totalVolume: '0', | ||||
|       totalTransactions: '0', | ||||
|       totalLocks: '0', | ||||
|       totalDeposits: '0', | ||||
|       totalReleases: '0' | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   const fetchAnalytics = async () => { | ||||
|     analyticsLoading.value = true; | ||||
| 
 | ||||
|     const query = ` | ||||
|       query GetAnalytics { | ||||
|         depositAddeds(first: 1000) { | ||||
|           amount | ||||
|           blockTimestamp | ||||
|         } | ||||
|         depositWithdrawns(first: 1000) { | ||||
|           amount | ||||
|           blockTimestamp | ||||
|         } | ||||
|         lockAddeds(first: 1000) { | ||||
|           amount | ||||
|           blockTimestamp | ||||
|         } | ||||
|         lockReleaseds(first: 1000) { | ||||
|           amount | ||||
|           blockTimestamp | ||||
|         } | ||||
|         lockReturneds(first: 1000) { | ||||
|           blockTimestamp | ||||
|         } | ||||
|       } | ||||
|     `;
 | ||||
| 
 | ||||
|     try { | ||||
|       const data = await executeQuery(query); | ||||
|       analyticsData.value = processAnalyticsData(data); | ||||
|     } catch (err) { | ||||
|       console.error('Failed to fetch analytics:', err); | ||||
|     } finally { | ||||
|       analyticsLoading.value = false; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const processActivityData = (data: any): Transaction[] => { | ||||
|     if (!data) return []; | ||||
|      | ||||
|     const activities: Transaction[] = []; | ||||
|      | ||||
|     if (data.depositAddeds) { | ||||
|       data.depositAddeds.forEach((deposit: any) => { | ||||
|         activities.push({ | ||||
|           id: deposit.id, | ||||
|           blockNumber: deposit.blockNumber, | ||||
|           blockTimestamp: deposit.blockTimestamp, | ||||
|           transactionHash: deposit.transactionHash, | ||||
|           type: 'deposit', | ||||
|           seller: deposit.seller, | ||||
|           buyer: undefined, | ||||
|           amount: deposit.amount, | ||||
|           token: deposit.token, | ||||
|           timestamp: formatTimestamp(deposit.blockTimestamp) | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     if (data.depositWithdrawns) { | ||||
|       data.depositWithdrawns.forEach((withdrawal: any) => { | ||||
|         activities.push({ | ||||
|           id: withdrawal.id, | ||||
|           blockNumber: withdrawal.blockNumber, | ||||
|           blockTimestamp: withdrawal.blockTimestamp, | ||||
|           transactionHash: withdrawal.transactionHash, | ||||
|           type: 'deposit', // Treat as deposit withdrawal
 | ||||
|           seller: withdrawal.seller, | ||||
|           buyer: undefined, | ||||
|           amount: withdrawal.amount, | ||||
|           token: withdrawal.token, | ||||
|           timestamp: formatTimestamp(withdrawal.blockTimestamp) | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     if (data.lockAddeds) { | ||||
|       data.lockAddeds.forEach((lock: any) => { | ||||
|         activities.push({ | ||||
|           id: lock.id, | ||||
|           blockNumber: lock.blockNumber, | ||||
|           blockTimestamp: lock.blockTimestamp, | ||||
|           transactionHash: lock.transactionHash, | ||||
|           type: 'lock', | ||||
|           seller: lock.seller, | ||||
|           buyer: lock.buyer, | ||||
|           amount: lock.amount, | ||||
|           token: lock.token, | ||||
|           timestamp: formatTimestamp(lock.blockTimestamp) | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     if (data.lockReleaseds) { | ||||
|       data.lockReleaseds.forEach((release: any) => { | ||||
|         activities.push({ | ||||
|           id: release.id, | ||||
|           blockNumber: release.blockNumber, | ||||
|           blockTimestamp: release.blockTimestamp, | ||||
|           transactionHash: release.transactionHash, | ||||
|           type: 'release', | ||||
|           seller: undefined, // Release doesn't have seller info
 | ||||
|           buyer: release.buyer, | ||||
|           amount: release.amount, | ||||
|           token: 'BRZ', // Default token
 | ||||
|           timestamp: formatTimestamp(release.blockTimestamp) | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     if (data.lockReturneds) { | ||||
|       data.lockReturneds.forEach((returnTx: any) => { | ||||
|         activities.push({ | ||||
|           id: returnTx.id, | ||||
|           blockNumber: returnTx.blockNumber, | ||||
|           blockTimestamp: returnTx.blockTimestamp, | ||||
|           transactionHash: returnTx.transactionHash, | ||||
|           type: 'return', | ||||
|           seller: undefined, // Return doesn't have seller info
 | ||||
|           buyer: returnTx.buyer, | ||||
|           amount: '0', // Return doesn't have amount
 | ||||
|           token: 'BRZ', // Default token
 | ||||
|           timestamp: formatTimestamp(returnTx.blockTimestamp) | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     return activities.sort((a, b) => parseInt(b.blockTimestamp) - parseInt(a.blockTimestamp)); | ||||
|   }; | ||||
| 
 | ||||
|   const formatTimestamp = (timestamp: string): string => { | ||||
|     const now = Date.now() / 1000; | ||||
|     const diff = now - parseInt(timestamp); | ||||
|      | ||||
|     if (diff < 60) return 'Just now'; | ||||
|     if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`; | ||||
|     if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`; | ||||
|     return `${Math.floor(diff / 86400)} days ago`; | ||||
|   }; | ||||
| 
 | ||||
|   const formatAmount = (amount: string): string => { | ||||
|     const num = parseFloat(amount); | ||||
|     if (num >= 1000000000000000) return `${(num / 1000000000000000).toFixed(1)}Q`; | ||||
|     if (num >= 1000000000000) return `${(num / 1000000000000).toFixed(1)}T`; | ||||
|     if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`; | ||||
|     if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; | ||||
|     if (num >= 1000) return `${(num / 1000).toFixed(1)}K`; | ||||
|     if (num < 1) return num.toFixed(4); | ||||
|     return num.toFixed(2); | ||||
|   }; | ||||
| 
 | ||||
|   const processAnalyticsData = (data: any): AnalyticsData => { | ||||
|     if (!data) { | ||||
|       return { | ||||
|         totalVolume: '0', | ||||
|         totalTransactions: '0', | ||||
|         totalLocks: '0', | ||||
|         totalDeposits: '0', | ||||
|         totalReleases: '0' | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     let totalVolume = 0; | ||||
|     let totalTransactions = 0; | ||||
|     let totalLocks = 0; | ||||
|     let totalDeposits = 0; | ||||
|     let totalReleases = 0; | ||||
| 
 | ||||
|     if (data.depositAddeds) { | ||||
|       data.depositAddeds.forEach((deposit: any) => { | ||||
|         totalVolume += parseFloat(deposit.amount || '0'); | ||||
|         totalTransactions++; | ||||
|         totalDeposits++; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if (data.depositWithdrawns) { | ||||
|       data.depositWithdrawns.forEach((withdrawal: any) => { | ||||
|         totalVolume += parseFloat(withdrawal.amount || '0'); | ||||
|         totalTransactions++; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if (data.lockAddeds) { | ||||
|       data.lockAddeds.forEach((lock: any) => { | ||||
|         totalVolume += parseFloat(lock.amount || '0'); | ||||
|         totalTransactions++; | ||||
|         totalLocks++; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if (data.lockReleaseds) { | ||||
|       data.lockReleaseds.forEach((release: any) => { | ||||
|         totalVolume += parseFloat(release.amount || '0'); | ||||
|         totalTransactions++; | ||||
|         totalReleases++; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     if (data.lockReturneds) { | ||||
|       data.lockReturneds.forEach((returnTx: any) => { | ||||
|         totalTransactions++; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     const result = { | ||||
|       totalVolume: formatAmount(totalVolume.toString()), | ||||
|       totalTransactions: totalTransactions.toString(), | ||||
|       totalLocks: totalLocks.toString(), | ||||
|       totalDeposits: totalDeposits.toString(), | ||||
|       totalReleases: totalReleases.toString() | ||||
|     }; | ||||
|      | ||||
|     return result; | ||||
|   }; | ||||
| 
 | ||||
|   const filteredTransactions = computed(() => { | ||||
|     let filtered = transactionsData.value; | ||||
|      | ||||
|     if (selectedType.value !== 'all') { | ||||
|       filtered = filtered.filter(tx => tx.type === selectedType.value); | ||||
|     } | ||||
|      | ||||
|     if (searchAddress.value) { | ||||
|       const searchLower = searchAddress.value.toLowerCase(); | ||||
|       filtered = filtered.filter(tx =>  | ||||
|         tx.seller?.toLowerCase().includes(searchLower) || | ||||
|         tx.buyer?.toLowerCase().includes(searchLower) | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     return filtered; | ||||
|   }); | ||||
| 
 | ||||
|   return { | ||||
|     searchAddress, | ||||
|     selectedType, | ||||
|     transactions: filteredTransactions, | ||||
|     analytics: analyticsData, | ||||
|     loading, | ||||
|     error, | ||||
|     analyticsLoading, | ||||
|     fetchAllActivity, | ||||
|     fetchUserActivity, | ||||
|     fetchAnalytics, | ||||
|     clearData | ||||
|   }; | ||||
| } | ||||
| @ -1,22 +1,28 @@ | ||||
| import { sepolia, rootstockTestnet } from "viem/chains"; | ||||
| import { sepolia, rootstock, rootstockTestnet } from "viem/chains"; | ||||
| import { NetworkConfig } from "@/model/NetworkEnum" | ||||
| // TODO: import addresses from p2pix-smart-contracts deployments
 | ||||
| 
 | ||||
| export const isTestnetEnvironment = () => { | ||||
|   return import.meta.env.VITE_ENVIRONMENT === 'testnet' ||  | ||||
|          import.meta.env.NODE_ENV === 'development' || | ||||
|          import.meta.env.MODE === 'development'; | ||||
| }; | ||||
| 
 | ||||
| export const Networks: {[key:string]: NetworkConfig} = { | ||||
|   sepolia: { ...sepolia, | ||||
|     rpcUrls: { default: { http: [import.meta.env.VITE_SEPOLIA_API_URL]}}, | ||||
|     contracts: { ...sepolia.contracts, | ||||
|       p2pix: {address:"0xb7cD135F5eFD9760981e02E2a898790b688939fe"} }, | ||||
|       p2pix: { address: import.meta.env.VITE_SEPOLIA_P2PIX_ADDRESS } }, | ||||
|     tokens: { | ||||
|       BRZ: {address:"0x3eBE67A2C7bdB2081CBd34ba3281E90377462289"} }, | ||||
|       BRZ: { address: import.meta.env.VITE_SEPOLIA_TOKEN_ADDRESS } }, | ||||
|     subgraphUrls: [import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL] | ||||
|   }, | ||||
|   rootstockTestnet: { ...rootstockTestnet, | ||||
|   rootstock: { ...(isTestnetEnvironment() ? rootstockTestnet : rootstock), | ||||
|     rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL]}}, | ||||
|     contracts: { ...rootstockTestnet.contracts, | ||||
|       p2pix: {address:"0x57Dcba05980761169508886eEdc6f5E7EC0411Dc"} }, | ||||
|     contracts: { ...(isTestnetEnvironment() ? rootstockTestnet.contracts : rootstock.contracts), | ||||
|       p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS } }, | ||||
|     tokens: { | ||||
|       BRZ: {address:"0xfE841c74250e57640390f46d914C88d22C51e82e"} }, | ||||
|       BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS } }, | ||||
|     subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL] | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| @ -1,11 +1,14 @@ | ||||
| import { createRouter, createWebHistory } from "vue-router"; | ||||
| import { createRouter, createWebHistory, createWebHashHistory } from "vue-router"; | ||||
| import HomeView from "@/views/HomeView.vue"; | ||||
| import FaqView from "@/views/FaqView.vue"; | ||||
| import ManageBidsView from "@/views/ManageBidsView.vue"; | ||||
| import SellerView from "@/views/SellerView.vue"; | ||||
| import ExploreView from "@/views/ExploreView.vue"; | ||||
| 
 | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(import.meta.env.BASE_URL), | ||||
|   history: import.meta.env.MODE === 'production' && import.meta.env.BASE_URL === './'  | ||||
|     ? createWebHashHistory()  | ||||
|     : createWebHistory(import.meta.env.BASE_URL), | ||||
|   routes: [ | ||||
|     { | ||||
|       path: "/", | ||||
| @ -33,6 +36,11 @@ const router = createRouter({ | ||||
|       name: "faq", | ||||
|       component: FaqView, | ||||
|     }, | ||||
|     { | ||||
|       path: "/explore", | ||||
|       name: "explore", | ||||
|       component: ExploreView, | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										163
									
								
								src/views/ExploreView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/views/ExploreView.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,163 @@ | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, watch } from 'vue'; | ||||
| import { useUser } from '@/composables/useUser'; | ||||
| import { useGraphQL } from '@/composables/useGraphQL'; | ||||
| import FormCard from '@/components/ui/FormCard.vue'; | ||||
| import LoadingComponent from '@/components/ui/LoadingComponent.vue'; | ||||
| import AnalyticsCard from '@/components/Explorer/AnalyticsCard.vue'; | ||||
| import TransactionTable from '@/components/Explorer/TransactionTable.vue'; | ||||
| 
 | ||||
| const user = useUser(); | ||||
| const { network } = user; | ||||
| 
 | ||||
| const { | ||||
|   searchAddress, | ||||
|   selectedType, | ||||
|   transactions, | ||||
|   analytics, | ||||
|   loading, | ||||
|   error, | ||||
|   analyticsLoading, | ||||
|   fetchAllActivity, | ||||
|   fetchUserActivity, | ||||
|   fetchAnalytics, | ||||
|   clearData | ||||
| } = useGraphQL(network); | ||||
| 
 | ||||
| const transactionTypes = [ | ||||
|   { key: 'all', label: 'Todas' }, | ||||
|   { key: 'deposit', label: 'Depósitos' }, | ||||
|   { key: 'lock', label: 'Bloqueios' }, | ||||
|   { key: 'release', label: 'Liberações' }, | ||||
|   { key: 'return', label: 'Retornos' } | ||||
| ]; | ||||
| 
 | ||||
| const handleTypeFilter = (type: string) => { | ||||
|   selectedType.value = type; | ||||
| }; | ||||
| 
 | ||||
| watch(searchAddress, async (newAddress) => { | ||||
|   if (newAddress.trim()) { | ||||
|     await fetchUserActivity(newAddress.trim()); | ||||
|   } else { | ||||
|     await fetchAllActivity(); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| watch(network, async () => { | ||||
|   clearData(); | ||||
|   await Promise.all([ | ||||
|     fetchAllActivity(), | ||||
|     fetchAnalytics() | ||||
|   ]); | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| onMounted(async () => { | ||||
|   await Promise.all([ | ||||
|     fetchAllActivity(), | ||||
|     fetchAnalytics() | ||||
|   ]); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div class="min-h-screen"> | ||||
|     <div class="container mx-auto px-4 py-8"> | ||||
| 
 | ||||
|       <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8"> | ||||
|         <AnalyticsCard | ||||
|           title="Volume Total" | ||||
|           :value="analytics.totalVolume" | ||||
|           :loading="analyticsLoading" | ||||
|         /> | ||||
|          | ||||
|         <AnalyticsCard | ||||
|           title="Total de Transações" | ||||
|           :value="analytics.totalTransactions" | ||||
|           :loading="analyticsLoading" | ||||
|         /> | ||||
|          | ||||
|         <AnalyticsCard | ||||
|           title="Total de Bloqueios" | ||||
|           :value="analytics.totalLocks" | ||||
|           :loading="analyticsLoading" | ||||
|         /> | ||||
|          | ||||
|         <AnalyticsCard | ||||
|           title="Total de Depósitos" | ||||
|           :value="analytics.totalDeposits" | ||||
|           :loading="analyticsLoading" | ||||
|         /> | ||||
|          | ||||
|         <AnalyticsCard | ||||
|           title="Total de Liberações" | ||||
|           :value="analytics.totalReleases" | ||||
|           :loading="analyticsLoading" | ||||
|         /> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Search and Filters --> | ||||
|       <FormCard padding="lg" class="mb-6"> | ||||
|         <div class="space-y-4"> | ||||
|           <!-- Search Input --> | ||||
|           <div class="flex-1"> | ||||
|             <input | ||||
|               v-model="searchAddress" | ||||
|               type="text" | ||||
|               placeholder="Buscar por endereço de carteira..." | ||||
|               class="w-full px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent" | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- Type Filters --> | ||||
|           <div class="flex flex-wrap gap-2"> | ||||
|             <button | ||||
|               v-for="type in transactionTypes" | ||||
|               :key="type.key" | ||||
|               @click="handleTypeFilter(type.key)" | ||||
|               :class="[ | ||||
|                 'px-4 py-2 rounded-lg text-sm font-medium transition-colors', | ||||
|                 selectedType === type.key | ||||
|                   ? 'bg-amber-400 text-gray-900' | ||||
|                   : 'bg-gray-100 text-gray-700 hover:bg-gray-200' | ||||
|               ]" | ||||
|             > | ||||
|               {{ type.label }} | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </FormCard> | ||||
| 
 | ||||
|       <!-- Loading State --> | ||||
|       <div v-if="loading" class="text-center py-12"> | ||||
|         <LoadingComponent title="Carregando transações..." /> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Error State --> | ||||
|       <div v-else-if="error" class="text-center py-12"> | ||||
|         <div class="text-red-600 text-lg mb-2">⚠️</div> | ||||
|         <p class="text-red-600 mb-2">Erro ao carregar transações</p> | ||||
|         <p class="text-gray-600 text-sm">{{ error }}</p> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Transactions Table --> | ||||
|       <FormCard v-else padding="lg"> | ||||
|         <div class="mb-6"> | ||||
|           <h2 class="text-xl font-semibold text-gray-900 mb-2">Transações Recentes</h2> | ||||
|           <p class="text-gray-600">{{ transactions.length }} transações encontradas</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <TransactionTable  | ||||
|           :transactions="transactions" | ||||
|           :network-explorer-url="network.blockExplorers?.default.url || ''" | ||||
|         /> | ||||
|       </FormCard> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .container { | ||||
|   max-width: 1200px; | ||||
| } | ||||
| </style> | ||||
| @ -69,7 +69,7 @@ const sendNetwork = async () => { | ||||
|     /> | ||||
|     <div v-if="flowStep == Step.Network"> | ||||
|       <SendNetwork | ||||
|         :sellerId="user.sellerId.value" | ||||
|         :sellerId="String(user.sellerId.value)" | ||||
|         :offer="Number(user.seller.value.offer)" | ||||
|         :selected-token="user.selectedToken.value" | ||||
|         v-if="!loading" | ||||
|  | ||||
| @ -7,6 +7,7 @@ import svgLoader from "vite-svg-loader"; | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| export default defineConfig({ | ||||
|   base: "./", | ||||
|   build: { | ||||
|     target: "esnext", | ||||
|   }, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user