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:
2025-08-15 20:29:02 +01:00
parent 5fdfe3e790
commit ef211ebe0a
27 changed files with 3457 additions and 353 deletions

View File

@ -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
View 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>
);
}

View File

@ -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>

View File

@ -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 ||

View File

@ -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>