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
@@ -1,9 +1,167 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('body_class', 'upload-page-only')
|
||||
|
||||
@section('title', 'Upload Video | ' . config('app.name'))
|
||||
|
||||
@section('extra_styles')
|
||||
<style>
|
||||
/* Mobile: Full page view */
|
||||
@media (max-width: 991px) {
|
||||
.upload-page-only .yt-header,
|
||||
.upload-page-only .yt-sidebar {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.upload-page-only .yt-main {
|
||||
display: block !important;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive {
|
||||
display: block;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .upload-modal-standalone {
|
||||
max-width: 100%;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .upload-modal-header {
|
||||
border-radius: 0;
|
||||
margin: -16px -16px 16px -16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .upload-modal-body {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .form-input,
|
||||
.upload-page-only.upload-page-responsive .form-textarea {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .form-label {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .visibility-content-modal {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .visibility-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.upload-page-only.upload-page-responsive .visibility-desc {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop: Modal view */
|
||||
@media (min-width: 992px) {
|
||||
.upload-page-only .yt-header,
|
||||
.upload-page-only .yt-sidebar,
|
||||
.upload-page-only .yt-main {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.upload-page-only {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Common Modal Styles */
|
||||
.upload-modal-standalone {
|
||||
background: linear-gradient(145deg, #1e1e1e 0%, #252525 100%);
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6), 0 0 40px rgba(230, 30, 30, 0.1);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
animation: modalPopIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes modalPopIn {
|
||||
0% {
|
||||
transform: scale(0.8) translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.02) translateY(-5px);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-modal-header {
|
||||
background: linear-gradient(135deg, #e63030 0%, #ff4d4d 100%);
|
||||
border-bottom: none;
|
||||
padding: 20px 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-modal-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 100%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.upload-icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: white;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.upload-modal-header .modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-subtitle {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.upload-modal-body {
|
||||
padding: 24px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Container for mobile */
|
||||
.upload-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
@@ -244,140 +402,503 @@
|
||||
margin-left: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop Modal Form Styles */
|
||||
@media (min-width: 992px) {
|
||||
.upload-modal-body .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.upload-modal-body .form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
.upload-modal-body .form-label i {
|
||||
color: #e63030;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.upload-modal-body .form-input,
|
||||
.upload-modal-body .form-textarea {
|
||||
width: 100%;
|
||||
background: #121212;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.upload-modal-body .form-input:focus,
|
||||
.upload-modal-body .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #e63030;
|
||||
box-shadow: 0 0 0 3px rgba(230, 30, 30, 0.15);
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-modal {
|
||||
border: 2px dashed #404040;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
background: #151515;
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-modal:hover {
|
||||
border-color: #e63030;
|
||||
background: rgba(230, 30, 30, 0.05);
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-modal.dragover {
|
||||
border-color: #e63030;
|
||||
background: rgba(230, 30, 30, 0.1);
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-modal input[type="file"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-icon {
|
||||
font-size: 36px;
|
||||
color: #e63030;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-title {
|
||||
color: #e5e5e5;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.upload-modal-body .dropzone-hint {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-options-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-option-modal {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-option-modal input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-content-modal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-option-modal:hover .visibility-content-modal {
|
||||
border-color: #555;
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-option-modal.active .visibility-content-modal {
|
||||
border-color: #e63030;
|
||||
background: rgba(230, 30, 30, 0.1);
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-content-modal i {
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-option-modal.active .visibility-content-modal i {
|
||||
color: #e63030;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
.upload-modal-body .visibility-desc {
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.upload-modal-actions .btn-cancel,
|
||||
.upload-modal-actions .btn-submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-modal-actions .btn-cancel {
|
||||
background: transparent;
|
||||
border: 1px solid #404040;
|
||||
color: #aaa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.upload-modal-actions .btn-cancel:hover {
|
||||
border-color: #666;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.upload-modal-actions .btn-submit {
|
||||
background: linear-gradient(135deg, #e63030 0%, #ff4d4d 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(230, 30, 30, 0.3);
|
||||
}
|
||||
|
||||
.upload-modal-actions .btn-submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(230, 30, 30, 0.4);
|
||||
}
|
||||
|
||||
.upload-modal-actions .btn-submit:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="upload-container">
|
||||
<h1 style="font-size: 24px; font-weight: 500; margin-bottom: 24px;">Upload Video</h1>
|
||||
<div class="upload-modal-standalone">
|
||||
<!-- Header -->
|
||||
<div class="upload-modal-header">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="upload-icon-wrapper">
|
||||
<i class="bi bi-cloud-arrow-up-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="modal-title">Upload Video</h5>
|
||||
<span class="upload-subtitle">Share your creativity</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('videos.index') }}" class="btn-close btn-close-white" style="position: relative; z-index: 1; text-decoration: none;">×</a>
|
||||
</div>
|
||||
|
||||
<form id="upload-form" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" required
|
||||
class="form-input"
|
||||
placeholder="Enter video title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" rows="4"
|
||||
class="form-textarea"
|
||||
placeholder="Tell viewers about your video"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="upload-row">
|
||||
<!-- Body -->
|
||||
<div class="upload-modal-body">
|
||||
<form id="upload-form" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Video File *</label>
|
||||
<div id="dropzone">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-card-heading"></i> Title *
|
||||
</label>
|
||||
<input type="text" name="title" required
|
||||
class="form-input"
|
||||
placeholder="Enter video title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-text-paragraph"></i> Description
|
||||
</label>
|
||||
<textarea name="description" rows="3"
|
||||
class="form-textarea"
|
||||
placeholder="Tell viewers about your video"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-camera-video"></i> Video File *
|
||||
</label>
|
||||
<div id="dropzone" class="dropzone-modal">
|
||||
<input type="file" name="video" id="video" accept="video/*" required>
|
||||
<div id="dropzone-default">
|
||||
<i class="bi bi-camera-video icon"></i>
|
||||
<p>Click to select or drag video here</p>
|
||||
<p class="hint">MP4, MOV, AVI, WebM up to 512MB</p>
|
||||
<div class="dropzone-icon">
|
||||
<i class="bi bi-cloud-arrow-up"></i>
|
||||
</div>
|
||||
<p class="dropzone-title">Click to select or drag video here</p>
|
||||
<p class="dropzone-hint">MP4, MOV, AVI, WebM up to 512MB</p>
|
||||
</div>
|
||||
<div id="file-info">
|
||||
<p class="filename" id="filename"></p>
|
||||
<p id="filesize"></p>
|
||||
<div class="file-preview">
|
||||
<i class="bi bi-film"></i>
|
||||
</div>
|
||||
<div class="file-details">
|
||||
<p class="filename" id="filename"></p>
|
||||
<p id="filesize"></p>
|
||||
</div>
|
||||
<button type="button" class="btn-remove-file" onclick="removeVideo(event)">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Thumbnail (optional)</label>
|
||||
<div id="thumbnail-dropzone">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-image"></i> Thumbnail (optional)
|
||||
</label>
|
||||
<div id="thumbnail-dropzone" class="dropzone-modal">
|
||||
<input type="file" name="thumbnail" id="thumbnail" accept="image/*">
|
||||
<div id="thumbnail-default">
|
||||
<i class="bi bi-image icon"></i>
|
||||
<p>Click to select or drag thumbnail</p>
|
||||
<p class="hint">JPG, PNG, WebP up to 5MB</p>
|
||||
<div class="dropzone-icon">
|
||||
<i class="bi bi-card-image"></i>
|
||||
</div>
|
||||
<p class="dropzone-title">Click to select or drag thumbnail</p>
|
||||
<p class="dropzone-hint">JPG, PNG, WebP up to 5MB</p>
|
||||
</div>
|
||||
<div id="thumbnail-info">
|
||||
<p class="filename" id="thumbnail-filename"></p>
|
||||
<p id="thumbnail-filesize"></p>
|
||||
<div class="file-preview thumbnail-preview">
|
||||
<img id="thumbnail-preview-img" src="" alt="Thumbnail preview">
|
||||
</div>
|
||||
<div class="file-details">
|
||||
<p class="filename" id="thumbnail-filename"></p>
|
||||
<p id="thumbnail-filesize"></p>
|
||||
</div>
|
||||
<button type="button" class="btn-remove-file" onclick="removeThumbnail(event)">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Video Type</label>
|
||||
<div class="visibility-options">
|
||||
<label class="visibility-option active">
|
||||
<input type="radio" name="type" value="generic" checked>
|
||||
<div class="visibility-content">
|
||||
<i class="bi bi-film"></i>
|
||||
<span class="visibility-title">Generic</span>
|
||||
<span class="visibility-desc">Standard video</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="type" value="music">
|
||||
<div class="visibility-content">
|
||||
<i class="bi bi-music-note"></i>
|
||||
<span class="visibility-title">Music</span>
|
||||
<span class="visibility-desc">Music video or song</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="type" value="match">
|
||||
<div class="visibility-content">
|
||||
<i class="bi bi-trophy"></i>
|
||||
<span class="visibility-title">Match</span>
|
||||
<span class="visibility-desc">Sports match or competition</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-collection-play"></i> Video Type
|
||||
</label>
|
||||
<div class="visibility-options-modal" id="type-options">
|
||||
<label class="visibility-option-modal active">
|
||||
<input type="radio" name="type" value="generic" checked>
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-film"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Generic</span>
|
||||
<span class="visibility-desc">Standard video</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option-modal">
|
||||
<input type="radio" name="type" value="music">
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-music-note"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Music</span>
|
||||
<span class="visibility-desc">Music video or song</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option-modal">
|
||||
<input type="radio" name="type" value="match">
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-trophy"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Match</span>
|
||||
<span class="visibility-desc">Sports match or competition</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Privacy</label>
|
||||
<div class="visibility-options">
|
||||
<label class="visibility-option active">
|
||||
<input type="radio" name="visibility" value="public" checked>
|
||||
<div class="visibility-content">
|
||||
<i class="bi bi-globe"></i>
|
||||
<span class="visibility-title">Public</span>
|
||||
<span class="visibility-desc">Everyone can see this video</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="visibility" value="unlisted">
|
||||
<div class="visibility-content">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
<span class="visibility-title">Unlisted</span>
|
||||
<span class="visibility-desc">Only people with the link can see</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="visibility" value="private">
|
||||
<div class="visibility-content">
|
||||
<i class="bi bi-lock"></i>
|
||||
<span class="visibility-title">Private</span>
|
||||
<span class="visibility-desc">Only you can see this video</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-shield-lock"></i> Privacy Setting
|
||||
</label>
|
||||
<div class="visibility-options-modal" id="visibility-options">
|
||||
<label class="visibility-option-modal active">
|
||||
<input type="radio" name="visibility" value="public" checked>
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-globe"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Public</span>
|
||||
<span class="visibility-desc">Everyone can see this video</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option-modal">
|
||||
<input type="radio" name="visibility" value="unlisted">
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Unlisted</span>
|
||||
<span class="visibility-desc">Only people with the link</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option-modal">
|
||||
<input type="radio" name="visibility" value="private">
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-lock"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Private</span>
|
||||
<span class="visibility-desc">Only you can see</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div id="progress-container">
|
||||
<div class="progress-bar-wrapper">
|
||||
<div id="progress-bar" class="progress-bar-fill"></div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div id="progress-container">
|
||||
<div class="progress-bar-wrapper">
|
||||
<div id="progress-bar" class="progress-bar-fill"></div>
|
||||
</div>
|
||||
<p id="progress-text" class="progress-text">Uploading... 0%</p>
|
||||
</div>
|
||||
<p id="progress-text" class="progress-text">Uploading... 0%</p>
|
||||
</div>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div id="status-message"></div>
|
||||
|
||||
<button type="submit" id="submit-btn" class="btn-submit">
|
||||
Upload Video
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div id="status-message"></div>
|
||||
|
||||
<div class="upload-modal-actions">
|
||||
<a href="{{ route('videos.index') }}" class="btn-cancel">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" id="submit-btn" class="btn-submit">
|
||||
<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop only: modal overlay styles -->
|
||||
<style>
|
||||
@media (min-width: 992px) {
|
||||
.file-preview {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, #e63030 0%, #ff6b6b 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-preview.thumbnail-preview {
|
||||
background: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-preview.thumbnail-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.file-details .filename {
|
||||
color: #e5e5e5;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
margin: 0 0 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.file-details p {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-remove-file {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #333;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-remove-file:hover {
|
||||
background: #e63030;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#file-info, #thumbnail-info {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #1f1f1f;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
#file-info.active, #thumbnail-info.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#dropzone, #thumbnail-dropzone {
|
||||
padding: 0;
|
||||
border: 2px dashed #404040;
|
||||
}
|
||||
|
||||
#dropzone-default, #thumbnail-default {
|
||||
padding: 32px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@@ -386,12 +907,14 @@
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const fileInput = document.getElementById('video');
|
||||
|
||||
// Add change event listener for video file selection
|
||||
fileInput.addEventListener('change', function() {
|
||||
handleFileSelect(this);
|
||||
});
|
||||
|
||||
dropzone.addEventListener('click', () => fileInput.click());
|
||||
dropzone.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('btn-remove-file')) return;
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
@@ -414,9 +937,8 @@
|
||||
function handleFileSelect(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
const maxSize = 512 * 1024 * 1024; // 512MB in bytes
|
||||
const maxSize = 512 * 1024 * 1024;
|
||||
|
||||
// Validate file type
|
||||
const validTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-flv', 'video/x-matroska'];
|
||||
const validExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv'];
|
||||
|
||||
@@ -429,16 +951,12 @@
|
||||
if (!isValidType) {
|
||||
alert('Invalid video format. Please select a valid video file (MP4, MOV, AVI, WebM, OGG, WMV, FLV, MKV).');
|
||||
input.value = '';
|
||||
document.getElementById('dropzone-default').style.display = 'block';
|
||||
document.getElementById('file-info').classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
alert('File size exceeds 512MB limit. Please select a smaller video file.');
|
||||
input.value = ''; // Clear the input
|
||||
document.getElementById('dropzone-default').style.display = 'block';
|
||||
document.getElementById('file-info').classList.remove('active');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -449,11 +967,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
function removeVideo(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
fileInput.value = '';
|
||||
document.getElementById('dropzone-default').style.display = 'block';
|
||||
document.getElementById('file-info').classList.remove('active');
|
||||
}
|
||||
|
||||
// Thumbnail Dropzone
|
||||
const thumbnailDropzone = document.getElementById('thumbnail-dropzone');
|
||||
const thumbnailInput = document.getElementById('thumbnail');
|
||||
|
||||
thumbnailDropzone.addEventListener('click', () => thumbnailInput.click());
|
||||
thumbnailDropzone.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('btn-remove-file')) return;
|
||||
thumbnailInput.click();
|
||||
});
|
||||
|
||||
thumbnailDropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
@@ -482,13 +1011,37 @@
|
||||
const file = input.files[0];
|
||||
document.getElementById('thumbnail-filename').textContent = file.name;
|
||||
document.getElementById('thumbnail-filesize').textContent = (file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('thumbnail-preview-img').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
document.getElementById('thumbnail-default').style.display = 'none';
|
||||
document.getElementById('thumbnail-info').classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function removeThumbnail(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
thumbnailInput.value = '';
|
||||
document.getElementById('thumbnail-default').style.display = 'block';
|
||||
document.getElementById('thumbnail-info').classList.remove('active');
|
||||
}
|
||||
|
||||
// Visibility option handling
|
||||
const visibilityOptions = document.querySelectorAll('.visibility-option');
|
||||
const typeOptions = document.querySelectorAll('#type-options .visibility-option-modal');
|
||||
typeOptions.forEach(option => {
|
||||
const radio = option.querySelector('input[type="radio"]');
|
||||
radio.addEventListener('change', function() {
|
||||
typeOptions.forEach(opt => opt.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const visibilityOptions = document.querySelectorAll('#visibility-options .visibility-option-modal');
|
||||
visibilityOptions.forEach(option => {
|
||||
const radio = option.querySelector('input[type="radio"]');
|
||||
radio.addEventListener('change', function() {
|
||||
@@ -505,7 +1058,7 @@
|
||||
|
||||
document.getElementById('progress-container').classList.add('active');
|
||||
document.getElementById('submit-btn').disabled = true;
|
||||
document.getElementById('submit-btn').textContent = 'Uploading...';
|
||||
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading...';
|
||||
document.getElementById('status-message').className = '';
|
||||
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
@@ -544,11 +1097,10 @@
|
||||
|
||||
function showError(message) {
|
||||
const status = document.getElementById('status-message');
|
||||
status.textContent = message;
|
||||
status.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
|
||||
status.className = 'error';
|
||||
document.getElementById('submit-btn').disabled = false;
|
||||
document.getElementById('submit-btn').textContent = 'Upload Video';
|
||||
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video';
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -401,7 +401,7 @@
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('body_class', 'edit-page-only')
|
||||
@section('body_class', 'edit-page-only edit-page-responsive')
|
||||
|
||||
@section('content')
|
||||
<div class="edit-modal-standalone">
|
||||
@@ -473,12 +473,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Type -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-collection-play"></i> Video Type
|
||||
</label>
|
||||
<div class="visibility-options-modal" id="type-options-modal">
|
||||
<label class="visibility-option-modal {{ ($video->type ?? 'generic') == 'generic' ? 'active' : '' }}">
|
||||
<input type="radio" name="type" value="generic" {{ ($video->type ?? 'generic') == 'generic' ? 'checked' : '' }}>
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-film"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Generic</span>
|
||||
<span class="visibility-desc">Standard video</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option-modal {{ $video->type == 'music' ? 'active' : '' }}">
|
||||
<input type="radio" name="type" value="music" {{ $video->type == 'music' ? 'checked' : '' }}>
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-music-note"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Music</span>
|
||||
<span class="visibility-desc">Music video or song</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="visibility-option-modal {{ $video->type == 'match' ? 'active' : '' }}">
|
||||
<input type="radio" name="type" value="match" {{ $video->type == 'match' ? 'checked' : '' }}>
|
||||
<div class="visibility-content-modal">
|
||||
<i class="bi bi-trophy"></i>
|
||||
<div class="visibility-text">
|
||||
<span class="visibility-title">Match</span>
|
||||
<span class="visibility-desc">Sports match or competition</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-shield-lock"></i> Privacy Setting
|
||||
</label>
|
||||
<div class="visibility-options-modal">
|
||||
<div class="visibility-options-modal" id="visibility-options-modal">
|
||||
<label class="visibility-option-modal {{ ($video->visibility ?? 'public') == 'public' ? 'active' : '' }}">
|
||||
<input type="radio" name="visibility" value="public" {{ ($video->visibility ?? 'public') == 'public' ? 'checked' : '' }}>
|
||||
<div class="visibility-content-modal">
|
||||
@@ -541,7 +580,16 @@
|
||||
@section('scripts')
|
||||
<script>
|
||||
// Visibility option handling
|
||||
const visibilityOptions = document.querySelectorAll('.visibility-option-modal');
|
||||
const typeOptionsModal = document.querySelectorAll('#type-options-modal .visibility-option-modal');
|
||||
typeOptionsModal.forEach(option => {
|
||||
const radio = option.querySelector('input[type="radio"]');
|
||||
radio.addEventListener('change', function() {
|
||||
typeOptionsModal.forEach(opt => opt.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const visibilityOptions = document.querySelectorAll('#visibility-options-modal .visibility-option-modal');
|
||||
visibilityOptions.forEach(option => {
|
||||
const radio = option.querySelector('input[type="radio"]');
|
||||
radio.addEventListener('change', function() {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<div class="comment-item" style="display: flex; gap: 12px; margin-bottom: 16px;" id="comment-{{ $comment->id }}">
|
||||
<img src="{{ $comment->user->avatar_url }}" class="channel-avatar" style="width: 36px; height: 36px; flex-shrink: 0;" alt="{{ $comment->user->name }}">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
|
||||
<span style="font-weight: 600; font-size: 14px;">{{ $comment->user->name }}</span>
|
||||
<span style="color: var(--text-secondary); font-size: 12px;">{{ $comment->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
<div class="comment-body" style="font-size: 14px; line-height: 1.5; word-wrap: break-word;">
|
||||
{{ $comment->body }}
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; margin-top: 8px;">
|
||||
@auth
|
||||
<button onclick="toggleReplyForm({{ $comment->id }})" style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
|
||||
Reply
|
||||
</button>
|
||||
@if(Auth::id() === $comment->user_id)
|
||||
<button onclick="deleteComment({{ $comment->id }})" style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
|
||||
Delete
|
||||
</button>
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<!-- Reply Form -->
|
||||
<div id="replyForm{{ $comment->id }}" style="display: none; margin-top: 12px;">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<textarea
|
||||
class="form-control"
|
||||
placeholder="Write a reply..."
|
||||
rows="2"
|
||||
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 8px; width: 100%; resize: none; font-size: 14px;"
|
||||
></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
|
||||
<button type="button" class="yt-action-btn" style="font-size: 12px; padding: 6px 12px;" onclick="toggleReplyForm({{ $comment->id }})">Cancel</button>
|
||||
<button type="button" class="yt-action-btn" style="background: var(--brand-red); color: white; font-size: 12px; padding: 6px 12px;" onclick="submitReply({{ $video->id ?? $comment->video_id }}, {{ $comment->id }})">Reply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replies -->
|
||||
@if($comment->replies && $comment->replies->count() > 0)
|
||||
<div style="margin-left: 24px; margin-top: 12px; border-left: 2px solid var(--border-color); padding-left: 12px;">
|
||||
@foreach($comment->replies as $reply)
|
||||
@include('videos.partials.comment', ['comment' => $reply, 'video' => $video ?? null])
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleReplyForm(commentId) {
|
||||
const form = document.getElementById('replyForm' + commentId);
|
||||
form.style.display = form.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,291 @@
|
||||
{{-- Video Type Icon (using Bootstrap Icons like video-card.blade.php) --}}
|
||||
@php
|
||||
$typeIcon = match($video->type) {
|
||||
'music' => 'bi-music-note',
|
||||
'match' => 'bi-trophy',
|
||||
default => 'bi-film',
|
||||
};
|
||||
@endphp
|
||||
|
||||
{{-- Video Title with Type Icon Inline --}}
|
||||
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
|
||||
<i class="bi {{ $typeIcon }}" style="color: #ef4444; margin-right: 6px;"></i>
|
||||
<span>{{ $video->title }}</span>
|
||||
</h1>
|
||||
|
||||
{{-- Channel Row with Actions Inline --}}
|
||||
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 6px 0; flex-wrap: wrap; gap: 12px;">
|
||||
{{-- Left: Channel Info --}}
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" style="width: 36px; height: 36px;" alt="{{ $video->user->name }}">
|
||||
@else
|
||||
<div class="channel-avatar" style="width: 36px; height: 36px;"></div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="channel-name">{{ $video->user->name ?? 'Unknown' }}</div>
|
||||
<div class="channel-subs">{{ number_format($video->user->subscriber_count ?? 0) }} subscribers</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{{-- Subscribe Button --}}
|
||||
@auth
|
||||
@if(Auth::id() !== $video->user_id)
|
||||
<button class="subscribe-btn" style="background: white; color: black; border: none; padding: 8px 16px; border-radius: 18px; font-weight: 600; font-size: 14px; cursor: pointer; white-space: nowrap;">
|
||||
Subscribe
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="subscribe-btn" style="background: white; color: black; border: none; padding: 8px 16px; border-radius: 18px; font-weight: 600; font-size: 14px; text-decoration: none; white-space: nowrap; display: inline-block;">
|
||||
Subscribe
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
{{-- Right: Action Buttons (Like, Edit, Share) --}}
|
||||
<div class="video-actions">
|
||||
@auth
|
||||
{{-- Like Button with Icon and Count --}}
|
||||
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{-- Edit Button - Only for video owner --}}
|
||||
@if(Auth::id() === $video->user_id)
|
||||
<button class="yt-action-btn" onclick="openEditVideoModal({{ $video->id }})">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="yt-action-btn">
|
||||
<i class="bi bi-hand-thumbs-up"></i> Like
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
{{-- Share Button --}}
|
||||
@if($video->isShareable())
|
||||
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Description Box (Expandable) --}}
|
||||
@if($video->description)
|
||||
@php
|
||||
// Parse markdown description
|
||||
$fullDescription = $video->description;
|
||||
$shortDescription = Str::limit($fullDescription, 200);
|
||||
$needsExpand = strlen($fullDescription) > 200;
|
||||
@endphp
|
||||
|
||||
<div class="video-description-box" style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-bottom: 16px;">
|
||||
{{-- Fixed Stats Row (views + date - cannot be manipulated) --}}
|
||||
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Separator Line --}}
|
||||
<div style="border-bottom: 1px solid var(--border-color); margin: 8px 0;"></div>
|
||||
|
||||
{{-- Description Content --}}
|
||||
<div class="description-content" id="descriptionContent">
|
||||
@if($needsExpand)
|
||||
<div class="description-short" id="descShort">
|
||||
<span class="description-text">{!! Str::markdown($shortDescription) !!}</span>
|
||||
<span class="description-ellipsis" style="color: var(--text-secondary);">... </span>
|
||||
</div>
|
||||
<div class="description-full" id="descFull" style="display: none;">
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
</div>
|
||||
<button onclick="toggleDescription()" id="descToggleBtn" style="background: none; border: none; color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; padding: 0; margin-top: 4px;">
|
||||
Show more
|
||||
</button>
|
||||
@else
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDescription() {
|
||||
const descShort = document.getElementById('descShort');
|
||||
const descFull = document.getElementById('descFull');
|
||||
const toggleBtn = document.getElementById('descToggleBtn');
|
||||
|
||||
if (descShort.style.display !== 'none') {
|
||||
descShort.style.display = 'none';
|
||||
descFull.style.display = 'block';
|
||||
toggleBtn.textContent = 'Show less';
|
||||
} else {
|
||||
descShort.style.display = 'block';
|
||||
descFull.style.display = 'none';
|
||||
toggleBtn.textContent = 'Show more';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.video-description-box .description-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.video-description-box .description-text p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.video-description-box .description-text p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.video-description-box .description-text a {
|
||||
color: #3ea6ff;
|
||||
}
|
||||
.video-description-box .description-text code {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.video-description-box .description-text pre {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.video-description-box .description-text ul,
|
||||
.video-description-box .description-text ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.video-description-box .description-text strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@endif
|
||||
|
||||
{{-- Comment Section --}}
|
||||
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
|
||||
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
|
||||
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
|
||||
</h3>
|
||||
|
||||
{{-- Comment Form --}}
|
||||
@auth
|
||||
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
|
||||
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
|
||||
<div style="flex: 1;">
|
||||
<textarea
|
||||
id="commentBody"
|
||||
class="form-control"
|
||||
placeholder="Add a comment... Use @ to mention someone"
|
||||
rows="3"
|
||||
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
|
||||
></textarea>
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
|
||||
<button type="button" class="yt-action-btn" onclick="document.getElementById('commentBody').value = ''">Cancel</button>
|
||||
<button type="button" class="yt-action-btn" style="background: var(--brand-red); color: white;" onclick="submitComment({{ $video->id }})">Comment</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div style="margin-bottom: 24px; padding: 16px; background: var(--bg-secondary); border-radius: 8px; text-align: center;">
|
||||
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
{{-- Comments List --}}
|
||||
<div id="commentsList">
|
||||
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
|
||||
@include('videos.partials.comment', ['comment' => $comment])
|
||||
@empty
|
||||
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the first to comment!</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitComment(videoId) {
|
||||
const body = document.getElementById('commentBody').value.trim();
|
||||
if (!body) return;
|
||||
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ body: body })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('commentBody').value = '';
|
||||
loadComments(videoId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadComments(videoId) {
|
||||
fetch(`/videos/${videoId}/comments`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Reload page to show new comments
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteComment(commentId) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
fetch(`/comments/${commentId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Highlight @mentions in comments
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const commentTexts = document.querySelectorAll('.comment-body');
|
||||
commentTexts.forEach(text => {
|
||||
const html = text.innerHTML.replace(/@(\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
|
||||
text.innerHTML = html;
|
||||
});
|
||||
});
|
||||
|
||||
function submitReply(videoId, parentId) {
|
||||
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
|
||||
const body = textarea.value.trim();
|
||||
if (!body) return;
|
||||
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ body: body, parent_id: parentId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -123,13 +123,15 @@
|
||||
}
|
||||
|
||||
.subscribe-btn {
|
||||
background: var(--brand-red);
|
||||
color: white;
|
||||
background: white;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border-radius: 18px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Description */
|
||||
@@ -259,19 +261,26 @@
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- Video Title -->
|
||||
<h1 class="video-title">{{ $video->title }}</h1>
|
||||
@php
|
||||
$typeIcon = match($video->type) {
|
||||
'music' => 'bi-music-note',
|
||||
'match' => 'bi-trophy',
|
||||
default => 'bi-film',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<!-- Video Title with Type Icon -->
|
||||
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi {{ $typeIcon }}" style="color: #ef4444;"></i>
|
||||
<span>{{ $video->title }}</span>
|
||||
</h1>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="video-stats-row">
|
||||
<div class="video-stats-left">
|
||||
<span>{{ number_format($video->size / 1024 / 1024, 0) }} MB</span>
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
@if($video->width && $video->height)
|
||||
<span>•</span>
|
||||
<span>{{ $video->width }}x{{ $video->height }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="video-actions">
|
||||
@auth
|
||||
@@ -280,7 +289,7 @@
|
||||
@csrf
|
||||
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? $video->like_count : 'Like' }}
|
||||
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -302,32 +311,156 @@
|
||||
</div>
|
||||
|
||||
<!-- Channel Row -->
|
||||
<div class="channel-row">
|
||||
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" alt="{{ $video->user->name }}">
|
||||
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" style="width: 36px; height: 36px;" alt="{{ $video->user->name }}">
|
||||
@else
|
||||
<div class="channel-avatar" style="width: 36px; height: 36px;"></div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="channel-name">{{ $video->user->name ?? 'Unknown' }}</div>
|
||||
<div class="channel-subs">{{ number_format($video->user->subscriber_count ?? 0) }} subscribers</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{{-- Subscribe Button --}}
|
||||
@auth
|
||||
@if(Auth::id() !== $video->user_id)
|
||||
<button class="subscribe-btn">Subscribe</button>
|
||||
@endif
|
||||
@else
|
||||
<div class="channel-avatar"></div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="channel-name">{{ $video->user->name ?? 'Unknown' }}</div>
|
||||
<div class="channel-subs">Video Creator</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
@if($video->description)
|
||||
<div class="video-description">
|
||||
<p class="description-text">{{ $video->description }}</p>
|
||||
@php
|
||||
$fullDescription = $video->description;
|
||||
$shortDescription = Str::limit($fullDescription, 200);
|
||||
$needsExpand = strlen($fullDescription) > 200;
|
||||
@endphp
|
||||
|
||||
<div class="video-description-box" style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-top: 12px;">
|
||||
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
</div>
|
||||
<div style="border-bottom: 1px solid var(--border-color); margin: 8px 0;"></div>
|
||||
<div class="description-content">
|
||||
@if($needsExpand)
|
||||
<div class="description-short">
|
||||
<span class="description-text">{!! Str::markdown($shortDescription) !!}</span>
|
||||
<span style="color: var(--text-secondary);">... </span>
|
||||
</div>
|
||||
<div class="description-full" style="display: none;">
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
</div>
|
||||
<button onclick="toggleDescription()" style="background: none; border: none; color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; padding: 0; margin-top: 4px;">Show more</button>
|
||||
@else
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDescription() {
|
||||
const short = document.querySelector('.description-short');
|
||||
const full = document.querySelector('.description-full');
|
||||
const btn = document.querySelector('.video-description-box button');
|
||||
if (short.style.display !== 'none') {
|
||||
short.style.display = 'none';
|
||||
full.style.display = 'block';
|
||||
btn.textContent = 'Show less';
|
||||
} else {
|
||||
short.style.display = 'block';
|
||||
full.style.display = 'none';
|
||||
btn.textContent = 'Show more';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
|
||||
.video-description-box .description-text p { margin-bottom: 8px; }
|
||||
.video-description-box .description-text a { color: #3ea6ff; }
|
||||
</style>
|
||||
@endif
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="comments-section" style="margin-top: 24px; padding-top: 16: 1px solid var(--borderpx; border-top-color);">
|
||||
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
|
||||
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
|
||||
</h3>
|
||||
|
||||
@auth
|
||||
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
|
||||
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
|
||||
<div style="flex: 1;">
|
||||
<textarea id="commentBody" class="form-control" placeholder="Add a comment... Use @ to mention someone" rows="3" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"></textarea>
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
|
||||
<button type="button" class="yt-action-btn" onclick="document.getElementById('commentBody').value = ''">Cancel</button>
|
||||
<button type="button" class="yt-action-btn" style="background: var(--brand-red); color: white;" onclick="submitComment({{ $video->id }})">Comment</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div style="margin-bottom: 24px; padding: 16px; background: var(--bg-secondary); border-radius: 8px; text-align: center;">
|
||||
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<div id="commentsList">
|
||||
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
|
||||
@include('videos.partials.comment', ['comment' => $comment])
|
||||
@empty
|
||||
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the first to comment!</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitComment(videoId) {
|
||||
const body = document.getElementById('commentBody').value.trim();
|
||||
if (!body) return;
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
body: JSON.stringify({ body: body })
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.success) { document.getElementById('commentBody').value = ''; location.reload(); }
|
||||
});
|
||||
}
|
||||
function deleteComment(commentId) {
|
||||
if (!confirm('Delete this comment?')) return;
|
||||
fetch(`/comments/${commentId}`, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }})
|
||||
.then(r => r.json()).then(data => { if (data.success) location.reload(); });
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.comment-body').forEach(text => {
|
||||
text.innerHTML = text.innerHTML.replace(/@(\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
|
||||
});
|
||||
});
|
||||
function submitReply(videoId, parentId) {
|
||||
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
|
||||
const body = textarea.value.trim();
|
||||
if (!body) return;
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
body: JSON.stringify({ body: body, parent_id: parentId })
|
||||
}).then(r => r.json()).then(data => { if (data.success) location.reload(); });
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="yt-sidebar-container">
|
||||
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
|
||||
<!-- Placeholder for recommended videos - would be dynamic in full implementation -->
|
||||
<div class="text-secondary">More videos coming soon...</div>
|
||||
</div>
|
||||
|
||||
@@ -340,7 +473,6 @@
|
||||
@auth
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-open edit modal when redirected from /videos/{id}/edit
|
||||
openEditVideoModal({{ $video->id }});
|
||||
});
|
||||
</script>
|
||||
@@ -348,24 +480,13 @@
|
||||
@endif
|
||||
|
||||
<script>
|
||||
// Auto-play video with sound when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var videoPlayer = document.getElementById('videoPlayer');
|
||||
if (videoPlayer) {
|
||||
// Set volume to 50%
|
||||
videoPlayer.volume = 0.5;
|
||||
|
||||
// Try to autoplay with sound
|
||||
var playPromise = videoPlayer.play();
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(function() {
|
||||
// Autoplay started successfully
|
||||
console.log('Video autoplayed with sound at 50% volume');
|
||||
}).catch(function(error) {
|
||||
// Autoplay was prevented
|
||||
console.log('Autoplay blocked');
|
||||
});
|
||||
playPromise.then(function() { console.log('Video autoplayed'); }).catch(function(error) { console.log('Autoplay blocked'); });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,123 +1,511 @@
|
||||
<!-- Video Layout Container -->
|
||||
<div class="video-layout-container" style="display: flex; gap: 24px; max-width: 1800px; margin: 0 auto;">
|
||||
@extends('layouts.app')
|
||||
|
||||
<!-- Video Section -->
|
||||
<div class="yt-video-section">
|
||||
<!-- Video Player -->
|
||||
<div class="video-container @if($video->orientation === 'portrait') portrait @elseif($video->orientation === 'square') square @elseif($video->orientation === 'ultrawide') ultrawide @endif" id="videoContainer">
|
||||
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
|
||||
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- Video Title -->
|
||||
<h1 class="video-title">{{ $video->title }}</h1>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="video-stats-row">
|
||||
<div class="video-stats-left">
|
||||
<span>{{ number_format($video->size / 1024 / 1024, 0) }} MB</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
@if($video->width && $video->height)
|
||||
<span>•</span>
|
||||
<span>{{ $video->width }}x{{ $video->height }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="video-actions">
|
||||
@auth
|
||||
<!-- Like Button -->
|
||||
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? $video->like_count : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Edit Button - Only for video owner -->
|
||||
@if(Auth::id() === $video->user_id)
|
||||
<button class="yt-action-btn" onclick="openEditVideoModal({{ $video->id }})">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="yt-action-btn">
|
||||
<i class="bi bi-hand-thumbs-up"></i> Like
|
||||
</a>
|
||||
@endauth
|
||||
@if($video->isShareable())
|
||||
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')"><i class="bi bi-share"></i> Share</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel Row -->
|
||||
<div class="channel-row">
|
||||
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" alt="{{ $video->user->name }}">
|
||||
@else
|
||||
<div class="channel-avatar"></div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="channel-name">{{ $video->user->name ?? 'Unknown' }}</div>
|
||||
<div class="channel-subs">Video Creator</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
@if($video->description)
|
||||
<div class="video-description">
|
||||
<p class="description-text">{{ $video->description }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="yt-sidebar-container">
|
||||
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
|
||||
<!-- Placeholder for recommended videos - would be dynamic in full implementation -->
|
||||
<div class="text-secondary">More videos coming soon...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@section('title', $video->title . ' | ' . config('app.name'))
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
|
||||
@if(Session::has('openEditModal') && Session::get('openEditModal'))
|
||||
@auth
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-open edit modal when redirected from /videos/{id}/edit
|
||||
openEditVideoModal({{ $video->id }});
|
||||
});
|
||||
</script>
|
||||
@endauth
|
||||
@endif
|
||||
|
||||
<script>
|
||||
// Auto-play video with sound when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var videoPlayer = document.getElementById('videoPlayer');
|
||||
if (videoPlayer) {
|
||||
// Set volume to 50%
|
||||
videoPlayer.volume = 0.5;
|
||||
@section('extra_styles')
|
||||
<style>
|
||||
/* Video Section */
|
||||
.yt-video-section { flex: 1; min-width: 0; }
|
||||
|
||||
/* Video Player */
|
||||
.video-container {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-container.portrait,
|
||||
.video-container.square,
|
||||
.video-container.ultrawide {
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.video-container.portrait { aspect-ratio: 9/16; max-width: 50vh; }
|
||||
.video-container.square { aspect-ratio: 1/1; max-width: 70vh; }
|
||||
.video-container.ultrawide { aspect-ratio: 21/9; max-width: 100%; }
|
||||
|
||||
.video-container video { width: 100%; height: 100%; object-fit: contain; }
|
||||
|
||||
/* Video Info */
|
||||
.video-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin: 16px 0 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.video-stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.video-stats-left { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); }
|
||||
|
||||
.video-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.yt-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.yt-action-btn:hover { background: var(--border-color); }
|
||||
|
||||
.yt-action-btn.liked { color: var(--brand-red); }
|
||||
|
||||
/* Channel Row */
|
||||
.channel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.channel-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.channel-avatar {
|
||||
width: 48px; height: 48px; border-radius: 50%; background: #555;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channel-subs {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.subscribe-btn {
|
||||
background: white;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 18px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.video-description {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.yt-sidebar-container {
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-video-card {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-thumb {
|
||||
width: 168px;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.sidebar-info { flex: 1; min-width: 0; }
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-meta { font-size: 12px; color: var(--text-secondary); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1300px) {
|
||||
.yt-sidebar-container { width: 300px; }
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.yt-main { margin-left: 0; flex-direction: column; }
|
||||
.yt-sidebar-container { width: 100%; }
|
||||
.yt-header-center { display: none; }
|
||||
.sidebar-video-card { flex-direction: column; }
|
||||
.sidebar-thumb { width: 100%; }
|
||||
|
||||
// Try to autoplay with sound
|
||||
var playPromise = videoPlayer.play();
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(function() {
|
||||
// Autoplay started successfully
|
||||
console.log('Video autoplayed with sound at 50% volume');
|
||||
}).catch(function(error) {
|
||||
// Autoplay was prevented
|
||||
console.log('Autoplay blocked');
|
||||
});
|
||||
.video-layout-container {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
.yt-video-section {
|
||||
width: 100% !important;
|
||||
flex: none !important;
|
||||
}
|
||||
.yt-sidebar-container {
|
||||
width: 100% !important;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.video-stats-row { flex-direction: column; align-items: flex-start; }
|
||||
.video-actions { width: 100%; overflow-x: auto; justify-content: flex-start; }
|
||||
.yt-main { padding: 12px !important; }
|
||||
|
||||
.video-container {
|
||||
max-height: 50vh !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.video-container video {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
.video-title {
|
||||
font-size: 16px !important;
|
||||
margin: 12px 0 6px !important;
|
||||
}
|
||||
.channel-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 12px;
|
||||
}
|
||||
.channel-info {
|
||||
width: 100%;
|
||||
}
|
||||
.subscribe-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.video-description {
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
<!-- Video Layout Container -->
|
||||
<div class="video-layout-container" style="display: flex; gap: 24px; max-width: 1800px; margin: 0 auto;">
|
||||
|
||||
<!-- Video Section -->
|
||||
<div class="yt-video-section">
|
||||
<!-- Video Player -->
|
||||
<div class="video-container @if($video->orientation === 'portrait') portrait @elseif($video->orientation === 'square') square @elseif($video->orientation === 'ultrawide') ultrawide @endif" id="videoContainer">
|
||||
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
|
||||
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- Video Title with Film Icon (Generic Type) -->
|
||||
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
|
||||
<i class="bi bi-film" style="color: #ef4444;"></i>
|
||||
<span>{{ $video->title }}</span>
|
||||
</h1>
|
||||
|
||||
<!-- Stats Row - Hidden, shown in description box -->
|
||||
<div class="video-stats-row" style="display: none;">
|
||||
<div class="video-stats-left">
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel Row - All in one line -->
|
||||
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 0; flex-wrap: wrap; gap: 16px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px; border-radius: 50%;" alt="{{ $video->user->name }}">
|
||||
@else
|
||||
<div class="channel-avatar" style="width: 40px; height: 40px; border-radius: 50%;"></div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="channel-name" style="font-weight: 600;">{{ $video->user->name ?? 'Unknown' }}</div>
|
||||
<div class="channel-subs" style="font-size: 12px; color: var(--text-secondary);">{{ number_format($video->user->subscriber_count ?? 0) }} subscribers</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
@auth
|
||||
@if(Auth::id() !== $video->user_id)
|
||||
<button class="subscribe-btn">Subscribe</button>
|
||||
@else
|
||||
<button class="yt-action-btn" onclick="openEditVideoModal({{ $video->id }})">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
|
||||
@endauth
|
||||
|
||||
@auth
|
||||
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="yt-action-btn">
|
||||
<i class="bi bi-hand-thumbs-up"></i> Like
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
@if($video->isShareable())
|
||||
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Box -->
|
||||
@if($video->description)
|
||||
@php
|
||||
$fullDescription = $video->description;
|
||||
$shortDescription = Str::limit($fullDescription, 200);
|
||||
$needsExpand = strlen($fullDescription) > 200;
|
||||
@endphp
|
||||
|
||||
<div class="video-description-box" style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-bottom: 16px;">
|
||||
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
</div>
|
||||
<div style="border-bottom: 1px solid var(--border-color); margin: 8px 0;"></div>
|
||||
<div class="description-content" id="descriptionContent">
|
||||
@if($needsExpand)
|
||||
<div class="description-short" id="descShort">
|
||||
<span class="description-text">{!! Str::markdown($shortDescription) !!}</span>
|
||||
<span class="description-ellipsis" style="color: var(--text-secondary);">... </span>
|
||||
</div>
|
||||
<div class="description-full" id="descFull" style="display: none;">
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
</div>
|
||||
<button onclick="toggleDescription()" id="descToggleBtn" style="background: none; border: none; color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; padding: 0; margin-top: 4px;">
|
||||
Show more
|
||||
</button>
|
||||
@else
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDescription() {
|
||||
const descShort = document.getElementById('descShort');
|
||||
const descFull = document.getElementById('descFull');
|
||||
const toggleBtn = document.getElementById('descToggleBtn');
|
||||
|
||||
if (descShort.style.display !== 'none') {
|
||||
descShort.style.display = 'none';
|
||||
descFull.style.display = 'block';
|
||||
toggleBtn.textContent = 'Show less';
|
||||
} else {
|
||||
descShort.style.display = 'block';
|
||||
descFull.style.display = 'none';
|
||||
toggleBtn.textContent = 'Show more';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
|
||||
.video-description-box .description-text p { margin-bottom: 8px; }
|
||||
.video-description-box .description-text a { color: #3ea6ff; }
|
||||
</style>
|
||||
@endif
|
||||
|
||||
<!-- Comment Section -->
|
||||
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
|
||||
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
|
||||
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
|
||||
</h3>
|
||||
|
||||
@auth
|
||||
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
|
||||
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
|
||||
<div style="flex: 1;">
|
||||
<textarea
|
||||
id="commentBody"
|
||||
class="form-control"
|
||||
placeholder="Add a comment... Use @ to mention someone"
|
||||
rows="3"
|
||||
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
|
||||
></textarea>
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
|
||||
<button type="button" class="yt-action-btn" onclick="document.getElementById('commentBody').value = ''">Cancel</button>
|
||||
<button type="button" class="yt-action-btn" style="background: var(--brand-red); color: white;" onclick="submitComment({{ $video->id }})">Comment</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div style="margin-bottom: 24px; padding: 16px; background: var(--bg-secondary); border-radius: 8px; text-align: center;">
|
||||
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<div id="commentsList">
|
||||
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
|
||||
@include('videos.partials.comment', ['comment' => $comment])
|
||||
@empty
|
||||
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the first to comment!</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitComment(videoId) {
|
||||
const body = document.getElementById('commentBody').value.trim();
|
||||
if (!body) return;
|
||||
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ body: body })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('commentBody').value = '';
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteComment(commentId) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
fetch(`/comments/${commentId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const commentTexts = document.querySelectorAll('.comment-body');
|
||||
commentTexts.forEach(text => {
|
||||
const html = text.innerHTML.replace(/@(\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
|
||||
text.innerHTML = html;
|
||||
});
|
||||
});
|
||||
|
||||
function submitReply(videoId, parentId) {
|
||||
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
|
||||
const body = textarea.value.trim();
|
||||
if (!body) return;
|
||||
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ body: body, parent_id: parentId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="yt-sidebar-container">
|
||||
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
|
||||
<div class="text-secondary">More videos coming soon...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
|
||||
@if(Session::has('openEditModal') && Session::get('openEditModal'))
|
||||
@auth
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
openEditVideoModal({{ $video->id }});
|
||||
});
|
||||
</script>
|
||||
@endauth
|
||||
@endif
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var videoPlayer = document.getElementById('videoPlayer');
|
||||
if (videoPlayer) {
|
||||
videoPlayer.volume = 0.5;
|
||||
var playPromise = videoPlayer.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(function() { console.log('Video autoplayed'); }).catch(function(error) { console.log('Autoplay blocked'); });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,511 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $video->title . ' | ' . config('app.name'))
|
||||
|
||||
@section('extra_styles')
|
||||
<style>
|
||||
/* Video Section */
|
||||
.yt-video-section { flex: 1; min-width: 0; }
|
||||
|
||||
/* Video Player */
|
||||
.video-container {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-container.portrait,
|
||||
.video-container.square,
|
||||
.video-container.ultrawide {
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.video-container.portrait { aspect-ratio: 9/16; max-width: 50vh; }
|
||||
.video-container.square { aspect-ratio: 1/1; max-width: 70vh; }
|
||||
.video-container.ultrawide { aspect-ratio: 21/9; max-width: 100%; }
|
||||
|
||||
.video-container video { width: 100%; height: 100%; object-fit: contain; }
|
||||
|
||||
/* Video Info */
|
||||
.video-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin: 16px 0 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.video-stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.video-stats-left { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); }
|
||||
|
||||
.video-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.yt-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.yt-action-btn:hover { background: var(--border-color); }
|
||||
|
||||
.yt-action-btn.liked { color: var(--brand-red); }
|
||||
|
||||
/* Channel Row */
|
||||
.channel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.channel-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.channel-avatar {
|
||||
width: 48px; height: 48px; border-radius: 50%; background: #555;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channel-subs {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.subscribe-btn {
|
||||
background: white;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 18px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.video-description {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.yt-sidebar-container {
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-video-card {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-thumb {
|
||||
width: 168px;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.sidebar-info { flex: 1; min-width: 0; }
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-meta { font-size: 12px; color: var(--text-secondary); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1300px) {
|
||||
.yt-sidebar-container { width: 300px; }
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.yt-main { margin-left: 0; flex-direction: column; }
|
||||
.yt-sidebar-container { width: 100%; }
|
||||
.yt-header-center { display: none; }
|
||||
.sidebar-video-card { flex-direction: column; }
|
||||
.sidebar-thumb { width: 100%; }
|
||||
|
||||
.video-layout-container {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
.yt-video-section {
|
||||
width: 100% !important;
|
||||
flex: none !important;
|
||||
}
|
||||
.yt-sidebar-container {
|
||||
width: 100% !important;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.video-stats-row { flex-direction: column; align-items: flex-start; }
|
||||
.video-actions { width: 100%; overflow-x: auto; justify-content: flex-start; }
|
||||
.yt-main { padding: 12px !important; }
|
||||
|
||||
.video-container {
|
||||
max-height: 50vh !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.video-container video {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
.video-title {
|
||||
font-size: 16px !important;
|
||||
margin: 12px 0 6px !important;
|
||||
}
|
||||
.channel-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 12px;
|
||||
}
|
||||
.channel-info {
|
||||
width: 100%;
|
||||
}
|
||||
.subscribe-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.video-description {
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
<!-- Video Layout Container -->
|
||||
<div class="video-layout-container" style="display: flex; gap: 24px; max-width: 1800px; margin: 0 auto;">
|
||||
|
||||
<!-- Video Section -->
|
||||
<div class="yt-video-section">
|
||||
<!-- Video Player -->
|
||||
<div class="video-container @if($video->orientation === 'portrait') portrait @elseif($video->orientation === 'square') square @elseif($video->orientation === 'ultrawide') ultrawide @endif" id="videoContainer">
|
||||
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
|
||||
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- Video Title with Music Note Icon (Music Type) -->
|
||||
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
|
||||
<i class="bi bi-music-note" style="color: #ef4444;"></i>
|
||||
<span>{{ $video->title }}</span>
|
||||
</h1>
|
||||
|
||||
<!-- Stats Row - Hidden, shown in description box -->
|
||||
<div class="video-stats-row" style="display: none;">
|
||||
<div class="video-stats-left">
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel Row - All in one line -->
|
||||
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 0; flex-wrap: wrap; gap: 16px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px; border-radius: 50%;" alt="{{ $video->user->name }}">
|
||||
@else
|
||||
<div class="channel-avatar" style="width: 40px; height: 40px; border-radius: 50%;"></div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="channel-name" style="font-weight: 600;">{{ $video->user->name ?? 'Unknown' }}</div>
|
||||
<div class="channel-subs" style="font-size: 12px; color: var(--text-secondary);">{{ number_format($video->user->subscriber_count ?? 0) }} subscribers</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
@auth
|
||||
@if(Auth::id() !== $video->user_id)
|
||||
<button class="subscribe-btn">Subscribe</button>
|
||||
@else
|
||||
<button class="yt-action-btn" onclick="openEditVideoModal({{ $video->id }})">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
|
||||
@endauth
|
||||
|
||||
@auth
|
||||
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="yt-action-btn">
|
||||
<i class="bi bi-hand-thumbs-up"></i> Like
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
@if($video->isShareable())
|
||||
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Box -->
|
||||
@if($video->description)
|
||||
@php
|
||||
$fullDescription = $video->description;
|
||||
$shortDescription = Str::limit($fullDescription, 200);
|
||||
$needsExpand = strlen($fullDescription) > 200;
|
||||
@endphp
|
||||
|
||||
<div class="video-description-box" style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-bottom: 16px;">
|
||||
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
|
||||
<span>{{ number_format($video->view_count) }} views</span>
|
||||
<span>•</span>
|
||||
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
||||
</div>
|
||||
<div style="border-bottom: 1px solid var(--border-color); margin: 8px 0;"></div>
|
||||
<div class="description-content" id="descriptionContent">
|
||||
@if($needsExpand)
|
||||
<div class="description-short" id="descShort">
|
||||
<span class="description-text">{!! Str::markdown($shortDescription) !!}</span>
|
||||
<span class="description-ellipsis" style="color: var(--text-secondary);">... </span>
|
||||
</div>
|
||||
<div class="description-full" id="descFull" style="display: none;">
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
</div>
|
||||
<button onclick="toggleDescription()" id="descToggleBtn" style="background: none; border: none; color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; padding: 0; margin-top: 4px;">
|
||||
Show more
|
||||
</button>
|
||||
@else
|
||||
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDescription() {
|
||||
const descShort = document.getElementById('descShort');
|
||||
const descFull = document.getElementById('descFull');
|
||||
const toggleBtn = document.getElementById('descToggleBtn');
|
||||
|
||||
if (descShort.style.display !== 'none') {
|
||||
descShort.style.display = 'none';
|
||||
descFull.style.display = 'block';
|
||||
toggleBtn.textContent = 'Show less';
|
||||
} else {
|
||||
descShort.style.display = 'block';
|
||||
descFull.style.display = 'none';
|
||||
toggleBtn.textContent = 'Show more';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
|
||||
.video-description-box .description-text p { margin-bottom: 8px; }
|
||||
.video-description-box .description-text a { color: #3ea6ff; }
|
||||
</style>
|
||||
@endif
|
||||
|
||||
<!-- Comment Section -->
|
||||
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
|
||||
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
|
||||
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
|
||||
</h3>
|
||||
|
||||
@auth
|
||||
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
|
||||
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
|
||||
<div style="flex: 1;">
|
||||
<textarea
|
||||
id="commentBody"
|
||||
class="form-control"
|
||||
placeholder="Add a comment... Use @ to mention someone"
|
||||
rows="3"
|
||||
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
|
||||
></textarea>
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
|
||||
<button type="button" class="yt-action-btn" onclick="document.getElementById('commentBody').value = ''">Cancel</button>
|
||||
<button type="button" class="yt-action-btn" style="background: var(--brand-red); color: white;" onclick="submitComment({{ $video->id }})">Comment</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div style="margin-bottom: 24px; padding: 16px; background: var(--bg-secondary); border-radius: 8px; text-align: center;">
|
||||
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<div id="commentsList">
|
||||
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
|
||||
@include('videos.partials.comment', ['comment' => $comment])
|
||||
@empty
|
||||
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the first to comment!</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitComment(videoId) {
|
||||
const body = document.getElementById('commentBody').value.trim();
|
||||
if (!body) return;
|
||||
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ body: body })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('commentBody').value = '';
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteComment(commentId) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
fetch(`/comments/${commentId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const commentTexts = document.querySelectorAll('.comment-body');
|
||||
commentTexts.forEach(text => {
|
||||
const html = text.innerHTML.replace(/@(\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
|
||||
text.innerHTML = html;
|
||||
});
|
||||
});
|
||||
|
||||
function submitReply(videoId, parentId) {
|
||||
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
|
||||
const body = textarea.value.trim();
|
||||
if (!body) return;
|
||||
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ body: body, parent_id: parentId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="yt-sidebar-container">
|
||||
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
|
||||
<div class="text-secondary">More videos coming soon...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
|
||||
@if(Session::has('openEditModal') && Session::get('openEditModal'))
|
||||
@auth
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
openEditVideoModal({{ $video->id }});
|
||||
});
|
||||
</script>
|
||||
@endauth
|
||||
@endif
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var videoPlayer = document.getElementById('videoPlayer');
|
||||
if (videoPlayer) {
|
||||
videoPlayer.volume = 0.5;
|
||||
var playPromise = videoPlayer.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(function() { console.log('Video autoplayed'); }).catch(function(error) { console.log('Autoplay blocked'); });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
Reference in new issue
Block a user