Mbonea commited on
Commit
4bd1a55
·
1 Parent(s): fb3fa71

Route economy drilldown pages

Browse files
web-app-v2/src/App.tsx CHANGED
@@ -46,6 +46,12 @@ function AppRoutes() {
46
  <Market initialTab="economy" />
47
  </ProtectedRoute>
48
  } />
 
 
 
 
 
 
49
 
50
  <Route path="/portfolio" element={
51
  <ProtectedRoute>
 
46
  <Market initialTab="economy" />
47
  </ProtectedRoute>
48
  } />
49
+
50
+ <Route path="/economy/:economyPage" element={
51
+ <ProtectedRoute>
52
+ <Market initialTab="economy" />
53
+ </ProtectedRoute>
54
+ } />
55
 
56
  <Route path="/portfolio" element={
57
  <ProtectedRoute>
web-app-v2/src/pages/Market.tsx CHANGED
@@ -10,7 +10,7 @@ import {
10
  useGetSecondaryBondDailyDetailQuery,
11
  useGetSecondaryBondsQuery,
12
  } from '../store/api/economyApi';
13
- import { useNavigate, useSearchParams } from 'react-router-dom';
14
  import {
15
  Search,
16
  TrendingUp,
@@ -39,6 +39,7 @@ type AssetTab = 'stocks' | 'funds' | 'bonds' | 'economy';
39
 
40
  export default function Market({ initialTab = 'stocks' }: { initialTab?: AssetTab }) {
41
  const [searchParams, setSearchParams] = useSearchParams();
 
42
  const initialSearch = searchParams.get('search') || '';
43
  const [activeTab, setActiveTab] = useState<AssetTab>(initialTab);
44
  const [searchQuery, setSearchQuery] = useState(initialSearch);
@@ -206,6 +207,7 @@ export default function Market({ initialTab = 'stocks' }: { initialTab?: AssetTa
206
 
207
  {activeTab === 'economy' ? (
208
  <EconomyPanel
 
209
  intelligence={intelligence}
210
  alerts={filteredAlerts}
211
  secondaryBonds={filteredSecondaryBonds}
@@ -308,6 +310,7 @@ type AssetRowProps = {
308
  };
309
 
310
  type EconomyPanelProps = {
 
311
  intelligence: any | null;
312
  alerts: any[];
313
  secondaryBonds: any[];
@@ -353,6 +356,7 @@ const buildInflationChartRows = (series: any[], selectedBaskets: string[]) => {
353
  const formatDuration = (value: any) => String(value || 'UNKNOWN');
354
 
355
  const EconomyPanel: React.FC<EconomyPanelProps> = ({
 
356
  intelligence,
357
  alerts,
358
  secondaryBonds,
@@ -362,7 +366,7 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
362
  isLoading,
363
  onRefresh,
364
  }) => {
365
- const [subPage, setSubPage] = React.useState<EconomySubPage>('overview');
366
  const [selectedBondDay, setSelectedBondDay] = React.useState<string | null>(null);
367
  const topOpportunities = alerts.filter((alert) => alert.severity === 'opportunity').slice(0, 4);
368
  const cautions = alerts.filter((alert) => alert.severity !== 'opportunity').slice(0, 4);
@@ -388,6 +392,17 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
388
  { key: 'bonds', label: 'BONDS', icon: <Landmark className="h-3.5 w-3.5" /> },
389
  { key: 'nbs', label: 'NBS_DATA', icon: <FileText className="h-3.5 w-3.5" /> },
390
  ];
 
 
 
 
 
 
 
 
 
 
 
391
 
392
  return (
393
  <div className="space-y-4">
@@ -414,7 +429,7 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
414
  <button
415
  key={item.key}
416
  type="button"
417
- onClick={() => setSubPage(item.key)}
418
  className={`flex items-center justify-center gap-2 px-4 py-3 text-[10px] mono font-bold transition-all ${subPage === item.key ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
419
  >
420
  {item.icon}
@@ -458,136 +473,149 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
458
  </div>
459
  </section>
460
 
461
- {subPage === 'overview' && (
462
- <>
463
- <div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
464
- <SecondaryBondSnapshotTable secondaryBonds={secondaryBonds} isLoading={isLoading} />
465
- <section className="space-y-4">
466
- <AlertList title="GOOD_DEALS" alerts={topOpportunities} emptyLabel="NO_GOOD_DEAL_ALERTS" />
467
- <AlertList title="STRANGE_OR_EXPENSIVE" alerts={cautions} emptyLabel="NO_CAUTION_ALERTS" />
468
- </section>
469
- </div>
470
- <SourceDocuments documents={documents} />
471
- </>
472
- )}
473
-
474
- {subPage === 'inflation' && (
475
- <div className="space-y-4">
476
- <section className="border border-line bg-bg-card">
477
- <PanelHeader icon={<TrendingUp className="h-4 w-4" />} title="INFLATION_BY_BASKET" />
478
- <div className="grid gap-px bg-line md:grid-cols-3">
479
- <MetricCell label="LATEST_MONTH" value={inflationDetail?.latest_date || '--'} />
480
- <MetricCell label="BASKETS_TRACKED" value={(inflationDetail?.baskets?.length || 0).toString()} />
481
- <MetricCell label="SERIES_POINTS" value={(inflationSeries.length || 0).toString()} />
482
- </div>
483
- <div className="h-[360px] px-5 py-4">
484
- <ResponsiveContainer width="100%" height="100%">
485
- <LineChart data={inflationChartRows}>
486
- <CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
487
- <XAxis dataKey="date" stroke="#8b95a1" tick={{ fontSize: 10 }} />
488
- <YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
489
- <Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
490
- {selectedBaskets.map((basket, index) => (
491
- <Line
492
- key={basket}
493
- type="monotone"
494
- dataKey={basket}
495
- dot={false}
496
- strokeWidth={2}
497
- stroke={chartColors[index % chartColors.length]}
498
- />
499
- ))}
500
- </LineChart>
501
- </ResponsiveContainer>
502
- </div>
503
- <div className="grid gap-px bg-line md:grid-cols-2">
504
- <InflationRank title="FASTEST_RISERS" rows={inflationDetail?.top_risers || []} />
505
- <InflationRank title="LOWEST_INFLATION" rows={inflationDetail?.top_disinflation || []} />
506
- </div>
507
- </section>
508
- </div>
509
- )}
510
 
511
- {subPage === 'bonds' && (
512
- <div className="space-y-4">
513
- <section className="border border-line bg-bg-card">
514
- <PanelHeader icon={<Landmark className="h-4 w-4" />} title="ECONOMIC_BONDS_DAILY" />
515
- <div className="flex gap-px overflow-x-auto bg-line border-b border-line">
516
- {bondDays.map((day: any) => (
517
- <button
518
- key={day.trade_date}
519
- type="button"
520
- onClick={() => setSelectedBondDay(day.trade_date)}
521
- className={`min-w-[150px] px-4 py-3 text-left ${activeBondDay === day.trade_date ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
522
- >
523
- <p className="text-[10px] mono font-bold">{day.trade_date}</p>
524
- <p className="mt-1 text-[9px] mono">{day.trade_count} trades</p>
525
- </button>
526
- ))}
527
- </div>
528
- <div className="grid gap-px bg-line md:grid-cols-4">
529
- <MetricCell label="ACTIVE_DAY" value={activeBondDay || '--'} />
530
- <MetricCell label="DAY_TRADES" value={activeTrades.length.toString()} />
531
- <MetricCell label="AVG_PRICE" value={formatPercent(bondDays.find((d: any) => d.trade_date === activeBondDay)?.average_price_percent)} />
532
- <MetricCell label="AVG_YIELD" value={formatPercent(bondDays.find((d: any) => d.trade_date === activeBondDay)?.average_yield_percent)} />
533
- </div>
534
- </section>
535
-
536
- <div className="grid gap-4 xl:grid-cols-2">
537
- <section className="border border-line bg-bg-card">
538
- <PanelHeader icon={<Activity className="h-4 w-4" />} title="YIELD_CURVE_BY_DURATION" />
539
- <div className="h-[320px] px-5 py-4">
540
- <ResponsiveContainer width="100%" height="100%">
541
- <LineChart data={yieldCurveRows}>
542
- <CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
543
- <XAxis dataKey="duration_bucket" stroke="#8b95a1" tick={{ fontSize: 10 }} />
544
- <YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
545
- <Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
546
- <Line type="monotone" dataKey="average_yield_percent" stroke="#00f09a" strokeWidth={2} dot />
547
- </LineChart>
548
- </ResponsiveContainer>
549
- </div>
550
- </section>
551
- <DurationTable rows={activeDayDurationRows} />
552
- </div>
553
 
554
- <div className="grid gap-4 xl:grid-cols-[1fr_0.9fr]">
555
- <BondTradesTable rows={activeTrades} />
556
- <AnomalyList rows={activeAnomalies} />
557
- </div>
558
- </div>
559
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
 
561
- {subPage === 'nbs' && (
562
- <div className="space-y-4">
563
- <SourceDocuments documents={documents.filter((doc) => String(doc.source || '').toLowerCase() === 'nbs')} />
564
- <section className="border border-line bg-bg-card">
565
- <PanelHeader icon={<FileText className="h-4 w-4" />} title="NBS_BASKET_LATEST_VALUES" />
566
- <div className="overflow-x-auto">
567
- <table className="w-full min-w-[760px] text-left">
568
- <thead className="border-b border-line bg-black/30">
569
- <tr>
570
- <HeaderCell>BASKET</HeaderCell>
571
- <HeaderCell align="right">INFLATION</HeaderCell>
572
- <HeaderCell align="right">INDEX</HeaderCell>
573
- <HeaderCell align="right">WEIGHT</HeaderCell>
574
- </tr>
575
- </thead>
576
- <tbody className="divide-y divide-line">
577
- {(inflationDetail?.latest || []).slice(0, 24).map((row: any) => (
578
- <tr key={`${row.basket}-${row.date}`}>
579
- <BodyCell strong>{row.basket}</BodyCell>
580
- <BodyCell align="right">{formatPercent(row.inflation_rate)}</BodyCell>
581
- <BodyCell align="right">{row.index_value != null ? Number(row.index_value).toFixed(2) : '--'}</BodyCell>
582
- <BodyCell align="right">{row.weight != null ? Number(row.weight).toFixed(2) : '--'}</BodyCell>
583
- </tr>
584
- ))}
585
- </tbody>
586
- </table>
587
- </div>
588
- </section>
589
- </div>
590
- )}
 
 
 
591
  </div>
592
  );
593
  };
 
10
  useGetSecondaryBondDailyDetailQuery,
11
  useGetSecondaryBondsQuery,
12
  } from '../store/api/economyApi';
13
+ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
14
  import {
15
  Search,
16
  TrendingUp,
 
39
 
40
  export default function Market({ initialTab = 'stocks' }: { initialTab?: AssetTab }) {
41
  const [searchParams, setSearchParams] = useSearchParams();
42
+ const { economyPage } = useParams();
43
  const initialSearch = searchParams.get('search') || '';
44
  const [activeTab, setActiveTab] = useState<AssetTab>(initialTab);
45
  const [searchQuery, setSearchQuery] = useState(initialSearch);
 
207
 
208
  {activeTab === 'economy' ? (
209
  <EconomyPanel
210
+ routePage={economyPage}
211
  intelligence={intelligence}
212
  alerts={filteredAlerts}
213
  secondaryBonds={filteredSecondaryBonds}
 
310
  };
311
 
312
  type EconomyPanelProps = {
313
+ routePage?: string;
314
  intelligence: any | null;
315
  alerts: any[];
316
  secondaryBonds: any[];
 
356
  const formatDuration = (value: any) => String(value || 'UNKNOWN');
357
 
358
  const EconomyPanel: React.FC<EconomyPanelProps> = ({
359
+ routePage,
360
  intelligence,
361
  alerts,
362
  secondaryBonds,
 
366
  isLoading,
367
  onRefresh,
368
  }) => {
369
+ const navigate = useNavigate();
370
  const [selectedBondDay, setSelectedBondDay] = React.useState<string | null>(null);
371
  const topOpportunities = alerts.filter((alert) => alert.severity === 'opportunity').slice(0, 4);
372
  const cautions = alerts.filter((alert) => alert.severity !== 'opportunity').slice(0, 4);
 
392
  { key: 'bonds', label: 'BONDS', icon: <Landmark className="h-3.5 w-3.5" /> },
393
  { key: 'nbs', label: 'NBS_DATA', icon: <FileText className="h-3.5 w-3.5" /> },
394
  ];
395
+ const validRoutePages = new Set<EconomySubPage>(['overview', 'inflation', 'bonds', 'nbs']);
396
+ const subPage = validRoutePages.has(routePage as EconomySubPage) ? routePage as EconomySubPage : 'overview';
397
+ const openSubPage = (page: EconomySubPage) => {
398
+ void navigate(page === 'overview' ? '/economy' : `/economy/${page}`);
399
+ };
400
+ const pageIndex = subPages.findIndex((item) => item.key === subPage);
401
+ const pageVariants = {
402
+ enter: { opacity: 0, x: 36 },
403
+ center: { opacity: 1, x: 0 },
404
+ exit: { opacity: 0, x: -28 },
405
+ };
406
 
407
  return (
408
  <div className="space-y-4">
 
429
  <button
430
  key={item.key}
431
  type="button"
432
+ onClick={() => openSubPage(item.key)}
433
  className={`flex items-center justify-center gap-2 px-4 py-3 text-[10px] mono font-bold transition-all ${subPage === item.key ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
434
  >
435
  {item.icon}
 
473
  </div>
474
  </section>
475
 
476
+ <div className="relative overflow-hidden">
477
+ <AnimatePresence mode="wait" custom={pageIndex}>
478
+ <motion.div
479
+ key={subPage}
480
+ custom={pageIndex}
481
+ variants={pageVariants}
482
+ initial="enter"
483
+ animate="center"
484
+ exit="exit"
485
+ transition={{ duration: 0.22, ease: 'easeOut' }}
486
+ className="space-y-4"
487
+ >
488
+ {subPage === 'overview' && (
489
+ <>
490
+ <div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
491
+ <SecondaryBondSnapshotTable secondaryBonds={secondaryBonds} isLoading={isLoading} />
492
+ <section className="space-y-4">
493
+ <AlertList title="GOOD_DEALS" alerts={topOpportunities} emptyLabel="NO_GOOD_DEAL_ALERTS" />
494
+ <AlertList title="STRANGE_OR_EXPENSIVE" alerts={cautions} emptyLabel="NO_CAUTION_ALERTS" />
495
+ </section>
496
+ </div>
497
+ <SourceDocuments documents={documents} />
498
+ </>
499
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
+ {subPage === 'inflation' && (
502
+ <section className="border border-line bg-bg-card">
503
+ <PanelHeader icon={<TrendingUp className="h-4 w-4" />} title="INFLATION_BY_BASKET" />
504
+ <div className="grid gap-px bg-line md:grid-cols-3">
505
+ <MetricCell label="LATEST_MONTH" value={inflationDetail?.latest_date || '--'} />
506
+ <MetricCell label="BASKETS_TRACKED" value={(inflationDetail?.baskets?.length || 0).toString()} />
507
+ <MetricCell label="SERIES_POINTS" value={(inflationSeries.length || 0).toString()} />
508
+ </div>
509
+ <div className="h-[360px] px-5 py-4">
510
+ <ResponsiveContainer width="100%" height="100%">
511
+ <LineChart data={inflationChartRows}>
512
+ <CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
513
+ <XAxis dataKey="date" stroke="#8b95a1" tick={{ fontSize: 10 }} />
514
+ <YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
515
+ <Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
516
+ {selectedBaskets.map((basket, index) => (
517
+ <Line
518
+ key={basket}
519
+ type="monotone"
520
+ dataKey={basket}
521
+ dot={false}
522
+ strokeWidth={2}
523
+ stroke={chartColors[index % chartColors.length]}
524
+ />
525
+ ))}
526
+ </LineChart>
527
+ </ResponsiveContainer>
528
+ </div>
529
+ <div className="grid gap-px bg-line md:grid-cols-2">
530
+ <InflationRank title="FASTEST_RISERS" rows={inflationDetail?.top_risers || []} />
531
+ <InflationRank title="LOWEST_INFLATION" rows={inflationDetail?.top_disinflation || []} />
532
+ </div>
533
+ </section>
534
+ )}
 
 
 
 
 
 
 
 
535
 
536
+ {subPage === 'bonds' && (
537
+ <>
538
+ <section className="border border-line bg-bg-card">
539
+ <PanelHeader icon={<Landmark className="h-4 w-4" />} title="ECONOMIC_BONDS_DAILY" />
540
+ <div className="flex gap-px overflow-x-auto bg-line border-b border-line">
541
+ {bondDays.map((day: any) => (
542
+ <button
543
+ key={day.trade_date}
544
+ type="button"
545
+ onClick={() => setSelectedBondDay(day.trade_date)}
546
+ className={`min-w-[150px] px-4 py-3 text-left ${activeBondDay === day.trade_date ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
547
+ >
548
+ <p className="text-[10px] mono font-bold">{day.trade_date}</p>
549
+ <p className="mt-1 text-[9px] mono">{day.trade_count} trades</p>
550
+ </button>
551
+ ))}
552
+ </div>
553
+ <div className="grid gap-px bg-line md:grid-cols-4">
554
+ <MetricCell label="ACTIVE_DAY" value={activeBondDay || '--'} />
555
+ <MetricCell label="DAY_TRADES" value={activeTrades.length.toString()} />
556
+ <MetricCell label="AVG_PRICE" value={formatPercent(bondDays.find((d: any) => d.trade_date === activeBondDay)?.average_price_percent)} />
557
+ <MetricCell label="AVG_YIELD" value={formatPercent(bondDays.find((d: any) => d.trade_date === activeBondDay)?.average_yield_percent)} />
558
+ </div>
559
+ </section>
560
+
561
+ <div className="grid gap-4 xl:grid-cols-2">
562
+ <section className="border border-line bg-bg-card">
563
+ <PanelHeader icon={<Activity className="h-4 w-4" />} title="YIELD_CURVE_BY_DURATION" />
564
+ <div className="h-[320px] px-5 py-4">
565
+ <ResponsiveContainer width="100%" height="100%">
566
+ <LineChart data={yieldCurveRows}>
567
+ <CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
568
+ <XAxis dataKey="duration_bucket" stroke="#8b95a1" tick={{ fontSize: 10 }} />
569
+ <YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
570
+ <Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
571
+ <Line type="monotone" dataKey="average_yield_percent" stroke="#00f09a" strokeWidth={2} dot />
572
+ </LineChart>
573
+ </ResponsiveContainer>
574
+ </div>
575
+ </section>
576
+ <DurationTable rows={activeDayDurationRows} />
577
+ </div>
578
+
579
+ <div className="grid gap-4 xl:grid-cols-[1fr_0.9fr]">
580
+ <BondTradesTable rows={activeTrades} />
581
+ <AnomalyList rows={activeAnomalies} />
582
+ </div>
583
+ </>
584
+ )}
585
 
586
+ {subPage === 'nbs' && (
587
+ <>
588
+ <SourceDocuments documents={documents.filter((doc) => String(doc.source || '').toLowerCase() === 'nbs')} />
589
+ <section className="border border-line bg-bg-card">
590
+ <PanelHeader icon={<FileText className="h-4 w-4" />} title="NBS_BASKET_LATEST_VALUES" />
591
+ <div className="overflow-x-auto">
592
+ <table className="w-full min-w-[760px] text-left">
593
+ <thead className="border-b border-line bg-black/30">
594
+ <tr>
595
+ <HeaderCell>BASKET</HeaderCell>
596
+ <HeaderCell align="right">INFLATION</HeaderCell>
597
+ <HeaderCell align="right">INDEX</HeaderCell>
598
+ <HeaderCell align="right">WEIGHT</HeaderCell>
599
+ </tr>
600
+ </thead>
601
+ <tbody className="divide-y divide-line">
602
+ {(inflationDetail?.latest || []).slice(0, 24).map((row: any) => (
603
+ <tr key={`${row.basket}-${row.date}`}>
604
+ <BodyCell strong>{row.basket}</BodyCell>
605
+ <BodyCell align="right">{formatPercent(row.inflation_rate)}</BodyCell>
606
+ <BodyCell align="right">{row.index_value != null ? Number(row.index_value).toFixed(2) : '--'}</BodyCell>
607
+ <BodyCell align="right">{row.weight != null ? Number(row.weight).toFixed(2) : '--'}</BodyCell>
608
+ </tr>
609
+ ))}
610
+ </tbody>
611
+ </table>
612
+ </div>
613
+ </section>
614
+ </>
615
+ )}
616
+ </motion.div>
617
+ </AnimatePresence>
618
+ </div>
619
  </div>
620
  );
621
  };