This commit is contained in:
zramsay 2025-05-03 09:42:05 -04:00
parent 2ecadcf19f
commit 43e4b38e96
13 changed files with 581 additions and 145 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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;
}

View File

@ -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>

View File

@ -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,30 +96,55 @@ 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>
<StatusDisplay
status={status}
txHash={txHash || undefined}
recordId={recordId || undefined}
appRecordId={appRecordId || undefined}
lrn={lrn || undefined}
error={error || undefined}
/>
{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 && (

View 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;
}

View 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 />;
}

View File

@ -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',
}}
>
{connecting ? 'Connecting...' : 'Connect Keplr Wallet'}
<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>

View File

@ -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>
<div className="mb-6">
<label htmlFor="amount" className="block text-sm font-medium text-gray-700 mb-1">
Amount (ATOM)
</label>
<input
id="amount"
type="number"
min="0.1"
step="0.1"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
{error && (
<div className="mb-4 p-2 bg-red-100 text-red-700 rounded-md text-sm">
{error}
<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>
<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.001"
step="0.001"
value={amount}
onChange={(e) => setAmount(e.target.value)}
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="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>

View File

@ -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,79 +20,127 @@ 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 (
<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>
);
};
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>
);
return (
<div className="mt-8 p-4 bg-gray-100 rounded-lg">
<h3 className="text-lg font-semibold mb-2">Status</h3>
{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...
</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">
<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...
</div>
)}
<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>
<span style={{ color: 'var(--warning)' }}>
{status === 'verifying' ? 'Verifying transaction...' : 'Creating Laconic Registry record...'}
</span>
</div>
</div>
)}
{status === 'success' && (
<div className="flex items-center">
<StatusBadge type="success" />
<span className="ml-3" style={{ color: 'var(--success)' }}>Successfully deployed!</span>
</div>
)}
{status === 'error' && (
<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="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 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>
)}
{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>
)}
{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="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>
)}
<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>

View File

@ -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>
<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"
disabled={disabled}
/>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
<div className="relative">
<input
id="url"
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com"
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}
/>
<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>
);

View File

@ -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 {

View File

@ -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;
}