Skip to content

Commit 3be4a97

Browse files
authored
Merge branch 'main' into tool-tip
2 parents d8b7e42 + f90a47c commit 3be4a97

88 files changed

Lines changed: 5815 additions & 292 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

app/activity/page.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import { useAccount } from 'wagmi';
6+
import Link from 'next/link';
7+
import { DashboardLayout } from '@/components/layouts/DashboardLayout';
8+
import { TransactionHistorySection } from '@/components/TransactionHistorySection';
9+
import { EmptyState } from '@/components/EmptyState';
10+
import { SkeletonRow } from '@/components/SkeletonRow';
11+
import { ErrorBoundary } from '@/components/ErrorBoundary';
12+
13+
export default function ActivityPage() {
14+
const { t } = useTranslation();
15+
const { address, isConnecting } = useAccount();
16+
const [loading, setLoading] = useState(true);
17+
18+
React.useEffect(() => {
19+
if (!isConnecting) {
20+
const timer = setTimeout(() => setLoading(false), 600);
21+
return () => clearTimeout(timer);
22+
}
23+
}, [isConnecting]);
24+
25+
if (isConnecting || loading) {
26+
return (
27+
<DashboardLayout>
28+
<section id="activity" className="card lg:col-span-2">
29+
<h2 className="text-sm font-semibold text-gray-900">{t('history.title')}</h2>
30+
<div className="mt-3 space-y-1">
31+
{Array.from({ length: 5 }).map((_, i) => (
32+
<SkeletonRow key={i} columns={3} />
33+
))}
34+
</div>
35+
</section>
36+
</DashboardLayout>
37+
);
38+
}
39+
40+
if (!address) {
41+
return (
42+
<DashboardLayout>
43+
<EmptyState
44+
headline={t('activity.empty.connectHeadline')}
45+
description={t('activity.empty.connectDescription')}
46+
primaryLabel={t('common.connectWallet')}
47+
primaryHref="/"
48+
/>
49+
</DashboardLayout>
50+
);
51+
}
52+
53+
return (
54+
<DashboardLayout>
55+
<section id="activity" className="card lg:col-span-2">
56+
<div className="flex items-center justify-between">
57+
<h2 className="text-sm font-semibold text-gray-900">{t('history.title')}</h2>
58+
<Link href="/dashboard" className="text-xs text-blue-600 hover:underline">
59+
{t('nav.home')}
60+
</Link>
61+
</div>
62+
<ErrorBoundary>
63+
<TransactionHistorySection />
64+
</ErrorBoundary>
65+
</section>
66+
</DashboardLayout>
67+
);
68+
}

app/dashboard/page.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { Suspense, useState } from 'react';
4+
import { useTranslation } from 'react-i18next';
45
import { DashboardLayout } from '@/components/layouts/DashboardLayout';
56
import { TransactionHistorySection } from '@/components/TransactionHistorySection';
67
import { TransactionSimulator } from '@/components/TransactionSimulator';
@@ -10,32 +11,58 @@ import { PortfolioAssets } from '@/components/PortfolioAssets';
1011
import { VaultTable } from '@/components/vaults/VaultTable';
1112
import { PluginGrid } from '@/components/dashboard/PluginGrid';
1213

14+
import { SwapCard } from '@/components/swap/SwapCard';
15+
1316
import { ErrorBoundary } from '@/components/ErrorBoundary';
17+
import { ProtocolStatsBar } from '@/components/dashboard/ProtocolStatsBar';
18+
import { RecentSwaps } from '@/components/dashboard/RecentSwaps';
1419

1520
export default function DashboardPage() {
21+
const { t } = useTranslation();
1622
const [sendOpen, setSendOpen] = useState(false);
1723

1824
return (
1925
<DashboardLayout>
2026
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
27+
<section id="swap" className="lg:col-span-2">
28+
<ErrorBoundary>
29+
<SwapCard />
30+
</ErrorBoundary>
31+
</section>
32+
33+
<section id="protocol-stats" className="lg:col-span-2">
34+
<ErrorBoundary>
35+
<ProtocolStatsBar />
36+
</ErrorBoundary>
37+
</section>
38+
2139
<section id="staking" className="card">
2240
<h2 className="text-sm font-semibold text-gray-900">Staking</h2>
2341
<ErrorBoundary>
2442
<p className="mt-2 text-sm text-gray-600">No active stakes yet.</p>
43+
<h2 className="text-sm font-semibold text-gray-900">{t('dashboard.staking')}</h2>
44+
<ErrorBoundary>
45+
<p className="mt-2 text-sm text-gray-600">{t('dashboard.noStakes')}</p>
2546
</ErrorBoundary>
2647
</section>
2748

2849
<section id="governance" className="card">
29-
<h2 className="text-sm font-semibold text-gray-900">Governance</h2>
30-
<p className="mt-2 text-sm text-gray-600">No open proposals.</p>
50+
<h2 className="text-sm font-semibold text-gray-900">{t('dashboard.governance')}</h2>
51+
<p className="mt-2 text-sm text-gray-600">{t('dashboard.noProposals')}</p>
52+
</section>
53+
54+
<section id="recent-swaps" className="lg:col-span-2">
55+
<ErrorBoundary>
56+
<RecentSwaps />
57+
</ErrorBoundary>
3158
</section>
3259

3360
<section id="portfolio" className="card lg:col-span-2">
3461
<ErrorBoundary>
3562
<PortfolioAssets />
3663
</ErrorBoundary>
3764
<button type="button" className="btn mt-3" onClick={() => setSendOpen(true)}>
38-
Send asset
65+
{t('dashboard.sendAsset')}
3966
</button>
4067
</section>
4168

app/globals.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,50 @@ html, body, :root {
2121
.btn-outline {
2222
@apply inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800;
2323
}
24+
25+
/* ─── RTL (Right-to-Left) support ────────────────────────────────────── */
26+
27+
/*
28+
* When the I18nProvider sets `dir="rtl"` on <html> (e.g. for Arabic),
29+
* these rules ensure the layout mirrors correctly.
30+
*/
31+
32+
[dir='rtl'] {
33+
text-align: right;
34+
}
35+
36+
[dir='rtl'] .container {
37+
direction: rtl;
38+
}
39+
40+
/*
41+
* Flip flex-row ordering for RTL so navbar items render in the expected
42+
* mirrored order. Tailwind's logical utilities (e.g. `ms-`, `me-`,
43+
* `ps-`, `pe-`, `start-`, `end-`) are already direction-aware, so we
44+
* only need to handle the overall row direction here.
45+
*/
46+
[dir='rtl'] .flex-row,
47+
[dir='rtl'] .md\:flex {
48+
flex-direction: row-reverse;
49+
}
50+
51+
/* Dropdown menus should open from the opposite side in RTL. */
52+
[dir='rtl'] .origin-top-right {
53+
transform-origin: top left;
54+
}
55+
56+
/* Subtle micro-animation for dropdown menus (shared LTR + RTL). */
57+
@keyframes fade-slide-in {
58+
from {
59+
opacity: 0;
60+
transform: translateY(-4px);
61+
}
62+
to {
63+
opacity: 1;
64+
transform: translateY(0);
65+
}
66+
}
67+
68+
.animate-in {
69+
animation: fade-slide-in 150ms ease-out;
70+
}

app/page.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
'use client';
2+
3+
import { useTranslation } from 'react-i18next';
4+
15
export default function HomePage() {
6+
const { t } = useTranslation();
27
return (
38
<div className="card">
4-
<h1 className="text-xl font-semibold text-gray-900">Welcome to WhiteChain</h1>
9+
<h1 className="text-xl font-semibold text-gray-900">{t('home.welcome')}</h1>
510
<p className="mt-2 text-sm text-gray-600">
6-
Connect your wallet from the navbar to get started.
11+
{t('home.connectPrompt')}
712
</p>
813
</div>
914
);

app/providers.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { wagmiConfig } from '@/lib/wagmi';
88
import { MempoolProvider } from '@/lib/mempool/MempoolProvider';
99
import { useBlockchainDataSync } from '@/lib/hooks/useBlockchainData';
1010
import { PluginSDKProvider } from '@/lib/core/PluginSDKProvider';
11+
import { I18nProvider } from '@/components/I18nProvider';
1112

1213
// Renders nothing; just mounts the block listener from issue #29 so
1314
// balance queries invalidate smartly app-wide, not per-component.
@@ -26,7 +27,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
2627
<MempoolProvider>
2728
{/* PluginSDKProvider must be inside WagmiProvider + QueryClientProvider
2829
so wallet hooks are available to the plugin bridge. */}
29-
<PluginSDKProvider>{children}</PluginSDKProvider>
30+
<PluginSDKProvider>
31+
<I18nProvider>{children}</I18nProvider>
32+
</PluginSDKProvider>
3033
</MempoolProvider>
3134
</QueryClientProvider>
3235
<Toaster richColors position="bottom-right" closeButton />

components/AssetTable.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface AssetRow {
77
symbol: string;
88
amount: string;
99
valueUsd?: string;
10+
/** When true, the balance is optimistically updated and pending confirmation. */
11+
isPending?: boolean;
1012
}
1113

1214
interface AssetTableProps {
@@ -47,7 +49,18 @@ export function AssetTable({ isLoading, rows, skeletonCount = 5 }: AssetTablePro
4749
{rows.map((row, i) => (
4850
<div key={`${row.symbol}-${i}`} className="flex items-center justify-between px-3 py-3">
4951
<span className="text-sm font-medium text-gray-900">{row.symbol}</span>
50-
<span className="text-sm text-gray-700">{row.amount}</span>
52+
<span className="flex items-center gap-2 text-sm text-gray-700">
53+
{row.amount}
54+
{row.isPending && (
55+
<span
56+
className="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700"
57+
title="Pending confirmation"
58+
>
59+
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500" />
60+
Pending
61+
</span>
62+
)}
63+
</span>
5164
{row.valueUsd && <span className="text-sm text-gray-500">{row.valueUsd}</span>}
5265
</div>
5366
))}

components/Avatar.test.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,18 @@ describe('Avatar', () => {
4343

4444
it('renders the resolved ENS avatar as an image', () => {
4545
hoisted.ensName = 'vitalik.eth';
46-
hoisted.avatarUrl = 'https://example.com/avatar.png';
46+
hoisted.avatarUrl = 'https://metadata.ens.domains/mainnet/avatar/vitalik.eth';
4747
render(<Avatar address={ADDRESS} />);
4848
const img = screen.getByRole('img', { name: 'vitalik.eth avatar' }) as HTMLImageElement;
4949
expect(img.tagName).toBe('IMG');
50-
expect(img.src).toBe('https://example.com/avatar.png');
50+
expect(img.getAttribute('loading')).toBe('lazy');
51+
expect(img.width).toBe(32);
52+
expect(img.height).toBe(32);
53+
const optimizedUrl = new URL(img.src);
54+
expect(optimizedUrl.pathname).toBe('/_next/image');
55+
expect(optimizedUrl.searchParams.get('url')).toBe(
56+
'https://metadata.ens.domains/mainnet/avatar/vitalik.eth'
57+
);
5158
});
5259

5360
it('falls back to the blocky circle if the image fails to load', () => {

components/Avatar.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use client';
22

3-
import React, { useState } from 'react';
3+
import React from 'react';
44
import { useEnsAvatar, useEnsName } from 'wagmi';
55
import { mainnet } from 'wagmi/chains';
6+
import { ImageWithFallback } from '@/components/ImageWithFallback';
67

78
export interface AvatarProps {
89
address?: `0x${string}`;
@@ -48,8 +49,6 @@ function BlockyFallback({ address, size }: { address?: string; size: number }) {
4849
* avatar record, or the image fails to load.
4950
*/
5051
export function Avatar({ address, size = 32, className }: AvatarProps) {
51-
const [imgErrored, setImgErrored] = useState(false);
52-
5352
const { data: ensName } = useEnsName({
5453
address,
5554
chainId: mainnet.id,
@@ -70,24 +69,25 @@ export function Avatar({ address, size = 32, className }: AvatarProps) {
7069
}
7170
});
7271

73-
if (!avatarUrl || imgErrored) {
72+
const fallback = <BlockyFallback address={address} size={size} />;
73+
74+
if (!avatarUrl) {
7475
return (
7576
<span className={className}>
76-
<BlockyFallback address={address} size={size} />
77+
{fallback}
7778
</span>
7879
);
7980
}
8081

8182
return (
8283
<span className={className}>
83-
{/* eslint-disable-next-line @next/next/no-img-element -- external, arbitrary ENS-hosted URL */}
84-
<img
84+
<ImageWithFallback
8585
src={avatarUrl}
8686
alt={ensName ? `${ensName} avatar` : 'ENS avatar'}
8787
width={size}
8888
height={size}
8989
className="inline-block shrink-0 rounded-full object-cover"
90-
onError={() => setImgErrored(true)}
90+
fallback={fallback}
9191
/>
9292
</span>
9393
);

0 commit comments

Comments
 (0)