From 7891ea67941ca67b11c67a1ff19e415469bb3458 Mon Sep 17 00:00:00 2001 From: Mohammad-Hassan027 Date: Tue, 2 Jun 2026 23:59:11 +0530 Subject: [PATCH 1/3] feat(ui): implement skeleton loaders for market map and leaderboard - Created reusable brutalist Skeleton component - Replaced 'DOWNLOADING_NODES...' text with skeleton UI in MarketMapPage - Updated loading states to match GlassCard dimensions and prevent layout shift Resolves #60 --- src/components/Skeleton.tsx | 9 +++++++++ src/pages/MarketMapPage.tsx | 26 +++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 src/components/Skeleton.tsx diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx new file mode 100644 index 0000000..fb86edf --- /dev/null +++ b/src/components/Skeleton.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +interface SkeletonProps extends React.HTMLAttributes {} + +export default function Skeleton({ className = "", ...props }: SkeletonProps) { + return ( +
+ ); +} diff --git a/src/pages/MarketMapPage.tsx b/src/pages/MarketMapPage.tsx index 89be0cf..a6c1ded 100644 --- a/src/pages/MarketMapPage.tsx +++ b/src/pages/MarketMapPage.tsx @@ -4,6 +4,7 @@ import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import GlassCard from '../components/GlassCard'; import StatusTerminal from '../components/StatusTerminal'; +import Skeleton from '../components/Skeleton'; import { api } from '../lib/api'; import type { Market } from '../lib/types'; @@ -120,7 +121,7 @@ export default function MarketMapPage() {
- {/* Bottom panel */} +{/* Bottom panel */}
{error && (

@@ -128,7 +129,25 @@ export default function MarketMapPage() {

)} - {selected ? ( + {loading ? ( + /* SKELETON LOADER STATE */ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ ) : selected ? ( + /* SELECTED NODE STATE */
@@ -154,9 +173,10 @@ export default function MarketMapPage() {
) : ( + /* IDLE/EMPTY STATE */
- {loading ? 'DOWNLOADING_NODES...' : 'SELECT_MARKET_NODE'} + SELECT_MARKET_NODE
)} From 77c770c4a987cf90159c087e0e4baafd731fc643 Mon Sep 17 00:00:00 2001 From: Mohammad-Hassan027 Date: Wed, 3 Jun 2026 00:16:50 +0530 Subject: [PATCH 2/3] refactor(Skeleton): replace interface with inline React.HTMLAttributes --- src/components/Skeleton.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx index fb86edf..7c035f8 100644 --- a/src/components/Skeleton.tsx +++ b/src/components/Skeleton.tsx @@ -1,8 +1,6 @@ import React from "react"; -interface SkeletonProps extends React.HTMLAttributes {} - -export default function Skeleton({ className = "", ...props }: SkeletonProps) { +export default function Skeleton({ className = "", ...props }: React.HTMLAttributes) { return (
); From 60a552fcb8d5268d842d9d34aee1429998ba7f38 Mon Sep 17 00:00:00 2001 From: Mohammad-Hassan027 Date: Wed, 3 Jun 2026 00:20:17 +0530 Subject: [PATCH 3/3] refactor(MarketMapPage): format code and apply quick fix to tailwind classname for font --- src/pages/MarketMapPage.tsx | 112 +++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/src/pages/MarketMapPage.tsx b/src/pages/MarketMapPage.tsx index a6c1ded..17e4597 100644 --- a/src/pages/MarketMapPage.tsx +++ b/src/pages/MarketMapPage.tsx @@ -1,32 +1,40 @@ -import { useState, useEffect } from 'react'; -import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'; -import L from 'leaflet'; -import 'leaflet/dist/leaflet.css'; -import GlassCard from '../components/GlassCard'; -import StatusTerminal from '../components/StatusTerminal'; -import Skeleton from '../components/Skeleton'; -import { api } from '../lib/api'; -import type { Market } from '../lib/types'; +import { useState, useEffect } from "react"; +import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; +import L from "leaflet"; +import "leaflet/dist/leaflet.css"; +import GlassCard from "../components/GlassCard"; +import StatusTerminal from "../components/StatusTerminal"; +import Skeleton from "../components/Skeleton"; +import { api } from "../lib/api"; +import type { Market } from "../lib/types"; -const TILE_DARK = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'; -const TILE_LIGHT = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'; +const TILE_DARK = + "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"; +const TILE_LIGHT = + "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"; function getActiveTile() { - return document.documentElement.classList.contains('light') ? TILE_LIGHT : TILE_DARK; + return document.documentElement.classList.contains("light") + ? TILE_LIGHT + : TILE_DARK; } function getScoreColor(score: number) { - return score >= 85 ? 'text-secondary' : score >= 70 ? 'text-neon' : 'text-error'; + return score >= 85 + ? "text-secondary" + : score >= 70 + ? "text-neon" + : "text-error"; } function getScoreBg(score: number) { - return score >= 85 ? 'bg-secondary' : score >= 70 ? 'bg-neon' : 'bg-error'; + return score >= 85 ? "bg-secondary" : score >= 70 ? "bg-neon" : "bg-error"; } const createCustomIcon = (score: number) => { - const hex = score >= 85 ? '#b5d25e' : score >= 70 ? '#c3f400' : '#ffb4ab'; + const hex = score >= 85 ? "#b5d25e" : score >= 70 ? "#c3f400" : "#ffb4ab"; return L.divIcon({ - className: 'custom-leaflet-icon bg-transparent', + className: "custom-leaflet-icon bg-transparent", html: `
without depending on custom events useEffect(() => { const observer = new MutationObserver(() => setTileUrl(getActiveTile())); - observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); return () => observer.disconnect(); }, []); @@ -63,8 +74,10 @@ export default function MarketMapPage() { const res = await api.getMarkets(); setMarkers(res.markets); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load market data.'); - console.error('Market fetch error:', err); + setError( + err instanceof Error ? err.message : "Failed to load market data.", + ); + console.error("Market fetch error:", err); } finally { setLoading(false); } @@ -78,13 +91,17 @@ export default function MarketMapPage() {
-

+

Market Trust Map

@@ -98,7 +115,7 @@ export default function MarketMapPage() { className="w-full h-full z-0" > - {markers.map(m => ( + {markers.map((m) => ( setSelected(m) }} > -
-
{m.name}
+
+
+ {m.name} +
= 85 ? '#b5d25e' : m.score >= 70 ? '#c3f400' : '#ffb4ab' }} + style={{ + color: + m.score >= 85 + ? "#b5d25e" + : m.score >= 70 + ? "#c3f400" + : "#ffb4ab", + }} > SCORE: {m.score} | VENDORS: {m.vendors}
@@ -121,10 +147,10 @@ export default function MarketMapPage() {
-{/* Bottom panel */} + {/* Bottom panel */}
{error && ( -

+

{error}

)} @@ -151,16 +177,20 @@ export default function MarketMapPage() {
-

{selected.name}

- +

+ {selected.name} +

+ {selected.vendors} VENDORS
- + {selected.score} - + AVG_FRESHNESS
@@ -175,7 +205,7 @@ export default function MarketMapPage() { ) : ( /* IDLE/EMPTY STATE */
- + SELECT_MARKET_NODE
@@ -183,13 +213,13 @@ export default function MarketMapPage() {
{[ - { l: 'HIGH (85+)', c: 'bg-secondary' }, - { l: 'MED (70-84)', c: 'bg-neon' }, - { l: 'LOW (<70)', c: 'bg-error' }, - ].map(x => ( + { l: "HIGH (85+)", c: "bg-secondary" }, + { l: "MED (70-84)", c: "bg-neon" }, + { l: "LOW (<70)", c: "bg-error" }, + ].map((x) => (
- + {x.l}