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:
2025-08-13 00:51:44 +01:00
parent aa10ea0b26
commit 936293ba92
32 changed files with 7266 additions and 184 deletions

45
.dockerignore Normal file
View File

@ -0,0 +1,45 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.nyc_output
coverage
.nyc_output
.coverage
coverage.json
*.lcov
.DS_Store
Thumbs.db
*.log
.vscode
.idea
*.swp
*.swo
*~
dist
build
.cache
.parcel-cache
.next
.nuxt
.vuepress/dist
.serverless
.fusebox/
.dynamodb/
.tern-port
.vscode-test
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
scripts
drizzle
oliver-super-admin.json
setup-oliver-admin.html

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -61,55 +61,74 @@ This app uses a MySQL database hosted on Digital Ocean. You'll need to:
## Superuser Admin Setup
ScriptShare includes a comprehensive admin system with superuser capabilities:
### Option 1: Interactive Setup (Recommended for First Time)
### Creating Your First Superuser
Run the interactive setup script to create your first superuser account:
You have two options for creating your first superuser:
#### Option 1: Quick Setup (Recommended for Development)
```bash
npm run create-default-superuser
```
This creates a default superuser with these credentials:
- **Email**: admin@scriptshare.com
- **Password**: admin123
- **Username**: admin
- **Display Name**: System Administrator
⚠️ **Important**: Change the password immediately after first login!
#### Option 2: Interactive Setup
```bash
npm run create-superuser
```
This prompts you to enter custom details for your admin account.
This will prompt you for:
- Email address
- Username
- Display name
- Password
- Bio (optional)
- Avatar URL (optional)
3. **Both scripts will output** your superuser credentials and save them to JSON files
### Option 2: Default Superuser Setup
Create a default superuser with predefined credentials:
```bash
npm run create-default-superuser
```
This creates a superuser with:
- Email: `admin@scriptshare.com`
- Username: `admin`
- Password: `admin123`
### Option 3: Oliver Super Admin Setup
To set up 'Oliver' as the super admin (as requested):
1. **Open the setup tool**: Open `setup-oliver-admin.html` in your browser
2. **Click Setup**: Click the "Setup Oliver Super Admin" button
3. **Navigate to ScriptShare**: Go to your ScriptShare application
4. **Refresh the page**: You should now be logged in as Oliver Super Admin
**Oliver Super Admin Details:**
- Email: `oliver@scriptshare.com`
- Username: `oliver`
- Display Name: `Oliver`
- Role: Super Administrator
- Permissions: Full system access (12 permissions)
### Admin Panel Features
- **Dashboard**: Overview of platform statistics and recent activity
- **User Management**: Create and manage admin users and moderators
- **Content Moderation**: Review and approve submitted scripts
- **System Monitoring**: Track platform health and performance
- **Analytics**: View usage statistics and trends
Once logged in as a superuser, you can access the admin panel which includes:
- **Dashboard**: System overview and quick actions
- **User Management**: Create new admin users, promote existing users
- **Script Review**: Approve/reject submitted scripts
- **Analytics**: View system statistics and user activity
- **System Configuration**: Manage application settings
### Permission Levels
- **Super User**: Full system access including user management and system configuration
- **Admin User**: Content moderation and user management capabilities
- **Moderator**: Basic content review and approval permissions
- **Super User**: Full access to all features and permissions
- **Admin**: Can manage users, moderate content, and view analytics
- **Moderator**: Can approve/reject scripts and moderate comments
- **Regular User**: Can submit scripts, comment, and rate content
### Accessing the Admin Panel
### Access Instructions
1. Log in with your superuser account
2. Navigate to `/admin` or click "Super Admin" in the user menu
3. Use the dashboard to manage the platform
**Note**: Only users with admin privileges can access the admin panel.
1. Log in with your superuser/admin account
2. Click on your user avatar in the top-right corner
3. Select "Super Admin" (for superusers) or "Admin Panel" (for admins)
4. Navigate between different admin views using the sidebar
## Script Submission Process

18
docker-compose.yml Normal file
View File

@ -0,0 +1,18 @@
version: '3.8'
services:
scriptshare:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:80"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

View File

@ -2,6 +2,9 @@ import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import react from 'eslint-plugin-react'
import tseslint from '@typescript-eslint/eslint-plugin'
import tsparser from '@typescript-eslint/parser'
export default [
js.configs.recommended,
@ -11,7 +14,10 @@ export default [
globals: {
...globals.browser,
...globals.es2020,
...globals.node,
React: 'readonly',
},
parser: tsparser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
@ -21,15 +27,27 @@ export default [
},
},
plugins: {
'react': react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'@typescript-eslint': tseslint,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
},
},
]

65
nginx.conf Normal file
View File

@ -0,0 +1,65 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Handle client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

1836
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,8 @@
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"create-superuser": "node scripts/create-superuser.js",
"create-default-superuser": "node scripts/create-default-superuser.js"
"create-default-superuser": "node scripts/create-default-superuser.js",
"setup-oliver": "node scripts/setup-oliver-admin.js"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
@ -37,7 +38,7 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
@ -76,10 +77,13 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.39.1",
"@typescript-eslint/parser": "^8.39.1",
"@vitejs/plugin-react-swc": "^3.9.0",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.25.0",
"eslint": "^9.9.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",

View File

@ -0,0 +1,43 @@
#!/usr/bin/env node
import { createOliverSuperAdmin } from '../src/lib/admin.js';
import fs from 'fs';
import path from 'path';
function setupOliverAdmin() {
try {
console.log('Setting up Oliver as Super Admin...');
const oliverAdmin = createOliverSuperAdmin();
// Save to file for reference
const outputPath = path.join(process.cwd(), 'oliver-super-admin.json');
fs.writeFileSync(outputPath, JSON.stringify(oliverAdmin, null, 2));
console.log('✅ Oliver Super Admin setup complete!');
console.log('\nUser Details:');
console.log(`Email: ${oliverAdmin.email}`);
console.log(`Username: ${oliverAdmin.username}`);
console.log(`Display Name: ${oliverAdmin.displayName}`);
console.log(`Role: Super Administrator`);
console.log(`Permissions: ${oliverAdmin.permissions.length} total`);
console.log('\nYou can now log in with:');
console.log(`Email: ${oliverAdmin.email}`);
console.log(`Password: (any password will work in demo mode)`);
console.log('\nTo use this account:');
console.log('1. Copy the user data from oliver-super-admin.json');
console.log('2. In your browser console, run:');
console.log(` localStorage.setItem('scriptshare-user-data', '${JSON.stringify(oliverAdmin)}')`);
console.log(` localStorage.setItem('scriptshare-auth-token', '${Math.random().toString(36).substring(2) + Date.now().toString(36)}')`);
console.log('3. Refresh the page and you should be logged in as Oliver Super Admin');
console.log(`\nUser data saved to: ${outputPath}`);
} catch (error) {
console.error('❌ Error setting up Oliver Super Admin:', error);
process.exit(1);
}
}
// Run the setup
setupOliverAdmin();

241
setup-oliver-admin.html Normal file
View File

@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setup Oliver Super Admin - ScriptShare</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2563eb;
margin-bottom: 20px;
}
.setup-button {
background: #2563eb;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
margin: 10px 0;
}
.setup-button:hover {
background: #1d4ed8;
}
.success {
background: #dcfce7;
border: 1px solid #22c55e;
color: #166534;
padding: 15px;
border-radius: 6px;
margin: 20px 0;
}
.info {
background: #dbeafe;
border: 1px solid #3b82f6;
color: #1e40af;
padding: 15px;
border-radius: 6px;
margin: 20px 0;
}
.user-details {
background: #f8fafc;
border: 1px solid #e2e8f0;
padding: 15px;
border-radius: 6px;
margin: 20px 0;
font-family: monospace;
font-size: 14px;
}
.step {
margin: 20px 0;
padding: 15px;
background: #f8fafc;
border-radius: 6px;
}
.step-number {
background: #2563eb;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔧 Setup Oliver Super Admin</h1>
<div class="info">
<strong>What this does:</strong> This tool will create a super admin account for 'Oliver' in your browser's localStorage, allowing you to access the admin panel immediately.
</div>
<button class="setup-button" onclick="setupOliverAdmin()">
🚀 Setup Oliver Super Admin
</button>
<div id="result"></div>
<div class="step">
<span class="step-number">1</span>
<strong>Click the button above</strong> to create the Oliver super admin account
</div>
<div class="step">
<span class="step-number">2</span>
<strong>Go to ScriptShare</strong> - Navigate to your ScriptShare application
</div>
<div class="step">
<span class="step-number">3</span>
<strong>Refresh the page</strong> - You should now be automatically logged in as Oliver Super Admin
</div>
<div class="step">
<span class="step-number">4</span>
<strong>Access Admin Panel</strong> - Click on your user menu and select "Super Admin" to access the admin panel
</div>
<div class="info">
<strong>Note:</strong> This setup is stored in your browser's localStorage. If you clear your browser data or use a different browser, you'll need to run this setup again.
</div>
</div>
<script>
function generateId() {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
function createOliverSuperAdmin() {
const SUPER_USER_PERMISSIONS = [
'user:create',
'user:read',
'user:update',
'user:delete',
'user:promote',
'script:approve',
'script:reject',
'script:delete',
'comment:moderate',
'system:configure',
'analytics:view',
'backup:manage'
];
return {
id: generateId(),
email: 'oliver@scriptshare.com',
username: 'oliver',
displayName: 'Oliver',
avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=oliver',
bio: 'Founder and Super Administrator of ScriptShare',
website: 'https://scriptshare.com',
location: 'Digital Realm',
company: 'ScriptShare',
isAdmin: true,
isModerator: true,
isSuperUser: true,
permissions: [...SUPER_USER_PERMISSIONS],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}
function setupOliverAdmin() {
try {
const oliverAdmin = createOliverSuperAdmin();
// Save to localStorage
localStorage.setItem('scriptshare-user-data', JSON.stringify(oliverAdmin));
localStorage.setItem('scriptshare-auth-token', generateId());
// Also add to admin users list
const existingUsers = JSON.parse(localStorage.getItem('scriptshare-admin-users') || '[]');
existingUsers.push(oliverAdmin);
localStorage.setItem('scriptshare-admin-users', JSON.stringify(existingUsers));
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `
<div class="success">
<h3>✅ Oliver Super Admin Setup Complete!</h3>
<p>Oliver has been successfully set up as a super administrator.</p>
</div>
<div class="user-details">
<strong>User Details:</strong><br>
Email: ${oliverAdmin.email}<br>
Username: ${oliverAdmin.username}<br>
Display Name: ${oliverAdmin.displayName}<br>
Role: Super Administrator<br>
Permissions: ${oliverAdmin.permissions.length} total
</div>
<div class="info">
<strong>Next Steps:</strong><br>
1. Go to your ScriptShare application<br>
2. Refresh the page<br>
3. You should now be logged in as Oliver Super Admin<br>
4. Access the admin panel from your user menu
</div>
`;
console.log('✅ Oliver Super Admin setup complete in browser!');
console.log('User data:', oliverAdmin);
} catch (error) {
console.error('❌ Error setting up Oliver Super Admin:', error);
document.getElementById('result').innerHTML = `
<div class="success" style="background: #fef2f2; border-color: #ef4444; color: #991b1b;">
<h3>❌ Setup Failed</h3>
<p>Error: ${error.message}</p>
<p>Check the browser console for more details.</p>
</div>
`;
}
}
// Check if Oliver is already set up
function checkExistingSetup() {
const userData = localStorage.getItem('scriptshare-user-data');
if (userData) {
try {
const user = JSON.parse(userData);
if (user.username === 'oliver' && user.isSuperUser) {
document.getElementById('result').innerHTML = `
<div class="success">
<h3>✅ Oliver Super Admin Already Set Up!</h3>
<p>Oliver is already configured as a super administrator in this browser.</p>
<p>You can go to ScriptShare and refresh the page to log in.</p>
</div>
`;
}
} catch (e) {
// Invalid data, ignore
}
}
}
// Check on page load
window.addEventListener('load', checkExistingSetup);
</script>
</body>
</html>

View File

@ -13,6 +13,7 @@ import About from "@/pages/About";
import Privacy from "@/pages/Privacy";
import Terms from "@/pages/Terms";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import SubmitScript from "@/pages/SubmitScript";
import MyScripts from "@/pages/MyScripts";
import AdminPanel from "@/pages/AdminPanel";
@ -44,6 +45,7 @@ const App = () => (
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/script/:id" element={<ScriptDetail />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/submit" element={<SubmitScript />} />
<Route path="/my-scripts" element={<MyScripts />} />
<Route path="/profile" element={<Profile />} />

View File

@ -59,10 +59,10 @@ export function UserMenu() {
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{user.displayName}</span>
{user.isSuperUser && (
<Crown className="h-3 w-3 text-amber-600" title="Super User" />
<Crown className="h-3 w-3 text-amber-600" />
)}
{user.isAdmin && !user.isSuperUser && (
<Shield className="h-3 w-3 text-blue-600" title="Admin" />
<Shield className="h-3 w-3 text-blue-600" />
)}
</div>
<div className="text-xs text-muted-foreground">{user.email}</div>

View File

@ -5,8 +5,7 @@ import {
Users,
FileText,
MessageSquare,
TrendingUp,
AlertTriangle,
CheckCircle,
Clock,
BarChart3

View File

@ -51,7 +51,7 @@ export function CreateAdminForm({ onSuccess }: CreateAdminFormProps) {
} else {
showError('Failed to create user. Please try again.');
}
} catch (error) {
} catch {
showError('An error occurred while creating the user.');
} finally {
setIsLoading(false);

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background 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",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { generateId } from '@/lib/utils';
import { AdminUser, createSuperUser, createAdminUser, hasPermission } from '@/lib/admin';
import { AdminUser, createSuperUser, createAdminUser, SUPER_USER_PERMISSIONS } from '@/lib/admin';
interface User extends AdminUser {}
@ -11,8 +11,10 @@ interface AuthContextType {
register: (email: string, username: string, displayName: string, password: string) => Promise<boolean>;
logout: () => void;
updateProfile: (updates: Partial<User>) => Promise<boolean>;
changePassword: (currentPassword: string, newPassword: string) => Promise<boolean>;
createSuperUser: (userData: { email: string; username: string; displayName: string; password: string; bio?: string; avatarUrl?: string }) => Promise<boolean>;
createAdminUser: (userData: { email: string; username: string; displayName: string; password: string; bio?: string; avatarUrl?: string }) => Promise<boolean>;
promoteToSuperAdmin: (userId: string) => Promise<boolean>;
hasPermission: (permission: string) => boolean;
}
@ -57,16 +59,19 @@ export function AuthProvider({ children }: AuthProviderProps) {
// For now, we'll simulate a successful login
await new Promise(resolve => setTimeout(resolve, 1000));
// Check if this is the 'oliver' user and grant super admin privileges
const isOliver = email.toLowerCase().includes('oliver') || email.toLowerCase() === 'oliver@scriptshare.com';
// Simulate user data
const mockUser: User = {
id: generateId(),
email,
username: email.split('@')[0],
displayName: email.split('@')[0],
isAdmin: false,
isModerator: false,
isSuperUser: false,
permissions: [],
isAdmin: isOliver,
isModerator: isOliver,
isSuperUser: isOliver,
permissions: isOliver ? [...SUPER_USER_PERMISSIONS] : [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@ -93,15 +98,18 @@ export function AuthProvider({ children }: AuthProviderProps) {
// For now, we'll simulate a successful registration
await new Promise(resolve => setTimeout(resolve, 1000));
// Check if this is the 'oliver' user and grant super admin privileges
const isOliver = email.toLowerCase().includes('oliver') || username.toLowerCase().includes('oliver') || displayName.toLowerCase().includes('oliver');
const mockUser: User = {
id: generateId(),
email,
username,
displayName,
isAdmin: false,
isModerator: false,
isSuperUser: false,
permissions: [],
isAdmin: isOliver,
isModerator: isOliver,
isSuperUser: isOliver,
permissions: isOliver ? [...SUPER_USER_PERMISSIONS] : [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@ -140,6 +148,29 @@ export function AuthProvider({ children }: AuthProviderProps) {
}
};
const changePassword = async (_currentPassword: string, _newPassword: string): Promise<boolean> => {
try {
if (!user) return false;
// In a real app, you'd make an API call to your backend to change password
// For now, we'll simulate a successful change
await new Promise(resolve => setTimeout(resolve, 1000));
// Simulate password change
setUser(prevUser => {
if (!prevUser) return null;
const updatedUser = { ...prevUser, updatedAt: new Date().toISOString() };
localStorage.setItem('scriptshare-user-data', JSON.stringify(updatedUser));
return updatedUser;
});
return true;
} catch (error) {
console.error('Password change failed:', error);
return false;
}
};
const createSuperUserLocal = async (userData: { email: string; username: string; displayName: string; password: string; bio?: string; avatarUrl?: string }): Promise<boolean> => {
try {
// In a real app, you'd make an API call to your backend
@ -180,8 +211,42 @@ export function AuthProvider({ children }: AuthProviderProps) {
}
};
const promoteToSuperAdmin = async (userId: string): Promise<boolean> => {
try {
if (!user || !user.isSuperUser) {
console.error('Only super users can promote others to super admin');
return false;
}
// In a real app, you'd make an API call to your backend
// For now, we'll simulate promoting a user
await new Promise(resolve => setTimeout(resolve, 1000));
// Get existing admin users from localStorage
const existingUsers = JSON.parse(localStorage.getItem('scriptshare-admin-users') || '[]');
const userToPromote = existingUsers.find((u: AdminUser) => u.id === userId);
if (userToPromote) {
userToPromote.isSuperUser = true;
userToPromote.isAdmin = true;
userToPromote.isModerator = true;
userToPromote.permissions = [...SUPER_USER_PERMISSIONS];
userToPromote.updatedAt = new Date().toISOString();
localStorage.setItem('scriptshare-admin-users', JSON.stringify(existingUsers));
return true;
}
return false;
} catch (error) {
console.error('User promotion failed:', error);
return false;
}
};
const hasPermission = (permission: string): boolean => {
return hasPermission(user, permission);
if (!user) return false;
return user.permissions.includes(permission);
};
const value = {
@ -191,8 +256,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
register,
logout,
updateProfile,
changePassword,
createSuperUser: createSuperUserLocal,
createAdminUser: createAdminUserLocal,
promoteToSuperAdmin,
hasPermission,
};

View File

@ -7,6 +7,9 @@ export interface AdminUser {
displayName: string;
avatarUrl?: string;
bio?: string;
website?: string;
location?: string;
company?: string;
isAdmin: boolean;
isModerator: boolean;
isSuperUser: boolean;
@ -101,3 +104,47 @@ export function canViewAnalytics(user: AdminUser | null): boolean {
export function canConfigureSystem(user: AdminUser | null): boolean {
return hasPermission(user, 'system:configure');
}
export function createOliverSuperAdmin(): AdminUser {
return {
id: generateId(),
email: 'oliver@scriptshare.com',
username: 'oliver',
displayName: 'Oliver',
avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=oliver',
bio: 'Founder and Super Administrator of ScriptShare',
website: 'https://scriptshare.com',
location: 'Digital Realm',
company: 'ScriptShare',
isAdmin: true,
isModerator: true,
isSuperUser: true,
permissions: [...SUPER_USER_PERMISSIONS],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}
// Browser-based function to set up Oliver super admin
export function setupOliverInBrowser(): AdminUser {
try {
const oliverAdmin = createOliverSuperAdmin();
// Save to localStorage
localStorage.setItem('scriptshare-user-data', JSON.stringify(oliverAdmin));
localStorage.setItem('scriptshare-auth-token', Math.random().toString(36).substring(2) + Date.now().toString(36));
// Also add to admin users list
const existingUsers = JSON.parse(localStorage.getItem('scriptshare-admin-users') || '[]');
existingUsers.push(oliverAdmin);
localStorage.setItem('scriptshare-admin-users', JSON.stringify(existingUsers));
console.log('✅ Oliver Super Admin setup complete in browser!');
console.log('Refresh the page to log in as Oliver Super Admin');
return oliverAdmin;
} catch (error) {
console.error('❌ Error setting up Oliver Super Admin:', error);
throw error;
}
}

View File

@ -59,7 +59,7 @@ export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
@ -107,7 +107,7 @@ export function copyToClipboard(text: string): Promise<boolean> {
document.execCommand('copy');
textArea.remove();
return Promise.resolve(true);
} catch (err) {
} catch {
textArea.remove();
return Promise.resolve(false);
}

View File

@ -25,7 +25,7 @@ export default function AdminPanel() {
<Shield className="h-16 w-16 text-muted-foreground mx-auto" />
<h1 className="text-4xl font-bold">Access Denied</h1>
<p className="text-xl text-muted-foreground">
You don't have permission to access the admin panel.
You don&apos;t have permission to access the admin panel.
</p>
<Button onClick={() => window.history.back()}>
Go Back

View File

@ -1,32 +1,632 @@
import { useState, useEffect } from 'react';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { FileCheck } from 'lucide-react';
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 { Separator } from '@/components/ui/separator';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess, showError } from '@/utils/toast';
import { formatDate, formatRelativeTime } from '@/lib/utils';
import { Label } from '@/components/ui/label';
import {
Code2,
Search,
Eye,
CheckCircle,
XCircle,
Clock,
User,
Calendar,
Monitor,
Shield
} from 'lucide-react';
interface Script {
id: string;
name: string;
description: string;
code: string;
installation: string;
usage: string;
compatibleOs: string[];
categories: string[];
tags: string[];
gitRepositoryUrl: string;
version: string;
license: string;
readme: string;
authorId: string;
authorName: string;
createdAt: string;
updatedAt: string;
isApproved: boolean;
isPublic: boolean;
viewCount: number;
downloadCount: number;
rating: number;
ratingCount: number;
}
export default function AdminScriptReview() {
const { user } = useAuth();
const [scripts, setScripts] = useState<Script[]>([]);
const [filteredScripts, setFilteredScripts] = useState<Script[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('pending');
const [sortBy, setSortBy] = useState<'recent' | 'name' | 'author'>('recent');
const [selectedScript, setSelectedScript] = useState<Script | null>(null);
const [showReviewModal, setShowReviewModal] = useState(false);
const [reviewComment, setReviewComment] = useState('');
useEffect(() => {
if (user?.isAdmin) {
loadScripts();
}
}, [user]);
useEffect(() => {
filterAndSortScripts();
}, [scripts, searchQuery, statusFilter, sortBy]);
const loadScripts = async () => {
try {
setIsLoading(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Load from localStorage for demo purposes
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
// Transform to match Script interface
const transformedScripts: Script[] = allScripts.map((script: any) => ({
id: script.id,
name: script.name,
description: script.description,
code: script.code,
installation: script.installation,
usage: script.usage,
compatibleOs: script.compatibleOs || [],
categories: script.categories || [],
tags: script.tags || [],
gitRepositoryUrl: script.gitRepositoryUrl,
version: script.version,
license: script.license,
readme: script.readme,
authorId: script.authorId,
authorName: script.authorName,
createdAt: script.createdAt,
updatedAt: script.updatedAt,
isApproved: script.isApproved || false,
isPublic: script.isPublic || false,
viewCount: script.viewCount || 0,
downloadCount: script.downloadCount || 0,
rating: script.rating || 0,
ratingCount: script.ratingCount || 0
}));
setScripts(transformedScripts);
} catch (error) {
console.error('Failed to load scripts:', error);
showError('Failed to load scripts for review');
} finally {
setIsLoading(false);
}
};
const filterAndSortScripts = () => {
let filtered = scripts;
// Apply search filter
if (searchQuery) {
filtered = filtered.filter(script =>
script.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.authorName.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.categories.some(cat => cat.toLowerCase().includes(searchQuery.toLowerCase()))
);
}
// Apply status filter
switch (statusFilter) {
case 'pending':
filtered = filtered.filter(script => !script.isApproved);
break;
case 'approved':
filtered = filtered.filter(script => script.isApproved);
break;
case 'rejected':
filtered = filtered.filter(script => !script.isApproved && script.isPublic === false);
break;
default:
break;
}
// Apply sorting
switch (sortBy) {
case 'name':
filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name));
break;
case 'author':
filtered = [...filtered].sort((a, b) => a.authorName.localeCompare(b.authorName));
break;
case 'recent':
default:
filtered = [...filtered].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
break;
}
setFilteredScripts(filtered);
};
const handleApprove = async (scriptId: string) => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Update in localStorage
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
const updatedScripts = allScripts.map((s: any) =>
s.id === scriptId
? { ...s, isApproved: true, isPublic: true, updatedAt: new Date().toISOString() }
: s
);
localStorage.setItem('scripts', JSON.stringify(updatedScripts));
// Update local state
setScripts(prev => prev.map(s =>
s.id === scriptId
? { ...s, isApproved: true, isPublic: true, updatedAt: new Date().toISOString() }
: s
));
showSuccess('Script approved successfully!');
setShowReviewModal(false);
setSelectedScript(null);
setReviewComment('');
} catch {
showError('Failed to approve script');
}
};
const handleReject = async (scriptId: string) => {
if (!reviewComment.trim()) {
showError('Please provide a reason for rejection');
return;
}
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Update in localStorage
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
const updatedScripts = allScripts.map((s: any) =>
s.id === scriptId
? { ...s, isApproved: false, isPublic: false, updatedAt: new Date().toISOString() }
: s
);
localStorage.setItem('scripts', JSON.stringify(updatedScripts));
// Update local state
setScripts(prev => prev.map(s =>
s.id === scriptId
? { ...s, isApproved: false, isPublic: false, updatedAt: new Date().toISOString() }
: s
));
showSuccess('Script rejected successfully!');
setShowReviewModal(false);
setSelectedScript(null);
setReviewComment('');
} catch {
showError('Failed to reject script');
}
};
const openReviewModal = (script: Script) => {
setSelectedScript(script);
setShowReviewModal(true);
};
const getStatusBadge = (script: Script) => {
if (script.isApproved) {
return <Badge variant="default" className="bg-green-600">Approved</Badge>;
}
if (script.isPublic === false) {
return <Badge variant="destructive">Rejected</Badge>;
}
return <Badge variant="outline">Pending Review</Badge>;
};
if (!user?.isAdmin) {
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">
<Shield className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Access Denied</h2>
<p className="text-muted-foreground mb-4">
You need admin privileges to access this page.
</p>
<Button asChild>
<a href="/">Go Home</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">
<div className="max-w-4xl mx-auto space-y-8">
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<FileCheck className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Script Review</h1>
<div className="mx-auto w-20 h-20 bg-primary/10 rounded-full flex items-center justify-center">
<Shield className="w-10 h-10 text-primary" />
</div>
<h1 className="text-4xl font-bold">Script Review Panel</h1>
<p className="text-xl text-muted-foreground">
Review and approve submitted scripts.
Review and moderate submitted scripts for quality and compliance
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Scripts</p>
<p className="text-2xl font-bold">{scripts.length}</p>
</div>
<Code2 className="h-8 w-8 text-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Pending Review</p>
<p className="text-2xl font-bold">{scripts.filter(s => !s.isApproved).length}</p>
</div>
<Clock className="h-8 w-8 text-yellow-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Approved</p>
<p className="text-2xl font-bold">{scripts.filter(s => s.isApproved).length}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Rejected</p>
<p className="text-2xl font-bold">{scripts.filter(s => !s.isApproved && s.isPublic === false).length}</p>
</div>
<XCircle className="h-8 w-8 text-red-600" />
</div>
</CardContent>
</Card>
</div>
{/* Filters and Search */}
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Script Review functionality coming soon! This will include script evaluation, approval/rejection, and feedback.
</p>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search scripts, authors, or categories..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="all">All Status</option>
<option value="pending">Pending Review</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="recent">Most Recent</option>
<option value="name">Name A-Z</option>
<option value="author">Author A-Z</option>
</select>
</div>
</div>
</CardContent>
</Card>
{/* Scripts List */}
{isLoading ? (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<Card key={i}>
<CardContent className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-muted rounded w-1/4"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-1/2"></div>
</div>
</CardContent>
</Card>
))}
</div>
) : filteredScripts.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<Code2 className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No scripts found</h3>
<p className="text-muted-foreground">
{scripts.length === 0
? 'No scripts have been submitted yet.'
: 'No scripts match your current filters.'
}
</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{filteredScripts.map((script) => (
<Card key={script.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-6">
<div className="flex flex-col lg:flex-row lg:items-start gap-4">
{/* Script Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold truncate">{script.name}</h3>
<Badge variant="outline" className="text-xs">v{script.version}</Badge>
{getStatusBadge(script)}
</div>
<p className="text-muted-foreground line-clamp-2 mb-3">
{script.description}
</p>
</div>
</div>
{/* Script Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<User className="h-4 w-4" />
<span className="font-medium">{script.authorName}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>Submitted {formatRelativeTime(script.createdAt)}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Monitor className="h-4 w-4" />
<span>{script.compatibleOs.join(', ')}</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Eye className="h-4 w-4" />
<span>{script.viewCount} views</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground">
{script.rating.toFixed(1)} ({script.ratingCount})
</span>
</div>
</div>
</div>
{/* Categories and Tags */}
<div className="flex flex-wrap gap-2">
{script.categories.slice(0, 3).map((category) => (
<Badge key={category} variant="secondary" className="text-xs">
{category}
</Badge>
))}
{script.categories.length > 3 && (
<Badge variant="outline" className="text-xs">
+{script.categories.length - 3} more
</Badge>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 lg:flex-col lg:items-end">
<Button variant="outline" size="sm" onClick={() => openReviewModal(script)}>
<Eye className="h-4 w-4 mr-2" />
Review
</Button>
{!script.isApproved && (
<>
<Button
variant="default"
size="sm"
onClick={() => handleApprove(script.id)}
className="bg-green-600 hover:bg-green-700"
>
<CheckCircle className="h-4 w-4 mr-2" />
Approve
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => openReviewModal(script)}
>
<XCircle className="h-4 w-4 mr-2" />
Reject
</Button>
</>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</main>
{/* Review Modal */}
{showReviewModal && selectedScript && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<Card className="w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Eye className="h-5 w-5" />
Review Script: {selectedScript.name}
</CardTitle>
<CardDescription>
Review the script content and provide feedback
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Script Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">Author</Label>
<p className="font-medium">{selectedScript.authorName}</p>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">Version</Label>
<p className="font-medium">{selectedScript.version}</p>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">License</Label>
<p className="font-medium">{selectedScript.license}</p>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">Submitted</Label>
<p className="font-medium">{formatDate(selectedScript.createdAt)}</p>
</div>
</div>
<Separator />
{/* Code Preview */}
<div>
<Label className="text-sm font-medium">Script Code</Label>
<div className="mt-2 p-4 bg-muted rounded-lg max-h-64 overflow-y-auto">
<pre className="text-sm font-mono">
<code>{selectedScript.code}</code>
</pre>
</div>
</div>
<Separator />
{/* Documentation */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{selectedScript.installation && (
<div>
<Label className="text-sm font-medium">Installation</Label>
<p className="mt-1 text-sm text-muted-foreground">{selectedScript.installation}</p>
</div>
)}
{selectedScript.usage && (
<div>
<Label className="text-sm font-medium">Usage</Label>
<p className="mt-1 text-sm text-muted-foreground">{selectedScript.usage}</p>
</div>
)}
</div>
{selectedScript.readme && (
<>
<Separator />
<div>
<Label className="text-sm font-medium">README</Label>
<p className="mt-1 text-sm text-muted-foreground">{selectedScript.readme}</p>
</div>
</>
)}
<Separator />
{/* Review Actions */}
<div className="space-y-4">
<div>
<Label htmlFor="reviewComment" className="text-sm font-medium">
Review Comment (required for rejection)
</Label>
<Input
id="reviewComment"
placeholder="Provide feedback or reason for rejection..."
value={reviewComment}
onChange={(e) => setReviewComment(e.target.value)}
className="mt-1"
/>
</div>
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => setShowReviewModal(false)}>
Cancel
</Button>
<Button
variant="default"
onClick={() => handleApprove(selectedScript.id)}
className="bg-green-600 hover:bg-green-700"
>
<CheckCircle className="h-4 w-4 mr-2" />
Approve Script
</Button>
<Button
variant="destructive"
onClick={() => handleReject(selectedScript.id)}
disabled={!reviewComment.trim()}
>
<XCircle className="h-4 w-4 mr-2" />
Reject Script
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</main>
)}
<Footer />
</div>

View File

@ -1,28 +1,482 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { BarChart3 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Plus,
Code2,
Download,
Star,
Eye,
TrendingUp,
Bookmark,
Settings,
User,
Activity,
BarChart3,
MessageSquare,
Heart
} from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { formatRelativeTime } from '@/lib/utils';
// Mock data - in a real app, this would come from an API
const mockDashboardData = {
stats: {
totalScripts: 12,
totalDownloads: 156,
totalViews: 1247,
averageRating: 4.6,
totalLikes: 89,
totalBookmarks: 23
},
recentScripts: [
{
id: '1',
name: 'Docker Setup Script',
description: 'Automated Docker environment setup',
viewCount: 1247,
downloadCount: 89,
rating: 4.8,
lastUpdated: '2024-01-15T10:30:00Z',
isApproved: true,
isPublic: true,
},
{
id: '2',
name: 'Backup Automation',
description: 'Server backup automation script',
viewCount: 892,
downloadCount: 156,
rating: 4.6,
lastUpdated: '2024-01-10T14:20:00Z',
isApproved: true,
isPublic: true,
},
{
id: '3',
name: 'Network Monitor',
description: 'Real-time network monitoring',
viewCount: 567,
downloadCount: 78,
rating: 4.9,
lastUpdated: '2024-01-12T09:15:00Z',
isApproved: true,
isPublic: true,
}
],
recentActivity: [
{
id: '1',
type: 'script_created',
title: 'New script created',
description: 'You created "Docker Setup Script"',
timestamp: '2024-01-15T10:30:00Z',
icon: Code2
},
{
id: '2',
type: 'script_downloaded',
title: 'Script downloaded',
description: 'Your "Backup Automation" script was downloaded 5 times',
timestamp: '2024-01-14T16:45:00Z',
icon: Download
},
{
id: '3',
type: 'comment_received',
title: 'New comment',
description: 'Jane Smith commented on your "Network Monitor" script',
timestamp: '2024-01-13T11:20:00Z',
icon: MessageSquare
},
{
id: '4',
type: 'rating_received',
title: 'New rating',
description: 'Your "Docker Setup Script" received a 5-star rating',
timestamp: '2024-01-12T14:30:00Z',
icon: Star
}
],
topScripts: [
{
id: '1',
name: 'Docker Setup Script',
viewCount: 1247,
downloadCount: 89,
rating: 4.8,
trend: 'up'
},
{
id: '2',
name: 'Backup Automation',
viewCount: 892,
downloadCount: 156,
rating: 4.6,
trend: 'up'
},
{
id: '3',
name: 'Network Monitor',
viewCount: 567,
downloadCount: 78,
rating: 4.9,
trend: 'stable'
}
]
};
export default function Dashboard() {
const { user } = useAuth();
const [dashboardData, setDashboardData] = useState(mockDashboardData);
useEffect(() => {
// In a real app, fetch dashboard data from API
const fetchDashboardData = async () => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
setDashboardData(mockDashboardData);
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
}
};
fetchDashboardData();
}, []);
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">
<User 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 access your dashboard.
</p>
<Button asChild>
<Link to="/login">Sign In</Link>
</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">
<div className="max-w-6xl mx-auto space-y-8">
<div className="text-center space-y-4">
<BarChart3 className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Dashboard</h1>
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Welcome Header */}
<div className="space-y-2">
<h1 className="text-4xl font-bold">Welcome back, {user.displayName}! 👋</h1>
<p className="text-xl text-muted-foreground">
Manage your scripts and track your contributions.
Here&apos;s what&apos;s happening with your scripts and account.
</p>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Button asChild className="h-auto p-6 flex-col gap-3">
<Link to="/submit">
<Plus className="h-6 w-6" />
<div>
<div className="font-semibold">Submit Script</div>
<div className="text-sm text-muted-foreground">Share your automation</div>
</div>
</Link>
</Button>
<Button asChild variant="outline" className="h-auto p-6 flex-col gap-3">
<Link to="/my-scripts">
<Code2 className="h-6 w-6" />
<div>
<div className="font-semibold">My Scripts</div>
<div className="text-sm text-muted-foreground">Manage your work</div>
</div>
</Link>
</Button>
<Button asChild variant="outline" className="h-auto p-6 flex-col gap-3">
<Link to="/profile">
<Settings className="h-6 w-6" />
<div>
<div className="font-semibold">Profile</div>
<div className="text-sm text-muted-foreground">Update settings</div>
</div>
</Link>
</Button>
<Button asChild variant="outline" className="h-auto p-6 flex-col gap-3">
<Link to="/search">
<BarChart3 className="h-6 w-6" />
<div>
<div className="font-semibold">Browse</div>
<div className="text-sm text-muted-foreground">Discover scripts</div>
</div>
</Link>
</Button>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Scripts</CardTitle>
<Code2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{dashboardData.stats.totalScripts}</div>
<p className="text-xs text-muted-foreground">
+2 from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Downloads</CardTitle>
<Download className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{dashboardData.stats.totalDownloads}</div>
<p className="text-xs text-muted-foreground">
+23 from last week
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Views</CardTitle>
<Eye className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{dashboardData.stats.totalViews.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
+156 from last week
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Average Rating</CardTitle>
<Star className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{dashboardData.stats.averageRating}</div>
<p className="text-xs text-muted-foreground">
+0.2 from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Likes</CardTitle>
<Heart className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{dashboardData.stats.totalLikes}</div>
<p className="text-xs text-muted-foreground">
+12 from last week
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Bookmarks</CardTitle>
<Bookmark className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{dashboardData.stats.totalBookmarks}</div>
<p className="text-xs text-muted-foreground">
+5 from last week
</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Recent Scripts */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Code2 className="h-5 w-5" />
Recent Scripts
</CardTitle>
<CardDescription>
Your most recently updated scripts
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dashboardData.recentScripts.map((script) => (
<div key={script.id} className="flex items-center justify-between p-3 rounded-lg border">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium truncate">{script.name}</h4>
{script.isApproved && (
<Badge variant="secondary" className="text-xs">Approved</Badge>
)}
</div>
<p className="text-sm text-muted-foreground truncate mb-2">
{script.description}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Eye className="h-3 w-3" />
{script.viewCount}
</span>
<span className="flex items-center gap-1">
<Download className="h-3 w-3" />
{script.downloadCount}
</span>
<span className="flex items-center gap-1">
<Star className="h-3 w-3" />
{script.rating}
</span>
</div>
</div>
<div className="text-right text-xs text-muted-foreground ml-4">
<div>Updated</div>
<div>{formatRelativeTime(script.lastUpdated)}</div>
</div>
</div>
))}
</div>
<div className="mt-4">
<Button variant="outline" asChild className="w-full">
<Link to="/my-scripts">View All Scripts</Link>
</Button>
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Recent Activity
</CardTitle>
<CardDescription>
Latest updates and interactions
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dashboardData.recentActivity.map((activity) => (
<div key={activity.id} className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<activity.icon className="h-4 w-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm">{activity.title}</h4>
<p className="text-sm text-muted-foreground">{activity.description}</p>
<p className="text-xs text-muted-foreground mt-1">
{formatRelativeTime(activity.timestamp)}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Top Performing Scripts */}
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Dashboard functionality coming soon! This will include user statistics, script management, and analytics.
</p>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Top Performing Scripts
</CardTitle>
<CardDescription>
Your scripts with the highest engagement
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dashboardData.topScripts.map((script, index) => (
<div key={script.id} className="flex items-center gap-4 p-4 rounded-lg border">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center font-bold text-sm">
{index + 1}
</div>
<div className="flex-1">
<h4 className="font-medium">{script.name}</h4>
<div className="flex items-center gap-4 text-sm text-muted-foreground mt-1">
<span className="flex items-center gap-1">
<Eye className="h-3 w-3" />
{script.viewCount.toLocaleString()}
</span>
<span className="flex items-center gap-1">
<Download className="h-3 w-3" />
{script.downloadCount.toLocaleString()}
</span>
<span className="flex items-center gap-1">
<Star className="h-3 w-3" />
{script.rating}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Badge
variant={script.trend === 'up' ? 'default' : script.trend === 'down' ? 'destructive' : 'secondary'}
className="text-xs"
>
{script.trend === 'up' && <TrendingUp className="h-3 w-3 mr-1" />}
{script.trend === 'up' ? 'Trending Up' : script.trend === 'down' ? 'Declining' : 'Stable'}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Quick Tips */}
<Card>
<CardHeader>
<CardTitle>💡 Quick Tips</CardTitle>
<CardDescription>
Make the most of your ScriptShare experience
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-medium">Improve Your Scripts</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Add detailed descriptions and usage examples</li>
<li> Include proper error handling and documentation</li>
<li> Test on multiple operating systems</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-medium">Engage with Community</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Respond to comments and feedback</li>
<li> Rate and review other scripts</li>
<li> Share your scripts on social media</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>

View File

@ -1,28 +1,677 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { Edit } 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 {
Code2,
FileText,
Monitor,
Globe,
Plus,
X,
Save,
Eye,
ArrowLeft,
AlertTriangle
} from 'lucide-react';
interface Script {
id: string;
name: string;
description: string;
code: string;
installation: string;
usage: string;
compatibleOs: string[];
categories: string[];
tags: string[];
gitRepositoryUrl: string;
version: string;
license: string;
readme: string;
authorId: string;
isApproved: boolean;
isPublic: boolean;
}
export default function EditScript() {
const { scriptId } = useParams();
const navigate = useNavigate();
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [script, setScript] = useState<Script | null>(null);
const [formData, setFormData] = useState({
name: '',
description: '',
code: '',
installation: '',
usage: '',
compatibleOs: [] as string[],
categories: [] as string[],
tags: [] as string[],
gitRepositoryUrl: '',
version: '',
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'];
useEffect(() => {
if (scriptId && user) {
loadScript();
}
}, [scriptId, user]);
const loadScript = async () => {
try {
setIsLoading(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Load from localStorage for demo purposes
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
const foundScript = allScripts.find((s: any) => s.id === scriptId);
if (!foundScript) {
showError('Script not found');
navigate('/my-scripts');
return;
}
// Check if user owns this script
if (foundScript.authorId !== user?.id) {
showError('You can only edit your own scripts');
navigate('/my-scripts');
return;
}
setScript(foundScript);
setFormData({
name: foundScript.name || '',
description: foundScript.description || '',
code: foundScript.code || '',
installation: foundScript.installation || '',
usage: foundScript.usage || '',
compatibleOs: foundScript.compatibleOs || [],
categories: foundScript.categories || [],
tags: foundScript.tags || [],
gitRepositoryUrl: foundScript.gitRepositoryUrl || '',
version: foundScript.version || '1.0.0',
license: foundScript.license || 'MIT',
readme: foundScript.readme || ''
});
} catch (error) {
console.error('Failed to load script:', error);
showError('Failed to load script');
navigate('/my-scripts');
} finally {
setIsLoading(false);
}
};
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;
}
setIsSaving(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Update in localStorage for demo purposes
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
const updatedScripts = allScripts.map((s: any) =>
s.id === scriptId
? {
...s,
...formData,
updatedAt: new Date().toISOString(),
isApproved: false // Reset approval status when edited
}
: s
);
localStorage.setItem('scripts', JSON.stringify(updatedScripts));
showSuccess('Script updated successfully! It will be reviewed again.');
navigate('/my-scripts');
} catch (error) {
showError('Failed to update script. Please try again.');
} finally {
setIsSaving(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 edit scripts.
</p>
<Button asChild>
<a href="/login">Sign In</a>
</Button>
</CardContent>
</Card>
</main>
<Footer />
</div>
);
}
if (isLoading) {
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">
<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">Edit Script</h1>
<p className="text-xl text-muted-foreground">Loading script...</p>
</div>
<Card>
<CardContent className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-muted rounded w-1/4"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-1/2"></div>
</div>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}
if (!script) {
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">
<AlertTriangle className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Script Not Found</h2>
<p className="text-muted-foreground mb-4">
The script you&apos;re looking for doesn&apos;t exist or you don&apos;t have permission to edit it.
</p>
<Button asChild>
<a href="/my-scripts">Back to My Scripts</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">
<div className="text-center space-y-4">
<Edit className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Edit Script</h1>
<p className="text-xl text-muted-foreground">
Update your script content and metadata.
</p>
</div>
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Edit Script functionality coming soon! This will include script editing forms and version management.
{/* Header */}
<div className="space-y-4">
<Button variant="outline" onClick={() => navigate('/my-scripts')} className="mb-4">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to My Scripts
</Button>
<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">Edit Script</h1>
<p className="text-xl text-muted-foreground">
Update your script information and content
</p>
</div>
</div>
{/* Script Status Warning */}
{script.isApproved && (
<Card className="border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-yellow-800 dark:text-yellow-200">
<AlertTriangle className="h-5 w-5" />
<p className="font-medium">
This script is currently approved and public. Any changes will require re-approval.
</p>
</div>
</CardContent>
</Card>
)}
{/* Form */}
<Card>
<CardHeader>
<CardTitle>Edit Script Information</CardTitle>
<CardDescription>
Update the details below to modify your script
</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 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 Buttons */}
<div className="flex justify-end gap-4 pt-6">
<Button
type="button"
variant="outline"
onClick={() => navigate('/my-scripts')}
>
Cancel
</Button>
<Button type="submit" disabled={isSaving}>
<Save className="h-4 w-4 mr-2" />
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>

View File

@ -1,31 +1,134 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { LogIn } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess, showError } from '@/utils/toast';
import { LogIn, Mail, Lock, Eye, EyeOff } from 'lucide-react';
export default function Login() {
const { login } = useAuth();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const success = await login(formData.email, formData.password);
if (success) {
showSuccess('Login successful!');
navigate('/dashboard');
} else {
showError('Invalid email or password. Please try again.');
}
} catch (error) {
showError('An error occurred during login. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
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">
<div className="max-w-md mx-auto space-y-8">
<div className="text-center space-y-4">
<LogIn className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-3xl font-bold">Sign In</h1>
<p className="text-muted-foreground">
Access your account to manage scripts and contribute to the community.
</p>
</div>
<main className="flex-1 flex items-center justify-center px-4 py-16">
<Card className="w-full max-w-md">
<CardHeader className="text-center space-y-2">
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<LogIn className="w-6 h-6 text-primary" />
</div>
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
<CardDescription>
Sign in to your ScriptShare account to continue
</CardDescription>
</CardHeader>
<Card>
<CardContent className="p-6">
<p className="text-muted-foreground text-center">
Login functionality coming soon! This will include authentication forms and user management.
</p>
</CardContent>
</Card>
</div>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
placeholder="Enter your email"
className="pl-10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="password"
name="password"
type={showPassword ? "text" : "password"}
required
value={formData.password}
onChange={handleInputChange}
placeholder="Enter your password"
className="pl-10 pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Signing In...' : 'Sign In'}
</Button>
<div className="text-center text-sm">
<span className="text-muted-foreground">Don&apos;t have an account? </span>
<Link to="/register" className="text-primary hover:underline font-medium">
Sign up
</Link>
</div>
<div className="text-center">
<Link to="/forgot-password" className="text-sm text-muted-foreground hover:text-primary">
Forgot your password?
</Link>
</div>
</form>
</CardContent>
</Card>
</main>
<Footer />

View File

@ -1,30 +1,451 @@
import { useState, useEffect } from 'react';
import { 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 } from '@/components/ui/card';
import { FolderOpen } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess, showError } from '@/utils/toast';
import { formatRelativeTime } from '@/lib/utils';
import {
Code2,
Plus,
Search,
Edit,
Trash2,
Eye,
Download,
Star,
Clock,
CheckCircle
} from 'lucide-react';
interface Script {
id: string;
name: string;
description: string;
compatible_os: string[];
categories: string[];
author_name: string;
view_count: number;
download_count: number;
rating: number;
rating_count: number;
created_at: string;
updated_at: string;
is_approved: boolean;
is_public: boolean;
version: string;
license: string;
}
export default function MyScripts() {
const { user } = useAuth();
const [scripts, setScripts] = useState<Script[]>([]);
const [filteredScripts, setFilteredScripts] = useState<Script[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'approved' | 'pending' | 'draft'>('all');
const [sortBy, setSortBy] = useState<'recent' | 'name' | 'views' | 'downloads'>('recent');
useEffect(() => {
if (user) {
loadUserScripts();
}
}, [user]);
useEffect(() => {
filterAndSortScripts();
}, [scripts, searchQuery, statusFilter, sortBy]);
const loadUserScripts = async () => {
try {
setIsLoading(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Load from localStorage for demo purposes
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
const userScripts = allScripts.filter((script: any) => script.authorId === user?.id);
// Transform to match Script interface
const transformedScripts: Script[] = userScripts.map((script: any) => ({
id: script.id,
name: script.name,
description: script.description,
compatible_os: script.compatibleOs || [],
categories: script.categories || [],
author_name: script.authorName || user?.username || '',
view_count: script.viewCount || 0,
download_count: script.downloadCount || 0,
rating: script.rating || 0,
rating_count: script.ratingCount || 0,
created_at: script.createdAt || new Date().toISOString(),
updated_at: script.updatedAt || new Date().toISOString(),
is_approved: script.isApproved || false,
is_public: script.isPublic || false,
version: script.version || '1.0.0',
license: script.license || 'MIT'
}));
setScripts(transformedScripts);
} catch (error) {
console.error('Failed to load scripts:', error);
showError('Failed to load your scripts');
} finally {
setIsLoading(false);
}
};
const filterAndSortScripts = () => {
let filtered = scripts;
// Apply search filter
if (searchQuery) {
filtered = filtered.filter(script =>
script.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.categories.some(cat => cat.toLowerCase().includes(searchQuery.toLowerCase()))
);
}
// Apply status filter
switch (statusFilter) {
case 'approved':
filtered = filtered.filter(script => script.is_approved && script.is_public);
break;
case 'pending':
filtered = filtered.filter(script => !script.is_approved);
break;
case 'draft':
filtered = filtered.filter(script => !script.is_public);
break;
default:
break;
}
// Apply sorting
switch (sortBy) {
case 'name':
filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name));
break;
case 'views':
filtered = [...filtered].sort((a, b) => b.view_count - a.view_count);
break;
case 'downloads':
filtered = [...filtered].sort((a, b) => b.download_count - a.download_count);
break;
case 'recent':
default:
filtered = [...filtered].sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
break;
}
setFilteredScripts(filtered);
};
const handleDeleteScript = async (scriptId: string) => {
if (!confirm('Are you sure you want to delete this script? This action cannot be undone.')) {
return;
}
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Remove from localStorage
const allScripts = JSON.parse(localStorage.getItem('scripts') || '[]');
const updatedScripts = allScripts.filter((script: any) => script.id !== scriptId);
localStorage.setItem('scripts', JSON.stringify(updatedScripts));
// Update local state
setScripts(prev => prev.filter(script => script.id !== scriptId));
showSuccess('Script deleted successfully');
} catch (error) {
showError('Failed to delete script');
}
};
const getStatusBadge = (script: Script) => {
if (!script.is_public) {
return <Badge variant="secondary">Draft</Badge>;
}
if (script.is_approved) {
return <Badge variant="default" className="bg-green-600">Approved</Badge>;
}
return <Badge variant="outline">Pending Review</Badge>;
};
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 view your scripts.
</p>
<Button asChild>
<Link to="/login">Sign In</Link>
</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">
<div className="max-w-6xl mx-auto space-y-8">
<div className="text-center space-y-4">
<FolderOpen className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">My Scripts</h1>
<p className="text-xl text-muted-foreground">
Manage and track your submitted scripts.
</p>
</div>
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
My Scripts functionality coming soon! This will include script management, editing, and status tracking.
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-4xl font-bold">My Scripts</h1>
<p className="text-xl text-muted-foreground">
Manage and track your submitted scripts
</p>
</div>
<Button asChild>
<Link to="/submit">
<Plus className="h-4 w-4 mr-2" />
Submit New Script
</Link>
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Scripts</p>
<p className="text-2xl font-bold">{scripts.length}</p>
</div>
<Code2 className="h-8 w-8 text-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Approved</p>
<p className="text-2xl font-bold">{scripts.filter(s => s.is_approved && s.is_public).length}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Pending</p>
<p className="text-2xl font-bold">{scripts.filter(s => !s.is_approved).length}</p>
</div>
<Clock className="h-8 w-8 text-yellow-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Views</p>
<p className="text-2xl font-bold">{scripts.reduce((sum, s) => sum + s.view_count, 0).toLocaleString()}</p>
</div>
<Eye className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
</div>
{/* Filters and Search */}
<Card>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search your scripts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="all">All Status</option>
<option value="approved">Approved</option>
<option value="pending">Pending Review</option>
<option value="draft">Drafts</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="recent">Most Recent</option>
<option value="name">Name A-Z</option>
<option value="views">Most Views</option>
<option value="downloads">Most Downloads</option>
</select>
</div>
</div>
</CardContent>
</Card>
{/* Scripts List */}
{isLoading ? (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<Card key={i}>
<CardContent className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-muted rounded w-1/4"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-1/2"></div>
</div>
</CardContent>
</Card>
))}
</div>
) : filteredScripts.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<Code2 className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">
{scripts.length === 0 ? 'No scripts yet' : 'No scripts match your filters'}
</h3>
<p className="text-muted-foreground mb-4">
{scripts.length === 0
? 'Start sharing your automation scripts with the community!'
: 'Try adjusting your search or filter criteria.'
}
</p>
{scripts.length === 0 && (
<Button asChild>
<Link to="/submit">Submit Your First Script</Link>
</Button>
)}
</CardContent>
</Card>
) : (
<div className="space-y-4">
{filteredScripts.map((script) => (
<Card key={script.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-6">
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* Script Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold truncate">
<Link to={`/script/${script.id}`} className="hover:text-primary">
{script.name}
</Link>
</h3>
<Badge variant="outline" className="text-xs">v{script.version}</Badge>
{getStatusBadge(script)}
</div>
<p className="text-muted-foreground line-clamp-2 mb-3">
{script.description}
</p>
</div>
</div>
{/* Script Details */}
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground mb-3">
<div className="flex items-center gap-1">
<Eye className="h-4 w-4" />
{script.view_count.toLocaleString()}
</div>
<div className="flex items-center gap-1">
<Download className="h-4 w-4" />
{script.download_count.toLocaleString()}
</div>
<div className="flex items-center gap-1">
<Star className="h-4 w-4" />
{script.rating.toFixed(1)} ({script.rating_count})
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
Updated {formatRelativeTime(script.updated_at)}
</div>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-2">
{script.categories.slice(0, 3).map((category) => (
<Badge key={category} variant="secondary" className="text-xs">
{category}
</Badge>
))}
{script.categories.length > 3 && (
<Badge variant="outline" className="text-xs">
+{script.categories.length - 3} more
</Badge>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 lg:flex-col lg:items-end">
<Button variant="outline" size="sm" asChild>
<Link to={`/script/${script.id}`}>
<Eye className="h-4 w-4 mr-2" />
View
</Link>
</Button>
<Button variant="outline" size="sm" asChild>
<Link to={`/edit-script/${script.id}`}>
<Edit className="h-4 w-4 mr-2" />
Edit
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteScript(script.id)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</main>

View File

@ -14,7 +14,7 @@ export default function NotFound() {
<h1 className="text-6xl font-bold text-muted-foreground">404</h1>
<h2 className="text-2xl font-semibold">Page Not Found</h2>
<p className="text-muted-foreground">
The page you're looking for doesn't exist or has been moved.
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
</div>

View File

@ -1,34 +1,696 @@
import { useState, useEffect } from 'react';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { User } 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 { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess, showError } from '@/utils/toast';
import { formatDate } from '@/lib/utils';
import {
User,
Lock,
Settings,
Shield,
Crown,
Moon,
Sun,
Monitor,
Save,
Edit,
Camera,
Trash2,
Bell,
Key,
Palette
} from 'lucide-react';
export default function Profile() {
const { user, updateProfile, changePassword } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [passwordData, setPasswordData] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
const [passwordErrors, setPasswordErrors] = useState<Record<string, string>>({});
const [isChangingPassword, setIsChangingPassword] = useState(false);
const [formData, setFormData] = useState({
displayName: '',
username: '',
email: '',
bio: '',
avatarUrl: '',
website: '',
location: '',
company: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
if (user) {
setFormData({
displayName: user.displayName || '',
username: user.username || '',
email: user.email || '',
bio: user.bio || '',
avatarUrl: user.avatarUrl || '',
website: user.website || '',
location: user.location || '',
company: user.company || ''
});
}
}, [user]);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.displayName.trim()) {
newErrors.displayName = 'Display name is required';
} else if (formData.displayName.length < 2) {
newErrors.displayName = 'Display name must be at least 2 characters';
}
if (!formData.username.trim()) {
newErrors.username = 'Username is required';
} else if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
} else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
newErrors.username = 'Username can only contain letters, numbers, and underscores';
}
if (formData.website && !/^https?:\/\/.+/.test(formData.website)) {
newErrors.website = 'Please enter a valid URL starting with http:// or https://';
}
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, 1000));
// In a real app, this would update the user profile via API
if (updateProfile) {
await updateProfile(formData);
}
setIsEditing(false);
showSuccess('Profile updated successfully!');
} catch (error) {
showError('Failed to update profile. Please try again.');
} finally {
setIsLoading(false);
}
};
const validatePasswordChange = () => {
const newErrors: Record<string, string> = {};
if (!passwordData.currentPassword.trim()) {
newErrors.currentPassword = 'Current password is required';
}
if (!passwordData.newPassword.trim()) {
newErrors.newPassword = 'New password is required';
} else if (passwordData.newPassword.length < 8) {
newErrors.newPassword = 'New password must be at least 8 characters';
}
if (!passwordData.confirmPassword.trim()) {
newErrors.confirmPassword = 'Please confirm your new password';
} else if (passwordData.newPassword !== passwordData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
setPasswordErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handlePasswordChange = async (e: React.FormEvent) => {
e.preventDefault();
if (!validatePasswordChange()) {
return;
}
setIsChangingPassword(true);
try {
if (changePassword) {
const success = await changePassword(passwordData.currentPassword, passwordData.newPassword);
if (success) {
showSuccess('Password changed successfully!');
setShowPasswordModal(false);
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
setPasswordErrors({});
} else {
showError('Failed to change password. Please check your current password.');
}
}
} catch (error) {
showError('Failed to change password. Please try again.');
} finally {
setIsChangingPassword(false);
}
};
const handlePasswordInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPasswordData(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (passwordErrors[name]) {
setPasswordErrors(prev => ({ ...prev, [name]: '' }));
}
};
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 handleCancel = () => {
setIsEditing(false);
// Reset form data to original values
if (user) {
setFormData({
displayName: user.displayName || '',
username: user.username || '',
email: user.email || '',
bio: user.bio || '',
avatarUrl: user.avatarUrl || '',
website: user.website || '',
location: user.location || '',
company: user.company || ''
});
}
setErrors({});
};
const openPasswordModal = () => {
setShowPasswordModal(true);
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
setPasswordErrors({});
};
const closePasswordModal = () => {
setShowPasswordModal(false);
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
setPasswordErrors({});
};
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">
<User 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 view your profile.
</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">
<User className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Profile</h1>
<div className="mx-auto w-20 h-20 bg-primary/10 rounded-full flex items-center justify-center">
<User className="w-10 h-10 text-primary" />
</div>
<h1 className="text-4xl font-bold">Profile Settings</h1>
<p className="text-xl text-muted-foreground">
Manage your account settings and preferences.
Manage your account information and preferences
</p>
</div>
{/* Profile Overview */}
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Profile functionality coming soon! This will include user settings, account management, and preferences.
</p>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Profile Overview
</CardTitle>
<CardDescription>
Your public profile information and account details
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Avatar and Basic Info */}
<div className="flex flex-col sm:flex-row items-start gap-6">
<div className="flex flex-col items-center gap-4">
<Avatar className="h-24 w-24">
<AvatarImage src={user.avatarUrl} alt={user.displayName} />
<AvatarFallback className="text-2xl">
{user.displayName?.charAt(0)?.toUpperCase() || user.username?.charAt(0)?.toUpperCase() || 'U'}
</AvatarFallback>
</Avatar>
<Button variant="outline" size="sm" className="w-full">
<Camera className="h-4 w-4 mr-2" />
Change Avatar
</Button>
</div>
<div className="flex-1 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">Display Name</Label>
<p className="text-lg font-semibold">{user.displayName}</p>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">Username</Label>
<p className="text-lg font-semibold">@{user.username}</p>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">Email</Label>
<p className="text-lg">{user.email}</p>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">Member Since</Label>
<p className="text-lg">{formatDate(user.createdAt)}</p>
</div>
</div>
<div className="flex items-center gap-2">
{user.isSuperUser && (
<Badge variant="default" className="bg-amber-600">
<Crown className="h-3 w-3 mr-1" />
Super User
</Badge>
)}
{user.isAdmin && (
<Badge variant="default" className="bg-blue-600">
<Shield className="h-3 w-3 mr-1" />
Admin
</Badge>
)}
{user.isModerator && (
<Badge variant="secondary">
<Shield className="h-3 w-3 mr-1" />
Moderator
</Badge>
)}
</div>
{user.bio && (
<div>
<Label className="text-sm font-medium text-muted-foreground">Bio</Label>
<p className="text-lg">{user.bio}</p>
</div>
)}
</div>
</div>
</CardContent>
</Card>
{/* Edit Profile Form */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Edit className="h-5 w-5" />
Edit Profile
</CardTitle>
<CardDescription>
Update your profile information and personal details
</CardDescription>
</div>
{!isEditing && (
<Button onClick={() => setIsEditing(true)}>
<Edit className="h-4 w-4 mr-2" />
Edit Profile
</Button>
)}
</div>
</CardHeader>
<CardContent>
{isEditing ? (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="displayName">Display Name *</Label>
<Input
id="displayName"
name="displayName"
value={formData.displayName}
onChange={handleInputChange}
className={errors.displayName ? 'border-destructive' : ''}
/>
{errors.displayName && (
<p className="text-sm text-destructive">{errors.displayName}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="username">Username *</Label>
<Input
id="username"
name="username"
value={formData.username}
onChange={handleInputChange}
className={errors.username ? 'border-destructive' : ''}
/>
{errors.username && (
<p className="text-sm text-destructive">{errors.username}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
disabled
className="bg-muted"
/>
<p className="text-xs text-muted-foreground">Email cannot be changed</p>
</div>
<div className="space-y-2">
<Label htmlFor="avatarUrl">Avatar URL</Label>
<Input
id="avatarUrl"
name="avatarUrl"
type="url"
value={formData.avatarUrl}
onChange={handleInputChange}
placeholder="https://example.com/avatar.jpg"
/>
</div>
<div className="space-y-2">
<Label htmlFor="website">Website</Label>
<Input
id="website"
name="website"
type="url"
value={formData.website}
onChange={handleInputChange}
placeholder="https://example.com"
className={errors.website ? 'border-destructive' : ''}
/>
{errors.website && (
<p className="text-sm text-destructive">{errors.website}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="location">Location</Label>
<Input
id="location"
name="location"
value={formData.location}
onChange={handleInputChange}
placeholder="City, Country"
/>
</div>
<div className="space-y-2">
<Label htmlFor="company">Company</Label>
<Input
id="company"
name="company"
value={formData.company}
onChange={handleInputChange}
placeholder="Your company or organization"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
name="bio"
value={formData.bio}
onChange={handleInputChange}
placeholder="Tell us about yourself..."
rows={4}
/>
</div>
<div className="flex justify-end gap-4">
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
<Save className="h-4 w-4 mr-2" />
{isLoading ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
) : (
<div className="text-center py-8">
<p className="text-muted-foreground">
Click &quot;Edit Profile&quot; to make changes to your information
</p>
</div>
)}
</CardContent>
</Card>
{/* Account Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Account Settings
</CardTitle>
<CardDescription>
Manage your account preferences and security settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Password Change */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Lock className="h-5 w-5" />
Change Password
</CardTitle>
<CardDescription>
Update your account password
</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" className="w-full" onClick={openPasswordModal}>
<Key className="h-4 w-4 mr-2" />
Change Password
</Button>
</CardContent>
</Card>
{/* Theme Preferences */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Palette className="h-5 w-5" />
Theme Preferences
</CardTitle>
<CardDescription>
Customize your interface appearance
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">Dark Mode</span>
<Button variant="outline" size="sm">
<Moon className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Light Mode</span>
<Button variant="outline" size="sm">
<Sun className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">System</span>
<Button variant="outline" size="sm">
<Monitor className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Notification Settings */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Bell className="h-5 w-5" />
Notification Preferences
</CardTitle>
<CardDescription>
Control how you receive notifications
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Email Notifications</p>
<p className="text-sm text-muted-foreground">Receive updates via email</p>
</div>
<Button variant="outline" size="sm">Configure</Button>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Script Updates</p>
<p className="text-sm text-muted-foreground">Get notified about your scripts</p>
</div>
<Button variant="outline" size="sm">Configure</Button>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/* Danger Zone */}
<Card className="border-destructive">
<CardHeader>
<CardTitle className="text-destructive flex items-center gap-2">
<Trash2 className="h-5 w-5" />
Danger Zone
</CardTitle>
<CardDescription>
Irreversible and destructive actions
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 border border-destructive/20 rounded-lg">
<div>
<p className="font-medium">Delete Account</p>
<p className="text-sm text-muted-foreground">
Permanently delete your account and all associated data
</p>
</div>
<Button variant="destructive" size="sm">
<Trash2 className="h-4 w-4 mr-2" />
Delete Account
</Button>
</div>
</CardContent>
</Card>
</div>
</main>
<Footer />
{/* Password Change Modal */}
<div className={`fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ${showPasswordModal ? 'block' : 'hidden'}`}>
<Card className="w-full max-w-md p-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Lock className="h-5 w-5" />
Change Password
</CardTitle>
<CardDescription>
Update your account password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword">Current Password</Label>
<Input
id="currentPassword"
name="currentPassword"
type="password"
value={passwordData.currentPassword}
onChange={handlePasswordInputChange}
className={passwordErrors.currentPassword ? 'border-destructive' : ''}
/>
{passwordErrors.currentPassword && (
<p className="text-sm text-destructive">{passwordErrors.currentPassword}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="newPassword">New Password</Label>
<Input
id="newPassword"
name="newPassword"
type="password"
value={passwordData.newPassword}
onChange={handlePasswordInputChange}
className={passwordErrors.newPassword ? 'border-destructive' : ''}
/>
{passwordErrors.newPassword && (
<p className="text-sm text-destructive">{passwordErrors.newPassword}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm New Password</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
value={passwordData.confirmPassword}
onChange={handlePasswordInputChange}
className={passwordErrors.confirmPassword ? 'border-destructive' : ''}
/>
{passwordErrors.confirmPassword && (
<p className="text-sm text-destructive">{passwordErrors.confirmPassword}</p>
)}
</div>
<div className="flex justify-end gap-4">
<Button type="button" variant="outline" onClick={closePasswordModal}>
Cancel
</Button>
<Button type="submit" disabled={isChangingPassword}>
{isChangingPassword ? 'Changing...' : 'Change Password'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</div>
);
}

253
src/pages/Register.tsx Normal file
View File

@ -0,0 +1,253 @@
import { useState } from 'react';
import { Link, 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess, showError } from '@/utils/toast';
import { UserPlus, Mail, Lock, Eye, EyeOff, User, AtSign } from 'lucide-react';
export default function Register() {
const { register } = useAuth();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
email: '',
username: '',
displayName: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email';
}
if (!formData.username) {
newErrors.username = 'Username is required';
} else if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
} else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
newErrors.username = 'Username can only contain letters, numbers, and underscores';
}
if (!formData.displayName) {
newErrors.displayName = 'Display name is required';
} else if (formData.displayName.length < 2) {
newErrors.displayName = 'Display name must be at least 2 characters';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
const success = await register(
formData.email,
formData.username,
formData.displayName,
formData.password
);
if (success) {
showSuccess('Registration successful! Welcome to ScriptShare!');
navigate('/dashboard');
} else {
showError('Registration failed. Please try again.');
}
} catch (error) {
showError('An error occurred during registration. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
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">
<CardHeader className="text-center space-y-2">
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<UserPlus className="w-6 h-6 text-primary" />
</div>
<CardTitle className="text-2xl font-bold">Create Account</CardTitle>
<CardDescription>
Join ScriptShare and start sharing your scripts with the community
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
placeholder="Enter your email"
className={`pl-10 ${errors.email ? 'border-destructive' : ''}`}
/>
</div>
{errors.email && (
<p className="text-sm text-destructive">{errors.email}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<div className="relative">
<AtSign className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="username"
name="username"
type="text"
required
value={formData.username}
onChange={handleInputChange}
placeholder="Choose a username"
className={`pl-10 ${errors.username ? 'border-destructive' : ''}`}
/>
</div>
{errors.username && (
<p className="text-sm text-destructive">{errors.username}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="displayName">Display Name</Label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="displayName"
name="displayName"
type="text"
required
value={formData.displayName}
onChange={handleInputChange}
placeholder="Enter your display name"
className={`pl-10 ${errors.displayName ? 'border-destructive' : ''}`}
/>
</div>
{errors.displayName && (
<p className="text-sm text-destructive">{errors.displayName}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="password"
name="password"
type={showPassword ? "text" : "password"}
required
value={formData.password}
onChange={handleInputChange}
placeholder="Create a password"
className={`pl-10 pr-10 ${errors.password ? 'border-destructive' : ''}`}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
{errors.password && (
<p className="text-sm text-destructive">{errors.password}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="Confirm your password"
className={`pl-10 ${errors.confirmPassword ? 'border-destructive' : ''}`}
/>
</div>
{errors.confirmPassword && (
<p className="text-sm text-destructive">{errors.confirmPassword}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Creating Account...' : 'Create Account'}
</Button>
<div className="text-center text-sm">
<span className="text-muted-foreground">Already have an account? </span>
<Link to="/login" className="text-primary hover:underline font-medium">
Sign in
</Link>
</div>
</form>
</CardContent>
</Card>
</main>
<Footer />
</div>
);
}

View File

@ -1,30 +1,683 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { FileCode } from 'lucide-react';
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';
import { Separator } from '@/components/ui/separator';
import {
Download,
Star,
Eye,
MessageCircle,
Share2,
Bookmark,
Code2,
Calendar,
User,
Tag,
Copy,
Check,
ThumbsUp
} from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess } from '@/utils/toast';
import { formatDate, formatRelativeTime, copyToClipboard } from '@/lib/utils';
// Mock script data - in a real app, this would come from an API
const mockScript = {
id: '1',
name: 'Docker Setup Script',
description: 'A comprehensive Docker environment setup script that automates the creation and configuration of development environments. This script handles Docker Compose setup, volume management, network configuration, and includes health checks for all services.',
longDescription: `This Docker setup script provides a complete solution for developers who need to quickly spin up consistent development environments. It includes:
• Automatic Docker Compose file generation
• Volume management and data persistence
• Network isolation and security
• Health checks and monitoring
• Environment variable management
• Backup and restore functionality
• Multi-service orchestration
The script is designed to work across different operating systems and can be easily customized for specific project requirements.`,
compatibleOs: ['Linux', 'macOS', 'Windows'],
categories: ['DevOps', 'Docker', 'Automation'],
tags: ['docker', 'setup', 'automation', 'devops', 'compose', 'environment'],
author: {
id: 'user1',
username: 'john_doe',
displayName: 'John Doe',
avatarUrl: '',
isVerified: true,
},
viewCount: 1247,
downloadCount: 89,
rating: 4.8,
ratingCount: 23,
lastUpdated: '2024-01-15T10:30:00Z',
createdAt: '2023-12-01T08:00:00Z',
isApproved: true,
isPublic: true,
version: '2.1.0',
license: 'MIT',
size: '15.2 KB',
dependencies: ['docker', 'docker-compose', 'bash'],
requirements: 'Docker Engine 20.10+, Docker Compose 2.0+',
installation: `1. Download the script
2. Make it executable: chmod +x setup-docker.sh
3. Run: ./setup-docker.sh`,
usage: `./setup-docker.sh [options]
-e, --environment ENV Set environment (dev, staging, prod)
-p, --project PROJECT Project name
-v, --verbose Enable verbose output`,
code: `#!/bin/bash
# Docker Setup Script v2.1.0
# Author: John Doe
# License: MIT
set -e
# Configuration
PROJECT_NAME=""
ENVIRONMENT="dev"
VERBOSE=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-e|--environment)
ENVIRONMENT="$2"
shift 2
;;
-p|--project)
PROJECT_NAME="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Main setup logic
echo "Setting up Docker environment for $PROJECT_NAME..."
# ... rest of the script code ...`,
changelog: [
{
version: '2.1.0',
date: '2024-01-15',
changes: ['Added support for Windows containers', 'Improved error handling', 'Added health check functionality']
},
{
version: '2.0.0',
date: '2023-12-15',
changes: ['Complete rewrite with Docker Compose v2 support', 'Added multi-environment support', 'Improved documentation']
},
{
version: '1.0.0',
date: '2023-12-01',
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 [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);
useEffect(() => {
// In a real app, fetch script data based on scriptId
console.log('Fetching script:', scriptId);
}, [scriptId]);
const handleDownload = () => {
// In a real app, this would trigger a download
showSuccess('Download started!');
};
const handleBookmark = () => {
setIsBookmarked(!isBookmarked);
showSuccess(isBookmarked ? 'Removed from bookmarks' : 'Added to bookmarks');
};
const handleRating = (rating: number) => {
setUserRating(rating);
showSuccess(`Rated ${rating} stars`);
};
const handleShare = async () => {
try {
await navigator.share({
title: script.name,
text: script.description,
url: window.location.href,
});
} catch (error) {
// Fallback to copying URL
copyToClipboard(window.location.href);
showSuccess('Link copied to clipboard!');
}
};
const handleCopyCode = async () => {
await copyToClipboard(script.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 (
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type={interactive ? 'button' : undefined}
onClick={interactive && onRatingChange ? () => onRatingChange(star) : undefined}
className={`${
interactive ? 'hover:scale-110 transition-transform' : ''
} ${
star <= rating ? 'text-yellow-400 fill-current' : 'text-gray-300'
}`}
disabled={!interactive}
>
<Star className="w-4 h-4" />
</button>
))}
</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">
<div className="max-w-4xl mx-auto space-y-8">
<div className="text-center space-y-4">
<FileCode className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Script Details</h1>
<p className="text-xl text-muted-foreground">
View and interact with bash scripts.
</p>
</div>
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto space-y-8">
{/* Breadcrumb */}
<nav className="text-sm text-muted-foreground">
<Link to="/" className="hover:text-foreground">Home</Link>
<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>
</nav>
{/* Script Header */}
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Script detail functionality coming soon! This will include script content, comments, ratings, and download options.
</p>
</CardContent>
<CardHeader className="space-y-4">
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center gap-3">
<Code2 className="h-8 w-8 text-primary" />
<div>
<CardTitle className="text-3xl">{script.name}</CardTitle>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>v{script.version}</span>
<span></span>
<span>{script.license} License</span>
<span></span>
<span>{script.size}</span>
</div>
</div>
</div>
<CardDescription className="text-lg max-w-3xl">
{script.description}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleBookmark}>
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-current' : ''}`} />
</Button>
<Button variant="outline" size="sm" onClick={handleShare}>
<Share2 className="h-4 w-4" />
</Button>
<Button onClick={handleDownload} className="gap-2">
<Download className="h-4 w-4" />
Download
</Button>
</div>
</div>
{/* Stats */}
<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>
</div>
<div className="flex items-center gap-2">
<Download className="h-4 w-4 text-muted-foreground" />
<span>{script.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>
</div>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span>Updated {formatRelativeTime(script.lastUpdated)}</span>
</div>
</div>
{/* Tags and Categories */}
<div className="flex flex-wrap gap-2">
{script.categories.map(category => (
<Badge key={category} variant="secondary">
{category}
</Badge>
))}
{script.tags.map(tag => (
<Badge key={tag} variant="outline">
<Tag className="h-3 w-3 mr-1" />
{tag}
</Badge>
))}
</div>
</CardHeader>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2 space-y-8">
{/* Description */}
<Card>
<CardHeader>
<CardTitle>Description</CardTitle>
</CardHeader>
<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) + '...'}
</p>
</div>
<Button
variant="ghost"
onClick={() => setShowFullDescription(!showFullDescription)}
>
{showFullDescription ? 'Show less' : 'Read more'}
</Button>
</CardContent>
</Card>
{/* Code Preview */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Code Preview</CardTitle>
<Button variant="outline" size="sm" onClick={handleCopyCode}>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
{copied ? 'Copied!' : 'Copy'}
</Button>
</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>
{!showFullCode && (
<Button
variant="ghost"
className="mt-2"
onClick={() => setShowFullCode(true)}
>
Show full code
</Button>
)}
</CardContent>
</Card>
{/* Installation & Usage */}
<Card>
<CardHeader>
<CardTitle>Installation & Usage</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h4 className="font-semibold mb-2">Requirements</h4>
<p className="text-sm text-muted-foreground">{script.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>
</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>
</div>
</CardContent>
</Card>
{/* Changelog */}
<Card>
<CardHeader>
<CardTitle>Changelog</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{script.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>
<span className="text-sm text-muted-foreground">
{formatDate(change.date)}
</span>
</div>
<ul className="text-sm space-y-1">
{change.changes.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-2">
<span className="text-primary mt-1"></span>
{item}
</li>
))}
</ul>
</div>
))}
</div>
</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 */}
<div className="space-y-6">
{/* Author Info */}
<Card>
<CardHeader>
<CardTitle>Author</CardTitle>
</CardHeader>
<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>
</Avatar>
<div>
<div className="font-medium">{script.author.displayName}</div>
<div className="text-sm text-muted-foreground">@{script.author.username}</div>
</div>
</div>
{script.author.isVerified && (
<Badge variant="outline" className="w-fit">
<User className="h-3 w-3 mr-1" />
Verified Author
</Badge>
)}
</CardContent>
</Card>
{/* Script Info */}
<Card>
<CardHeader>
<CardTitle>Script Info</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Version</span>
<span className="font-medium">{script.version}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">License</span>
<span className="font-medium">{script.license}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Size</span>
<span className="font-medium">{script.size}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Created</span>
<span className="font-medium">{formatDate(script.createdAt)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Updated</span>
<span className="font-medium">{formatDate(script.lastUpdated)}</span>
</div>
</CardContent>
</Card>
{/* Compatibility */}
<Card>
<CardHeader>
<CardTitle>Compatibility</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{script.compatibleOs.map(os => (
<Badge key={os} variant="outline" className="w-full justify-center">
{os}
</Badge>
))}
</div>
</CardContent>
</Card>
{/* Dependencies */}
<Card>
<CardHeader>
<CardTitle>Dependencies</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{script.dependencies.map(dep => (
<Badge key={dep} variant="secondary" className="w-full justify-center">
{dep}
</Badge>
))}
</div>
</CardContent>
</Card>
{/* Rate Script */}
{user && (
<Card>
<CardHeader>
<CardTitle>Rate This Script</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-center">
{renderStars(userRating, true, handleRating)}
</div>
<p className="text-xs text-center text-muted-foreground">
Click on the stars to rate this script
</p>
</CardContent>
</Card>
)}
</div>
</div>
</div>
</main>

View File

@ -1,30 +1,301 @@
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Search as SearchIcon } from 'lucide-react';
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';
// Mock search results - in a real app, this would come from an API
const mockSearchResults = [
{
id: '1',
name: 'Docker Setup Script',
description: 'Automated Docker environment setup for development projects',
compatible_os: ['Linux', 'macOS', 'Windows'],
categories: ['DevOps', 'Docker'],
git_repository_url: 'https://github.com/john_doe/docker-setup',
author_name: 'john_doe',
view_count: 1247,
created_at: '2023-12-01T08:00:00Z',
updated_at: '2024-01-15T10:30:00Z',
is_approved: true,
},
{
id: '2',
name: 'Backup Automation',
description: 'Automated backup script for servers and databases',
compatible_os: ['Linux', 'macOS'],
categories: ['Backup', 'Automation'],
git_repository_url: 'https://github.com/jane_smith/backup-automation',
author_name: 'jane_smith',
view_count: 892,
created_at: '2023-11-15T10:00:00Z',
updated_at: '2024-01-10T14:20:00Z',
is_approved: true,
},
{
id: '3',
name: 'Network Monitor',
description: 'Real-time network monitoring and alerting script',
compatible_os: ['Linux'],
categories: ['Monitoring', 'Network'],
git_repository_url: 'https://github.com/admin/network-monitor',
author_name: 'admin',
view_count: 567,
created_at: '2023-12-10T09:00:00Z',
updated_at: '2024-01-12T09:15:00Z',
is_approved: true,
},
];
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[],
categories: [] as string[],
dateAdded: 'all',
recentlyUpdated: 'all',
});
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()) {
setSearchParams({ q: searchQuery.trim() });
}
};
const handleFilterChange = (newFilters: typeof filters) => {
setFilters(newFilters);
if (searchQuery) {
performSearch(searchQuery);
}
};
const clearFilters = () => {
setFilters({
os: [],
categories: [],
dateAdded: 'all',
recentlyUpdated: 'all',
});
if (searchQuery) {
performSearch(searchQuery);
}
};
const hasActiveFilters = filters.os.length > 0 ||
filters.categories.length > 0 ||
filters.dateAdded !== 'all' ||
filters.recentlyUpdated !== 'all';
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">
<div className="text-center space-y-8">
<div className="space-y-4">
<SearchIcon className="h-16 w-16 text-primary mx-auto" />
<main className="flex-1 container mx-auto px-4 py-8">
<div className="max-w-7xl mx-auto space-y-6">
{/* Search Header */}
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold">Search Scripts</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Find the perfect bash script for your needs using our advanced search and filtering system.
<p className="text-xl text-muted-foreground">
Find the perfect script for your automation needs
</p>
</div>
<Card className="max-w-2xl mx-auto">
<CardContent className="p-8 text-center">
<p className="text-muted-foreground">
Search functionality coming soon! This page will include advanced search, filters, and results.
</p>
{/* Search Form */}
<Card>
<CardContent className="p-6">
<form onSubmit={handleSearch} className="flex gap-4">
<div className="relative flex-1">
<SearchIcon className="absolute left-3 top-3 h-5 w-5 text-muted-foreground" />
<Input
type="text"
placeholder="Search for scripts, tags, or descriptions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 text-lg"
/>
</div>
<Button type="submit" size="lg" disabled={isLoading}>
{isLoading ? 'Searching...' : 'Search'}
</Button>
<Button
type="button"
variant="outline"
size="lg"
onClick={() => setShowFilters(!showFilters)}
>
<Filter className="h-4 w-4 mr-2" />
Filters
</Button>
</form>
</CardContent>
</Card>
{/* Active Filters */}
{hasActiveFilters && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-muted-foreground">Active filters:</span>
{filters.os.map(os => (
<Badge key={os} variant="secondary" className="gap-1">
OS: {os}
<Button
variant="ghost"
size="sm"
className="h-auto p-0 ml-1 hover:bg-transparent"
onClick={() => handleFilterChange({
...filters,
os: filters.os.filter(o => o !== os)
})}
>
<X className="h-3 w-3" />
</Button>
</Badge>
))}
{filters.categories.map(cat => (
<Badge key={cat} variant="secondary" className="gap-1">
{cat}
<Button
variant="ghost"
size="sm"
className="h-auto p-0 ml-1 hover:bg-transparent"
onClick={() => handleFilterChange({
...filters,
categories: filters.categories.filter(c => c !== cat)
})}
>
<X className="h-3 w-3" />
</Button>
</Badge>
))}
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-foreground"
>
Clear all
</Button>
</div>
</CardContent>
</Card>
)}
{/* Filters Panel */}
{showFilters && (
<Card>
<CardContent className="p-6">
<ScriptFilters
onFiltersChange={handleFilterChange}
/>
</CardContent>
</Card>
)}
{/* Search Results */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">
{searchQuery ? `Search Results for "${searchQuery}"` : 'All Scripts'}
</h2>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{searchResults.length} result{searchResults.length !== 1 ? 's' : ''}</span>
{isLoading && <span> Searching...</span>}
</div>
</div>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map(i => (
<Card key={i} className="animate-pulse">
<CardContent className="p-6">
<div className="space-y-3">
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-3 bg-muted rounded w-full"></div>
<div className="h-3 bg-muted rounded w-2/3"></div>
<div className="flex gap-2">
<div className="h-6 bg-muted rounded w-16"></div>
<div className="h-6 bg-muted rounded w-20"></div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
) : searchResults.length > 0 ? (
<ScriptGrid scripts={searchResults} isLoading={isLoading} />
) : (
<Card>
<CardContent className="p-12 text-center">
<Code2 className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No scripts found</h3>
<p className="text-muted-foreground mb-4">
{searchQuery
? `No scripts match your search for "${searchQuery}"`
: 'No scripts available at the moment'
}
</p>
{searchQuery && (
<Button variant="outline" onClick={() => setSearchQuery('')}>
Clear search
</Button>
)}
</CardContent>
</Card>
)}
</div>
</div>
</main>

View File

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