commit e7c9ea5386abf2c6d9823303dc3f80c7246bb2b2 Author: Michael Howard Date: Wed Apr 29 13:36:44 2026 -0500 Initial commit of ChurchTube v2.7 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e2d7ef --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# ⛪ ChurchTube + +ChurchTube is a premium, self-hosted video platform designed specifically for churches to share sermons and media with their congregation. It features a modern, "YouTube-like" interface with advanced moderation and branding tools. + +![ChurchTube Preview](assets/images/screenshot.png) + +## ✨ Features + +- **Premium UX**: Modern, responsive design with glassmorphism and smooth animations. +- **Dual Video Sources**: Upload videos directly or link them from external sources (NAS, Google Drive, Cloud). +- **Smart Google Drive Support**: Automatically converts Google Drive links for seamless embedding. +- **Interactive Community**: + - AJAX-based commenting (no video reloads). + - 5 reaction types (👍, ❤️, 🙏, 💡, 👏). + - Automated Profanity Filter with auto-reporting. +- **Robust Moderation**: + - Role-Based Access Control (Admin, Moderator, Editor, User). + - Dedicated Admin/Moderator dashboard for reports and users. +- **Custom Branding**: Real-time control over site title, colors, logo, and footer. +- **Search & Discovery**: Tag-based categorization, search, and intelligent recommendations. +- **Analytics**: Engagement-based view counting (only counts when video is played). + +## 🛠️ Technology Stack + +- **Backend**: PHP 7.4+ (PDO for database security) +- **Database**: MySQL / MariaDB +- **Frontend**: Vanilla HTML5, CSS3 (Modern Flexbox/Grid), Javascript (ES6+) +- **Icons**: FontAwesome 6+ + +## 🚀 Installation + +1. **Clone the repository**: + ```bash + git clone https://git.linology.tech/michael/churchtube.git + ``` + +2. **Configure Permissions**: + Ensure the `uploads/` and `includes/` directories are writable by the web server: + ```bash + chmod -R 775 uploads/ includes/ + chown -R www-data:www-data uploads/ includes/ + ``` + +3. **Run the Wizard**: + Navigate to `http://yourdomain.com/install.php` in your browser. The First-Run Wizard will guide you through: + - PHP environment checks (upload limits, extensions). + - Database connection setup. + - Creating the primary Admin account. + +4. **Security Hardening**: + After installation, it is recommended to delete `install.php` or set its permissions to `000`. + +## 📁 Storage Recommendations + +- **Local Storage**: Best for high performance if your server has ample space. +- **NAS/Cloud**: Perfect for large libraries. Simply provide the direct link to the MP4 file or a Google Drive "view" link. + +## 📄 License + +This project is licensed under the MIT License. Feel free to use and modify it for your church's needs. + +--- +*Built with love for the Church community.* diff --git a/admin/add_video.php b/admin/add_video.php new file mode 100644 index 0000000..0103186 --- /dev/null +++ b/admin/add_video.php @@ -0,0 +1,179 @@ +prepare("INSERT INTO videos (title, description, release_date, source_type, video_url, thumbnail_url, uploader_id) VALUES (?, ?, ?, ?, ?, ?, ?)"); + if ($stmt->execute([$title, $description, $release_date, $source_type, $video_url, $thumbnail_url, $_SESSION['user_id']])) { + $video_id = $pdo->lastInsertId(); + if (!empty($_POST['tags'])) { + $tags = explode(',', $_POST['tags']); + foreach ($tags as $tag_name) { + $tag_name = trim($tag_name); + if (!$tag_name) continue; + $pdo->prepare("INSERT IGNORE INTO tags (name) VALUES (?)")->execute([$tag_name]); + $tag_stmt = $pdo->prepare("SELECT id FROM tags WHERE name = ?"); + $tag_stmt->execute([$tag_name]); + $tag_id = $tag_stmt->fetchColumn(); + $pdo->prepare("INSERT IGNORE INTO video_tags (video_id, tag_id) VALUES (?, ?)")->execute([$video_id, $tag_id]); + } + } + $success = "Video added successfully!"; + } else { + $error = "Database error. Could not add video."; + } + } +} + +// Reuse header by adjusting paths +ob_start(); +require_once '../includes/header.php'; +$header = ob_get_clean(); +echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header); +?> + +
+
+

Add New Sermon

+ + +
+ +
+ + + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + + Max size: +
+ + + +
+ + + Recommended: 1280x720 (16:9) +
+ +
+ + Cancel +
+
+
+
+ + + + diff --git a/admin/edit_video.php b/admin/edit_video.php new file mode 100644 index 0000000..6d19337 --- /dev/null +++ b/admin/edit_video.php @@ -0,0 +1,190 @@ +prepare("SELECT * FROM videos WHERE id = ?"); +$stmt->execute([$id]); +$video = $stmt->fetch(); + +if (!$video) { + die("Video not found."); +} + +$error = ''; +$success = ''; + +function formatVideoUrl($url) { + if (strpos($url, 'drive.google.com') !== false) { + $url = preg_replace('/\/view(\?.*)?$/', '/preview', $url); + if (strpos($url, '/preview') === false && strpos($url, '/file/d/') !== false) { + $url = rtrim($url, '/') . '/preview'; + } + } + return $url; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $title = trim($_POST['title']); + $description = trim($_POST['description']); + $release_date = $_POST['release_date']; + $source_type = $_POST['source_type']; + $video_url = $video['video_url']; + $thumbnail_url = $video['thumbnail_url']; + + // Handle Thumbnail Upload + if (isset($_FILES['thumbnail_file']) && $_FILES['thumbnail_file']['error'] === 0) { + $t_ext = strtolower(pathinfo($_FILES['thumbnail_file']['name'], PATHINFO_EXTENSION)); + $allowed_img = ['jpg', 'jpeg', 'png', 'webp']; + + if (in_array($t_ext, $allowed_img)) { + $t_filename = 'thumb_' . uniqid() . '.' . $t_ext; + if (move_uploaded_file($_FILES['thumbnail_file']['tmp_name'], '../uploads/' . $t_filename)) { + if ($video['thumbnail_url'] && strpos($video['thumbnail_url'], 'uploads/') === 0) { + @unlink('../' . $video['thumbnail_url']); + } + $thumbnail_url = 'uploads/' . $t_filename; + } + } else { + $error = "Invalid thumbnail format."; + } + } + + if (!$error && $source_type === 'upload' && isset($_FILES['video_file']) && $_FILES['video_file']['error'] === 0) { + $ext = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)); + $allowed_vid = ['mp4', 'webm', 'ogg']; + + if (in_array($ext, $allowed_vid)) { + $filename = uniqid() . '.' . $ext; + $upload_path = '../uploads/' . $filename; + if (move_uploaded_file($_FILES['video_file']['tmp_name'], $upload_path)) { + if ($video['source_type'] === 'upload') @unlink('../' . $video['video_url']); + $video_url = 'uploads/' . $filename; + } + } else { + $error = "Invalid video format."; + } + } elseif (!$error && $source_type === 'link') { + $video_url = formatVideoUrl(trim($_POST['external_url'])); + } + + $stmt = $pdo->prepare("UPDATE videos SET title = ?, description = ?, release_date = ?, source_type = ?, video_url = ?, thumbnail_url = ? WHERE id = ?"); + if ($stmt->execute([$title, $description, $release_date, $source_type, $video_url, $thumbnail_url, $id])) { + // Update tags + $pdo->prepare("DELETE FROM video_tags WHERE video_id = ?")->execute([$id]); + if (!empty($_POST['tags'])) { + $tags = explode(',', $_POST['tags']); + foreach ($tags as $tag_name) { + $tag_name = trim($tag_name); + if (!$tag_name) continue; + $pdo->prepare("INSERT IGNORE INTO tags (name) VALUES (?)")->execute([$tag_name]); + $tag_stmt = $pdo->prepare("SELECT id FROM tags WHERE name = ?"); + $tag_stmt->execute([$tag_name]); + $tag_id = $tag_stmt->fetchColumn(); + $pdo->prepare("INSERT IGNORE INTO video_tags (video_id, tag_id) VALUES (?, ?)")->execute([$id, $tag_id]); + } + } + $success = "Video updated successfully!"; + // Refresh video data + $stmt = $pdo->prepare("SELECT * FROM videos WHERE id = ?"); + $stmt->execute([$id]); + $video = $stmt->fetch(); + } else { + $error = "Update failed."; + } +} + +// Get current tags +$stmt = $pdo->prepare("SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag_id WHERE vt.video_id = ?"); +$stmt->execute([$id]); +$current_tags = $stmt->fetchAll(PDO::FETCH_COLUMN); +$tags_str = implode(', ', $current_tags); + +ob_start(); +require_once '../includes/header.php'; +$header = ob_get_clean(); +echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header); +?> + +
+
+

Edit Sermon:

+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + + Current: +
+ + + +
+ + +
+ +
+ + + Leave empty to keep current thumbnail. +
+ +
+ + Back to Dashboard +
+
+
+
+ + + + diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..af489e5 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,103 @@ +prepare("SELECT video_url, source_type FROM videos WHERE id = ?"); + $stmt->execute([$id]); + $v = $stmt->fetch(); + if ($v && $v['source_type'] === 'upload') { + @unlink('../' . $v['video_url']); + } + + $pdo->prepare("DELETE FROM videos WHERE id = ?")->execute([$id]); + header('Location: index.php?msg=deleted'); + exit; +} + +$stmt = $pdo->prepare("SELECT v.*, u.username FROM videos v JOIN users u ON v.uploader_id = u.id ORDER BY v.created_at DESC"); +$stmt->execute(); +$videos = $stmt->fetchAll(); + +// Get reported comments count +$reported_count = $pdo->query("SELECT COUNT(*) FROM comments WHERE is_reported = TRUE")->fetchColumn(); + +// Reuse header +ob_start(); +require_once '../includes/header.php'; +$header = ob_get_clean(); +echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header); +?> + +
+ +
+ + + Branding & Site + + + + Manage Users + + + + Reports () + 0): ?> + ! + + +
+ +
+

Manage Sermons

+ Add New Video +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
VideoUploaderDateViewsActions
+
+
+ +
+
+
+ + + +
+
No videos uploaded yet.
+
+
+ + diff --git a/admin/reports.php b/admin/reports.php new file mode 100644 index 0000000..7e209e9 --- /dev/null +++ b/admin/reports.php @@ -0,0 +1,83 @@ +prepare("DELETE FROM comments WHERE id = ?")->execute([$comment_id]); + $msg = "Comment deleted."; + } elseif (isset($_POST['ignore'])) { + $pdo->prepare("UPDATE comments SET is_reported = FALSE WHERE id = ?")->execute([$comment_id]); + $msg = "Report dismissed."; + } +} + +$reports = $pdo->query(" + SELECT c.*, u.username, v.title as video_title + FROM comments c + JOIN users u ON c.user_id = u.id + JOIN videos v ON c.video_id = v.id + WHERE c.is_reported = TRUE + ORDER BY c.created_at DESC +")->fetchAll(); + +ob_start(); +require_once '../includes/header.php'; +$header = ob_get_clean(); +echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header); +?> + +
+
+

Reported Comments

+ Back to Dashboard +
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
CommentUserVideoActions
+
""
+
+
+
+ + + +
+
No reported comments. Everything is clean!
+
+
+ + diff --git a/admin/settings.php b/admin/settings.php new file mode 100644 index 0000000..fd4d97f --- /dev/null +++ b/admin/settings.php @@ -0,0 +1,95 @@ + 'ChurchTube', + 'primary_color' => '#7c4dff', + 'secondary_color' => '#ff4081', + 'logo_url' => '', + 'footer_text' => '© 2024 ChurchTube. All rights reserved.' + ]; + $stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?"); + foreach ($defaults as $k => $v) { + $stmt->execute([$v, $k]); + } + $success = "Defaults restored!"; + } else { + $keys = ['site_title', 'primary_color', 'secondary_color', 'logo_url', 'footer_text']; + $stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?"); + foreach ($keys as $key) { + $stmt->execute([$_POST[$key], $key]); + } + $success = "Settings updated!"; + } +} + +// Get current settings +$settings = []; +$res = $pdo->query("SELECT * FROM settings")->fetchAll(); +foreach ($res as $r) { + $settings[$r['setting_key']] = $r['setting_value']; +} + +ob_start(); +require_once '../includes/header.php'; +$header = ob_get_clean(); +echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header); +?> + +
+
+
+

Site Settings & Branding

+
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + + Leave empty to use text title as logo. +
+ +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/admin/users.php b/admin/users.php new file mode 100644 index 0000000..a2d1307 --- /dev/null +++ b/admin/users.php @@ -0,0 +1,169 @@ +prepare("SELECT * FROM users WHERE id = ?"); + $stmt->execute([$edit_id]); + $edit_user = $stmt->fetch(); +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['add_user'])) { + $username = trim($_POST['username']); + $email = trim($_POST['email']); + $password = password_hash($_POST['password'], PASSWORD_DEFAULT); + $role = $_POST['role']; + + $stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, ?)"); + try { + $stmt->execute([$username, $email, $password, $role]); + $success = "User added successfully!"; + } catch (Exception $e) { + $error = "Error adding user: " . $e->getMessage(); + } + } elseif (isset($_POST['update_user'])) { + $id = (int)$_POST['user_id']; + $username = trim($_POST['username']); + $email = trim($_POST['email']); + $role = $_POST['role']; + + // Protection: Don't demote yourself + if ($id == $_SESSION['user_id'] && $role !== 'admin') { + $error = "You cannot demote yourself from Admin!"; + } else { + $stmt = $pdo->prepare("UPDATE users SET username = ?, email = ?, role = ? WHERE id = ?"); + try { + $stmt->execute([$username, $email, $role, $id]); + $success = "User updated successfully!"; + header("Location: users.php?msg=" . urlencode($success)); + exit; + } catch (Exception $e) { + $error = "Error updating user: " . $e->getMessage(); + } + } + } elseif (isset($_POST['delete_user'])) { + $id = (int)$_POST['user_id']; + if ($id != $_SESSION['user_id']) { + $pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]); + $success = "User deleted!"; + } else { + $error = "You cannot delete yourself!"; + } + } +} + +if (isset($_GET['msg'])) $success = $_GET['msg']; + +$users = $pdo->query("SELECT id, username, email, role, created_at FROM users ORDER BY created_at DESC")->fetchAll(); + +ob_start(); +require_once '../includes/header.php'; +$header = ob_get_clean(); +echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header); +?> + +
+
+ + +
+

System Users

+ + + + + + + + + + + + + + + + + +
UsernameRoleAction
+
+
+
+ + + + +
+ +
+ + +
+
+
+
+ + +
+

+
+
+ +
+ + + + + + + +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + Cancel + +
+
+ +
+
+ + diff --git a/api/delete_comment.php b/api/delete_comment.php new file mode 100644 index 0000000..66340ac --- /dev/null +++ b/api/delete_comment.php @@ -0,0 +1,25 @@ + false, 'error' => 'Moderator privileges required']); + exit; +} + +$comment_id = (int)($_POST['comment_id'] ?? 0); + +if (!$comment_id) { + echo json_encode(['success' => false, 'error' => 'Invalid data']); + exit; +} + +try { + $pdo->prepare("DELETE FROM comments WHERE id = ?")->execute([$comment_id]); + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => 'DB error']); +} +?> diff --git a/api/get_comments.php b/api/get_comments.php new file mode 100644 index 0000000..76d19d3 --- /dev/null +++ b/api/get_comments.php @@ -0,0 +1,36 @@ +prepare(" + SELECT c.*, u.username, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'thumb') as thumbs, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'heart') as hearts, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'pray') as prays, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'insight') as insights, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'clap') as claps + FROM comments c JOIN users u ON c.user_id = u.id + WHERE c.video_id = ? ORDER BY c.created_at DESC + "); + $stmt->execute([$video_id]); + $comments = $stmt->fetchAll(); + + // Format dates for display + foreach ($comments as &$c) { + $c['display_date'] = date('M d, Y', strtotime($c['created_at'])); + } + + echo json_encode($comments); +} catch (Exception $e) { + echo json_encode([]); +} +?> diff --git a/api/increment_views.php b/api/increment_views.php new file mode 100644 index 0000000..a63c6a8 --- /dev/null +++ b/api/increment_views.php @@ -0,0 +1,20 @@ + false, 'error' => 'Invalid video ID']); + exit; +} + +try { + $stmt = $pdo->prepare("UPDATE videos SET views = views + 1 WHERE id = ?"); + $stmt->execute([$video_id]); + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => 'Database error']); +} +?> diff --git a/api/post_comment.php b/api/post_comment.php new file mode 100644 index 0000000..686a3d9 --- /dev/null +++ b/api/post_comment.php @@ -0,0 +1,43 @@ + false, 'error' => 'Login required']); + exit; +} + +$video_id = (int)($_POST['video_id'] ?? 0); +$comment_text = trim($_POST['comment'] ?? ''); + +if (!$video_id || empty($comment_text)) { + echo json_encode(['success' => false, 'error' => 'Invalid data']); + exit; +} + +// Profanity Filter +$bad_words = ['damn', 'hell', 'crap', 'shit', 'fuck', 'ass', 'bitch']; // Basic list, user can expand +$is_flagged = false; +$filtered_text = $comment_text; + +foreach ($bad_words as $word) { + $pattern = '/\b' . preg_quote($word, '/') . '\b/i'; + if (preg_match($pattern, $comment_text)) { + $is_flagged = true; + $filtered_text = preg_replace($pattern, str_repeat('*', strlen($word)), $filtered_text); + } +} + +try { + $stmt = $pdo->prepare("INSERT INTO comments (video_id, user_id, comment_text, is_reported) VALUES (?, ?, ?, ?)"); + if ($stmt->execute([$video_id, $_SESSION['user_id'], $filtered_text, $is_flagged ? 1 : 0])) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false, 'error' => 'Database error']); + } +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} +?> diff --git a/api/react.php b/api/react.php new file mode 100644 index 0000000..403dcc9 --- /dev/null +++ b/api/react.php @@ -0,0 +1,43 @@ + false, 'error' => 'Login required']); + exit; +} + +$comment_id = (int)($_POST['comment_id'] ?? 0); +$type = $_POST['type'] ?? ''; + +if (!$comment_id || !in_array($type, ['thumb', 'heart', 'pray', 'insight', 'clap'])) { + echo json_encode(['success' => false, 'error' => 'Invalid data']); + exit; +} + +try { + // Check if exists + $stmt = $pdo->prepare("SELECT id FROM reactions WHERE comment_id = ? AND user_id = ? AND reaction_type = ?"); + $stmt->execute([$comment_id, $_SESSION['user_id'], $type]); + $exists = $stmt->fetch(); + + if ($exists) { + $pdo->prepare("DELETE FROM reactions WHERE id = ?")->execute([$exists['id']]); + $action = 'removed'; + } else { + $pdo->prepare("INSERT INTO reactions (comment_id, user_id, reaction_type) VALUES (?, ?, ?)")->execute([$comment_id, $_SESSION['user_id'], $type]); + $action = 'added'; + } + + // Get new count + $stmt = $pdo->prepare("SELECT COUNT(*) FROM reactions WHERE comment_id = ? AND reaction_type = ?"); + $stmt->execute([$comment_id, $type]); + $count = $stmt->fetchColumn(); + + echo json_encode(['success' => true, 'action' => $action, 'count' => $count]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => 'DB error']); +} +?> diff --git a/api/report.php b/api/report.php new file mode 100644 index 0000000..fb45c8c --- /dev/null +++ b/api/report.php @@ -0,0 +1,26 @@ + false, 'error' => 'Login required']); + exit; +} + +$comment_id = (int)($_POST['comment_id'] ?? 0); + +if (!$comment_id) { + echo json_encode(['success' => false, 'error' => 'Invalid data']); + exit; +} + +try { + $stmt = $pdo->prepare("UPDATE comments SET is_reported = TRUE WHERE id = ?"); + $stmt->execute([$comment_id]); + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => 'DB error']); +} +?> diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..42b5101 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,215 @@ +:root { + --primary-color: #7c4dff; + --secondary-color: #ff4081; + --bg-dark: #0f0f0f; + --bg-card: #1e1e1e; + --text-main: #f1f1f1; + --text-muted: #aaaaaa; + --accent: #ffd740; + --glass: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--bg-dark); + color: var(--text-main); + font-family: var(--font-sans); + line-height: 1.6; + overflow-x: hidden; +} + +a { + color: inherit; + text-decoration: none; +} + +/* Header & Navigation */ +header { + height: 64px; + background: rgba(15, 15, 15, 0.95); + backdrop-filter: blur(10px); + position: sticky; + top: 0; + z-index: 1000; + display: flex; + align-items: center; + padding: 0 24px; + border-bottom: 1px solid var(--glass-border); + justify-content: space-between; +} + +.logo { + font-size: 1.5rem; + font-weight: 800; + letter-spacing: -1px; + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.search-bar { + flex: 0 1 600px; + display: flex; + background: var(--glass); + border: 1px solid var(--glass-border); + border-radius: 40px; + padding: 4px 16px; + margin: 0 20px; +} + +.search-bar input { + background: transparent; + border: none; + color: white; + width: 100%; + padding: 8px; + font-size: 1rem; +} + +.search-bar input:focus { + outline: none; +} + +/* Video Grid */ +.video-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 24px; + padding: 24px; +} + +.video-card { + background: var(--bg-card); + border-radius: 12px; + overflow: hidden; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--glass-border); + cursor: pointer; +} + +.video-card:hover { + transform: translateY(-8px); + box-shadow: 0 10px 30px rgba(0,0,0,0.5); +} + +.video-thumbnail { + width: 100%; + aspect-ratio: 16 / 9; + background: #333; + background-size: cover; + background-position: center; + position: relative; +} + +.video-duration { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0,0,0,0.8); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.8rem; +} + +.video-info { + padding: 16px; +} + +.video-title { + font-weight: 600; + margin-bottom: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.video-meta { + font-size: 0.9rem; + color: var(--text-muted); +} + +/* Buttons */ +.btn { + padding: 10px 24px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + border: none; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.btn-primary { + background: var(--primary-color); + color: white; +} + +.btn-primary:hover { + filter: brightness(1.1); + transform: scale(1.02); +} + +/* Forms */ +.form-group { + margin-bottom: 20px; +} + +.form-label { + display: block; + margin-bottom: 8px; + color: var(--text-muted); + font-size: 0.9rem; +} + +.form-control { + width: 100%; + padding: 12px 16px; + background: var(--glass); + border: 1px solid var(--glass-border); + border-radius: 8px; + color: white; + font-family: inherit; + transition: border-color 0.2s; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + background: rgba(255,255,255,0.08); +} + +/* Wizard / Auth Layout */ +.centered-container { + max-width: 500px; + margin: 80px auto; + background: var(--bg-card); + padding: 40px; + border-radius: 20px; + border: 1px solid var(--glass-border); + box-shadow: 0 20px 40px rgba(0,0,0,0.4); +} + +.wizard-step { + display: none; +} + +.wizard-step.active { + display: block; + animation: fadeIn 0.4s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/assets/images/default_thumb.png b/assets/images/default_thumb.png new file mode 100644 index 0000000..2cd7be5 Binary files /dev/null and b/assets/images/default_thumb.png differ diff --git a/assets/images/screenshot.png b/assets/images/screenshot.png new file mode 100644 index 0000000..a28b43d Binary files /dev/null and b/assets/images/screenshot.png differ diff --git a/diagnostics.php b/diagnostics.php new file mode 100644 index 0000000..23cc557 --- /dev/null +++ b/diagnostics.php @@ -0,0 +1,87 @@ +[PASS] $success"; + } else { + echo "
  • [FAIL] $failure
  • "; + return false; + } + return true; +} + +?> + + + + + ChurchTube Diagnostics + + + + +
    +

    ChurchTube Diagnostics

    +
    +

    System Health Check

    +
      + ='), + "PHP Version: " . PHP_VERSION, + "PHP Version: " . PHP_VERSION . " (Requires 7.4+)"); + + // Extensions + check_status(extension_loaded('pdo_mysql'), "PDO MySQL extension is loaded.", "PDO MySQL extension is MISSING."); + check_status(extension_loaded('curl'), "CURL extension is loaded.", "CURL extension is MISSING (required for external link validation)."); + + // Config File + $has_config = file_exists('includes/config.php'); + if (check_status($has_config, "Configuration file exists.", "Configuration file (includes/config.php) is MISSING.")) { + require_once 'includes/config.php'; + // DB Connection + try { + $pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASS); + check_status(true, "Database connection established successfully.", ""); + } catch (Exception $e) { + check_status(false, "", "Database connection failed: " . $e->getMessage()); + } + } + + // Write Permissions + check_status(is_writable('uploads'), "Uploads directory is writable.", "Uploads directory is NOT writable. Run: chmod 777 uploads"); + + // PHP Settings + $upload_max = ini_get('upload_max_filesize'); + $post_max = ini_get('post_max_size'); + $is_low = (int)$upload_max < 100 || (int)$post_max < 100; + + if ($is_low) { + echo "
    • [WARN] Max Upload Size: $upload_max (Post Max: $post_max) - This is low for videos!
    • "; + } else { + echo "
    • [PASS] Max Upload Size: $upload_max (Post Max: $post_max)
    • "; + } + ?> +
    + +
    +

    Pro-Tips & Fixes:

    +
      +
    • MySQL Issues? Run: sudo systemctl status mysql. If stopped, run sudo systemctl start mysql.
    • +
    • Upload Limits? Edit /etc/php/7.4/apache2/php.ini. Look for upload_max_filesize and post_max_size. Set them to 500M or more, then run sudo systemctl restart apache2.
    • +
    • Permission Issues? Run: sudo chmod -R 777 uploads/ to ensure the web server can save video files.
    • +
    +
    +
    +
    + Back to Site +
    +
    + + diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..fc1bea7 --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,48 @@ + diff --git a/includes/db.php b/includes/db.php new file mode 100644 index 0000000..8ef75ea --- /dev/null +++ b/includes/db.php @@ -0,0 +1,11 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); +} catch (PDOException $e) { + die("Database Connection Error: " . $e->getMessage()); +} +?> diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..d398c36 --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,7 @@ + + + + diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..15c0e99 --- /dev/null +++ b/includes/header.php @@ -0,0 +1,70 @@ + + + + + + + <?= htmlspecialchars($site_title) ?> + + + + + +
    + + + + + +
    +
    diff --git a/includes/settings_helper.php b/includes/settings_helper.php new file mode 100644 index 0000000..7db6f72 --- /dev/null +++ b/includes/settings_helper.php @@ -0,0 +1,13 @@ +prepare("SELECT setting_value FROM settings WHERE setting_key = ?"); + $stmt->execute([$key]); + $res = $stmt->fetch(); + return $res ? $res['setting_value'] : $default; + } catch (Exception $e) { + return $default; + } +} +?> diff --git a/index.php b/index.php new file mode 100644 index 0000000..935c6ff --- /dev/null +++ b/index.php @@ -0,0 +1,71 @@ +prepare($query); +$stmt->execute($params); +$videos = $stmt->fetchAll(); + +// Get popular tags for chips +$popular_tags = $pdo->query("SELECT name FROM tags LIMIT 10")->fetchAll(PDO::FETCH_COLUMN); +?> + +
    + All + + + +
    + +
    + +
    + +

    No sermons found.

    + +

    Click here to add your first video.

    + +
    + + + +
    + +
    +
    +

    +
    + • + +
    +
    +
    + + +
    + + diff --git a/install.php b/install.php new file mode 100644 index 0000000..3c3445c --- /dev/null +++ b/install.php @@ -0,0 +1,251 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); + $pdo->exec("USE `$db_name`"); + + // Create tables + $sql = " + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + role ENUM('admin', 'moderator', 'editor', 'user') DEFAULT 'user', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS videos ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + release_date DATE, + source_type ENUM('upload', 'link') NOT NULL, + video_url TEXT NOT NULL, + thumbnail_url TEXT, + uploader_id INT, + views INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (uploader_id) REFERENCES users(id) ON DELETE SET NULL + ); + CREATE TABLE IF NOT EXISTS comments ( + id INT AUTO_INCREMENT PRIMARY KEY, + video_id INT NOT NULL, + user_id INT NOT NULL, + comment_text TEXT NOT NULL, + is_reported BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS reactions ( + id INT AUTO_INCREMENT PRIMARY KEY, + comment_id INT NOT NULL, + user_id INT NOT NULL, + reaction_type ENUM('thumb', 'heart', 'pray', 'insight', 'clap') NOT NULL, + UNIQUE KEY unique_reaction (comment_id, user_id, reaction_type), + FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS tags ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE + ); + CREATE TABLE IF NOT EXISTS video_tags ( + video_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (video_id, tag_id), + FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS settings ( + setting_key VARCHAR(50) PRIMARY KEY, + setting_value TEXT + ); + "; + $pdo->exec($sql); + + // Default settings + $pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES + ('site_title', 'ChurchTube'), + ('primary_color', '#7c4dff'), + ('secondary_color', '#ff4081'), + ('logo_url', ''), + ('footer_text', '© 2024 ChurchTube. All rights reserved.')"); + + // Save config temporarily in session + session_start(); + $_SESSION['db_config'] = [ + 'host' => $db_host, + 'name' => $db_name, + 'user' => $db_user, + 'pass' => $db_pass + ]; + + header('Location: install.php?step=3'); + exit; + } catch (PDOException $e) { + $msg = $e->getMessage(); + $hint = ""; + if (strpos($msg, 'Access denied') !== false) { + $hint = "Hint: Check your username and password. On Ubuntu, 'root' often requires a password or 'sudo' access. Try creating a dedicated database user."; + } elseif (strpos($msg, 'Connection refused') !== false || strpos($msg, 'Can\'t connect') !== false) { + $hint = "Hint: MySQL might not be running. Try: sudo systemctl start mysql"; + } elseif (strpos($msg, 'getaddrinfo failed') !== false) { + $hint = "Hint: The host '$db_host' could not be found. If MySQL is local, use 'localhost' or '127.0.0.1'."; + } + $error = "Database Connection Failed: " . $msg . "

    " . $hint; + } + } elseif ($step === 3) { + // Admin Setup + session_start(); + $db = $_SESSION['db_config']; + $admin_user = $_POST['admin_user']; + $admin_email = $_POST['admin_email']; + $admin_pass = password_hash($_POST['admin_pass'], PASSWORD_DEFAULT); + + try { + $pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']}", $db['user'], $db['pass']); + $stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, 'admin')"); + $stmt->execute([$admin_user, $admin_email, $admin_pass]); + + // Generate config file + $config_content = ""; + if (!file_exists('includes')) mkdir('includes'); + file_put_contents($config_file, $config_content); + + session_destroy(); + header('Location: install.php?step=4'); + exit; + } catch (PDOException $e) { + $error = "Admin Creation Failed: " . $e->getMessage(); + } + } +} +?> + + + + + + ChurchTube | Installation + + + +
    +

    ChurchTube

    + + +
    + +
    + + + +
    +

    Welcome to ChurchTube

    +

    This wizard will help you set up your sermon video platform in just a few minutes.

    +
    + System Check: +
      +
    • =') ? '✅' : '❌' ?> PHP 7.4+ (Current: )
    • +
    • PDO MySQL Extension
    • +
    • Directory Permissions
    • + + +
    • Upload Limits: / + +
      + Recommendation: Your upload limit is low for videos. + Edit /etc/php/7.4/apache2/php.ini and set: +
      upload_max_filesize = 500M
      post_max_size = 500M
      +
      + +
    • +
    +
    + Get Started +
    + +
    +

    Database Configuration

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    + +
    +

    Create Admin Account

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    + +
    +

    Installation Successful!

    +

    Your ChurchTube instance is now ready to use.

    +
    🎉
    + Go to Homepage +
    + +
    + + diff --git a/login.php b/login.php new file mode 100644 index 0000000..31514bf --- /dev/null +++ b/login.php @@ -0,0 +1,61 @@ +prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['user_role'] = $user['role']; + header('Location: index.php'); + exit; + } else { + $error = "Invalid username or password."; + } +} +?> + + + + + Login | ChurchTube + + + +
    + +

    Login

    + +
    + +
    +
    + + +
    +
    + + +
    + +
    +

    + Don't have an account? Register +

    +
    + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..e5ab4b6 --- /dev/null +++ b/logout.php @@ -0,0 +1,6 @@ + diff --git a/register.php b/register.php new file mode 100644 index 0000000..8a2dea6 --- /dev/null +++ b/register.php @@ -0,0 +1,76 @@ +prepare("SELECT id FROM users WHERE username = ? OR email = ?"); + $stmt->execute([$username, $email]); + if ($stmt->fetch()) { + $error = "Username or Email already exists."; + } else { + $hash = password_hash($password, PASSWORD_DEFAULT); + $stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, 'user')"); + if ($stmt->execute([$username, $email, $hash])) { + header('Location: login.php?registered=1'); + exit; + } else { + $error = "Registration failed. Please try again."; + } + } + } +} +?> + + + + + Register | ChurchTube + + + +
    + +

    Create Account

    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +

    + Already have an account? Login +

    +
    + + diff --git a/watch.php b/watch.php new file mode 100644 index 0000000..360abb4 --- /dev/null +++ b/watch.php @@ -0,0 +1,381 @@ +prepare("SELECT v.*, u.username as uploader FROM videos v JOIN users u ON v.uploader_id = u.id WHERE v.id = ?"); +$stmt->execute([$video_id]); +$video = $stmt->fetch(); + +if (!$video) { + header('Location: index.php'); + exit; +} + +// Get Recommendations (videos with same tags) +$stmt = $pdo->prepare(" + SELECT DISTINCT v.* FROM videos v + JOIN video_tags vt ON v.id = vt.video_id + WHERE vt.tag_id IN (SELECT tag_id FROM video_tags WHERE video_id = ?) + AND v.id != ? + LIMIT 4 +"); +$stmt->execute([$video_id, $video_id]); +$recommendations = $stmt->fetchAll(); + +if (empty($recommendations)) { + $stmt = $pdo->prepare("SELECT * FROM videos WHERE id != ? ORDER BY created_at DESC LIMIT 4"); + $stmt->execute([$video_id]); + $recommendations = $stmt->fetchAll(); +} + +// Fetch comments with reaction counts +$stmt = $pdo->prepare(" + SELECT c.*, u.username, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'thumb') as thumbs, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'heart') as hearts, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'pray') as prays, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'insight') as insights, + (SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'clap') as claps + FROM comments c JOIN users u ON c.user_id = u.id + WHERE c.video_id = ? ORDER BY c.created_at DESC +"); +$stmt->execute([$video_id]); +$comments = $stmt->fetchAll(); + +require_once 'includes/header.php'; +?> + +
    + + + + +
    + +
    + + + + +
    +
    + +
    +
    + + + + + +
    + + +
    +
    +

    + + Edit + +
    +
    +
    + views • +
    +
    + + + + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +

    Comments

    + +
    + + + +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    +
    + + +
    + + + + + + + REPORT + + + + +
    +
    +
    + +
    +
    +
    + + +
    +

    Recommended

    + + +
    +
    +
    + +
    +
    + views +
    +
    +
    + +
    + +
    + + + +