From 3d912c03c8b2e62ca6a7b6f83d21da5870bd02f5 Mon Sep 17 00:00:00 2001 From: Gaurav Karakoti Date: Mon, 1 Jun 2026 17:35:00 +0000 Subject: [PATCH] Moved from .jsx to .tsx --- .../{DashboardGrid.jsx => DashboardGrid.tsx} | 194 +++++++++-------- .../{MobileHeader.jsx => MobileHeader.tsx} | 40 ++-- ...ileNavigation.jsx => MobileNavigation.tsx} | 33 +-- .../{MobileSidebar.jsx => MobileSidebar.tsx} | 19 +- src/components/layout/ResponsiveContainer.jsx | 154 -------------- src/components/layout/ResponsiveContainer.tsx | 186 ++++++++++++++++ ...nsiveSidebar.jsx => ResponsiveSidebar.tsx} | 16 +- .../layout/{SearchBar.jsx => SearchBar.tsx} | 35 ++- .../layout/{Sidebar.jsx => Sidebar.tsx} | 200 ++++++++++-------- .../{ThemeToggle.jsx => ThemeToggle.tsx} | 4 +- ...{WidgetSelector.jsx => WidgetSelector.tsx} | 59 ++++-- ...StatsWidget.jsx => AccountStatsWidget.tsx} | 29 +-- .../{AssetsWidget.jsx => AssetsWidget.tsx} | 24 ++- .../{BalanceWidget.jsx => BalanceWidget.tsx} | 16 +- ...StatsWidget.jsx => NetworkStatsWidget.tsx} | 32 +-- ...TickerWidget.jsx => PriceTickerWidget.tsx} | 28 ++- ...tionsWidget.jsx => QuickActionsWidget.tsx} | 17 +- ...tionsWidget.jsx => TransactionsWidget.tsx} | 65 +++--- .../{WidgetBase.jsx => WidgetBase.tsx} | 42 ++-- 19 files changed, 697 insertions(+), 496 deletions(-) rename src/components/layout/{DashboardGrid.jsx => DashboardGrid.tsx} (72%) rename src/components/layout/{MobileHeader.jsx => MobileHeader.tsx} (66%) rename src/components/layout/{MobileNavigation.jsx => MobileNavigation.tsx} (74%) rename src/components/layout/{MobileSidebar.jsx => MobileSidebar.tsx} (95%) delete mode 100644 src/components/layout/ResponsiveContainer.jsx create mode 100644 src/components/layout/ResponsiveContainer.tsx rename src/components/layout/{ResponsiveSidebar.jsx => ResponsiveSidebar.tsx} (65%) rename src/components/layout/{SearchBar.jsx => SearchBar.tsx} (84%) rename src/components/layout/{Sidebar.jsx => Sidebar.tsx} (79%) rename src/components/layout/{ThemeToggle.jsx => ThemeToggle.tsx} (91%) rename src/components/layout/{WidgetSelector.jsx => WidgetSelector.tsx} (87%) rename src/components/layout/widgets/{AccountStatsWidget.jsx => AccountStatsWidget.tsx} (92%) rename src/components/layout/widgets/{AssetsWidget.jsx => AssetsWidget.tsx} (87%) rename src/components/layout/widgets/{BalanceWidget.jsx => BalanceWidget.tsx} (89%) rename src/components/layout/widgets/{NetworkStatsWidget.jsx => NetworkStatsWidget.tsx} (91%) rename src/components/layout/widgets/{PriceTickerWidget.jsx => PriceTickerWidget.tsx} (90%) rename src/components/layout/widgets/{QuickActionsWidget.jsx => QuickActionsWidget.tsx} (88%) rename src/components/layout/widgets/{TransactionsWidget.jsx => TransactionsWidget.tsx} (71%) rename src/components/layout/widgets/{WidgetBase.jsx => WidgetBase.tsx} (86%) diff --git a/src/components/layout/DashboardGrid.jsx b/src/components/layout/DashboardGrid.tsx similarity index 72% rename from src/components/layout/DashboardGrid.jsx rename to src/components/layout/DashboardGrid.tsx index 7d313717..bc6520da 100644 --- a/src/components/layout/DashboardGrid.jsx +++ b/src/components/layout/DashboardGrid.tsx @@ -3,32 +3,58 @@ import { useResponsive } from '../../hooks/useResponsive'; import { useErrorHandler } from '../../hooks/useErrorHandler'; import { addBreadcrumb } from '../../lib/errorReporting'; +export interface Widget { + id: string; + type?: string; + component: React.ReactNode; + width?: number; + height?: number; + span?: number; +} + +export interface GridColumns { + mobile: number; + tablet: number; + desktop: number; +} + +export interface DashboardGridProps { + widgets?: Widget[]; + onLayoutChange?: (layout: Widget[]) => void; + onWidgetResize?: (widget: Widget, size: { width: number; height: number }) => void; + onWidgetRemove?: (widget: Widget) => void; + editable?: boolean; + columns?: GridColumns; + gap?: number; + minWidgetHeight?: number; +} + /** * Customizable dashboard grid with drag-and-drop and resizable widgets */ -export default function DashboardGrid({ - widgets = [], - onLayoutChange, +export default function DashboardGrid({ + widgets = [], + onLayoutChange, onWidgetResize, onWidgetRemove, editable = false, columns = { mobile: 1, tablet: 2, desktop: 3 }, gap = 16, minWidgetHeight = 200 -}) { - const [layout, setLayout] = useState(widgets); - const [draggedWidget, setDraggedWidget] = useState(null); - const [dragOverIndex, setDragOverIndex] = useState(null); - const [resizingWidget, setResizingWidget] = useState(null); +}: DashboardGridProps) { + const [layout, setLayout] = useState(widgets); + const [draggedWidget, setDraggedWidget] = useState<{ widget: Widget; index: number } | null>(null); + const [dragOverIndex, setDragOverIndex] = useState(null); + const [resizingWidget, setResizingWidget] = useState<{ widget: Widget; index: number } | null>(null); const [resizeStartPos, setResizeStartPos] = useState({ x: 0, y: 0 }); const [resizeStartSize, setResizeStartSize] = useState({ width: 0, height: 0 }); - - const gridRef = useRef(null); - const { isMobile, isTablet } = useResponsive(); + + const gridRef = useRef(null); + const { isMobile, isTablet } = useResponsive() as { isMobile: boolean; isTablet: boolean }; const { handleError } = useErrorHandler('DashboardGrid'); // Get responsive column count - const getColumnCount = () => { + const getColumnCount = (): number => { if (isMobile) return columns.mobile; if (isTablet) return columns.tablet; return columns.desktop; @@ -37,62 +63,62 @@ export default function DashboardGrid({ const columnCount = getColumnCount(); // Handle layout changes - const updateLayout = useCallback((newLayout) => { + const updateLayout = useCallback((newLayout: Widget[]) => { setLayout(newLayout); onLayoutChange?.(newLayout); - addBreadcrumb('Dashboard layout updated', 'user_action', { + addBreadcrumb('Dashboard layout updated', 'user_action', { widgetCount: newLayout.length, - editable + editable }); }, [onLayoutChange, editable]); // Drag and drop handlers - const handleDragStart = (e, widget, index) => { + const handleDragStart = (e: React.DragEvent, widget: Widget, index: number) => { if (!editable) return; - + setDraggedWidget({ widget, index }); e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/html', e.target.outerHTML); - + e.dataTransfer.setData('text/html', (e.target as HTMLElement).outerHTML); + // Add drag styling - e.target.style.opacity = '0.5'; - - addBreadcrumb('Widget drag started', 'user_action', { + (e.target as HTMLElement).style.opacity = '0.5'; + + addBreadcrumb('Widget drag started', 'user_action', { widgetId: widget.id, - widgetType: widget.type + widgetType: widget.type }); }; - const handleDragEnd = (e) => { - e.target.style.opacity = '1'; + const handleDragEnd = (e: React.DragEvent) => { + (e.target as HTMLElement).style.opacity = '1'; setDraggedWidget(null); setDragOverIndex(null); }; - const handleDragOver = (e, index) => { + const handleDragOver = (e: React.DragEvent, index: number) => { if (!editable || !draggedWidget) return; - + e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverIndex(index); }; - const handleDrop = (e, dropIndex) => { + const handleDrop = (e: React.DragEvent, dropIndex: number) => { if (!editable || !draggedWidget) return; - + e.preventDefault(); - + const { index: dragIndex } = draggedWidget; if (dragIndex === dropIndex) return; const newLayout = [...layout]; const [draggedItem] = newLayout.splice(dragIndex, 1); newLayout.splice(dropIndex, 0, draggedItem); - + updateLayout(newLayout); setDragOverIndex(null); - - addBreadcrumb('Widget dropped', 'user_action', { + + addBreadcrumb('Widget dropped', 'user_action', { from: dragIndex, to: dropIndex, widgetId: draggedItem.id @@ -100,35 +126,39 @@ export default function DashboardGrid({ }; // Resize handlers - const handleResizeStart = (e, widget, index) => { + const handleResizeStart = (e: React.MouseEvent, widget: Widget, index: number) => { if (!editable) return; - + e.preventDefault(); e.stopPropagation(); - - const rect = e.target.closest('.widget-container').getBoundingClientRect(); + + const target = e.target as HTMLElement; + const container = target.closest('.widget-container') as HTMLElement; + if (!container) return; + + const rect = container.getBoundingClientRect(); setResizingWidget({ widget, index }); setResizeStartPos({ x: e.clientX, y: e.clientY }); setResizeStartSize({ width: rect.width, height: rect.height }); - + document.addEventListener('mousemove', handleResizeMove); document.addEventListener('mouseup', handleResizeEnd); - - addBreadcrumb('Widget resize started', 'user_action', { - widgetId: widget.id + + addBreadcrumb('Widget resize started', 'user_action', { + widgetId: widget.id }); }; - const handleResizeMove = useCallback((e) => { + const handleResizeMove = useCallback((e: MouseEvent) => { if (!resizingWidget) return; - + const deltaX = e.clientX - resizeStartPos.x; const deltaY = e.clientY - resizeStartPos.y; - + const newWidth = Math.max(200, resizeStartSize.width + deltaX); const newHeight = Math.max(minWidgetHeight, resizeStartSize.height + deltaY); - - const widgetElement = document.querySelector(`[data-widget-id="${resizingWidget.widget.id}"]`); + + const widgetElement = document.querySelector(`[data-widget-id="${resizingWidget.widget.id}"]`) as HTMLElement; if (widgetElement) { widgetElement.style.width = `${newWidth}px`; widgetElement.style.height = `${newHeight}px`; @@ -137,28 +167,28 @@ export default function DashboardGrid({ const handleResizeEnd = useCallback(() => { if (!resizingWidget) return; - - const widgetElement = document.querySelector(`[data-widget-id="${resizingWidget.widget.id}"]`); + + const widgetElement = document.querySelector(`[data-widget-id="${resizingWidget.widget.id}"]`) as HTMLElement; if (widgetElement) { const rect = widgetElement.getBoundingClientRect(); - const updatedWidget = { + const updatedWidget: Widget = { ...resizingWidget.widget, width: rect.width, height: rect.height }; - + const newLayout = [...layout]; newLayout[resizingWidget.index] = updatedWidget; updateLayout(newLayout); - + onWidgetResize?.(updatedWidget, { width: rect.width, height: rect.height }); - - addBreadcrumb('Widget resized', 'user_action', { + + addBreadcrumb('Widget resized', 'user_action', { widgetId: updatedWidget.id, newSize: { width: rect.width, height: rect.height } }); } - + setResizingWidget(null); document.removeEventListener('mousemove', handleResizeMove); document.removeEventListener('mouseup', handleResizeEnd); @@ -173,20 +203,20 @@ export default function DashboardGrid({ }, [handleResizeMove, handleResizeEnd]); // Remove widget - const handleRemoveWidget = (widget, index) => { + const handleRemoveWidget = (widget: Widget, index: number) => { if (!editable) return; - + const newLayout = layout.filter((_, i) => i !== index); updateLayout(newLayout); onWidgetRemove?.(widget); - - addBreadcrumb('Widget removed', 'user_action', { + + addBreadcrumb('Widget removed', 'user_action', { widgetId: widget.id, - widgetType: widget.type + widgetType: widget.type }); }; - const gridStyles = { + const gridStyles: React.CSSProperties = { display: 'grid', gridTemplateColumns: `repeat(${columnCount}, 1fr)`, gap: `${gap}px`, @@ -195,8 +225,8 @@ export default function DashboardGrid({ position: 'relative' }; - const getWidgetStyles = (widget, index) => { - const baseStyles = { + const getWidgetStyles = (widget: Widget, index: number): React.CSSProperties => { + const baseStyles: React.CSSProperties = { position: 'relative', minHeight: `${minWidgetHeight}px`, background: 'var(--bg-card)', @@ -226,11 +256,7 @@ export default function DashboardGrid({ }; return ( -
+
{layout.map((widget, index) => (
{/* Widget Header */} {editable && ( -
{/* Resize Handle */} - + {/* Remove Button */}
- - {/* Show controls on hover */} -
))} @@ -350,7 +370,7 @@ export default function DashboardGrid({ No Widgets Added
- {editable + {editable ? 'Add widgets to customize your dashboard layout.' : 'Enable edit mode to add and arrange widgets.' } diff --git a/src/components/layout/MobileHeader.jsx b/src/components/layout/MobileHeader.tsx similarity index 66% rename from src/components/layout/MobileHeader.jsx rename to src/components/layout/MobileHeader.tsx index 5e3d1825..67b71d2c 100644 --- a/src/components/layout/MobileHeader.jsx +++ b/src/components/layout/MobileHeader.tsx @@ -1,11 +1,11 @@ -import React from 'react' -import { useStore } from '../../lib/store' +import React from 'react'; +import { useStore } from '../../lib/store'; export default function MobileHeader() { - const { setMobileMenuOpen, theme, toggleTheme } = useStore() + const { setMobileMenuOpen, theme, toggleTheme } = useStore(); return ( -
{ - e.currentTarget.style.color = 'var(--text-primary)' - e.currentTarget.style.background = 'var(--bg-hover)' + onMouseEnter={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-primary)'; + e.currentTarget.style.background = 'var(--bg-hover)'; }} - onMouseLeave={e => { - e.currentTarget.style.color = 'var(--text-secondary)' - e.currentTarget.style.background = 'transparent' + onMouseLeave={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-secondary)'; + e.currentTarget.style.background = 'transparent'; }} > - - - + + + @@ -84,17 +84,17 @@ export default function MobileHeader() { transition: 'var(--transition)', }} title={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`} - onMouseEnter={e => { - e.currentTarget.style.color = 'var(--text-primary)' - e.currentTarget.style.background = 'var(--bg-hover)' + onMouseEnter={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-primary)'; + e.currentTarget.style.background = 'var(--bg-hover)'; }} - onMouseLeave={e => { - e.currentTarget.style.color = 'var(--text-secondary)' - e.currentTarget.style.background = 'transparent' + onMouseLeave={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-secondary)'; + e.currentTarget.style.background = 'transparent'; }} > {theme === 'light' ? '☾' : '☀'}
- ) + ); } \ No newline at end of file diff --git a/src/components/layout/MobileNavigation.jsx b/src/components/layout/MobileNavigation.tsx similarity index 74% rename from src/components/layout/MobileNavigation.jsx rename to src/components/layout/MobileNavigation.tsx index dce7e250..81f05a93 100644 --- a/src/components/layout/MobileNavigation.jsx +++ b/src/components/layout/MobileNavigation.tsx @@ -1,17 +1,23 @@ -import React from 'react' -import { useStore } from '../../lib/store' +import React from 'react'; +import { useStore } from '../../lib/store'; + +interface NavItem { + id: string; + label: string; + icon: string; +} // Mirrors the most-used tabs; full nav is in the sidebar (hamburger menu). -const QUICK_NAV = [ - { id: 'overview', label: 'Home', icon: '◈' }, - { id: 'transactions', label: 'Txns', icon: '⇄' }, - { id: 'dex', label: 'DEX', icon: '⇌' }, - { id: 'wallet', label: 'Wallet', icon: '⊡' }, +const QUICK_NAV: NavItem[] = [ + { id: 'overview', label: 'Home', icon: '◈' }, + { id: 'transactions', label: 'Txns', icon: '⇄' }, + { id: 'dex', label: 'DEX', icon: '⇌' }, + { id: 'wallet', label: 'Wallet', icon: '⊡' }, { id: 'settings', label: 'Settings', icon: '⚙' }, -] +]; export default function MobileNavigation() { - const { activeTab, setActiveTab } = useStore() + const { activeTab, setActiveTab } = useStore(); return ( - ) -} + ); +} \ No newline at end of file diff --git a/src/components/layout/MobileSidebar.jsx b/src/components/layout/MobileSidebar.tsx similarity index 95% rename from src/components/layout/MobileSidebar.jsx rename to src/components/layout/MobileSidebar.tsx index 413f79aa..c5706a7c 100644 --- a/src/components/layout/MobileSidebar.jsx +++ b/src/components/layout/MobileSidebar.tsx @@ -9,7 +9,13 @@ import React, { useCallback, useEffect, useRef } from "react"; import { useStore } from "../../lib/store"; -const NAV_ITEMS = [ +interface NavItem { + id: string; + label: string; + icon: string; +} + +const NAV_ITEMS: NavItem[] = [ { id: "overview", label: "Overview", icon: "◈" }, { id: "account", label: "Account", icon: "◉" }, { id: "compare", label: "Compare", icon: "◫" }, @@ -74,16 +80,15 @@ export function HamburgerButton() { * Full-screen overlay + slide-in drawer for mobile navigation. */ export default function MobileSidebar() { - const { activeTab, setActiveTab, isMobileMenuOpen, setMobileMenuOpen, theme, toggleTheme, network } = - useStore(); - const drawerRef = useRef(null); + const { activeTab, setActiveTab, isMobileMenuOpen, setMobileMenuOpen, theme, toggleTheme, network } = useStore(); + const drawerRef = useRef(null); const close = useCallback(() => setMobileMenuOpen(false), [setMobileMenuOpen]); // Close on Escape useEffect(() => { if (!isMobileMenuOpen) return; - const handler = (e) => { if (e.key === "Escape") close(); }; + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") close(); }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, [isMobileMenuOpen, close]); @@ -91,7 +96,7 @@ export default function MobileSidebar() { // Focus first nav item when opening useEffect(() => { if (!isMobileMenuOpen) return; - const first = drawerRef.current?.querySelector("button, a"); + const first = drawerRef.current?.querySelector("button, a") as HTMLElement | null; first?.focus(); }, [isMobileMenuOpen]); @@ -275,4 +280,4 @@ export default function MobileSidebar() { ); -} +} \ No newline at end of file diff --git a/src/components/layout/ResponsiveContainer.jsx b/src/components/layout/ResponsiveContainer.jsx deleted file mode 100644 index 65d33e63..00000000 --- a/src/components/layout/ResponsiveContainer.jsx +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react' -import { useResponsive } from '../../hooks/useResponsive' - -export default function ResponsiveContainer({ - children, - className = '', - mobileLayout = false, - tabletLayout = false, - style = {}, - ...props -}) { - const { isMobile, isTablet } = useResponsive() - - const getResponsiveStyles = () => { - const baseStyles = { - width: '100%', - ...style, - } - - if (isMobile && mobileLayout) { - return { - ...baseStyles, - display: 'flex', - flexDirection: 'column', - gap: '12px', - } - } - - if (isTablet && tabletLayout) { - return { - ...baseStyles, - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', - gap: '16px', - } - } - - return baseStyles - } - - const getResponsiveClassName = () => { - let classes = className - - if (isMobile) { - classes += ' mobile-layout' - } - - if (isTablet) { - classes += ' tablet-layout' - } - - return classes.trim() - } - - return ( -
- {children} -
- ) -} - -// Responsive grid component -export function ResponsiveGrid({ - children, - columns = { mobile: 1, tablet: 2, desktop: 3 }, - gap = { mobile: '12px', tablet: '16px', desktop: '20px' }, - className = '', - style = {}, - ...props -}) { - const { isMobile, isTablet } = useResponsive() - - const getGridStyles = () => { - let gridColumns = columns.desktop - let gridGap = gap.desktop - - if (isMobile) { - gridColumns = columns.mobile - gridGap = gap.mobile - } else if (isTablet) { - gridColumns = columns.tablet - gridGap = gap.tablet - } - - return { - display: 'grid', - gridTemplateColumns: `repeat(${gridColumns}, 1fr)`, - gap: gridGap, - width: '100%', - ...style, - } - } - - return ( -
- {children} -
- ) -} - -// Responsive flex component -export function ResponsiveFlex({ - children, - direction = { mobile: 'column', tablet: 'row', desktop: 'row' }, - gap = { mobile: '12px', tablet: '16px', desktop: '20px' }, - align = 'stretch', - justify = 'flex-start', - className = '', - style = {}, - ...props -}) { - const { isMobile, isTablet } = useResponsive() - - const getFlexStyles = () => { - let flexDirection = direction.desktop - let flexGap = gap.desktop - - if (isMobile) { - flexDirection = direction.mobile - flexGap = gap.mobile - } else if (isTablet) { - flexDirection = direction.tablet - flexGap = gap.tablet - } - - return { - display: 'flex', - flexDirection, - gap: flexGap, - alignItems: align, - justifyContent: justify, - width: '100%', - ...style, - } - } - - return ( -
- {children} -
- ) -} \ No newline at end of file diff --git a/src/components/layout/ResponsiveContainer.tsx b/src/components/layout/ResponsiveContainer.tsx new file mode 100644 index 00000000..46ea796d --- /dev/null +++ b/src/components/layout/ResponsiveContainer.tsx @@ -0,0 +1,186 @@ +import React, { ReactNode } from 'react'; +import { useResponsive } from '../../hooks/useResponsive'; + +type ResponsiveSpacing = { mobile: string | number; tablet: string | number; desktop: string | number }; +type ResponsiveColumns = { mobile: number; tablet: number; desktop: number }; +type ResponsiveDirection = { mobile: React.CSSProperties['flexDirection']; tablet: React.CSSProperties['flexDirection']; desktop: React.CSSProperties['flexDirection'] }; + +export interface ResponsiveContainerProps extends React.HTMLAttributes { + children: ReactNode; + className?: string; + mobileLayout?: boolean; + tabletLayout?: boolean; + style?: React.CSSProperties; +} + +export function ResponsiveContainer({ + children, + className = '', + mobileLayout = false, + tabletLayout = false, + style = {}, + ...props +}: ResponsiveContainerProps) { + const { isMobile, isTablet } = useResponsive() as { isMobile: boolean; isTablet: boolean }; + + const getResponsiveStyles = (): React.CSSProperties => { + const baseStyles: React.CSSProperties = { + width: '100%', + ...style, + }; + + if (isMobile && mobileLayout) { + return { + ...baseStyles, + display: 'flex', + flexDirection: 'column', + gap: '12px', + }; + } + + if (isTablet && tabletLayout) { + return { + ...baseStyles, + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', + gap: '16px', + }; + } + + return baseStyles; + }; + + const getResponsiveClassName = () => { + let classes = className; + + if (isMobile) { + classes += ' mobile-layout'; + } + + if (isTablet) { + classes += ' tablet-layout'; + } + + return classes.trim(); + }; + + return ( +
+ {children} +
+ ); +} + +export interface ResponsiveGridProps extends React.HTMLAttributes { + children: ReactNode; + columns?: ResponsiveColumns; + gap?: ResponsiveSpacing; + className?: string; + style?: React.CSSProperties; +} + +// Responsive grid component +export function ResponsiveGrid({ + children, + columns = { mobile: 1, tablet: 2, desktop: 3 }, + gap = { mobile: '12px', tablet: '16px', desktop: '20px' }, + className = '', + style = {}, + ...props +}: ResponsiveGridProps) { + const { isMobile, isTablet } = useResponsive() as { isMobile: boolean; isTablet: boolean }; + + const getGridStyles = (): React.CSSProperties => { + let gridColumns = columns.desktop; + let gridGap = gap.desktop; + + if (isMobile) { + gridColumns = columns.mobile; + gridGap = gap.mobile; + } else if (isTablet) { + gridColumns = columns.tablet; + gridGap = gap.tablet; + } + + return { + display: 'grid', + gridTemplateColumns: `repeat(${gridColumns}, 1fr)`, + gap: gridGap, + width: '100%', + ...style, + }; + }; + + return ( +
+ {children} +
+ ); +} + +export interface ResponsiveFlexProps extends React.HTMLAttributes { + children: ReactNode; + direction?: ResponsiveDirection; + gap?: ResponsiveSpacing; + align?: React.CSSProperties['alignItems']; + justify?: React.CSSProperties['justifyContent']; + className?: string; + style?: React.CSSProperties; +} + +// Responsive flex component +export function ResponsiveFlex({ + children, + direction = { mobile: 'column', tablet: 'row', desktop: 'row' }, + gap = { mobile: '12px', tablet: '16px', desktop: '20px' }, + align = 'stretch', + justify = 'flex-start', + className = '', + style = {}, + ...props +}: ResponsiveFlexProps) { + const { isMobile, isTablet } = useResponsive() as { isMobile: boolean; isTablet: boolean }; + + const getFlexStyles = (): React.CSSProperties => { + let flexDirection = direction.desktop; + let flexGap = gap.desktop; + + if (isMobile) { + flexDirection = direction.mobile; + flexGap = gap.mobile; + } else if (isTablet) { + flexDirection = direction.tablet; + flexGap = gap.tablet; + } + + return { + display: 'flex', + flexDirection, + gap: flexGap, + alignItems: align, + justifyContent: justify, + width: '100%', + ...style, + }; + }; + + return ( +
+ {children} +
+ ); +} + +export default ResponsiveContainer; \ No newline at end of file diff --git a/src/components/layout/ResponsiveSidebar.jsx b/src/components/layout/ResponsiveSidebar.tsx similarity index 65% rename from src/components/layout/ResponsiveSidebar.jsx rename to src/components/layout/ResponsiveSidebar.tsx index dcd8ef88..cf4cab67 100644 --- a/src/components/layout/ResponsiveSidebar.jsx +++ b/src/components/layout/ResponsiveSidebar.tsx @@ -3,14 +3,14 @@ * On mobile: renders as a slide-in drawer controlled by isMobileMenuOpen. * On desktop: renders as a fixed sidebar. */ -import React from 'react' -import { useStore } from '../../lib/store' -import { useResponsive } from '../../hooks/useResponsive' -import Sidebar from './Sidebar' +import React from 'react'; +import { useStore } from '../../lib/store'; +import { useResponsive } from '../../hooks/useResponsive'; +import Sidebar from './Sidebar'; export default function ResponsiveSidebar() { - const { isMobileMenuOpen, setMobileMenuOpen } = useStore() - const { isMobile } = useResponsive() + const { isMobileMenuOpen, setMobileMenuOpen } = useStore(); + const { isMobile } = useResponsive() as { isMobile: boolean }; return ( <> @@ -24,5 +24,5 @@ export default function ResponsiveSidebar() { )} - ) -} + ); +} \ No newline at end of file diff --git a/src/components/layout/SearchBar.jsx b/src/components/layout/SearchBar.tsx similarity index 84% rename from src/components/layout/SearchBar.jsx rename to src/components/layout/SearchBar.tsx index d1fa69eb..313af64f 100644 --- a/src/components/layout/SearchBar.jsx +++ b/src/components/layout/SearchBar.tsx @@ -2,7 +2,19 @@ import React, { useMemo, useState } from "react"; import { Search, Save, X } from "lucide-react"; import { useSearch } from "../../hooks/useSearch"; -export default function SearchBar({ onSelectResult }) { +export interface SearchResult { + id: string; + label: string; + type: string; + meta?: string; + [key: string]: any; +} + +export interface SearchBarProps { + onSelectResult?: (result: SearchResult) => void; +} + +export default function SearchBar({ onSelectResult }: SearchBarProps) { const { query, setQuery, @@ -12,8 +24,9 @@ export default function SearchBar({ onSelectResult }) { removeSavedSearch, applySavedSearch, } = useSearch(); - const [nameInput, setNameInput] = useState(""); - const [open, setOpen] = useState(false); + + const [nameInput, setNameInput] = useState(""); + const [open, setOpen] = useState(false); const showResults = open && query.trim().length > 0; const hasResults = results.length > 0; @@ -44,7 +57,7 @@ export default function SearchBar({ onSelectResult }) { value={query} onFocus={() => setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 120)} - onChange={(event) => setQuery(event.target.value)} + onChange={(event: React.ChangeEvent) => setQuery(event.target.value)} placeholder="Search transactions, operations, and account data" style={{ width: "100%", @@ -67,6 +80,7 @@ export default function SearchBar({ onSelectResult }) { padding: "5px", display: "flex", alignItems: "center", + cursor: 'pointer' }} > @@ -76,7 +90,7 @@ export default function SearchBar({ onSelectResult }) {
setNameInput(event.target.value)} + onChange={(event: React.ChangeEvent) => setNameInput(event.target.value)} placeholder="Saved search name" style={{ border: "1px solid var(--border)", @@ -91,7 +105,7 @@ export default function SearchBar({ onSelectResult }) { />
- {topSaved.map((entry) => ( + {topSaved.map((entry: any) => ( {entry.name} @@ -126,6 +141,7 @@ export default function SearchBar({ onSelectResult }) { color: "var(--text-muted)", display: "flex", alignItems: "center", + cursor: 'pointer' }} > @@ -156,7 +172,7 @@ export default function SearchBar({ onSelectResult }) { No results for this query.
)} - {results.map((result) => ( + {results.map((result: SearchResult) => (
); -} +} \ No newline at end of file diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.tsx similarity index 79% rename from src/components/layout/Sidebar.jsx rename to src/components/layout/Sidebar.tsx index a3217511..bb556a7f 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.tsx @@ -1,13 +1,20 @@ -import React, { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { useStore } from '../../lib/store' -import CopyableValue from '../dashboard/CopyableValue' -import { NETWORKS, updateCustomNetworkConfig, switchToCustomProfile, loadCustomNetworkProfiles } from '../../lib/stellar' -import { getActiveProfile } from '../../lib/userPreferences' +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useStore } from '../../lib/store'; +import CopyableValue from '../dashboard/CopyableValue'; +import { NETWORKS, updateCustomNetworkConfig, switchToCustomProfile, loadCustomNetworkProfiles } from '../../lib/stellar'; +import { getActiveProfile } from '../../lib/userPreferences'; -const SESSION_API_KEY = 'stellar_custom_api_key' +const SESSION_API_KEY = 'stellar_custom_api_key'; -const NAV_ITEMS = [ +interface NavItem { + id?: string; + type?: 'header' | 'link'; + label: string; + icon?: string; +} + +const NAV_ITEMS: NavItem[] = [ { type: 'header', label: 'ANALYTICS' }, { id: 'overview', label: 'Overview', icon: '◈' }, { id: 'account', label: 'Account', icon: '◉' }, @@ -18,19 +25,19 @@ const NAV_ITEMS = [ { id: 'assets', label: 'Assets', icon: '💎' }, { id: 'anchors', label: 'Anchors', icon: '⚓' }, { id: 'search', label: 'Search', icon: '🔍' }, - + { type: 'header', label: 'NETWORK' }, { id: 'network', label: 'Network Info', icon: '◎' }, { id: 'realtime', label: 'Real-Time', icon: '◉' }, { id: 'liveActivity', label: 'Live Activity', icon: '⚡' }, { id: 'cacheStats', label: 'Cache Stats', icon: '⊞' }, - + { type: 'header', label: 'BUILD' }, { id: 'builder', label: 'Builder', icon: '⚒' }, { id: 'txSimulator', label: 'Simulator', icon: '▷' }, { id: 'advancedSim', label: 'Advanced', icon: '⚡' }, { id: 'faucet', label: 'Faucet', icon: '⬡' }, - + { type: 'header', label: 'TOOLS' }, { id: 'wallet', label: 'Wallet', icon: '⊡' }, { id: 'signer', label: 'Signer', icon: '✎' }, @@ -41,59 +48,80 @@ const NAV_ITEMS = [ { id: 'systemHealth', label: 'Health', icon: '⚕' }, { id: 'settings', label: 'Settings', icon: '⚙' }, { id: 'audit', label: 'Audit', icon: '⊟' }, -] +]; + +export interface SidebarProps { + isMobile?: boolean; +} -export default function Sidebar({ isMobile = false }) { - const navigate = useNavigate() - const { - activeTab, - network, - setNetwork, - connectedAddress, - theme, +export interface CustomProfile { + id: string; + name: string; + horizonUrl: string; + sorobanUrl?: string; + passphrase: string; +} + +export default function Sidebar({ isMobile = false }: SidebarProps) { + const navigate = useNavigate(); + const { + activeTab, + network, + setNetwork, + connectedAddress, + theme, toggleTheme, isMobileMenuOpen, setMobileMenuOpen, - } = useStore() + } = useStore(); - const [customProfiles, setCustomProfiles] = useState([]) - const [activeProfileId, setActiveProfileId] = useState(null) + const [customProfiles, setCustomProfiles] = useState([]); + const [activeProfileId, setActiveProfileId] = useState(null); + const [customHeaderName, setCustomHeaderName] = useState(''); + const [customHeaderValue, setCustomHeaderValue] = useState(''); // Load custom profiles on mount (Issue #188) useEffect(() => { if (network === 'custom') { - loadCustomNetworkProfiles().then(profiles => { - setCustomProfiles(profiles) + loadCustomNetworkProfiles().then((profiles: CustomProfile[]) => { + setCustomProfiles(profiles); // Load active profile - getActiveProfile().then(profile => { + getActiveProfile().then((profile: CustomProfile | null) => { if (profile) { - setActiveProfileId(profile.id) + setActiveProfileId(profile.id); // Populate the network config updateCustomNetworkConfig({ horizonUrl: profile.horizonUrl, sorobanUrl: profile.sorobanUrl, passphrase: profile.passphrase, - }) + }); } - }) - }) + }); + }); } - }, [network]) + }, [network]); + + const handleNavClick = (tabId: string) => { + navigate(`/${tabId}`); + setMobileMenuOpen(false); + }; - const handleNavClick = (tabId) => { - navigate(`/${tabId}`) - setMobileMenuOpen(false) // Close mobile menu after navigation - } + const handleSwitchProfile = (id: string) => { + setActiveProfileId(id); + if (id && typeof switchToCustomProfile === 'function') { + switchToCustomProfile(id); + } + }; // Restore custom API key from sessionStorage on mount useEffect(() => { - const saved = sessionStorage.getItem(SESSION_API_KEY) + const saved = sessionStorage.getItem(SESSION_API_KEY); if (saved) { - updateCustomNetworkConfig({ customHeaders: { Authorization: `Bearer ${saved}` } }) + updateCustomNetworkConfig({ customHeaders: { Authorization: `Bearer ${saved}` } }); } - }, []) + }, []); - const sidebarStyles = { + const sidebarStyles: React.CSSProperties = { width: isMobile ? 'var(--sidebar-width-mobile)' : 'var(--sidebar-width)', minHeight: '100vh', background: 'var(--bg-surface)', @@ -108,9 +136,9 @@ export default function Sidebar({ isMobile = false }) { transform: isMobile ? (isMobileMenuOpen ? 'translateX(0)' : 'translateX(-100%)') : 'translateX(0)', transition: 'transform var(--transition)', boxShadow: isMobile && isMobileMenuOpen ? '4px 0 20px rgba(0, 0, 0, 0.3)' : 'none', - } + }; - const customInputStyle = { + const customInputStyle: React.CSSProperties = { width: '100%', padding: '6px 10px', fontSize: '10px', @@ -120,15 +148,18 @@ export default function Sidebar({ isMobile = false }) { borderRadius: 'var(--radius-sm)', color: 'var(--text-primary)', outline: 'none', - } + }; - const updateCustomHeader = (name, value) => { - setCustomHeaderName(name) - setCustomHeaderValue(value) + const updateCustomHeader = (name: string, value: string) => { + setCustomHeaderName(name); + setCustomHeaderValue(value); + + const updatedHeaders: Record = name.trim() && value.trim() ? { [name.trim()]: value.trim() } : {}; + updateCustomNetworkConfig({ - headers: name.trim() && value.trim() ? { [name.trim()]: value.trim() } : {}, - }) - } + headers: updatedHeaders, + }); + }; return ( <> @@ -154,13 +185,13 @@ export default function Sidebar({ isMobile = false }) { cursor: 'pointer', transition: 'var(--transition)', }} - onMouseEnter={e => { - e.currentTarget.style.color = 'var(--text-primary)' - e.currentTarget.style.background = 'var(--bg-elevated)' + onMouseEnter={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-primary)'; + e.currentTarget.style.background = 'var(--bg-elevated)'; }} - onMouseLeave={e => { - e.currentTarget.style.color = 'var(--text-secondary)' - e.currentTarget.style.background = 'var(--bg-hover)' + onMouseLeave={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-secondary)'; + e.currentTarget.style.background = 'var(--bg-hover)'; }} > ✕ @@ -191,7 +222,7 @@ export default function Sidebar({ isMobile = false }) {
NETWORK
)} - + updateCustomNetworkConfig({ horizonUrl: e.target.value.trim() })} /> updateCustomNetworkConfig({ sorobanUrl: e.target.value.trim() })} /> updateCustomNetworkConfig({ passphrase: e.target.value.trim() })} /> @@ -268,13 +298,13 @@ export default function Sidebar({ isMobile = false }) { defaultValue={sessionStorage.getItem(SESSION_API_KEY) || ''} style={customInputStyle} onChange={(e) => { - const val = e.target.value.trim() + const val = e.target.value.trim(); if (val) { - sessionStorage.setItem(SESSION_API_KEY, val) - updateCustomNetworkConfig({ customHeaders: { Authorization: `Bearer ${val}` } }) + sessionStorage.setItem(SESSION_API_KEY, val); + updateCustomNetworkConfig({ customHeaders: { Authorization: `Bearer ${val}` } }); } else { - sessionStorage.removeItem(SESSION_API_KEY) - updateCustomNetworkConfig({ customHeaders: {} }) + sessionStorage.removeItem(SESSION_API_KEY); + updateCustomNetworkConfig({ customHeaders: {} }); } }} /> @@ -298,14 +328,14 @@ export default function Sidebar({ isMobile = false }) { }}> {item.label}
- ) + ); } - const isActive = activeTab === item.id - const isDisabled = item.id === 'faucet' && network === 'mainnet' + const isActive = activeTab === item.id; + const isDisabled = item.id === 'faucet' && network === 'mainnet'; return ( - ) + ); })} @@ -407,13 +437,13 @@ export default function Sidebar({ isMobile = false }) { transition: 'var(--transition)', }} title={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`} - onMouseEnter={e => { - e.currentTarget.style.color = 'var(--text-primary)' - e.currentTarget.style.background = 'var(--bg-hover)' + onMouseEnter={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-primary)'; + e.currentTarget.style.background = 'var(--bg-hover)'; }} - onMouseLeave={e => { - e.currentTarget.style.color = 'var(--text-secondary)' - e.currentTarget.style.background = 'transparent' + onMouseLeave={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-secondary)'; + e.currentTarget.style.background = 'transparent'; }} > {theme === 'light' ? '☾' : '☀'} @@ -421,5 +451,5 @@ export default function Sidebar({ isMobile = false }) { - ) -} + ); +} \ No newline at end of file diff --git a/src/components/layout/ThemeToggle.jsx b/src/components/layout/ThemeToggle.tsx similarity index 91% rename from src/components/layout/ThemeToggle.jsx rename to src/components/layout/ThemeToggle.tsx index 6132257c..d301a627 100644 --- a/src/components/layout/ThemeToggle.jsx +++ b/src/components/layout/ThemeToggle.tsx @@ -3,7 +3,7 @@ import { Sun, Moon } from 'lucide-react'; import { useStore } from '../../lib/store'; import { THEMES } from '../../styles/themes'; -const ThemeToggle = () => { +const ThemeToggle: React.FC = () => { const { theme, toggleTheme } = useStore(); const isLight = theme === THEMES.LIGHT; @@ -26,4 +26,4 @@ const ThemeToggle = () => { ); }; -export default ThemeToggle; +export default ThemeToggle; \ No newline at end of file diff --git a/src/components/layout/WidgetSelector.jsx b/src/components/layout/WidgetSelector.tsx similarity index 87% rename from src/components/layout/WidgetSelector.jsx rename to src/components/layout/WidgetSelector.tsx index 6d65dc51..e8b67f27 100644 --- a/src/components/layout/WidgetSelector.jsx +++ b/src/components/layout/WidgetSelector.tsx @@ -11,8 +11,34 @@ import AccountStatsWidget from './widgets/AccountStatsWidget'; import QuickActionsWidget from './widgets/QuickActionsWidget'; import PriceTickerWidget from './widgets/PriceTickerWidget'; +export interface WidgetDefinition { + id: string; + name: string; + description: string; + icon: string; + component: React.ComponentType; + defaultSize: { width: number; height: number }; + category: string; +} + +export interface WidgetInstance { + id: string; + type: string; + component: React.ReactNode; + width: number; + height: number; + span: number; +} + +export interface WidgetSelectorProps { + onAddWidget: (widget: WidgetInstance) => void; + existingWidgets?: WidgetInstance[]; + isOpen: boolean; + onClose: () => void; +} + // Widget definitions -export const AVAILABLE_WIDGETS = { +export const AVAILABLE_WIDGETS: Record = { balance: { id: 'balance', name: 'XLM Balance', @@ -78,7 +104,7 @@ export const AVAILABLE_WIDGETS = { } }; -export const WIDGET_CATEGORIES = { +export const WIDGET_CATEGORIES: Record = { account: { name: 'Account', icon: '👤', color: 'var(--cyan)' }, activity: { name: 'Activity', icon: '⚡', color: 'var(--green)' }, network: { name: 'Network', icon: '🌐', color: 'var(--amber)' }, @@ -86,24 +112,25 @@ export const WIDGET_CATEGORIES = { tools: { name: 'Tools', icon: '🔧', color: 'var(--text-secondary)' } }; -export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOpen, onClose }) { - const [selectedCategory, setSelectedCategory] = useState('account'); - const { isMobile } = useResponsive(); +export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOpen, onClose }: WidgetSelectorProps) { + const [selectedCategory, setSelectedCategory] = useState('account'); + const { isMobile } = useResponsive() as { isMobile: boolean }; if (!isOpen) return null; - const handleAddWidget = (widgetType) => { + const handleAddWidget = (widgetType: string) => { const widget = AVAILABLE_WIDGETS[widgetType]; if (!widget) return; - const newWidget = { + const newWidget: WidgetInstance = { id: `${widgetType}-${Date.now()}`, type: widgetType, component: React.createElement(widget.component, { key: `${widgetType}-${Date.now()}`, onRefresh: () => {} }), - ...widget.defaultSize, + width: widget.defaultSize.width, + height: widget.defaultSize.height, span: 1 }; @@ -116,15 +143,15 @@ export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOp }); }; - const isWidgetAdded = (widgetType) => { + const isWidgetAdded = (widgetType: string) => { return existingWidgets.some(w => w.type === widgetType); }; - const getWidgetsByCategory = (category) => { + const getWidgetsByCategory = (category: string) => { return Object.values(AVAILABLE_WIDGETS).filter(w => w.category === category); }; - const overlayStyles = { + const overlayStyles: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0, 0, 0, 0.5)', @@ -136,7 +163,7 @@ export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOp padding: isMobile ? '20px' : '40px' }; - const modalStyles = { + const modalStyles: React.CSSProperties = { background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', @@ -148,7 +175,7 @@ export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOp flexDirection: 'column' }; - const headerStyles = { + const headerStyles: React.CSSProperties = { padding: isMobile ? '16px 20px' : '20px 24px', borderBottom: '1px solid var(--border)', display: 'flex', @@ -156,7 +183,7 @@ export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOp justifyContent: 'space-between' }; - const contentStyles = { + const contentStyles: React.CSSProperties = { flex: 1, overflow: 'auto', padding: isMobile ? '16px 20px' : '20px 24px' @@ -269,13 +296,13 @@ export default function WidgetSelector({ onAddWidget, existingWidgets = [], isOp transition: 'var(--transition)' }} onClick={() => !isAdded && handleAddWidget(widget.id)} - onMouseEnter={e => { + onMouseEnter={(e: React.MouseEvent) => { if (!isAdded) { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.boxShadow = '0 4px 12px var(--cyan-glow-sm)'; } }} - onMouseLeave={e => { + onMouseLeave={(e: React.MouseEvent) => { if (!isAdded) { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.boxShadow = 'none'; diff --git a/src/components/layout/widgets/AccountStatsWidget.jsx b/src/components/layout/widgets/AccountStatsWidget.tsx similarity index 92% rename from src/components/layout/widgets/AccountStatsWidget.jsx rename to src/components/layout/widgets/AccountStatsWidget.tsx index f7de54f3..31bda67a 100644 --- a/src/components/layout/widgets/AccountStatsWidget.jsx +++ b/src/components/layout/widgets/AccountStatsWidget.tsx @@ -1,9 +1,14 @@ +import React from 'react'; import { useStore } from '../../../lib/store'; import { shortAddress } from '../../../lib/stellar'; import CopyableValue from '../../dashboard/CopyableValue'; import WidgetBase from './WidgetBase'; -export default function AccountStatsWidget({ onRefresh }) { +export interface BaseWidgetProps { + onRefresh?: () => void; +} + +export default function AccountStatsWidget({ onRefresh }: BaseWidgetProps) { const { accountData, transactions, operations, connectedAddress, txLoading, opsLoading } = useStore(); const stats = [ @@ -23,7 +28,7 @@ export default function AccountStatsWidget({ onRefresh }) { }, { label: 'Transactions', - value: txLoading ? '...' : transactions.length.toString(), + value: txLoading ? '...' : (transactions?.length || 0).toString(), subtitle: 'recent', icon: '⇄', color: 'var(--green)', @@ -31,7 +36,7 @@ export default function AccountStatsWidget({ onRefresh }) { }, { label: 'Operations', - value: opsLoading ? '...' : operations.length.toString(), + value: opsLoading ? '...' : (operations?.length || 0).toString(), subtitle: 'recent', icon: '⚙️', color: 'var(--text-secondary)', @@ -73,17 +78,17 @@ export default function AccountStatsWidget({ onRefresh }) { textAlign: 'center', transition: 'var(--transition)' }} - onMouseEnter={e => { + onMouseEnter={(e: React.MouseEvent) => { e.currentTarget.style.borderColor = stat.color; }} - onMouseLeave={e => { + onMouseLeave={(e: React.MouseEvent) => { e.currentTarget.style.borderColor = 'var(--border)'; }} >
{stat.icon}
- +
- +
🔐 Security Thresholds
- +
✍️ Signers ({signers.length})
- +
- {signers.slice(0, 3).map((signer) => ( + {signers.slice(0, 3).map((signer: any) => (
- diff --git a/src/components/layout/widgets/AssetsWidget.jsx b/src/components/layout/widgets/AssetsWidget.tsx similarity index 87% rename from src/components/layout/widgets/AssetsWidget.jsx rename to src/components/layout/widgets/AssetsWidget.tsx index 8ac599ff..9720bf0e 100644 --- a/src/components/layout/widgets/AssetsWidget.jsx +++ b/src/components/layout/widgets/AssetsWidget.tsx @@ -5,7 +5,19 @@ import useAssetUsdEstimates, { formatEstimatedUsd } from '../../../hooks/useAsse import CopyableValue from '../../dashboard/CopyableValue'; import WidgetBase from './WidgetBase'; -export default function AssetsWidget({ onRefresh, maxAssets = 5 }) { +export interface AssetsWidgetProps { + onRefresh?: () => void; + maxAssets?: number; +} + +export interface AssetBalance { + asset_type: string; + asset_code?: string; + asset_issuer?: string; + balance: string; +} + +export default function AssetsWidget({ onRefresh, maxAssets = 5 }: AssetsWidgetProps) { const { accountData, connectedAddress, network } = useStore(); const { getEstimate } = useAssetUsdEstimates({ @@ -15,7 +27,7 @@ export default function AssetsWidget({ onRefresh, maxAssets = 5 }) { refreshKey: accountData, }); - const otherAssets = accountData?.balances?.filter(b => b.asset_type !== 'native') || []; + const otherAssets: AssetBalance[] = accountData?.balances?.filter((b: AssetBalance) => b.asset_type !== 'native') || []; const displayAssets = otherAssets.slice(0, maxAssets); const hasMore = otherAssets.length > maxAssets; @@ -68,8 +80,8 @@ export default function AssetsWidget({ onRefresh, maxAssets = 5 }) { borderBottom: i < displayAssets.length - 1 ? '1px solid var(--border)' : 'none', transition: 'var(--transition)', }} - onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} - onMouseLeave={e => e.currentTarget.style.background = 'transparent'} + onMouseEnter={(e: React.MouseEvent) => e.currentTarget.style.background = 'var(--bg-hover)'} + onMouseLeave={(e: React.MouseEvent) => e.currentTarget.style.background = 'transparent'} >
- {formatEstimatedUsd(getEstimate(asset).usd)} + {formatEstimatedUsd(getEstimate(asset)!.usd)} )}
@@ -158,7 +170,7 @@ export default function AssetsWidget({ onRefresh, maxAssets = 5 }) { fontSize: '12px', color: 'var(--text-muted)' }}> - +{otherAssets.length - maxAssets} more asset{otherAssets.length - maxAssets !== 1 ? 's' : ''} + +{(otherAssets.length) - maxAssets} more asset{(otherAssets.length) - maxAssets !== 1 ? 's' : ''}
)} diff --git a/src/components/layout/widgets/BalanceWidget.jsx b/src/components/layout/widgets/BalanceWidget.tsx similarity index 89% rename from src/components/layout/widgets/BalanceWidget.jsx rename to src/components/layout/widgets/BalanceWidget.tsx index d55d5bcd..b2144060 100644 --- a/src/components/layout/widgets/BalanceWidget.jsx +++ b/src/components/layout/widgets/BalanceWidget.tsx @@ -4,9 +4,13 @@ import { formatXLM } from '../../../lib/stellar'; import useAssetUsdEstimates, { formatEstimatedUsd } from '../../../hooks/useAssetUsdEstimates'; import WidgetBase from './WidgetBase'; -export default function BalanceWidget({ onRefresh }) { +export interface BaseWidgetProps { + onRefresh?: () => void; +} + +export default function BalanceWidget({ onRefresh }: BaseWidgetProps) { const { accountData, connectedAddress, network } = useStore(); - + const { getEstimate } = useAssetUsdEstimates({ balances: accountData?.balances || [], connectedAddress, @@ -14,10 +18,10 @@ export default function BalanceWidget({ onRefresh }) { refreshKey: accountData, }); - const xlmBalance = accountData?.balances?.find(b => b.asset_type === 'native'); + const xlmBalance = accountData?.balances?.find((b: any) => b.asset_type === 'native'); const xlmEstimate = xlmBalance ? getEstimate(xlmBalance) : null; - const totalUsdValue = accountData?.balances?.reduce((total, balance) => { + const totalUsdValue = accountData?.balances?.reduce((total: number, balance: any) => { const estimate = getEstimate(balance); return total + (estimate?.usd || 0); }, 0) || 0; @@ -100,8 +104,8 @@ export default function BalanceWidget({ onRefresh }) { Total Portfolio: - diff --git a/src/components/layout/widgets/NetworkStatsWidget.jsx b/src/components/layout/widgets/NetworkStatsWidget.tsx similarity index 91% rename from src/components/layout/widgets/NetworkStatsWidget.jsx rename to src/components/layout/widgets/NetworkStatsWidget.tsx index 5eebe9c6..70ae47ba 100644 --- a/src/components/layout/widgets/NetworkStatsWidget.jsx +++ b/src/components/layout/widgets/NetworkStatsWidget.tsx @@ -5,15 +5,19 @@ import { useErrorHandler } from '../../../hooks/useErrorHandler'; import WidgetBase from './WidgetBase'; import { format } from 'date-fns'; -export default function NetworkStatsWidget({ onRefresh }) { - const { - network, - networkStats, - setNetworkStats, - statsLoading, - setStatsLoading +export interface BaseWidgetProps { + onRefresh?: () => void; +} + +export default function NetworkStatsWidget({ onRefresh }: BaseWidgetProps) { + const { + network, + networkStats, + setNetworkStats, + statsLoading, + setStatsLoading } = useStore(); - + const { handleError } = useErrorHandler('NetworkStatsWidget'); const loadNetworkStats = async () => { @@ -76,7 +80,7 @@ export default function NetworkStatsWidget({ onRefresh }) { gap: '16px', height: '100%' }}> - {stats.map((stat, index) => ( + {stats.map((stat) => (
{ + onMouseEnter={(e: React.MouseEvent) => { e.currentTarget.style.borderColor = stat.color; e.currentTarget.style.boxShadow = `0 0 0 1px ${stat.color}20`; }} - onMouseLeave={e => { + onMouseLeave={(e: React.MouseEvent) => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.boxShadow = 'none'; }} @@ -104,7 +108,7 @@ export default function NetworkStatsWidget({ onRefresh }) { }}> {stat.icon}
- +
{stat.label}
- +
{stat.value}
- + {stat.subtitle && (
void; +} + +export interface PriceData { + xlm: { + price: number; + change24h: number; + volume24h: number; + }; +} + +export default function PriceTickerWidget({ onRefresh }: BaseWidgetProps) { const { network } = useStore(); - const [priceData, setPriceData] = useState(null); - const [loading, setLoading] = useState(false); + const [priceData, setPriceData] = useState(null); + const [loading, setLoading] = useState(false); const { handleError } = useErrorHandler('PriceTickerWidget'); const fetchPriceData = async () => { if (network !== 'mainnet') return; // Only show prices for mainnet - + try { setLoading(true); // Mock price data - in a real app, you'd fetch from a price API - const mockData = { + const mockData: PriceData = { xlm: { price: 0.1234, change24h: 2.45, @@ -158,8 +170,8 @@ export default function PriceTickerWidget({ onRefresh }) { 24h Volume: - diff --git a/src/components/layout/widgets/QuickActionsWidget.jsx b/src/components/layout/widgets/QuickActionsWidget.tsx similarity index 88% rename from src/components/layout/widgets/QuickActionsWidget.jsx rename to src/components/layout/widgets/QuickActionsWidget.tsx index 684be42f..74a76137 100644 --- a/src/components/layout/widgets/QuickActionsWidget.jsx +++ b/src/components/layout/widgets/QuickActionsWidget.tsx @@ -1,10 +1,15 @@ +import React from 'react'; import { useStore } from '../../../lib/store'; import WidgetBase from './WidgetBase'; import { useResponsive } from '../../../hooks/useResponsive'; -export default function QuickActionsWidget({ onRefresh }) { +export interface BaseWidgetProps { + onRefresh?: () => void; +} + +export default function QuickActionsWidget({ onRefresh }: BaseWidgetProps) { const { setActiveTab } = useStore(); - const { isMobile } = useResponsive(); + const { isMobile } = useResponsive() as { isMobile: boolean }; const actions = [ { @@ -67,12 +72,12 @@ export default function QuickActionsWidget({ onRefresh }) { alignItems: 'center', gap: '8px' }} - onMouseEnter={e => { + onMouseEnter={(e: React.MouseEvent) => { e.currentTarget.style.borderColor = action.color; e.currentTarget.style.background = `${action.color}10`; e.currentTarget.style.transform = 'translateY(-2px)'; }} - onMouseLeave={e => { + onMouseLeave={(e: React.MouseEvent) => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--bg-elevated)'; e.currentTarget.style.transform = 'translateY(0)'; @@ -81,7 +86,7 @@ export default function QuickActionsWidget({ onRefresh }) {
{action.icon}
- +
{action.label}
- +
void; + maxTransactions?: number; +} + +export default function TransactionsWidget({ onRefresh, maxTransactions = 5 }: TransactionsWidgetProps) { const { transactions, txLoading, network } = useStore(); - - const displayTransactions = transactions.slice(0, maxTransactions); - const hasMore = transactions.length > maxTransactions; + + const displayTransactions = transactions?.slice(0, maxTransactions) || []; + const hasMore = (transactions?.length || 0) > maxTransactions; return ( - {transactions.length === 0 ? ( + {!transactions || transactions.length === 0 ? (
) : (
- {displayTransactions.map((tx, i) => ( -
( +
e.currentTarget.style.background = 'var(--bg-hover)'} - onMouseLeave={e => e.currentTarget.style.background = 'transparent'} + onMouseEnter={(e: React.MouseEvent) => e.currentTarget.style.background = 'var(--bg-hover)'} + onMouseLeave={(e: React.MouseEvent) => e.currentTarget.style.background = 'transparent'} > {/* Status Indicator */} ))} - + {hasMore && (
- +{transactions.length - maxTransactions} more transaction{transactions.length - maxTransactions !== 1 ? 's' : ''} + +{(transactions?.length || 0) - maxTransactions} more transaction{(transactions?.length || 0) - maxTransactions !== 1 ? 's' : ''}
)} diff --git a/src/components/layout/widgets/WidgetBase.jsx b/src/components/layout/widgets/WidgetBase.tsx similarity index 86% rename from src/components/layout/widgets/WidgetBase.jsx rename to src/components/layout/widgets/WidgetBase.tsx index d44254de..1c5af6ad 100644 --- a/src/components/layout/widgets/WidgetBase.jsx +++ b/src/components/layout/widgets/WidgetBase.tsx @@ -1,14 +1,28 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { useResponsive } from '../../../hooks/useResponsive'; +export interface WidgetBaseProps { + title?: string; + subtitle?: string; + icon?: ReactNode; + children?: ReactNode; + loading?: boolean; + error?: { message?: string } | Error | null; + onRefresh?: () => void; + headerActions?: ReactNode; + className?: string; + style?: React.CSSProperties; + contentPadding?: boolean; +} + /** * Base widget component with common styling and functionality */ -export default function WidgetBase({ - title, +export default function WidgetBase({ + title, subtitle, icon, - children, + children, loading = false, error = null, onRefresh, @@ -16,10 +30,10 @@ export default function WidgetBase({ className = '', style = {}, contentPadding = true -}) { - const { isMobile } = useResponsive(); +}: WidgetBaseProps) { + const { isMobile } = useResponsive() as { isMobile: boolean }; - const containerStyles = { + const containerStyles: React.CSSProperties = { display: 'flex', flexDirection: 'column', height: '100%', @@ -30,7 +44,7 @@ export default function WidgetBase({ ...style }; - const headerStyles = { + const headerStyles: React.CSSProperties = { padding: isMobile ? '12px 16px' : '14px 18px', borderBottom: '1px solid var(--border)', background: 'var(--bg-surface)', @@ -40,7 +54,7 @@ export default function WidgetBase({ minHeight: '48px' }; - const titleStyles = { + const titleStyles: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: '8px', @@ -48,7 +62,7 @@ export default function WidgetBase({ minWidth: 0 }; - const contentStyles = { + const contentStyles: React.CSSProperties = { flex: 1, padding: contentPadding ? (isMobile ? '16px' : '18px') : '0', overflow: 'auto', @@ -94,7 +108,7 @@ export default function WidgetBase({ )}
- +
{onRefresh && (
{onRefresh && (