Add deployment scripts and integrate APIs for dashboard #1
+1
-3
@@ -1,3 +1 @@
|
||||
CERC_TEST_WEBAPP_CONFIG1="this string"
|
||||
CERC_TEST_WEBAPP_CONFIG2="this different string"
|
||||
CERC_WEBAPP_DEBUG=0
|
||||
NEXT_PUBLIC_MTM_SERVICE_URL=http://localhost:3000
|
||||
@@ -125,7 +125,6 @@ The app currently reads and displays three environment variables:
|
||||
- Failed transaction analysis and retry functionality
|
||||
- Account balance monitoring with ETH refill interface
|
||||
- App download analytics with charts and version tracking
|
||||
- Customer support ticket management system
|
||||
- PWA configuration updated for admin dashboard
|
||||
|
||||
## Dashboard Pages
|
||||
@@ -135,7 +134,6 @@ The app currently reads and displays three environment variables:
|
||||
- `/dashboard/failed` - Failed transaction analysis
|
||||
- `/dashboard/balances` - Account balance management
|
||||
- `/dashboard/downloads` - App download analytics
|
||||
- `/dashboard/support` - Customer support system
|
||||
|
||||
## Technical Implementation
|
||||
- **Framework**: Next.js with TypeScript
|
||||
@@ -159,4 +157,219 @@ The app currently reads and displays three environment variables:
|
||||
- Bar charts using CSS flex and dynamic heights
|
||||
- Line charts using SVG paths and CSS positioning
|
||||
- Progress bars for platform distribution
|
||||
- All charts are animated and responsive
|
||||
- All charts are animated and responsive
|
||||
|
||||
## Design Requirements
|
||||
**CRITICAL: These requirements must be followed in ALL future modifications:**
|
||||
|
||||
### Dashboard Stats Display
|
||||
- **NEVER add percentage changes, change indicators, or trend arrows to dashboard stats**
|
||||
- Dashboard stats should only show: `id`, `name`, `stat`, and `icon` properties
|
||||
- Do NOT add `change`, `changeType`, percentage indicators, or trend arrows
|
||||
- Keep stats display clean and simple without any change/trend visualization
|
||||
|
||||
### UI Components
|
||||
- No support/customer service functionality
|
||||
- Focus purely on transaction monitoring and analytics
|
||||
- Android-only for app downloads (no iOS)
|
||||
- No authentication flow (direct redirect to dashboard)
|
||||
|
||||
# MTM to NYM Service API Documentation
|
||||
|
||||
This document describes the API endpoints for the MTM to NYM conversion service that powers the dashboard.
|
||||
|
||||
## Base URLs
|
||||
|
||||
- Dashboard API: `/api/dashboard`
|
||||
- Transactions API: `/api/transactions`
|
||||
|
||||
## Dashboard Endpoints
|
||||
|
||||
### GET /api/dashboard/stats
|
||||
|
||||
Returns overall dashboard statistics with monthly conversion data.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"totalConversions": 1247,
|
||||
"successfulConversions": 1189,
|
||||
"failedConversions": 58,
|
||||
"totalDownloads": 3229,
|
||||
"monthlyData": [
|
||||
{
|
||||
"month": "Mar",
|
||||
"totalConversions": 137,
|
||||
"successfulConversions": 130,
|
||||
"failedConversions": 7
|
||||
},
|
||||
{
|
||||
"month": "Apr",
|
||||
"totalConversions": 124,
|
||||
"successfulConversions": 118,
|
||||
"failedConversions": 6
|
||||
},
|
||||
{
|
||||
"month": "May",
|
||||
"totalConversions": 149,
|
||||
"successfulConversions": 142,
|
||||
"failedConversions": 7
|
||||
},
|
||||
{
|
||||
"month": "Jun",
|
||||
"totalConversions": 162,
|
||||
"successfulConversions": 155,
|
||||
"failedConversions": 7
|
||||
},
|
||||
{
|
||||
"month": "Jul",
|
||||
"totalConversions": 149,
|
||||
"successfulConversions": 142,
|
||||
"failedConversions": 7
|
||||
},
|
||||
{
|
||||
"month": "Aug",
|
||||
"totalConversions": 311,
|
||||
"successfulConversions": 296,
|
||||
"failedConversions": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Transaction Endpoints
|
||||
|
||||
### GET /api/transactions/conversions
|
||||
|
||||
Returns paginated MTM to NYM conversion data with filtering options.
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` (optional): Page number (default: 1)
|
||||
- `limit` (optional): Items per page (default: 10, max: 100)
|
||||
- `status` (optional): Filter by status (`success`, `failed`, `all`)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"transactions": [
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"transactionHash": "2AUxZpuQ...",
|
||||
"fromAddress": "HmWkGTaL...",
|
||||
"nymTransactionHash": "4E169C93...",
|
||||
"error": null,
|
||||
"createdAt": "2025-08-24T10:30:00.000Z"
|
||||
}
|
||||
],
|
||||
"totalCount": 1247,
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"totalPages": 125
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Other Transaction Endpoints
|
||||
|
||||
### POST /api/transactions/get-nym
|
||||
|
||||
Processes MTM to NYM conversion request.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"transactionHash": "2AUxZpuQ...",
|
||||
"nyxAddress": "n1sdnrq62m07gcwzpfmdgvqfpqqjvtnnllcypur8",
|
||||
"signedTxHash": "429ameDUN..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"transactionHash": "2AUxZpuQ...",
|
||||
"fromAddress": "HmWkGTaL...",
|
||||
"nymTransactionHash": "4E169C93..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/transactions/get-nym-balance
|
||||
|
||||
Returns current NYM balance of the service wallet.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"balanceInNym": 75000.00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return standard error responses:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Error message description"
|
||||
}
|
||||
```
|
||||
|
||||
Common HTTP status codes:
|
||||
- `200` - Success
|
||||
- `400` - Bad Request
|
||||
- `404` - Not Found
|
||||
- `500` - Internal Server Error
|
||||
|
||||
## Database Integration
|
||||
|
||||
The API endpoints integrate directly with the existing database models:
|
||||
- `Transaction` - MTM to NYM conversions with fields:
|
||||
- `id` (UUID)
|
||||
- `transactionHash` (Solana transaction hash)
|
||||
- `fromAddress` (Solana wallet address)
|
||||
- `nymTransactionHash` (NYM transaction hash, optional)
|
||||
- `error` (Error message if failed, optional)
|
||||
- `createdAt` (Timestamp)
|
||||
|
||||
## API Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```env
|
||||
# CORS Configuration (Required)
|
||||
CORS_ALLOWED_ORIGINS="http://localhost:3001,https://dashboard.example.com"
|
||||
|
||||
# Database and Service Configuration
|
||||
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
|
||||
PORT=3000
|
||||
NYX_PRIVATE_KEY=your_private_key
|
||||
NYX_RPC_ENDPOINT=https://rpc.nymtech.net
|
||||
COSMOS_GAS_PRICE=0.025
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
Start the service:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
APIs will be available at:
|
||||
- Dashboard: `http://localhost:3000/api/dashboard/`
|
||||
- Transactions: `http://localhost:3000/api/transactions/`
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
The MTM VPN Dashboard connects via:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_MTM_SERVICE_URL=http://localhost:3000
|
||||
```
|
||||
+1
-55
@@ -1,4 +1,4 @@
|
||||
import { Transaction, BridgeTransaction, SwapTransaction, AccountBalance, AppDownload, SupportTicket, DashboardStats } from '../types';
|
||||
import { Transaction, BridgeTransaction, SwapTransaction, AccountBalance, AppDownload, DashboardStats } from '../types';
|
||||
|
||||
export const mockTransactions: Transaction[] = [
|
||||
{
|
||||
@@ -263,60 +263,6 @@ export const mockAppDownloads: AppDownload[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const mockSupportTickets: SupportTicket[] = [
|
||||
{
|
||||
id: '1',
|
||||
userId: 'user_123',
|
||||
email: 'john.doe@example.com',
|
||||
subject: 'Unable to connect to VPN servers',
|
||||
message: 'I\'ve been trying to connect to VPN servers for the past 2 hours but keep getting connection timeout errors. Please help.',
|
||||
status: 'open',
|
||||
priority: 'high',
|
||||
createdAt: new Date('2025-08-24T14:30:00Z'),
|
||||
updatedAt: new Date('2025-08-24T14:30:00Z'),
|
||||
responses: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
userId: 'user_456',
|
||||
email: 'jane.smith@example.com',
|
||||
subject: 'Transaction failed but MTM tokens were deducted',
|
||||
message: 'I sent 50 MTM tokens but the transaction shows as failed, however the tokens are missing from my wallet.',
|
||||
status: 'in-progress',
|
||||
priority: 'critical',
|
||||
createdAt: new Date('2025-08-24T13:15:00Z'),
|
||||
updatedAt: new Date('2025-08-24T13:45:00Z'),
|
||||
responses: [
|
||||
{
|
||||
id: 'resp_1',
|
||||
ticketId: '2',
|
||||
message: 'We are investigating this issue. Can you please provide the transaction hash?',
|
||||
isAdminResponse: true,
|
||||
createdAt: new Date('2025-08-24T13:45:00Z')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
userId: 'user_789',
|
||||
email: 'mike.wilson@example.com',
|
||||
subject: 'Feature request: Dark mode support',
|
||||
message: 'Would love to see dark mode support in the VPN app. The current bright theme is hard on the eyes during night usage.',
|
||||
status: 'resolved',
|
||||
priority: 'low',
|
||||
createdAt: new Date('2025-08-23T16:20:00Z'),
|
||||
updatedAt: new Date('2025-08-24T10:00:00Z'),
|
||||
responses: [
|
||||
{
|
||||
id: 'resp_2',
|
||||
ticketId: '3',
|
||||
message: 'Thank you for the suggestion! Dark mode is planned for the next major release.',
|
||||
isAdminResponse: true,
|
||||
createdAt: new Date('2025-08-24T10:00:00Z')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const mockDashboardStats: DashboardStats = {
|
||||
totalTransactions: 1247,
|
||||
|
||||
+7
-3
@@ -2,8 +2,12 @@
|
||||
|
||||
## Setup
|
||||
|
||||
- Clone the repo and run the next set of steps inside cloned repo directory
|
||||
<!-- TODO: Add command to copy -->
|
||||
- Clone the repo and change to the [deploy](./) directory
|
||||
|
||||
```bash
|
||||
git clone git@git.vdb.to:cerc-io/mtm-vpn-dashboard.git
|
||||
cd mtm-vpn-dashboard/deploy
|
||||
```
|
||||
|
||||
- Build registry CLI image:
|
||||
|
||||
@@ -16,7 +20,7 @@
|
||||
- Configure `userKey` in the [registry CLI config](./config.yml):
|
||||
|
||||
- User key should be of the account that owns the `laconic-deploy` authority (owner account address: `laconic1kwx2jm6vscz38qlyujvq6msujmk8l3zangqahs`)
|
||||
|
||||
|
||||
- Account should also own the bond `5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2`
|
||||
|
||||
```bash
|
||||
|
||||
+4
-3
@@ -9,17 +9,18 @@ echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://github.com/deep-stack/mtm-vpn-dashboard"
|
||||
REPO_URL="https://git.vdb.to/cerc-io/mtm-vpn-dashboard"
|
||||
|
||||
# Get the latest commit hash for a branch
|
||||
# TODO: Change to main branch when ready
|
||||
BRANCH_NAME="ng-deploy-laconic"
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||
|
||||
# Gitea
|
||||
# PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version)
|
||||
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version)
|
||||
|
||||
# GitHub
|
||||
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/refs/heads/$BRANCH_NAME/package.json | jq -r .version)
|
||||
# PACKAGE_VERSION=$(curl -s $REPO_URL/raw/refs/heads/$BRANCH_NAME/package.json | jq -r .version)
|
||||
|
||||
APP_NAME=mtm-vpn-dashboard
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta name="description" content="MTM VPN Admin Dashboard - Monitor transactions, balances, and support" />
|
||||
<meta name="description" content="MTM VPN Admin Dashboard - Monitor transactions" />
|
||||
<meta name="keywords" content="MTM, VPN, Admin, Dashboard, Blockchain, NYM" />
|
||||
<title>MTM VPN Admin Dashboard</title>
|
||||
|
||||
|
||||
+138
-70
@@ -1,63 +1,122 @@
|
||||
// Charts removed for compatibility
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
ArrowsRightLeftIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
CurrencyDollarIcon,
|
||||
UsersIcon,
|
||||
CloudArrowDownIcon,
|
||||
WalletIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/20/solid';
|
||||
import Layout from '../../components/Layout';
|
||||
import { mockDashboardStats, mockTransactions, mockAppDownloads, mockAccountBalances } from '../../data/mockData';
|
||||
|
||||
const stats = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Total MTM to NYM Conversions',
|
||||
stat: mockDashboardStats.totalTransactions.toLocaleString(),
|
||||
icon: ArrowsRightLeftIcon,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Successful Conversions',
|
||||
stat: mockDashboardStats.successfulTransactions.toLocaleString(),
|
||||
icon: CheckCircleIcon,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Failed Conversions',
|
||||
stat: mockDashboardStats.failedTransactions.toLocaleString(),
|
||||
icon: XCircleIcon,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Total Downloads',
|
||||
stat: mockDashboardStats.totalDownloads.toLocaleString(),
|
||||
icon: CloudArrowDownIcon,
|
||||
},
|
||||
];
|
||||
|
||||
// Generate mock chart data
|
||||
const transactionChartData = [
|
||||
{ name: 'Jan', conversions: 850 },
|
||||
{ name: 'Feb', conversions: 920 },
|
||||
{ name: 'Mar', conversions: 1150 },
|
||||
{ name: 'Apr', conversions: 980 },
|
||||
{ name: 'May', conversions: 1200 },
|
||||
{ name: 'Jun', conversions: 1350 },
|
||||
{ name: 'Jul', conversions: 1180 },
|
||||
{ name: 'Aug', conversions: mockDashboardStats.totalTransactions },
|
||||
];
|
||||
|
||||
const downloadChartData = mockAppDownloads.map(download => ({
|
||||
version: download.version,
|
||||
downloads: download.downloads,
|
||||
platform: download.platform,
|
||||
}));
|
||||
import { mockAccountBalances } from '../../data/mockData';
|
||||
import dashboardApi, { ApiError, DashboardStats, TransactionData, MonthlyData } from '../../utils/api';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [dashboardStats, setDashboardStats] = useState<DashboardStats | null>(null);
|
||||
const [recentTransactions, setRecentTransactions] = useState<TransactionData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Fetch dashboard stats from MTM service
|
||||
const statsData = await dashboardApi.getStats();
|
||||
setDashboardStats(statsData);
|
||||
|
||||
// Fetch recent conversions from MTM service
|
||||
const conversionsData = await dashboardApi.getConversions({
|
||||
limit: 5,
|
||||
status: 'all'
|
||||
});
|
||||
|
||||
// Use conversion data directly
|
||||
setRecentTransactions(conversionsData.transactions);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(`API Error: ${err.message}`);
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStats = () => {
|
||||
if (!dashboardStats) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Total MTM to NYM Conversions',
|
||||
stat: dashboardStats.totalConversions.toLocaleString(),
|
||||
icon: ArrowsRightLeftIcon,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Successful Conversions',
|
||||
stat: dashboardStats.successfulConversions.toLocaleString(),
|
||||
icon: CheckCircleIcon,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Failed Conversions',
|
||||
stat: dashboardStats.failedConversions.toLocaleString(),
|
||||
icon: XCircleIcon,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Total Downloads',
|
||||
stat: dashboardStats.totalDownloads.toLocaleString(),
|
||||
icon: CloudArrowDownIcon,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">Error loading dashboard</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={fetchDashboardData}
|
||||
className="bg-red-100 px-2 py-1 text-sm text-red-800 rounded hover:bg-red-200"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const stats = getStats();
|
||||
return (
|
||||
<Layout>
|
||||
<div>
|
||||
@@ -89,21 +148,27 @@ export default function Dashboard() {
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">MTM to NYM Conversions Trend</h3>
|
||||
<div className="h-80 flex items-end justify-between space-x-2 border-b border-l border-gray-200 p-4">
|
||||
{transactionChartData.map((data, index) => (
|
||||
<div key={data.name} className="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
className="w-full bg-blue-500 rounded-t transition-all duration-500"
|
||||
style={{
|
||||
height: `${(data.conversions / Math.max(...transactionChartData.map(d => d.conversions))) * 100}%`,
|
||||
minHeight: '4px'
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 text-xs text-gray-500 font-medium">{data.name}</div>
|
||||
<div className="text-xs text-gray-400">{data.conversions.toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{dashboardStats && (
|
||||
<div className="h-80 flex items-end justify-between space-x-2 border-b border-l border-gray-200 p-4">
|
||||
{/* Monthly data from API */}
|
||||
{dashboardStats.monthlyData && dashboardStats.monthlyData.map((data, index) => {
|
||||
const maxConversions = Math.max(...dashboardStats.monthlyData.map(d => d.totalConversions));
|
||||
return (
|
||||
<div key={data.month} className="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
className="w-full bg-blue-500 rounded-t transition-all duration-500"
|
||||
style={{
|
||||
height: `${Math.max((data.totalConversions / Math.max(maxConversions, 1)) * 300, 8)}px`,
|
||||
minHeight: '8px'
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 text-xs text-gray-500 font-medium">{data.month}</div>
|
||||
<div className="text-xs text-gray-400">{data.totalConversions.toLocaleString()}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +232,7 @@ export default function Dashboard() {
|
||||
</a>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{mockTransactions.slice(0, 5).map((transaction) => (
|
||||
{recentTransactions.map((transaction) => (
|
||||
<li key={transaction.id}>
|
||||
<div className="px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
@@ -180,16 +245,19 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{transaction.transactionHash.slice(0, 8)}...{transaction.transactionHash.slice(-8)}
|
||||
{`${transaction.transactionHash.slice(0, 8)}...${transaction.transactionHash.slice(-8)}`}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
From: {transaction.fromAddress.slice(0, 8)}...{transaction.fromAddress.slice(-8)}
|
||||
{transaction.error ?
|
||||
`Error: ${transaction.error.substring(0, 40)}...` :
|
||||
`From: ${transaction.fromAddress.slice(0, 8)}...${transaction.fromAddress.slice(-8)}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-900">
|
||||
<span suppressHydrationWarning>
|
||||
{transaction.createdAt.toLocaleDateString()}
|
||||
{new Date(transaction.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,4 +269,4 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-25
@@ -38,32 +38,18 @@ export interface AppDownload {
|
||||
fileSize: string;
|
||||
}
|
||||
|
||||
export interface SupportTicket {
|
||||
id: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
status: 'open' | 'in-progress' | 'resolved' | 'closed';
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
responses: SupportResponse[];
|
||||
}
|
||||
|
||||
export interface SupportResponse {
|
||||
id: string;
|
||||
ticketId: string;
|
||||
message: string;
|
||||
isAdminResponse: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
totalTransactions: number;
|
||||
successfulTransactions: number;
|
||||
failedTransactions: number;
|
||||
totalVolume: string;
|
||||
activeUsers: number;
|
||||
totalConversions: number;
|
||||
successfulConversions: number;
|
||||
failedConversions: number;
|
||||
totalDownloads: number;
|
||||
monthlyData: MonthlyData[];
|
||||
}
|
||||
|
||||
export interface MonthlyData {
|
||||
month: string;
|
||||
totalConversions: number;
|
||||
successfulConversions: number;
|
||||
failedConversions: number;
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
const MTM_SERVICE_URL = process.env.NEXT_PUBLIC_MTM_SERVICE_URL || 'http://localhost:3000';
|
||||
|
||||
export interface MonthlyData {
|
||||
month: string;
|
||||
totalConversions: number;
|
||||
successfulConversions: number;
|
||||
failedConversions: number;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
totalConversions: number;
|
||||
successfulConversions: number;
|
||||
failedConversions: number;
|
||||
totalDownloads: number;
|
||||
monthlyData: MonthlyData[];
|
||||
}
|
||||
|
||||
export interface TransactionData {
|
||||
id: string;
|
||||
transactionHash: string;
|
||||
fromAddress: string;
|
||||
nymTransactionHash?: string;
|
||||
error?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface TransactionsResponse {
|
||||
transactions: TransactionData[];
|
||||
totalCount: number;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public response?: any
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
async function apiRequest<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${MTM_SERVICE_URL}${endpoint}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new ApiError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
response.status,
|
||||
errorData
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Network or parsing errors
|
||||
throw new ApiError(
|
||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const dashboardApi = {
|
||||
// Get dashboard statistics
|
||||
getStats: (): Promise<DashboardStats> => apiRequest<DashboardStats>('/api/dashboard/stats'),
|
||||
|
||||
// Get conversions with pagination and filtering
|
||||
getConversions: (params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: string;
|
||||
type?: string;
|
||||
}): Promise<TransactionsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
if (params?.limit) searchParams.set('limit', params.limit.toString());
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.type) searchParams.set('type', params.type);
|
||||
|
||||
const query = searchParams.toString();
|
||||
const endpoint = `/api/transactions/conversions${query ? `?${query}` : ''}`;
|
||||
return apiRequest<TransactionsResponse>(endpoint);
|
||||
},
|
||||
};
|
||||
|
||||
export default dashboardApi;
|
||||
Reference in New Issue
Block a user