Update README.md to provide a comprehensive overview of the ScriptShare platform, including features, tech stack, setup instructions, admin capabilities, and contribution guidelines.

This commit is contained in:
2025-08-12 22:31:26 +01:00
parent 978b9b26bc
commit aa10ea0b26
64 changed files with 12998 additions and 2 deletions

91
.gitignore vendored Normal file
View File

@ -0,0 +1,91 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Database
drizzle/
*.sqlite
*.sqlite3
# Build outputs
build/
out/
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/

128
README.md
View File

@ -1,3 +1,127 @@
# scriptshare-cursor
# ScriptShare - Bash Script Library
Cursor Scriptshare Development.
A modern, community-driven platform for discovering, sharing, and collaborating on bash scripts for system administration, automation, and development.
## Features
- 🚀 **Browse & Search**: Find scripts by category, OS compatibility, and other filters
- 📝 **Submit Scripts**: Share your scripts with the community
- 🔍 **Advanced Filtering**: Filter by OS, categories, date added, and more
- 👥 **User Profiles**: Manage your scripts and track contributions
- 🛡️ **Community Review**: All scripts are reviewed for quality and security
- 📊 **Analytics**: Track script views, downloads, and popularity
- 💬 **Comments & Ratings**: Community feedback and discussion
- 🎨 **Modern UI**: Beautiful, responsive design with dark/light themes
- 🔐 **Authentication**: Secure user accounts and profiles
## Tech Stack
- **Frontend**: React 18 + TypeScript + Vite
- **Styling**: Tailwind CSS + shadcn/ui components
- **Database**: MySQL (Digital Ocean Managed Database)
- **ORM**: Drizzle ORM
- **State Management**: React Query + React Context
- **Routing**: React Router DOM
- **Forms**: React Hook Form + Zod validation
## Getting Started
1. **Install dependencies**:
```bash
npm install
```
2. **Set up environment variables**:
```bash
cp .env.example .env.local
# Edit .env.local with your database credentials
```
3. **Set up the database**:
```bash
npm run db:generate
npm run db:migrate
```
4. **Start development server**:
```bash
npm run dev
```
5. **Open your browser**:
Navigate to `http://localhost:5173`
## Database Setup
This app uses a MySQL database hosted on Digital Ocean. You'll need to:
1. Create a managed MySQL database on Digital Ocean
2. Get your connection credentials
3. Update the `.env.local` file with your database details
## Superuser Admin Setup
ScriptShare includes a comprehensive admin system with superuser capabilities:
### Creating Your First Superuser
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.
3. **Both scripts will output** your superuser credentials and save them to JSON files
### 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
### 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
### Accessing the Admin Panel
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.
## Script Submission Process
1. Users submit scripts through the `/submit` page
2. Scripts are reviewed by community moderators
3. Approved scripts appear in the public library
4. Users can rate, comment, and download approved scripts
## Contributing
We welcome contributions! Please see our contributing guidelines for more details.
## License
This project is licensed under the MIT License.

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -0,0 +1,28 @@
{
"id": "92acee3490facf4eef9d2a5695397ca6",
"email": "admin@scriptshare.com",
"username": "admin",
"displayName": "System Administrator",
"avatarUrl": "",
"bio": "Default system administrator account",
"isAdmin": true,
"isModerator": true,
"isSuperUser": true,
"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"
],
"createdAt": "2025-08-12T21:28:52.080Z",
"updatedAt": "2025-08-12T21:28:52.080Z",
"passwordHash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9"
}

12
drizzle.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/lib/db/schema.ts',
out: './drizzle',
dialect: 'mysql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
})

12
env.example Normal file
View File

@ -0,0 +1,12 @@
# Database Configuration (Digital Ocean Managed MySQL)
DATABASE_URL="mysql://username:password@host:port/database_name"
# JWT Secret for authentication
JWT_SECRET="your-super-secret-jwt-key-here"
# App Configuration
VITE_APP_NAME="ScriptShare"
VITE_APP_URL="http://localhost:5173"
# Optional: Analytics
VITE_ANALYTICS_ENABLED="false"

35
eslint.config.js Normal file
View File

@ -0,0 +1,35 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
js.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
...globals.es2020,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

30
index.html Normal file
View File

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ScriptShare - Bash Script Library</title>
<meta name="description" content="A community-driven platform for discovering, sharing, and collaborating on powerful bash scripts for system administration, automation, and development." />
<meta name="keywords" content="bash scripts, shell scripts, automation, system administration, devops, linux, unix" />
<meta name="author" content="ScriptShare Community" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://scriptshare.com/" />
<meta property="og:title" content="ScriptShare - Bash Script Library" />
<meta property="og:description" content="A community-driven platform for discovering, sharing, and collaborating on powerful bash scripts." />
<meta property="og:image" content="https://scriptshare.com/og-image.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://scriptshare.com/" />
<meta property="twitter:title" content="ScriptShare - Bash Script Library" />
<meta property="twitter:description" content="A community-driven platform for discovering, sharing, and collaborating on powerful bash scripts." />
<meta property="twitter:image" content="https://scriptshare.com/og-image.jpg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

7918
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

92
package.json Normal file
View File

@ -0,0 +1,92 @@
{
"name": "scriptshare",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:dev": "tsc && vite build --mode development",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"db:generate": "drizzle-kit generate",
"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"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@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-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.56.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.37.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"mysql2": "^3.12.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2",
"react-syntax-highlighter": "^15.6.1",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react-swc": "^3.9.0",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.25.0",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^6.3.4"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,125 @@
#!/usr/bin/env node
/**
* Script to create a default superuser account
* This script creates a superuser without requiring interactive input
*
* Usage: node scripts/create-default-superuser.js
*/
import crypto from 'crypto';
import fs from 'fs';
function generateId() {
return crypto.randomBytes(16).toString('hex');
}
function createSuperUser(userData) {
const now = new Date().toISOString();
return {
id: generateId(),
email: userData.email,
username: userData.username,
displayName: userData.displayName,
avatarUrl: userData.avatarUrl,
bio: userData.bio,
isAdmin: true,
isModerator: true,
isSuperUser: true,
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'
],
createdAt: now,
updatedAt: now,
// In a real app, this would be hashed
passwordHash: crypto.createHash('sha256').update(userData.password).digest('hex')
};
}
function main() {
console.log('🚀 ScriptShare Default Super User Creation Tool');
console.log('==============================================\n');
try {
// Default superuser data
const defaultSuperUser = createSuperUser({
email: 'admin@scriptshare.com',
username: 'admin',
displayName: 'System Administrator',
password: 'admin123',
bio: 'Default system administrator account',
avatarUrl: ''
});
console.log('✅ Default Super User Created Successfully!');
console.log('===========================================');
console.log(`ID: ${defaultSuperUser.id}`);
console.log(`Email: ${defaultSuperUser.email}`);
console.log(`Username: @${defaultSuperUser.username}`);
console.log(`Display Name: ${defaultSuperUser.displayName}`);
console.log(`Password: admin123 (CHANGE THIS IN PRODUCTION!)`);
console.log(`Super User: ${defaultSuperUser.isSuperUser ? 'Yes' : 'No'}`);
console.log(`Admin: ${defaultSuperUser.isAdmin ? 'Yes' : 'No'}`);
console.log(`Moderator: ${defaultSuperUser.isModerator ? 'Yes' : 'No'}`);
console.log(`Permissions: ${defaultSuperUser.permissions.length} total`);
console.log(`Created: ${defaultSuperUser.createdAt}`);
console.log('\n📋 Next Steps:');
console.log('1. Save this user data to your database');
console.log('2. Use these credentials to log into the admin panel:');
console.log(' - Email: admin@scriptshare.com');
console.log(' - Password: admin123');
console.log('3. CHANGE THE PASSWORD IMMEDIATELY after first login');
console.log('4. Create additional admin users through the web interface');
// Save to a JSON file for reference
const outputPath = './default-superuser-account.json';
fs.writeFileSync(outputPath, JSON.stringify(defaultSuperUser, null, 2));
console.log(`\n💾 User data saved to: ${outputPath}`);
// Also save to localStorage format for the web app
const webAppData = {
id: defaultSuperUser.id,
email: defaultSuperUser.email,
username: defaultSuperUser.username,
displayName: defaultSuperUser.displayName,
avatarUrl: defaultSuperUser.avatarUrl,
bio: defaultSuperUser.bio,
isAdmin: defaultSuperUser.isAdmin,
isModerator: defaultSuperUser.isModerator,
isSuperUser: defaultSuperUser.isSuperUser,
permissions: defaultSuperUser.permissions,
createdAt: defaultSuperUser.createdAt,
updatedAt: defaultSuperUser.updatedAt
};
const webAppPath = './webapp-superuser.json';
fs.writeFileSync(webAppPath, JSON.stringify(webAppData, null, 2));
console.log(`💾 Web app user data saved to: ${webAppPath}`);
console.log('\n⚠ IMPORTANT SECURITY NOTES:');
console.log('- This is a default account for development only');
console.log('- Change the password immediately after first login');
console.log('- Delete this script in production environments');
console.log('- Use strong, unique passwords in production');
} catch (error) {
console.error('❌ Error creating super user:', error.message);
process.exit(1);
}
}
// Run the main function
main();

126
scripts/create-superuser.js Normal file
View File

@ -0,0 +1,126 @@
#!/usr/bin/env node
/**
* Script to create the first superuser account
* Run this script to set up the initial admin user for ScriptShare
*
* Usage: node scripts/create-superuser.js
*/
import readline from 'readline';
import crypto from 'crypto';
import fs from 'fs';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
}
function generateId() {
return crypto.randomBytes(16).toString('hex');
}
function createSuperUser(userData) {
const now = new Date().toISOString();
return {
id: generateId(),
email: userData.email,
username: userData.username,
displayName: userData.displayName,
avatarUrl: userData.avatarUrl,
bio: userData.bio,
isAdmin: true,
isModerator: true,
isSuperUser: true,
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'
],
createdAt: now,
updatedAt: now,
// In a real app, this would be hashed
passwordHash: crypto.createHash('sha256').update(userData.password).digest('hex')
};
}
async function main() {
console.log('🚀 ScriptShare Super User Creation Tool');
console.log('=====================================\n');
try {
const email = await question('Email address: ');
const username = await question('Username: ');
const displayName = await question('Display Name: ');
const password = await question('Password: ');
const bio = await question('Bio (optional): ') || '';
const avatarUrl = await question('Avatar URL (optional): ') || '';
if (!email || !username || !displayName || !password) {
console.error('❌ All required fields must be provided');
process.exit(1);
}
const superUser = createSuperUser({
email,
username,
displayName,
password,
bio,
avatarUrl
});
console.log('\n✅ Super User Created Successfully!');
console.log('=====================================');
console.log(`ID: ${superUser.id}`);
console.log(`Email: ${superUser.email}`);
console.log(`Username: @${superUser.username}`);
console.log(`Display Name: ${superUser.displayName}`);
console.log(`Super User: ${superUser.isSuperUser ? 'Yes' : 'No'}`);
console.log(`Admin: ${superUser.isAdmin ? 'Yes' : 'No'}`);
console.log(`Moderator: ${superUser.isModerator ? 'Yes' : 'No'}`);
console.log(`Permissions: ${superUser.permissions.length} total`);
console.log(`Created: ${superUser.createdAt}`);
console.log('\n📋 Next Steps:');
console.log('1. Save this user data to your database');
console.log('2. Use these credentials to log into the admin panel');
console.log('3. Create additional admin users through the web interface');
// Save to a JSON file for reference
const outputPath = './superuser-account.json';
fs.writeFileSync(outputPath, JSON.stringify(superUser, null, 2));
console.log(`\n💾 User data saved to: ${outputPath}`);
} catch (error) {
console.error('❌ Error creating super user:', error.message);
process.exit(1);
} finally {
rl.close();
}
}
// Handle Ctrl+C gracefully
process.on('SIGINT', () => {
console.log('\n\n❌ Operation cancelled by user');
process.exit(0);
});
// Run the main function
main();

66
src/App.tsx Normal file
View File

@ -0,0 +1,66 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { AuthProvider } from "@/contexts/AuthContext";
import Index from "@/pages/Index";
import ScriptDetail from "@/pages/ScriptDetail";
import Search from "@/pages/Search";
import Dashboard from "@/pages/Dashboard";
import About from "@/pages/About";
import Privacy from "@/pages/Privacy";
import Terms from "@/pages/Terms";
import Login from "@/pages/Login";
import SubmitScript from "@/pages/SubmitScript";
import MyScripts from "@/pages/MyScripts";
import AdminPanel from "@/pages/AdminPanel";
import AdminScriptReview from "@/pages/AdminScriptReview";
import Profile from "@/pages/Profile";
import EditScript from "@/pages/EditScript";
import NotFound from "@/pages/NotFound";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
});
const App = () => (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" storageKey="scriptshare-theme">
<AuthProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/search" element={<Search />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/script/:id" element={<ScriptDetail />} />
<Route path="/login" element={<Login />} />
<Route path="/submit" element={<SubmitScript />} />
<Route path="/my-scripts" element={<MyScripts />} />
<Route path="/profile" element={<Profile />} />
<Route path="/edit-script/:id" element={<EditScript />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/admin/script/:id" element={<AdminScriptReview />} />
<Route path="/about" element={<About />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/terms" element={<Terms />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
);
export default App;

162
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,162 @@
import { Link } from 'react-router-dom';
import { Code2, Github, Twitter, Mail, Heart } from 'lucide-react';
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand Section */}
<div className="space-y-4">
<Link to="/" className="flex items-center space-x-2">
<Code2 className="h-8 w-8 text-primary" />
<span className="text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
ScriptShare
</span>
</Link>
<p className="text-sm text-muted-foreground max-w-xs">
A community-driven platform for discovering, sharing, and collaborating on powerful bash scripts.
</p>
<div className="flex space-x-4">
<a
href="https://github.com"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary transition-colors"
>
<Github className="h-5 w-5" />
</a>
<a
href="https://twitter.com"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary transition-colors"
>
<Twitter className="h-5 w-5" />
</a>
<a
href="mailto:contact@scriptshare.com"
className="text-muted-foreground hover:text-primary transition-colors"
>
<Mail className="h-5 w-5" />
</a>
</div>
</div>
{/* Platform Links */}
<div className="space-y-4">
<h3 className="font-semibold">Platform</h3>
<ul className="space-y-2 text-sm">
<li>
<Link to="/search" className="text-muted-foreground hover:text-foreground transition-colors">
Browse Scripts
</Link>
</li>
<li>
<Link to="/submit" className="text-muted-foreground hover:text-foreground transition-colors">
Submit Script
</Link>
</li>
<li>
<Link to="/dashboard" className="text-muted-foreground hover:text-foreground transition-colors">
Dashboard
</Link>
</li>
<li>
<Link to="/about" className="text-muted-foreground hover:text-foreground transition-colors">
About
</Link>
</li>
</ul>
</div>
{/* Community Links */}
<div className="space-y-4">
<h3 className="font-semibold">Community</h3>
<ul className="space-y-2 text-sm">
<li>
<Link to="/privacy" className="text-muted-foreground hover:text-foreground transition-colors">
Privacy Policy
</Link>
</li>
<li>
<Link to="/terms" className="text-muted-foreground hover:text-foreground transition-colors">
Terms of Service
</Link>
</li>
<li>
<a
href="https://github.com/scriptshare/contributing"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Contributing
</a>
</li>
<li>
<a
href="https://github.com/scriptshare/issues"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Report Issues
</a>
</li>
</ul>
</div>
{/* Support Links */}
<div className="space-y-4">
<h3 className="font-semibold">Support</h3>
<ul className="space-y-2 text-sm">
<li>
<a
href="https://docs.scriptshare.com"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Documentation
</a>
</li>
<li>
<a
href="https://discord.gg/scriptshare"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Discord Community
</a>
</li>
<li>
<a
href="mailto:support@scriptshare.com"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Contact Support
</a>
</li>
</ul>
</div>
</div>
{/* Bottom Section */}
<div className="border-t mt-8 pt-8 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<div className="text-sm text-muted-foreground">
© {currentYear} ScriptShare. All rights reserved.
</div>
<div className="text-sm text-muted-foreground flex items-center space-x-1">
<span>Made with</span>
<Heart className="h-4 w-4 text-red-500 fill-current" />
<span>by the community</span>
</div>
</div>
</div>
</footer>
);
}

211
src/components/Header.tsx Normal file
View File

@ -0,0 +1,211 @@
import { useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { UserMenu } from '@/components/UserMenu';
import { ThemeToggle } from '@/components/ThemeToggle';
import { useAuth } from '@/contexts/AuthContext';
import { Search, Menu, X, Code2 } from 'lucide-react';
interface HeaderProps {
onSearch: (query: string) => void;
}
export function Header({ onSearch }: HeaderProps) {
const { user } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
onSearch(searchQuery.trim());
navigate('/search');
}
};
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
const isActive = (path: string) => {
return location.pathname === path;
};
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4">
<div className="flex h-16 items-center justify-between">
{/* Logo */}
<Link to="/" className="flex items-center space-x-2">
<Code2 className="h-8 w-8 text-primary" />
<span className="text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
ScriptShare
</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6">
<Link
to="/"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/') ? 'text-primary' : 'text-muted-foreground'
}`}
>
Home
</Link>
<Link
to="/search"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/search') ? 'text-primary' : 'text-muted-foreground'
}`}
>
Browse
</Link>
<Link
to="/submit"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/submit') ? 'text-primary' : 'text-muted-foreground'
}`}
>
Submit
</Link>
<Link
to="/about"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/about') ? 'text-primary' : 'text-muted-foreground'
}`}
>
About
</Link>
</nav>
{/* Search Bar */}
<div className="hidden md:flex flex-1 max-w-md mx-8">
<form onSubmit={handleSearch} className="relative w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search scripts..."
value={searchQuery}
onChange={handleSearchInput}
className="pl-10 pr-4 w-full"
/>
</form>
</div>
{/* Right Side Actions */}
<div className="flex items-center space-x-4">
<ThemeToggle />
{user ? (
<UserMenu />
) : (
<div className="hidden md:flex items-center space-x-2">
<Button variant="ghost" onClick={() => navigate('/login')}>
Sign In
</Button>
<Button onClick={() => navigate('/login')}>
Get Started
</Button>
</div>
)}
{/* Mobile Menu Button */}
<Button
variant="ghost"
size="sm"
className="md:hidden"
onClick={toggleMobileMenu}
>
{isMobileMenuOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</Button>
</div>
</div>
{/* Mobile Search Bar */}
<div className="md:hidden pb-4">
<form onSubmit={handleSearch} className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search scripts..."
value={searchQuery}
onChange={handleSearchInput}
className="pl-10 pr-4 w-full"
/>
</form>
</div>
{/* Mobile Navigation Menu */}
{isMobileMenuOpen && (
<div className="md:hidden border-t py-4">
<nav className="flex flex-col space-y-4">
<Link
to="/"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/') ? 'text-primary' : 'text-muted-foreground'
}`}
onClick={closeMobileMenu}
>
Home
</Link>
<Link
to="/search"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/search') ? 'text-primary' : 'text-muted-foreground'
}`}
onClick={closeMobileMenu}
>
Browse
</Link>
<Link
to="/submit"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/submit') ? 'text-primary' : 'text-muted-foreground'
}`}
onClick={closeMobileMenu}
>
Submit
</Link>
<Link
to="/about"
className={`text-sm font-medium transition-colors hover:text-primary ${
isActive('/about') ? 'text-primary' : 'text-muted-foreground'
}`}
onClick={closeMobileMenu}
>
About
</Link>
{!user && (
<div className="pt-4 border-t">
<Button variant="ghost" className="w-full justify-start" onClick={() => { navigate('/login'); closeMobileMenu(); }}>
Sign In
</Button>
<Button className="w-full justify-start mt-2" onClick={() => { navigate('/login'); closeMobileMenu(); }}>
Get Started
</Button>
</div>
)}
</nav>
</div>
)}
</div>
</header>
);
}

View File

@ -0,0 +1,148 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Eye, Calendar, User, ExternalLink, Star, TrendingUp } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { formatDate, formatRelativeTime } from '@/lib/utils';
export interface Script {
id: string;
name: string;
description: string;
compatible_os: string[];
categories: string[];
git_repository_url?: string;
author_name: string;
view_count: number;
created_at: string;
updated_at: string;
is_approved: boolean;
}
interface ScriptCardProps {
script: Script;
}
export function ScriptCard({ script }: ScriptCardProps) {
const navigate = useNavigate();
const isPopular = script.view_count > 500;
const isTrending = script.view_count > 200;
return (
<Card className="group h-full hover:shadow-2xl hover:shadow-primary/20 transition-all duration-500 cursor-pointer border-muted/50 hover:border-primary/30 bg-gradient-to-br from-card/80 to-card/40 backdrop-blur-sm hover:scale-[1.02] relative overflow-hidden">
{/* Animated background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{/* Popular/Trending indicators */}
{isPopular && (
<div className="absolute top-3 right-3 z-10">
<Badge variant="secondary" className="bg-gradient-to-r from-yellow-500/20 to-orange-500/20 text-yellow-600 border-yellow-500/30 animate-pulse">
<Star className="h-3 w-3 mr-1 fill-current" />
Popular
</Badge>
</div>
)}
{isTrending && !isPopular && (
<div className="absolute top-3 right-3 z-10">
<Badge variant="secondary" className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 text-green-600 border-green-500/30">
<TrendingUp className="h-3 w-3 mr-1" />
Trending
</Badge>
</div>
)}
<CardHeader onClick={() => navigate(`/script/${script.id}`)} className="pb-3 relative z-10">
<CardTitle className="line-clamp-2 group-hover:text-transparent group-hover:bg-gradient-to-r group-hover:from-primary group-hover:to-accent group-hover:bg-clip-text transition-all duration-300 text-lg font-bold">
{script.name}
</CardTitle>
<CardDescription className="line-clamp-3 text-muted-foreground group-hover:text-foreground/80 transition-colors duration-300">
{script.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 relative z-10">
{/* OS Badges with enhanced styling */}
<div className="flex flex-wrap gap-1.5">
{script.compatible_os.map(os => (
<Badge
key={os}
variant="outline"
className="text-xs border-primary/40 text-primary/90 hover:bg-primary/15 transition-all duration-200 rounded-full px-2 py-1 font-medium shadow-sm"
>
{os}
</Badge>
))}
</div>
{/* Categories with gradient backgrounds */}
<div className="flex flex-wrap gap-1.5">
{script.categories.slice(0, 2).map((category, index) => (
<Badge
key={category}
variant="secondary"
className={`text-xs rounded-full px-3 py-1 font-medium shadow-sm transition-all duration-200 hover:scale-105 ${
index === 0
? 'bg-gradient-to-r from-accent/20 to-accent/30 text-accent border-accent/30'
: 'bg-gradient-to-r from-primary/20 to-primary/30 text-primary border-primary/30'
}`}
>
{category}
</Badge>
))}
{script.categories.length > 2 && (
<Badge
variant="secondary"
className="text-xs bg-gradient-to-r from-muted to-muted/80 text-muted-foreground border-muted rounded-full px-2 py-1"
>
+{script.categories.length - 2}
</Badge>
)}
</div>
{/* Script metadata */}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-1">
<User className="h-3 w-3" />
<span>{script.author_name}</span>
</div>
<div className="flex items-center space-x-1">
<Eye className="h-3 w-3" />
<span>{script.view_count.toLocaleString()}</span>
</div>
</div>
<div className="flex items-center space-x-1">
<Calendar className="h-3 w-3" />
<span title={formatDate(script.updated_at)}>
{formatRelativeTime(script.updated_at)}
</span>
</div>
</div>
{/* Action buttons */}
<div className="flex space-x-2 pt-2">
<Button
size="sm"
className="flex-1 bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
onClick={() => navigate(`/script/${script.id}`)}
>
View Script
</Button>
{script.git_repository_url && (
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
window.open(script.git_repository_url, '_blank');
}}
>
<ExternalLink className="h-3 w-3" />
</Button>
)}
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,242 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { X, Filter, ChevronDown } from 'lucide-react';
export interface FilterOptions {
os: string[];
categories: string[];
dateAdded: string;
recentlyUpdated: string;
}
interface ScriptFiltersProps {
onFiltersChange: (filters: FilterOptions) => void;
}
const availableOS = ['Linux', 'Ubuntu', 'CentOS', 'Debian', 'macOS', 'Windows', 'FreeBSD'];
const availableCategories = [
'System Administration',
'Backup',
'DevOps',
'Docker',
'Development',
'Git',
'Networking',
'Monitoring',
'Security',
'Automation',
'Database',
'Web Development'
];
export function ScriptFilters({ onFiltersChange }: ScriptFiltersProps) {
const [filters, setFilters] = useState<FilterOptions>({
os: [],
categories: [],
dateAdded: 'all',
recentlyUpdated: 'all',
});
const [isExpanded, setIsExpanded] = useState(false);
const handleFilterChange = (newFilters: Partial<FilterOptions>) => {
const updatedFilters = { ...filters, ...newFilters };
setFilters(updatedFilters);
onFiltersChange(updatedFilters);
};
const toggleOS = (os: string) => {
const newOS = filters.os.includes(os)
? filters.os.filter(o => o !== os)
: [...filters.os, os];
handleFilterChange({ os: newOS });
};
const toggleCategory = (category: string) => {
const newCategories = filters.categories.includes(category)
? filters.categories.filter(c => c !== category)
: [...filters.categories, category];
handleFilterChange({ categories: newCategories });
};
const clearFilters = () => {
const clearedFilters: FilterOptions = {
os: [],
categories: [],
dateAdded: 'all',
recentlyUpdated: 'all',
};
setFilters(clearedFilters);
onFiltersChange(clearedFilters);
};
const hasActiveFilters = filters.os.length > 0 ||
filters.categories.length > 0 ||
filters.dateAdded !== 'all' ||
filters.recentlyUpdated !== 'all';
return (
<div className="space-y-4">
{/* Filter Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Filter className="h-5 w-5 text-muted-foreground" />
<h3 className="text-lg font-semibold">Filters</h3>
{hasActiveFilters && (
<Badge variant="secondary" className="ml-2">
{filters.os.length + filters.categories.length +
(filters.dateAdded !== 'all' ? 1 : 0) +
(filters.recentlyUpdated !== 'all' ? 1 : 0)} active
</Badge>
)}
</div>
<div className="flex items-center space-x-2">
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4 mr-1" />
Clear All
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
className="text-muted-foreground hover:text-foreground"
>
<ChevronDown className={`h-4 w-4 mr-1 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
{isExpanded ? 'Hide' : 'Show'} Filters
</Button>
</div>
</div>
{/* Active Filters Display */}
{hasActiveFilters && (
<div className="flex flex-wrap gap-2">
{filters.os.map(os => (
<Badge
key={os}
variant="secondary"
className="cursor-pointer hover:bg-destructive/20 hover:text-destructive"
onClick={() => toggleOS(os)}
>
OS: {os}
<X className="h-3 w-3 ml-1" />
</Badge>
))}
{filters.categories.map(category => (
<Badge
key={category}
variant="secondary"
className="cursor-pointer hover:bg-destructive/20 hover:text-destructive"
onClick={() => toggleCategory(category)}
>
{category}
<X className="h-3 w-3 ml-1" />
</Badge>
))}
{filters.dateAdded !== 'all' && (
<Badge
variant="secondary"
className="cursor-pointer hover:bg-destructive/20 hover:text-destructive"
onClick={() => handleFilterChange({ dateAdded: 'all' })}
>
Added: {filters.dateAdded}
<X className="h-3 w-3 ml-1" />
</Badge>
)}
{filters.recentlyUpdated !== 'all' && (
<Badge
variant="secondary"
className="cursor-pointer hover:bg-destructive/20 hover:text-destructive"
onClick={() => handleFilterChange({ recentlyUpdated: 'all' })}
>
Updated: {filters.recentlyUpdated}
<X className="h-3 w-3 ml-1" />
</Badge>
)}
</div>
)}
{/* Expandable Filter Options */}
{isExpanded && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 p-4 border rounded-lg bg-muted/30">
{/* Operating Systems */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Operating Systems</h4>
<div className="space-y-2">
{availableOS.map(os => (
<label key={os} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={filters.os.includes(os)}
onChange={() => toggleOS(os)}
className="rounded border-gray-300 text-primary focus:ring-primary"
/>
<span className="text-sm">{os}</span>
</label>
))}
</div>
</div>
{/* Categories */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Categories</h4>
<div className="space-y-2">
{availableCategories.map(category => (
<label key={category} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={filters.categories.includes(category)}
onChange={() => toggleCategory(category)}
className="rounded border-gray-300 text-primary focus:ring-primary"
/>
<span className="text-sm">{category}</span>
</label>
))}
</div>
</div>
{/* Date Added */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Date Added</h4>
<select
value={filters.dateAdded}
onChange={(e) => handleFilterChange({ dateAdded: e.target.value })}
className="w-full p-2 text-sm border rounded-md bg-background"
>
<option value="all">All Time</option>
<option value="1d">Last 24 hours</option>
<option value="1w">Last week</option>
<option value="1m">Last month</option>
<option value="1y">Last year</option>
</select>
</div>
{/* Recently Updated */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Recently Updated</h4>
<select
value={filters.recentlyUpdated}
onChange={(e) => handleFilterChange({ recentlyUpdated: e.target.value })}
className="w-full p-2 text-sm border rounded-md bg-background"
>
<option value="all">All Time</option>
<option value="1d">Last 24 hours</option>
<option value="1w">Last week</option>
<option value="1m">Last month</option>
<option value="1y">Last year</option>
</select>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,72 @@
import { ScriptCard, Script } from './ScriptCard';
import { Skeleton } from '@/components/ui/skeleton';
interface ScriptGridProps {
scripts: Script[];
isLoading: boolean;
}
export function ScriptGrid({ scripts, isLoading }: ScriptGridProps) {
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, index) => (
<ScriptCardSkeleton key={index} />
))}
</div>
);
}
if (scripts.length === 0) {
return (
<div className="text-center py-12">
<div className="text-muted-foreground text-lg">
No scripts found matching your criteria.
</div>
<div className="text-sm text-muted-foreground mt-2">
Try adjusting your filters or search terms.
</div>
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{scripts.map((script) => (
<ScriptCard key={script.id} script={script} />
))}
</div>
);
}
function ScriptCardSkeleton() {
return (
<div className="border rounded-lg p-6 space-y-4">
<div className="space-y-2">
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>
<div className="flex flex-wrap gap-2">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-20 rounded-full" />
</div>
<div className="flex flex-wrap gap-2">
<Skeleton className="h-6 w-24 rounded-full" />
<Skeleton className="h-6 w-20 rounded-full" />
</div>
<div className="flex justify-between text-sm">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-16" />
</div>
<div className="flex space-x-2 pt-2">
<Skeleton className="h-9 flex-1" />
<Skeleton className="h-9 w-9" />
</div>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTheme } from '@/contexts/ThemeContext';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="h-9 w-9"
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}

132
src/components/UserMenu.tsx Normal file
View File

@ -0,0 +1,132 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useAuth } from '@/contexts/AuthContext';
import { User, Settings, LogOut, Code2, Shield, Crown } from 'lucide-react';
export function UserMenu() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const handleLogout = () => {
logout();
navigate('/');
setIsOpen(false);
};
const toggleMenu = () => {
setIsOpen(!isOpen);
};
const closeMenu = () => {
setIsOpen(false);
};
if (!user) return null;
return (
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={toggleMenu}
className="flex items-center space-x-2"
>
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatarUrl} alt={user.displayName} />
<AvatarFallback>
{user.displayName.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="hidden md:block">{user.displayName}</span>
</Button>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={closeMenu}
/>
{/* Menu */}
<div className="absolute right-0 top-full mt-2 w-56 rounded-md border bg-background shadow-lg z-50">
<div className="p-2">
{/* User Info */}
<div className="px-3 py-2 border-b">
<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" />
)}
{user.isAdmin && !user.isSuperUser && (
<Shield className="h-3 w-3 text-blue-600" title="Admin" />
)}
</div>
<div className="text-xs text-muted-foreground">{user.email}</div>
</div>
{/* Menu Items */}
<div className="py-1">
<Link
to="/dashboard"
className="flex items-center px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-sm"
onClick={closeMenu}
>
<User className="mr-2 h-4 w-4" />
Dashboard
</Link>
<Link
to="/my-scripts"
className="flex items-center px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-sm"
onClick={closeMenu}
>
<Code2 className="mr-2 h-4 w-4" />
My Scripts
</Link>
<Link
to="/profile"
className="flex items-center px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-sm"
onClick={closeMenu}
>
<Settings className="mr-2 h-4 w-4" />
Profile
</Link>
{user.isAdmin && (
<Link
to="/admin"
className="flex items-center px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-sm"
onClick={closeMenu}
>
{user.isSuperUser ? (
<Crown className="mr-2 h-4 w-4 text-amber-600" />
) : (
<Shield className="mr-2 h-4 w-4" />
)}
{user.isSuperUser ? 'Super Admin' : 'Admin Panel'}
</Link>
)}
</div>
{/* Logout */}
<div className="border-t pt-1">
<button
onClick={handleLogout}
className="flex w-full items-center px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-sm"
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</button>
</div>
</div>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,238 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Users,
FileText,
MessageSquare,
TrendingUp,
AlertTriangle,
CheckCircle,
Clock,
BarChart3
} from 'lucide-react';
interface AdminDashboardProps {
onCreateUser: () => void;
onViewScripts: () => void;
onViewAnalytics: () => void;
}
export function AdminDashboard({ onCreateUser, onViewScripts, onViewAnalytics }: AdminDashboardProps) {
// Mock data - in a real app, this would come from API calls
const stats = {
totalUsers: 1247,
totalScripts: 89,
pendingApprovals: 12,
totalComments: 456,
activeUsers: 234,
scriptsThisWeek: 8,
commentsThisWeek: 23,
systemHealth: 'healthy'
};
const recentActivity = [
{ id: 1, type: 'script', action: 'submitted', title: 'Docker Setup Script', user: 'john_doe', time: '2 hours ago' },
{ id: 2, type: 'comment', action: 'reported', title: 'Comment on Backup Script', user: 'jane_smith', time: '4 hours ago' },
{ id: 3, type: 'user', action: 'registered', title: 'New user account', user: 'alex_wilson', time: '6 hours ago' },
{ id: 4, type: 'script', action: 'approved', title: 'Network Monitor', user: 'admin', time: '1 day ago' },
];
const getActivityIcon = (type: string) => {
switch (type) {
case 'script': return <FileText className="h-4 w-4" />;
case 'comment': return <MessageSquare className="h-4 w-4" />;
case 'user': return <Users className="h-4 w-4" />;
default: return <BarChart3 className="h-4 w-4" />;
}
};
const getActivityColor = (type: string) => {
switch (type) {
case 'script': return 'text-blue-600';
case 'comment': return 'text-green-600';
case 'user': return 'text-purple-600';
default: return 'text-gray-600';
}
};
return (
<div className="space-y-6">
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalUsers.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
+{stats.activeUsers} active this 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 Scripts</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalScripts}</div>
<p className="text-xs text-muted-foreground">
+{stats.scriptsThisWeek} this 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">Pending Approvals</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-amber-600">{stats.pendingApprovals}</div>
<p className="text-xs text-muted-foreground">
Require attention
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">System Health</CardTitle>
<CheckCircle className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600 capitalize">{stats.systemHealth}</div>
<p className="text-xs text-muted-foreground">
All systems operational
</p>
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Common administrative tasks and shortcuts.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button
onClick={onCreateUser}
className="h-20 flex flex-col items-center justify-center gap-2"
>
<Users className="h-6 w-6" />
<span>Create Admin User</span>
</Button>
<Button
onClick={onViewScripts}
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2"
>
<FileText className="h-6 w-6" />
<span>Review Scripts</span>
</Button>
<Button
onClick={onViewAnalytics}
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2"
>
<BarChart3 className="h-6 w-6" />
<span>View Analytics</span>
</Button>
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Latest platform activities requiring attention.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{recentActivity.map((activity) => (
<div key={activity.id} className="flex items-center gap-3 p-3 rounded-lg hover:bg-muted/50 transition-colors">
<div className={`p-2 rounded-full bg-muted ${getActivityColor(activity.type)}`}>
{getActivityIcon(activity.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">
{activity.title}
</span>
<Badge variant="outline" className="text-xs">
{activity.action}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
by @{activity.user} {activity.time}
</div>
</div>
<Button variant="ghost" size="sm">
View
</Button>
</div>
))}
</div>
</CardContent>
</Card>
{/* System Status */}
<Card>
<CardHeader>
<CardTitle>System Status</CardTitle>
<CardDescription>
Current platform status and alerts.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span className="font-medium text-green-800 dark:text-green-200">Database</span>
</div>
<Badge variant="secondary" className="bg-green-100 text-green-800">
Operational
</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span className="font-medium text-green-800 dark:text-green-200">File Storage</span>
</div>
<Badge variant="secondary" className="bg-green-100 text-green-800">
Operational
</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span className="font-medium text-green-800 dark:text-green-200">Email Service</span>
</div>
<Badge variant="secondary" className="bg-green-100 text-green-800">
Operational
</Badge>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,171 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Crown, Shield, User, Trash2, Edit, MoreHorizontal } from 'lucide-react';
import { AdminUser } from '@/lib/admin';
import { formatDate } from '@/lib/utils';
export function AdminUsersList() {
const [adminUsers, setAdminUsers] = useState<AdminUser[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Load admin users from localStorage (in a real app, this would be an API call)
const loadAdminUsers = () => {
try {
const users = JSON.parse(localStorage.getItem('scriptshare-admin-users') || '[]');
setAdminUsers(users);
} catch (error) {
console.error('Failed to load admin users:', error);
} finally {
setIsLoading(false);
}
};
loadAdminUsers();
}, []);
const handleDeleteUser = (userId: string) => {
if (window.confirm('Are you sure you want to delete this admin user? This action cannot be undone.')) {
const updatedUsers = adminUsers.filter(user => user.id !== userId);
setAdminUsers(updatedUsers);
localStorage.setItem('scriptshare-admin-users', JSON.stringify(updatedUsers));
}
};
const getPermissionBadges = (user: AdminUser) => {
const badges = [];
if (user.isSuperUser) {
badges.push(
<Badge key="super" variant="default" className="bg-amber-600 hover:bg-amber-700">
<Crown className="h-3 w-3 mr-1" />
Super User
</Badge>
);
} else if (user.isAdmin) {
badges.push(
<Badge key="admin" variant="secondary">
<Shield className="h-3 w-3 mr-1" />
Admin
</Badge>
);
}
if (user.isModerator) {
badges.push(
<Badge key="mod" variant="outline">
<User className="h-3 w-3 mr-1" />
Moderator
</Badge>
);
}
return badges;
};
const getInitials = (displayName: string) => {
return displayName
.split(' ')
.map(name => name[0])
.join('')
.toUpperCase()
.slice(0, 2);
};
if (isLoading) {
return (
<Card>
<CardContent className="p-8">
<div className="text-center text-muted-foreground">
Loading admin users...
</div>
</CardContent>
</Card>
);
}
if (adminUsers.length === 0) {
return (
<Card>
<CardContent className="p-8">
<div className="text-center text-muted-foreground">
<Shield className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-medium mb-2">No Admin Users Found</h3>
<p className="text-sm">
Create your first admin user to get started with platform management.
</p>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Admin Users ({adminUsers.length})
</CardTitle>
<CardDescription>
Manage platform administrators and moderators.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{adminUsers.map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={user.avatarUrl} alt={user.displayName} />
<AvatarFallback className="text-sm font-medium">
{getInitials(user.displayName)}
</AvatarFallback>
</Avatar>
<div className="space-y-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{user.displayName}</h4>
{getPermissionBadges(user)}
</div>
<div className="text-sm text-muted-foreground space-y-1">
<div>@{user.username} {user.email}</div>
{user.bio && <div className="max-w-md truncate">{user.bio}</div>}
<div className="text-xs">
Created {formatDate(user.createdAt)} Last updated {formatDate(user.updatedAt)}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteUser(user.id)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,225 @@
import { useState } from '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 { Shield, Crown, UserPlus } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { showSuccess, showError } from '@/utils/toast';
interface CreateAdminFormProps {
onSuccess?: () => void;
}
export function CreateAdminForm({ onSuccess }: CreateAdminFormProps) {
const { createSuperUser, createAdminUser } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [isSuperUser, setIsSuperUser] = useState(false);
const [formData, setFormData] = useState({
email: '',
username: '',
displayName: '',
password: '',
bio: '',
avatarUrl: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const success = isSuperUser
? await createSuperUser(formData)
: await createAdminUser(formData);
if (success) {
showSuccess(
isSuperUser
? 'Super user created successfully!'
: 'Admin user created successfully!'
);
setFormData({
email: '',
username: '',
displayName: '',
password: '',
bio: '',
avatarUrl: '',
});
onSuccess?.();
} else {
showError('Failed to create user. Please try again.');
}
} catch (error) {
showError('An error occurred while creating the user.');
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<Card className="w-full max-w-2xl">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserPlus className="h-5 w-5" />
Create {isSuperUser ? 'Super User' : 'Admin User'}
</CardTitle>
<CardDescription>
Create a new {isSuperUser ? 'super user' : 'admin user'} with elevated permissions.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<Button
type="button"
variant={isSuperUser ? "default" : "outline"}
size="sm"
onClick={() => setIsSuperUser(true)}
className="flex items-center gap-2"
>
<Crown className="h-4 w-4" />
Super User
</Button>
<Button
type="button"
variant={!isSuperUser ? "default" : "outline"}
size="sm"
onClick={() => setIsSuperUser(false)}
className="flex items-center gap-2"
>
<Shield className="h-4 w-4" />
Admin User
</Button>
</div>
{isSuperUser && (
<div className="p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-center gap-2 text-amber-800 dark:text-amber-200">
<Crown className="h-4 w-4" />
<span className="font-medium">Super User Permissions</span>
</div>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
Super users have full system access including user management, system configuration, and all moderation capabilities.
</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">
Email *
</label>
<Input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
placeholder="admin@example.com"
/>
</div>
<div className="space-y-2">
<label htmlFor="username" className="text-sm font-medium">
Username *
</label>
<Input
id="username"
name="username"
type="text"
required
value={formData.username}
onChange={handleInputChange}
placeholder="admin_user"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="displayName" className="text-sm font-medium">
Display Name *
</label>
<Input
id="displayName"
name="displayName"
type="text"
required
value={formData.displayName}
onChange={handleInputChange}
placeholder="Admin User"
/>
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium">
Password *
</label>
<Input
id="password"
name="password"
type="password"
required
value={formData.password}
onChange={handleInputChange}
placeholder="••••••••"
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="bio" className="text-sm font-medium">
Bio
</label>
<textarea
id="bio"
name="bio"
rows={3}
value={formData.bio}
onChange={handleInputChange}
placeholder="Brief description about this user..."
className="w-full px-3 py-2 border border-input bg-background rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
/>
</div>
<div className="space-y-2">
<label htmlFor="avatarUrl" className="text-sm font-medium">
Avatar URL
</label>
<Input
id="avatarUrl"
name="avatarUrl"
type="url"
value={formData.avatarUrl}
onChange={handleInputChange}
placeholder="https://example.com/avatar.jpg"
/>
</div>
<div className="flex items-center justify-between pt-4">
<div className="flex items-center gap-2">
<Badge variant={isSuperUser ? "default" : "secondary"}>
{isSuperUser ? "Super User" : "Admin User"}
</Badge>
<span className="text-sm text-muted-foreground">
{isSuperUser ? "Full system access" : "Moderation access"}
</span>
</div>
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : `Create ${isSuperUser ? 'Super User' : 'Admin User'}`}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"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",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,25 @@
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

127
src/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,33 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/hooks/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,212 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { generateId } from '@/lib/utils';
import { AdminUser, createSuperUser, createAdminUser, hasPermission } from '@/lib/admin';
interface User extends AdminUser {}
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<boolean>;
register: (email: string, username: string, displayName: string, password: string) => Promise<boolean>;
logout: () => void;
updateProfile: (updates: Partial<User>) => 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>;
hasPermission: (permission: string) => boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: React.ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check for existing session on mount
const checkAuth = async () => {
try {
const token = localStorage.getItem('scriptshare-auth-token');
if (token) {
// In a real app, you'd validate the token with your backend
// For now, we'll just check if it exists
const userData = localStorage.getItem('scriptshare-user-data');
if (userData) {
setUser(JSON.parse(userData));
}
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
const login = async (email: string, _password: string): Promise<boolean> => {
try {
setIsLoading(true);
// In a real app, you'd make an API call to your backend
// For now, we'll simulate a successful login
await new Promise(resolve => setTimeout(resolve, 1000));
// Simulate user data
const mockUser: User = {
id: generateId(),
email,
username: email.split('@')[0],
displayName: email.split('@')[0],
isAdmin: false,
isModerator: false,
isSuperUser: false,
permissions: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const token = generateId();
localStorage.setItem('scriptshare-auth-token', token);
localStorage.setItem('scriptshare-user-data', JSON.stringify(mockUser));
setUser(mockUser);
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
} finally {
setIsLoading(false);
}
};
const register = async (email: string, username: string, displayName: string, _password: string): Promise<boolean> => {
try {
setIsLoading(true);
// In a real app, you'd make an API call to your backend
// For now, we'll simulate a successful registration
await new Promise(resolve => setTimeout(resolve, 1000));
const mockUser: User = {
id: generateId(),
email,
username,
displayName,
isAdmin: false,
isModerator: false,
isSuperUser: false,
permissions: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const token = generateId();
localStorage.setItem('scriptshare-auth-token', token);
localStorage.setItem('scriptshare-user-data', JSON.stringify(mockUser));
setUser(mockUser);
return true;
} catch (error) {
console.error('Registration failed:', error);
return false;
} finally {
setIsLoading(false);
}
};
const logout = () => {
localStorage.removeItem('scriptshare-auth-token');
localStorage.removeItem('scriptshare-user-data');
setUser(null);
};
const updateProfile = async (updates: Partial<User>): Promise<boolean> => {
try {
if (!user) return false;
const updatedUser = { ...user, ...updates, updatedAt: new Date().toISOString() };
localStorage.setItem('scriptshare-user-data', JSON.stringify(updatedUser));
setUser(updatedUser);
return true;
} catch (error) {
console.error('Profile update 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
// For now, we'll simulate creating a superuser
await new Promise(resolve => setTimeout(resolve, 1000));
const newSuperUser = createSuperUser(userData);
// Store in localStorage for demo purposes
const existingUsers = JSON.parse(localStorage.getItem('scriptshare-admin-users') || '[]');
existingUsers.push(newSuperUser);
localStorage.setItem('scriptshare-admin-users', JSON.stringify(existingUsers));
return true;
} catch (error) {
console.error('Super user creation failed:', error);
return false;
}
};
const createAdminUserLocal = 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
// For now, we'll simulate creating an admin user
await new Promise(resolve => setTimeout(resolve, 1000));
const newAdminUser = createAdminUser(userData);
// Store in localStorage for demo purposes
const existingUsers = JSON.parse(localStorage.getItem('scriptshare-admin-users') || '[]');
existingUsers.push(newAdminUser);
localStorage.setItem('scriptshare-admin-users', JSON.stringify(existingUsers));
return true;
} catch (error) {
console.error('Admin user creation failed:', error);
return false;
}
};
const hasPermission = (permission: string): boolean => {
return hasPermission(user, permission);
};
const value = {
user,
isLoading,
login,
register,
logout,
updateProfile,
createSuperUser: createSuperUserLocal,
createAdminUser: createAdminUserLocal,
hasPermission,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@ -0,0 +1,89 @@
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'dark' | 'light' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'dark' | 'light';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
}
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'scriptshare-theme',
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem(storageKey);
return (stored as Theme) || defaultTheme;
});
const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>(() => {
if (theme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return theme;
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
root.classList.add(systemTheme);
setResolvedTheme(systemTheme);
} else {
root.classList.add(theme);
setResolvedTheme(theme);
}
}, [theme]);
useEffect(() => {
localStorage.setItem(storageKey, theme);
}, [theme, storageKey]);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (theme === 'system') {
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
setResolvedTheme(systemTheme);
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(systemTheme);
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
const value = {
theme,
setTheme,
resolvedTheme,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

191
src/hooks/use-toast.ts Normal file
View File

@ -0,0 +1,191 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

75
src/index.css Normal file
View File

@ -0,0 +1,75 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
}

103
src/lib/admin.ts Normal file
View File

@ -0,0 +1,103 @@
import { generateId } from './utils';
export interface AdminUser {
id: string;
email: string;
username: string;
displayName: string;
avatarUrl?: string;
bio?: string;
isAdmin: boolean;
isModerator: boolean;
isSuperUser: boolean;
permissions: string[];
createdAt: string;
updatedAt: string;
}
export interface CreateSuperUserData {
email: string;
username: string;
displayName: string;
password: string;
bio?: string;
avatarUrl?: string;
}
export 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'
] as const;
export const MODERATOR_PERMISSIONS = [
'script:approve',
'script:reject',
'comment:moderate',
'user:read'
] as const;
export function createSuperUser(userData: CreateSuperUserData): AdminUser {
return {
id: generateId(),
email: userData.email,
username: userData.username,
displayName: userData.displayName,
avatarUrl: userData.avatarUrl,
bio: userData.bio,
isAdmin: true,
isModerator: true,
isSuperUser: true,
permissions: [...SUPER_USER_PERMISSIONS],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}
export function createAdminUser(userData: CreateSuperUserData): AdminUser {
return {
id: generateId(),
email: userData.email,
username: userData.username,
displayName: userData.displayName,
avatarUrl: userData.avatarUrl,
bio: userData.bio,
isAdmin: true,
isModerator: true,
isSuperUser: false,
permissions: [...MODERATOR_PERMISSIONS],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}
export function hasPermission(user: AdminUser | null, permission: string): boolean {
if (!user) return false;
if (user.isSuperUser) return true;
return user.permissions.includes(permission);
}
export function canManageUsers(user: AdminUser | null): boolean {
return hasPermission(user, 'user:create') || hasPermission(user, 'user:promote');
}
export function canModerateContent(user: AdminUser | null): boolean {
return hasPermission(user, 'script:approve') || hasPermission(user, 'comment:moderate');
}
export function canViewAnalytics(user: AdminUser | null): boolean {
return hasPermission(user, 'analytics:view');
}
export function canConfigureSystem(user: AdminUser | null): boolean {
return hasPermission(user, 'system:configure');
}

26
src/lib/db/index.ts Normal file
View File

@ -0,0 +1,26 @@
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';
// Create the connection pool
const connection = await mysql.createConnection({
uri: process.env.DATABASE_URL!,
});
// Create the drizzle database instance
export const db = drizzle(connection, { schema, mode: 'default' });
// Export the schema for use in other parts of the app
export * from './schema';
// Test the connection
export const testConnection = async () => {
try {
await connection.ping();
console.log('✅ Database connection successful');
return true;
} catch (error) {
console.error('❌ Database connection failed:', error);
return false;
}
};

217
src/lib/db/schema.ts Normal file
View File

@ -0,0 +1,217 @@
import { mysqlTable, varchar, text, timestamp, int, boolean, json, index } from 'drizzle-orm/mysql-core';
import { relations } from 'drizzle-orm';
// Users table
export const users = mysqlTable('users', {
id: varchar('id', { length: 255 }).primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
username: varchar('username', { length: 100 }).notNull().unique(),
displayName: varchar('display_name', { length: 100 }).notNull(),
avatarUrl: varchar('avatar_url', { length: 500 }),
bio: text('bio'),
isAdmin: boolean('is_admin').default(false),
isModerator: boolean('is_moderator').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
}, (table) => ({
emailIdx: index('email_idx').on(table.email),
usernameIdx: index('username_idx').on(table.username),
}));
// Scripts table
export const scripts = mysqlTable('scripts', {
id: varchar('id', { length: 255 }).primaryKey(),
name: varchar('name', { length: 200 }).notNull(),
description: text('description').notNull(),
content: text('content').notNull(),
compatibleOs: json('compatible_os').$type<string[]>().notNull(),
categories: json('categories').$type<string[]>().notNull(),
tags: json('tags').$type<string[]>(),
gitRepositoryUrl: varchar('git_repository_url', { length: 500 }),
authorId: varchar('author_id', { length: 255 }).notNull(),
authorName: varchar('author_name', { length: 100 }).notNull(),
viewCount: int('view_count').default(0).notNull(),
downloadCount: int('download_count').default(0).notNull(),
rating: int('rating').default(0).notNull(),
ratingCount: int('rating_count').default(0).notNull(),
isApproved: boolean('is_approved').default(false).notNull(),
isPublic: boolean('is_public').default(true).notNull(),
version: varchar('version', { length: 20 }).default('1.0.0').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
}, (table) => ({
authorIdx: index('author_idx').on(table.authorId),
approvedIdx: index('approved_idx').on(table.isApproved),
publicIdx: index('public_idx').on(table.isPublic),
createdAtIdx: index('created_at_idx').on(table.createdAt),
}));
// Script versions table
export const scriptVersions = mysqlTable('script_versions', {
id: varchar('id', { length: 255 }).primaryKey(),
scriptId: varchar('script_id', { length: 255 }).notNull(),
version: varchar('version', { length: 20 }).notNull(),
content: text('content').notNull(),
changelog: text('changelog'),
createdAt: timestamp('created_at').defaultNow().notNull(),
createdBy: varchar('created_by', { length: 255 }).notNull(),
}, (table) => ({
scriptIdx: index('script_idx').on(table.scriptId),
versionIdx: index('version_idx').on(table.version),
}));
// Comments table
export const comments = mysqlTable('comments', {
id: varchar('id', { length: 255 }).primaryKey(),
scriptId: varchar('script_id', { length: 255 }).notNull(),
authorId: varchar('author_id', { length: 255 }).notNull(),
authorName: varchar('author_name', { length: 100 }).notNull(),
content: text('content').notNull(),
parentId: varchar('parent_id', { length: 255 }),
isApproved: boolean('is_approved').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
}, (table) => ({
scriptIdx: index('script_idx').on(table.scriptId),
authorIdx: index('author_idx').on(table.authorId),
parentIdx: index('parent_idx').on(table.parentId),
}));
// Ratings table
export const ratings = mysqlTable('ratings', {
id: varchar('id', { length: 255 }).primaryKey(),
scriptId: varchar('script_id', { length: 255 }).notNull(),
userId: varchar('user_id', { length: 255 }).notNull(),
rating: int('rating').notNull(), // 1-5 stars
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
}, (table) => ({
scriptIdx: index('script_idx').on(table.scriptId),
userIdx: index('user_idx').on(table.userId),
uniqueRating: index('unique_rating').on(table.scriptId, table.userId),
}));
// Script collections table
export const scriptCollections = mysqlTable('script_collections', {
id: varchar('id', { length: 255 }).primaryKey(),
name: varchar('name', { length: 200 }).notNull(),
description: text('description'),
authorId: varchar('author_id', { length: 255 }).notNull(),
isPublic: boolean('is_public').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
}, (table) => ({
authorIdx: index('author_idx').on(table.authorId),
publicIdx: index('public_idx').on(table.isPublic),
}));
// Collection scripts junction table
export const collectionScripts = mysqlTable('collection_scripts', {
id: varchar('id', { length: 255 }).primaryKey(),
collectionId: varchar('collection_id', { length: 255 }).notNull(),
scriptId: varchar('script_id', { length: 255 }).notNull(),
addedAt: timestamp('added_at').defaultNow().notNull(),
}, (table) => ({
collectionIdx: index('collection_idx').on(table.collectionId),
scriptIdx: index('script_idx').on(table.scriptId),
}));
// Script analytics table
export const scriptAnalytics = mysqlTable('script_analytics', {
id: varchar('id', { length: 255 }).primaryKey(),
scriptId: varchar('script_id', { length: 255 }).notNull(),
eventType: varchar('event_type', { length: 50 }).notNull(), // view, download, share
userId: varchar('user_id', { length: 255 }),
userAgent: text('user_agent'),
ipAddress: varchar('ip_address', { length: 45 }),
referrer: varchar('referrer', { length: 500 }),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
scriptIdx: index('script_idx').on(table.scriptId),
eventIdx: index('event_idx').on(table.eventType),
userIdx: index('user_idx').on(table.userId),
createdAtIdx: index('created_at_idx').on(table.createdAt),
}));
// Define relationships
export const usersRelations = relations(users, ({ many }) => ({
scripts: many(scripts),
comments: many(comments),
ratings: many(ratings),
collections: many(scriptCollections),
}));
export const scriptsRelations = relations(scripts, ({ one, many }) => ({
author: one(users, {
fields: [scripts.authorId],
references: [users.id],
}),
versions: many(scriptVersions),
comments: many(comments),
ratings: many(ratings),
analytics: many(scriptAnalytics),
}));
export const scriptVersionsRelations = relations(scriptVersions, ({ one }) => ({
script: one(scripts, {
fields: [scriptVersions.scriptId],
references: [scripts.id],
}),
}));
export const commentsRelations = relations(comments, ({ one, many }) => ({
script: one(scripts, {
fields: [comments.scriptId],
references: [scripts.id],
}),
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
parent: one(comments, {
fields: [comments.parentId],
references: [comments.id],
}),
replies: many(comments),
}));
export const ratingsRelations = relations(ratings, ({ one }) => ({
script: one(scripts, {
fields: [ratings.scriptId],
references: [scripts.id],
}),
user: one(users, {
fields: [ratings.userId],
references: [users.id],
}),
}));
export const scriptCollectionsRelations = relations(scriptCollections, ({ one, many }) => ({
author: one(users, {
fields: [scriptCollections.authorId],
references: [users.id],
}),
scripts: many(collectionScripts),
}));
export const collectionScriptsRelations = relations(collectionScripts, ({ one }) => ({
collection: one(scriptCollections, {
fields: [collectionScripts.collectionId],
references: [scriptCollections.id],
}),
script: one(scripts, {
fields: [collectionScripts.scriptId],
references: [scripts.id],
}),
}));
export const scriptAnalyticsRelations = relations(scriptAnalytics, ({ one }) => ({
script: one(scripts, {
fields: [scriptAnalytics.scriptId],
references: [scripts.id],
}),
user: one(users, {
fields: [scriptAnalytics.userId],
references: [users.id],
}),
}));

115
src/lib/utils.ts Normal file
View File

@ -0,0 +1,115 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function generateId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
export function formatDate(date: string | Date): string {
const d = new Date(date);
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
export function formatDateTime(date: string | Date): string {
const d = new Date(date);
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function formatRelativeTime(date: string | Date): string {
const now = new Date();
const d = new Date(date);
const diffInSeconds = Math.floor((now.getTime() - d.getTime()) / 1000);
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)}d ago`;
if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)}mo ago`;
return `${Math.floor(diffInSeconds / 31536000)}y ago`;
}
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export function isValidUsername(username: string): boolean {
const usernameRegex = /^[a-zA-Z0-9_-]{3,20}$/;
return usernameRegex.test(username);
}
export function copyToClipboard(text: string): Promise<boolean> {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text).then(() => true).catch(() => false);
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
textArea.remove();
return Promise.resolve(true);
} catch (err) {
textArea.remove();
return Promise.resolve(false);
}
}
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

34
src/pages/About.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { Code2 } from 'lucide-react';
export default function About() {
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-12">
<div className="text-center space-y-6">
<Code2 className="h-20 w-20 text-primary mx-auto" />
<h1 className="text-5xl font-bold">About ScriptShare</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
A community-driven platform for discovering, sharing, and collaborating on powerful bash scripts.
</p>
</div>
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
About page content coming soon! This will include our mission, team, and community information.
</p>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

165
src/pages/AdminPanel.tsx Normal file
View File

@ -0,0 +1,165 @@
import { useState } from 'react';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Button } from '@/components/ui/button';
import { Shield, Users, BarChart3, FileText, ArrowLeft } from 'lucide-react';
import { AdminDashboard } from '@/components/admin/AdminDashboard';
import { CreateAdminForm } from '@/components/admin/CreateAdminForm';
import { AdminUsersList } from '@/components/admin/AdminUsersList';
import { useAuth } from '@/contexts/AuthContext';
type AdminView = 'dashboard' | 'create-user' | 'users' | 'scripts' | 'analytics';
export default function AdminPanel() {
const { user } = useAuth();
const [currentView, setCurrentView] = useState<AdminView>('dashboard');
// Check if user has admin access
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 container mx-auto px-4 py-16">
<div className="max-w-2xl mx-auto text-center space-y-6">
<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.
</p>
<Button onClick={() => window.history.back()}>
Go Back
</Button>
</div>
</main>
<Footer />
</div>
);
}
const renderView = () => {
switch (currentView) {
case 'dashboard':
return (
<AdminDashboard
onCreateUser={() => setCurrentView('create-user')}
onViewScripts={() => setCurrentView('scripts')}
onViewAnalytics={() => setCurrentView('analytics')}
/>
);
case 'create-user':
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => setCurrentView('dashboard')}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Dashboard
</Button>
</div>
<CreateAdminForm onSuccess={() => setCurrentView('users')} />
</div>
);
case 'users':
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => setCurrentView('dashboard')}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Dashboard
</Button>
</div>
<Button onClick={() => setCurrentView('create-user')}>
<Users className="h-4 w-4 mr-2" />
Create Admin User
</Button>
</div>
<AdminUsersList />
</div>
);
case 'scripts':
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => setCurrentView('dashboard')}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Dashboard
</Button>
</div>
<div className="text-center py-12">
<FileText className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-2">Script Review</h2>
<p className="text-muted-foreground">
Script review functionality coming soon!
</p>
</div>
</div>
);
case 'analytics':
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => setCurrentView('dashboard')}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Dashboard
</Button>
</div>
<div className="text-center py-12">
<BarChart3 className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-2">Analytics Dashboard</h2>
<p className="text-muted-foreground">
Analytics functionality coming soon!
</p>
</div>
</div>
);
default:
return null;
}
};
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">
<Shield className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Admin Panel</h1>
<p className="text-xl text-muted-foreground">
Manage the platform and moderate content.
</p>
{user.isSuperUser && (
<div className="inline-flex items-center gap-2 px-3 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 rounded-full text-sm font-medium">
<Shield className="h-4 w-4" />
Super User Access
</div>
)}
</div>
{renderView()}
</div>
</main>
<Footer />
</div>
);
}

View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { FileCheck } from 'lucide-react';
export default function AdminScriptReview() {
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">
<FileCheck className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Script Review</h1>
<p className="text-xl text-muted-foreground">
Review and approve submitted scripts.
</p>
</div>
<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>
</Card>
</div>
</main>
<Footer />
</div>
);
}

34
src/pages/Dashboard.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { BarChart3 } from 'lucide-react';
export default function Dashboard() {
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>
<p className="text-xl text-muted-foreground">
Manage your scripts and track your contributions.
</p>
</div>
<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>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

34
src/pages/EditScript.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { Edit } from 'lucide-react';
export default function EditScript() {
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">
<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.
</p>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

349
src/pages/Index.tsx Normal file
View File

@ -0,0 +1,349 @@
import { useState, useEffect } from 'react';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { ScriptFilters, FilterOptions } from '@/components/ScriptFilters';
import { ScriptGrid } from '@/components/ScriptGrid';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Script } from '@/components/ScriptCard';
import { showError } from '@/utils/toast';
import { Code2, Users, FileText, Star, ArrowRight, Sparkles, Zap, Shield } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
// Mock data for development
const mockScripts: Script[] = [
{
id: '1',
name: 'System Backup Automation',
description: 'Automated backup script for Linux systems with compression and encryption support.',
compatible_os: ['Linux', 'Ubuntu', 'CentOS'],
categories: ['System Administration', 'Backup'],
git_repository_url: 'https://github.com/user/backup-script',
author_name: 'John Doe',
view_count: 1250,
created_at: '2024-01-15T10:00:00Z',
updated_at: '2024-01-20T14:30:00Z',
is_approved: true,
},
{
id: '2',
name: 'Docker Cleanup Utility',
description: 'Clean up unused Docker containers, images, and volumes to free up disk space.',
compatible_os: ['Linux', 'macOS', 'Windows'],
categories: ['DevOps', 'Docker'],
git_repository_url: 'https://github.com/user/docker-cleanup',
author_name: 'Jane Smith',
view_count: 890,
created_at: '2024-01-10T09:00:00Z',
updated_at: '2024-01-18T16:45:00Z',
is_approved: true,
},
{
id: '3',
name: 'Git Repository Manager',
description: 'Manage multiple Git repositories with status checking and batch operations.',
compatible_os: ['Linux', 'macOS', 'Windows'],
categories: ['Development', 'Git'],
git_repository_url: 'https://github.com/user/git-manager',
author_name: 'Mike Johnson',
view_count: 567,
created_at: '2024-01-05T11:00:00Z',
updated_at: '2024-01-12T13:20:00Z',
is_approved: true,
},
{
id: '4',
name: 'Network Monitor',
description: 'Monitor network connectivity and performance with detailed reporting.',
compatible_os: ['Linux', 'Ubuntu'],
categories: ['Networking', 'Monitoring'],
git_repository_url: 'https://github.com/user/network-monitor',
author_name: 'Sarah Wilson',
view_count: 432,
created_at: '2024-01-01T08:00:00Z',
updated_at: '2024-01-08T10:15:00Z',
is_approved: true,
},
];
const Index = () => {
const navigate = useNavigate();
const [scripts, setScripts] = useState<Script[]>([]);
const [filteredScripts, setFilteredScripts] = useState<Script[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [error, setError] = useState<string | null>(null);
// Load mock data on mount
useEffect(() => {
console.log('📜 Index component mounted at:', new Date().toISOString());
loadScripts();
}, []);
const loadScripts = async () => {
try {
setIsLoading(true);
setError(null);
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✅ Successfully loaded scripts:', mockScripts.length);
setScripts(mockScripts);
setFilteredScripts(mockScripts);
setError(null);
} catch (error: any) {
console.error('❌ Error in loadScripts:', error);
const errorMessage = error.message || 'Failed to load scripts';
setError(errorMessage);
showError(`Failed to load scripts: ${errorMessage}`);
} finally {
setIsLoading(false);
}
};
const handleFiltersChange = (filters: FilterOptions) => {
console.log('🔍 Applying filters:', filters);
let filtered = [...scripts];
// Apply search query
if (searchQuery) {
filtered = filtered.filter(script =>
script.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
script.author_name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Apply OS filter
if (filters.os.length > 0) {
filtered = filtered.filter(script =>
filters.os.some(os => script.compatible_os.includes(os))
);
}
// Apply category filter
if (filters.categories.length > 0) {
filtered = filtered.filter(script =>
filters.categories.some(category => script.categories.includes(category))
);
}
// Apply date filters
const now = new Date();
if (filters.dateAdded !== 'all') {
const days = getDateFilterDays(filters.dateAdded);
const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
filtered = filtered.filter(script => new Date(script.created_at) >= cutoff);
}
if (filters.recentlyUpdated !== 'all') {
const days = getDateFilterDays(filters.recentlyUpdated);
const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
filtered = filtered.filter(script => new Date(script.updated_at) >= cutoff);
}
console.log('🔍 Filtered scripts:', filtered.length);
setFilteredScripts(filtered);
};
const getDateFilterDays = (filter: string): number => {
switch (filter) {
case '1d': return 1;
case '1w': return 7;
case '1m': return 30;
case '1y': return 365;
default: return 0;
}
};
const handleSearch = (query: string) => {
console.log('🔍 Search query:', query);
setSearchQuery(query);
// Re-apply filters with new search query
handleFiltersChange({
os: [],
categories: [],
dateAdded: 'all',
recentlyUpdated: 'all',
});
};
const totalViews = scripts.reduce((sum, script) => sum + script.view_count, 0);
const totalAuthors = new Set(scripts.map(script => script.author_name)).size;
return (
<div className="min-h-screen flex flex-col bg-gradient-to-br from-background via-background to-primary/5">
<Header onSearch={handleSearch} />
<main className="flex-1">
{/* Hero Section */}
<section className="relative overflow-hidden">
{/* Animated background elements */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full blur-3xl animate-pulse" />
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-tr from-accent/20 to-primary/20 rounded-full blur-3xl animate-pulse delay-1000" />
</div>
<div className="container mx-auto px-4 py-16 relative z-10">
<div className="text-center space-y-8 max-w-4xl mx-auto">
{/* Main heading with enhanced styling */}
<div className="space-y-4">
<div className="flex items-center justify-center gap-2 mb-4">
<Sparkles className="h-6 w-6 text-accent animate-pulse" />
<span className="text-sm font-semibold text-muted-foreground tracking-wider uppercase">
Community Driven
</span>
<Sparkles className="h-6 w-6 text-primary animate-pulse delay-500" />
</div>
<h1 className="text-5xl md:text-7xl font-bold bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent animate-gradient-x leading-tight">
Bash Script Library
</h1>
<p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
Discover, share, and collaborate on powerful bash scripts for
<span className="text-primary font-semibold"> system administration</span>,
<span className="text-accent font-semibold"> automation</span>, and
<span className="text-primary font-semibold"> development</span>.
</p>
</div>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Button
size="lg"
className="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-white shadow-2xl hover:shadow-primary/30 transition-all duration-300 rounded-xl px-8 py-3 text-lg font-semibold group"
onClick={() => navigate('/search')}
>
Explore Scripts
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform duration-200" />
</Button>
<Button
size="lg"
variant="outline"
className="border-primary/30 hover:border-primary hover:bg-gradient-to-r hover:from-primary/10 hover:to-accent/10 transition-all duration-300 rounded-xl px-8 py-3 text-lg font-semibold shadow-lg"
onClick={() => navigate('/submit')}
>
<Code2 className="mr-2 h-5 w-5" />
Share Your Script
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
<Card className="bg-gradient-to-br from-card/80 to-card/40 backdrop-blur-sm border-primary/20 hover:border-primary/40 transition-all duration-300 hover:scale-105">
<CardContent className="p-6 text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-primary/20 to-primary/30 rounded-full mb-4">
<FileText className="h-6 w-6 text-primary" />
</div>
<div className="text-3xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
{scripts.length}
</div>
<div className="text-muted-foreground font-medium">Quality Scripts</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-card/80 to-card/40 backdrop-blur-sm border-accent/20 hover:border-accent/40 transition-all duration-300 hover:scale-105">
<CardContent className="p-6 text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-accent/20 to-accent/30 rounded-full mb-4">
<Users className="h-6 w-6 text-accent" />
</div>
<div className="text-3xl font-bold bg-gradient-to-r from-accent to-primary bg-clip-text text-transparent">
{totalAuthors}
</div>
<div className="text-muted-foreground font-medium">Contributors</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-card/80 to-card/40 backdrop-blur-sm border-primary/20 hover:border-primary/40 transition-all duration-300 hover:scale-105">
<CardContent className="p-6 text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-primary/20 to-accent/20 rounded-full mb-4">
<Star className="h-6 w-6 text-primary" />
</div>
<div className="text-3xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
{totalViews.toLocaleString()}
</div>
<div className="text-muted-foreground font-medium">Total Views</div>
</CardContent>
</Card>
</div>
{/* Feature highlights */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-16">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-primary/10 to-primary/20 rounded-2xl">
<Zap className="h-8 w-8 text-primary" />
</div>
<h3 className="text-xl font-bold">Lightning Fast</h3>
<p className="text-muted-foreground">
Find the perfect script in seconds with our advanced search and filtering system.
</p>
</div>
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-accent/10 to-accent/20 rounded-2xl">
<Shield className="h-8 w-8 text-accent" />
</div>
<h3 className="text-xl font-bold">Community Reviewed</h3>
<p className="text-muted-foreground">
All scripts are reviewed by our community to ensure quality and security.
</p>
</div>
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-primary/10 to-accent/10 rounded-2xl">
<Code2 className="h-8 w-8 text-primary" />
</div>
<h3 className="text-xl font-bold">Ready to Use</h3>
<p className="text-muted-foreground">
Copy, download, or fork scripts directly. No setup required.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Scripts Section */}
<section className="container mx-auto px-4 py-16 space-y-8">
<div className="text-center space-y-4">
<h2 className="text-3xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
Featured Scripts
</h2>
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
Explore our curated collection of bash scripts, crafted by developers for developers.
</p>
</div>
{/* Error Display */}
{error && (
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
<CardContent className="p-4">
<div className="text-red-800 dark:text-red-200">
<strong>Error loading scripts:</strong> {error}
<Button
variant="outline"
size="sm"
onClick={loadScripts}
className="ml-4"
>
Retry
</Button>
</div>
</CardContent>
</Card>
)}
<ScriptFilters onFiltersChange={handleFiltersChange} />
<ScriptGrid scripts={filteredScripts} isLoading={isLoading} />
</section>
</main>
<Footer />
</div>
);
};
export default Index;

34
src/pages/Login.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { LogIn } from 'lucide-react';
export default function Login() {
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>
<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>
</main>
<Footer />
</div>
);
}

34
src/pages/MyScripts.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { FolderOpen } from 'lucide-react';
export default function MyScripts() {
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.
</p>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

37
src/pages/NotFound.tsx Normal file
View File

@ -0,0 +1,37 @@
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Code2, Home, ArrowLeft } from 'lucide-react';
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-background to-primary/5">
<div className="text-center space-y-6 max-w-md mx-auto px-4">
<div className="flex justify-center">
<Code2 className="h-24 w-24 text-primary/50" />
</div>
<div className="space-y-2">
<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.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button asChild>
<Link to="/">
<Home className="mr-2 h-4 w-4" />
Go Home
</Link>
</Button>
<Button variant="outline" onClick={() => window.history.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
</div>
</div>
</div>
);
}

34
src/pages/Privacy.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { Shield } from 'lucide-react';
export default function Privacy() {
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">
<Shield className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Privacy Policy</h1>
<p className="text-xl text-muted-foreground">
How we protect and handle your data.
</p>
</div>
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Privacy Policy content coming soon! This will include detailed information about data handling and privacy practices.
</p>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

34
src/pages/Profile.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { User } from 'lucide-react';
export default function Profile() {
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">
<User className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Profile</h1>
<p className="text-xl text-muted-foreground">
Manage your account settings and preferences.
</p>
</div>
<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>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { FileCode } from 'lucide-react';
export default function ScriptDetail() {
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>
<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>
</Card>
</div>
</main>
<Footer />
</div>
);
}

34
src/pages/Search.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { Search as SearchIcon } from 'lucide-react';
export default function Search() {
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" />
<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>
</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>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { Upload } from 'lucide-react';
export default function SubmitScript() {
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">
<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.
</p>
</div>
<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>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

34
src/pages/Terms.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Card, CardContent } from '@/components/ui/card';
import { FileText } from 'lucide-react';
export default function Terms() {
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">
<FileText className="h-16 w-16 text-primary mx-auto" />
<h1 className="text-4xl font-bold">Terms of Service</h1>
<p className="text-xl text-muted-foreground">
Our terms and conditions for using the platform.
</p>
</div>
<Card>
<CardContent className="p-8">
<p className="text-muted-foreground text-center">
Terms of Service content coming soon! This will include detailed terms and conditions for platform usage.
</p>
</CardContent>
</Card>
</div>
</main>
<Footer />
</div>
);
}

39
src/utils/toast.ts Normal file
View File

@ -0,0 +1,39 @@
import { toast } from 'sonner';
export const showSuccess = (message: string) => {
toast.success(message, {
duration: 4000,
position: 'top-right',
});
};
export const showError = (message: string) => {
toast.error(message, {
duration: 6000,
position: 'top-right',
});
};
export const showInfo = (message: string) => {
toast.info(message, {
duration: 4000,
position: 'top-right',
});
};
export const showWarning = (message: string) => {
toast.warning(message, {
duration: 5000,
position: 'top-right',
});
};
export const showLoading = (message: string) => {
return toast.loading(message, {
position: 'top-right',
});
};
export const dismissToast = (toastId: string | number) => {
toast.dismiss(toastId);
};

91
tailwind.config.ts Normal file
View File

@ -0,0 +1,91 @@
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"gradient-x": {
"0%, 100%": {
"background-size": "200% 200%",
"background-position": "left center",
},
"50%": {
"background-size": "200% 200%",
"background-position": "right center",
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"gradient-x": "gradient-x 15s ease infinite",
},
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
} satisfies Config
export default config

31
tsconfig.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

21
vite.config.ts Normal file
View File

@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
port: 5173,
host: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
})

27
webapp-superuser.json Normal file
View File

@ -0,0 +1,27 @@
{
"id": "92acee3490facf4eef9d2a5695397ca6",
"email": "admin@scriptshare.com",
"username": "admin",
"displayName": "System Administrator",
"avatarUrl": "",
"bio": "Default system administrator account",
"isAdmin": true,
"isModerator": true,
"isSuperUser": true,
"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"
],
"createdAt": "2025-08-12T21:28:52.080Z",
"updatedAt": "2025-08-12T21:28:52.080Z"
}