Initial commit of ChurchTube v2.7
This commit is contained in:
commit
e7c9ea5386
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## ✨ 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.*
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
requireEditor();
|
||||
|
||||
$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'];
|
||||
$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)) {
|
||||
$thumbnail_url = 'uploads/' . $t_filename;
|
||||
}
|
||||
} else {
|
||||
$error = "Invalid thumbnail format. Use JPG, PNG or WebP.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error && $source_type === 'upload') {
|
||||
if (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)) {
|
||||
$video_url = 'uploads/' . $filename;
|
||||
} else {
|
||||
$error = "Failed to move uploaded file.";
|
||||
}
|
||||
} else {
|
||||
$error = "Invalid video format. Use MP4, WebM or OGG.";
|
||||
}
|
||||
} else {
|
||||
$error = "Please select a valid video file.";
|
||||
}
|
||||
} elseif (!$error) {
|
||||
$video_url = formatVideoUrl(trim($_POST['external_url']));
|
||||
if (empty($video_url)) {
|
||||
$error = "Please provide an external video URL.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$stmt = $pdo->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);
|
||||
?>
|
||||
|
||||
<div style="max-width: 800px; margin: 40px auto; padding: 0 24px;">
|
||||
<div style="background: var(--bg-card); padding: 32px; border-radius: 16px; border: 1px solid var(--glass-border);">
|
||||
<h2 style="margin-bottom: 24px;"><i class="fas fa-upload" style="margin-right: 12px;"></i> Add New Sermon</h2>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div style="background: rgba(255,64,129,0.1); color: #ff4081; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid rgba(255,64,129,0.2);">
|
||||
<?= $error ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div style="background: rgba(76,175,80,0.1); color: #4caf50; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid rgba(76,175,80,0.2);">
|
||||
<?= $success ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Video Title</label>
|
||||
<input type="text" name="title" class="form-control" placeholder="e.g. The Path to Salvation" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Release Date</label>
|
||||
<input type="date" name="release_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control" style="min-height: 120px;" placeholder="What is this sermon about?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags (comma separated)</label>
|
||||
<input type="text" name="tags" class="form-control" placeholder="God, Love, Salvation">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Video Source</label>
|
||||
<div style="display: flex; gap: 20px; margin-top: 8px;">
|
||||
<label style="cursor: pointer;">
|
||||
<input type="radio" name="source_type" value="upload" checked onclick="toggleSource('upload')"> Upload File
|
||||
</label>
|
||||
<label style="cursor: pointer;">
|
||||
<input type="radio" name="source_type" value="link" onclick="toggleSource('link')"> External Link (NAS/Cloud)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="source_upload" class="form-group">
|
||||
<label class="form-label">Select Video File</label>
|
||||
<input type="file" name="video_file" class="form-control" accept="video/*">
|
||||
<small style="color: var(--text-muted);">Max size: <?= ini_get('upload_max_filesize') ?></small>
|
||||
</div>
|
||||
|
||||
<div id="source_link" class="form-group" style="display: none;">
|
||||
<label class="form-label">External Video URL</label>
|
||||
<input type="url" name="external_url" class="form-control" placeholder="https://nas.example.com/videos/sermon.mp4">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Upload Thumbnail</label>
|
||||
<input type="file" name="thumbnail_file" class="form-control" accept="image/*">
|
||||
<small style="color: var(--text-muted);">Recommended: 1280x720 (16:9)</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 32px; display: flex; gap: 16px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex-grow: 1;">Publish Sermon</button>
|
||||
<a href="index.php" class="btn" style="background: var(--glass);">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSource(type) {
|
||||
document.getElementById('source_upload').style.display = (type === 'upload' ? 'block' : 'none');
|
||||
document.getElementById('source_link').style.display = (type === 'link' ? 'block' : 'none');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once '../includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
requireEditor();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$stmt = $pdo->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);
|
||||
?>
|
||||
|
||||
<div style="max-width: 800px; margin: 40px auto; padding: 0 24px;">
|
||||
<div style="background: var(--bg-card); padding: 32px; border-radius: 16px; border: 1px solid var(--glass-border);">
|
||||
<h2 style="margin-bottom: 24px;">Edit Sermon: <?= htmlspecialchars($video['title']) ?></h2>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div style="background: rgba(76,175,80,0.1); color: #4caf50; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid rgba(76,175,80,0.2);">
|
||||
<?= $success ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Video Title</label>
|
||||
<input type="text" name="title" class="form-control" value="<?= htmlspecialchars($video['title']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Release Date</label>
|
||||
<input type="date" name="release_date" class="form-control" value="<?= $video['release_date'] ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control" style="min-height: 120px;"><?= htmlspecialchars($video['description']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags (comma separated)</label>
|
||||
<input type="text" name="tags" class="form-control" value="<?= htmlspecialchars($tags_str) ?>" placeholder="God, Love, Salvation">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Video Source</label>
|
||||
<div style="display: flex; gap: 20px; margin-top: 8px;">
|
||||
<label style="cursor: pointer;">
|
||||
<input type="radio" name="source_type" value="upload" <?= $video['source_type'] === 'upload' ? 'checked' : '' ?> onclick="toggleSource('upload')"> Upload File
|
||||
</label>
|
||||
<label style="cursor: pointer;">
|
||||
<input type="radio" name="source_type" value="link" <?= $video['source_type'] === 'link' ? 'checked' : '' ?> onclick="toggleSource('link')"> External Link
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="source_upload" class="form-group" style="display: <?= $video['source_type'] === 'upload' ? 'block' : 'none' ?>;">
|
||||
<label class="form-label">Replace Video File (Optional)</label>
|
||||
<input type="file" name="video_file" class="form-control" accept="video/*">
|
||||
<small style="color: var(--text-muted);">Current: <?= htmlspecialchars($video['video_url']) ?></small>
|
||||
</div>
|
||||
|
||||
<div id="source_link" class="form-group" style="display: <?= $video['source_type'] === 'link' ? 'block' : 'none' ?>;">
|
||||
<label class="form-label">External Video URL</label>
|
||||
<input type="url" name="external_url" class="form-control" value="<?= $video['source_type'] === 'link' ? htmlspecialchars($video['video_url']) : '' ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Thumbnail</label>
|
||||
<?php if ($video['thumbnail_url']): ?>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<img src="../<?= htmlspecialchars($video['thumbnail_url']) ?>" style="width: 120px; border-radius: 8px;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="thumbnail_file" class="form-control" accept="image/*">
|
||||
<small style="color: var(--text-muted);">Leave empty to keep current thumbnail.</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 32px; display: flex; gap: 16px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex-grow: 1;">Update Sermon</button>
|
||||
<a href="index.php" class="btn" style="background: var(--glass);">Back to Dashboard</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSource(type) {
|
||||
document.getElementById('source_upload').style.display = (type === 'upload' ? 'block' : 'none');
|
||||
document.getElementById('source_link').style.display = (type === 'link' ? 'block' : 'none');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once '../includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
requireEditor();
|
||||
|
||||
// Handle deletion
|
||||
if (isset($_GET['delete'])) {
|
||||
$id = (int)$_GET['delete'];
|
||||
// Optional: Delete physical file if it was an upload
|
||||
$stmt = $pdo->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);
|
||||
?>
|
||||
|
||||
<div style="padding: 40px 24px; max-width: 1200px; margin: 0 auto;">
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px;">
|
||||
<a href="settings.php" class="btn" style="background: var(--bg-card); border: 1px solid var(--glass-border); flex-direction: column; padding: 20px;">
|
||||
<i class="fas fa-palette" style="font-size: 1.5rem; margin-bottom: 10px;"></i>
|
||||
Branding & Site
|
||||
</a>
|
||||
<a href="users.php" class="btn" style="background: var(--bg-card); border: 1px solid var(--glass-border); flex-direction: column; padding: 20px;">
|
||||
<i class="fas fa-users" style="font-size: 1.5rem; margin-bottom: 10px;"></i>
|
||||
Manage Users
|
||||
</a>
|
||||
<a href="reports.php" class="btn" style="background: var(--bg-card); border: 1px solid var(--glass-border); flex-direction: column; padding: 20px; position: relative; display: flex; text-decoration: none; color: inherit;">
|
||||
<i class="fas fa-flag" style="font-size: 1.5rem; margin-bottom: 10px; color: <?= $reported_count > 0 ? '#ff4081' : 'inherit' ?>;"></i>
|
||||
<span>Reports (<?= $reported_count ?>)</span>
|
||||
<?php if ($reported_count > 0): ?>
|
||||
<span style="position: absolute; top: -5px; right: -5px; background: #ff4081; width: 20px; height: 20px; border-radius: 50%; font-size: 0.7rem; display: flex; align-items: center; justify-content: center; color: white;">!</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
|
||||
<h1>Manage Sermons</h1>
|
||||
<a href="add_video.php" class="btn btn-primary"><i class="fas fa-plus"></i> Add New Video</a>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--bg-card); border-radius: 16px; border: 1px solid var(--glass-border); overflow: hidden;">
|
||||
<table style="width: 100%; border-collapse: collapse; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: var(--glass); color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px;">
|
||||
<th style="padding: 16px;">Video</th>
|
||||
<th style="padding: 16px;">Uploader</th>
|
||||
<th style="padding: 16px;">Date</th>
|
||||
<th style="padding: 16px;">Views</th>
|
||||
<th style="padding: 16px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($videos as $v): ?>
|
||||
<tr style="border-bottom: 1px solid var(--glass-border);">
|
||||
<td style="padding: 16px;">
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<div style="width: 80px; aspect-ratio: 16/9; background: #333; border-radius: 4px; background-image: url('../<?= $v['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>'); background-size: cover;"></div>
|
||||
<span style="font-weight: 500;"><?= htmlspecialchars($v['title']) ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding: 16px; color: var(--text-muted);"><?= htmlspecialchars($v['username']) ?></td>
|
||||
<td style="padding: 16px; color: var(--text-muted);"><?= date('Y-m-d', strtotime($v['release_date'])) ?></td>
|
||||
<td style="padding: 16px;"><?= number_format($v['views']) ?></td>
|
||||
<td style="padding: 16px;">
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<a href="../watch.php?id=<?= $v['id'] ?>" title="View" style="color: var(--primary-color);"><i class="fas fa-eye"></i></a>
|
||||
<a href="edit_video.php?id=<?= $v['id'] ?>" title="Edit" style="color: var(--accent);"><i class="fas fa-edit"></i></a>
|
||||
<a href="?delete=<?= $v['id'] ?>" title="Delete" style="color: #ff4081;" onclick="return confirm('Are you sure?')"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($videos)): ?>
|
||||
<tr>
|
||||
<td colspan="5" style="padding: 40px; text-align: center; color: var(--text-muted);">No videos uploaded yet.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once '../includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
requireModerator();
|
||||
|
||||
// Handle moderation actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$comment_id = (int)$_POST['comment_id'];
|
||||
if (isset($_POST['delete'])) {
|
||||
$pdo->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);
|
||||
?>
|
||||
|
||||
<div style="max-width: 1000px; margin: 40px auto; padding: 0 24px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
|
||||
<h1>Reported Comments</h1>
|
||||
<a href="index.php" class="btn" style="background: var(--glass);">Back to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<?php if (isset($msg)): ?>
|
||||
<div style="background: rgba(76,175,80,0.1); color: #4caf50; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid rgba(76,175,80,0.2);">
|
||||
<?= $msg ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="background: var(--bg-card); border-radius: 16px; border: 1px solid var(--glass-border); overflow: hidden;">
|
||||
<table style="width: 100%; border-collapse: collapse; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: var(--glass); color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase;">
|
||||
<th style="padding: 16px;">Comment</th>
|
||||
<th style="padding: 16px;">User</th>
|
||||
<th style="padding: 16px;">Video</th>
|
||||
<th style="padding: 16px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($reports as $r): ?>
|
||||
<tr style="border-bottom: 1px solid var(--glass-border);">
|
||||
<td style="padding: 16px; max-width: 300px;">
|
||||
<div style="font-style: italic; color: var(--text-main);">"<?= htmlspecialchars($r['comment_text']) ?>"</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;"><?= date('M d, Y', strtotime($r['created_at'])) ?></div>
|
||||
</td>
|
||||
<td style="padding: 16px;"><?= htmlspecialchars($r['username']) ?></td>
|
||||
<td style="padding: 16px; font-size: 0.9rem; color: var(--text-muted);"><?= htmlspecialchars($r['video_title']) ?></td>
|
||||
<td style="padding: 16px;">
|
||||
<form method="POST" style="display: flex; gap: 10px;">
|
||||
<input type="hidden" name="comment_id" value="<?= $r['id'] ?>">
|
||||
<button type="submit" name="delete" class="btn" style="background: #ff4081; padding: 6px 12px; font-size: 0.8rem;">Delete</button>
|
||||
<button type="submit" name="ignore" class="btn" style="background: var(--glass); padding: 6px 12px; font-size: 0.8rem;">Ignore</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($reports)): ?>
|
||||
<tr>
|
||||
<td colspan="4" style="padding: 40px; text-align: center; color: var(--text-muted);">No reported comments. Everything is clean!</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once '../includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
requireAdmin();
|
||||
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['restore_defaults'])) {
|
||||
$defaults = [
|
||||
'site_title' => '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);
|
||||
?>
|
||||
|
||||
<div style="max-width: 800px; margin: 40px auto; padding: 0 24px;">
|
||||
<div style="background: var(--bg-card); padding: 32px; border-radius: 16px; border: 1px solid var(--glass-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
||||
<h2>Site Settings & Branding</h2>
|
||||
<form method="POST" onsubmit="return confirm('Restore all settings to default?')">
|
||||
<button type="submit" name="restore_defaults" class="btn" style="background: rgba(255,255,255,0.1); font-size: 0.8rem;">Restore Defaults</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div style="background: rgba(76,175,80,0.1); color: #4caf50; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid rgba(76,175,80,0.2);">
|
||||
<?= $success ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Site Title</label>
|
||||
<input type="text" name="site_title" class="form-control" value="<?= htmlspecialchars($settings['site_title'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Primary Color</label>
|
||||
<input type="color" name="primary_color" class="form-control" style="height: 50px; padding: 5px;" value="<?= htmlspecialchars($settings['primary_color'] ?? '#7c4dff') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Secondary Color</label>
|
||||
<input type="color" name="secondary_color" class="form-control" style="height: 50px; padding: 5px;" value="<?= htmlspecialchars($settings['secondary_color'] ?? '#ff4081') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Logo URL</label>
|
||||
<input type="url" name="logo_url" class="form-control" value="<?= htmlspecialchars($settings['logo_url'] ?? '') ?>" placeholder="https://example.com/logo.png">
|
||||
<small style="color: var(--text-muted);">Leave empty to use text title as logo.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Footer Text</label>
|
||||
<textarea name="footer_text" class="form-control" style="min-height: 80px;"><?= htmlspecialchars($settings['footer_text'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 32px;">
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Save Branding</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once '../includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
requireAdmin();
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
$edit_user = null;
|
||||
|
||||
// Handle Edit Mode
|
||||
if (isset($_GET['edit'])) {
|
||||
$edit_id = (int)$_GET['edit'];
|
||||
$stmt = $pdo->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);
|
||||
?>
|
||||
|
||||
<div style="max-width: 1000px; margin: 40px auto; padding: 0 24px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 350px; gap: 32px;">
|
||||
|
||||
<!-- User List -->
|
||||
<div style="background: var(--bg-card); padding: 24px; border-radius: 16px; border: 1px solid var(--glass-border);">
|
||||
<h3>System Users</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
|
||||
<thead>
|
||||
<tr style="text-align: left; color: var(--text-muted); font-size: 0.85rem; border-bottom: 1px solid var(--glass-border);">
|
||||
<th style="padding: 12px 8px;">Username</th>
|
||||
<th style="padding: 12px 8px;">Role</th>
|
||||
<th style="padding: 12px 8px;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<tr style="border-bottom: 1px solid var(--glass-border);">
|
||||
<td style="padding: 12px 8px;">
|
||||
<div><?= htmlspecialchars($u['username']) ?></div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);"><?= htmlspecialchars($u['email']) ?></div>
|
||||
</td>
|
||||
<td style="padding: 12px 8px;">
|
||||
<span style="padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; background: var(--glass);">
|
||||
<?= strtoupper($u['role']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding: 12px 8px;">
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<a href="?edit=<?= $u['id'] ?>" style="color: var(--primary-color);"><i class="fas fa-edit"></i></a>
|
||||
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete user?')">
|
||||
<input type="hidden" name="user_id" value="<?= $u['id'] ?>">
|
||||
<button type="submit" name="delete_user" style="background:none; border:none; color:#ff4081; cursor:pointer; padding:0;">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Form Section -->
|
||||
<div style="background: var(--bg-card); padding: 24px; border-radius: 16px; border: 1px solid var(--glass-border); height: fit-content;">
|
||||
<h3><?= $edit_user ? 'Edit User' : 'Add New User' ?></h3>
|
||||
<?php if ($success): ?> <div style="color:#4caf50; font-size:0.9rem; margin:10px 0;"><?= $success ?></div> <?php endif; ?>
|
||||
<?php if ($error): ?> <div style="color:#ff4081; font-size:0.9rem; margin:10px 0;"><?= $error ?></div> <?php endif; ?>
|
||||
|
||||
<form method="POST" style="margin-top: 20px;">
|
||||
<?php if ($edit_user): ?>
|
||||
<input type="hidden" name="update_user" value="1">
|
||||
<input type="hidden" name="user_id" value="<?= $edit_user['id'] ?>">
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="add_user" value="1">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" value="<?= $edit_user ? htmlspecialchars($edit_user['username']) : '' ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="<?= $edit_user ? htmlspecialchars($edit_user['email']) : '' ?>" required>
|
||||
</div>
|
||||
|
||||
<?php if (!$edit_user): ?>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-control">
|
||||
<option value="user" <?= ($edit_user && $edit_user['role'] == 'user') ? 'selected' : '' ?>>User (Commenter)</option>
|
||||
<option value="moderator" <?= ($edit_user && $edit_user['role'] == 'moderator') ? 'selected' : '' ?>>Moderator (Comments/Reports)</option>
|
||||
<option value="editor" <?= ($edit_user && $edit_user['role'] == 'editor') ? 'selected' : '' ?>>Editor (Videos Only)</option>
|
||||
<option value="admin" <?= ($edit_user && $edit_user['role'] == 'admin') ? 'selected' : '' ?>>Admin (Full Access)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 10px;">
|
||||
<?= $edit_user ? 'Update User' : 'Create User' ?>
|
||||
</button>
|
||||
|
||||
<?php if ($edit_user): ?>
|
||||
<a href="users.php" class="btn" style="width: 100%; margin-top: 10px; background: var(--glass); display: block; text-align: center; text-decoration: none; color: white;">Cancel</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once '../includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isModerator()) {
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$video_id = (int)($_GET['video_id'] ?? 0);
|
||||
|
||||
if (!$video_id) {
|
||||
echo json_encode([]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$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();
|
||||
|
||||
// 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([]);
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$video_id = (int)($_POST['video_id'] ?? 0);
|
||||
|
||||
if (!$video_id) {
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
echo json_encode(['success' => 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()]);
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
require_once '../includes/db.php';
|
||||
require_once '../includes/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
?>
|
||||
|
|
@ -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); }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 625 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
// ChurchTube Diagnostics Script
|
||||
|
||||
function check_status($condition, $success, $failure) {
|
||||
if ($condition) {
|
||||
echo "<li style='color: #4CAF50;'>[PASS] $success</li>";
|
||||
} else {
|
||||
echo "<li style='color: #f44336;'>[FAIL] $failure</li>";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ChurchTube Diagnostics</title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
<style>
|
||||
.diag-box { background: #1e1e1e; padding: 30px; border-radius: 12px; margin-top: 50px; border: 1px solid #333; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { padding: 12px; border-bottom: 1px solid #333; font-family: monospace; }
|
||||
.advice { background: rgba(124, 77, 255, 0.1); padding: 15px; border-radius: 8px; margin-top: 20px; border-left: 4px solid var(--primary-color); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="centered-container" style="max-width: 800px;">
|
||||
<h1 class="logo">ChurchTube Diagnostics</h1>
|
||||
<div class="diag-box">
|
||||
<h3>System Health Check</h3>
|
||||
<ul>
|
||||
<?php
|
||||
// PHP Version
|
||||
check_status(version_compare(PHP_VERSION, '7.4.0', '>='),
|
||||
"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 "<li style='color: #ffab40;'>[WARN] Max Upload Size: $upload_max (Post Max: $post_max) - This is low for videos!</li>";
|
||||
} else {
|
||||
echo "<li style='color: #4CAF50;'>[PASS] Max Upload Size: $upload_max (Post Max: $post_max)</li>";
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
|
||||
<div class="advice">
|
||||
<h4>Pro-Tips & Fixes:</h4>
|
||||
<ul style="border:none; margin-top: 10px;">
|
||||
<li><strong>MySQL Issues?</strong> Run: <code>sudo systemctl status mysql</code>. If stopped, run <code>sudo systemctl start mysql</code>.</li>
|
||||
<li><strong>Upload Limits?</strong> Edit <code>/etc/php/7.4/apache2/php.ini</code>. Look for <code>upload_max_filesize</code> and <code>post_max_size</code>. Set them to <code>500M</code> or more, then run <code>sudo systemctl restart apache2</code>.</li>
|
||||
<li><strong>Permission Issues?</strong> Run: <code>sudo chmod -R 777 uploads/</code> to ensure the web server can save video files.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<a href="index.php" class="btn btn-primary">Back to Site</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
session_start();
|
||||
|
||||
function isLoggedIn() {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin';
|
||||
}
|
||||
|
||||
function isModerator() {
|
||||
return isset($_SESSION['user_role']) && ($_SESSION['user_role'] === 'admin' || $_SESSION['user_role'] === 'moderator');
|
||||
}
|
||||
|
||||
function isEditor() {
|
||||
return isset($_SESSION['user_role']) && ($_SESSION['user_role'] === 'admin' || $_SESSION['user_role'] === 'editor');
|
||||
}
|
||||
|
||||
function requireLogin() {
|
||||
if (!isLoggedIn()) {
|
||||
$prefix = file_exists('includes/auth.php') ? '' : '../';
|
||||
header('Location: ' . $prefix . 'login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin() {
|
||||
requireLogin();
|
||||
if (!isAdmin()) {
|
||||
die("Unauthorized access. Admin privileges required.");
|
||||
}
|
||||
}
|
||||
|
||||
function requireModerator() {
|
||||
requireLogin();
|
||||
if (!isModerator()) {
|
||||
die("Unauthorized access. Moderator privileges required.");
|
||||
}
|
||||
}
|
||||
|
||||
function requireEditor() {
|
||||
requireLogin();
|
||||
if (!isEditor()) {
|
||||
die("Unauthorized access. Editor or Admin privileges required.");
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
|
||||
$pdo->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());
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
</main>
|
||||
<footer style="padding: 40px 24px; border-top: 1px solid var(--glass-border); margin-top: 60px; text-align: center; color: var(--text-muted);">
|
||||
<p><?= get_setting('footer_text', '© ' . date('Y') . ' ChurchTube. All rights reserved.') ?></p>
|
||||
<p style="font-size: 0.8rem; margin-top: 8px;">Built for the glory of God.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/settings_helper.php';
|
||||
|
||||
$site_title = get_setting('site_title', 'ChurchTube');
|
||||
$primary_color = get_setting('primary_color', '#7c4dff');
|
||||
$secondary_color = get_setting('secondary_color', '#ff4081');
|
||||
$logo_url = get_setting('logo_url', '');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($site_title) ?></title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: <?= $primary_color ?>;
|
||||
--secondary-color: <?= $secondary_color ?>;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.logo {
|
||||
font-size: 1.2rem !important;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.search-bar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="index.php" class="logo">
|
||||
<?php if ($logo_url): ?>
|
||||
<img src="<?= htmlspecialchars($logo_url) ?>" alt="Logo" style="height: 40px;">
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($site_title) ?>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
|
||||
<form class="search-bar" action="index.php" method="GET">
|
||||
<input type="text" name="q" placeholder="Search sermons..." value="<?= htmlspecialchars($_GET['q'] ?? '') ?>">
|
||||
<button type="submit" style="background:none; border:none; color:white; cursor:pointer; padding: 0 10px;">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="user-actions" style="display: flex; gap: 16px; align-items: center;">
|
||||
<?php if (isEditor()): ?>
|
||||
<a href="admin/index.php" class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9rem;">
|
||||
<i class="fas fa-plus"></i> Admin
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isLoggedIn()): ?>
|
||||
<span style="color: var(--text-muted);">Hi, <?= htmlspecialchars($_SESSION['username']) ?></span>
|
||||
<a href="logout.php" title="Logout"><i class="fas fa-sign-out-alt"></i></a>
|
||||
<?php else: ?>
|
||||
<a href="login.php" class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9rem;">Login</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
<main style="min-height: calc(100vh - 64px);">
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
function get_setting($key, $default = '') {
|
||||
global $pdo;
|
||||
try {
|
||||
$stmt = $pdo->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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
if (!file_exists('includes/config.php')) {
|
||||
header('Location: install.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once 'includes/db.php';
|
||||
require_once 'includes/settings_helper.php';
|
||||
require_once 'includes/header.php';
|
||||
|
||||
$search = $_GET['q'] ?? '';
|
||||
$tag_filter = $_GET['tag'] ?? '';
|
||||
|
||||
$query = "SELECT DISTINCT v.*, u.username as uploader FROM videos v
|
||||
JOIN users u ON v.uploader_id = u.id
|
||||
LEFT JOIN video_tags vt ON v.id = vt.video_id
|
||||
LEFT JOIN tags t ON vt.tag_id = t.id";
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$query .= " WHERE (v.title LIKE ? OR v.description LIKE ? OR t.name LIKE ?)";
|
||||
$params = ["%$search%", "%$search%", "%$search%"];
|
||||
} elseif ($tag_filter) {
|
||||
$query .= " WHERE t.name = ?";
|
||||
$params = [$tag_filter];
|
||||
}
|
||||
|
||||
$query .= " ORDER BY v.release_date DESC, v.created_at DESC";
|
||||
$stmt = $pdo->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);
|
||||
?>
|
||||
|
||||
<div style="padding: 12px 24px; display: flex; gap: 12px; overflow-x: auto; border-bottom: 1px solid var(--glass-border); margin-bottom: 20px;">
|
||||
<a href="index.php" class="btn" style="background: <?= !$tag_filter ? 'var(--primary-color)' : 'var(--glass)' ?>; border-radius: 20px; font-size: 0.85rem; padding: 6px 16px;">All</a>
|
||||
<?php foreach ($popular_tags as $ptag): ?>
|
||||
<a href="index.php?tag=<?= urlencode($ptag) ?>" class="btn" style="background: <?= $tag_filter === $ptag ? 'var(--primary-color)' : 'var(--glass)' ?>; border-radius: 20px; font-size: 0.85rem; padding: 6px 16px;"><?= htmlspecialchars($ptag) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="video-grid">
|
||||
<?php if (empty($videos)): ?>
|
||||
<div style="grid-column: 1/-1; text-align: center; padding: 100px; color: var(--text-muted);">
|
||||
<i class="fas fa-video-slash" style="font-size: 3rem; margin-bottom: 20px; display: block;"></i>
|
||||
<h3>No sermons found.</h3>
|
||||
<?php if (isEditor()): ?>
|
||||
<p>Click <a href="admin/add_video.php" style="color: var(--primary-color);">here</a> to add your first video.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($videos as $video): ?>
|
||||
<a href="watch.php?id=<?= $video['id'] ?>" class="video-card">
|
||||
<div class="video-thumbnail" style="background-image: url('<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>');">
|
||||
<!-- Placeholder thumbnail if none exists -->
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<h3 class="video-title"><?= htmlspecialchars($video['title']) ?></h3>
|
||||
<div class="video-meta">
|
||||
<span><?= htmlspecialchars($video['uploader']) ?></span> •
|
||||
<span><?= date('M d, Y', strtotime($video['release_date'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
<?php
|
||||
$config_file = 'includes/config.php';
|
||||
|
||||
if (file_exists($config_file)) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($step === 2) {
|
||||
// DB Setup
|
||||
$db_host = $_POST['db_host'];
|
||||
$db_name = $_POST['db_name'];
|
||||
$db_user = $_POST['db_user'];
|
||||
$db_pass = $_POST['db_pass'];
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host", $db_user, $db_pass);
|
||||
$pdo->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 = "<strong>Hint:</strong> 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 = "<strong>Hint:</strong> MySQL might not be running. Try: <code>sudo systemctl start mysql</code>";
|
||||
} elseif (strpos($msg, 'getaddrinfo failed') !== false) {
|
||||
$hint = "<strong>Hint:</strong> The host '$db_host' could not be found. If MySQL is local, use 'localhost' or '127.0.0.1'.";
|
||||
}
|
||||
$error = "Database Connection Failed: " . $msg . "<br><br>" . $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 = "<?php
|
||||
define('DB_HOST', '{$db['host']}');
|
||||
define('DB_NAME', '{$db['name']}');
|
||||
define('DB_USER', '{$db['user']}');
|
||||
define('DB_PASS', '{$db['pass']}');
|
||||
define('SITE_NAME', 'ChurchTube');
|
||||
?>";
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ChurchTube | Installation</title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="centered-container">
|
||||
<h1 class="logo" style="text-align: center; margin-bottom: 24px;">ChurchTube</h1>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div style="background: rgba(255,0,0,0.2); padding: 12px; border-radius: 8px; margin-bottom: 20px; border: 1px solid rgba(255,0,0,0.3);">
|
||||
<?= $error ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($step === 1): ?>
|
||||
<div class="wizard-step active">
|
||||
<h2>Welcome to ChurchTube</h2>
|
||||
<p style="margin: 16px 0; color: var(--text-muted);">This wizard will help you set up your sermon video platform in just a few minutes.</p>
|
||||
<div style="background: var(--glass); padding: 16px; border-radius: 8px; margin-bottom: 24px;">
|
||||
<strong>System Check:</strong>
|
||||
<ul style="margin-top: 8px; list-style: none;">
|
||||
<li><?= version_compare(PHP_VERSION, '7.4.0', '>=') ? '✅' : '❌' ?> PHP 7.4+ (Current: <?= phpversion() ?>)</li>
|
||||
<li><?= extension_loaded('pdo_mysql') ? '✅' : '❌' ?> PDO MySQL Extension</li>
|
||||
<li><?= is_writable('.') ? '✅' : '❌' ?> Directory Permissions</li>
|
||||
|
||||
<?php
|
||||
$u_max = ini_get('upload_max_filesize');
|
||||
$p_max = ini_get('post_max_size');
|
||||
$is_low = (int)$u_max < 100 || (int)$p_max < 100;
|
||||
?>
|
||||
<li><?= $is_low ? '⚠️' : '✅' ?> Upload Limits: <?= $u_max ?> / <?= $p_max ?>
|
||||
<?php if ($is_low): ?>
|
||||
<div style="font-size: 0.8rem; color: #ffab40; margin-top: 4px;">
|
||||
<strong>Recommendation:</strong> Your upload limit is low for videos.
|
||||
Edit <code>/etc/php/7.4/apache2/php.ini</code> and set:
|
||||
<pre style="margin-top: 4px; font-size: 0.75rem;">upload_max_filesize = 500M post_max_size = 500M</pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="?step=2" class="btn btn-primary" style="width: 100%;">Get Started</a>
|
||||
</div>
|
||||
<?php elseif ($step === 2): ?>
|
||||
<div class="wizard-step active">
|
||||
<h2>Database Configuration</h2>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Database Host</label>
|
||||
<input type="text" name="db_host" class="form-control" value="localhost" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Database Name</label>
|
||||
<input type="text" name="db_name" class="form-control" value="churchtube_db" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="db_user" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="db_pass" class="form-control">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Connect & Initialize</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php elseif ($step === 3): ?>
|
||||
<div class="wizard-step active">
|
||||
<h2>Create Admin Account</h2>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Admin Username</label>
|
||||
<input type="text" name="admin_user" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email Address</label>
|
||||
<input type="email" name="admin_email" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="admin_pass" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Complete Setup</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php elseif ($step === 4): ?>
|
||||
<div class="wizard-step active" style="text-align: center;">
|
||||
<h2 style="color: var(--accent);">Installation Successful!</h2>
|
||||
<p style="margin: 20px 0; color: var(--text-muted);">Your ChurchTube instance is now ready to use.</p>
|
||||
<div style="font-size: 3rem; margin-bottom: 24px;">🎉</div>
|
||||
<a href="index.php" class="btn btn-primary" style="width: 100%;">Go to Homepage</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
require_once 'includes/db.php';
|
||||
require_once 'includes/auth.php';
|
||||
|
||||
if (isLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
|
||||
$stmt = $pdo->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.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login | ChurchTube</title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="centered-container">
|
||||
<a href="index.php" class="logo" style="display: block; text-align: center; margin-bottom: 24px;">ChurchTube</a>
|
||||
<h2>Login</h2>
|
||||
<?php if ($error): ?>
|
||||
<div style="color: #ff4081; margin-bottom: 16px;"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Login</button>
|
||||
</form>
|
||||
<p style="margin-top: 20px; text-align: center; color: var(--text-muted);">
|
||||
Don't have an account? <a href="register.php" style="color: var(--primary-color);">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
?>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
require_once 'includes/db.php';
|
||||
require_once 'includes/auth.php';
|
||||
|
||||
if (isLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username']);
|
||||
$email = trim($_POST['email']);
|
||||
$password = $_POST['password'];
|
||||
$confirm_password = $_POST['confirm_password'];
|
||||
|
||||
if ($password !== $confirm_password) {
|
||||
$error = "Passwords do not match.";
|
||||
} else {
|
||||
$stmt = $pdo->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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Register | ChurchTube</title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="centered-container">
|
||||
<a href="index.php" class="logo" style="display: block; text-align: center; margin-bottom: 24px;">ChurchTube</a>
|
||||
<h2>Create Account</h2>
|
||||
<?php if ($error): ?>
|
||||
<div style="color: #ff4081; margin-bottom: 16px;"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Confirm Password</label>
|
||||
<input type="password" name="confirm_password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Register</button>
|
||||
</form>
|
||||
<p style="margin-top: 20px; text-align: center; color: var(--text-muted);">
|
||||
Already have an account? <a href="login.php" style="color: var(--primary-color);">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
<?php
|
||||
require_once 'includes/db.php';
|
||||
require_once 'includes/settings_helper.php';
|
||||
require_once 'includes/auth.php';
|
||||
|
||||
$video_id = $_GET['id'] ?? 0;
|
||||
$stmt = $pdo->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';
|
||||
?>
|
||||
|
||||
<div id="watch-container" style="max-width: 1200px; margin: 0 auto; padding: 24px; display: grid; grid-template-columns: 1fr 350px; gap: 32px;">
|
||||
|
||||
<style>
|
||||
@media (max-width: 900px) {
|
||||
#watch-container {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
#sidebar-recommendations {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div>
|
||||
<!-- Video Player Area -->
|
||||
<div id="video-wrapper" style="background: #000; aspect-ratio: 16/9; border-radius: 12px; overflow: hidden; margin-bottom: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); position: relative; width: 100%;">
|
||||
<?php if ($video['source_type'] === 'upload'): ?>
|
||||
<video id="main-video" controls onplay="incrementViews(<?= $video_id ?>)" style="width: 100%; height: 100%; object-fit: contain;" poster="<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>">
|
||||
<source src="<?= htmlspecialchars($video['video_url']) ?>" type="video/mp4">
|
||||
</video>
|
||||
<?php else: ?>
|
||||
<!-- Play Overlay for Linked Videos -->
|
||||
<div id="external-overlay" style="position: absolute; inset: 0; background-image: url('<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>'); background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 5;" onclick="loadExternalVideo()">
|
||||
<div style="background: rgba(var(--primary-rgb, 124, 77, 255), 0.9); width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; box-shadow: 0 0 30px rgba(0,0,0,0.5); transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">
|
||||
<i class="fas fa-play" style="margin-left: 5px;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="iframe-container" style="width: 100%; height: 100%; display: none;">
|
||||
<iframe id="external-iframe" width="100%" height="100%" src="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="width: 100%; height: 100%; object-fit: contain;"></iframe>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Recommendation Overlay (for uploaded videos) -->
|
||||
<div id="video-end-overlay" style="display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.8); z-index: 10; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 20px;">
|
||||
<h3 style="margin-bottom: 20px;">Up Next</h3>
|
||||
<div style="display: flex; gap: 16px;">
|
||||
<?php foreach (array_slice($recommendations, 0, 2) as $rec): ?>
|
||||
<a href="watch.php?id=<?= $rec['id'] ?>" style="width: 200px;">
|
||||
<div style="aspect-ratio: 16/9; background: #333; background-image: url('<?= $rec['thumbnail_url'] ?>'); background-size: cover; border-radius: 8px;"></div>
|
||||
<div style="margin-top: 8px; font-size: 0.9rem;"><?= htmlspecialchars($rec['title']) ?></div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button onclick="document.getElementById('video-end-overlay').style.display='none'" class="btn" style="margin-top: 20px; background: var(--glass);">Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Info -->
|
||||
<div style="margin-bottom: 32px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<h1 style="font-size: 1.8rem; margin-bottom: 12px;"><?= htmlspecialchars($video['title']) ?></h1>
|
||||
<?php if (isEditor()): ?>
|
||||
<a href="admin/edit_video.php?id=<?= $video_id ?>" class="btn" style="background: var(--glass);"><i class="fas fa-edit"></i> Edit</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--glass-border); padding-bottom: 20px;">
|
||||
<div style="color: var(--text-muted); font-size: 0.95rem;">
|
||||
<?= number_format($video['views']) ?> views • <?= date('M d, Y', strtotime($video['release_date'])) ?>
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; position: relative;">
|
||||
<button class="btn" onclick="toggleShareMenu()" style="background: var(--primary-color); color: white;"><i class="fas fa-share"></i> Share</button>
|
||||
|
||||
<!-- Share Menu Dropdown -->
|
||||
<div id="share-menu" style="display: none; position: absolute; top: 100%; right: 0; background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 8px; padding: 12px; z-index: 100; min-width: 200px; margin-top: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
|
||||
<div style="margin-bottom: 12px; font-weight: 600; font-size: 0.9rem;">Share this sermon</div>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<button onclick="copyCurrentLink()" class="btn" style="background: var(--glass); width: 100%; justify-content: flex-start; font-size: 0.85rem;">
|
||||
<i class="fas fa-link" style="margin-right: 8px;"></i> Copy Link
|
||||
</button>
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=<?= urlencode((isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']) ?>" target="_blank" class="btn" style="background: #1877f2; width: 100%; justify-content: flex-start; font-size: 0.85rem; text-decoration: none; color: white;">
|
||||
<i class="fab fa-facebook" style="margin-right: 8px;"></i> Facebook
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/tweet?url=<?= urlencode((isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']) ?>&text=Check out this sermon on ChurchTube!" target="_blank" class="btn" style="background: #000; width: 100%; justify-content: flex-start; font-size: 0.85rem; text-decoration: none; color: white;">
|
||||
<i class="fab fa-x-twitter" style="margin-right: 8px;"></i> Twitter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px 0;">
|
||||
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
||||
<div style="width: 48px; height: 48px; background: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
||||
<?= strtoupper(substr($video['uploader'], 0, 1)) ?>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight: 600;"><?= htmlspecialchars($video['uploader']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background: var(--glass); padding: 16px; border-radius: 12px; white-space: pre-wrap;"><?= htmlspecialchars($video['description']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div>
|
||||
<h3 id="comment-count"><?= count($comments) ?> Comments</h3>
|
||||
<?php if (isLoggedIn()): ?>
|
||||
<form id="comment-form" style="margin: 24px 0; display: flex; gap: 16px;">
|
||||
<input type="hidden" name="video_id" value="<?= $video_id ?>">
|
||||
<textarea name="comment" id="comment-text" class="form-control" placeholder="Add a comment..." style="min-height: 80px;"></textarea>
|
||||
<button type="submit" class="btn btn-primary" id="submit-comment" style="height: fit-content; align-self: flex-end;">Post</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="comment-list" style="display: flex; flex-direction: column; gap: 24px;">
|
||||
<?php foreach ($comments as $c): ?>
|
||||
<div class="comment-item" style="display: flex; gap: 16px;">
|
||||
<div style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
||||
<?= strtoupper(substr($c['username'], 0, 1)) ?>
|
||||
</div>
|
||||
<div style="flex-grow: 1;">
|
||||
<div style="font-weight: 600; font-size: 0.9rem;">
|
||||
<?= htmlspecialchars($c['username']) ?>
|
||||
<span style="font-weight: 400; color: var(--text-muted); margin-left: 8px; font-size: 0.8rem;"><?= date('M d, Y', strtotime($c['created_at'])) ?></span>
|
||||
</div>
|
||||
<div style="margin-top: 4px;"><?= htmlspecialchars($c['comment_text']) ?></div>
|
||||
|
||||
<!-- Reactions & Actions -->
|
||||
<div style="display: flex; gap: 16px; margin-top: 10px; align-items: center; font-size: 0.85rem; color: var(--text-muted);">
|
||||
<span onclick="react(<?= $c['id'] ?>, 'thumb')" style="cursor:pointer;"><i class="fas fa-thumbs-up"></i> <span id="count-thumb-<?= $c['id'] ?>"><?= $c['thumbs'] ?></span></span>
|
||||
<span onclick="react(<?= $c['id'] ?>, 'heart')" style="cursor:pointer;"><i class="fas fa-heart"></i> <span id="count-heart-<?= $c['id'] ?>"><?= $c['hearts'] ?></span></span>
|
||||
<span onclick="react(<?= $c['id'] ?>, 'pray')" style="cursor:pointer;"><i class="fas fa-hands-praying"></i> <span id="count-pray-<?= $c['id'] ?>"><?= $c['prays'] ?></span></span>
|
||||
<span onclick="react(<?= $c['id'] ?>, 'insight')" style="cursor:pointer;"><i class="fas fa-lightbulb"></i> <span id="count-insight-<?= $c['id'] ?>"><?= $c['insights'] ?></span></span>
|
||||
<span onclick="react(<?= $c['id'] ?>, 'clap')" style="cursor:pointer;"><i class="fas fa-hands-clapping"></i> <span id="count-clap-<?= $c['id'] ?>"><?= $c['claps'] ?></span></span>
|
||||
|
||||
<span onclick="report(<?= $c['id'] ?>)" style="cursor:pointer; margin-left: 10px; font-weight: 600; color: #ff4081;">REPORT</span>
|
||||
|
||||
<?php if (isModerator()): ?>
|
||||
<span onclick="deleteComment(<?= $c['id'] ?>)" style="cursor:pointer; margin-left: 10px; color: #ff4081;"><i class="fas fa-trash"></i></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Recommendations -->
|
||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<h3 style="font-size: 1.1rem;">Recommended</h3>
|
||||
<?php foreach ($recommendations as $rec): ?>
|
||||
<a href="watch.php?id=<?= $rec['id'] ?>" style="display: flex; gap: 12px;">
|
||||
<div style="width: 140px; aspect-ratio: 16/9; background: #333; background-image: url('<?= $rec['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>'); background-size: cover; border-radius: 8px; flex-shrink: 0;"></div>
|
||||
<div>
|
||||
<div style="font-weight: 600; font-size: 0.9rem; line-height: 1.2; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
<?= htmlspecialchars($rec['title']) ?>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 4px;">
|
||||
<?= number_format($rec['views']) ?> views
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const videoId = <?= $video_id ?>;
|
||||
const isModerator = <?= isModerator() ? 'true' : 'false' ?>;
|
||||
const videoElem = document.getElementById('main-video');
|
||||
const commentForm = document.getElementById('comment-form');
|
||||
const commentList = document.getElementById('comment-list');
|
||||
const commentCount = document.getElementById('comment-count');
|
||||
let currentCommentsJson = '';
|
||||
|
||||
if (videoElem) {
|
||||
videoElem.onended = () => {
|
||||
document.getElementById('video-end-overlay').style.display = 'flex';
|
||||
};
|
||||
}
|
||||
|
||||
function loadExternalVideo() {
|
||||
const overlay = document.getElementById('external-overlay');
|
||||
const container = document.getElementById('iframe-container');
|
||||
const iframe = document.getElementById('external-iframe');
|
||||
const videoUrl = "<?= htmlspecialchars($video['video_url']) ?>";
|
||||
|
||||
incrementViews(videoId);
|
||||
|
||||
overlay.style.display = 'none';
|
||||
container.style.display = 'block';
|
||||
iframe.src = videoUrl;
|
||||
}
|
||||
|
||||
let viewsIncremented = false;
|
||||
async function incrementViews(id) {
|
||||
if (viewsIncremented) return;
|
||||
viewsIncremented = true;
|
||||
try {
|
||||
await fetch('api/increment_views.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `video_id=${id}`
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to increment views');
|
||||
}
|
||||
}
|
||||
|
||||
async function react(commentId, type) {
|
||||
const res = await fetch('api/react.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `comment_id=${commentId}&type=${type}`
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
document.getElementById(`count-${type}-${commentId}`).innerText = result.count;
|
||||
}
|
||||
}
|
||||
|
||||
async function report(commentId) {
|
||||
if (!confirm('Report this comment for moderation?')) return;
|
||||
const res = await fetch('api/report.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `comment_id=${commentId}`
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) updateComments();
|
||||
}
|
||||
|
||||
async function deleteComment(commentId) {
|
||||
if (!confirm('Moderator: Delete this comment permanently?')) return;
|
||||
const res = await fetch('api/delete_comment.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `comment_id=${commentId}`
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) updateComments();
|
||||
}
|
||||
|
||||
async function updateComments() {
|
||||
try {
|
||||
const response = await fetch(`api/get_comments.php?video_id=${videoId}`);
|
||||
const comments = await response.json();
|
||||
const json = JSON.stringify(comments);
|
||||
|
||||
if (json !== currentCommentsJson) {
|
||||
renderComments(comments);
|
||||
currentCommentsJson = json;
|
||||
commentCount.innerText = `${comments.length} Comments`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch comments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderComments(comments) {
|
||||
commentList.innerHTML = '';
|
||||
comments.forEach(c => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'comment-item';
|
||||
div.style.display = 'flex';
|
||||
div.style.gap = '16px';
|
||||
div.innerHTML = `
|
||||
<div style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
||||
${c.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div style="flex-grow: 1;">
|
||||
<div style="font-weight: 600; font-size: 0.9rem;">
|
||||
${escapeHtml(c.username)}
|
||||
<span style="font-weight: 400; color: var(--text-muted); margin-left: 8px; font-size: 0.8rem;">${c.display_date}</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px;">${escapeHtml(c.comment_text)}</div>
|
||||
|
||||
<div style="display: flex; gap: 16px; margin-top: 10px; align-items: center; font-size: 0.85rem; color: var(--text-muted);">
|
||||
<span onclick="react(${c.id}, 'thumb')" style="cursor:pointer;"><i class="fas fa-thumbs-up"></i> <span id="count-thumb-${c.id}">${c.thumbs}</span></span>
|
||||
<span onclick="react(${c.id}, 'heart')" style="cursor:pointer;"><i class="fas fa-heart"></i> <span id="count-heart-${c.id}">${c.hearts}</span></span>
|
||||
<span onclick="react(${c.id}, 'pray')" style="cursor:pointer;"><i class="fas fa-hands-praying"></i> <span id="count-pray-${c.id}">${c.prays}</span></span>
|
||||
<span onclick="react(${c.id}, 'insight')" style="cursor:pointer;"><i class="fas fa-lightbulb"></i> <span id="count-insight-${c.id}">${c.insights}</span></span>
|
||||
<span onclick="react(${c.id}, 'clap')" style="cursor:pointer;"><i class="fas fa-hands-clapping"></i> <span id="count-clap-${c.id}">${c.claps}</span></span>
|
||||
|
||||
<span onclick="report(${c.id})" style="cursor:pointer; margin-left: 10px; font-weight: 600; color: #ff4081;">REPORT</span>
|
||||
|
||||
${isModerator ? `<span onclick="deleteComment(${c.id})" style="cursor:pointer; margin-left: 10px; color: #ff4081;"><i class="fas fa-trash"></i></span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
commentList.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleShareMenu() {
|
||||
const menu = document.getElementById('share-menu');
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function copyCurrentLink() {
|
||||
const url = window.location.href;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
alert('Link copied to clipboard!');
|
||||
toggleShareMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
commentForm?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const text = document.getElementById('comment-text');
|
||||
const btn = document.getElementById('submit-comment');
|
||||
if (!text.value.trim()) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const formData = new FormData();
|
||||
formData.append('video_id', videoId);
|
||||
formData.append('comment', text.value);
|
||||
|
||||
const res = await fetch('api/post_comment.php', { method: 'POST', body: formData });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
text.value = '';
|
||||
await updateComments();
|
||||
}
|
||||
btn.disabled = false;
|
||||
});
|
||||
|
||||
// Initialize polling
|
||||
setInterval(updateComments, 5000);
|
||||
updateComments(); // Initial load
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
Loading…
Reference in New Issue