Enhance ESLint configuration for TypeScript and React, update dependencies, and add new super admin setup script. Update README for improved clarity on superuser setup options and modify user interface components for better user experience.
This commit is contained in:
@ -1,28 +1,534 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Upload } from 'lucide-react';
|
||||
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';
|
||||
|
||||
export default function SubmitScript() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 container mx-auto px-4 py-16">
|
||||
<main className="flex-1 container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4">
|
||||
<Upload className="h-16 w-16 text-primary mx-auto" />
|
||||
<h1 className="text-4xl font-bold">Submit Script</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Share your bash script with the community.
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Form */}
|
||||
<Card>
|
||||
<CardContent className="p-8">
|
||||
<p className="text-muted-foreground text-center">
|
||||
Script submission functionality coming soon! This will include forms for script details, content, and metadata.
|
||||
</p>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user