feat: add transactions explorer page
This commit is contained in:
		
							parent
							
								
									7ec73e8c6f
								
							
						
					
					
						commit
						799f7cfe09
					
				
							
								
								
									
										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> | ||||||
| @ -154,6 +154,19 @@ onClickOutside(infoMenuRef, () => { | |||||||
|           > |           > | ||||||
|             <div class="mt-2"> |             <div class="mt-2"> | ||||||
|               <div class="bg-white rounded-md z-10 -left-36 w-52"> |               <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"> |                 <div class="menu-button gap-2 px-4 rounded-md cursor-pointer"> | ||||||
|                   <span |                   <span | ||||||
|                     class="text-gray-900 py-4 text-end font-semibold text-sm" |                     class="text-gray-900 py-4 text-end font-semibold text-sm" | ||||||
| @ -413,6 +426,9 @@ onClickOutside(infoMenuRef, () => { | |||||||
|           <div class="w-full flex justify-center"> |           <div class="w-full flex justify-center"> | ||||||
|             <hr class="w-4/5" /> |             <hr class="w-4/5" /> | ||||||
|           </div> |           </div> | ||||||
|  |           <div class="w-full flex justify-center"> | ||||||
|  |             <hr class="w-4/5" /> | ||||||
|  |           </div> | ||||||
|           <div class="menu-button" @click="closeMenu()"> |           <div class="menu-button" @click="closeMenu()"> | ||||||
|             <RouterLink to="/manage_bids" class="redirect_button"> |             <RouterLink to="/manage_bids" class="redirect_button"> | ||||||
|               Gerenciar Ofertas |               Gerenciar Ofertas | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ const props = withDefaults( | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .form-card:not(.no-border) { | .form-card:not(.no-border) { | ||||||
|   @apply border-y-10; |   @apply border-y; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .form-card.full-width { | .form-card.full-width { | ||||||
|  | |||||||
							
								
								
									
										471
									
								
								src/composables/useGraphQL.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										471
									
								
								src/composables/useGraphQL.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,471 @@ | |||||||
|  | import { ref, computed } from 'vue'; | ||||||
|  | import { NetworkEnum } from '@/model/NetworkEnum'; | ||||||
|  | 
 | ||||||
|  | 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(networkName: NetworkEnum) { | ||||||
|  |   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 = (networkName: string) => { | ||||||
|  |     switch (networkName) { | ||||||
|  |       case 'sepolia': | ||||||
|  |         return import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL || 'https://api.studio.thegraph.com/query/113713/p-2-pix/sepolia'; | ||||||
|  |       case 'rootstockTestnet': | ||||||
|  |         return import.meta.env.VITE_RSK_SUBGRAPH_URL || 'https://api.studio.thegraph.com/query/113713/p-2-pix/1'; | ||||||
|  |       default: | ||||||
|  |         return 'https://api.studio.thegraph.com/query/113713/p-2-pix/sepolia'; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const executeQuery = async (query: string, variables: any = {}) => { | ||||||
|  |     const url = getGraphQLUrl(networkName.toString()); | ||||||
|  |      | ||||||
|  |     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,11 +1,14 @@ | |||||||
| import { createRouter, createWebHistory } from "vue-router"; | import { createRouter, createWebHistory, createWebHashHistory } from "vue-router"; | ||||||
| import HomeView from "@/views/HomeView.vue"; | import HomeView from "@/views/HomeView.vue"; | ||||||
| import FaqView from "@/views/FaqView.vue"; | import FaqView from "@/views/FaqView.vue"; | ||||||
| import ManageBidsView from "@/views/ManageBidsView.vue"; | import ManageBidsView from "@/views/ManageBidsView.vue"; | ||||||
| import SellerView from "@/views/SellerView.vue"; | import SellerView from "@/views/SellerView.vue"; | ||||||
|  | import ExploreView from "@/views/ExploreView.vue"; | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | 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: [ |   routes: [ | ||||||
|     { |     { | ||||||
|       path: "/", |       path: "/", | ||||||
| @ -33,6 +36,11 @@ const router = createRouter({ | |||||||
|       name: "faq", |       name: "faq", | ||||||
|       component: FaqView, |       component: FaqView, | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       path: "/explore", | ||||||
|  |       name: "explore", | ||||||
|  |       component: ExploreView, | ||||||
|  |     }, | ||||||
|   ], |   ], | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										164
									
								
								src/views/ExploreView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/views/ExploreView.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,164 @@ | |||||||
|  | <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'; | ||||||
|  | import { getBlockExplorerUrl } from '@/config/networks'; | ||||||
|  | 
 | ||||||
|  | const user = useUser(); | ||||||
|  | const { networkName } = user; | ||||||
|  | 
 | ||||||
|  | const { | ||||||
|  |   searchAddress, | ||||||
|  |   selectedType, | ||||||
|  |   transactions, | ||||||
|  |   analytics, | ||||||
|  |   loading, | ||||||
|  |   error, | ||||||
|  |   analyticsLoading, | ||||||
|  |   fetchAllActivity, | ||||||
|  |   fetchUserActivity, | ||||||
|  |   fetchAnalytics, | ||||||
|  |   clearData | ||||||
|  | } = useGraphQL(networkName.value); | ||||||
|  | 
 | ||||||
|  | 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(networkName, 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="getBlockExplorerUrl(networkName)" | ||||||
|  |         /> | ||||||
|  |       </FormCard> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .container { | ||||||
|  |   max-width: 1200px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -7,6 +7,7 @@ import svgLoader from "vite-svg-loader"; | |||||||
| 
 | 
 | ||||||
| // https://vitejs.dev/config/
 | // https://vitejs.dev/config/
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|  |   base: "./", | ||||||
|   build: { |   build: { | ||||||
|     target: "esnext", |     target: "esnext", | ||||||
|   }, |   }, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user