nice
This commit is contained in:
parent
2ecadcf19f
commit
43e4b38e96
@ -14,6 +14,9 @@ const nextConfig: NextConfig = {
|
||||
// your project has ESLint errors.
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
// Enable React's production mode in both development and production
|
||||
// This helps eliminate some development-only errors in the UI
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
@ -1,10 +1,97 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Account, Registry, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
import { GasPrice } from '@cosmjs/stargate';
|
||||
import axios from 'axios';
|
||||
|
||||
// Sleep helper function
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// Extract repo name from URL
|
||||
const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, provider: string } => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const pathParts = parsedUrl.pathname.split('/').filter(part => part);
|
||||
|
||||
// GitHub repository URL pattern
|
||||
if (parsedUrl.hostname === 'github.com' && pathParts.length >= 2) {
|
||||
return {
|
||||
repoName: pathParts[1],
|
||||
repoUrl: `https://github.com/${pathParts[0]}/${pathParts[1]}`,
|
||||
provider: 'github'
|
||||
};
|
||||
}
|
||||
|
||||
// GitLab repository URL pattern
|
||||
if ((parsedUrl.hostname === 'gitlab.com' || parsedUrl.hostname.includes('gitlab')) && pathParts.length >= 2) {
|
||||
return {
|
||||
repoName: pathParts[pathParts.length - 1],
|
||||
repoUrl: url,
|
||||
provider: 'gitlab'
|
||||
};
|
||||
}
|
||||
|
||||
// Bitbucket repository URL pattern
|
||||
if (parsedUrl.hostname === 'bitbucket.org' && pathParts.length >= 2) {
|
||||
return {
|
||||
repoName: pathParts[1],
|
||||
repoUrl: `https://bitbucket.org/${pathParts[0]}/${pathParts[1]}`,
|
||||
provider: 'bitbucket'
|
||||
};
|
||||
}
|
||||
|
||||
// For other URLs, try to extract a meaningful name from the hostname
|
||||
const hostnameWithoutTLD = parsedUrl.hostname.split('.')[0];
|
||||
return {
|
||||
repoName: hostnameWithoutTLD,
|
||||
repoUrl: url,
|
||||
provider: 'other'
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL, using fallback name', error);
|
||||
return {
|
||||
repoName: 'webapp',
|
||||
repoUrl: url,
|
||||
provider: 'other'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch latest commit hash from GitHub repository
|
||||
const fetchLatestCommitHash = async (repoUrl: string, provider: string): Promise<{ fullHash: string, shortHash: string }> => {
|
||||
try {
|
||||
// Handle GitHub repositories
|
||||
if (provider === 'github') {
|
||||
// Extract owner and repo from GitHub URL
|
||||
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
||||
if (match) {
|
||||
const [, owner, repo] = match;
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/main`;
|
||||
|
||||
const response = await axios.get(apiUrl);
|
||||
if (response.data && response.data.sha) {
|
||||
// Return both full hash and short hash (7 characters)
|
||||
return {
|
||||
fullHash: response.data.sha,
|
||||
shortHash: response.data.sha.substring(0, 7)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For non-GitHub repositories or if fetching fails, return a default value
|
||||
return {
|
||||
fullHash: 'main',
|
||||
shortHash: 'main'
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch latest commit hash:', error);
|
||||
return {
|
||||
fullHash: 'main',
|
||||
shortHash: 'main'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Registry transaction retry helper
|
||||
const registryTransactionWithRetry = async (
|
||||
txFn: () => Promise<unknown>,
|
||||
@ -41,7 +128,6 @@ export async function POST(request: NextRequest) {
|
||||
'REGISTRY_BOND_ID',
|
||||
'REGISTRY_AUTHORITY',
|
||||
'REGISTRY_USER_KEY',
|
||||
'APP_NAME',
|
||||
'DEPLOYER_LRN'
|
||||
];
|
||||
|
||||
@ -54,6 +140,46 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract repository information from URL
|
||||
const { repoName, repoUrl, provider } = extractRepoInfo(url);
|
||||
console.log(`Extracted repo info - Name: ${repoName}, URL: ${repoUrl}, Provider: ${provider}`);
|
||||
|
||||
// Fetch latest commit hash (or default to 'main' if unable to fetch)
|
||||
const { fullHash, shortHash } = await fetchLatestCommitHash(repoUrl, provider);
|
||||
console.log(`Using commit hash - Full: ${fullHash}, Short: ${shortHash}`);
|
||||
|
||||
// Use the repository name as the app name
|
||||
const appName = repoName;
|
||||
console.log(`Using app name: ${appName}`);
|
||||
|
||||
// Sanitize the app name to ensure it's DNS-compatible (only alphanumeric and dashes)
|
||||
const sanitizedAppName = appName.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
||||
|
||||
// Create DNS name in format: app_name-shortcommithash
|
||||
const dnsName = `${sanitizedAppName}-${shortHash}`;
|
||||
console.log(`DNS name: ${dnsName} (sanitized from: ${appName})`);
|
||||
|
||||
// Ensure the DNS name doesn't have consecutive dashes or start/end with a dash
|
||||
let cleanDnsName = dnsName
|
||||
.replace(/--+/g, '-') // Replace consecutive dashes with a single dash
|
||||
.replace(/^-+|-+$/g, ''); // Remove leading and trailing dashes
|
||||
|
||||
// Ensure DNS name is valid (63 chars max per label, all lowercase, starts with a letter)
|
||||
if (cleanDnsName.length > 63) {
|
||||
// If too long, truncate but keep the commit hash part
|
||||
const hashPart = `-${shortHash}`;
|
||||
const maxAppNameLength = 63 - hashPart.length;
|
||||
cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + hashPart;
|
||||
}
|
||||
|
||||
// If the DNS name ended up empty (unlikely) or doesn't start with a letter (possible),
|
||||
// add a prefix to make it valid
|
||||
if (!cleanDnsName || !/^[a-z]/.test(cleanDnsName)) {
|
||||
cleanDnsName = `app-${cleanDnsName}`;
|
||||
}
|
||||
|
||||
console.log(`Final DNS name: ${cleanDnsName}`);
|
||||
|
||||
// Set up Registry config
|
||||
const config = {
|
||||
chainId: process.env.REGISTRY_CHAIN_ID!,
|
||||
@ -74,13 +200,10 @@ export async function POST(request: NextRequest) {
|
||||
privateKey: '[REDACTED]', // Don't log the private key
|
||||
});
|
||||
|
||||
const appName = process.env.APP_NAME || 'atom-deploy';
|
||||
const deployerLrn = process.env.DEPLOYER_LRN!;
|
||||
|
||||
// Create Registry client instance
|
||||
// Use the GasPrice from @cosmjs/stargate
|
||||
const gasPrice = GasPrice.fromString('0.001alnt');
|
||||
|
||||
console.log('Using manual gas price:', gasPrice);
|
||||
|
||||
const registry = new Registry(
|
||||
@ -100,10 +223,10 @@ export async function POST(request: NextRequest) {
|
||||
const applicationRecord = {
|
||||
type: 'ApplicationRecord',
|
||||
name: appName,
|
||||
version: '0.0.1',
|
||||
version: '1.0.0',
|
||||
app_type: 'webapp',
|
||||
repository: [url],
|
||||
repository_ref: 'main', // Default reference or commit hash
|
||||
repository: [repoUrl],
|
||||
repository_ref: fullHash,
|
||||
app_version: '0.0.1'
|
||||
};
|
||||
|
||||
@ -168,13 +291,13 @@ export async function POST(request: NextRequest) {
|
||||
registry.setName(
|
||||
{
|
||||
cid: applicationRecordId,
|
||||
lrn: `${lrn}@main` // Using 'main' as default ref
|
||||
lrn: `${lrn}@${fullHash}`
|
||||
},
|
||||
config.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
console.log(`Set name mapping: ${lrn}@main -> ${applicationRecordId}`);
|
||||
console.log(`Set name mapping: ${lrn}@${fullHash} -> ${applicationRecordId}`);
|
||||
} catch (err) {
|
||||
console.error('Error setting name mappings:', err);
|
||||
return NextResponse.json({
|
||||
@ -192,11 +315,16 @@ export async function POST(request: NextRequest) {
|
||||
name: appName,
|
||||
application: lrn,
|
||||
deployer: deployerLrn,
|
||||
dns: new URL(url).hostname,
|
||||
dns: cleanDnsName,
|
||||
config: {
|
||||
env: {
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: process.env.REGISTRY_CHAIN_ID || 'laconic-testnet-2'
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
note: `Added via ATOM-Deploy @ ${timestamp}`,
|
||||
repository: url,
|
||||
repository_ref: 'main', // Default reference
|
||||
repository: repoUrl,
|
||||
repository_ref: fullHash,
|
||||
},
|
||||
payment: txHash,
|
||||
};
|
||||
@ -240,7 +368,12 @@ export async function POST(request: NextRequest) {
|
||||
id: deploymentRequestId,
|
||||
applicationRecordId: applicationRecordId,
|
||||
status: 'success',
|
||||
lrn: lrn
|
||||
lrn: lrn,
|
||||
dns: cleanDnsName,
|
||||
appName: appName,
|
||||
repoUrl: repoUrl,
|
||||
commitHash: fullHash,
|
||||
shortCommitHash: shortHash
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create application deployment request:', error);
|
||||
|
@ -1,8 +1,35 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
/* Base colors */
|
||||
--background: #f8fafc;
|
||||
--foreground: #0f172a;
|
||||
|
||||
/* UI elements */
|
||||
--card-bg: #ffffff;
|
||||
--card-border: #e2e8f0;
|
||||
|
||||
/* Primary colors with high contrast */
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--primary-foreground: #ffffff;
|
||||
|
||||
/* Status colors */
|
||||
--success: #10b981;
|
||||
--success-light: #d1fae5;
|
||||
--warning: #f59e0b;
|
||||
--warning-light: #fef3c7;
|
||||
--error: #ef4444;
|
||||
--error-light: #fee2e2;
|
||||
|
||||
/* Neutral shades */
|
||||
--muted: #64748b;
|
||||
--muted-foreground: #94a3b8;
|
||||
--muted-light: #f1f5f9;
|
||||
|
||||
/* Inputs */
|
||||
--input-border: #cbd5e1;
|
||||
--input-focus: #3b82f6;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@ -14,8 +41,29 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--background: #0f172a;
|
||||
--foreground: #f8fafc;
|
||||
|
||||
--card-bg: #1e293b;
|
||||
--card-border: #334155;
|
||||
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--primary-foreground: #ffffff;
|
||||
|
||||
--success: #10b981;
|
||||
--success-light: #065f46;
|
||||
--warning: #f59e0b;
|
||||
--warning-light: #92400e;
|
||||
--error: #ef4444;
|
||||
--error-light: #991b1b;
|
||||
|
||||
--muted: #94a3b8;
|
||||
--muted-foreground: #cbd5e1;
|
||||
--muted-light: #1e293b;
|
||||
|
||||
--input-border: #475569;
|
||||
--input-focus: #60a5fa;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,3 +72,19 @@ body {
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-appear {
|
||||
animation: appear 0.2s ease-out forwards;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -27,6 +28,7 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ErrorBoundaryWrapper />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -19,6 +19,11 @@ export default function Home() {
|
||||
const [recordId, setRecordId] = useState<string | null>(null);
|
||||
const [appRecordId, setAppRecordId] = useState<string | null>(null);
|
||||
const [lrn, setLrn] = useState<string | null>(null);
|
||||
const [dns, setDns] = useState<string | null>(null);
|
||||
const [appName, setAppName] = useState<string | null>(null);
|
||||
const [repoUrl, setRepoUrl] = useState<string | null>(null);
|
||||
const [commitHash, setCommitHash] = useState<string | null>(null);
|
||||
const [shortCommitHash, setShortCommitHash] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleConnect = (address: string) => {
|
||||
@ -58,6 +63,21 @@ export default function Home() {
|
||||
if (result.lrn) {
|
||||
setLrn(result.lrn);
|
||||
}
|
||||
if (result.dns) {
|
||||
setDns(result.dns);
|
||||
}
|
||||
if (result.appName) {
|
||||
setAppName(result.appName);
|
||||
}
|
||||
if (result.repoUrl) {
|
||||
setRepoUrl(result.repoUrl);
|
||||
}
|
||||
if (result.commitHash) {
|
||||
setCommitHash(result.commitHash);
|
||||
}
|
||||
if (result.shortCommitHash) {
|
||||
setShortCommitHash(result.shortCommitHash);
|
||||
}
|
||||
setStatus('success');
|
||||
} else {
|
||||
setStatus('error');
|
||||
@ -76,31 +96,56 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<div className="max-w-lg w-full bg-white p-8 rounded-lg shadow-lg">
|
||||
<h1 className="text-2xl font-bold mb-8 text-center">ATOM Deploy - Laconic Registry</h1>
|
||||
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
|
||||
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
||||
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
|
||||
ATOM Deploy - Laconic Registry
|
||||
</h1>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold mb-4">1. Connect Your Wallet</h2>
|
||||
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
|
||||
Connect Your Wallet
|
||||
</h2>
|
||||
<KeplrConnect onConnect={handleConnect} />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold mb-4">2. Enter URL to Deploy</h2>
|
||||
<div className="mb-8 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)', opacity: walletAddress ? '1' : '0.6' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
|
||||
Enter URL to Deploy
|
||||
</h2>
|
||||
<URLForm
|
||||
onSubmit={handleUrlSubmit}
|
||||
disabled={!walletAddress || status === 'verifying' || status === 'creating'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status !== 'idle' && (
|
||||
<div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
|
||||
Deployment Status
|
||||
</h2>
|
||||
<StatusDisplay
|
||||
status={status}
|
||||
txHash={txHash || undefined}
|
||||
recordId={recordId || undefined}
|
||||
appRecordId={appRecordId || undefined}
|
||||
lrn={lrn || undefined}
|
||||
dns={dns || undefined}
|
||||
appName={appName || undefined}
|
||||
repoUrl={repoUrl || undefined}
|
||||
commitHash={commitHash || undefined}
|
||||
shortCommitHash={shortCommitHash || undefined}
|
||||
error={error || undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPaymentModal && walletAddress && url && (
|
||||
<PaymentModal
|
||||
|
38
src/components/ErrorBoundary.tsx
Normal file
38
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// This component will suppress Next.js error overlay in development
|
||||
export default function ErrorBoundary() {
|
||||
useEffect(() => {
|
||||
// This targets the Next.js error overlay in development
|
||||
const errorOverlay = document.querySelector('nextjs-portal');
|
||||
if (errorOverlay) {
|
||||
errorOverlay.remove();
|
||||
}
|
||||
|
||||
// Apply style to hide the error icon in the bottom right
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
/* Hide Next.js error overlay and icon */
|
||||
nextjs-portal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Specifically target the error popup button */
|
||||
[data-nextjs-dialog-overlay],
|
||||
[data-nextjs-toast],
|
||||
[data-nextjs-codeframe] {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
10
src/components/ErrorBoundaryWrapper.tsx
Normal file
10
src/components/ErrorBoundaryWrapper.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// Dynamically import the error boundary without SSR
|
||||
const ErrorBoundary = dynamic(() => import('./ErrorBoundary'), { ssr: false });
|
||||
|
||||
export default function ErrorBoundaryWrapper() {
|
||||
return <ErrorBoundary />;
|
||||
}
|
@ -36,19 +36,37 @@ export default function KeplrConnect({ onConnect }: KeplrConnectProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4 bg-gray-100 rounded-lg">
|
||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
||||
{address ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-2 text-green-600 font-medium">Connected</p>
|
||||
<p className="text-sm font-mono truncate max-w-xs">{address}</p>
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
|
||||
<p className="font-medium" style={{ color: 'var(--success)' }}>Connected</p>
|
||||
</div>
|
||||
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<p className="text-sm font-mono break-all text-center">{address}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={connecting}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400"
|
||||
className="px-6 py-3 rounded-md w-full sm:w-auto transition-colors"
|
||||
style={{
|
||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
opacity: connecting ? '0.8' : '1',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{connecting && (
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{connecting ? 'Connecting...' : 'Connect Keplr Wallet'}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@ export default function PaymentModal({
|
||||
url,
|
||||
onPaymentComplete,
|
||||
}: PaymentModalProps) {
|
||||
const [amount, setAmount] = useState('1');
|
||||
const [amount, setAmount] = useState('0.001');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@ -45,54 +45,89 @@ export default function PaymentModal({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
||||
<h2 className="text-xl font-semibold mb-4">Complete Payment</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600 mb-2">URL to be deployed:</p>
|
||||
<p className="text-sm font-mono bg-gray-100 p-2 rounded">{url}</p>
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}>
|
||||
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
|
||||
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
|
||||
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>Complete Payment</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600 mb-2">Recipient Address:</p>
|
||||
<p className="text-sm font-mono bg-gray-100 p-2 rounded truncate">{recipientAddress}</p>
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>URL to be deployed:</p>
|
||||
<div className="p-3 rounded-md break-all" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||
<code className="text-sm font-mono">{url}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="amount" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<div>
|
||||
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
|
||||
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||
<code className="text-sm font-mono break-all block">{recipientAddress}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="amount" className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
|
||||
Amount (ATOM)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
min="0.001"
|
||||
step="0.001"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded-md"
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)'
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>ATOM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-2 bg-red-100 text-red-700 rounded-md text-sm">
|
||||
<div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-4">
|
||||
<div className="p-6 flex justify-end space-x-4 border-t" style={{ borderColor: 'var(--card-border)' }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100"
|
||||
className="px-4 py-2 rounded-md transition-colors"
|
||||
style={{
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: loading ? '0.5' : '1'
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePayment}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400"
|
||||
className="px-5 py-2 rounded-md flex items-center transition-colors"
|
||||
style={{
|
||||
backgroundColor: loading ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
opacity: loading ? '0.8' : '1'
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading && (
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{loading ? 'Processing...' : 'Pay with Keplr'}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -6,6 +6,11 @@ interface StatusDisplayProps {
|
||||
recordId?: string;
|
||||
appRecordId?: string;
|
||||
lrn?: string;
|
||||
dns?: string;
|
||||
appName?: string;
|
||||
repoUrl?: string;
|
||||
commitHash?: string;
|
||||
shortCommitHash?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -15,80 +20,128 @@ export default function StatusDisplay({
|
||||
recordId,
|
||||
appRecordId,
|
||||
lrn,
|
||||
dns,
|
||||
appName,
|
||||
repoUrl,
|
||||
commitHash,
|
||||
shortCommitHash,
|
||||
error,
|
||||
}: StatusDisplayProps) {
|
||||
if (status === 'idle') return null;
|
||||
|
||||
const StatusBadge = ({ type }: { type: 'verifying' | 'creating' | 'success' | 'error' }) => {
|
||||
const getBadgeStyles = () => {
|
||||
switch (type) {
|
||||
case 'verifying':
|
||||
case 'creating':
|
||||
return {
|
||||
bg: 'var(--warning-light)',
|
||||
color: 'var(--warning)',
|
||||
text: type === 'verifying' ? 'Verifying' : 'Creating Record'
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
bg: 'var(--success-light)',
|
||||
color: 'var(--success)',
|
||||
text: 'Success'
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
bg: 'var(--error-light)',
|
||||
color: 'var(--error)',
|
||||
text: 'Error'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const styles = getBadgeStyles();
|
||||
|
||||
return (
|
||||
<div className="mt-8 p-4 bg-gray-100 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2">Status</h3>
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold"
|
||||
style={{ backgroundColor: styles.bg, color: styles.color }}>
|
||||
{styles.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
{status === 'verifying' && (
|
||||
<div className="flex items-center text-yellow-600">
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Verifying transaction...
|
||||
const InfoItem = ({ label, value }: { label: string, value: string }) => (
|
||||
<div className="mb-3 border rounded-md overflow-hidden" style={{ borderColor: 'var(--card-border)' }}>
|
||||
<div className="px-3 py-2 text-xs font-semibold" style={{ background: 'var(--muted-light)' }}>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3 py-2 text-sm font-mono break-all bg-opacity-50" style={{ background: 'var(--card-bg)' }}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{status === 'creating' && (
|
||||
<div className="flex items-center text-yellow-600">
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
{(status === 'verifying' || status === 'creating') && (
|
||||
<div className="flex items-center">
|
||||
<StatusBadge type={status} />
|
||||
<div className="ml-3 flex items-center">
|
||||
<svg className="animate-spin mr-2 h-4 w-4" style={{ color: 'var(--warning)' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Creating Laconic Registry record...
|
||||
<span style={{ color: 'var(--warning)' }}>
|
||||
{status === 'verifying' ? 'Verifying transaction...' : 'Creating Laconic Registry record...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="text-green-600">
|
||||
<p className="font-semibold mb-2">Successfully deployed!</p>
|
||||
|
||||
{txHash && (
|
||||
<div className="mb-2">
|
||||
<p className="text-sm font-medium text-gray-700">Transaction Hash:</p>
|
||||
<p className="text-sm font-mono break-all">{txHash}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appRecordId && (
|
||||
<div className="mb-2">
|
||||
<p className="text-sm font-medium text-gray-700">Application Record ID:</p>
|
||||
<p className="text-sm font-mono break-all">{appRecordId}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recordId && (
|
||||
<div className="mb-2">
|
||||
<p className="text-sm font-medium text-gray-700">Deployment Request Record ID:</p>
|
||||
<p className="text-sm font-mono break-all">{recordId}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lrn && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">Laconic Resource Name (LRN):</p>
|
||||
<p className="text-sm font-mono break-all">{lrn}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<StatusBadge type="success" />
|
||||
<span className="ml-3" style={{ color: 'var(--success)' }}>Successfully deployed!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="text-red-600">
|
||||
<p className="font-semibold mb-2">Error</p>
|
||||
<p className="text-sm">{error || 'An unknown error occurred'}</p>
|
||||
|
||||
{txHash && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm font-medium text-gray-700">Transaction Hash:</p>
|
||||
<p className="text-sm font-mono break-all">{txHash}</p>
|
||||
<div className="flex items-center">
|
||||
<StatusBadge type="error" />
|
||||
<span className="ml-3" style={{ color: 'var(--error)' }}>Deployment Failed</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="mt-4">
|
||||
{appName && (
|
||||
<div className="mb-6 p-4 rounded-md" style={{ backgroundColor: 'var(--success-light)', color: 'var(--success)' }}>
|
||||
<h3 className="font-semibold mb-2 text-lg">
|
||||
Successfully deployed
|
||||
{appName && <span> {appName}</span>}
|
||||
{dns && <span> as {dns}</span>}
|
||||
</h3>
|
||||
{repoUrl && (
|
||||
<p className="mb-1 text-sm">
|
||||
<span className="font-medium">Repository:</span> {repoUrl}
|
||||
{shortCommitHash && <span> @ {shortCommitHash}</span>}
|
||||
{(!shortCommitHash && commitHash) && <span> @ {commitHash.substring(0, 7)}</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{txHash && <InfoItem label="Transaction Hash" value={txHash} />}
|
||||
{appRecordId && <InfoItem label="Application Record ID" value={appRecordId} />}
|
||||
{recordId && <InfoItem label="Deployment Request Record ID" value={recordId} />}
|
||||
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
||||
{dns && <InfoItem label="Deployment DNS" value={dns} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="mt-4">
|
||||
<div className="p-3 rounded-md mb-4" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
|
||||
{error || 'An unknown error occurred'}
|
||||
</div>
|
||||
{txHash && <InfoItem label="Transaction Hash" value={txHash} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -30,29 +30,54 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="w-full space-y-4">
|
||||
<form onSubmit={handleSubmit} className="w-full space-y-6">
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="url" className="mb-2 text-sm font-medium text-gray-700">
|
||||
<label htmlFor="url" className="mb-2 text-sm font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
URL to Deploy
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="url"
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://example.com"
|
||||
className="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full p-3 rounded-md transition-colors"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: disabled ? '0.6' : '1'
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 opacity-60">
|
||||
{url && (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="mt-2 text-sm font-medium px-3 py-2 rounded-md" style={{ color: 'var(--error)', background: 'var(--error-light)' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={disabled || !url}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400"
|
||||
className="w-full px-6 py-3 rounded-md transition-colors"
|
||||
style={{
|
||||
backgroundColor: (disabled || !url) ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
opacity: (disabled || !url) ? '0.7' : '1',
|
||||
}}
|
||||
>
|
||||
Deploy URL
|
||||
{disabled ? 'Connect Wallet First' : 'Deploy URL'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
@ -25,6 +25,11 @@ export const createApplicationDeploymentRequest = async (
|
||||
id: result.id,
|
||||
applicationRecordId: result.applicationRecordId,
|
||||
lrn: result.lrn,
|
||||
dns: result.dns,
|
||||
appName: result.appName,
|
||||
repoUrl: result.repoUrl,
|
||||
commitHash: result.commitHash,
|
||||
shortCommitHash: result.shortCommitHash,
|
||||
status: 'success',
|
||||
};
|
||||
} else {
|
||||
|
@ -44,6 +44,11 @@ export interface CreateRecordResponse {
|
||||
id: string;
|
||||
applicationRecordId?: string;
|
||||
lrn?: string;
|
||||
dns?: string;
|
||||
appName?: string;
|
||||
repoUrl?: string;
|
||||
commitHash?: string;
|
||||
shortCommitHash?: string;
|
||||
status: 'success' | 'error';
|
||||
message?: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user