2025-08-13 00:51:44 +01:00
|
|
|
import { useState } from 'react';
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-08-12 22:31:26 +01:00
|
|
|
import { Header } from '@/components/Header';
|
|
|
|
import { Footer } from '@/components/Footer';
|
2025-08-13 00:51:44 +01:00
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
import { Separator } from '@/components/ui/separator';
|
|
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
|
|
import { showSuccess, showError } from '@/utils/toast';
|
|
|
|
import { generateId } from '@/lib/utils';
|
|
|
|
import {
|
|
|
|
Code2,
|
|
|
|
FileText,
|
|
|
|
Monitor,
|
|
|
|
Globe,
|
|
|
|
Plus,
|
|
|
|
X,
|
|
|
|
Save,
|
|
|
|
Eye
|
|
|
|
} from 'lucide-react';
|
2025-08-12 22:31:26 +01:00
|
|
|
|
|
|
|
export default function SubmitScript() {
|
2025-08-13 00:51:44 +01:00
|
|
|
const { user } = useAuth();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
name: '',
|
|
|
|
description: '',
|
|
|
|
code: '',
|
|
|
|
installation: '',
|
|
|
|
usage: '',
|
|
|
|
compatibleOs: [] as string[],
|
|
|
|
categories: [] as string[],
|
|
|
|
tags: [] as string[],
|
|
|
|
gitRepositoryUrl: '',
|
|
|
|
version: '1.0.0',
|
|
|
|
license: 'MIT',
|
|
|
|
readme: ''
|
|
|
|
});
|
|
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
const [newTag, setNewTag] = useState('');
|
|
|
|
const [newCategory, setNewCategory] = useState('');
|
|
|
|
|
|
|
|
const operatingSystems = ['Linux', 'macOS', 'Windows', 'BSD', 'Android'];
|
|
|
|
const availableCategories = ['DevOps', 'Automation', 'System Admin', 'Development', 'Security', 'Networking', 'Data Processing', 'Web Development', 'Mobile Development', 'Game Development'];
|
|
|
|
const licenses = ['MIT', 'Apache 2.0', 'GPL v3', 'GPL v2', 'BSD 3-Clause', 'BSD 2-Clause', 'Unlicense', 'Custom'];
|
|
|
|
|
|
|
|
const validateForm = () => {
|
|
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
|
|
|
|
if (!formData.name.trim()) {
|
|
|
|
newErrors.name = 'Script name is required';
|
|
|
|
} else if (formData.name.length < 3) {
|
|
|
|
newErrors.name = 'Script name must be at least 3 characters';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!formData.description.trim()) {
|
|
|
|
newErrors.description = 'Description is required';
|
|
|
|
} else if (formData.description.length < 20) {
|
|
|
|
newErrors.description = 'Description must be at least 20 characters';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!formData.code.trim()) {
|
|
|
|
newErrors.code = 'Script code is required';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formData.compatibleOs.length === 0) {
|
|
|
|
newErrors.compatibleOs = 'At least one compatible OS is required';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formData.categories.length === 0) {
|
|
|
|
newErrors.categories = 'At least one category is required';
|
|
|
|
}
|
|
|
|
|
|
|
|
setErrors(newErrors);
|
|
|
|
return Object.keys(newErrors).length === 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
if (!validateForm()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Simulate API call
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
|
|
|
|
// In a real app, this would send the data to your backend
|
|
|
|
const scriptData = {
|
|
|
|
id: generateId(),
|
|
|
|
...formData,
|
|
|
|
authorId: user?.id || '',
|
|
|
|
authorName: user?.username || '',
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
isApproved: false,
|
|
|
|
isPublic: false,
|
|
|
|
viewCount: 0,
|
|
|
|
downloadCount: 0,
|
|
|
|
rating: 0,
|
|
|
|
ratingCount: 0
|
|
|
|
};
|
|
|
|
|
|
|
|
// Save to localStorage for demo purposes
|
|
|
|
const existingScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
|
|
|
|
existingScripts.push(scriptData);
|
|
|
|
localStorage.setItem('scripts', JSON.stringify(existingScripts));
|
|
|
|
|
|
|
|
showSuccess('Script submitted successfully! It will be reviewed by our team.');
|
|
|
|
navigate('/my-scripts');
|
|
|
|
} catch (error) {
|
|
|
|
showError('Failed to submit script. Please try again.');
|
|
|
|
} finally {
|
|
|
|
setIsLoading(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
|
|
const { name, value } = e.target;
|
|
|
|
setFormData(prev => ({ ...prev, [name]: value }));
|
|
|
|
|
|
|
|
// Clear error when user starts typing
|
|
|
|
if (errors[name]) {
|
|
|
|
setErrors(prev => ({ ...prev, [name]: '' }));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
|
|
const { name, value } = e.target;
|
|
|
|
setFormData(prev => ({ ...prev, [name]: value }));
|
|
|
|
};
|
|
|
|
|
|
|
|
const toggleOs = (os: string) => {
|
|
|
|
setFormData(prev => ({
|
|
|
|
...prev,
|
|
|
|
compatibleOs: prev.compatibleOs.includes(os)
|
|
|
|
? prev.compatibleOs.filter(o => o !== os)
|
|
|
|
: [...prev.compatibleOs, os]
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
const toggleCategory = (category: string) => {
|
|
|
|
setFormData(prev => ({
|
|
|
|
...prev,
|
|
|
|
categories: prev.categories.includes(category)
|
|
|
|
? prev.categories.filter(c => c !== category)
|
|
|
|
: [...prev.categories, category]
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
const addTag = () => {
|
|
|
|
if (newTag.trim() && !formData.tags.includes(newTag.trim())) {
|
|
|
|
setFormData(prev => ({
|
|
|
|
...prev,
|
|
|
|
tags: [...prev.tags, newTag.trim()]
|
|
|
|
}));
|
|
|
|
setNewTag('');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const removeTag = (tagToRemove: string) => {
|
|
|
|
setFormData(prev => ({
|
|
|
|
...prev,
|
|
|
|
tags: prev.tags.filter(tag => tag !== tagToRemove)
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
const addCategory = () => {
|
|
|
|
if (newCategory.trim() && !formData.categories.includes(newCategory.trim())) {
|
|
|
|
setFormData(prev => ({
|
|
|
|
...prev,
|
|
|
|
categories: [...prev.categories, newCategory.trim()]
|
|
|
|
}));
|
|
|
|
setNewCategory('');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
return (
|
|
|
|
<div className="min-h-screen flex flex-col bg-gradient-to-br from-background via-background to-primary/5">
|
|
|
|
<Header onSearch={() => {}} />
|
|
|
|
<main className="flex-1 flex items-center justify-center px-4 py-16">
|
|
|
|
<Card className="w-full max-w-md text-center">
|
|
|
|
<CardContent className="p-8">
|
|
|
|
<Code2 className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
|
|
|
<h2 className="text-xl font-semibold mb-2">Authentication Required</h2>
|
|
|
|
<p className="text-muted-foreground mb-4">
|
|
|
|
Please log in to submit a script.
|
|
|
|
</p>
|
|
|
|
<Button asChild>
|
|
|
|
<a href="/login">Sign In</a>
|
|
|
|
</Button>
|
|
|
|
</CardContent>
|
|
|
|
</Card>
|
|
|
|
</main>
|
|
|
|
<Footer />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-08-12 22:31:26 +01:00
|
|
|
return (
|
|
|
|
<div className="min-h-screen flex flex-col bg-gradient-to-br from-background via-background to-primary/5">
|
|
|
|
<Header onSearch={() => {}} />
|
|
|
|
|
2025-08-13 00:51:44 +01:00
|
|
|
<main className="flex-1 container mx-auto px-4 py-8">
|
2025-08-12 22:31:26 +01:00
|
|
|
<div className="max-w-4xl mx-auto space-y-8">
|
2025-08-13 00:51:44 +01:00
|
|
|
{/* Header */}
|
2025-08-12 22:31:26 +01:00
|
|
|
<div className="text-center space-y-4">
|
2025-08-13 00:51:44 +01:00
|
|
|
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center">
|
|
|
|
<Code2 className="w-8 h-8 text-primary" />
|
|
|
|
</div>
|
|
|
|
<h1 className="text-4xl font-bold">Submit Your Script</h1>
|
|
|
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
|
|
|
Share your automation scripts with the community. Make sure to provide clear documentation and examples.
|
2025-08-12 22:31:26 +01:00
|
|
|
</p>
|
|
|
|
</div>
|
2025-08-13 00:51:44 +01:00
|
|
|
|
|
|
|
{/* Form */}
|
2025-08-12 22:31:26 +01:00
|
|
|
<Card>
|
2025-08-13 00:51:44 +01:00
|
|
|
<CardHeader>
|
|
|
|
<CardTitle>Script Information</CardTitle>
|
|
|
|
<CardDescription>
|
|
|
|
Fill in the details below to submit your script for review
|
|
|
|
</CardDescription>
|
|
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
|
|
{/* Basic Information */}
|
|
|
|
<div className="space-y-4">
|
|
|
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
|
|
<FileText className="h-5 w-5" />
|
|
|
|
Basic Information
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="name">Script Name *</Label>
|
|
|
|
<Input
|
|
|
|
id="name"
|
|
|
|
name="name"
|
|
|
|
value={formData.name}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="e.g., Docker Setup Script"
|
|
|
|
className={errors.name ? 'border-destructive' : ''}
|
|
|
|
/>
|
|
|
|
{errors.name && (
|
|
|
|
<p className="text-sm text-destructive">{errors.name}</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="version">Version</Label>
|
|
|
|
<Input
|
|
|
|
id="version"
|
|
|
|
name="version"
|
|
|
|
value={formData.version}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="1.0.0"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="description">Description *</Label>
|
|
|
|
<Textarea
|
|
|
|
id="description"
|
|
|
|
name="description"
|
|
|
|
value={formData.description}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="Describe what your script does, its purpose, and key features..."
|
|
|
|
rows={4}
|
|
|
|
className={errors.description ? 'border-destructive' : ''}
|
|
|
|
/>
|
|
|
|
{errors.description && (
|
|
|
|
<p className="text-sm text-destructive">{errors.description}</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
{/* Code Section */}
|
|
|
|
<div className="space-y-4">
|
|
|
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
|
|
<Code2 className="h-5 w-5" />
|
|
|
|
Script Code
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<Label htmlFor="code">Script Code *</Label>
|
|
|
|
<Button
|
|
|
|
type="button"
|
|
|
|
variant="outline"
|
|
|
|
size="sm"
|
|
|
|
onClick={() => setShowPreview(!showPreview)}
|
|
|
|
>
|
|
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
|
|
{showPreview ? 'Hide' : 'Show'} Preview
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
<Textarea
|
|
|
|
id="code"
|
|
|
|
name="code"
|
|
|
|
value={formData.code}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="Paste your script code here..."
|
|
|
|
rows={12}
|
|
|
|
className={`font-mono text-sm ${errors.code ? 'border-destructive' : ''}`}
|
|
|
|
/>
|
|
|
|
{errors.code && (
|
|
|
|
<p className="text-sm text-destructive">{errors.code}</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{showPreview && formData.code && (
|
|
|
|
<div className="p-4 bg-muted rounded-lg">
|
|
|
|
<h4 className="font-medium mb-2">Code Preview:</h4>
|
|
|
|
<pre className="text-sm overflow-x-auto">
|
|
|
|
<code>{formData.code}</code>
|
|
|
|
</pre>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
{/* Compatibility & Categories */}
|
|
|
|
<div className="space-y-4">
|
|
|
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
|
|
<Monitor className="h-5 w-5" />
|
|
|
|
Compatibility & Categories
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label>Compatible Operating Systems *</Label>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
{operatingSystems.map(os => (
|
|
|
|
<Button
|
|
|
|
key={os}
|
|
|
|
type="button"
|
|
|
|
variant={formData.compatibleOs.includes(os) ? "default" : "outline"}
|
|
|
|
size="sm"
|
|
|
|
onClick={() => toggleOs(os)}
|
|
|
|
>
|
|
|
|
{os}
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
{errors.compatibleOs && (
|
|
|
|
<p className="text-sm text-destructive">{errors.compatibleOs}</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label>Categories *</Label>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
{availableCategories.map(category => (
|
|
|
|
<Button
|
|
|
|
key={category}
|
|
|
|
type="button"
|
|
|
|
variant={formData.categories.includes(category) ? "default" : "outline"}
|
|
|
|
size="sm"
|
|
|
|
onClick={() => toggleCategory(category)}
|
|
|
|
>
|
|
|
|
{category}
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
<div className="flex gap-2 mt-2">
|
|
|
|
<Input
|
|
|
|
placeholder="Add custom category"
|
|
|
|
value={newCategory}
|
|
|
|
onChange={(e) => setNewCategory(e.target.value)}
|
|
|
|
className="max-w-xs"
|
|
|
|
/>
|
|
|
|
<Button type="button" variant="outline" size="sm" onClick={addCategory}>
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
{errors.categories && (
|
|
|
|
<p className="text-sm text-destructive">{errors.categories}</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label>Tags</Label>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
{formData.tags.map(tag => (
|
|
|
|
<Badge key={tag} variant="secondary" className="gap-1">
|
|
|
|
{tag}
|
|
|
|
<Button
|
|
|
|
type="button"
|
|
|
|
variant="ghost"
|
|
|
|
size="sm"
|
|
|
|
className="h-auto p-0 hover:bg-transparent"
|
|
|
|
onClick={() => removeTag(tag)}
|
|
|
|
>
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
</Button>
|
|
|
|
</Badge>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
|
|
<Input
|
|
|
|
placeholder="Add a tag"
|
|
|
|
value={newTag}
|
|
|
|
onChange={(e) => setNewTag(e.target.value)}
|
|
|
|
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addTag())}
|
|
|
|
className="max-w-xs"
|
|
|
|
/>
|
|
|
|
<Button type="button" variant="outline" size="sm" onClick={addTag}>
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
{/* Documentation */}
|
|
|
|
<div className="space-y-4">
|
|
|
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
|
|
<FileText className="h-5 w-5" />
|
|
|
|
Documentation
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="installation">Installation Instructions</Label>
|
|
|
|
<Textarea
|
|
|
|
id="installation"
|
|
|
|
name="installation"
|
|
|
|
value={formData.installation}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="How to install or set up the script..."
|
|
|
|
rows={4}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="usage">Usage Instructions</Label>
|
|
|
|
<Textarea
|
|
|
|
id="usage"
|
|
|
|
name="usage"
|
|
|
|
value={formData.usage}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="How to use the script, examples, parameters..."
|
|
|
|
rows={4}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="readme">README Content</Label>
|
|
|
|
<Textarea
|
|
|
|
id="readme"
|
|
|
|
name="readme"
|
|
|
|
value={formData.readme}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="Additional documentation, troubleshooting, contributing guidelines..."
|
|
|
|
rows={6}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
{/* Additional Information */}
|
|
|
|
<div className="space-y-4">
|
|
|
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
|
|
<Globe className="h-5 w-5" />
|
|
|
|
Additional Information
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="gitRepositoryUrl">Git Repository URL</Label>
|
|
|
|
<Input
|
|
|
|
id="gitRepositoryUrl"
|
|
|
|
name="gitRepositoryUrl"
|
|
|
|
type="url"
|
|
|
|
value={formData.gitRepositoryUrl}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
placeholder="https://github.com/username/repo"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label htmlFor="license">License</Label>
|
|
|
|
<select
|
|
|
|
id="license"
|
|
|
|
name="license"
|
|
|
|
value={formData.license}
|
|
|
|
onChange={handleSelectChange}
|
|
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
>
|
|
|
|
{licenses.map(license => (
|
|
|
|
<option key={license} value={license}>{license}</option>
|
|
|
|
))}
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Submit Button */}
|
|
|
|
<div className="flex justify-end gap-4 pt-6">
|
|
|
|
<Button
|
|
|
|
type="button"
|
|
|
|
variant="outline"
|
|
|
|
onClick={() => navigate('/my-scripts')}
|
|
|
|
>
|
|
|
|
Cancel
|
|
|
|
</Button>
|
|
|
|
<Button type="submit" disabled={isLoading}>
|
|
|
|
<Save className="h-4 w-4 mr-2" />
|
|
|
|
{isLoading ? 'Submitting...' : 'Submit Script'}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</form>
|
2025-08-12 22:31:26 +01:00
|
|
|
</CardContent>
|
|
|
|
</Card>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
|
|
|
|
<Footer />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|