admin panel added and comments are working and likes are working

This commit is contained in:
ghassan committed 2026-03-03 17:36:19 +03:00
1 parent 72e9439727
commit a28023c29b
32 files changed
+5077 -2999

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
@extends('admin.layout')
@section('title', 'Admin Dashboard')
@section('page_title', 'Dashboard')
@section('content')
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="stats-card">
<div class="stats-card-icon">
<i class="bi bi-people"></i>
</div>
<div class="stats-card-value">{{ $stats['total_users'] }}</div>
<div class="stats-card-label">Total Users</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<div class="stats-card-icon">
<i class="bi bi-play-circle"></i>
</div>
<div class="stats-card-value">{{ $stats['total_videos'] }}</div>
<div class="stats-card-label">Total Videos</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<div class="stats-card-icon">
<i class="bi bi-eye"></i>
</div>
<div class="stats-card-value">{{ number_format($stats['total_views']) }}</div>
<div class="stats-card-label">Total Views</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<div class="stats-card-icon">
<i class="bi bi-hand-thumbs-up"></i>
</div>
<div class="stats-card-value">{{ number_format($stats['total_likes']) }}</div>
<div class="stats-card-label">Total Likes</div>
</div>
</div>
</div>
<!-- Recent Users & Videos -->
<div class="row">
<!-- Recent Users -->
<div class="col-lg-6">
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Recent Users</h5>
<a href="{{ route('admin.users') }}" class="btn btn-outline-light btn-sm">View All</a>
</div>
<table class="admin-table">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Joined</th>
</tr>
</thead>
<tbody>
@forelse($recentUsers as $user)
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="user-avatar">
<div>
<div>{{ $user->name }}</div>
<small class="text-secondary">{{ $user->email }}</small>
</div>
</div>
</td>
<td>
@if($user->role === 'super_admin')
<span class="badge-role badge-super-admin">Super Admin</span>
@elseif($user->role === 'admin')
<span class="badge-role badge-admin">Admin</span>
@else
<span class="badge-role badge-user">User</span>
@endif
</td>
<td>{{ $user->created_at->diffForHumans() }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center text-secondary">No users found</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<!-- Recent Videos -->
<div class="col-lg-6">
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Recent Videos</h5>
<a href="{{ route('admin.videos') }}" class="btn btn-outline-light btn-sm">View All</a>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Video</th>
<th>Status</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
@forelse($recentVideos as $video)
<tr>
<td>
<div class="d-flex align-items-center gap-2">
@if($video->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $video->thumbnail) }}" alt="{{ $video->title }}" style="width: 60px; height: 40px; object-fit: cover; border-radius: 4px;">
@else
<div style="width: 60px; height: 40px; background: #333; border-radius: 4px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-play-circle text-secondary"></i>
</div>
@endif
<div>
<div style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ $video->title }}</div>
<small class="text-secondary">by {{ $video->user->name }}</small>
</div>
</div>
</td>
<td>
@switch($video->status)
@case('ready')
<span class="badge-status badge-ready">Ready</span>
@break
@case('processing')
<span class="badge-status badge-processing">Processing</span>
@break
@case('pending')
<span class="badge-status badge-pending">Pending</span>
@break
@case('failed')
<span class="badge-status badge-failed">Failed</span>
@break
@endswitch
</td>
<td>{{ $video->created_at->diffForHumans() }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center text-secondary">No videos found</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<!-- Videos by Status & Visibility -->
<div class="row">
<!-- Videos by Status -->
<div class="col-lg-6">
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Videos by Status</h5>
</div>
<div class="d-flex gap-3 flex-wrap">
<div class="text-center">
<div class="h3 text-success">{{ $videosByStatus['ready'] ?? 0 }}</div>
<small class="text-secondary">Ready</small>
</div>
<div class="text-center">
<div class="h3 text-primary">{{ $videosByStatus['processing'] ?? 0 }}</div>
<small class="text-secondary">Processing</small>
</div>
<div class="text-center">
<div class="h3 text-warning">{{ $videosByStatus['pending'] ?? 0 }}</div>
<small class="text-secondary">Pending</small>
</div>
<div class="text-center">
<div class="h3 text-danger">{{ $videosByStatus['failed'] ?? 0 }}</div>
<small class="text-secondary">Failed</small>
</div>
</div>
</div>
</div>
<!-- Videos by Visibility -->
<div class="col-lg-6">
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Videos by Visibility</h5>
</div>
<div class="d-flex gap-3 flex-wrap">
<div class="text-center">
<div class="h3 text-success">{{ $videosByVisibility['public'] ?? 0 }}</div>
<small class="text-secondary">Public</small>
</div>
<div class="text-center">
<div class="h3 text-warning">{{ $videosByVisibility['unlisted'] ?? 0 }}</div>
<small class="text-secondary">Unlisted</small>
</div>
<div class="text-center">
<div class="h3 text-secondary">{{ $videosByVisibility['private'] ?? 0 }}</div>
<small class="text-secondary">Private</small>
</div>
</div>
</div>
</div>
</div>
@endsection
+133
View File
@@ -0,0 +1,133 @@
@extends('admin.layout')
@section('title', 'Edit User')
@section('page_title', 'Edit User')
@section('content')
<!-- Alerts -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="row">
<div class="col-lg-8">
<!-- Edit User Form -->
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Edit User</h5>
<a href="{{ route('admin.users') }}" class="btn btn-outline-light btn-sm">
<i class="bi bi-arrow-left"></i> Back to Users
</a>
</div>
<form method="POST" action="{{ route('admin.users.update', $user->id) }}">
@csrf
@method('PUT')
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control @error('name') is-invalid @enderror" id="name" name="name" value="{{ old('name', $user->name) }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $user->email) }}" required>
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role" required>
<option value="user" {{ old('role', $user->role) == 'user' ? 'selected' : '' }}>User</option>
<option value="admin" {{ old('role', $user->role) == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="super_admin" {{ old('role', $user->role) == 'super_admin' ? 'selected' : '' }}>Super Admin</option>
</select>
@error('role')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<hr style="border-color: var(--border-color);">
<h6 class="mb-3">Change Password (Optional)</h6>
<div class="mb-3">
<label for="new_password" class="form-label">New Password</label>
<input type="password" class="form-control @error('new_password') is-invalid @enderror" id="new_password" name="new_password" placeholder="Leave blank to keep current password">
@error('new_password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="new_password_confirmation" class="form-label">Confirm New Password</label>
<input type="password" class="form-control" id="new_password_confirmation" name="new_password_confirmation" placeholder="Confirm new password">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Update User
</button>
<a href="{{ route('admin.users') }}" class="btn btn-outline-light">Cancel</a>
</div>
</form>
</div>
</div>
<div class="col-lg-4">
<!-- User Info -->
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">User Info</h5>
</div>
<div class="text-center mb-3">
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="rounded-circle" style="width: 80px; height: 80px; object-fit: cover;">
<h5 class="mt-2">{{ $user->name }}</h5>
<p class="text-secondary mb-1">{{ $user->email }}</p>
@if($user->role === 'super_admin')
<span class="badge-role badge-super-admin">Super Admin</span>
@elseif($user->role === 'admin')
<span class="badge-role badge-admin">Admin</span>
@else
<span class="badge-role badge-user">User</span>
@endif
</div>
<hr style="border-color: var(--border-color);">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">User ID</span>
<span>#{{ $user->id }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Joined</span>
<span>{{ $user->created_at->format('M d, Y') }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Total Videos</span>
<span>{{ $user->videos->count() }}</span>
</div>
<div class="d-flex justify-content-between">
<span class="text-secondary">Email Verified</span>
<span>{{ $user->email_verified_at ? 'Yes' : 'No' }}</span>
</div>
</div>
</div>
</div>
@endsection
+166
View File
@@ -0,0 +1,166 @@
@extends('admin.layout')
@section('title', 'Edit Video')
@section('page_title', 'Edit Video')
@section('content')
<!-- Alerts -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="row">
<div class="col-lg-8">
<!-- Edit Video Form -->
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Edit Video</h5>
<a href="{{ route('admin.videos') }}" class="btn btn-outline-light btn-sm">
<i class="bi bi-arrow-left"></i> Back to Videos
</a>
</div>
<form method="POST" action="{{ route('admin.videos.update', $video->id) }}">
@csrf
@method('PUT')
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title', $video->title) }}" required>
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror" id="description" name="description" rows="4">{{ old('description', $video->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="row mb-3">
<div class="col-md-4">
<label for="visibility" class="form-label">Visibility</label>
<select class="form-select @error('visibility') is-invalid @enderror" id="visibility" name="visibility" required>
<option value="public" {{ old('visibility', $video->visibility) == 'public' ? 'selected' : '' }}>Public</option>
<option value="unlisted" {{ old('visibility', $video->visibility) == 'unlisted' ? 'selected' : '' }}>Unlisted</option>
<option value="private" {{ old('visibility', $video->visibility) == 'private' ? 'selected' : '' }}>Private</option>
</select>
@error('visibility')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label for="type" class="form-label">Type</label>
<select class="form-select @error('type') is-invalid @enderror" id="type" name="type" required>
<option value="generic" {{ old('type', $video->type) == 'generic' ? 'selected' : '' }}>Generic</option>
<option value="music" {{ old('type', $video->type) == 'music' ? 'selected' : '' }}>Music</option>
<option value="match" {{ old('type', $video->type) == 'match' ? 'selected' : '' }}>Match</option>
</select>
@error('type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label for="status" class="form-label">Status</label>
<select class="form-select @error('status') is-invalid @enderror" id="status" name="status" required>
<option value="pending" {{ old('status', $video->status) == 'pending' ? 'selected' : '' }}>Pending</option>
<option value="processing" {{ old('status', $video->status) == 'processing' ? 'selected' : '' }}>Processing</option>
<option value="ready" {{ old('status', $video->status) == 'ready' ? 'selected' : '' }}>Ready</option>
<option value="failed" {{ old('status', $video->status) == 'failed' ? 'selected' : '' }}>Failed</option>
</select>
@error('status')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Update Video
</button>
<a href="{{ route('admin.videos') }}" class="btn btn-outline-light">Cancel</a>
</div>
</form>
</div>
</div>
<div class="col-lg-4">
<!-- Video Info -->
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">Video Info</h5>
</div>
@if($video->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $video->thumbnail) }}" alt="{{ $video->title }}" class="img-fluid rounded mb-3" style="width: 100%;">
@else
<div class="bg-secondary rounded d-flex align-items-center justify-content-center mb-3" style="height: 180px;">
<i class="bi bi-play-circle text-white" style="font-size: 3rem;"></i>
</div>
@endif
<hr style="border-color: var(--border-color);">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Video ID</span>
<span>#{{ $video->id }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Owner</span>
<a href="{{ route('channel', $video->user->id) }}" target="_blank">{{ $video->user->name }}</a>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Uploaded</span>
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">File Size</span>
<span>{{ number_format($video->size / 1024 / 1024, 2) }} MB</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Duration</span>
<span>{{ $video->duration ? gmdate('H:i:s', $video->duration) : 'N/A' }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Orientation</span>
<span class="text-capitalize">{{ $video->orientation }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Dimensions</span>
<span>{{ $video->width ?? 'N/A' }} x {{ $video->height ?? 'N/A' }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Views</span>
<span>{{ number_format(\DB::table('video_views')->where('video_id', $video->id)->count()) }}</span>
</div>
<div class="d-flex justify-content-between">
<span class="text-secondary">Likes</span>
<span>{{ number_format(\DB::table('video_likes')->where('video_id', $video->id)->count()) }}</span>
</div>
<hr style="border-color: var(--border-color);">
<div class="d-grid gap-2">
<a href="{{ route('videos.show', $video->id) }}" target="_blank" class="btn btn-outline-light btn-sm">
<i class="bi bi-play-circle"></i> View Video
</a>
</div>
</div>
</div>
</div>
@endsection
+611
View File
@@ -0,0 +1,611 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Admin Dashboard') - {{ config('app.name') }}</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ asset('storage/images/logo.png') }}">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
:root {
--brand-red: #e61e1e;
--bg-dark: #0f0f0f;
--bg-secondary: #1e1e1e;
--border-color: #303030;
--text-primary: #f1f1f1;
--text-secondary: #aaaaaa;
}
* { box-sizing: border-box; }
body {
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: "Roboto", "Arial", sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* Header */
.yt-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 1000;
border-bottom: 1px solid var(--border-color);
}
.yt-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.yt-menu-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
background: transparent;
border: none;
color: var(--text-primary);
}
.yt-menu-btn:hover { background: var(--border-color); }
.yt-logo {
display: flex;
align-items: center;
text-decoration: none;
gap: 4px;
}
.yt-logo-text {
font-size: 1.4rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -1px;
}
/* Search */
.yt-header-center {
flex: 1;
max-width: 640px;
margin: 0 40px;
display: flex;
}
.yt-search {
flex: 1;
display: flex;
height: 40px;
}
.yt-search-input {
flex: 1;
background: #121212;
border: 1px solid var(--border-color);
border-right: none;
border-radius: 20px 0 0 20px;
padding: 0 16px;
color: var(--text-primary);
font-size: 16px;
}
.yt-search-input:focus {
outline: none;
border-color: #1c62b9;
}
.yt-search-btn {
width: 64px;
background: #222;
border: 1px solid var(--border-color);
border-radius: 0 20px 20px 0;
color: var(--text-primary);
cursor: pointer;
}
.yt-search-btn:hover { background: #303030; }
/* Header Right */
.yt-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.yt-icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 1.2rem;
}
.yt-icon-btn:hover { background: var(--border-color); }
.yt-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #555;
cursor: pointer;
}
/* Sidebar */
.yt-sidebar {
position: fixed;
top: 56px;
left: 0;
bottom: 0;
width: 240px;
background: var(--bg-dark);
overflow-y: auto;
padding: 12px;
transition: transform 0.3s;
z-index: 999;
}
.yt-sidebar-section {
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 12px;
}
.yt-sidebar-link {
display: flex;
align-items: center;
gap: 20px;
padding: 0 12px;
height: 40px;
border-radius: 10px;
color: var(--text-primary);
text-decoration: none;
cursor: pointer;
transition: background 0.2s;
}
.yt-sidebar-link:hover { background: var(--border-color); }
.yt-sidebar-link.active {
background: var(--border-color);
font-weight: 500;
}
.yt-sidebar-link i { font-size: 1.2rem; }
/* Admin Sidebar */
.admin-sidebar {
position: fixed;
top: 56px;
left: 0;
bottom: 0;
width: 240px;
background: var(--bg-secondary);
padding: 20px 0;
z-index: 1000;
border-right: 1px solid var(--border-color);
}
.admin-sidebar-brand {
padding: 0 20px 20px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
}
.admin-sidebar-brand h4 {
margin: 0;
color: var(--brand-red);
font-weight: 700;
}
.admin-sidebar-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
color: var(--text-primary);
text-decoration: none;
transition: background 0.2s;
}
.admin-sidebar-link:hover {
background: var(--border-color);
color: var(--text-primary);
}
.admin-sidebar-link.active {
background: var(--border-color);
color: var(--brand-red);
border-left: 3px solid var(--brand-red);
}
.admin-sidebar-link i {
font-size: 1.2rem;
}
/* Main Content */
.admin-main {
margin-top: 56px;
margin-left: 240px;
padding: 24px;
min-height: calc(100vh - 56px);
}
/* Upload Button */
.yt-upload-btn {
background: var(--brand-red);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
text-decoration: none;
}
.yt-upload-btn:hover { background: #cc1a1a; }
/* Dropdown */
.dropdown-menu-dark {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.dropdown-item {
color: var(--text-primary);
}
.dropdown-item:hover {
background: var(--border-color);
color: var(--text-primary);
}
/* Cards */
.admin-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.admin-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.admin-card-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
/* Stats Cards */
.stats-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
text-align: center;
}
.stats-card-icon {
font-size: 2rem;
margin-bottom: 10px;
color: var(--brand-red);
}
.stats-card-value {
font-size: 2rem;
font-weight: 700;
margin-bottom: 5px;
}
.stats-card-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Tables */
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table th,
.admin-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.admin-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.admin-table tr:hover {
background: rgba(255,255,255,0.02);
}
/* Forms */
.form-control, .form-select {
background: #282828;
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 8px;
padding: 10px 15px;
}
.form-control:focus, .form-select:focus {
background: #282828;
border-color: var(--brand-red);
color: var(--text-primary);
box-shadow: 0 0 0 2px rgba(230, 30, 30, 0.2);
}
.form-label {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 8px;
}
/* Buttons */
.btn-primary {
background: var(--brand-red);
border-color: var(--brand-red);
}
.btn-primary:hover {
background: #cc1a1a;
border-color: #cc1a1a;
}
.btn-outline-light {
border-color: var(--border-color);
color: var(--text-primary);
}
.btn-outline-light:hover {
background: var(--border-color);
border-color: var(--border-color);
color: var(--text-primary);
}
/* Badges */
.badge-role {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-super-admin {
background: #dc3545;
color: white;
}
.badge-admin {
background: #fd7e14;
color: white;
}
.badge-user {
background: #6c757d;
color: white;
}
/* Status badges */
.badge-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-ready { background: #198754; color: white; }
.badge-processing { background: #0d6efd; color: white; }
.badge-pending { background: #ffc107; color: black; }
.badge-failed { background: #dc3545; color: white; }
/* Visibility badges */
.badge-public { background: #198754; color: white; }
.badge-unlisted { background: #fd7e14; color: white; }
.badge-private { background: #6c757d; color: white; }
/* User avatar */
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
}
/* Search */
.search-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-form .form-control {
max-width: 300px;
}
/* Filters */
.filter-form {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: flex-end;
}
.filter-form .form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.filter-form label {
font-size: 0.85rem;
color: var(--text-secondary);
}
.filter-form .form-select {
min-width: 150px;
}
/* Pagination */
.pagination {
margin-top: 20px;
}
.page-link {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
}
.page-link:hover {
background: var(--border-color);
border-color: var(--border-color);
color: var(--text-primary);
}
.page-item.active .page-link {
background: var(--brand-red);
border-color: var(--brand-red);
}
/* Modal */
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.modal-header {
border-bottom-color: var(--border-color);
}
.modal-footer {
border-top-color: var(--border-color);
}
/* Alerts */
.alert-success {
background: #198754;
border: none;
color: white;
}
.alert-danger {
background: #dc3545;
border: none;
color: white;
}
/* Responsive */
@media (max-width: 768px) {
.admin-sidebar {
width: 60px;
}
.admin-sidebar-brand h4,
.admin-sidebar-link span {
display: none;
}
.admin-sidebar-link {
justify-content: center;
padding: 15px;
}
.admin-main {
margin-left: 60px;
}
}
</style>
@yield('extra_styles')
</head>
<body>
<!-- Header -->
@include('layouts.partials.header')
<!-- Sidebar -->
<aside class="admin-sidebar">
<div class="admin-sidebar-brand">
<h4><i class="bi bi-speedometer2"></i> Admin</h4>
</div>
<nav>
<a href="{{ route('admin.dashboard') }}" class="admin-sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-grid"></i>
<span>Dashboard</span>
</a>
<a href="{{ route('admin.users') }}" class="admin-sidebar-link {{ request()->routeIs('admin.users*') ? 'active' : '' }}">
<i class="bi bi-people"></i>
<span>Users</span>
</a>
<a href="{{ route('admin.videos') }}" class="admin-sidebar-link {{ request()->routeIs('admin.videos*') ? 'active' : '' }}">
<i class="bi bi-play-circle"></i>
<span>Videos</span>
</a>
<hr style="border-color: var(--border-color); margin: 20px 0;">
<a href="{{ route('videos.index') }}" class="admin-sidebar-link">
<i class="bi bi-arrow-left"></i>
<span>Back to Site</span>
</a>
</nav>
</aside>
<!-- Main Content -->
<main class="admin-main">
<!-- Page Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="margin: 0; font-size: 1.8rem; font-weight: 600;">@yield('page_title', 'Dashboard')</h1>
</div>
<!-- Content -->
@yield('content')
</main>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
@yield('scripts')
</body>
</html>
+192
View File
@@ -0,0 +1,192 @@
@extends('admin.layout')
@section('title', 'User Management')
@section('page_title', 'User Management')
@section('content')
<!-- Alerts -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<!-- Search & Filters -->
<div class="admin-card">
<form method="GET" action="{{ route('admin.users') }}" class="filter-form">
<div class="form-group">
<label for="search">Search</label>
<input type="text" name="search" id="search" class="form-control" placeholder="Search by name or email..." value="{{ request('search') }}">
</div>
<div class="form-group">
<label for="role">Role</label>
<select name="role" id="role" class="form-select">
<option value="">All Roles</option>
<option value="user" {{ request('role') == 'user' ? 'selected' : '' }}>User</option>
<option value="admin" {{ request('role') == 'admin' ? 'selected' : '' }}>Admin</option>
<option value="super_admin" {{ request('role') == 'super_admin' ? 'selected' : '' }}>Super Admin</option>
</select>
</div>
<div class="form-group">
<label for="sort">Sort By</label>
<select name="sort" id="sort" class="form-select">
<option value="latest" {{ request('sort') == 'latest' ? 'selected' : '' }}>Latest First</option>
<option value="oldest" {{ request('sort') == 'oldest' ? 'selected' : '' }}>Oldest First</option>
<option value="name_asc" {{ request('sort') == 'name_asc' ? 'selected' : '' }}>Name (A-Z)</option>
<option value="name_desc" {{ request('sort') == 'name_desc' ? 'selected' : '' }}>Name (Z-A)</option>
</select>
</div>
<div class="form-group">
<label>&nbsp;</label>
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> Filter
</button>
<a href="{{ route('admin.users') }}" class="btn btn-outline-light">
<i class="bi bi-x-circle"></i> Clear
</a>
</div>
</form>
</div>
<!-- Users Table -->
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">All Users ({{ $users->total() }})</h5>
</div>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>User</th>
<th>Email</th>
<th>Role</th>
<th>Videos</th>
<th>Joined</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="user-avatar">
<div>
<div>{{ $user->name }}</div>
@if($user->id === auth()->id())
<small class="text-info">(You)</small>
@endif
</div>
</div>
</td>
<td>{{ $user->email }}</td>
<td>
@if($user->role === 'super_admin')
<span class="badge-role badge-super-admin">Super Admin</span>
@elseif($user->role === 'admin')
<span class="badge-role badge-admin">Admin</span>
@else
<span class="badge-role badge-user">User</span>
@endif
</td>
<td>
<a href="{{ route('channel', $user->id) }}" target="_blank" class="text-decoration-none">
{{ $user->videos->count() }} videos
</a>
</td>
<td>{{ $user->created_at->format('M d, Y') }}</td>
<td>
<div class="dropdown">
<button class="btn btn-sm btn-outline-light dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-gear"></i>
</button>
<ul class="dropdown-menu dropdown-menu-dark">
<li>
<a class="dropdown-item" href="{{ route('admin.users.edit', $user->id) }}">
<i class="bi bi-pencil"></i> Edit
</a>
</li>
@if($user->id !== auth()->id())
<li>
<button class="dropdown-item text-danger" onclick="confirmDeleteUser({{ $user->id }}, '{{ $user->name }}')">
<i class="bi bi-trash"></i> Delete
</button>
</li>
@endif
</ul>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-secondary py-4">
No users found
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="d-flex justify-content-center">
{{ $users->links() }}
</div>
</div>
<!-- Delete User Modal -->
<div class="modal fade" id="deleteUserModal" tabindex="-1" aria-labelledby="deleteUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="background: #1a1a1a; border: 1px solid #3f3f3f; border-radius: 12px;">
<div class="modal-header" style="border-bottom: 1px solid #3f3f3f; padding: 20px 24px;">
<h5 class="modal-title" id="deleteUserModalLabel" style="color: #fff; font-weight: 600;">
<i class="bi bi-exclamation-triangle-fill text-danger me-2"></i>
Delete User
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" style="padding: 24px;">
<p>Are you sure you want to delete this user? This action cannot be undone.</p>
<p><strong>User:</strong> <span id="deleteUserName"></span></p>
<div class="alert alert-warning">
<i class="bi bi-info-circle me-2"></i>
This will also delete all videos uploaded by this user.
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid #3f3f3f; padding: 16px 24px;">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
<form id="deleteUserForm" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete User</button>
</form>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
let currentDeleteUserId = null;
function confirmDeleteUser(userId, userName) {
currentDeleteUserId = userId;
document.getElementById('deleteUserName').textContent = userName;
document.getElementById('deleteUserForm').action = '/admin/users/' + userId;
const modal = new bootstrap.Modal(document.getElementById('deleteUserModal'));
modal.show();
}
</script>
@endsection
+243
View File
@@ -0,0 +1,243 @@
@extends('admin.layout')
@section('title', 'Video Management')
@section('page_title', 'Video Management')
@section('content')
<!-- Alerts -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<!-- Search & Filters -->
<div class="admin-card">
<form method="GET" action="{{ route('admin.videos') }}" class="filter-form">
<div class="form-group">
<label for="search">Search</label>
<input type="text" name="search" id="search" class="form-control" placeholder="Search by title or description..." value="{{ request('search') }}">
</div>
<div class="form-group">
<label for="status">Status</label>
<select name="status" id="status" class="form-select">
<option value="">All Status</option>
<option value="ready" {{ request('status') == 'ready' ? 'selected' : '' }}>Ready</option>
<option value="processing" {{ request('status') == 'processing' ? 'selected' : '' }}>Processing</option>
<option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>Pending</option>
<option value="failed" {{ request('status') == 'failed' ? 'selected' : '' }}>Failed</option>
</select>
</div>
<div class="form-group">
<label for="visibility">Visibility</label>
<select name="visibility" id="visibility" class="form-select">
<option value="">All Visibility</option>
<option value="public" {{ request('visibility') == 'public' ? 'selected' : '' }}>Public</option>
<option value="unlisted" {{ request('visibility') == 'unlisted' ? 'selected' : '' }}>Unlisted</option>
<option value="private" {{ request('visibility') == 'private' ? 'selected' : '' }}>Private</option>
</select>
</div>
<div class="form-group">
<label for="type">Type</label>
<select name="type" id="type" class="form-select">
<option value="">All Types</option>
<option value="generic" {{ request('type') == 'generic' ? 'selected' : '' }}>Generic</option>
<option value="music" {{ request('type') == 'music' ? 'selected' : '' }}>Music</option>
<option value="match" {{ request('type') == 'match' ? 'selected' : '' }}>Match</option>
</select>
</div>
<div class="form-group">
<label for="sort">Sort By</label>
<select name="sort" id="sort" class="form-select">
<option value="latest" {{ request('sort') == 'latest' ? 'selected' : '' }}>Latest First</option>
<option value="oldest" {{ request('sort') == 'oldest' ? 'selected' : '' }}>Oldest First</option>
<option value="title_asc" {{ request('sort') == 'title_asc' ? 'selected' : '' }}>Title (A-Z)</option>
<option value="title_desc" {{ request('sort') == 'title_desc' ? 'selected' : '' }}>Title (Z-A)</option>
</select>
</div>
<div class="form-group">
<label>&nbsp;</label>
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> Filter
</button>
<a href="{{ route('admin.videos') }}" class="btn btn-outline-light">
<i class="bi bi-x-circle"></i> Clear
</a>
</div>
</form>
</div>
<!-- Videos Table -->
<div class="admin-card">
<div class="admin-card-header">
<h5 class="admin-card-title">All Videos ({{ $videos->total() }})</h5>
</div>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>Video</th>
<th>Owner</th>
<th>Status</th>
<th>Visibility</th>
<th>Type</th>
<th>Views</th>
<th>Likes</th>
<th>Uploaded</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($videos as $video)
<tr>
<td>
<div class="d-flex align-items-center gap-2">
@if($video->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $video->thumbnail) }}" alt="{{ $video->title }}" style="width: 80px; height: 50px; object-fit: cover; border-radius: 4px;">
@else
<div style="width: 80px; height: 50px; background: #333; border-radius: 4px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-play-circle text-secondary"></i>
</div>
@endif
<div style="max-width: 200px;">
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500;">{{ $video->title }}</div>
<small class="text-secondary">{{ Str::limit($video->description, 50) }}</small>
</div>
</div>
</td>
<td>
<a href="{{ route('channel', $video->user->id) }}" target="_blank" class="text-decoration-none">
{{ $video->user->name }}
</a>
</td>
<td>
@switch($video->status)
@case('ready')
<span class="badge-status badge-ready">Ready</span>
@break
@case('processing')
<span class="badge-status badge-processing">Processing</span>
@break
@case('pending')
<span class="badge-status badge-pending">Pending</span>
@break
@case('failed')
<span class="badge-status badge-failed">Failed</span>
@break
@endswitch
</td>
<td>
@switch($video->visibility)
@case('public')
<span class="badge-status badge-public">Public</span>
@break
@case('unlisted')
<span class="badge-status badge-unlisted">Unlisted</span>
@break
@case('private')
<span class="badge-status badge-private">Private</span>
@break
@endswitch
</td>
<td>
<span class="text-capitalize">{{ $video->type }}</span>
</td>
<td>{{ number_format(\DB::table('video_views')->where('video_id', $video->id)->count()) }}</td>
<td>{{ number_format(\DB::table('video_likes')->where('video_id', $video->id)->count()) }}</td>
<td>{{ $video->created_at->format('M d, Y') }}</td>
<td>
<div class="dropdown">
<button class="btn btn-sm btn-outline-light dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-gear"></i>
</button>
<ul class="dropdown-menu dropdown-menu-dark">
<li>
<a class="dropdown-item" href="{{ route('videos.show', $video->id) }}" target="_blank">
<i class="bi bi-play-circle"></i> View
</a>
</li>
<li>
<a class="dropdown-item" href="{{ route('admin.videos.edit', $video->id) }}">
<i class="bi bi-pencil"></i> Edit
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item text-danger" onclick="confirmDeleteVideo({{ $video->id }}, '{{ $video->title }}')">
<i class="bi bi-trash"></i> Delete
</button>
</li>
</ul>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="9" class="text-center text-secondary py-4">
No videos found
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="d-flex justify-content-center">
{{ $videos->links() }}
</div>
</div>
<!-- Delete Video Modal -->
<div class="modal fade" id="deleteVideoModal" tabindex="-1" aria-labelledby="deleteVideoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="background: #1a1a1a; border: 1px solid #3f3f3f; border-radius: 12px;">
<div class="modal-header" style="border-bottom: 1px solid #3f3f3f; padding: 20px 24px;">
<h5 class="modal-title" id="deleteVideoModalLabel" style="color: #fff; font-weight: 600;">
<i class="bi bi-exclamation-triangle-fill text-danger me-2"></i>
Delete Video
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" style="padding: 24px;">
<p>Are you sure you want to delete this video? This action cannot be undone.</p>
<p><strong>Video:</strong> <span id="deleteVideoTitle"></span></p>
<div class="alert alert-warning">
<i class="bi bi-info-circle me-2"></i>
This will also delete all likes and views associated with this video.
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid #3f3f3f; padding: 16px 24px;">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
<form id="deleteVideoForm" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete Video</button>
</form>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function confirmDeleteVideo(videoId, videoTitle) {
document.getElementById('deleteVideoTitle').textContent = videoTitle;
document.getElementById('deleteVideoForm').action = '/admin/videos/' + videoId;
const modal = new bootstrap.Modal(document.getElementById('deleteVideoModal'));
modal.show();
}
</script>
@endsection