Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/components/QueryError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { Box, Button, Typography, useTheme } from '@mui/material';
import { FONTS } from '../theme';

interface QueryErrorProps {
onRetry: () => void;
title?: string;
}

export const QueryError: React.FC<QueryErrorProps> = ({
onRetry,
title = 'Failed to load data',
}) => {
const theme = useTheme();
return (
<Box
sx={{
p: 3,
border: '1px solid',
borderColor: 'error.main',
backgroundColor: 'background.paper',
textAlign: 'center',
}}
>
<Typography
sx={{
fontFamily: FONTS.mono,
fontSize: '0.9rem',
fontWeight: 600,
color: 'error.main',
mb: 2,
}}
>
{title}
</Typography>
<Button
onClick={onRetry}
size="small"
variant="outlined"
sx={{
fontFamily: FONTS.mono,
fontSize: '0.7rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
borderRadius: 0,
borderColor: 'primary.main',
color: 'primary.main',
'&:hover': {
backgroundColor: `${theme.palette.primary.main}11`,
},
}}
>
Retry
</Button>
</Box>
);
};
15 changes: 10 additions & 5 deletions src/components/dashboard/EventFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Link as RouterLink } from 'react-router-dom';
import { Box, Button, Chip, Stack, Typography, useTheme } from '@mui/material';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { useLatestEvents } from '../../api';
import type { ContractEvent } from '../../api/models/Events';
import { FONTS } from '../../theme';
import CopyableAddress from '../CopyableAddress';
import { QueryError } from '../QueryError';
import { EventFeedSkeleton } from './Skeletons';

const getEventColor = (
Expand Down Expand Up @@ -39,7 +41,7 @@ const getEventColor = (

const EventFeed: React.FC = () => {
const theme = useTheme();
const { data: events, isLoading } = useLatestEvents();
const { data: events, isLoading, isError, refetch } = useLatestEvents();
const scrollRef = useRef<HTMLDivElement>(null);
const [scrolled, setScrolled] = useState(false);

Expand All @@ -52,9 +54,11 @@ const EventFeed: React.FC = () => {
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
}, []);

return isLoading || !events ? (
<EventFeedSkeleton />
) : (
if (isLoading) return <EventFeedSkeleton />;
if (isError) return <QueryError onRetry={() => refetch()} title="Failed to load events" />;
if (!events) return <EventFeedSkeleton />;

return (
<Box sx={{ position: 'relative' }}>
<Typography
variant="h6"
Expand All @@ -76,7 +80,7 @@ const EventFeed: React.FC = () => {
}}
>
<Stack spacing={1}>
{events?.map((event) => (
{events?.map((event: ContractEvent) => (
<Box
key={event.id}
sx={{
Expand Down Expand Up @@ -199,6 +203,7 @@ const EventFeed: React.FC = () => {
<Button
onClick={scrollToTop}
size="small"
aria-label="Scroll to top of event feed"
sx={{
position: 'absolute',
bottom: 8,
Expand Down
11 changes: 7 additions & 4 deletions src/components/dashboard/MinerRatesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SearchIcon from '@mui/icons-material/Search';
import { useMiners, type Miner } from '../../api';
import { FONTS } from '../../theme';
import CopyableAddress from '../CopyableAddress';
import { QueryError } from '../QueryError';
import { MinerRatesTableSkeleton } from './Skeletons';

type SortKey = 'uid' | 'pair' | 'rate' | 'collateral' | 'status' | 'hotkey';
Expand Down Expand Up @@ -110,7 +111,7 @@ const MinerRatesTable: React.FC = () => {
borderBottom: `1px solid ${theme.palette.divider}`,
};

const { data: miners, isLoading } = useMiners();
const { data: miners, isLoading, isError, refetch } = useMiners();
const [sortKey, setSortKey] = useState<SortKey>('rate');
const [sortDir, setSortDir] = useState<SortDir>('desc');
const [search, setSearch] = useState('');
Expand Down Expand Up @@ -265,9 +266,11 @@ const MinerRatesTable: React.FC = () => {
return hasForward !== hasReverse;
};

return isLoading || !miners ? (
<MinerRatesTableSkeleton />
) : (
if (isLoading) return <MinerRatesTableSkeleton />;
if (isError) return <QueryError onRetry={() => refetch()} title="Failed to load miner rates" />;
if (!miners) return <MinerRatesTableSkeleton />;

return (
<Box>
<Box
sx={{
Expand Down
99 changes: 51 additions & 48 deletions src/components/dashboard/OrderbookDepth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,71 +17,72 @@ import {
} from '@mui/material';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { useMiners } from '../../api';
import type { Miner } from '../../api/models/Miners';
import { FONTS } from '../../theme';
import { OrderbookDepthSkeleton } from './Skeletons';
import { QueryError } from '../QueryError';

const OrderbookDepth: React.FC = () => {
const BtcIcon = ({ size = 16 }: { size?: number }) => {
const theme = useTheme();

const TAO_COLOR = theme.palette.asset.tao;
const BTC_COLOR = theme.palette.asset.btc;

const BtcIcon = ({ size = 16 }: { size?: number }) => (
return (
<svg viewBox="0 0 32 32" width={size} height={size}>
<circle cx="16" cy="16" r="16" fill={BTC_COLOR} />
<circle cx="16" cy="16" r="16" fill={theme.palette.asset.btc} />
<path
fill={theme.palette.common.white}
fillRule="evenodd"
d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"
/>
</svg>
);
};

const TaoIcon = ({ size = 16, color }: { size?: number; color?: string }) => (
const TaoIcon = ({ size = 16, color }: { size?: number; color?: string }) => {
const theme = useTheme();
const fill = color || theme.palette.asset.tao;
return (
<svg viewBox="0 0 21.6 23.1" width={size} height={size}>
<path
fill={color || TAO_COLOR}
fill={fill}
d="M13.1,17.7V8.3c0-2.4-1.9-4.3-4.3-4.3v15.1c0,2.2,1.7,4,3.9,4c0.1,0,0.1,0,0.2,0c1,0.1,2.1-0.2,2.9-0.9C13.3,22,13.1,20.5,13.1,17.7L13.1,17.7z"
/>
<path
fill={color || TAO_COLOR}
fill={fill}
d="M3.9,0C1.8,0,0,1.8,0,4h17.6c2.2,0,3.9-1.8,3.9-4C21.6,0,3.9,0,3.9,0z"
/>
</svg>
);
};

const AssetIcon = ({
asset,
size = 16,
}: {
asset: string;
size?: number;
}) => {
if (asset.toUpperCase() === 'BTC') return <BtcIcon size={size} />;
return (
<Box
const AssetIcon = ({ asset, size = 16 }: { asset: string; size?: number }) => {
const theme = useTheme();
if (asset.toUpperCase() === 'BTC') return <BtcIcon size={size} />;
return (
<Box
sx={{
width: size,
height: size,
borderRadius: '50%',
backgroundColor: theme.palette.text.secondary,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography
sx={{
width: size,
height: size,
borderRadius: '50%',
backgroundColor: theme.palette.text.secondary,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: size * 0.6,
color: theme.palette.background.paper,
fontWeight: 'bold',
}}
>
<Typography
sx={{
fontSize: size * 0.6,
color: theme.palette.background.paper,
fontWeight: 'bold',
}}
>
{asset[0]?.toUpperCase()}
</Typography>
</Box>
);
};
{asset[0]?.toUpperCase()}
</Typography>
</Box>
);
};

const OrderbookDepth: React.FC = () => {
const theme = useTheme();

const headerSx = {
fontFamily: FONTS.mono,
Expand All @@ -99,12 +100,12 @@ const OrderbookDepth: React.FC = () => {
borderBottom: `1px solid ${theme.palette.divider}`,
};

const { data: miners, isLoading } = useMiners();
const { data: miners, isLoading, isError, refetch } = useMiners();
const [selectedPair, setSelectedPair] = useState<string>('');

const uniqueAssets = useMemo(() => {
const assets = new Set<string>();
miners?.forEach((m) => {
miners?.forEach((m: Miner) => {
const s = m.sourceChain?.toLowerCase();
const d = m.destChain?.toLowerCase();
if (!s || !d) return;
Expand Down Expand Up @@ -139,7 +140,7 @@ const OrderbookDepth: React.FC = () => {
const forwardGroups: Record<string, number> = {}; // key = rate, val = capacity TAO
const reverseGroups: Record<string, number> = {}; // key = counterRate, val = capacity TAO

miners.forEach((m) => {
miners.forEach((m: Miner) => {
if (!m.collateralRao) return;
const s = m.sourceChain?.toLowerCase();
const d = m.destChain?.toLowerCase();
Expand Down Expand Up @@ -204,9 +205,11 @@ const OrderbookDepth: React.FC = () => {
const getAssetSymbol = () =>
selectedPair ? selectedPair.replace('/TAO', '').trim() : '';

return isLoading || !miners ? (
<OrderbookDepthSkeleton />
) : (
if (isLoading) return <OrderbookDepthSkeleton />;
if (isError) return <QueryError onRetry={() => refetch()} title="Failed to load orderbook data" />;
if (!miners) return <OrderbookDepthSkeleton />;

return (
<Box>
<Box
sx={{
Expand Down Expand Up @@ -243,7 +246,7 @@ const OrderbookDepth: React.FC = () => {
arrow
placement="right"
>
<IconButton size="small" sx={{ p: 0, color: 'text.secondary' }}>
<IconButton size="small" sx={{ p: 0, color: 'text.secondary' }} aria-label="Orderbook depth information">
<InfoOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
Expand Down Expand Up @@ -370,9 +373,9 @@ const OrderbookDepth: React.FC = () => {
};

const assetThemeColor = isBtc
? BTC_COLOR
? theme.palette.asset.btc
: theme.palette.primary.main;
const taoThemeColor = TAO_COLOR;
const taoThemeColor = theme.palette.asset.tao;

const leftGradColor = hexToRgba(assetThemeColor, 0.1);
const rightGradColor =
Expand Down
11 changes: 7 additions & 4 deletions src/components/dashboard/StatsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Box, Grid, Typography, keyframes } from '@mui/material';
import { useStats } from '../../api';
import { FONTS } from '../../theme';
import { QueryError } from '../QueryError';
import { StatsPanelSkeleton } from './Skeletons';

const slideOut = keyframes`
Expand Down Expand Up @@ -131,13 +132,15 @@ const StatCard: React.FC<{ label: string; value: string }> = ({
);

const StatsPanel: React.FC = () => {
const { data: stats, isLoading } = useStats();
const { data: stats, isLoading, isError, refetch } = useStats();

const volume = stats ? parseFloat(stats.totalVolumeTao).toFixed(2) : '0';

return isLoading || !stats ? (
<StatsPanelSkeleton />
) : (
if (isLoading) return <StatsPanelSkeleton />;
if (isError) return <QueryError onRetry={() => refetch()} title="Failed to load statistics" />;
if (!stats) return <StatsPanelSkeleton />;

return (
<Grid container spacing={1.5}>
<Grid item xs={12} sm={6} md={3}>
<StatCard label="Successful Swaps" value={String(stats.totalSwaps)} />
Expand Down
2 changes: 2 additions & 0 deletions src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const DashboardPage: React.FC = () => {
href={docsUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Documentation"
sx={{
color: 'text.secondary',
border: '1px solid',
Expand All @@ -88,6 +89,7 @@ const DashboardPage: React.FC = () => {
</Tooltip>
<IconButton
onClick={toggleTheme}
aria-label={mode === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
sx={{
color: 'text.secondary',
border: '1px solid',
Expand Down