This commit is contained in:
Kar
2025-03-19 20:42:16 +05:30
commit 11eb64b5e5
44 changed files with 11825 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Label } from './ui/label';
import { Select } from './ui/select';
import { Button } from './ui/button';
export function ContactForm() {
const [formState, setFormState] = useState({
name: '',
email: '',
company: '',
service: '',
message: '',
});
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormState(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setFormStatus('submitting');
// Simulate form submission with a timeout
setTimeout(() => {
// In a real app, you would send the data to a server here
console.log('Form submitted:', formState);
setFormStatus('success');
// Reset form after successful submission
setFormState({
name: '',
email: '',
company: '',
service: '',
message: '',
});
// Reset status after showing success message for a while
setTimeout(() => {
setFormStatus('idle');
}, 3000);
}, 1500);
};
return (
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
<div className="space-y-3 sm:space-y-4">
{/* Name and Email - Stack on mobile, side-by-side on tablet+ */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="name" className="text-sm sm:text-base">Name</Label>
<Input
id="name"
name="name"
placeholder="Your name"
value={formState.name}
onChange={handleChange}
required
className="text-sm sm:text-base"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="email" className="text-sm sm:text-base">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your.email@example.com"
value={formState.email}
onChange={handleChange}
required
className="text-sm sm:text-base"
/>
</div>
</div>
{/* Company and Service Interest - Stack on mobile, side-by-side on tablet+ */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="company" className="text-sm sm:text-base">Company</Label>
<Input
id="company"
name="company"
placeholder="Your company name"
value={formState.company}
onChange={handleChange}
className="text-sm sm:text-base"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest</Label>
<Select
id="service"
name="service"
value={formState.service}
onChange={handleChange}
required
className="text-sm sm:text-base"
>
<option value="" disabled>Select a service</option>
<option value="php">PHP Hosting</option>
<option value="nodejs">Node.js Hosting</option>
<option value="python">Python Hosting</option>
<option value="kubernetes">Kubernetes (K8s)</option>
<option value="k3s">K3s Lightweight Kubernetes</option>
<option value="custom">Custom Solution</option>
</Select>
</div>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="message" className="text-sm sm:text-base">Message</Label>
<Textarea
id="message"
name="message"
placeholder="Tell us about your project requirements..."
value={formState.message}
onChange={handleChange}
required
className="min-h-[100px] sm:min-h-[150px] text-sm sm:text-base"
/>
</div>
</div>
<Button
type="submit"
size="lg"
className="w-full mt-2 text-sm sm:text-base py-2 sm:py-3"
disabled={formStatus === 'submitting'}
>
{formStatus === 'submitting' ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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>
Sending...
</>
) : 'Send Message'}
</Button>
{formStatus === 'success' && (
<div className="p-3 sm:p-4 bg-green-900/30 border border-green-800 rounded-md text-green-400 text-center text-sm sm:text-base">
Thank you for your message! We'll get back to you soon.
</div>
)}
{formStatus === 'error' && (
<div className="p-3 sm:p-4 bg-red-900/30 border border-red-800 rounded-md text-red-400 text-center text-sm sm:text-base">
There was an error sending your message. Please try again.
</div>
)}
</form>
);
}

View File

@@ -0,0 +1,713 @@
import React, { useState, useEffect } from 'react';
import { Toast } from './Toast';
import { TemplatePreview } from './TemplatePreview';
export const DomainSetupForm = ({ defaultSubdomain }) => {
// Deployment type and app selections
const [deploymentType, setDeploymentType] = useState('app');
const [appType, setAppType] = useState('wordpress');
const [sampleWebAppType, setSampleWebAppType] = useState('developer'); // New state for sample web app type
const [sourceType, setSourceType] = useState('public');
const [repoUrl, setRepoUrl] = useState('');
const [deploymentKey, setDeploymentKey] = useState('');
const [fileName, setFileName] = useState('');
// Domain configuration
const [useSubdomain, setUseSubdomain] = useState(true);
const [useCustomDomain, setUseCustomDomain] = useState(false);
const [customDomain, setCustomDomain] = useState('');
const [customSubdomain, setCustomSubdomain] = useState('');
const [domainType, setDomainType] = useState('domain'); // 'domain' or 'subdomain'
// Domain validation states
const [isValidating, setIsValidating] = useState(false);
const [isValidDomain, setIsValidDomain] = useState(false);
const [validationMessage, setValidationMessage] = useState('');
// DNS configuration
const [dnsMethod, setDnsMethod] = useState('cname');
const [showDnsConfig, setShowDnsConfig] = useState(false);
const [dnsVerified, setDnsVerified] = useState({
cname: false,
ns: false,
a: false
});
// Form validation
const [formValid, setFormValid] = useState(true);
// Toast notification
const [toast, setToast] = useState({ visible: false, message: '' });
// File upload reference
const fileInputRef = React.useRef(null);
// Function to clean domain input
const cleanDomainInput = (input) => {
// Remove http://, https://, www., and trailing slashes
return input
.replace(/^(https?:\/\/)?(www\.)?/i, '')
.replace(/\/+$/, '')
.trim();
};
// Effect for handling domain type changes
useEffect(() => {
if (!useCustomDomain) {
setShowDnsConfig(false);
setIsValidDomain(false);
setValidationMessage('');
setIsValidating(false);
}
validateForm();
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, domainType, dnsMethod]);
// Show toast notification
const showToast = (message) => {
setToast({ visible: true, message });
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
};
// Handle deployment type change
const handleDeploymentTypeChange = (e) => {
setDeploymentType(e.target.value);
};
// Handle source type change
const handleSourceTypeChange = (e) => {
setSourceType(e.target.value);
};
// Handle file upload
const handleFileChange = (e) => {
if (e.target.files.length > 0) {
setFileName(e.target.files[0].name);
} else {
setFileName('');
}
};
// Handle domain checkbox changes
const handleUseSubdomainChange = (e) => {
setUseSubdomain(e.target.checked);
// If CNAME record is selected, SiliconPin subdomain must be enabled
if (useCustomDomain && dnsMethod === 'cname' && !e.target.checked) {
setUseCustomDomain(false);
}
validateForm();
};
const handleUseCustomDomainChange = (e) => {
setUseCustomDomain(e.target.checked);
if (!e.target.checked) {
setShowDnsConfig(false);
setIsValidDomain(false);
setValidationMessage('');
setIsValidating(false);
setDnsVerified({
cname: false,
ns: false,
a: false
});
} else {
// Force SiliconPin subdomain to be checked if custom domain is checked
setUseSubdomain(true);
}
validateForm();
};
// Handle domain type change
const handleDomainTypeChange = (e) => {
setDomainType(e.target.value);
// Reset validation when changing domain type
setIsValidDomain(false);
setValidationMessage('');
setDnsVerified({
cname: false,
ns: false,
a: false
});
validateForm();
};
// Handle domain and subdomain input changes
const handleDomainChange = (e) => {
const cleanedValue = cleanDomainInput(e.target.value);
setCustomDomain(cleanedValue);
};
const handleSubdomainChange = (e) => {
const cleanedValue = cleanDomainInput(e.target.value);
setCustomSubdomain(cleanedValue);
};
// Handle DNS method change
const handleDnsMethodChange = (e) => {
setDnsMethod(e.target.value);
// If changing to CNAME, ensure SiliconPin subdomain is enabled
if (e.target.value === 'cname') {
setUseSubdomain(true);
}
// Reset DNS verification
setDnsVerified(prev => ({
...prev,
[e.target.value]: false
}));
validateForm();
};
// Validate domain
const validateDomain = () => {
const domain = domainType === 'domain' ? customDomain : customSubdomain;
if (!domain) {
setValidationMessage('Please enter a domain name.');
setIsValidDomain(false);
return;
}
// Initial validation: check format with regex
const validFormat = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(domain);
if (!validFormat) {
setValidationMessage('Domain format is invalid. Please check your entry.');
setIsValidDomain(false);
return;
}
setIsValidating(true);
setValidationMessage('');
setShowDnsConfig(false);
// Simulate an API call to validate the domain
setTimeout(() => {
// Simulate a real domain check - in a real app this would be an API call
const checkResult = true; // Assume domain is valid for demo
setIsValidating(false);
setIsValidDomain(checkResult);
if (checkResult) {
setValidationMessage('Domain is valid and registered.');
setShowDnsConfig(true);
} else {
setValidationMessage('Domain appears to be unregistered or unavailable.');
}
validateForm();
}, 1500);
};
// Check DNS configuration
const checkDnsConfig = (type) => {
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
// Simulate DNS check
setTimeout(() => {
setDnsVerified(prev => ({
...prev,
[type]: true
}));
showToast(`${type} verified successfully!`);
validateForm();
}, 1500);
};
// Copy to clipboard
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => {
showToast('Copied to clipboard!');
})
.catch(err => {
showToast('Failed to copy: ' + err);
});
};
// Validate form
const validateForm = () => {
// For custom domain, require DNS verification
if (useCustomDomain) {
if (dnsMethod === 'cname' && !dnsVerified.cname) {
setFormValid(false);
return;
}
if (dnsMethod === 'ns' && !dnsVerified.ns) {
setFormValid(false);
return;
}
}
setFormValid(true);
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (!formValid) {
showToast('Please complete DNS verification before deploying.');
return;
}
// In a real app, this would submit the form data to the server
console.log({
deploymentType,
appType,
sampleWebAppType,
sourceType,
repoUrl,
deploymentKey,
useSubdomain,
useCustomDomain,
customDomain,
customSubdomain,
domainType,
dnsMethod
});
showToast('Form submitted successfully!');
};
return (
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Deployment Type Selection */}
<div className="space-y-2">
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
<select
id="deployment-type"
value={deploymentType}
onChange={handleDeploymentTypeChange}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="app">💻 Deploy an App</option>
<option value="source"> From Source</option>
<option value="static">📄 Static Site Upload</option>
<option value="sample-web-app">🌐 Sample Web App</option>
</select>
</div>
{/* App Options */}
{deploymentType === 'app' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Application</label>
<select
id="app-type"
value={appType}
onChange={(e) => setAppType(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="wordpress">🔌 WordPress</option>
<option value="prestashop">🛒 PrestaShop</option>
<option value="laravel">🚀 Laravel</option>
<option value="cakephp">🍰 CakePHP</option>
<option value="symfony">🎯 Symfony</option>
</select>
</div>
)}
{/* Sample Web App Options */}
{deploymentType === 'sample-web-app' && (
<div className="space-y-2">
<label htmlFor="sample-web-app-type" className="block text-white font-medium">Select Template Type</label>
<select
id="sample-web-app-type"
value={sampleWebAppType}
onChange={(e) => setSampleWebAppType(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="developer">👨💻 Developer Portfolio</option>
<option value="designer">🎨 Designer Portfolio</option>
<option value="photographer">📸 Photographer Portfolio</option>
<option value="documentation">📚 Documentation Site</option>
<option value="business">🏢 Single Page Business Site</option>
</select>
<div className="mt-4 p-4 bg-neutral-700/30 rounded-md border border-neutral-600">
<div className="flex items-start">
<div className="mr-3 text-2xl"></div>
<div className="flex-1">
<p className="text-sm text-neutral-300">
Deploy a ready-to-use template that you can customize. We'll set up the basic structure and you can modify it to fit your needs.
</p>
</div>
</div>
</div>
{/* Template Preview */}
<TemplatePreview templateType={sampleWebAppType} />
</div>
)}
{/* Source Options */}
{deploymentType === 'source' && (
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="source-type" className="block text-white font-medium">Source Type</label>
<select
id="source-type"
value={sourceType}
onChange={handleSourceTypeChange}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="public">🌐 Public Repository</option>
<option value="private">🔒 Private Repository</option>
</select>
</div>
<div className="pt-2">
<label htmlFor="repo-url" className="block text-white font-medium mb-2">Repository URL</label>
<input
type="text"
id="repo-url"
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/username/repository"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
</div>
{sourceType === 'private' && (
<div className="pt-2">
<div className="flex items-center gap-2 mb-2">
<label htmlFor="deployment-key" className="block text-white font-medium">Deployment Key</label>
<button
type="button"
onClick={() => showToast('Deployment keys are used for secure access to private repositories')}
className="text-neutral-400 hover:text-white focus:outline-none"
aria-label="Deployment Key Information"
>
<span className="text-lg">❓</span>
</button>
</div>
<textarea
id="deployment-key"
value={deploymentKey}
onChange={(e) => setDeploymentKey(e.target.value)}
placeholder="Paste your SSH private key here"
rows="6"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37] font-mono text-sm"
/>
<p className="mt-1 text-sm text-neutral-400">Your private key is used only for deploying and is never stored on our servers.</p>
</div>
)}
</div>
)}
{/* Static Site Options */}
{deploymentType === 'static' && (
<div className="space-y-4">
<div className="p-4 bg-neutral-700/50 rounded-md text-neutral-300 text-center">
Upload the zip file containing your static website
</div>
<div className="pt-2">
<label htmlFor="file-upload" className="block text-white font-medium mb-2">Upload File (ZIP/TAR)</label>
<div className="flex items-center justify-center w-full">
<label
htmlFor="file-upload"
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer border-neutral-600 hover:border-[#6d9e37]"
>
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<span className="text-3xl mb-3 text-neutral-400">📁</span>
<p className="mb-2 text-sm text-neutral-400">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-neutral-500">ZIP or TAR files only (max. 100MB)</p>
</div>
<input
id="file-upload"
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept=".zip,.tar"
/>
</label>
</div>
{fileName && (
<div className="mt-2 text-sm text-neutral-400">
Selected file: {fileName}
</div>
)}
</div>
</div>
)}
{/* Domain Configuration */}
<div className="pt-4 border-t border-neutral-700">
<h3 className="text-lg font-medium text-white mb-4">Destination</h3>
<div className="space-y-4">
{/* SiliconPin Subdomain */}
<div className="flex items-start gap-2">
<input
type="checkbox"
id="use-subdomain"
checked={useSubdomain}
onChange={handleUseSubdomainChange}
className="mt-1"
/>
<div className="flex-1">
<label htmlFor="use-subdomain" className="block text-white font-medium">Use SiliconPin Subdomain</label>
<div className="mt-2 flex">
<input
type="text"
id="subdomain"
value={defaultSubdomain}
className="rounded-l-md py-2 px-3 bg-neutral-600 border-y border-l border-neutral-600 text-neutral-300 focus:outline-none w-1/3 font-mono"
readOnly
/>
<span className="rounded-r-md py-2 px-3 bg-neutral-800 border border-neutral-700 text-neutral-400 w-2/3">.subdomain.siliconpin.com</span>
</div>
</div>
</div>
{/* Custom Domain */}
<div className="flex items-start gap-2">
<input
type="checkbox"
id="use-custom-domain"
checked={useCustomDomain}
onChange={handleUseCustomDomainChange}
className="mt-1"
/>
<div className="flex-1">
<label htmlFor="use-custom-domain" className="block text-white font-medium">Use Custom Domain</label>
{useCustomDomain && (
<div className="mt-3 space-y-4">
{/* Domain Type Selection */}
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
<div className="flex items-center">
<input
type="radio"
id="domain-type-domain"
name="domain-type"
value="domain"
checked={domainType === 'domain'}
onChange={handleDomainTypeChange}
className="mr-2"
/>
<label htmlFor="domain-type-domain" className="text-white">Root Domain</label>
</div>
<div className="flex items-center">
<input
type="radio"
id="domain-type-subdomain"
name="domain-type"
value="subdomain"
checked={domainType === 'subdomain'}
onChange={handleDomainTypeChange}
className="mr-2"
/>
<label htmlFor="domain-type-subdomain" className="text-white">Subdomain</label>
</div>
</div>
{/* Domain Input */}
{domainType === 'domain' ? (
<div>
<input
type="text"
id="custom-domain"
value={customDomain}
onChange={handleDomainChange}
placeholder="yourdomain.com"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
<p className="mt-1 text-xs text-neutral-400">
Enter domain without http://, www, or trailing slashes (example.com). You can configure www or other subdomains later.
</p>
</div>
) : (
<div>
<input
type="text"
id="custom-subdomain"
value={customSubdomain}
onChange={handleSubdomainChange}
placeholder="blog.yourdomain.com"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
<p className="mt-1 text-xs text-neutral-400">
Enter the full subdomain without http:// or trailing slashes. www and protocol prefixes will be automatically removed.
</p>
</div>
)}
{/* Domain Validation */}
<button
type="button"
onClick={validateDomain}
className="px-4 py-2 bg-neutral-600 text-white font-medium rounded-md hover:bg-neutral-500 transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-500"
>
Validate Domain
</button>
{/* Validation Status */}
{useCustomDomain && isValidating && (
<div className="p-3 bg-neutral-700/50 rounded-md">
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
<p className="text-white">Verifying domain registration and availability...</p>
</div>
</div>
)}
{useCustomDomain && !isValidating && validationMessage && (
<div className={`p-3 rounded-md ${isValidDomain ? 'bg-green-900/20 border border-green-700/50' : 'bg-red-900/20 border border-red-700/50'}`}>
<p className={isValidDomain ? 'text-green-400' : 'text-red-400'}>
{validationMessage}
</p>
</div>
)}
{/* DNS Configuration Options */}
{showDnsConfig && (
<div className="mt-4 space-y-4 p-4 rounded-md bg-neutral-800/50 border border-neutral-700">
<h4 className="text-white font-medium">Connect Your Domain</h4>
{/* CNAME Record Option */}
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
<div className="flex items-start">
<input
type="radio"
id="dns-cname"
name="dns-method"
value="cname"
checked={dnsMethod === 'cname'}
onChange={handleDnsMethodChange}
className="mt-1 mr-2"
/>
<div className="flex-1">
<label htmlFor="dns-cname" className="block text-white font-medium">Use CNAME Record</label>
<p className="text-sm text-neutral-300">Point your domain to our SiliconPin subdomain</p>
<div className="mt-3 flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">
{defaultSubdomain}.subdomain.siliconpin.com
</div>
<button
type="button"
onClick={() => copyToClipboard(`${defaultSubdomain}.subdomain.siliconpin.com`)}
className="ml-2 text-[#6d9e37] hover:text-white"
aria-label="Copy CNAME value"
>
<span className="text-lg">📋</span>
</button>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => checkDnsConfig('cname')}
className={`px-3 py-1 text-white text-sm rounded
${dnsVerified.cname
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{dnsVerified.cname ? ' CNAME Verified' : 'Check CNAME'}
</button>
</div>
</div>
</div>
</div>
{/* Nameserver Option (only for full domains, not subdomains) */}
{domainType === 'domain' && (
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
<div className="flex items-start">
<input
type="radio"
id="dns-ns"
name="dns-method"
value="ns"
checked={dnsMethod === 'ns'}
onChange={handleDnsMethodChange}
className="mt-1 mr-2"
/>
<div className="flex-1">
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
<div className="mt-3 space-y-2">
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
<button
type="button"
onClick={() => copyToClipboard('ns1.siliconpin.com')}
className="ml-2 text-[#6d9e37] hover:text-white"
aria-label="Copy nameserver value"
>
<span className="text-lg">📋</span>
</button>
</div>
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
<button
type="button"
onClick={() => copyToClipboard('ns2.siliconpin.com')}
className="ml-2 text-[#6d9e37] hover:text-white"
aria-label="Copy nameserver value"
>
<span className="text-lg">📋</span>
</button>
</div>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => checkDnsConfig('ns')}
className={`px-3 py-1 text-white text-sm rounded
${dnsVerified.ns
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
{/* Form Submit Button */}
<button
type="submit"
disabled={useCustomDomain && !formValid}
className={`w-full mt-6 px-6 py-3 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800
${useCustomDomain && !formValid
? 'bg-neutral-600 cursor-not-allowed'
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
}`}
>
Start the Deployment
</button>
</form>
<Toast visible={toast.visible} message={toast.message} />
</div>
);
};

View File

@@ -0,0 +1,46 @@
import React, { useState, useEffect } from 'react';
import { Toast } from './Toast';
export const DomainSetupForm = ({ defaultSubdomain }) => {
// Deployment state
const [deploymentType, setDeploymentType] = useState('app');
const [appType, setAppType] = useState('wordpress');
const [sampleWebAppType, setSampleWebAppType] = useState('developer'); // New state for sample web app type
const [sourceType, setSourceType] = useState('public');
const [repoUrl, setRepoUrl] = useState('');
const [deploymentKey, setDeploymentKey] = useState('');
const [fileName, setFileName] = useState('');
// Sample web app template information
const templateInfo = {
developer: {
name: "Developer Portfolio",
description: "A modern, responsive portfolio site with sections for projects, skills, and contact information. Perfect for developers to showcase their work.",
features: ["Project showcase", "Skills section", "GitHub integration", "Contact form", "Blog ready"],
image: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2072&q=80"
},
designer: {
name: "Designer Portfolio",
description: "A visually stunning portfolio for designers with image galleries, case studies, and animations to showcase creative work.",
features: ["Visual gallery", "Case studies", "Color scheme customization", "Smooth animations", "Design process showcase"],
image: "https://images.unsplash.com/photo-1561070791-2526d30994b5?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2000&q=80"
},
photographer: {
name: "Photographer Portfolio",
description: "An elegant portfolio with fullscreen galleries, image zooming, and lightbox features designed for photographers to display their work.",
features: ["Fullscreen galleries", "Image zoom", "Lightbox", "Category filtering", "Client proofing"],
image: "https://images.unsplash.com/photo-1542038784456-1ea8e935640e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
},
documentation: {
name: "Documentation Site",
description: "A comprehensive documentation site with search, code snippets, and versioning support for technical documentation.",
features: ["Search functionality", "Code snippets with syntax highlighting", "Versioning", "Sidebar navigation", "Mobile-friendly"],
image: "https://images.unsplash.com/photo-1456406644174-8ddd4cd52a06?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2068&q=80"
},
business: {
name: "Single Page Business Site",
description: "A professional one-page website for businesses with sections for services, testimonials, team members, and contact information.",
features: ["Single page layout", "Services section", "Testimonials", "Team profiles", "Contact form with map"],
image: "https://images.unsplash.com/photo-1560179707-f14e90ef3623?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2073&q=80"
}
};

View File

@@ -0,0 +1,318 @@
import React, { useState, useEffect } from 'react';
import { Toast } from './Toast';
export const FeedbackForm = () => {
// Form state
const [formType, setFormType] = useState('suggestion'); // 'suggestion' or 'report'
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [title, setTitle] = useState('');
const [details, setDetails] = useState('');
const [urgency, setUrgency] = useState('low');
const [category, setCategory] = useState('general');
const [referrer, setReferrer] = useState('');
const [screenshot, setScreenshot] = useState(null);
const [screenshotName, setScreenshotName] = useState('');
// UI states
const [isSubmitting, setIsSubmitting] = useState(false);
const [toast, setToast] = useState({ visible: false, message: '', type: 'success' });
// Get referrer information on mount
useEffect(() => {
if (typeof window !== 'undefined') {
const savedReferrer = localStorage.getItem('pageReferrer') || 'Direct Access';
setReferrer(savedReferrer);
}
}, []);
// Handle form type toggle
const handleFormTypeChange = (e) => {
setFormType(e.target.checked ? 'report' : 'suggestion');
};
// Handle file upload
const handleFileChange = (e) => {
if (e.target.files.length > 0) {
setScreenshot(e.target.files[0]);
setScreenshotName(e.target.files[0].name);
} else {
setScreenshot(null);
setScreenshotName('');
}
};
// Show toast notification
const showToast = (message, type = 'success') => {
setToast({ visible: true, message, type });
setTimeout(() => setToast({ visible: false, message: '', type: 'success' }), 5000);
};
// Form validation
const validateForm = () => {
if (!name.trim()) {
showToast('Please enter your name.', 'error');
return false;
}
if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showToast('Please enter a valid email address.', 'error');
return false;
}
if (!title.trim()) {
showToast('Please enter a title for your ' + formType + '.', 'error');
return false;
}
if (!details.trim()) {
showToast('Please provide details for your ' + formType + '.', 'error');
return false;
}
return true;
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
// In a real app, this would be an API call to submit the form
setTimeout(() => {
console.log({
formType,
name,
email,
title,
details,
urgency: formType === 'report' ? urgency : undefined,
category,
referrer,
screenshot: screenshot ? 'File attached' : 'No file'
});
// Reset form
setTitle('');
setDetails('');
setUrgency('low');
setCategory('general');
setScreenshot(null);
setScreenshotName('');
setIsSubmitting(false);
showToast(
formType === 'suggestion'
? 'Thank you for your suggestion! We appreciate your feedback.'
: 'Your report has been submitted. We will look into it as soon as possible.'
);
}, 1500);
};
return (
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
<div className="mb-6">
<div className="flex items-center justify-center gap-3">
<span className={`text-lg font-medium ${formType === 'suggestion' ? 'text-[#6d9e37]' : 'text-neutral-400'}`}>
Suggestion
</span>
{/* Toggle Switch */}
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={formType === 'report'}
onChange={handleFormTypeChange}
/>
<div className="w-11 h-6 bg-neutral-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-[#6d9e37] rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-neutral-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#6d9e37]"></div>
</label>
<span className={`text-lg font-medium ${formType === 'report' ? 'text-[#6d9e37]' : 'text-neutral-400'}`}>
Report
</span>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Hidden referrer field */}
<input
type="hidden"
id="referrerField"
name="referrer"
value={referrer}
/>
{/* Name and Email (2-column layout on larger screens) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div>
<label htmlFor="name" className="block text-white font-medium mb-1">Name <span className="text-red-500">*</span></label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
placeholder="Your full name"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-white font-medium mb-1">Email <span className="text-red-500">*</span></label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
placeholder="Your email address"
required
/>
</div>
</div>
{/* Title */}
<div>
<label htmlFor="title" className="block text-white font-medium mb-1">
{formType === 'suggestion' ? 'Suggestion Title' : 'Report Title'} <span className="text-red-500">*</span>
</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
placeholder={formType === 'suggestion' ? "Brief title for your suggestion" : "Brief title for your report"}
required
/>
</div>
{/* Category */}
<div>
<label htmlFor="category" className="block text-white font-medium mb-1">Category</label>
<select
id="category"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="general">General</option>
<option value="ui">User Interface</option>
<option value="performance">Performance</option>
<option value="feature">Feature Request</option>
<option value="technical">Technical Issue</option>
<option value="billing">Billing/Payment</option>
<option value="security">Security</option>
<option value="other">Other</option>
</select>
</div>
{/* Urgency (only for reports) */}
{formType === 'report' && (
<div>
<label htmlFor="urgency" className="block text-white font-medium mb-1">Urgency Level</label>
<select
id="urgency"
value={urgency}
onChange={(e) => setUrgency(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="low">Low - Not urgent</option>
<option value="medium">Medium - Needs attention</option>
<option value="high">High - Impacts usage</option>
<option value="critical">Critical - Service unavailable</option>
</select>
</div>
)}
{/* Details */}
<div>
<label htmlFor="details" className="block text-white font-medium mb-1">
Details <span className="text-red-500">*</span>
</label>
<textarea
id="details"
value={details}
onChange={(e) => setDetails(e.target.value)}
rows="6"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
placeholder={formType === 'suggestion'
? "Please describe your suggestion in detail. What would you like to see improved or added?"
: "Please describe the issue in detail. What happened, and what were you trying to do?"
}
required
></textarea>
</div>
{/* Screenshot upload (primarily for reports, but available for both) */}
<div>
<label htmlFor="screenshot" className="block text-white font-medium mb-1">
{formType === 'report' ? 'Screenshot (Optional)' : 'Attachment (Optional)'}
</label>
<div className="flex flex-col items-center justify-center w-full">
<label
htmlFor="screenshot"
className="flex flex-col items-center justify-center w-full h-24 border-2 border-dashed rounded-lg cursor-pointer border-neutral-600 hover:border-[#6d9e37]"
>
<div className="flex flex-col items-center justify-center pt-3 pb-3">
<span className="text-2xl mb-1 text-neutral-400">📎</span>
<p className="mb-1 text-sm text-neutral-400">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-neutral-500">PNG, JPG, or PDF (max. 5MB)</p>
</div>
<input
id="screenshot"
type="file"
onChange={handleFileChange}
className="hidden"
accept=".png,.jpg,.jpeg,.pdf"
/>
</label>
</div>
{screenshotName && (
<div className="mt-2 text-sm text-neutral-400">
Selected file: {screenshotName}
</div>
)}
</div>
{/* Referrer information display */}
<div className="text-sm text-neutral-400 italic">
<span>Page referrer: {referrer}</span>
</div>
{/* Submit button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full mt-2 px-6 py-3 bg-[#6d9e37] text-white font-medium rounded-md hover:bg-[#598035] transition-colors focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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>
Processing...
</span>
) : (
`Submit ${formType === 'suggestion' ? 'Suggestion' : 'Report'}`
)}
</button>
</form>
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type === 'error' ? 'error' : 'success'}
/>
</div>
);
};

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
export interface ServiceCardProps {
title: string;
description: string;
imageUrl: string;
features: string[];
learnMoreUrl: string;
}
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl }: ServiceCardProps) {
return (
<Card className="overflow-hidden transition-all hover:shadow-lg dark:border-neutral-700 h-full flex flex-col">
<div className="relative h-48 w-full overflow-hidden bg-neutral-100 dark:bg-neutral-800">
<img
src={imageUrl}
alt={`${title} illustration`}
className="object-cover w-full h-full transition-transform duration-300 hover:scale-105"
/>
</div>
<CardHeader className="pb-3">
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
<CardDescription className="text-neutral-400">{description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2 text-sm">
{features.map((feature, index) => (
<li key={index} className="flex items-start">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span>{feature}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter>
<a
href={learnMoreUrl}
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
>
Learn More
</a>
</CardFooter>
</Card>
);
}

View File

@@ -0,0 +1,80 @@
import React from 'react';
export const TemplatePreview = ({ templateType }) => {
// Template information for different types
const templateInfo = {
developer: {
name: "Developer Portfolio",
description: "A modern, responsive portfolio site with sections for projects, skills, and contact information. Perfect for developers to showcase their work.",
features: ["Project showcase", "Skills section", "GitHub integration", "Contact form", "Blog ready"],
image: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2072&q=80"
},
designer: {
name: "Designer Portfolio",
description: "A visually stunning portfolio for designers with image galleries, case studies, and animations to showcase creative work.",
features: ["Visual gallery", "Case studies", "Color scheme customization", "Smooth animations", "Design process showcase"],
image: "https://images.unsplash.com/photo-1561070791-2526d30994b5?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2000&q=80"
},
photographer: {
name: "Photographer Portfolio",
description: "An elegant portfolio with fullscreen galleries, image zooming, and lightbox features designed for photographers to display their work.",
features: ["Fullscreen galleries", "Image zoom", "Lightbox", "Category filtering", "Client proofing"],
image: "https://images.unsplash.com/photo-1542038784456-1ea8e935640e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
},
documentation: {
name: "Documentation Site",
description: "A comprehensive documentation site with search, code snippets, and versioning support for technical documentation.",
features: ["Search functionality", "Code snippets with syntax highlighting", "Versioning", "Sidebar navigation", "Mobile-friendly"],
image: "https://images.unsplash.com/photo-1456406644174-8ddd4cd52a06?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2068&q=80"
},
business: {
name: "Single Page Business Site",
description: "A professional one-page website for businesses with sections for services, testimonials, team members, and contact information.",
features: ["Single page layout", "Services section", "Testimonials", "Team profiles", "Contact form with map"],
image: "https://images.unsplash.com/photo-1560179707-f14e90ef3623?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2073&q=80"
}
};
const template = templateInfo[templateType] || templateInfo.developer;
return (
<div className="mt-6 bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
{/* Template preview image */}
<div className="relative h-48 overflow-hidden">
<img
src={template.image}
alt={`${template.name} Preview`}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex items-end">
<div className="p-4">
<h3 className="text-xl font-semibold text-white">{template.name}</h3>
</div>
</div>
</div>
{/* Template details */}
<div className="p-5 space-y-4">
<p className="text-neutral-300">{template.description}</p>
<div>
<h4 className="text-white font-medium mb-2">Features:</h4>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{template.features.map((feature, index) => (
<li key={index} className="text-neutral-400 flex items-center">
<span className="mr-2 text-[#6d9e37]"></span>
{feature}
</li>
))}
</ul>
</div>
<div className="pt-2">
<button className="px-4 py-2 bg-[#6d9e37] text-white rounded-md hover:bg-[#598035] transition-colors w-full">
Select This Template
</button>
</div>
</div>
</div>
);
};

20
src/components/Toast.jsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react';
export const Toast = ({ visible, message, type = 'success' }) => {
// Color scheme based on type
const bgColor = type === 'error' ? 'bg-red-900' : 'bg-green-900';
const icon = type === 'error' ? '✕' : '✓';
return (
<div
className={`fixed bottom-5 right-5 p-3 ${bgColor} text-white rounded-md shadow-lg transform transition-all duration-300 z-50 ${
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
}`}
>
<div className="flex items-center gap-2">
<span className="text-lg">{icon}</span>
<span>{message}</span>
</div>
</div>
);
};

View File

@@ -0,0 +1,33 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "outline";
size?: "default" | "sm" | "lg";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", ...props }, ref) => {
return (
<button
className={cn(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-neutral-900",
{
"bg-[#6d9e37] text-white hover:bg-[#598035]": variant === "default",
"border border-[#6d9e37] text-[#6d9e37] hover:bg-[#6d9e37] hover:text-white": variant === "outline",
"h-10 py-2 px-4": size === "default",
"h-9 px-3 rounded-md": size === "sm",
"h-11 px-8 rounded-md": size === "lg",
},
className
)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button };

View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import { cn } from "../../lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-neutral-200 bg-white text-neutral-950 shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };

View File

@@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface LabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement> {}
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ className, ...props }, ref) => (
<label
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
)
);
Label.displayName = "Label";
export { Label };

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface SelectProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
className={cn(
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
>
{children}
</select>
);
}
);
Select.displayName = "Select";
export { Select };

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[120px] w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = "Textarea";
export { Textarea };