diff --git a/README.md b/README.md index b89cc70..1150796 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ An explorer tool to look up information about cross-chain messages in [Base Bridge](https://github.com/base/bridge). Supports both testnet and mainnet. +## Features + +- 🔍 **Transaction Lookup**: Search for bridge transactions using Solana signatures or Base transaction hashes +- 🔗 **Cross-Chain Tracking**: Track message flow from initiation → validation → execution +- ⏱️ **Real-Time Status**: View current status of bridge transactions (Pending, Pre-validated, Executed) +- 🌐 **Multi-Network Support**: Works with both mainnet (Base + Solana) and testnet (Base Sepolia + Solana Devnet) +- 📋 **Easy Copy**: One-click copy for transaction hashes and addresses +- 🔗 **Explorer Links**: Direct links to block explorers (Basescan, Solana Explorer) + ## Quick Start 1. Install dependencies @@ -14,12 +23,63 @@ npm install ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` 3. Visit [localhost:3000](http://localhost:3000/) in your browser + +## Environment Variables (Optional) + +Create a `.env.local` file with custom RPC endpoints for better performance: + +```env +BASE_MAINNET_RPC=https://your-base-mainnet-rpc.com +BASE_SEPOLIA_RPC=https://your-base-sepolia-rpc.com +SOLANA_MAINNET_RPC=https://your-solana-mainnet-rpc.com +SOLANA_DEVNET_RPC=https://your-solana-devnet-rpc.com +``` + +## Tech Stack + +- **Framework**: [Next.js 15](https://nextjs.org/) with App Router +- **Styling**: [Tailwind CSS 4](https://tailwindcss.com/) +- **EVM**: [viem](https://viem.sh/) for Base blockchain interactions +- **Solana**: [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) for Solana blockchain interactions +- **TypeScript**: Full type safety throughout the codebase + +## Project Structure + +``` +src/ +├── app/ # Next.js App Router pages and API routes +│ ├── api/ # API endpoints for blockchain queries +│ ├── layout.tsx # Root layout with fonts and analytics +│ └── page.tsx # Main explorer page +├── components/ # React components +│ ├── ErrorMessage.tsx # Error display component +│ ├── ExploreButton.tsx # Main explore action button +│ ├── Footer.tsx # Footer with useful links +│ ├── Header.tsx # Page header +│ ├── InputForm.tsx # Transaction input form +│ ├── LoadingSkeleton.tsx # Loading state skeleton +│ ├── Results.tsx # Transaction results display +│ ├── Status.tsx # Status badge component +│ └── Toast.tsx # Toast notification system +└── lib/ # Utility libraries + ├── base.ts # Base blockchain decoder + ├── bridge.ts # Bridge types and interfaces + ├── solana.ts # Solana blockchain decoder + └── transaction.ts # Transaction types +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +MIT License + +## Related Projects + +- [Base Bridge](https://github.com/base/bridge) - The core bridge implementation +- [Base Docs](https://docs.base.org) - Official Base documentation diff --git a/src/app/globals.css b/src/app/globals.css index b19b309..7da4cae 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -98,3 +98,40 @@ a:hover { 0 1px 2px rgba(15, 23, 42, 0.08), 0 8px 24px rgba(0, 0, 0, 0.12); backdrop-filter: blur(8px); } + +/* Toast slide-in animation */ +@keyframes slide-in { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.animate-slide-in { + animation: slide-in 0.3s ease-out; +} + +/* Skeleton pulse animation override for better visibility */ +@keyframes skeleton-pulse { + 0%, 100% { + opacity: 0.4; + } + 50% { + opacity: 0.8; + } +} + +/* Focus visible improvements for accessibility */ +:focus-visible { + outline: 2px solid var(--brand); + outline-offset: 2px; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 52f67b5..5a379f3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,19 +5,49 @@ import { BridgeQueryResult } from "@/lib/bridge"; import { Header } from "@/components/Header"; import { InputForm } from "@/components/InputForm"; import { Results } from "@/components/Results"; +import { Footer } from "@/components/Footer"; +import { LoadingSkeleton } from "@/components/LoadingSkeleton"; +import { ErrorMessage } from "@/components/ErrorMessage"; export default function Home() { const [result, setResult] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSetResult = (r: BridgeQueryResult | null) => { + setError(null); + setResult(r); + }; + + const handleError = (message: string) => { + setError(message); + setResult(null); + }; return ( -
+
- + + + {error && ( + setError(null)} + /> + )} + + {isLoading && !result && } - + {!isLoading && }
+
); } diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx new file mode 100644 index 0000000..df8f941 --- /dev/null +++ b/src/components/ErrorMessage.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; + +interface ErrorMessageProps { + title?: string; + message: string; + onDismiss?: () => void; +} + +export const ErrorMessage = ({ + title = "Error", + message, + onDismiss, +}: ErrorMessageProps) => { + const [isVisible, setIsVisible] = useState(true); + + const handleDismiss = () => { + setIsVisible(false); + onDismiss?.(); + }; + + if (!isVisible) return null; + + return ( +
+
+
+ +
+
+

+ {title} +

+

+ {message} +

+
+ {onDismiss && ( + + )} +
+
+ ); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..f44c024 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,67 @@ +export const Footer = () => { + return ( + + ); +}; diff --git a/src/components/LoadingSkeleton.tsx b/src/components/LoadingSkeleton.tsx new file mode 100644 index 0000000..7a929bd --- /dev/null +++ b/src/components/LoadingSkeleton.tsx @@ -0,0 +1,116 @@ +"use client"; + +export const LoadingSkeleton = () => { + return ( +
+
+ {/* Header skeleton */} +
+
+
+
+
+
+ + {/* Description skeleton */} +
+ + {/* Transaction cards skeleton */} +
    + {/* Initial transaction skeleton */} +
  1. +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
  2. + + {/* Validation transaction skeleton */} +
  3. +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
  4. + + {/* Execute transaction skeleton */} +
  5. +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
  6. +
+
+
+ ); +}; diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..a397542 --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { createContext, useContext, useState, useCallback, ReactNode } from "react"; + +interface Toast { + id: string; + message: string; + type: "success" | "error" | "info"; +} + +interface ToastContextType { + showToast: (message: string, type?: Toast["type"]) => void; +} + +const ToastContext = createContext(undefined); + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +}; + +export const ToastProvider = ({ children }: { children: ReactNode }) => { + const [toasts, setToasts] = useState([]); + + const showToast = useCallback((message: string, type: Toast["type"] = "success") => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { id, message, type }]); + + // Auto-dismiss after 3 seconds + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 3000); + }, []); + + const dismissToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + return ( + + {children} + {/* Toast container */} +
+ {toasts.map((toast) => ( +
+ {toast.type === "success" && ( + + )} + {toast.type === "error" && ( + + )} + {toast.type === "info" && ( + + )} + {toast.message} + +
+ ))} +
+
+ ); +};