@qhristen/paygrid 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +98 -0
- package/dist/auth/actions.d.ts +4 -0
- package/dist/auth/actions.js +37 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +28 -0
- package/dist/auth/login-screen.d.ts +2 -0
- package/dist/auth/login-screen.jsx +49 -0
- package/dist/blockchain/index.d.ts +11 -0
- package/dist/blockchain/index.js +59 -0
- package/dist/checkout/index.d.ts +10 -0
- package/dist/checkout/index.jsx +235 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +4 -0
- package/dist/config/index.d.ts +39 -0
- package/dist/config/index.js +44 -0
- package/dist/core/paygrid.d.ts +44 -0
- package/dist/core/paygrid.js +238 -0
- package/dist/core/privacy-wrapper.d.ts +29 -0
- package/dist/core/privacy-wrapper.js +72 -0
- package/dist/dashboard/api-section.d.ts +7 -0
- package/dist/dashboard/api-section.jsx +146 -0
- package/dist/dashboard/constant.d.ts +13 -0
- package/dist/dashboard/constant.jsx +13 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.jsx +319 -0
- package/dist/dashboard/payment-table.d.ts +8 -0
- package/dist/dashboard/payment-table.jsx +85 -0
- package/dist/db/index.d.ts +15 -0
- package/dist/db/index.js +174 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +4 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.js +20 -0
- package/package.json +60 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TokenSymbol as Symbol } from "../types";
|
|
2
|
+
import { ShadowWireClient, } from "@radr/shadowwire";
|
|
3
|
+
export class PrivacyWrapper {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.rpcUrl = this.config.rpcUrl;
|
|
7
|
+
this.client = new ShadowWireClient({
|
|
8
|
+
network: "mainnet-beta",
|
|
9
|
+
// apiBaseUrl: this.config.rpcUrl
|
|
10
|
+
debug: false,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async getPrivateBalance(walletAddress, symbol) {
|
|
14
|
+
return await this.client.getBalance(walletAddress, symbol);
|
|
15
|
+
}
|
|
16
|
+
async withdraw({ symbol, recipient }) {
|
|
17
|
+
const balance = await this.client.getBalance(recipient, symbol);
|
|
18
|
+
if (balance.available === 0) {
|
|
19
|
+
console.log("No funds available");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const amount = symbol === Symbol.SOL
|
|
23
|
+
? (balance?.available ?? 0) / 1e9
|
|
24
|
+
: (balance?.available ?? 0) / 1e6;
|
|
25
|
+
return await this.client.withdraw({
|
|
26
|
+
amount,
|
|
27
|
+
wallet: recipient,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async transfer({ symbol, sender, amount, }) {
|
|
31
|
+
return await this.client.transfer({
|
|
32
|
+
amount,
|
|
33
|
+
recipient: this.config.marchantWalletADdress,
|
|
34
|
+
sender,
|
|
35
|
+
token: symbol,
|
|
36
|
+
type: "internal",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async createTransferTransaction(params) {
|
|
40
|
+
const result = await this.client.transfer({
|
|
41
|
+
sender: params.walletAddress,
|
|
42
|
+
recipient: this.config.marchantWalletADdress,
|
|
43
|
+
amount: params.amount,
|
|
44
|
+
token: params.symbol,
|
|
45
|
+
type: "external",
|
|
46
|
+
});
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
async createDepositTransaction(params) {
|
|
50
|
+
try {
|
|
51
|
+
let response;
|
|
52
|
+
console.log(params, "params");
|
|
53
|
+
if (params.symbol === "SOL") {
|
|
54
|
+
response = await this.client.deposit({
|
|
55
|
+
wallet: params.walletAddress,
|
|
56
|
+
amount: params.amount * 1000000000,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
response = await this.client.deposit({
|
|
61
|
+
wallet: params.walletAddress,
|
|
62
|
+
amount: params.amount * 1000000,
|
|
63
|
+
token_mint: params.tokenMint,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return response;
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
throw new Error(`Failed to create privacy transaction: ${e?.message ?? String(e)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
const ApiKeysSection = ({ apiKey, baseUrl }) => {
|
|
4
|
+
const [keys, setKeys] = useState([]);
|
|
5
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
6
|
+
const [newKeyData, setNewKeyData] = useState(null);
|
|
7
|
+
const fetchKeys = async () => {
|
|
8
|
+
setIsLoading(true);
|
|
9
|
+
try {
|
|
10
|
+
const headers = {};
|
|
11
|
+
if (apiKey)
|
|
12
|
+
headers['x-api-key'] = apiKey;
|
|
13
|
+
const res = await fetch(`${baseUrl}/api-keys`, { headers });
|
|
14
|
+
if (res.ok) {
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
setKeys(data);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('Failed to fetch API keys:', error);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
setIsLoading(false);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const createKey = async () => {
|
|
27
|
+
const name = prompt('Enter a name for the new API key:');
|
|
28
|
+
if (!name)
|
|
29
|
+
return;
|
|
30
|
+
setIsLoading(true);
|
|
31
|
+
try {
|
|
32
|
+
const headers = {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
};
|
|
35
|
+
if (apiKey)
|
|
36
|
+
headers['x-api-key'] = apiKey;
|
|
37
|
+
const res = await fetch(`${baseUrl}/api-keys`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({ name }),
|
|
41
|
+
});
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
const result = await res.json();
|
|
44
|
+
setNewKeyData({ key: result.key, name: result.apiKey.name });
|
|
45
|
+
fetchKeys();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Failed to create API key:', error);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const revokeKey = async (id, name) => {
|
|
56
|
+
if (!confirm(`Are you sure you want to revoke the key "${name}"? This action cannot be undone.`))
|
|
57
|
+
return;
|
|
58
|
+
setIsLoading(true);
|
|
59
|
+
try {
|
|
60
|
+
const headers = {};
|
|
61
|
+
if (apiKey)
|
|
62
|
+
headers['x-api-key'] = apiKey;
|
|
63
|
+
const res = await fetch(`${baseUrl}/api-keys/${id}`, {
|
|
64
|
+
method: 'DELETE',
|
|
65
|
+
headers
|
|
66
|
+
});
|
|
67
|
+
if (res.ok) {
|
|
68
|
+
fetchKeys();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Failed to revoke API key:', error);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
fetchKeys();
|
|
80
|
+
}, [apiKey, baseUrl]);
|
|
81
|
+
return (<div className="space-y-6">
|
|
82
|
+
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
|
83
|
+
<div>
|
|
84
|
+
<h3 className="text-xl font-bold">API Access Tokens</h3>
|
|
85
|
+
<p className="text-gray-500 text-sm">Generate keys to integrate PayGrid into your applications.</p>
|
|
86
|
+
</div>
|
|
87
|
+
<button onClick={createKey} disabled={isLoading} className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-xl text-sm font-semibold transition-colors disabled:opacity-50">
|
|
88
|
+
{isLoading ? 'Creating...' : '+ Create New Key'}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{newKeyData && (<div className="bg-emerald-500/10 border border-emerald-500/20 p-6 rounded-2xl relative">
|
|
93
|
+
<button onClick={() => setNewKeyData(null)} className="absolute top-4 right-4 text-gray-400 hover:text-white">
|
|
94
|
+
✕
|
|
95
|
+
</button>
|
|
96
|
+
<h4 className="text-emerald-400 font-semibold mb-2">New API Key Created!</h4>
|
|
97
|
+
<p className="text-sm text-gray-400 mb-4">Make sure to copy your API key now. You won't be able to see it again.</p>
|
|
98
|
+
<div className="flex items-center gap-2">
|
|
99
|
+
<pre className="bg-black/50 p-4 rounded-xl font-mono text-sm text-emerald-300 flex-1 overflow-x-auto">
|
|
100
|
+
{newKeyData.key}
|
|
101
|
+
</pre>
|
|
102
|
+
<button onClick={() => navigator.clipboard.writeText(newKeyData.key)} className="bg-white/5 hover:bg-white/10 p-4 rounded-xl transition-colors" title="Copy to clipboard">
|
|
103
|
+
📋
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>)}
|
|
107
|
+
|
|
108
|
+
<div className="grid gap-4">
|
|
109
|
+
{isLoading && keys.length === 0 ? (<div className="text-center py-12 text-gray-500">Loading keys...</div>) : keys.length === 0 ? (<div className="text-center py-12 text-gray-500 font-medium bg-[#111] border border-white/10 rounded-2xl">
|
|
110
|
+
No API keys found. Create one to get started.
|
|
111
|
+
</div>) : (keys.map(key => (<div key={key.id} className="bg-[#111] border border-white/10 p-4 md:p-6 rounded-2xl flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
|
112
|
+
<div className="flex items-center gap-4">
|
|
113
|
+
<div className="w-10 h-10 bg-white/5 rounded-full flex-shrink-0 flex items-center justify-center">
|
|
114
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#888" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 2-2 2.23"/><path d="M7 22 22.77 6.23a2 2 0 0 0 0-2.83l-.77-.77a2 2 0 0 0-2.83 0L3.41 18.41A2 2 0 0 0 3 19.83V22h2.17a2 2 0 0 0 1.42-.59Z"/><path d="m15 5 4 4"/></svg>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="min-w-0">
|
|
117
|
+
<h4 className="font-semibold truncate">{key.name}</h4>
|
|
118
|
+
<p className="text-xs font-mono text-gray-500 mt-1">{key.keyHint}••••••••••••••••</p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="flex items-center justify-between md:justify-end gap-6 border-t md:border-t-0 border-white/5 pt-4 md:pt-0">
|
|
122
|
+
<div className="text-left md:text-right">
|
|
123
|
+
<p className="text-[10px] text-gray-500 uppercase font-bold">Created</p>
|
|
124
|
+
<p className="text-sm">{new Date(key.createdAt).toLocaleDateString()}</p>
|
|
125
|
+
</div>
|
|
126
|
+
<button onClick={() => revokeKey(key.id, key.name)} className="text-red-400 hover:text-red-300 text-xs font-semibold p-2">
|
|
127
|
+
Revoke
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>)))}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="bg-indigo-500/5 border border-indigo-500/20 p-6 rounded-2xl">
|
|
134
|
+
<h4 className="text-indigo-400 font-semibold mb-2">Integration Guide</h4>
|
|
135
|
+
<p className="text-sm text-gray-400 mb-4">Initialize the SDK in your Next.js application using your API secret key.</p>
|
|
136
|
+
<pre className="bg-black/50 p-4 rounded-xl font-mono text-xs text-indigo-300 overflow-x-auto">
|
|
137
|
+
{`const paygrid = initPayGrid({
|
|
138
|
+
apiKey: process.env.PAYGRID_API_KEY,
|
|
139
|
+
rpcUrl: 'https://api.devnet.solana.com',
|
|
140
|
+
network: 'devnet'
|
|
141
|
+
});`}
|
|
142
|
+
</pre>
|
|
143
|
+
</div>
|
|
144
|
+
</div>);
|
|
145
|
+
};
|
|
146
|
+
export default ApiKeysSection;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export declare const SUPPORTED_TOKENS: {
|
|
3
|
+
symbol: string;
|
|
4
|
+
mint: string;
|
|
5
|
+
color: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare const Icons: {
|
|
8
|
+
Dashboard: () => React.JSX.Element;
|
|
9
|
+
Payments: () => React.JSX.Element;
|
|
10
|
+
Key: () => React.JSX.Element;
|
|
11
|
+
Search: () => React.JSX.Element;
|
|
12
|
+
External: () => React.JSX.Element;
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export const SUPPORTED_TOKENS = [
|
|
3
|
+
{ symbol: 'SOL', mint: '11111111111111111111111111111111', color: '#14F195' },
|
|
4
|
+
{ symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', color: '#2775CA' },
|
|
5
|
+
{ symbol: 'BONK', mint: 'DezXAZ8z7PnrnRJjz3wXBoRgixJ6WoPBw5DRF6S49t38', color: '#FFA500' }
|
|
6
|
+
];
|
|
7
|
+
export const Icons = {
|
|
8
|
+
Dashboard: () => (<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>),
|
|
9
|
+
Payments: () => (<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>),
|
|
10
|
+
Key: () => (<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21 2-2 2.23"/><path d="M7 22 22.77 6.23a2 2 0 0 0 0-2.83l-.77-.77a2 2 0 0 0-2.83 0L3.41 18.41A2 2 0 0 0 3 19.83V22h2.17a2 2 0 0 0 1.42-.59Z"/><path d="m15 5 4 4"/></svg>),
|
|
11
|
+
Search: () => (<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>),
|
|
12
|
+
External: () => (<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>)
|
|
13
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { initWASM, isWASMSupported, ShadowWireClient } from "@radr/shadowwire";
|
|
3
|
+
import { clsx } from "clsx";
|
|
4
|
+
import { LogOut, Menu, X } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts";
|
|
7
|
+
import { twMerge } from "tailwind-merge";
|
|
8
|
+
import { logout } from "../auth/actions";
|
|
9
|
+
import { LoginScreen } from "../auth/login-screen";
|
|
10
|
+
import ApiKeysSection from "./api-section";
|
|
11
|
+
import { Icons } from "./constant";
|
|
12
|
+
import PaymentsTable from "./payment-table";
|
|
13
|
+
function cn(...inputs) {
|
|
14
|
+
return twMerge(clsx(inputs));
|
|
15
|
+
}
|
|
16
|
+
export function PayGridDashboard({ baseUrl = "/api/paygrid", }) {
|
|
17
|
+
const [activeTab, setActiveTab] = useState("overview");
|
|
18
|
+
const [analytics, setAnalytics] = useState(null);
|
|
19
|
+
const [intents, setIntents] = useState([]);
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
+
const [isAnalyticsLoading, setIsAnalyticsLoading] = useState(false);
|
|
22
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
23
|
+
const [timeframe, setTimeframe] = useState(30);
|
|
24
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
25
|
+
const [isAdmin, setIsAdmin] = useState(null);
|
|
26
|
+
const apiKey = process.env.NEXT_PUBLIC_PAYGRID_API_SECRET;
|
|
27
|
+
const merchantAddress = process.env.NEXT_PUBLIC_MERCHANT_WALLET_ADDRESS;
|
|
28
|
+
const [client] = useState(() => new ShadowWireClient());
|
|
29
|
+
const [wasmInitialized, setWasmInitialized] = useState(false);
|
|
30
|
+
const [balance, setBalance] = useState(null);
|
|
31
|
+
const fetchAnalytics = async (showLoading = false) => {
|
|
32
|
+
if (showLoading)
|
|
33
|
+
setIsAnalyticsLoading(true);
|
|
34
|
+
try {
|
|
35
|
+
const headers = {};
|
|
36
|
+
if (apiKey)
|
|
37
|
+
headers["x-api-key"] = apiKey;
|
|
38
|
+
const res = await fetch(`${baseUrl}/analytics?days=${timeframe}`, {
|
|
39
|
+
headers,
|
|
40
|
+
});
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
const stats = await res.json();
|
|
43
|
+
setAnalytics(stats);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error("Failed to fetch analytics:", error);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (showLoading)
|
|
51
|
+
setIsAnalyticsLoading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const fetchPayments = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const headers = {};
|
|
57
|
+
if (apiKey)
|
|
58
|
+
headers["x-api-key"] = apiKey;
|
|
59
|
+
const res = await fetch(`${baseUrl}/payments`, { headers });
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
const list = await res.json();
|
|
62
|
+
setIntents(list);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error("Failed to fetch payments:", error);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
async function init() {
|
|
71
|
+
if (!isWASMSupported()) {
|
|
72
|
+
// setError('WebAssembly not supported');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await initWASM("/wasm/settler_wasm_bg.wasm");
|
|
77
|
+
setWasmInitialized(true);
|
|
78
|
+
await loadBalance();
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// setError('Initialization failed: ' + err.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
init();
|
|
85
|
+
}, []);
|
|
86
|
+
console.log(balance, "wallet balance");
|
|
87
|
+
const loadBalance = async () => {
|
|
88
|
+
try {
|
|
89
|
+
const data = await client.getBalance(merchantAddress ?? "", "USDC");
|
|
90
|
+
setBalance(data.available / 1e9);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error("Balance load failed:", err);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
// Initial load for payments
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setIsLoading(true);
|
|
99
|
+
fetchPayments().then(() => setIsLoading(false));
|
|
100
|
+
}, [apiKey, baseUrl]);
|
|
101
|
+
// Analytics load (including timeframe changes)
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
fetchAnalytics(true);
|
|
104
|
+
// Check admin session cookie on client side
|
|
105
|
+
const hasSession = document.cookie.includes("paygrid_admin_session=true");
|
|
106
|
+
setIsAdmin(hasSession);
|
|
107
|
+
}, [apiKey, baseUrl, timeframe]);
|
|
108
|
+
const filteredIntents = intents.filter((intent) => {
|
|
109
|
+
const query = searchQuery.toLowerCase();
|
|
110
|
+
return (intent.id.toLowerCase().includes(query) ||
|
|
111
|
+
intent.status.toLowerCase().includes(query) ||
|
|
112
|
+
intent.amount.toString().includes(query) ||
|
|
113
|
+
(intent.walletAddress &&
|
|
114
|
+
intent.walletAddress.toLowerCase().includes(query)));
|
|
115
|
+
});
|
|
116
|
+
const renderContent = () => {
|
|
117
|
+
switch (activeTab) {
|
|
118
|
+
case "overview":
|
|
119
|
+
return (<div className="space-y-6">
|
|
120
|
+
<div className="grid grid-cols-3 md:grid-cols-3 gap-2 md:gap-6">
|
|
121
|
+
<div className="bg-[#111] border border-white/10 p-6 rounded-2xl relative overflow-hidden">
|
|
122
|
+
{isAnalyticsLoading && (<div className="absolute inset-0 bg-black/20 backdrop-blur-[1px] flex items-center justify-center z-10">
|
|
123
|
+
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
|
|
124
|
+
</div>)}
|
|
125
|
+
<p className="text-gray-400 text-sm font-medium">
|
|
126
|
+
Total Volume
|
|
127
|
+
</p>
|
|
128
|
+
<h3 className="text-3xl font-bold mt-1 text-white">
|
|
129
|
+
${analytics?.totalRevenue.toFixed(2)}
|
|
130
|
+
</h3>
|
|
131
|
+
<p className={cn("text-xs mt-2", (analytics?.revenueGrowth || 0) >= 0
|
|
132
|
+
? "text-emerald-400"
|
|
133
|
+
: "text-red-400")}>
|
|
134
|
+
{(analytics?.revenueGrowth || 0) >= 0 ? "↑" : "↓"}{" "}
|
|
135
|
+
{Math.abs(analytics?.revenueGrowth || 0).toFixed(1)}% from
|
|
136
|
+
last {timeframe === 30 ? "month" : "week"}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="bg-[#111] border border-white/10 p-6 rounded-2xl relative overflow-hidden">
|
|
140
|
+
{isAnalyticsLoading && (<div className="absolute inset-0 bg-black/20 backdrop-blur-[1px] flex items-center justify-center z-10">
|
|
141
|
+
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
|
|
142
|
+
</div>)}
|
|
143
|
+
<p className="text-gray-400 text-sm font-medium">
|
|
144
|
+
Payment Intents
|
|
145
|
+
</p>
|
|
146
|
+
<h3 className="text-3xl font-bold mt-1 text-white">
|
|
147
|
+
{analytics?.transactionCount}
|
|
148
|
+
</h3>
|
|
149
|
+
<p className="text-gray-500 text-xs mt-2">
|
|
150
|
+
Past {timeframe} days
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="bg-[#111] border border-white/10 p-6 rounded-2xl relative overflow-hidden">
|
|
154
|
+
{isAnalyticsLoading && (<div className="absolute inset-0 bg-black/20 backdrop-blur-[1px] flex items-center justify-center z-10">
|
|
155
|
+
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
|
|
156
|
+
</div>)}
|
|
157
|
+
<p className="text-gray-400 text-sm font-medium">
|
|
158
|
+
Settlement Rate
|
|
159
|
+
</p>
|
|
160
|
+
<h3 className="text-3xl font-bold mt-1 text-white">
|
|
161
|
+
{analytics?.settlementRate.toFixed(1)}%
|
|
162
|
+
</h3>
|
|
163
|
+
<p className="text-emerald-400 text-xs mt-2">
|
|
164
|
+
High performance
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div className="bg-[#111] border border-white/10 p-6 rounded-2xl h-[400px] relative overflow-hidden">
|
|
170
|
+
{isAnalyticsLoading && (<div className="absolute inset-0 bg-black/20 backdrop-blur-[1px] flex items-center justify-center z-10">
|
|
171
|
+
<div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
|
|
172
|
+
</div>)}
|
|
173
|
+
<div className="flex justify-between items-center mb-6">
|
|
174
|
+
<h3 className="font-semibold text-lg">Revenue Over Time</h3>
|
|
175
|
+
<div className="flex gap-2">
|
|
176
|
+
<button onClick={() => setTimeframe(7)} className={cn("text-xs px-3 py-1 rounded-full transition-all border", timeframe === 7
|
|
177
|
+
? "bg-indigo-500/20 border-indigo-500/30 text-indigo-400"
|
|
178
|
+
: "bg-white/5 border-white/10 text-gray-400 hover:bg-white/10")}>
|
|
179
|
+
7D
|
|
180
|
+
</button>
|
|
181
|
+
<button onClick={() => setTimeframe(30)} className={cn("text-xs px-3 py-1 rounded-full transition-all border", timeframe === 30
|
|
182
|
+
? "bg-indigo-500/20 border-indigo-500/30 text-indigo-400"
|
|
183
|
+
: "bg-white/5 border-white/10 text-gray-400 hover:bg-white/10")}>
|
|
184
|
+
30D
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<ResponsiveContainer width="100%" height="80%" minWidth={0}>
|
|
189
|
+
<AreaChart data={analytics?.history || []}>
|
|
190
|
+
<defs>
|
|
191
|
+
<linearGradient id="colorAmt" x1="0" y1="0" x2="0" y2="1">
|
|
192
|
+
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3}/>
|
|
193
|
+
<stop offset="95%" stopColor="#6366f1" stopOpacity={0}/>
|
|
194
|
+
</linearGradient>
|
|
195
|
+
</defs>
|
|
196
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#222" vertical={false}/>
|
|
197
|
+
<XAxis dataKey="date" stroke="#444" fontSize={12} tickLine={false} axisLine={false}/>
|
|
198
|
+
<YAxis stroke="#444" fontSize={12} tickLine={false} axisLine={false}/>
|
|
199
|
+
<Tooltip contentStyle={{
|
|
200
|
+
backgroundColor: "#111",
|
|
201
|
+
border: "1px solid #333",
|
|
202
|
+
borderRadius: "8px",
|
|
203
|
+
}} itemStyle={{ color: "#fff" }}/>
|
|
204
|
+
<Area type="monotone" dataKey="amount" stroke="#6366f1" fillOpacity={1} fill="url(#colorAmt)" strokeWidth={2}/>
|
|
205
|
+
</AreaChart>
|
|
206
|
+
</ResponsiveContainer>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="bg-[#111] border border-white/10 rounded-2xl overflow-hidden">
|
|
210
|
+
<div className="px-6 py-4 border-b border-white/10 flex justify-between items-center">
|
|
211
|
+
<h3 className="font-semibold">Recent Activity</h3>
|
|
212
|
+
<button onClick={() => setActiveTab("payments")} className="text-xs text-indigo-400 hover:text-indigo-300">
|
|
213
|
+
View all
|
|
214
|
+
</button>
|
|
215
|
+
</div>
|
|
216
|
+
<PaymentsTable intents={filteredIntents.slice(0, 5)}/>
|
|
217
|
+
</div>
|
|
218
|
+
</div>);
|
|
219
|
+
case "payments":
|
|
220
|
+
return <PaymentsTable intents={filteredIntents} isFullPage/>;
|
|
221
|
+
case "apikeys":
|
|
222
|
+
return <ApiKeysSection apiKey={apiKey} baseUrl={baseUrl}/>;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
if (isAdmin === null) {
|
|
226
|
+
return null; // or loading spinner
|
|
227
|
+
}
|
|
228
|
+
if (!isAdmin) {
|
|
229
|
+
return <LoginScreen />;
|
|
230
|
+
}
|
|
231
|
+
return (<div className="flex min-h-screen">
|
|
232
|
+
{/* Sidebar Overlay */}
|
|
233
|
+
{isSidebarOpen && (<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-30 lg:hidden" onClick={() => setIsSidebarOpen(false)}/>)}
|
|
234
|
+
|
|
235
|
+
{/* Sidebar */}
|
|
236
|
+
<aside className={cn("w-64 border-r border-white/10 flex flex-col fixed h-full bg-[#050505] z-40 transition-transform duration-300 ease-in-out", isSidebarOpen
|
|
237
|
+
? "translate-x-0"
|
|
238
|
+
: "-translate-x-full lg:translate-x-0")}>
|
|
239
|
+
<div className="p-6">
|
|
240
|
+
<div className="flex items-center justify-between mb-8">
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center font-bold text-lg">
|
|
243
|
+
P
|
|
244
|
+
</div>
|
|
245
|
+
<span className="font-bold text-xl tracking-tight">PayGrid</span>
|
|
246
|
+
</div>
|
|
247
|
+
<button className="lg:hidden text-gray-400 hover:text-white" onClick={() => setIsSidebarOpen(false)}>
|
|
248
|
+
<X size={20}/>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<nav className="space-y-1">
|
|
253
|
+
<button onClick={() => {
|
|
254
|
+
setActiveTab("overview");
|
|
255
|
+
setIsSidebarOpen(false);
|
|
256
|
+
}} className={`w-full cursor-pointer flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${activeTab === "overview" ? "bg-white/10 text-white" : "text-gray-400 hover:text-white hover:bg-white/5"}`}>
|
|
257
|
+
<Icons.Dashboard /> Overview
|
|
258
|
+
</button>
|
|
259
|
+
<button onClick={() => {
|
|
260
|
+
setActiveTab("payments");
|
|
261
|
+
setIsSidebarOpen(false);
|
|
262
|
+
}} className={`w-full cursor-pointer flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${activeTab === "payments" ? "bg-white/10 text-white" : "text-gray-400 hover:text-white hover:bg-white/5"}`}>
|
|
263
|
+
<Icons.Payments /> Payments
|
|
264
|
+
</button>
|
|
265
|
+
<button onClick={() => {
|
|
266
|
+
setActiveTab("apikeys");
|
|
267
|
+
setIsSidebarOpen(false);
|
|
268
|
+
}} className={`w-full cursor-pointer flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${activeTab === "apikeys" ? "bg-white/10 text-white" : "text-gray-400 hover:text-white hover:bg-white/5"}`}>
|
|
269
|
+
<Icons.Key /> API Keys
|
|
270
|
+
</button>
|
|
271
|
+
</nav>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="mt-auto p-6 border-t border-white/10">
|
|
275
|
+
<div className="flex items-center gap-3">
|
|
276
|
+
<img src="https://picsum.photos/32/32" className="w-8 h-8 rounded-full" alt="User"/>
|
|
277
|
+
<div className="flex flex-col">
|
|
278
|
+
<span className="text-sm font-medium">Merchant Account</span>
|
|
279
|
+
<span className="text-xs text-gray-500">Live Mode</span>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
{<button onClick={logout} className="w-full cursor-pointer flex items-center gap-2 mt-4 px-2 py-2 text-red-400 hover:text-red-300 hover:bg-white/5 rounded-lg transition-all text-sm font-medium">
|
|
283
|
+
<LogOut size={16}/> Sign Out
|
|
284
|
+
</button>}
|
|
285
|
+
</div>
|
|
286
|
+
</aside>
|
|
287
|
+
|
|
288
|
+
{/* Main Content */}
|
|
289
|
+
<main className="flex-1 lg:ml-64 p-4 md:p-8 pt-20 lg:pt-8 w-full bg-[#050505] overflow-hidden">
|
|
290
|
+
<header className="flex flex-col md:flex-row md:justify-between md:items-center mb-8 gap-4">
|
|
291
|
+
<div className="flex items-center gap-4">
|
|
292
|
+
<button className="lg:hidden p-2 bg-[#111] border border-white/10 rounded-lg text-gray-400" onClick={() => setIsSidebarOpen(true)}>
|
|
293
|
+
<Menu size={20}/>
|
|
294
|
+
</button>
|
|
295
|
+
<div>
|
|
296
|
+
<h1 className="text-xl md:text-2xl font-bold capitalize">
|
|
297
|
+
{activeTab}
|
|
298
|
+
</h1>
|
|
299
|
+
<p className="text-gray-500 text-xs md:text-sm">
|
|
300
|
+
Manage your Solana payments infrastructure
|
|
301
|
+
</p>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div className="flex gap-3 w-full md:w-auto">
|
|
305
|
+
<div className="relative w-full md:w-auto">
|
|
306
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500">
|
|
307
|
+
<Icons.Search />
|
|
308
|
+
</div>
|
|
309
|
+
<input type="text" placeholder="Search intents..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full bg-[#111] border border-white/10 rounded-xl pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/50"/>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</header>
|
|
313
|
+
|
|
314
|
+
{isLoading ? (<div className="flex items-center justify-center h-64">
|
|
315
|
+
<div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
|
|
316
|
+
</div>) : (renderContent())}
|
|
317
|
+
</main>
|
|
318
|
+
</div>);
|
|
319
|
+
}
|