diff --git a/next.config.ts b/next.config.ts index e1ea5b8..a4ae642 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index 9d0f821..802d6e9 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -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, @@ -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); diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..51d17e7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 063d56a..1b9742b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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({ + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index f331cf3..20a60ec 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,6 +19,11 @@ export default function Home() { const [recordId, setRecordId] = useState(null); const [appRecordId, setAppRecordId] = useState(null); const [lrn, setLrn] = useState(null); + const [dns, setDns] = useState(null); + const [appName, setAppName] = useState(null); + const [repoUrl, setRepoUrl] = useState(null); + const [commitHash, setCommitHash] = useState(null); + const [shortCommitHash, setShortCommitHash] = useState(null); const [error, setError] = useState(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 (
-
-

ATOM Deploy - Laconic Registry

+
+

+ ATOM Deploy - Laconic Registry +

-
-

1. Connect Your Wallet

+
+

+ 1 + Connect Your Wallet +

-
-

2. Enter URL to Deploy

+
+

+ 2 + Enter URL to Deploy +

- + {status !== 'idle' && ( +
+

+ 3 + Deployment Status +

+ +
+ )}
{showPaymentModal && walletAddress && url && ( diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..9cb1fc3 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -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; +} \ No newline at end of file diff --git a/src/components/ErrorBoundaryWrapper.tsx b/src/components/ErrorBoundaryWrapper.tsx new file mode 100644 index 0000000..4efe18b --- /dev/null +++ b/src/components/ErrorBoundaryWrapper.tsx @@ -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 ; +} \ No newline at end of file diff --git a/src/components/KeplrConnect.tsx b/src/components/KeplrConnect.tsx index e39c42a..490c570 100644 --- a/src/components/KeplrConnect.tsx +++ b/src/components/KeplrConnect.tsx @@ -36,19 +36,37 @@ export default function KeplrConnect({ onConnect }: KeplrConnectProps) { }, []); return ( -
+
{address ? ( -
-

Connected

-

{address}

+
+
+ +

Connected

+
+
+

{address}

+
) : ( )}
diff --git a/src/components/PaymentModal.tsx b/src/components/PaymentModal.tsx index bf57aaa..d063322 100644 --- a/src/components/PaymentModal.tsx +++ b/src/components/PaymentModal.tsx @@ -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 ( -
-
-

Complete Payment

- -
-

URL to be deployed:

-

{url}

+
+
+
+

Complete Payment

-
-

Recipient Address:

-

{recipientAddress}

-
- -
- - setAmount(e.target.value)} - className="w-full p-2 border border-gray-300 rounded-md" - /> -
- - {error && ( -
- {error} +
+
+

URL to be deployed:

+
+ {url} +
- )} + +
+

Recipient Address:

+
+ {recipientAddress} +
+
+ +
+ +
+ 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)' + }} + /> +
+ ATOM +
+
+
+ + {error && ( +
+ {error} +
+ )} +
-
+
diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx index e6e44e6..5d4633f 100644 --- a/src/components/StatusDisplay.tsx +++ b/src/components/StatusDisplay.tsx @@ -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 ( + + {styles.text} + + ); + }; + + const InfoItem = ({ label, value }: { label: string, value: string }) => ( +
+
+ {label} +
+
+ {value} +
+
+ ); + return ( -
-

Status

- - {status === 'verifying' && ( -
- - - - - Verifying transaction... -
- )} - - {status === 'creating' && ( -
- - - - - Creating Laconic Registry record... -
- )} +
+
+ {(status === 'verifying' || status === 'creating') && ( +
+ +
+ + + + + + {status === 'verifying' ? 'Verifying transaction...' : 'Creating Laconic Registry record...'} + +
+
+ )} + + {status === 'success' && ( +
+ + Successfully deployed! +
+ )} + + {status === 'error' && ( +
+ + Deployment Failed +
+ )} +
{status === 'success' && ( -
-

Successfully deployed!

- - {txHash && ( -
-

Transaction Hash:

-

{txHash}

+
+ {appName && ( +
+

+ Successfully deployed + {appName && {appName}} + {dns && as {dns}} +

+ {repoUrl && ( +

+ Repository: {repoUrl} + {shortCommitHash && @ {shortCommitHash}} + {(!shortCommitHash && commitHash) && @ {commitHash.substring(0, 7)}} +

+ )}
)} - {appRecordId && ( -
-

Application Record ID:

-

{appRecordId}

-
- )} - - {recordId && ( -
-

Deployment Request Record ID:

-

{recordId}

-
- )} - - {lrn && ( -
-

Laconic Resource Name (LRN):

-

{lrn}

-
- )} + {txHash && } + {appRecordId && } + {recordId && } + {lrn && } + {dns && }
)} {status === 'error' && ( -
-

Error

-

{error || 'An unknown error occurred'}

- - {txHash && ( -
-

Transaction Hash:

-

{txHash}

-
- )} +
+
+ {error || 'An unknown error occurred'} +
+ {txHash && }
)}
diff --git a/src/components/URLForm.tsx b/src/components/URLForm.tsx index 2f79ca5..70289ac 100644 --- a/src/components/URLForm.tsx +++ b/src/components/URLForm.tsx @@ -30,29 +30,54 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) { }; return ( -
+
-
); diff --git a/src/services/registry.ts b/src/services/registry.ts index 10e44a1..17068c7 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -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 { diff --git a/src/types/index.ts b/src/types/index.ts index bcceb49..4c7fb0b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; } \ No newline at end of file