diff --git a/src/components/layout/DashboardGrid.jsx b/src/components/layout/DashboardGrid.tsx similarity index 81% rename from src/components/layout/DashboardGrid.jsx rename to src/components/layout/DashboardGrid.tsx index 57294bec..8288a4a4 100644 --- a/src/components/layout/DashboardGrid.jsx +++ b/src/components/layout/DashboardGrid.tsx @@ -4,6 +4,33 @@ 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: { height: number; span: number }) => void; + onWidgetRemove?: (widget: Widget) => void; + editable?: boolean; + columns?: GridColumns; + gap?: number; + minWidgetHeight?: number; + rowHeight?: number; +} + /** * Customizable responsive dashboard grid with drag-and-drop and resizable widgets. * Width resizing persists as a grid column span so layouts stay fluid at every breakpoint. @@ -18,18 +45,25 @@ export default function DashboardGrid({ gap = 16, minWidgetHeight = 200, rowHeight = 80, -}) { - 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, initialSpan: 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 = (): number => { + if (isMobile) return columns.mobile; + if (isTablet) return columns.tablet; + return columns.desktop; + }; + useEffect(() => { setLayout(widgets); }, [widgets]); @@ -48,7 +82,8 @@ export default function DashboardGrid({ return (gridWidth - gap * (columnCount - 1)) / columnCount; }, [columnCount, gap]); - const updateLayout = useCallback((newLayout) => { + // Handle layout changes + const updateLayout = useCallback((newLayout: Widget[]) => { setLayout(newLayout); onLayoutChange?.(newLayout); addBreadcrumb('Dashboard layout updated', 'user_action', { @@ -57,13 +92,13 @@ export default function DashboardGrid({ }); }, [onLayoutChange, editable]); - const handleDragStart = (event, widget, index) => { + const handleDragStart = (event: React.DragEvent, widget: Widget, index: number) => { if (!editable) return; setDraggedWidget({ widget, index }); event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData('text/plain', widget.id); - event.currentTarget.style.opacity = '0.5'; + event.dataTransfer.setData('text/html', (event.target as HTMLElement).outerHTML); + (event.currentTarget as HTMLElement).style.opacity = '0.5'; addBreadcrumb('Widget drag started', 'user_action', { widgetId: widget.id, @@ -71,13 +106,13 @@ export default function DashboardGrid({ }); }; - const handleDragEnd = (event) => { - event.currentTarget.style.opacity = '1'; + const handleDragEnd = (e: React.DragEvent) => { + (event.currentTarget as HTMLElement).style.opacity = '1'; setDraggedWidget(null); setDragOverIndex(null); }; - const handleDragOver = (event, index) => { + const handleDragOver = (event: React.DragEvent, index: number) => { if (!editable || !draggedWidget) return; event.preventDefault(); @@ -85,7 +120,7 @@ export default function DashboardGrid({ setDragOverIndex(index); }; - const handleDrop = (event, dropIndex) => { + const handleDrop = (event: React.DragEvent, dropIndex: number) => { if (!editable || !draggedWidget) return; event.preventDefault(); @@ -107,13 +142,13 @@ export default function DashboardGrid({ }); }; - const handleResizeStart = (event, widget, index) => { + const handleResizeStart = (event: React.MouseEvent, widget: Widget, index: number) => { if (!editable) return; event.preventDefault(); event.stopPropagation(); - const rect = event.currentTarget.closest('.widget-container').getBoundingClientRect(); + const rect = ((event.currentTarget as HTMLElement).closest('.widget-container') as HTMLElement).getBoundingClientRect(); setResizingWidget({ widget, index, @@ -127,7 +162,7 @@ export default function DashboardGrid({ }); }; - const handleResizeMove = useCallback((event) => { + const handleResizeMove = useCallback((event: MouseEvent) => { if (!resizingWidget) return; const deltaX = event.clientX - resizeStartPos.x; @@ -140,7 +175,7 @@ export default function DashboardGrid({ const nextSpan = clampSpan(spanFromWidth); const nextHeight = Math.max(minWidgetHeight, Math.round(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.gridColumn = `span ${nextSpan}`; widgetElement.style.height = `${nextHeight}px`; @@ -161,7 +196,7 @@ export default function DashboardGrid({ if (!resizingWidget) return; try { - const widgetElement = document.querySelector(`[data-widget-id="${resizingWidget.widget.id}"]`); + const widgetElement = document.querySelector(`[data-widget-id="${resizingWidget.widget.id}"]`) as HTMLElement; if (!widgetElement) return; const rect = widgetElement.getBoundingClientRect(); @@ -170,7 +205,7 @@ export default function DashboardGrid({ ? clampSpan(Math.round((rect.width + gap) / (columnWidth + gap))) : resizingWidget.initialSpan; const nextHeight = Math.max(minWidgetHeight, Math.round(rect.height)); - const updatedWidget = { + const updatedWidget: Widget = { ...resizingWidget.widget, span: nextSpan, height: nextHeight, @@ -216,7 +251,8 @@ export default function DashboardGrid({ }; }, [resizingWidget, handleResizeMove, handleResizeEnd]); - const handleRemoveWidget = (widget, index) => { + // Remove widget + const handleRemoveWidget = (widget: Widget, index: number) => { if (!editable) return; const newLayout = layout.filter((_, i) => i !== index); @@ -225,11 +261,11 @@ export default function DashboardGrid({ 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}, minmax(0, 1fr))`, gridAutoRows: `${rowHeight}px`, @@ -242,11 +278,11 @@ export default function DashboardGrid({ transition: 'grid-template-columns 180ms ease, gap 180ms ease', }; - const getWidgetStyles = (widget, index) => { + const getWidgetStyles = (widget: Widget, index: number): React.CSSProperties => { const height = Math.max(Number(widget.height) || minWidgetHeight, minWidgetHeight); const rowSpan = Math.max(1, Math.ceil((height + gap) / (rowHeight + gap))); const isResizing = resizingWidget?.index === index; - const baseStyles = { + const baseStyles: React.CSSProperties = { position: 'relative', minHeight: `${minWidgetHeight}px`, background: 'var(--bg-card)', @@ -316,7 +352,7 @@ export default function DashboardGrid({ gap: '4px', zIndex: 10, opacity: 0, - transition: 'opacity var(--transition)', + transition: 'opacity var(--transition)' }} className="widget-controls" > @@ -368,11 +404,6 @@ export default function DashboardGrid({ {widget.component} - ))} diff --git a/src/components/layout/MobileHeader.jsx b/src/components/layout/MobileHeader.tsx similarity index 73% rename from src/components/layout/MobileHeader.jsx rename to src/components/layout/MobileHeader.tsx index d136c3a2..d33e358a 100644 --- a/src/components/layout/MobileHeader.jsx +++ b/src/components/layout/MobileHeader.tsx @@ -6,7 +6,7 @@ export default function MobileHeader() { const { isMobileMenuOpen, 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'; }} > - - - + + + @@ -75,7 +75,7 @@ export default function MobileHeader() {
- +
{/* Theme toggle */}
- ) + ); } \ No newline at end of file diff --git a/src/components/layout/MobileNavigation.jsx b/src/components/layout/MobileNavigation.tsx similarity index 80% rename from src/components/layout/MobileNavigation.jsx rename to src/components/layout/MobileNavigation.tsx index 023e2f42..2d74bf96 100644 --- a/src/components/layout/MobileNavigation.jsx +++ b/src/components/layout/MobileNavigation.tsx @@ -1,15 +1,21 @@ -import React from 'react' +import React from 'react'; +import { useStore } from '../../lib/store'; import { useNavigate } from 'react-router-dom' -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: '⊡' }, - { id: 'settings', label: 'More', 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: 'More', icon: '⚙' }, +]; export default function MobileNavigation() { const { activeTab, setActiveTab } = useStore() @@ -27,7 +33,7 @@ export default function MobileNavigation() { aria-label="Quick navigation" > {QUICK_NAV.map((item) => { - const isActive = activeTab === item.id + const isActive = activeTab === item.id; return ( - ) + ); })} - ) -} + ); +} \ No newline at end of file diff --git a/src/components/layout/MobileSidebar.jsx b/src/components/layout/MobileSidebar.tsx similarity index 96% rename from src/components/layout/MobileSidebar.jsx rename to src/components/layout/MobileSidebar.tsx index cfec292f..83313c6b 100644 --- a/src/components/layout/MobileSidebar.jsx +++ b/src/components/layout/MobileSidebar.tsx @@ -11,7 +11,13 @@ import { useNavigate } from "react-router-dom"; import { useStore } from "../../lib/store"; import { useSwipeGesture } from "../../hooks/useSwipeGesture"; -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: "◫" }, @@ -79,7 +85,7 @@ export default function MobileSidebar() { const navigate = useNavigate(); const { activeTab, isMobileMenuOpen, setMobileMenuOpen, theme, toggleTheme, network } = useStore(); - const drawerRef = useRef(null); + const drawerRef = useRef(null); const close = useCallback(() => setMobileMenuOpen(false), [setMobileMenuOpen]); @@ -104,7 +110,7 @@ export default function MobileSidebar() { // 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]); @@ -112,7 +118,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]); @@ -276,4 +282,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 83% rename from src/components/layout/Sidebar.jsx rename to src/components/layout/Sidebar.tsx index 0c00f203..1a892bb3 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: '◉' }, @@ -50,10 +57,22 @@ const NAV_ITEMS = [ { id: 'dataExport', label: 'Export', 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() +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, @@ -63,42 +82,51 @@ export default function Sidebar({ isMobile = false }) { 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(''); useEffect(() => { if (network === 'custom') { - loadCustomNetworkProfiles().then(profiles => { + loadCustomNetworkProfiles().then((profiles: CustomProfile[]) => { setCustomProfiles(profiles) - getActiveProfile().then(profile => { + getActiveProfile().then((profile: CustomProfile | null) => { if (profile) { setActiveProfileId(profile.id) updateCustomNetworkConfig({ horizonUrl: profile.horizonUrl, sorobanUrl: profile.sorobanUrl, passphrase: profile.passphrase, - }) + }); } - }) - }) + }); + }); } - }, [network]) + }, [network]); - const handleNavClick = (tabId) => { + const handleNavClick = (tabId: string) => { navigate(`/${tabId}`) setMobileMenuOpen(false) } + const handleSwitchProfile = (id: string) => { + setActiveProfileId(id); + if (id && typeof switchToCustomProfile === 'function') { + switchToCustomProfile(id); + } + }; + 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)', maxWidth: isMobile ? 'calc(100vw - 24px)' : 'none', minHeight: '100vh', @@ -114,9 +142,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', @@ -126,7 +154,18 @@ export default function Sidebar({ isMobile = false }) { borderRadius: 'var(--radius-sm)', color: 'var(--text-primary)', outline: 'none', - } + }; + + const updateCustomHeader = (name: string, value: string) => { + setCustomHeaderName(name); + setCustomHeaderValue(value); + + const updatedHeaders: Record = name.trim() && value.trim() ? { [name.trim()]: value.trim() } : {}; + + updateCustomNetworkConfig({ + headers: updatedHeaders, + }); + }; return ( <> @@ -158,6 +197,14 @@ export default function Sidebar({ isMobile = false }) { cursor: 'pointer', transition: 'var(--transition)', }} + onMouseEnter={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-primary)'; + e.currentTarget.style.background = 'var(--bg-elevated)'; + }} + onMouseLeave={(e: React.MouseEvent) => { + e.currentTarget.style.color = 'var(--text-secondary)'; + e.currentTarget.style.background = 'var(--bg-hover)'; + }} > ✕ @@ -197,7 +244,7 @@ export default function Sidebar({ isMobile = false }) {