Use solana GOR token payments to deploy apps (#1)
Part of https://www.notion.so/Laconic-Mainnet-Plan-1eca6b22d47280569cd0d1e6d711d949 Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Co-authored-by: Nabarun <nabarun@deepstacksoft.com> Reviewed-on: #1 Co-authored-by: shreerang <shreerang@noreply.git.vdb.to> Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { connectKeplr } from '@/services/keplr';
|
||||
|
||||
interface KeplrConnectProps {
|
||||
onConnect: (address: string) => void;
|
||||
}
|
||||
|
||||
export default function KeplrConnect({ onConnect }: KeplrConnectProps) {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [address, setAddress] = useState<string | null>(null);
|
||||
|
||||
const handleConnect = async () => {
|
||||
setConnecting(true);
|
||||
try {
|
||||
const userAddress = await connectKeplr();
|
||||
if (userAddress) {
|
||||
setAddress(userAddress);
|
||||
onConnect(userAddress);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Keplr:', error);
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check if Keplr is available
|
||||
if (typeof window !== 'undefined' && window.keplr) {
|
||||
// Auto-connect on page load
|
||||
handleConnect();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
||||
{address ? (
|
||||
<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-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>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { sendAtomPayment } from '@/services/keplr';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import BN from 'bn.js';
|
||||
import assert from 'assert';
|
||||
|
||||
interface PaymentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
url: string;
|
||||
onPaymentComplete: (txHash: string) => void;
|
||||
}
|
||||
import { Connection } from '@solana/web3.js';
|
||||
|
||||
import { sendSolanaTokenPayment } from '@/services/solana';
|
||||
import { PaymentModalProps } from '@/types';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
|
||||
export default function PaymentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
url,
|
||||
onPaymentComplete,
|
||||
walletState,
|
||||
}: PaymentModalProps) {
|
||||
const [amount, setAmount] = useState('0.01');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Get recipient address from environment variables
|
||||
const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
|
||||
|
||||
// Validate amount on change
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setAmount(value);
|
||||
|
||||
// Clear error when user types
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
const connection = useMemo(() => new Connection(SOLANA_RPC_URL), [])
|
||||
|
||||
const handlePayment = async () => {
|
||||
// Validate amount before sending
|
||||
const parsedAmount = parseFloat(amount);
|
||||
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
||||
setError('Please enter a valid positive amount');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get configuration from environment variables directly
|
||||
const amount = parseInt(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT!);
|
||||
const decimals = parseInt(process.env.NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS!);
|
||||
|
||||
const recipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
||||
|
||||
const handlePayment = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
|
||||
try {
|
||||
const result = await sendAtomPayment(recipientAddress, amount);
|
||||
|
||||
if (result.status === 'success' && result.hash) {
|
||||
onPaymentComplete(result.hash);
|
||||
const fullAmount = amount * Math.pow(10, decimals);
|
||||
const tokenAmount = new BN(fullAmount);
|
||||
|
||||
const result = await sendSolanaTokenPayment(connection, walletState.publicKey!, tokenAmount, walletState.walletType!);
|
||||
|
||||
if (result.success && result.transactionSignature) {
|
||||
onPaymentComplete(result.transactionSignature);
|
||||
} else {
|
||||
setError(result.message || 'Payment failed. Please try again.');
|
||||
setError(result.error || `${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment failed. Please try again.`);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [connection, walletState, amount, decimals, onPaymentComplete]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<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="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>
|
||||
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
Complete {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
@@ -77,51 +71,54 @@ export default function PaymentModal({
|
||||
<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)
|
||||
Amount ({process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL})
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
value={amount}
|
||||
onChange={handleAmountChange}
|
||||
disabled={true} // Fixed amount for Solana tokens
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)'
|
||||
color: 'var(--foreground)',
|
||||
opacity: '0.7'
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<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>
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
||||
Fixed amount required for deployment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<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 rounded-md transition-colors"
|
||||
style={{
|
||||
style={{
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: loading ? '0.5' : '1'
|
||||
@@ -146,7 +143,7 @@ export default function PaymentModal({
|
||||
<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'}
|
||||
{loading ? 'Processing...' : 'Pay with Solana Wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { connectSolanaWallet, disconnectSolanaWallet, checkSolanaWalletConnection } from '@/services/solana';
|
||||
import { SolanaWalletType, SolanaWalletState } from '@/types';
|
||||
|
||||
interface SolanaConnectProps {
|
||||
onConnect: (walletState: SolanaWalletState) => void;
|
||||
}
|
||||
|
||||
export default function SolanaConnect({ onConnect }: SolanaConnectProps) {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [walletState, setWalletState] = useState<SolanaWalletState>({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
walletType: null
|
||||
});
|
||||
|
||||
const handleConnect = async (walletType: SolanaWalletType) => {
|
||||
setConnecting(true);
|
||||
try {
|
||||
const newWalletState = await connectSolanaWallet(walletType);
|
||||
setWalletState(newWalletState);
|
||||
onConnect(newWalletState);
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Solana wallet:', error);
|
||||
alert(error instanceof Error ? error.message : 'Failed to connect wallet');
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
if (walletState.walletType) {
|
||||
try {
|
||||
await disconnectSolanaWallet(walletState.walletType);
|
||||
const disconnectedState = {
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
walletType: null
|
||||
};
|
||||
setWalletState(disconnectedState);
|
||||
onConnect(disconnectedState);
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect wallet:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check for auto-connection on page load
|
||||
const checkConnection = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Check Phantom
|
||||
if (window.phantom?.solana && checkSolanaWalletConnection('phantom')) {
|
||||
handleConnect('phantom');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check Solflare
|
||||
if (window.solflare && checkSolanaWalletConnection('solflare')) {
|
||||
handleConnect('solflare');
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkConnection();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
||||
{walletState.connected ? (
|
||||
<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 ({walletState.walletType})
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full p-3 rounded-md mb-3" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<p className="text-sm font-mono break-all text-center">{walletState.publicKey}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
className="px-4 py-2 rounded-md text-sm transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--muted)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--input-border)'
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-3">
|
||||
<button
|
||||
onClick={() => handleConnect('phantom')}
|
||||
disabled={connecting || !window.phantom?.solana}
|
||||
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
}}
|
||||
>
|
||||
<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 Phantom Wallet'}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleConnect('solflare')}
|
||||
disabled={connecting || !window.solflare}
|
||||
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
}}
|
||||
>
|
||||
<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 Solflare Wallet'}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!window.phantom?.solana && !window.solflare && (
|
||||
<p className="text-sm text-center" style={{ color: 'var(--muted)' }}>
|
||||
Please install Phantom or Solflare wallet extension
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default function StatusDisplay({
|
||||
error,
|
||||
}: StatusDisplayProps) {
|
||||
// Get domain suffix from environment variable
|
||||
const domainSuffix = process.env.NEXT_PUBLIC_DOMAIN_SUFFIX || '';
|
||||
const domainSuffix = process.env.NEXT_PUBLIC_DOMAIN_SUFFIX;
|
||||
if (status === 'idle') return null;
|
||||
|
||||
const StatusBadge = ({ type }: { type: 'creating' | 'success' | 'error' }) => {
|
||||
@@ -58,7 +58,7 @@ export default function StatusDisplay({
|
||||
const styles = getBadgeStyles();
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold"
|
||||
<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>
|
||||
@@ -108,7 +108,7 @@ export default function StatusDisplay({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="mt-4">
|
||||
{appName && (
|
||||
@@ -124,15 +124,19 @@ export default function StatusDisplay({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{txHash && <InfoItem label="ATOM Payment Transaction Hash" value={txHash} />}
|
||||
|
||||
{txHash && <InfoItem label={`${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment Transaction Hash`} value={txHash} />}
|
||||
{appRecordId && <InfoItem label="Laconic Application Record ID" value={appRecordId} />}
|
||||
{recordId && <InfoItem label="Laconic Deployment Request Record ID" value={recordId} />}
|
||||
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
||||
{dns && <InfoItem label="Deployment URL (ready in 2-3 minutes)" value={domainSuffix ? `https://${dns}${domainSuffix}` : `http://${dns}`} />}
|
||||
{dns && (
|
||||
domainSuffix
|
||||
? <InfoItem label="Deployment URL (ready in 2-3 minutes)" value={`https://${dns}.${domainSuffix}`} />
|
||||
: <InfoItem label="Please set domain suffix" value="" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="mt-4">
|
||||
<div className="p-3 rounded-md mb-4" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
|
||||
|
||||
+14
-13
@@ -10,46 +10,47 @@ interface URLFormProps {
|
||||
export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
// Get example URL from environment variables or use a default
|
||||
const exampleUrl = process.env.NEXT_PUBLIC_EXAMPLE_URL || 'https://example.com';
|
||||
const [url, setUrl] = useState(exampleUrl);
|
||||
const [url, setUrl] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// Trim the URL to remove any whitespace
|
||||
const trimmedUrl = url.trim();
|
||||
|
||||
|
||||
if (!trimmedUrl) {
|
||||
setError('Please enter a URL');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
const parsedUrl = new URL(trimmedUrl);
|
||||
|
||||
|
||||
// Check for protocol
|
||||
if (!parsedUrl.protocol.startsWith('http')) {
|
||||
setError('URL must use HTTP or HTTPS protocol');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check for hostname
|
||||
if (!parsedUrl.hostname || parsedUrl.hostname.length < 3) {
|
||||
setError('URL must contain a valid hostname');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Basic sanity check for common invalid URLs
|
||||
if (parsedUrl.href === 'http://localhost' || parsedUrl.href === 'https://localhost') {
|
||||
setError('Please enter a valid public URL, not localhost');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// All validations passed
|
||||
setError('');
|
||||
onSubmit(trimmedUrl);
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError('Please enter a valid URL (e.g., https://example.com)');
|
||||
}
|
||||
};
|
||||
@@ -68,7 +69,7 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder={exampleUrl}
|
||||
className="w-full p-3 rounded-md transition-colors"
|
||||
style={{
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
@@ -91,12 +92,12 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={disabled || !url}
|
||||
className="w-full px-6 py-3 rounded-md transition-colors"
|
||||
style={{
|
||||
style={{
|
||||
backgroundColor: (disabled || !url) ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
opacity: (disabled || !url) ? '0.7' : '1',
|
||||
@@ -106,4 +107,4 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user