Skip to content

Commit 56c39fc

Browse files
committed
feat: add keyboard shortcuts with help overlay
1 parent bce979a commit 56c39fc

18 files changed

Lines changed: 205 additions & 998 deletions

frontend/src/App.tsx

Lines changed: 11 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,11 @@
1-
import { lazy, Suspense, useEffect, useState } from "react";
2-
import {
3-
BrowserRouter as Router,
4-
Navigate,
5-
Route,
6-
Routes,
7-
} from "react-router-dom";
1+
import React, { lazy, Suspense, useState } from "react";
2+
import { Navigate, Route, Routes } from "react-router-dom";
83
import * as Sentry from "@sentry/react";
94
import Navbar from "./components/Navbar";
10-
import ErrorFallback from "./components/ErrorFallback";
11-
import { ThemeProvider } from "./context/ThemeContext";
12-
import { VaultProvider } from "./context/VaultContext";
13-
import { fetchUsdcBalance } from "./lib/stellarAccount";
14-
import "./index.css";
155
import { KeyboardShortcutProvider } from "./context/KeyboardShortcutContext";
16-
import Navbar from "./components/Navbar";
176
import ShortcutHelpModal from "./components/ShortcutHelpModal";
187
import { FeatureGate } from "./components/FeatureGate";
198
import { FeatureFlagProvider } from "./context/FeatureFlagContext";
20-
import "./index.css";
21-
22-
import * as Sentry from "@sentry/react";
239
import { useTranslation } from "./i18n";
2410
import { useUsdcBalance } from "./hooks/useBalanceData";
2511

@@ -29,12 +15,8 @@ const Home = lazy(() => import("./pages/Home"));
2915
const Portfolio = lazy(() => import("./pages/Portfolio"));
3016
const Analytics = lazy(() => import("./pages/Analytics"));
3117
const UIPreview = lazy(() => import("./pages/UIPreview"));
32-
33-
const LoadingPage = () => (
34-
<div className="loading-page" role="status" aria-live="polite">
35-
<div style={{ textAlign: "center" }}>
36-
<div className="text-gradient loading-title">Loading...</div>
37-
<div style={{ opacity: 0.7 }}>Securing RWA connection</div>
18+
const TransactionHistory = lazy(() => import("./pages/TransactionHistory"));
19+
const Settings = lazy(() => import("./pages/Settings"));
3820
const LoadingPage = () => {
3921
const { t } = useTranslation();
4022
return (
@@ -79,83 +61,19 @@ function AppContent() {
7961
setWalletAddress(null);
8062
};
8163

82-
useEffect(() => {
83-
const loadBalance = async () => {
84-
if (!walletAddress) {
85-
setUsdcBalance(0);
86-
return;
87-
}
88-
89-
try {
90-
setUsdcBalance(await fetchUsdcBalance(walletAddress));
91-
} catch {
92-
setUsdcBalance(0);
93-
}
94-
};
95-
96-
void loadBalance();
97-
}, [walletAddress]);
9864

9965
return (
100-
<Sentry.ErrorBoundary
101-
fallback={({ error, resetError }) => (
102-
<ErrorFallback error={error as Error} resetError={resetError} />
103-
)}
104-
showDialog
105-
>
106-
<ThemeProvider>
107-
<ToastProvider>
108-
<VaultProvider>
109-
<Router>
110-
<a className="skip-link" href="#main-content">
111-
Skip to main content
112-
</a>
113-
<div className="app-container">
114-
<Navbar
115-
walletAddress={walletAddress}
116-
onConnect={handleConnect}
117-
onDisconnect={handleDisconnect}
118-
/>
119-
<main id="main-content" className="container app-main">
120-
<Suspense fallback={<LoadingPage />}>
121-
<SentryRoutes>
122-
<Route
123-
path="/"
124-
element={
125-
<Home
126-
walletAddress={walletAddress}
127-
usdcBalance={usdcBalance}
128-
/>
129-
}
130-
/>
131-
<Route
132-
path="/portfolio"
133-
element={<Portfolio walletAddress={walletAddress} />}
134-
/>
135-
<Route path="/analytics" element={<Analytics />} />
136-
<Route
137-
path="/transactions"
138-
element={<TransactionHistory walletAddress={walletAddress} />}
139-
/>
140-
<Route path="*" element={<Navigate to="/" replace />} />
141-
</SentryRoutes>
142-
</Suspense>
143-
</main>
144-
</div>
145-
</Router>
146-
</VaultProvider>
147-
</ToastProvider>
14866
<KeyboardShortcutProvider>
67+
<a className="skip-link" href="#main-content">
68+
Skip to main content
69+
</a>
14970
<div className="app-container">
15071
<Navbar
15172
walletAddress={walletAddress}
15273
onConnect={handleConnect}
15374
onDisconnect={handleDisconnect}
15475
/>
155-
<main
156-
className="container"
157-
style={{ marginTop: "100px", paddingBottom: "60px" }}
158-
>
76+
<main id="main-content" className="container app-main" style={{ marginTop: "100px", paddingBottom: "60px" }}>
15977
<Suspense fallback={<LoadingPage />}>
16078
<SentryRoutes>
16179
<Route
@@ -184,8 +102,8 @@ function AppContent() {
184102
</FeatureGate>
185103
}
186104
/>
187-
<Route path="/analytics" element={<Analytics />} />
188-
<Route path="/settings" element={<div>Settings Page</div>} />
105+
<Route path="/transactions" element={<TransactionHistory walletAddress={walletAddress} />} />
106+
<Route path="/settings" element={<Settings />} />
189107
<Route path="/ui-kit" element={<UIPreview />} />
190108
<Route path="*" element={<Navigate to="/" replace />} />
191109
</SentryRoutes>
@@ -201,13 +119,7 @@ function App() {
201119
return (
202120
<Sentry.ErrorBoundary fallback={<AppErrorFallback />} showDialog>
203121
<FeatureFlagProvider>
204-
<ThemeProvider>
205-
<VaultProvider>
206-
<Router>
207-
<AppContent />
208-
</Router>
209-
</VaultProvider>
210-
</ThemeProvider>
122+
<AppContent />
211123
</FeatureFlagProvider>
212124
</Sentry.ErrorBoundary>
213125
);

frontend/src/components/ApiStatusBanner.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ApiError, ValidationError } from "../lib/api";
22
import type { FC } from "react";
3-
import type { ApiError } from "../lib/api";
43
import { useTranslation } from "../i18n";
54

65
interface ApiStatusBannerProps {

frontend/src/components/Navbar.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,18 @@ const Navbar: React.FC<NavbarProps> = ({
111111
</NavLink>
112112
</div>
113113
</div>
114-
</div>
115-
</nav>
116-
);
114+
115+
<div className="flex items-center gap-md">
116+
<ThemeToggle />
117+
<WalletConnect
118+
walletAddress={walletAddress}
119+
onConnect={onConnect}
120+
onDisconnect={onDisconnect}
121+
/>
122+
</div>
123+
</div>
124+
</nav>
125+
);
117126
};
118127

119128
export default Navbar;

frontend/src/components/PageHeader.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import { Link } from "react-router-dom";
33
import { ChevronRight } from "./icons";
4-
import Badge, { BadgeColor } from "./Badge";
4+
import Badge from "./Badge";
5+
import type { BadgeColor } from "./Badge";
56

67
export interface Breadcrumb {
78
label: string;

frontend/src/components/Tabs.tsx

Lines changed: 16 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { createContext, useContext, useState, useEffect } from "react";
22
import type { KeyboardEvent, ReactNode } from "react";
3+
import { useSearchParams } from "react-router-dom";
34
import "./Tabs.css";
45

56
interface TabsContextType {
@@ -27,61 +28,23 @@ interface TabsProps {
2728
className?: string;
2829
}
2930

30-
/** Inner component that uses useSearchParams — only rendered when syncWithUrl=true */
3131
function TabsWithUrl({
3232
defaultValue,
3333
value: controlledValue,
3434
onValueChange,
3535
urlParam = "tab",
3636
children,
3737
className = "",
38-
}: TabsProps) {
39-
const [internalValue, setInternalValue] = useState(defaultValue || "");
40-
41-
const [urlValue, setUrlValue] = useState<string | null>(() => {
42-
if (!syncWithUrl || typeof window === "undefined") {
43-
return null;
44-
}
45-
return new URLSearchParams(window.location.search).get(urlParam);
46-
});
4738
}: Omit<TabsProps, "syncWithUrl">) {
4839
const [searchParams, setSearchParams] = useSearchParams();
4940
const [internalValue, setInternalValue] = useState(defaultValue || "");
5041

5142
const urlValue = searchParams.get(urlParam);
52-
const activeValue =
53-
controlledValue !== undefined
54-
? controlledValue
55-
: urlValue
56-
? urlValue
57-
: internalValue;
58-
59-
useEffect(() => {
60-
if (!syncWithUrl || typeof window === "undefined") {
61-
return;
62-
}
63-
64-
const handlePopState = () => {
65-
setUrlValue(new URLSearchParams(window.location.search).get(urlParam));
66-
};
67-
68-
window.addEventListener("popstate", handlePopState);
69-
return () => {
70-
window.removeEventListener("popstate", handlePopState);
71-
};
72-
}, [syncWithUrl, urlParam]);
43+
const activeValue = controlledValue !== undefined
44+
? controlledValue
45+
: (urlValue || internalValue);
7346

7447
useEffect(() => {
75-
if (!syncWithUrl || typeof window === "undefined") {
76-
return;
77-
}
78-
if (!urlValue && defaultValue) {
79-
const params = new URLSearchParams(window.location.search);
80-
params.set(urlParam, defaultValue);
81-
window.history.replaceState({}, "", `${window.location.pathname}?${params.toString()}`);
82-
setUrlValue(defaultValue);
83-
}
84-
}, [syncWithUrl, urlValue, defaultValue, urlParam]);
8548
if (!urlValue && defaultValue) {
8649
setSearchParams(
8750
(prev) => {
@@ -98,12 +61,7 @@ function TabsWithUrl({
9861
if (controlledValue === undefined) {
9962
setInternalValue(newValue);
10063
}
101-
102-
if (syncWithUrl) {
103-
const params = new URLSearchParams(window.location.search);
104-
params.set(urlParam, newValue);
105-
window.history.replaceState({}, "", `${window.location.pathname}?${params.toString()}`);
106-
setUrlValue(newValue);
64+
10765
setSearchParams(
10866
(prev) => {
10967
const newParams = new URLSearchParams(prev);
@@ -127,7 +85,6 @@ function TabsWithUrl({
12785
);
12886
}
12987

130-
/** Inner component for tabs without URL sync */
13188
function TabsWithoutUrl({
13289
defaultValue,
13390
value: controlledValue,
@@ -137,8 +94,7 @@ function TabsWithoutUrl({
13794
}: Omit<TabsProps, "syncWithUrl" | "urlParam">) {
13895
const [internalValue, setInternalValue] = useState(defaultValue || "");
13996

140-
const activeValue =
141-
controlledValue !== undefined ? controlledValue : internalValue;
97+
const activeValue = controlledValue !== undefined ? controlledValue : internalValue;
14298

14399
const handleValueChange = (newValue: string) => {
144100
if (controlledValue === undefined) {
@@ -158,10 +114,7 @@ function TabsWithoutUrl({
158114
);
159115
}
160116

161-
export function Tabs({
162-
syncWithUrl = false,
163-
...props
164-
}: TabsProps) {
117+
export function Tabs({ syncWithUrl = false, ...props }: TabsProps) {
165118
if (syncWithUrl) {
166119
return <TabsWithUrl {...props} />;
167120
}
@@ -170,12 +123,7 @@ export function Tabs({
170123

171124
export function TabsList({ children, className = "", style }: { children: ReactNode; className?: string; style?: React.CSSProperties }) {
172125
return (
173-
<div
174-
role="tablist"
175-
aria-orientation="horizontal"
176-
className={`tabs-list ${className}`}
177-
style={style}
178-
>
126+
<div role="tablist" aria-orientation="horizontal" className={`tabs-list ${className}`} style={style}>
179127
{children}
180128
</div>
181129
);
@@ -208,13 +156,16 @@ export function TabsTrigger({ value, children, className = "" }: { value: string
208156

209157
e.preventDefault();
210158
tabs[nextIndex].focus();
211-
onValueChange(tabs[nextIndex].dataset.value!);
159+
onValueChange(tabs[nextIndex].getAttribute('data-value')!);
212160
};
213161

214162
return (
215163
<button
216164
type="button"
217-
aria-pressed={isActive}
165+
role="tab"
166+
aria-selected={isActive}
167+
aria-controls={`panel-${value}`}
168+
id={`tab-${value}`}
218169
data-state={isActive ? "active" : "inactive"}
219170
data-value={value}
220171
className={`tabs-trigger ${isActive ? "active" : ""} ${className}`}
@@ -234,6 +185,9 @@ export function TabsContent({ value, children, className = "" }: { value: string
234185

235186
return (
236187
<div
188+
role="tabpanel"
189+
id={`panel-${value}`}
190+
aria-labelledby={`tab-${value}`}
237191
data-state={isActive ? "active" : "inactive"}
238192
className={`tabs-content ${className}`}
239193
tabIndex={0}

0 commit comments

Comments
 (0)