admin panel added and comments are working and likes are working
This commit is contained in:
1 parent
72e9439727
commit
a28023c29b
32 files changed
+5077
-2999
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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> </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
|
||||
@@ -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> </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
|
||||
Reference in new issue
Block a user