From 028d57f780cfbb503eb9b58b9aac52c63bbbb492 Mon Sep 17 00:00:00 2001 From: Muhammad Zayyad Mukhtar <95658387+El-swaggerito@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:56:56 +0100 Subject: [PATCH] refactor: enhance UI components with improved styling and accessibility - Add comprehensive JSDoc comments and type definitions for all components - Implement consistent styling with Tailwind CSS and improved visual hierarchy - Enhance accessibility with ARIA attributes and keyboard navigation support - Add new features: tooltip positioning, recent tokens management, and network selection - Improve error handling and user feedback across all interactive components --- src/components/AddTrustlineButton.tsx | 71 +++++++++++---- src/components/ConnectWallet.tsx | 81 ++++++++++++----- src/components/NetworkSelector.tsx | 123 ++++++++++++++++++-------- src/components/WalletModal.tsx | 107 ++++++++++++++-------- src/components/ui/Tooltip.tsx | 58 ++++++++++-- src/hooks/useRecentTokens.ts | 74 ++++++++++++---- 6 files changed, 378 insertions(+), 136 deletions(-) diff --git a/src/components/AddTrustlineButton.tsx b/src/components/AddTrustlineButton.tsx index fb32444..7c1f51c 100644 --- a/src/components/AddTrustlineButton.tsx +++ b/src/components/AddTrustlineButton.tsx @@ -1,50 +1,68 @@ +/** + * Add Trustline Button Component. + * Encapsulates the logic for establishing a Stellar trustline for a specific asset. + * A trustline is mandatory before an account can hold non-native assets. + */ + import React, { useState } from "react"; import { toast } from "react-hot-toast"; -import { Plus, Check, Loader2 } from "lucide-react"; +import { Plus, Check, Loader2, ShieldCheck, AlertCircle } from "lucide-react"; import { addTrustline } from "../lib/stellar"; import Button from "./ui/Button"; +/** + * Props for the AddTrustlineButton component. + */ interface AddTrustlineButtonProps { + /** The 1-12 character asset code (e.g., "USDC") */ assetCode: string; + /** The public Stellar address of the asset issuer */ assetIssuer: string; } /** - * A reusable button that establishes a Trustline for a specific Stellar asset - * using the Freighter wallet extension. + * A specialized button that handles the 'change_trust' operation flow. */ export default function AddTrustlineButton({ assetCode, assetIssuer }: AddTrustlineButtonProps) { + // --- Component State --- + /** Current status of the asynchronous trustline operation */ const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + /** + * Initiates the trustline transaction flow. + */ const handleAddTrustline = async () => { + // Prevent redundant clicks if (status === "loading" || status === "success") return; setStatus("loading"); - const toastId = toast.loading(`Requesting ${assetCode} trustline...`); + const toastId = toast.loading(`Establishing ${assetCode} trustline...`); try { + // 1. Trigger the Stellar SDK / Wallet transaction await addTrustline(assetCode, assetIssuer); setStatus("success"); - toast.success(`${assetCode} Trustline Established!`, { + toast.success(`${assetCode} Trustline Active`, { id: toastId, - icon: '✅' + icon: '🛡️' }); - // Revert to idle after 5 seconds + // 2. Revert to idle after 5 seconds to reset UI setTimeout(() => setStatus("idle"), 5000); + console.log(`[AddTrustline] Successfully added ${assetCode} from ${assetIssuer}`); } catch (error: any) { - console.error(`[AddTrustline] Error:`, error); + console.error(`[AddTrustline] Failed to add ${assetCode}:`, error); setStatus("error"); - // Handle rejection vs generic error - const errorMsg = error.message?.includes("denied") - ? "Access Denied by User" - : `Failed to add ${assetCode}`; + // User-friendly error mapping + const errorMsg = error.message?.toLowerCase().includes("denied") + ? "Transaction rejected by user" + : `Network error adding ${assetCode}`; toast.error(errorMsg, { id: toastId }); - // Revert to idle after 3 seconds to allow retry + // 3. Revert to idle after a short delay to allow retry setTimeout(() => setStatus("idle"), 3000); } }; @@ -54,21 +72,36 @@ export default function AddTrustlineButton({ assetCode, assetIssuer }: AddTrustl variant="secondary" onClick={handleAddTrustline} disabled={status === "loading" || status === "success"} - className={`flex items-center gap-2 text-xs py-1.5 px-3 h-auto transition-all duration-200 ${ - status === "success" ? "bg-green-600/20 text-green-400 border-green-600/50" : "" + className={`flex items-center gap-2 text-[10px] font-black uppercase tracking-widest py-2 px-4 h-auto transition-all duration-300 border ${ + status === "success" + ? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30" + : "bg-slate-800/50 text-slate-400 border-slate-700 hover:border-slate-500 hover:text-white" }`} + aria-label={`Add trustline for ${assetCode}`} > + {/* Dynamic Icon State */} {status === "loading" ? ( - + ) : status === "success" ? ( - + + ) : status === "error" ? ( + ) : ( - + )} + {/* Dynamic Label State */} - {status === "loading" ? "Signing..." : status === "success" ? "Trustline Active" : `Add ${assetCode}`} + {status === "loading" + ? "Signing..." + : status === "success" + ? "Established" + : status === "error" + ? "Retry" + : `Add ${assetCode}` + } ); } + diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index e403781..2bde2c9 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -1,19 +1,33 @@ +/** + * Connect Wallet Component. + * A standalone button/dropdown component for managing Stellar wallet sessions. + * Displays the connected address and provides a disconnect option. + */ + "use client"; + import { useState, useRef, useEffect } from "react"; -import { connectWallet, WalletType, FREIGHTER_ID } from "../lib/stellar"; +import { connectWallet, WalletType, FREIGHTER_ID, shortenAddress } from "../lib/stellar"; import Button from "./ui/Button"; import { useTokenStore } from "../stores/tokenStore"; -import { LogOut, ChevronDown } from "lucide-react"; +import { LogOut, ChevronDown, User, ShieldCheck } from "lucide-react"; +/** Key for purging recent token history on logout */ const RECENT_TOKENS_KEY = "tradeflow_recent_tokens"; +/** + * A component that handles the wallet connection flow and account display. + */ export default function ConnectWallet() { + // --- Component State --- const [pubKey, setPubKey] = useState(null); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const { setConnected } = useTokenStore(); const dropdownRef = useRef(null); - // Close dropdown when clicking outside + /** + * Effect: Closes the account dropdown when clicking outside. + */ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -24,65 +38,92 @@ export default function ConnectWallet() { return () => document.removeEventListener("mousedown", handleClickOutside); }, []); + /** + * Initiates the wallet connection process. + * + * @param {WalletType} walletType - The ID of the wallet provider. + */ const handleConnect = async (walletType: WalletType = FREIGHTER_ID) => { try { const userInfo = await connectWallet(walletType); if (userInfo.publicKey) { setPubKey(userInfo.publicKey); setConnected(true, userInfo.publicKey); + console.log(`[ConnectWallet] Session started for: ${userInfo.publicKey}`); } } catch (e: any) { - console.error("Connection error:", e); + console.error("[ConnectWallet] Connection failed:", e); + // TODO: Use a toast instead of native alert alert(e.message || "Failed to connect to wallet!"); } }; + /** + * Terminates the session and clears related data. + */ const handleDisconnect = () => { setPubKey(null); setConnected(false, undefined); + // Clear local history for privacy/security localStorage.removeItem(RECENT_TOKENS_KEY); setIsDropdownOpen(false); + console.log("[ConnectWallet] Session terminated."); }; + // 1. Authenticated UI (Dropdown) if (pubKey) { return (
{isDropdownOpen && ( -
-
-

Connected Wallet

-

{pubKey}

+
+
+
+ +

Verified Account

+
+

{pubKey}

+
+
+
-
)}
); } + // 2. Unauthenticated UI (CTA Button) return (
); } + diff --git a/src/components/NetworkSelector.tsx b/src/components/NetworkSelector.tsx index a44d7a0..96c05c5 100644 --- a/src/components/NetworkSelector.tsx +++ b/src/components/NetworkSelector.tsx @@ -1,32 +1,57 @@ +/** + * Network Selector Component. + * Allows users to toggle between Stellar Mainnet and Testnet environments. + * Persists the selection in local state and notifies parent components of changes. + */ + "use client"; import React, { useState, useRef, useEffect } from "react"; -import { ChevronDown, AlertTriangle } from "lucide-react"; +import { ChevronDown, AlertTriangle, Globe, FlaskConical } from "lucide-react"; +/** Supported Stellar network types */ export type Network = "mainnet" | "testnet"; +/** + * Props for the NetworkSelector component. + */ interface NetworkSelectorProps { + /** Optional callback triggered when the network is changed */ onNetworkChange?: (network: Network) => void; } +/** + * A dropdown component for selecting the active Stellar network. + */ export default function NetworkSelector({ onNetworkChange }: NetworkSelectorProps) { + // --- Component State --- const [selectedNetwork, setSelectedNetwork] = useState("mainnet"); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); + /** + * Available network configurations. + */ const networks = [ { id: "mainnet" as Network, name: "Stellar Mainnet", - description: "Production network" + description: "Production assets", + icon: }, { id: "testnet" as Network, name: "Stellar Testnet", - description: "Development network" + description: "Development assets", + icon: } ]; + // --- Handlers --- + + /** + * Effect: Closes the dropdown when clicking outside the component. + */ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -40,6 +65,11 @@ export default function NetworkSelector({ onNetworkChange }: NetworkSelectorProp }; }, []); + /** + * Updates the selected network and propagates the change. + * + * @param {Network} network - The new network ID. + */ const handleNetworkSelect = (network: Network) => { setSelectedNetwork(network); setIsOpen(false); @@ -52,64 +82,79 @@ export default function NetworkSelector({ onNetworkChange }: NetworkSelectorProp return (
- {/* Selected Network Button */} + {/* Trigger Button */} {/* Testnet Warning Badge */} {selectedNetwork === "testnet" && ( -
- +
+ Testnet
)} - {/* Dropdown Menu */} + {/* Dropdown List */} {isOpen && ( -
- {networks.map((network) => ( - - ))} + + ))} +
)}
); } + diff --git a/src/components/WalletModal.tsx b/src/components/WalletModal.tsx index 3fa0c69..a668f8a 100644 --- a/src/components/WalletModal.tsx +++ b/src/components/WalletModal.tsx @@ -1,103 +1,138 @@ +/** + * Wallet Modal Component. + * Displays a selection of supported Stellar wallet providers (Freighter, xBull, Albedo). + * Provides information about each wallet and handles the selection callback. + */ + import React from "react"; import { FREIGHTER_ID, XBULL_ID, ALBEDO_ID, WalletType } from "../lib/stellar"; +import { X, Info, Shield, Smartphone, Globe } from "lucide-react"; +/** + * Props for the WalletModal component. + */ interface WalletModalProps { + /** Visibility toggle for the modal */ isOpen: boolean; + /** Callback to close the modal */ onClose: () => void; + /** Callback triggered when a wallet provider is selected */ onConnect?: (walletType: WalletType) => void; } +/** + * Internal representation of a wallet provider option. + */ interface WalletOption { + /** The internal ID used by the stellar-wallets-kit */ id: WalletType; + /** Human-readable name of the wallet */ name: string; + /** Short description of the wallet's features */ description: string; - icon: string; + /** Lucide icon component to represent the wallet type */ + icon: React.ReactNode; + /** Tailwind background color class for the icon container */ bgColor: string; } +/** + * Supported wallet providers configuration. + */ const walletOptions: WalletOption[] = [ { id: FREIGHTER_ID, name: "Freighter", - description: "Popular browser extension wallet", - icon: "M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z", + description: "Secure browser extension wallet by Stellar Development Foundation.", + icon: , bgColor: "bg-blue-500" }, { id: XBULL_ID, name: "xBull", - description: "Mobile-first Stellar wallet", - icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z", + description: "Mobile-first and multi-platform Stellar wallet with advanced features.", + icon: , bgColor: "bg-orange-500" }, { id: ALBEDO_ID, name: "Albedo", - description: "Web-based Stellar wallet", - icon: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z", + description: "Convenient web-based wallet for seamless Stellar network access.", + icon: , bgColor: "bg-purple-500" } ]; +/** + * A modal overlay for selecting a wallet provider. + */ export default function WalletModal({ isOpen, onClose, onConnect }: WalletModalProps) { + // 1. Conditional Rendering for Visibility if (!isOpen) return null; + /** + * Selection handler: closes the modal and triggers the connection flow. + * @param {WalletType} walletType - The ID of the chosen wallet. + */ const handleWalletSelect = (walletType: WalletType) => { if (onConnect) onConnect(walletType); onClose(); }; return ( -
-
-
-

Connect Wallet

+
+
+ {/* Header Section */} +
+
+
+ +
+ +
-
+ {/* Options List */} +
+

Select Provider

{walletOptions.map((wallet) => ( ))}
-
-

- By connecting a wallet, you agree to the Terms of Service and Privacy Policy + {/* Footer/Disclosure */} +

+ +

+ By connecting a wallet, you agree to our Terms of Service and acknowledge you have read our Privacy Policy.

); } + diff --git a/src/components/ui/Tooltip.tsx b/src/components/ui/Tooltip.tsx index 4302fe1..915b303 100644 --- a/src/components/ui/Tooltip.tsx +++ b/src/components/ui/Tooltip.tsx @@ -1,31 +1,75 @@ +/** + * Shared Tooltip Component. + * Displays a non-interactive popover containing descriptive text when + * the user hovers over or focuses on the child element. + */ + import React, { useState } from "react"; +/** + * Props for the Tooltip component. + */ interface TooltipProps { + /** The text content to display inside the tooltip bubble */ content: string; + /** The element that triggers the tooltip (usually an icon or button) */ children: React.ReactNode; + /** Optional position override (defaults to top) */ + position?: "top" | "bottom" | "left" | "right"; } -export default function Tooltip({ children, content }: TooltipProps) { +/** + * A lightweight, accessible tooltip component. + */ +export default function Tooltip({ children, content, position = "top" }: TooltipProps) { + // --- Component State --- const [isVisible, setIsVisible] = useState(false); + // --- Style Logic --- + + /** Position-specific Tailwind classes */ + const positionClasses = { + top: "bottom-full left-1/2 -translate-x-1/2 mb-2.5", + bottom: "top-full left-1/2 -translate-x-1/2 mt-2.5", + left: "right-full top-1/2 -translate-y-1/2 mr-2.5", + right: "left-full top-1/2 -translate-y-1/2 ml-2.5" + }; + + /** Arrow orientation classes */ + const arrowClasses = { + top: "top-full left-1/2 -translate-x-1/2 border-t-slate-800", + bottom: "bottom-full left-1/2 -translate-x-1/2 border-b-slate-800", + left: "left-full top-1/2 -translate-y-1/2 border-l-slate-800", + right: "right-full top-1/2 -translate-y-1/2 border-r-slate-800" + }; + return (
setIsVisible(true)} onMouseLeave={() => setIsVisible(false)} + onFocus={() => setIsVisible(true)} + onBlur={() => setIsVisible(false)} > + {/* The trigger element */} {children} + + {/* The Tooltip Bubble */}
-
+
{content} - {/* Arrow */} -
+ + {/* Decorative Arrow */} +
); } + diff --git a/src/hooks/useRecentTokens.ts b/src/hooks/useRecentTokens.ts index c03b380..cfdecf3 100644 --- a/src/hooks/useRecentTokens.ts +++ b/src/hooks/useRecentTokens.ts @@ -1,42 +1,86 @@ +/** + * Recent Tokens Hook. + * Manages a persistent list of recently used tokens (symbols) in the swap interface. + * Helps improve UX by providing quick access to common assets. + */ + import { useState, useEffect, useCallback } from 'react'; +/** localStorage key for recent token symbols */ const STORAGE_KEY = 'tradeflow_recent_tokens'; +/** Maximum number of unique tokens to track */ const MAX_RECENT_TOKENS = 3; +/** + * Custom hook for managing the recent tokens list. + */ export function useRecentTokens() { + // --- State Initialization --- const [recentTokens, setRecentTokens] = useState([]); + /** + * Effect: Hydrate the state from localStorage on component mount. + * Only runs in the browser environment. + */ useEffect(() => { if (typeof window === 'undefined') return; try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); + const storedData = localStorage.getItem(STORAGE_KEY); + if (storedData) { + const parsed = JSON.parse(storedData); if (Array.isArray(parsed)) { - setRecentTokens(parsed); + // Validate that the array contains only strings + const validTokens = parsed.filter(t => typeof t === 'string'); + setRecentTokens(validTokens); } } } catch (error) { - console.warn('Error reading recent tokens from localStorage:', error); + console.warn('[useRecentTokens] Error loading history:', error); } }, []); - const addRecentToken = useCallback((token: string) => { + /** + * Adds a token to the top of the recent list. + * If the token already exists, it is moved to the front. + * + * @param {string} tokenSymbol - The symbol of the token (e.g., "XLM"). + */ + const addRecentToken = useCallback((tokenSymbol: string) => { + if (!tokenSymbol) return; + setRecentTokens((prev) => { - // Remove if exists to move to top - const filtered = prev.filter((t) => t !== token); - // Prepend new token and slice to max length - const newRecent = [token, ...filtered].slice(0, MAX_RECENT_TOKENS); + // 1. Remove duplicates to ensure uniqueness + const filteredList = prev.filter((t) => t !== tokenSymbol); + + // 2. Prepend the new token and enforce the size limit + const updatedHistory = [tokenSymbol, ...filteredList].slice(0, MAX_RECENT_TOKENS); + // 3. Persist the updated list for future sessions try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(newRecent)); + localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedHistory)); } catch (error) { - console.warn('Error saving recent tokens:', error); + console.warn('[useRecentTokens] Failed to persist update:', error); } - return newRecent; + + return updatedHistory; }); }, []); - return { recentTokens, addRecentToken }; -} \ No newline at end of file + /** + * Resets the recent tokens history. + */ + const clearHistory = useCallback(() => { + setRecentTokens([]); + localStorage.removeItem(STORAGE_KEY); + }, []); + + return { + /** The current array of recently used token symbols */ + recentTokens, + /** Function to track a new token usage */ + addRecentToken, + /** Function to wipe the history */ + clearHistory + }; +}