diff --git a/veilend-web/package-lock.json b/veilend-web/package-lock.json
index 83dc600..5612945 100644
--- a/veilend-web/package-lock.json
+++ b/veilend-web/package-lock.json
@@ -26,13 +26,14 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@vitest/ui": "^3.2.7",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.4",
"tailwindcss": "^4",
"typescript": "^5",
- "vitest": "^3.2.6"
+ "vitest": "^3.2.7"
}
},
"node_modules/@alloc/quick-lru": {
@@ -2259,6 +2260,13 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@radix-ui/number": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz",
@@ -5270,15 +5278,15 @@
]
},
"node_modules/@vitest/expect": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz",
- "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.6",
- "@vitest/utils": "3.2.6",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -5287,13 +5295,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz",
- "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "3.2.6",
+ "@vitest/spy": "3.2.7",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -5314,9 +5322,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz",
- "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5327,13 +5335,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz",
- "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "3.2.6",
+ "@vitest/utils": "3.2.7",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
@@ -5342,13 +5350,13 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz",
- "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.6",
+ "@vitest/pretty-format": "3.2.7",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -5357,9 +5365,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz",
- "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5369,14 +5377,36 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@vitest/ui": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.7.tgz",
+ "integrity": "sha512-eVtcpJXGhS0GjMuHROfbXLhlxooyUcuip8GNzzjDD5jzafZqzanJH4W3VGmUxHNx4fv6qQGUGJHRpGUdj+9D6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.7",
+ "fflate": "^0.8.2",
+ "flatted": "^3.3.3",
+ "pathe": "^2.0.3",
+ "sirv": "^3.0.1",
+ "tinyglobby": "^0.2.14",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "3.2.7"
+ }
+ },
"node_modules/@vitest/utils": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz",
- "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.6",
+ "@vitest/pretty-format": "3.2.7",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
@@ -7826,6 +7856,13 @@
"node": "^12.20 || >= 14.13"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -9873,6 +9910,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -11741,6 +11788,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -12317,6 +12379,16 @@
"node": ">=0.6"
}
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -12893,20 +12965,20 @@
}
},
"node_modules/vitest": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz",
- "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.6",
- "@vitest/mocker": "3.2.6",
- "@vitest/pretty-format": "^3.2.6",
- "@vitest/runner": "3.2.6",
- "@vitest/snapshot": "3.2.6",
- "@vitest/spy": "3.2.6",
- "@vitest/utils": "3.2.6",
+ "@vitest/expect": "3.2.7",
+ "@vitest/mocker": "3.2.7",
+ "@vitest/pretty-format": "^3.2.7",
+ "@vitest/runner": "3.2.7",
+ "@vitest/snapshot": "3.2.7",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
@@ -12936,8 +13008,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.6",
- "@vitest/ui": "3.2.6",
+ "@vitest/browser": "3.2.7",
+ "@vitest/ui": "3.2.7",
"happy-dom": "*",
"jsdom": "*"
},
diff --git a/veilend-web/package.json b/veilend-web/package.json
index 05d6b2f..a747b19 100644
--- a/veilend-web/package.json
+++ b/veilend-web/package.json
@@ -30,12 +30,13 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@vitest/ui": "^3.2.7",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.4",
"tailwindcss": "^4",
"typescript": "^5",
- "vitest": "^3.2.6"
+ "vitest": "^3.2.7"
}
}
diff --git a/veilend-web/src/app/dashboard/error.tsx b/veilend-web/src/app/dashboard/error.tsx
index 1e32bba..fecfd67 100644
--- a/veilend-web/src/app/dashboard/error.tsx
+++ b/veilend-web/src/app/dashboard/error.tsx
@@ -2,41 +2,80 @@
import { useEffect } from 'react';
import { Container, Flex, Section } from '@/components/Layout';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { AlertCircle, RefreshCw } from 'lucide-react';
-export default function DashboardError({
- error,
- reset,
-}: {
+interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
-}) {
+}
+
+export default function DashboardError({ error, reset }: ErrorProps) {
useEffect(() => {
// Log the error to an error reporting service
- console.error('Dashboard failed to load:', error);
+ console.error('Dashboard Error:', error);
}, [error]);
+ const isAuthError = error.message?.includes('address') || error.message?.includes('wallet');
+ const isNetworkError = error.message?.includes('fetch') || error.message?.includes('network');
+
return (
-
-
- Failed to load dashboard
-
- We encountered an error while fetching your live portfolio and activity data.
- Please try again or check your network connection.
+
+
+
+
+ {isAuthError ? 'Wallet Connection Error' :
+ isNetworkError ? 'Network Error' :
+ 'Dashboard Error'}
+
+
+
+ {isAuthError
+ ? 'Unable to load dashboard data. Please ensure your wallet is properly connected and try again.'
+ : isNetworkError
+ ? 'Network connection issue. Please check your internet connection and try again.'
+ : error.message || 'An unexpected error occurred while loading the dashboard.'
+ }
+
+ {error.digest && (
+
+ Error ID: {error.digest}
+
+ )}
-
-
);
-}
+}
\ No newline at end of file
diff --git a/veilend-web/src/app/dashboard/loading.tsx b/veilend-web/src/app/dashboard/loading.tsx
index 181f0d8..1f2deee 100644
--- a/veilend-web/src/app/dashboard/loading.tsx
+++ b/veilend-web/src/app/dashboard/loading.tsx
@@ -1,7 +1,6 @@
import { Container, Flex, Grid, Section } from '@/components/Layout';
-import { LoaderCircle } from 'lucide-react';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export default function DashboardLoading() {
return (
@@ -9,47 +8,87 @@ export default function DashboardLoading() {
-
- Dashboard
-
-
- Fetching live portfolio data...
+
+
+
+
+ {/* Portfolio Section Loading */}
{[1, 2, 3].map((i) => (
-
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ {/* Asset Breakdown Loading */}
+
+
+ {[1, 2].map((i) => (
+
+
+
+
+
-
-
+
+ {[1, 2, 3].map((j) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
))}
+ {/* Recent Activity Loading */}
- Recent Activity
+
+
+
- {[1, 2, 3, 4].map((i) => (
-
+ {[1, 2, 3, 4, 5].map((i) => (
+
-
-
-
+
+
+
+
+
-
+
+
+
+
))}
@@ -59,4 +98,4 @@ export default function DashboardLoading() {
);
-}
+}
\ No newline at end of file
diff --git a/veilend-web/src/app/dashboard/page.tsx b/veilend-web/src/app/dashboard/page.tsx
index 317708c..d59cc52 100644
--- a/veilend-web/src/app/dashboard/page.tsx
+++ b/veilend-web/src/app/dashboard/page.tsx
@@ -1,31 +1,101 @@
import { Container, Flex, Grid, Section } from '@/components/Layout';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Alert, AlertDescription } from '@/components/ui/alert';
import { fetchDashboardData } from '@/lib/api/dashboard';
+import { headers } from 'next/headers';
+import { redirect } from 'next/navigation';
export const dynamic = 'force-dynamic';
+// Helper to get wallet address from session/cookie
+async function getWalletAddress(): Promise {
+ // In a production app, this would get the address from a secure session
+ // For now, we'll check for a wallet address in the headers
+ const headersList = await headers();
+ const walletAddress = headersList.get('x-wallet-address');
+
+ if (walletAddress && walletAddress.startsWith('G')) {
+ return walletAddress;
+ }
+
+ return null;
+}
+
+// Helper functions moved outside component
+const getActionBadgeClassName = (action: string): string | undefined => {
+ switch (action) {
+ case 'DEPOSIT':
+ return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-400';
+ case 'BORROW':
+ return 'border-amber-500/20 bg-amber-500/10 text-amber-400';
+ case 'REPAY':
+ return 'border-purple-500/20 bg-purple-500/10 text-purple-400';
+ case 'WITHDRAW':
+ return 'border-cyan-500/20 bg-cyan-500/10 text-cyan-400';
+ default:
+ return undefined;
+ }
+};
+
+const formatUsd = (val: number): string =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(val);
+
+const formatHealthFactor = (factor: number): string => {
+ if (factor === Infinity) return '∞';
+ return factor.toFixed(2);
+};
+
export default async function DashboardPage() {
- const data = await fetchDashboardData();
- const { portfolio, recentActivity } = data;
-
- const getActionBadgeClassName = (action: string) => {
- switch (action) {
- case 'DEPOSIT':
- return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-400';
- case 'BORROW':
- return 'border-amber-500/20 bg-amber-500/10 text-amber-400';
- case 'REPAY':
- return 'border-purple-500/20 bg-purple-500/10 text-purple-400';
- case 'WITHDRAW':
- return 'border-cyan-500/20 bg-cyan-500/10 text-cyan-400';
- default:
- return undefined;
- }
+ const walletAddress = await getWalletAddress();
+
+ // Redirect to home if no wallet is connected
+ if (!walletAddress) {
+ redirect('/');
+ }
+
+ let data;
+ let error: string | null = null;
+
+ try {
+ data = await fetchDashboardData(walletAddress);
+ } catch (err) {
+ error = err instanceof Error ? err.message : 'Failed to load dashboard data';
+ data = null;
+ }
+
+ const { portfolio, recentActivity } = data || {
+ portfolio: {
+ totalBalanceUsd: 0,
+ healthFactor: Infinity,
+ totalDepositedUsd: 0,
+ totalBorrowedUsd: 0,
+ depositedAssets: [],
+ borrowedAssets: [],
+ lastUpdated: new Date().toISOString(),
+ },
+ recentActivity: [],
};
- const formatUsd = (val: number) =>
- new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(val);
+ // Handle error state
+ if (error) {
+ return (
+
+ );
+ }
return (
@@ -36,6 +106,12 @@ export default async function DashboardPage() {
Live Dashboard
Overview of your Stellar network portfolio and recent activity.
+
+ Wallet: {walletAddress.slice(0, 6)}...{walletAddress.slice(-6)}
+
+
+
+ Last updated: {new Date(portfolio.lastUpdated).toLocaleString()}
@@ -49,7 +125,7 @@ export default async function DashboardPage() {
Total Balance
-
+
= 0 ? 'text-text' : 'text-error'}`}>
{formatUsd(portfolio.totalBalanceUsd)}
@@ -71,10 +147,12 @@ export default async function DashboardPage() {
Total Borrowed
= 1.1 || portfolio.healthFactor === Infinity
+ ? 'bg-emerald-500/10 text-emerald-400'
+ : undefined}
>
- Health: {portfolio.healthFactor.toFixed(2)}
+ Health: {formatHealthFactor(portfolio.healthFactor)}
@@ -96,7 +174,7 @@ export default async function DashboardPage() {
{portfolio.depositedAssets.length === 0 ? (
- No deposited assets.
+ No deposited assets found.
) : (
{portfolio.depositedAssets.map((asset) => (
@@ -113,7 +191,7 @@ export default async function DashboardPage() {
{asset.assetName}
- {asset.balance.toLocaleString()} {asset.assetSymbol}
+ {asset.balance.toLocaleString(undefined, { maximumFractionDigits: 4 })} {asset.assetSymbol}
@@ -148,7 +226,7 @@ export default async function DashboardPage() {
{asset.assetName}
- {asset.balance.toLocaleString()} {asset.assetSymbol}
+ {asset.balance.toLocaleString(undefined, { maximumFractionDigits: 4 })} {asset.assetSymbol}
@@ -173,7 +251,7 @@ export default async function DashboardPage() {
No recent activity found.
) : (
- {recentActivity.map((activity) => (
+ {recentActivity.slice(0, 20).map((activity) => (
- {activity.amount.toLocaleString()} {activity.assetSymbol}
+ {activity.amount.toLocaleString(undefined, { maximumFractionDigits: 4 })} {activity.assetSymbol}
@@ -213,4 +291,4 @@ export default async function DashboardPage() {
);
-}
+}
\ No newline at end of file
diff --git a/veilend-web/src/app/page.tsx b/veilend-web/src/app/page.tsx
index 80f60fc..6c4e21f 100644
--- a/veilend-web/src/app/page.tsx
+++ b/veilend-web/src/app/page.tsx
@@ -18,9 +18,10 @@ import { Badge } from "@/components/ui/badge"
import { useWallet } from "@/context/WalletContext"
export default function VeilLendLandingPage() {
- const { isConnected, isAuthenticated } = useWallet();
+ const { isConnected, isAuthenticated, isLoading, address, error } = useWallet();
// Simulated Live Campaign Metrics for the contributor block
const [totalContributed, setTotalContributed] = React.useState(642850)
+ const [connectionError, setConnectionError] = React.useState(null)
React.useEffect(() => {
const interval = setInterval(() => {
@@ -29,6 +30,38 @@ export default function VeilLendLandingPage() {
return () => clearInterval(interval)
}, [])
+ // Handle wallet errors - using useCallback to avoid setState in effect
+ React.useEffect(() => {
+ if (error) {
+ // Use a timeout to avoid synchronous setState in effect
+ const timer = setTimeout(() => {
+ setConnectionError(error)
+ }, 0)
+
+ const clearTimer = setTimeout(() => {
+ setConnectionError(null)
+ }, 5000)
+
+ return () => {
+ clearTimeout(timer)
+ clearTimeout(clearTimer)
+ }
+ }
+ }, [error])
+
+ const getConnectButtonText = () => {
+ if (isLoading) return "Connecting..."
+ if (isConnected && isAuthenticated) return "Go to Dashboard"
+ if (connectionError) return "Retry Connection"
+ return "Connect Wallet to Start"
+ }
+
+ const getConnectButtonIcon = () => {
+ if (isLoading) return null
+ if (isConnected && isAuthenticated) return
+ return
+ }
+
return (
@@ -55,20 +88,52 @@ export default function VeilLendLandingPage() {
+ {/* Connection status display */}
+ {isConnected && isAuthenticated && address && (
+
+
+ Connected: {address.slice(0, 6)}...{address.slice(-4)}
+
+ )}
+
+ {/* Primary CTA Button */}
{isConnected && isAuthenticated ? (
-
- Go to Dashboard
+
+
+ {getConnectButtonText()}
+ {getConnectButtonIcon()}
+
) : (
-
- Connect Wallet to Start
+
+
+ {getConnectButtonText()}
+ {getConnectButtonIcon()}
+
)}
+
Explore Architecture
+ {/* Error display */}
+ {connectionError && (
+
+ )}
+
{/* Dynamic Abstract Code Terminal Frame Mockup */}
@@ -233,11 +298,31 @@ export default function VeilLendLandingPage() {
Go to Dashboard
) : (
-
- Connect Wallet to Start
+
+
+ {isLoading ? "Connecting..." : "Connect Wallet to Start"}
+ {!isLoading && }
+
)}
+ {/* Connected status display for CTA section */}
+ {isConnected && isAuthenticated && address && (
+
+
+ Wallet connected: {address.slice(0, 6)}...{address.slice(-4)}
+
+ )}
+ {connectionError && (
+
+ )}
diff --git a/veilend-web/src/components/Layout.tsx b/veilend-web/src/components/Layout.tsx
index 41870b3..b14dc5e 100644
--- a/veilend-web/src/components/Layout.tsx
+++ b/veilend-web/src/components/Layout.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { cn } from "@/lib/utils";
interface ContainerProps {
children: React.ReactNode;
@@ -10,7 +11,7 @@ export const Container: React.FC
= ({
className = "",
}) => {
return (
-
+
{children}
);
@@ -19,19 +20,32 @@ export const Container: React.FC
= ({
interface SectionProps {
children: React.ReactNode;
className?: string;
+ padding?: "none" | "sm" | "md" | "lg";
}
export const Section: React.FC = ({
children,
className = "",
+ padding = "md",
}) => {
- return ;
+ const paddingClasses: Record = {
+ none: "py-0",
+ sm: "py-6",
+ md: "py-12",
+ lg: "py-20",
+ };
+
+ return (
+
+ );
};
interface FlexProps {
children: React.ReactNode;
direction?: "row" | "col";
- justify?: "start" | "center" | "end" | "between" | "around";
+ justify?: "start" | "center" | "end" | "between" | "around" | "evenly";
align?: "start" | "center" | "end" | "stretch";
gap?: "none" | "sm" | "md" | "lg" | "xl";
wrap?: boolean;
@@ -58,6 +72,7 @@ export const Flex: React.FC = ({
end: "justify-end",
between: "justify-between",
around: "justify-around",
+ evenly: "justify-evenly",
};
const alignClasses: Record = {
@@ -77,7 +92,15 @@ export const Flex: React.FC = ({
return (
{children}
@@ -86,7 +109,7 @@ export const Flex: React.FC = ({
interface GridProps {
children: React.ReactNode;
- columns?: number;
+ columns?: number | { sm?: number; md?: number; lg?: number; xl?: number };
gap?: "none" | "sm" | "md" | "lg" | "xl";
className?: string;
}
@@ -105,18 +128,91 @@ export const Grid: React.FC = ({
xl: "gap-8",
};
- const columnClasses: Record = {
- 1: "grid-cols-1",
- 2: "grid-cols-1 md:grid-cols-2",
- 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
- 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
+ const getColumnsClass = () => {
+ if (typeof columns === "number") {
+ const colMap: Record = {
+ 1: "grid-cols-1",
+ 2: "grid-cols-1 md:grid-cols-2",
+ 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
+ 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
+ 5: "grid-cols-1 md:grid-cols-3 lg:grid-cols-5",
+ 6: "grid-cols-1 md:grid-cols-3 lg:grid-cols-6",
+ };
+ return colMap[columns] || `grid-cols-${columns}`;
+ }
+
+ const parts: string[] = [];
+ if (columns.sm) parts.push(`grid-cols-${columns.sm}`);
+ if (columns.md) parts.push(`md:grid-cols-${columns.md}`);
+ if (columns.lg) parts.push(`lg:grid-cols-${columns.lg}`);
+ if (columns.xl) parts.push(`xl:grid-cols-${columns.xl}`);
+
+ if (parts.length === 0) return "grid-cols-1";
+
+ return parts.join(" ");
};
return (
{children}
);
};
+
+interface GridResponsiveProps {
+ children: React.ReactNode;
+ columns?: { sm?: number; md?: number; lg?: number; xl?: number };
+ gap?: "none" | "sm" | "md" | "lg" | "xl";
+ className?: string;
+}
+
+export const GridResponsive: React.FC = ({
+ children,
+ columns = { sm: 1, md: 2, lg: 3 },
+ gap = "md",
+ className = "",
+}) => {
+ const gapClasses: Record = {
+ none: "gap-0",
+ sm: "gap-2",
+ md: "gap-4",
+ lg: "gap-6",
+ xl: "gap-8",
+ };
+
+ const parts: string[] = [];
+ if (columns.sm) parts.push(`grid-cols-${columns.sm}`);
+ if (columns.md) parts.push(`md:grid-cols-${columns.md}`);
+ if (columns.lg) parts.push(`lg:grid-cols-${columns.lg}`);
+ if (columns.xl) parts.push(`xl:grid-cols-${columns.xl}`);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const LayoutComponents = {
+ Container,
+ Section,
+ Flex,
+ Grid,
+ GridResponsive,
+};
+
+export default LayoutComponents;
diff --git a/veilend-web/src/components/WalletConnect.tsx b/veilend-web/src/components/WalletConnect.tsx
index 1751ddc..790f3f5 100644
--- a/veilend-web/src/components/WalletConnect.tsx
+++ b/veilend-web/src/components/WalletConnect.tsx
@@ -1,7 +1,7 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
-import { Wallet, X, ExternalLink } from "lucide-react";
+import { Wallet, X, ExternalLink, Loader2, AlertCircle, CheckCircle2, Download } from "lucide-react";
import { useWallet } from "@/context/WalletContext";
import { Button } from "@/components/ui/button";
import {
@@ -13,25 +13,39 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+// import { Flex } from "@/components/Layout";
import { getWalletConnectionMessage, isFreighterInstalled } from "@/lib/stellar/wallet";
-// Define the Freighter window interface
-interface FreighterWindow extends Window {
- freighter?: {
- isConnected: () => Promise<{ isConnected: boolean }>;
- getAddress: () => Promise<{ address: string }>;
- signTransaction: (txXdr: string, opts?: { networkPassphrase?: string }) => Promise<{ signedTxXdr: string }>;
- };
-}
-
interface WalletConnectProps {
+ variant?: 'default' | 'compact' | 'full';
className?: string;
size?: "sm" | "default" | "lg";
+ onSuccess?: (address: string) => void;
+ onError?: (error: Error) => void;
}
-export function WalletConnect({ className, size = "default" }: WalletConnectProps) {
- const { address, isConnected, isAuthenticated, isLoading, error, connect, disconnect, clearError } = useWallet();
+export function WalletConnect({
+ variant = 'default',
+ className = "",
+ size = "default",
+ onSuccess,
+ onError
+}: WalletConnectProps) {
+ const {
+ address,
+ isConnected,
+ isAuthenticated,
+ isInstalled,
+ isLoading,
+ error,
+ connect,
+ disconnect,
+ clearError
+ } = useWallet();
+
const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isConnecting, setIsConnecting] = useState(false);
const primaryActionRef = useRef(null);
const handleOpenChange = (nextOpen: boolean) => {
@@ -42,20 +56,47 @@ export function WalletConnect({ className, size = "default" }: WalletConnectProp
};
const handleConnect = async () => {
- const success = await connect();
- if (success) {
- setIsModalOpen(false);
+ if (isConnecting || isLoading) return;
+
+ try {
+ setIsConnecting(true);
+ clearError();
+
+ const success = await connect();
+
+ if (success && address && onSuccess) {
+ onSuccess(address);
+ }
+
+ if (success) {
+ setIsModalOpen(false);
+ }
+ } catch (err) {
+ const errorObj = err instanceof Error ? err : new Error('Failed to connect wallet');
+ if (onError) {
+ onError(errorObj);
+ }
+ } finally {
+ setIsConnecting(false);
}
};
const handleDisconnect = async () => {
- await disconnect();
- setIsModalOpen(false);
- clearError();
+ try {
+ await disconnect();
+ setIsModalOpen(false);
+ clearError();
+ } catch (err) {
+ console.error('Disconnect error:', err);
+ }
};
const walletMessage = getWalletConnectionMessage(error, isFreighterInstalled());
+ const truncatedAddress = address
+ ? `${address.slice(0, 6)}...${address.slice(-4)}`
+ : "";
+ // Focus management
useEffect(() => {
if (!isModalOpen) return;
@@ -66,12 +107,99 @@ export function WalletConnect({ className, size = "default" }: WalletConnectProp
return () => window.clearTimeout(focusTimer);
}, [isModalOpen, error]);
- const truncatedAddress = address
- ? `${address.slice(0, 6)}...${address.slice(-4)}`
- : "";
+ // Compact variant - minimal display
+ if (variant === 'compact') {
+ if (isConnected && isAuthenticated && address) {
+ return (
+
+
+
+
+ {address.slice(0, 6)}...{address.slice(-4)}
+
+
+
+ Disconnect
+
+
+ );
+ }
+
+ return (
+ setIsModalOpen(true)}
+ disabled={isLoading || isConnecting}
+ className={className}
+ >
+ {isLoading || isConnecting ? (
+
+ ) : (
+
+ )}
+ {isLoading || isConnecting ? "Connecting..." : "Connect"}
+
+ );
+ }
- // Wallet is connected
- if (isConnected && isAuthenticated) {
+ // Full variant - with extra description
+ if (variant === 'full') {
+ return (
+
+ {isConnected && isAuthenticated && address ? (
+
+
+
+
+
+
+
Connected
+
+ {address.slice(0, 6)}...{address.slice(-6)}
+
+
+
+
+ Disconnect
+
+
+ ) : (
+ <>
+
setIsModalOpen(true)}
+ disabled={isLoading || isConnecting}
+ className="w-full flex items-center justify-center gap-2 py-6 text-base"
+ size="lg"
+ >
+ {isLoading || isConnecting ? (
+
+ ) : (
+
+ )}
+ {isLoading || isConnecting ? "Connecting..." : "Connect Wallet"}
+
+
+ Connect your Stellar wallet to access the dashboard
+
+ >
+ )}
+
+ );
+ }
+
+ // Default variant - connected state
+ if (isConnected && isAuthenticated && address) {
return (
@@ -90,18 +218,22 @@ export function WalletConnect({ className, size = "default" }: WalletConnectProp
);
}
- // Wallet not connected
+ // Default variant - disconnected state
return (
<>
setIsModalOpen(true)}
- disabled={isLoading}
+ disabled={isLoading || isConnecting}
className={className}
>
-
- {isLoading ? "Connecting..." : "Connect Wallet"}
+ {isLoading || isConnecting ? (
+
+ ) : (
+
+ )}
+ {isLoading || isConnecting ? "Connecting..." : "Connect Wallet"}