Update package dependencies, enhance README for clarity, and implement new features in the admin panel and script detail pages. Added support for collections, improved script submission previews, and refactored comment handling in the script detail view.
This commit is contained in:
@ -4,6 +4,8 @@ import { Footer } from '@/components/Footer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Shield, Users, BarChart3, FileText, ArrowLeft } from 'lucide-react';
|
||||
import { AdminDashboard } from '@/components/admin/AdminDashboard';
|
||||
import AnalyticsDashboard from '@/components/admin/AnalyticsDashboard';
|
||||
import ScriptReviewDashboard from '@/components/admin/ScriptReviewDashboard';
|
||||
import { CreateAdminForm } from '@/components/admin/CreateAdminForm';
|
||||
import { AdminUsersList } from '@/components/admin/AdminUsersList';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
@ -87,49 +89,9 @@ export default function AdminPanel() {
|
||||
</div>
|
||||
);
|
||||
case 'scripts':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentView('dashboard')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center py-12">
|
||||
<FileText className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Script Review</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Script review functionality coming soon!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <ScriptReviewDashboard onBack={() => setCurrentView('dashboard')} />;
|
||||
case 'analytics':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentView('dashboard')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center py-12">
|
||||
<BarChart3 className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Analytics Dashboard</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Analytics functionality coming soon!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <AnalyticsDashboard onBack={() => setCurrentView('dashboard')} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
342
src/pages/Collections.tsx
Normal file
342
src/pages/Collections.tsx
Normal file
@ -0,0 +1,342 @@
|
||||
import { useState } from 'react';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Plus,
|
||||
Folder,
|
||||
Users,
|
||||
Lock,
|
||||
FileText,
|
||||
Calendar,
|
||||
Search,
|
||||
Grid,
|
||||
List
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { usePublicCollections, useUserCollections, useCreateCollection } from '@/hooks/useCollections';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Collections() {
|
||||
const { user } = useAuth();
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Collection form data
|
||||
const [newCollection, setNewCollection] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
// API hooks
|
||||
const { data: publicCollections, isLoading: publicLoading } = usePublicCollections();
|
||||
const { data: userCollections, isLoading: userLoading } = useUserCollections(user?.id || '');
|
||||
const createCollection = useCreateCollection();
|
||||
|
||||
const handleCreateCollection = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user || !newCollection.name.trim()) return;
|
||||
|
||||
createCollection.mutate({
|
||||
name: newCollection.name,
|
||||
description: newCollection.description,
|
||||
authorId: user.id,
|
||||
isPublic: newCollection.isPublic,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setNewCollection({ name: '', description: '', isPublic: true });
|
||||
setShowCreateForm(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filteredPublicCollections = publicCollections?.filter(collection =>
|
||||
collection.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
collection.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gradient-to-br from-background via-background to-primary/5">
|
||||
<Header onSearch={setSearchQuery} />
|
||||
|
||||
<main className="flex-1 container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Script Collections</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Discover and manage curated collections of scripts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('grid')}
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('list')}
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{user && (
|
||||
<Button onClick={() => setShowCreateForm(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Collection
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search collections..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Create Collection Form */}
|
||||
{showCreateForm && user && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create New Collection</CardTitle>
|
||||
<CardDescription>
|
||||
Create a collection to organize and share your favorite scripts
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleCreateCollection} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Collection Name</label>
|
||||
<Input
|
||||
value={newCollection.name}
|
||||
onChange={(e) => setNewCollection(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Enter collection name..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Description (Optional)</label>
|
||||
<Textarea
|
||||
value={newCollection.description}
|
||||
onChange={(e) => setNewCollection(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Describe your collection..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isPublic"
|
||||
checked={newCollection.isPublic}
|
||||
onChange={(e) => setNewCollection(prev => ({ ...prev, isPublic: e.target.checked }))}
|
||||
/>
|
||||
<label htmlFor="isPublic" className="text-sm">
|
||||
Make this collection public
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" disabled={createCollection.isPending}>
|
||||
{createCollection.isPending ? 'Creating...' : 'Create Collection'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowCreateForm(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* User Collections */}
|
||||
{user && userCollections && userCollections.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">My Collections</h2>
|
||||
<div className={viewMode === 'grid'
|
||||
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
: "space-y-4"
|
||||
}>
|
||||
{userCollections.map((collection) => (
|
||||
<CollectionCard
|
||||
key={collection.id}
|
||||
collection={collection}
|
||||
viewMode={viewMode}
|
||||
isOwner={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Public Collections */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Public Collections</h2>
|
||||
|
||||
{publicLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="text-muted-foreground mt-2">Loading collections...</p>
|
||||
</div>
|
||||
) : filteredPublicCollections.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="text-center py-8">
|
||||
<Folder className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">No collections found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery ? 'No collections match your search.' : 'No public collections available yet.'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className={viewMode === 'grid'
|
||||
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
: "space-y-4"
|
||||
}>
|
||||
{filteredPublicCollections.map((collection) => (
|
||||
<CollectionCard
|
||||
key={collection.id}
|
||||
collection={collection}
|
||||
viewMode={viewMode}
|
||||
isOwner={collection.authorId === user?.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CollectionCardProps {
|
||||
collection: any;
|
||||
viewMode: 'grid' | 'list';
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
function CollectionCard({ collection, viewMode, isOwner }: CollectionCardProps) {
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Link to={`/collections/${collection.id}`}>
|
||||
<h3 className="text-lg font-semibold hover:text-primary transition-colors">
|
||||
{collection.name}
|
||||
</h3>
|
||||
</Link>
|
||||
{!collection.isPublic && <Lock className="h-4 w-4 text-muted-foreground" />}
|
||||
{isOwner && <Badge variant="secondary">Owner</Badge>}
|
||||
</div>
|
||||
|
||||
{collection.description && (
|
||||
<p className="text-muted-foreground mb-3">{collection.description}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
{collection.scripts?.length || 0} scripts
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{new Date(collection.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={collection.author?.avatarUrl} />
|
||||
<AvatarFallback>{collection.author?.displayName?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm font-medium">{collection.author?.displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CardTitle className="text-lg">
|
||||
<Link to={`/collections/${collection.id}`} className="hover:text-primary transition-colors">
|
||||
{collection.name}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
{!collection.isPublic && <Lock className="h-4 w-4 text-muted-foreground" />}
|
||||
</div>
|
||||
{isOwner && <Badge variant="secondary" className="w-fit">Owner</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{collection.description && (
|
||||
<CardDescription className="line-clamp-2">
|
||||
{collection.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
{collection.scripts?.length || 0} scripts
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{new Date(collection.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={collection.author?.avatarUrl} />
|
||||
<AvatarFallback>{collection.author?.displayName?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm font-medium">{collection.author?.displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -3,7 +3,6 @@ import { useParams, Link } 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
@ -12,7 +11,7 @@ import {
|
||||
Download,
|
||||
Star,
|
||||
Eye,
|
||||
MessageCircle,
|
||||
|
||||
Share2,
|
||||
Bookmark,
|
||||
Code2,
|
||||
@ -21,11 +20,16 @@ import {
|
||||
Tag,
|
||||
Copy,
|
||||
Check,
|
||||
ThumbsUp
|
||||
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { showSuccess } from '@/utils/toast';
|
||||
import { formatDate, formatRelativeTime, copyToClipboard } from '@/lib/utils';
|
||||
import { formatDate, copyToClipboard } from '@/lib/utils';
|
||||
import { useScript, useTrackView, useTrackDownload } from '@/hooks/useScripts';
|
||||
import { useUserRating, useRateScript, useScriptRatingStats } from '@/hooks/useRatings';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
|
||||
// Mock script data - in a real app, this would come from an API
|
||||
const mockScript = {
|
||||
@ -128,69 +132,43 @@ echo "Setting up Docker environment for $PROJECT_NAME..."
|
||||
changes: ['Initial release', 'Basic Docker setup functionality']
|
||||
}
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'comment1',
|
||||
author: {
|
||||
username: 'jane_smith',
|
||||
displayName: 'Jane Smith',
|
||||
avatarUrl: '',
|
||||
},
|
||||
content: 'This script saved me hours of setup time! Works perfectly on Ubuntu 22.04.',
|
||||
rating: 5,
|
||||
createdAt: '2024-01-14T15:30:00Z',
|
||||
likes: 8,
|
||||
replies: []
|
||||
},
|
||||
{
|
||||
id: 'comment2',
|
||||
author: {
|
||||
username: 'dev_mike',
|
||||
displayName: 'Mike Developer',
|
||||
avatarUrl: '',
|
||||
},
|
||||
content: 'Great script! I had to modify it slightly for my specific use case, but the structure is excellent.',
|
||||
rating: 4,
|
||||
createdAt: '2024-01-13T09:15:00Z',
|
||||
likes: 3,
|
||||
replies: [
|
||||
{
|
||||
id: 'reply1',
|
||||
author: {
|
||||
username: 'john_doe',
|
||||
displayName: 'John Doe',
|
||||
avatarUrl: '',
|
||||
},
|
||||
content: 'Thanks! What modifications did you make? I\'d love to incorporate them in the next version.',
|
||||
createdAt: '2024-01-13T10:00:00Z',
|
||||
likes: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
export default function ScriptDetail() {
|
||||
const { scriptId } = useParams();
|
||||
const { user } = useAuth();
|
||||
const [script] = useState(mockScript);
|
||||
const { theme } = useTheme();
|
||||
|
||||
// API hooks
|
||||
const { data: script, isLoading: scriptLoading } = useScript(scriptId || '');
|
||||
const { data: userRatingData } = useUserRating(scriptId || '', user?.id);
|
||||
const { data: ratingStats } = useScriptRatingStats(scriptId || '');
|
||||
const trackView = useTrackView();
|
||||
const trackDownload = useTrackDownload();
|
||||
const rateScript = useRateScript();
|
||||
|
||||
// Local state
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
const [userRating, setUserRating] = useState(0);
|
||||
const [showFullDescription, setShowFullDescription] = useState(false);
|
||||
const [showFullCode, setShowFullCode] = useState(false);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [isSubmittingComment, setIsSubmittingComment] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Use mock data for now, but ready for real API
|
||||
const displayScript = script || mockScript;
|
||||
const userRating = userRatingData?.rating || 0;
|
||||
|
||||
useEffect(() => {
|
||||
// In a real app, fetch script data based on scriptId
|
||||
console.log('Fetching script:', scriptId);
|
||||
}, [scriptId]);
|
||||
if (scriptId) {
|
||||
// Track view when component mounts
|
||||
trackView.mutate(scriptId);
|
||||
}
|
||||
}, [scriptId, trackView]);
|
||||
|
||||
const handleDownload = () => {
|
||||
// In a real app, this would trigger a download
|
||||
showSuccess('Download started!');
|
||||
if (scriptId) {
|
||||
trackDownload.mutate(scriptId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -201,15 +179,20 @@ export default function ScriptDetail() {
|
||||
};
|
||||
|
||||
const handleRating = (rating: number) => {
|
||||
setUserRating(rating);
|
||||
showSuccess(`Rated ${rating} stars`);
|
||||
if (!user || !scriptId) return;
|
||||
|
||||
rateScript.mutate({
|
||||
scriptId,
|
||||
userId: user.id,
|
||||
rating,
|
||||
});
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: script.name,
|
||||
text: script.description,
|
||||
title: displayScript.name,
|
||||
text: displayScript.description,
|
||||
url: window.location.href,
|
||||
});
|
||||
} catch (error) {
|
||||
@ -220,26 +203,13 @@ export default function ScriptDetail() {
|
||||
};
|
||||
|
||||
const handleCopyCode = async () => {
|
||||
await copyToClipboard(script.code);
|
||||
await copyToClipboard(displayScript.content || displayScript.code || '');
|
||||
setCopied(true);
|
||||
showSuccess('Code copied to clipboard!');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleCommentSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newComment.trim()) return;
|
||||
|
||||
setIsSubmittingComment(true);
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// In a real app, submit comment to API
|
||||
showSuccess('Comment submitted successfully!');
|
||||
setNewComment('');
|
||||
setIsSubmittingComment(false);
|
||||
};
|
||||
|
||||
const renderStars = (rating: number, interactive = false, onRatingChange?: (rating: number) => void) => {
|
||||
return (
|
||||
@ -275,7 +245,7 @@ export default function ScriptDetail() {
|
||||
<span className="mx-2">/</span>
|
||||
<Link to="/search" className="hover:text-foreground">Scripts</Link>
|
||||
<span className="mx-2">/</span>
|
||||
<span className="text-foreground">{script.name}</span>
|
||||
<span className="text-foreground">{displayScript.name}</span>
|
||||
</nav>
|
||||
|
||||
{/* Script Header */}
|
||||
@ -286,18 +256,18 @@ export default function ScriptDetail() {
|
||||
<div className="flex items-center gap-3">
|
||||
<Code2 className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
<CardTitle className="text-3xl">{script.name}</CardTitle>
|
||||
<CardTitle className="text-3xl">{displayScript.name}</CardTitle>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>v{script.version}</span>
|
||||
<span>v{displayScript.version}</span>
|
||||
<span>•</span>
|
||||
<span>{script.license} License</span>
|
||||
<span>{displayScript.license || 'MIT'} License</span>
|
||||
<span>•</span>
|
||||
<span>{script.size}</span>
|
||||
<span>{displayScript.size || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-lg max-w-3xl">
|
||||
{script.description}
|
||||
{displayScript.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@ -319,30 +289,30 @@ export default function ScriptDetail() {
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{script.viewCount.toLocaleString()} views</span>
|
||||
<span>{displayScript.viewCount.toLocaleString()} views</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Download className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{script.downloadCount.toLocaleString()} downloads</span>
|
||||
<span>{displayScript.downloadCount.toLocaleString()} downloads</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-current" />
|
||||
<span>{script.rating} ({script.ratingCount} ratings)</span>
|
||||
<span>{displayScript.rating} ({displayScript.ratingCount} ratings)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Updated {formatRelativeTime(script.lastUpdated)}</span>
|
||||
<span>Updated {formatDate(displayScript.updatedAt || displayScript.lastUpdated)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags and Categories */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{script.categories.map(category => (
|
||||
{displayScript.categories.map(category => (
|
||||
<Badge key={category} variant="secondary">
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
{script.tags.map(tag => (
|
||||
{displayScript.tags?.map(tag => (
|
||||
<Badge key={tag} variant="outline">
|
||||
<Tag className="h-3 w-3 mr-1" />
|
||||
{tag}
|
||||
@ -363,7 +333,7 @@ export default function ScriptDetail() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<p className="whitespace-pre-line">
|
||||
{showFullDescription ? script.longDescription : script.longDescription.slice(0, 300) + '...'}
|
||||
{showFullDescription ? displayScript.longDescription : displayScript.longDescription.slice(0, 300) + '...'}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@ -387,18 +357,32 @@ export default function ScriptDetail() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-muted rounded-lg p-4 font-mono text-sm overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{showFullCode ? script.code : script.code.slice(0, 500) + '...'}
|
||||
</pre>
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={theme === 'dark' ? vscDarkPlus : vs}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
showLineNumbers={true}
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{showFullCode
|
||||
? (displayScript.content || displayScript.code || '')
|
||||
: (displayScript.content || displayScript.code || '').slice(0, 1000) + (((displayScript.content || displayScript.code || '').length > 1000) ? '\n...' : '')
|
||||
}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
{!showFullCode && (
|
||||
{!showFullCode && (displayScript.content || displayScript.code || '').length > 1000 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mt-2"
|
||||
onClick={() => setShowFullCode(true)}
|
||||
>
|
||||
Show full code
|
||||
Show full code ({((displayScript.content || displayScript.code || '').length / 1000).toFixed(1)}k characters)
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
@ -412,20 +396,44 @@ export default function ScriptDetail() {
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Requirements</h4>
|
||||
<p className="text-sm text-muted-foreground">{script.requirements}</p>
|
||||
<p className="text-sm text-muted-foreground">{displayScript.requirements}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Installation</h4>
|
||||
<div className="bg-muted rounded-lg p-3 font-mono text-sm">
|
||||
<pre className="whitespace-pre-line">{script.installation}</pre>
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={theme === 'dark' ? vscDarkPlus : vs}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{displayScript.installation || ''}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Usage</h4>
|
||||
<div className="bg-muted rounded-lg p-3 font-mono text-sm">
|
||||
<pre className="whitespace-pre-line">{script.usage}</pre>
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={theme === 'dark' ? vscDarkPlus : vs}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{displayScript.usage || ''}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -438,7 +446,7 @@ export default function ScriptDetail() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{script.changelog.map((change, index) => (
|
||||
{displayScript.changelog.map((change, index) => (
|
||||
<div key={index} className="border-l-2 border-primary/20 pl-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="outline">v{change.version}</Badge>
|
||||
@ -460,116 +468,7 @@ export default function ScriptDetail() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Comments */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
Comments ({script.comments.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Add Comment */}
|
||||
{user && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatarUrl} />
|
||||
<AvatarFallback>{user.displayName[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-2">
|
||||
<form onSubmit={handleCommentSubmit}>
|
||||
<Input
|
||||
placeholder="Add a comment..."
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
disabled={isSubmittingComment}
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Rate this script:</span>
|
||||
{renderStars(userRating, true, handleRating)}
|
||||
</div>
|
||||
<Button type="submit" size="sm" disabled={isSubmittingComment || !newComment.trim()}>
|
||||
{isSubmittingComment ? 'Posting...' : 'Post Comment'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Comments List */}
|
||||
<div className="space-y-4">
|
||||
{script.comments.map((comment) => (
|
||||
<div key={comment.id} className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={comment.author.avatarUrl} />
|
||||
<AvatarFallback>{comment.author.displayName[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{comment.author.displayName}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatRelativeTime(comment.createdAt)}
|
||||
</span>
|
||||
{comment.rating > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
{renderStars(comment.rating)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm">{comment.content}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<button className="flex items-center gap-1 hover:text-foreground">
|
||||
<ThumbsUp className="h-3 w-3" />
|
||||
{comment.likes}
|
||||
</button>
|
||||
<button className="flex items-center gap-1 hover:text-foreground">
|
||||
<MessageCircle className="h-3 w-3" />
|
||||
Reply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Replies */}
|
||||
{comment.replies && comment.replies.length > 0 && (
|
||||
<div className="ml-11 space-y-3">
|
||||
{comment.replies.map((reply) => (
|
||||
<div key={reply.id} className="flex items-start gap-3">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={reply.author.avatarUrl} />
|
||||
<AvatarFallback>{reply.author.displayName[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{reply.author.displayName}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatRelativeTime(reply.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm">{reply.content}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<button className="flex items-center gap-1 hover:text-foreground">
|
||||
<ThumbsUp className="h-3 w-3" />
|
||||
{reply.likes}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
@ -582,15 +481,15 @@ export default function ScriptDetail() {
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={script.author.avatarUrl} />
|
||||
<AvatarFallback>{script.author.displayName[0]}</AvatarFallback>
|
||||
<AvatarImage src={displayScript.author.avatarUrl} />
|
||||
<AvatarFallback>{displayScript.author.displayName[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{script.author.displayName}</div>
|
||||
<div className="text-sm text-muted-foreground">@{script.author.username}</div>
|
||||
<div className="font-medium">{displayScript.author.displayName}</div>
|
||||
<div className="text-sm text-muted-foreground">@{displayScript.author.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
{script.author.isVerified && (
|
||||
{displayScript.author.isVerified && (
|
||||
<Badge variant="outline" className="w-fit">
|
||||
<User className="h-3 w-3 mr-1" />
|
||||
Verified Author
|
||||
@ -607,23 +506,23 @@ export default function ScriptDetail() {
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Version</span>
|
||||
<span className="font-medium">{script.version}</span>
|
||||
<span className="font-medium">{displayScript.version}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">License</span>
|
||||
<span className="font-medium">{script.license}</span>
|
||||
<span className="font-medium">{displayScript.license}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Size</span>
|
||||
<span className="font-medium">{script.size}</span>
|
||||
<span className="font-medium">{displayScript.size}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Created</span>
|
||||
<span className="font-medium">{formatDate(script.createdAt)}</span>
|
||||
<span className="font-medium">{formatDate(displayScript.createdAt)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Updated</span>
|
||||
<span className="font-medium">{formatDate(script.lastUpdated)}</span>
|
||||
<span className="font-medium">{formatDate(displayScript.lastUpdated)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -635,7 +534,7 @@ export default function ScriptDetail() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{script.compatibleOs.map(os => (
|
||||
{displayScript.compatibleOs.map(os => (
|
||||
<Badge key={os} variant="outline" className="w-full justify-center">
|
||||
{os}
|
||||
</Badge>
|
||||
@ -651,7 +550,7 @@ export default function ScriptDetail() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{script.dependencies.map(dep => (
|
||||
{displayScript.dependencies.map(dep => (
|
||||
<Badge key={dep} variant="secondary" className="w-full justify-center">
|
||||
{dep}
|
||||
</Badge>
|
||||
|
@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Search as SearchIcon, Filter, X, Code2 } from 'lucide-react';
|
||||
import { ScriptFilters } from '@/components/ScriptFilters';
|
||||
import { ScriptGrid } from '@/components/ScriptGrid';
|
||||
import { useScripts } from '@/hooks/useScripts';
|
||||
|
||||
// Mock search results - in a real app, this would come from an API
|
||||
const mockSearchResults = [
|
||||
@ -56,8 +57,6 @@ const mockSearchResults = [
|
||||
export default function Search() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '');
|
||||
const [searchResults, setSearchResults] = useState(mockSearchResults);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
os: [] as string[],
|
||||
@ -66,42 +65,24 @@ export default function Search() {
|
||||
recentlyUpdated: 'all',
|
||||
});
|
||||
|
||||
// API call with real filters
|
||||
const { data: scriptsData, isLoading } = useScripts({
|
||||
search: searchQuery || undefined,
|
||||
compatibleOs: filters.os.length > 0 ? filters.os : undefined,
|
||||
categories: filters.categories.length > 0 ? filters.categories : undefined,
|
||||
sortBy: 'newest',
|
||||
limit: 50,
|
||||
isApproved: true, // Only show approved scripts in search
|
||||
});
|
||||
|
||||
const searchResults = scriptsData?.scripts || [];
|
||||
|
||||
useEffect(() => {
|
||||
// Update search query when URL params change
|
||||
const query = searchParams.get('q') || '';
|
||||
setSearchQuery(query);
|
||||
if (query) {
|
||||
performSearch(query);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const performSearch = async (query: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Filter results based on query and filters
|
||||
let filtered = mockSearchResults.filter(script => {
|
||||
const matchesQuery = query === '' ||
|
||||
script.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
script.description.toLowerCase().includes(query.toLowerCase());
|
||||
|
||||
const matchesOs = filters.os.length === 0 ||
|
||||
filters.os.some(os => script.compatible_os.includes(os));
|
||||
|
||||
const matchesCategories = filters.categories.length === 0 ||
|
||||
filters.categories.some(cat => script.categories.includes(cat));
|
||||
|
||||
return matchesQuery && matchesOs && matchesCategories;
|
||||
});
|
||||
|
||||
|
||||
|
||||
setSearchResults(filtered);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (searchQuery.trim()) {
|
||||
@ -111,9 +92,6 @@ export default function Search() {
|
||||
|
||||
const handleFilterChange = (newFilters: typeof filters) => {
|
||||
setFilters(newFilters);
|
||||
if (searchQuery) {
|
||||
performSearch(searchQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
@ -123,9 +101,6 @@ export default function Search() {
|
||||
dateAdded: 'all',
|
||||
recentlyUpdated: 'all',
|
||||
});
|
||||
if (searchQuery) {
|
||||
performSearch(searchQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const hasActiveFilters = filters.os.length > 0 ||
|
||||
|
@ -12,6 +12,9 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { showSuccess, showError } from '@/utils/toast';
|
||||
import { generateId } from '@/lib/utils';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import {
|
||||
Code2,
|
||||
FileText,
|
||||
@ -26,6 +29,7 @@ import {
|
||||
export default function SubmitScript() {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
@ -48,6 +52,10 @@ export default function SubmitScript() {
|
||||
|
||||
const operatingSystems = ['Linux', 'macOS', 'Windows', 'BSD', 'Android'];
|
||||
const availableCategories = ['DevOps', 'Automation', 'System Admin', 'Development', 'Security', 'Networking', 'Data Processing', 'Web Development', 'Mobile Development', 'Game Development'];
|
||||
|
||||
// State for preview toggles
|
||||
const [showInstallationPreview, setShowInstallationPreview] = useState(false);
|
||||
const [showUsagePreview, setShowUsagePreview] = useState(false);
|
||||
const licenses = ['MIT', 'Apache 2.0', 'GPL v3', 'GPL v2', 'BSD 3-Clause', 'BSD 2-Clause', 'Unlicense', 'Custom'];
|
||||
|
||||
const validateForm = () => {
|
||||
@ -323,11 +331,24 @@ export default function SubmitScript() {
|
||||
</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 className="space-y-2">
|
||||
<h4 className="font-medium">Code Preview:</h4>
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={theme === 'dark' ? vscDarkPlus : vs}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
showLineNumbers={true}
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{formData.code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -438,7 +459,18 @@ export default function SubmitScript() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="installation">Installation Instructions</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="installation">Installation Instructions</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowInstallationPreview(!showInstallationPreview)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
{showInstallationPreview ? 'Hide' : 'Show'} Preview
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
id="installation"
|
||||
name="installation"
|
||||
@ -446,11 +478,40 @@ export default function SubmitScript() {
|
||||
onChange={handleInputChange}
|
||||
placeholder="How to install or set up the script..."
|
||||
rows={4}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
{showInstallationPreview && formData.installation && (
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={theme === 'dark' ? vscDarkPlus : vs}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{formData.installation}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="usage">Usage Instructions</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="usage">Usage Instructions</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowUsagePreview(!showUsagePreview)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
{showUsagePreview ? 'Hide' : 'Show'} Preview
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
id="usage"
|
||||
name="usage"
|
||||
@ -458,7 +519,25 @@ export default function SubmitScript() {
|
||||
onChange={handleInputChange}
|
||||
placeholder="How to use the script, examples, parameters..."
|
||||
rows={4}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
{showUsagePreview && formData.usage && (
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={theme === 'dark' ? vscDarkPlus : vs}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{formData.usage}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Reference in New Issue
Block a user