Files
scriptshare-cursor-clone/src/pages/SubmitScript.tsx

541 lines
21 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
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-8">
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<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>
<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>
</main>
<Footer />
</div>
);
}