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

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