ghassan 6b43a7060a Fix player UI: sports Highlights pane, scrubber knob, volume knob, description + save-to-playlist
Player/UI fixes:
- Sports (match) Highlights pane: move the toggle click-handler script out of
  the always-false @if(false ...) legacy block so the "Highlights" button opens,
  and emit videoId/isOwner with @json instead of {{ json_encode() }} (escaped
  quotes crashed the data/tab/CRUD block once route keys became hashid strings).
- Progress scrubber knob (video + audio players): center on the track line with
  translate(-50%, -50%); drop the margin-top hack that floated it above the line.
- Music player volume: give the slider wrap height so overflow:hidden no longer
  crops the knob; add a subtle thumb shadow for visibility.
- Music language menu: cap height to available space above the header, sticky
  header, internal scroll; keep controls from auto-hiding while the menu is open.
- Description box: re-check "Show more" overflow at every settle point (fonts,
  images, rAF, tab reopen) so SPA autonext keeps the expand button.

Also bundles pending work: save-to-playlist button component, playlist/channel
views, PlaylistController + NasSyncService, and component-usage tracker updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 22:12:58 +03:00

2017 lines
106 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.app')
@section('title', $user->name . ' — Channel | ' . config('app.name'))
@section('main_class', 'no-pad')
@section('extra_styles')
{{-- Styles are split by viewport so mobile and desktop can evolve
independently. Desktop is the base; mobile overrides via media
queries inside its own file. The $hue / $hue2 / $hue3 palette is
defined here at the parent level so both partials can reference
it each included partial runs in its own variable scope, so
hoisting the PHP block here makes the variables visible to both.
See partials/channel/styles/{desktop,mobile}.blade.php. --}}
@php
$hue = (crc32($user->name) % 360 + 360) % 360;
$hue2 = ($hue + 45) % 360;
$hue3 = ($hue + 200) % 360;
@endphp
@include('user.partials.channel.styles.desktop')
@include('user.partials.channel.styles.mobile')
@endsection
@section('content')
{{-- ─────────────── PREVIEW BANNER ─────────────────── --}}
@if($preview)
<div id="chPreviewBar" style="
position: sticky; top: 56px; z-index: 200;
background: #1a1200; border-bottom: 2px solid #f59e0b;
display: flex; align-items: center; justify-content: space-between;
padding: 10px 24px; gap: 12px;">
<div style="display:flex; align-items:center; gap:10px; color:#fbbf24; font-size:14px; font-weight:600;">
<i class="bi bi-eye-fill" style="font-size:16px;"></i>
You're viewing your profile as others see it
</div>
<a href="{{ route('channel', $user->channel) }}"
style="display:inline-flex; align-items:center; gap:6px; background:#f59e0b; color:#000;
font-size:13px; font-weight:700; padding:6px 14px; border-radius:6px; text-decoration:none;">
<i class="bi bi-arrow-left"></i> Back to my channel
</a>
</div>
@endif
{{-- ───────────────────── BANNER ───────────────────── --}}
<div class="ch-banner" id="chBanner">
@if($user->banner)
<img src="{{ $user->banner_url }}" id="chBannerImg" class="ch-banner-img" alt="">
@endif
<div class="ch-banner-noise"></div>
@if($isOwner && !$preview)
<button class="ch-banner-edit-btn" onclick="openCropperModal_banner()" title="Change banner">
<i class="bi bi-camera-fill"></i>
<span>Change banner</span>
</button>
@else
<div class="ch-banner-actions">
@auth
<button type="button"
class="ch-banner-subscribe-btn subscribe-toggle-btn {{ $isSubscriber ? 'subscribed-ch' : '' }}"
data-channel-id="{{ $user->id }}"
data-subscribe-url="{{ route('channel.subscribe', $user->id) }}"
data-subscribed="{{ $isSubscriber ? 'true' : 'false' }}">
<i class="bi {{ $isSubscriber ? 'bi-bell-fill' : 'bi-person-plus-fill' }}"></i>
<span class="subscribe-label">{{ $isSubscriber ? 'Subscribed' : 'Subscribe' }}</span>
</button>
@else
<a href="{{ route('login') }}" class="ch-banner-subscribe-btn">
<i class="bi bi-person-plus-fill"></i>
Subscribe
</a>
@endauth
<button class="ch-banner-share-btn" title="Share channel"
onclick="navigator.clipboard.writeText(window.location.href).then(()=>showToast('Channel link copied!','success'))">
<i class="bi bi-share"></i>
</button>
</div>
@endif
</div>
{{-- ─────────────────── CHANNEL HEADER ─────────────── --}}
@php
$headerSocialMap = [
'twitter' => ['icon'=>'bi-twitter-x', 'color'=>'#38bdf8', 'bg'=>'rgba(56,189,248,.1)', 'hover'=>'#38bdf8', 'href'=>fn($v)=>"https://twitter.com/{$v}"],
'instagram' => ['icon'=>'bi-instagram', 'color'=>'#818cf8', 'bg'=>'rgba(129,140,248,.1)', 'hover'=>'#818cf8', 'href'=>fn($v)=>"https://instagram.com/{$v}"],
'facebook' => ['icon'=>'bi-facebook', 'color'=>'#60a5fa', 'bg'=>'rgba(96,165,250,.1)', 'hover'=>'#60a5fa', 'href'=>fn($v)=>"https://facebook.com/{$v}"],
'youtube' => ['icon'=>'bi-youtube', 'color'=>'#94a3b8', 'bg'=>'rgba(148,163,184,.1)', 'hover'=>'#94a3b8', 'href'=>fn($v)=>"https://youtube.com/@{$v}"],
'tiktok' => ['icon'=>'bi-tiktok', 'color'=>'#67e8f9', 'bg'=>'rgba(103,232,249,.1)', 'hover'=>'#67e8f9', 'href'=>fn($v)=>"https://tiktok.com/@{$v}"],
'linkedin' => ['icon'=>'bi-linkedin', 'color'=>'#7dd3fc', 'bg'=>'rgba(125,211,252,.1)', 'hover'=>'#7dd3fc', 'href'=>fn($v)=>"https://linkedin.com/in/{$v}"],
'whatsapp' => ['icon'=>'bi-whatsapp', 'color'=>'#4ade80', 'bg'=>'rgba(74,222,128,.1)', 'hover'=>'#4ade80', 'href'=>fn($v)=>"https://wa.me/".preg_replace('/\D/','',$v)],
'website' => ['icon'=>'bi-globe2', 'color'=>'#a78bfa', 'bg'=>'rgba(167,139,250,.1)', 'hover'=>'#a78bfa', 'href'=>fn($v)=>preg_match('/^https?:\/\//',$v)?$v:"https://{$v}"],
'social_phone' => ['icon'=>'bi-telephone-fill', 'color'=>'#34d399', 'bg'=>'rgba(52,211,153,.1)', 'hover'=>'#34d399', 'href'=>fn($v)=>"tel:{$v}"],
'social_email' => ['icon'=>'bi-envelope-fill', 'color'=>'#fbbf24', 'bg'=>'rgba(251,191,36,.1)', 'hover'=>'#fbbf24', 'href'=>fn($v)=>"mailto:{$v}"],
'google_location'=> ['icon'=>'bi-geo-alt-fill', 'color'=>'#6ee7b7', 'bg'=>'rgba(110,231,183,.1)', 'hover'=>'#6ee7b7', 'href'=>fn($v)=>$v],
];
@endphp
<div class="ch-header">
<div class="ch-header-inner">
{{-- Avatar --}}
<div class="ch-avatar-wrap">
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="ch-avatar" id="chAvatarImg">
@if($isOwner && !$preview)
<button class="ch-avatar-edit-btn" onclick="openCropperModal_avatar()" title="Change photo">
<i class="bi bi-camera-fill"></i>
</button>
@endif
</div>
{{-- Info --}}
<div class="ch-info">
{{-- Name row --}}
<div class="ch-name-row">
<h1 class="ch-name">{{ $user->name }}</h1>
@if($user->email_verified_at)
<span class="ch-verified-badge" title="Verified"><i class="bi bi-check-lg"></i></span>
@endif
</div>
{{-- Flat stats row no pills --}}
<div class="ch-meta-row">
<span class="ch-meta-item">
<i class="bi bi-camera-video-fill"></i>
{{ number_format($videos->count() + $shorts->count()) }} videos
</span>
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item">
<i class="bi bi-eye-fill"></i>
{{ number_format($totalViews) }} views
</span>
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item channel-subs"
data-channel-id="{{ $user->id }}"
data-count="{{ $user->subscriber_count }}">
<i class="bi bi-people-fill"></i>
{{ number_format($user->subscriber_count) }} {{ Str::plural('subscriber', $user->subscriber_count) }}
</span>
@if($user->location)
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item">
<i class="bi bi-geo-alt-fill"></i>
{{ $user->location }}
</span>
@endif
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item">
<i class="bi bi-calendar3"></i>
Joined {{ $user->created_at->format('M Y') }}
</span>
@if($user->birthday)
@php $age = \Carbon\Carbon::parse($user->birthday)->age; @endphp
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item">
<i class="bi bi-person-fill"></i>
{{ $age }} yrs
</span>
@endif
@if($user->gender === 'male')
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item" style="color:#3b82f6;font-weight:700;">
<i class="bi bi-gender-male" style="font-size:14px;-webkit-text-stroke:.4px #3b82f6;filter:drop-shadow(0 0 6px #3b82f640);"></i>
Male
</span>
@elseif($user->gender === 'female')
<span class="ch-meta-sep">·</span>
<span class="ch-meta-item" style="color:#ec4899;font-weight:700;">
<i class="bi bi-gender-female" style="font-size:14px;-webkit-text-stroke:.4px #ec4899;filter:drop-shadow(0 0 6px #ec489940);"></i>
Female
</span>
@endif
</div>
@if($user->bio)
<div class="ch-bio" id="ch-bio">
<span class="ch-bio-short" id="ch-bio-short">{{ $user->bio }}</span>
<span class="ch-bio-full" id="ch-bio-full" style="display:none;">{{ $user->bio }}</span>
@if(strlen($user->bio) > 120)
<button class="ch-bio-toggle" id="ch-bio-btn" onclick="toggleBio()">…more</button>
@endif
</div>
@endif
{{-- Horoscope + Compatibility strip & Social buttons side by side --}}
@if($horoscope || $socialLinks->isNotEmpty())
@php
$hColor = $horoscope ? \App\Helpers\Horoscope::elementColor($horoscope['element']) : null;
$showCompat = $horoscope && ($compatibility !== null && $viewerSign && !$isOwner);
if ($showCompat) {
$compatColor = $compatibility >= 75 ? '#22c55e' : ($compatibility >= 55 ? '#eab308' : '#ef4444');
$compatLabel = $compatibility >= 85 ? 'Soulmates' : ($compatibility >= 70 ? 'Great match' : ($compatibility >= 55 ? 'Good vibes' : ($compatibility >= 40 ? 'Some friction' : 'Tough combo')));
}
@endphp
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:14px;">
@if($horoscope)
<div class="ch-horo-strip" style="margin-bottom:0; cursor:pointer;"
onclick="switchTab('about',document.querySelector('[data-tab=about]'))" title="Click to view horoscope">
<div class="ch-horo-sign-part">
<span class="ch-horo-emoji" style="filter:drop-shadow(0 0 8px {{ $hColor }});">{{ $horoscope['emoji'] }}</span>
<span class="ch-horo-name">{{ $horoscope['name'] }}</span>
<span class="ch-horo-el" style="color:{{ $hColor }};border-color:{{ $hColor }}20;background:{{ $hColor }}18;">{{ $horoscope['element'] }}</span>
</div>
@if($showCompat)
<span class="ch-horo-divider"></span>
<div class="ch-compat-mini">
<span style="font-size:13px;">❤️</span>
<span class="ch-compat-mini-pct" style="color:{{ $compatColor }};">{{ $compatibility }}%</span>
<div class="ch-compat-mini-track">
<div class="ch-compat-mini-fill" id="chCompatMiniBar" style="width:0%;background:{{ $compatColor }};"></div>
</div>
<span class="ch-compat-mini-label">{{ $compatLabel }}</span>
</div>
@endif
</div>
@endif
@if($isOwner && !$preview)
{{-- Owner manage dropdown: consolidates Edit channel / 2FA / Log Out All
so it can sit inline with the horoscope strip instead of taking up
a whole row beneath it. --}}
<div class="ch-manage-wrap" id="chManageWrap">
<button type="button" class="ch-manage-btn" id="chManageBtn"
onclick="event.stopPropagation(); window._chToggleManage && window._chToggleManage();">
<i class="bi bi-sliders"></i>
<span>Manage</span>
<i class="bi bi-chevron-down"></i>
</button>
<div class="ch-manage-menu" id="chManageMenu">
<button type="button" class="ch-manage-item" onclick="switchTab('settings', null); document.getElementById('chManageMenu').classList.remove('open');">
<i class="bi bi-pencil"></i>
<span>Edit channel</span>
</button>
<button type="button" class="ch-manage-item {{ $user->two_factor_enabled ? 'success' : 'warning' }}"
onclick="switchTab('settings', document.querySelector('[data-tab=settings]')); setTimeout(()=>document.getElementById('twoFactorCard')?.scrollIntoView({behavior:'smooth',block:'center'}),400); document.getElementById('chManageMenu').classList.remove('open');">
<i class="bi {{ $user->two_factor_enabled ? 'bi-shield-check-fill' : 'bi-shield-exclamation' }}"></i>
<span>{{ $user->two_factor_enabled ? '2FA is On' : 'Enable 2FA' }}</span>
</button>
<button type="button" class="ch-manage-item danger" onclick="openLogoutAllModal(); document.getElementById('chManageMenu').classList.remove('open');">
<i class="bi bi-box-arrow-right"></i>
<span>Log out all devices</span>
</button>
</div>
</div>
<script>
(function () {
var menu = document.getElementById('chManageMenu');
if (menu && menu.parentNode !== document.body) document.body.appendChild(menu);
window._chToggleManage = function () {
var b = document.getElementById('chManageBtn');
var m = document.getElementById('chManageMenu');
if (!b || !m) return;
var willOpen = !m.classList.contains('open');
m.classList.toggle('open');
if (willOpen) {
var r = b.getBoundingClientRect();
m.style.left = r.left + 'px';
m.style.top = (r.bottom + 6) + 'px';
m.style.minWidth = Math.max(220, r.width) + 'px';
}
};
document.addEventListener('click', function (e) {
var b = document.getElementById('chManageBtn');
var m = document.getElementById('chManageMenu');
if (m && b && !b.contains(e.target) && !m.contains(e.target)) m.classList.remove('open');
});
})();
</script>
{{-- Primary CTA + visitor-preview icon, inline alongside Manage. --}}
<button type="button" class="ch-btn-ghost" onclick="openUploadChooser()"
style="color:#fff;background:rgba(230,30,30,.18);border-color:rgba(230,30,30,.5);">
<i class="bi bi-camera-video"></i>
<span>Upload</span>
</button>
<a href="{{ route('channel', $user->channel) }}?preview=1" class="ch-btn-icon" title="Preview as visitor">
<i class="bi bi-eye"></i>
</a>
@endif
@if($socialLinks->isNotEmpty())
<div class="ch-social-row" style="margin-bottom:0;">
@foreach($socialLinks as $slink)
@if(isset($headerSocialMap[$slink->platform]))
@php $sm = $headerSocialMap[$slink->platform]; @endphp
<a href="{{ $sm['href']($slink->value) }}"
target="{{ in_array($slink->platform, ['social_phone','social_email']) ? '_self' : '_blank' }}"
rel="noopener"
class="ch-social-btn"
style="--sc:{{ $sm['color'] }};--sb:{{ $sm['bg'] }};--sh:{{ $sm['hover'] }};"
title="{{ ucfirst(str_replace(['social_','google_'],['',''],$slink->platform)) }}">
<i class="bi {{ $sm['icon'] }}"></i>
</a>
@endif
@endforeach
</div>
@endif
</div>
@endif
{{-- Owner action row moved up into the horoscope strip Edit/2FA/Logout
collapsed into Manage; Upload + Preview sit inline next to it. --}}
@if($isOwner && !$preview)
{{-- Logout All Devices Modal appended to body on open to escape stacking context --}}
<template id="logoutAllModalTpl">
<div id="logoutAllModal" style="position:fixed;inset:0;z-index:10100;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;padding:20px 20px 80px;" onclick="if(event.target===this)closeLogoutAllModal()">
<div style="background:var(--bg-secondary,#1a1a1a);border:1px solid var(--border-color,#333);border-radius:16px;padding:28px;max-width:400px;width:100%;box-shadow:0 24px 64px rgba(0,0,0,.6);" onclick="event.stopPropagation()">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<div style="width:44px;height:44px;border-radius:50%;background:rgba(248,113,113,.15);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-box-arrow-right" style="color:#f87171;font-size:20px;"></i>
</div>
<div>
<div style="font-size:16px;font-weight:700;color:var(--text-primary,#fff);">Log Out All Other Devices</div>
<div style="font-size:12px;color:var(--text-secondary,#aaa);margin-top:2px;">Your current session will stay active</div>
</div>
</div>
<p style="font-size:13px;color:var(--text-secondary,#aaa);margin-bottom:20px;line-height:1.6;">
This will immediately end all other active sessions on phones, tablets, and other browsers. Enter your password to confirm.
</p>
<form method="POST" action="{{ route('settings.logoutAllDevices') }}">
@csrf
<div style="margin-bottom:16px;">
<label style="display:block;font-size:12px;font-weight:600;color:var(--text-secondary,#aaa);margin-bottom:7px;letter-spacing:.5px;text-transform:uppercase;">Password</label>
<input type="password" name="password" required placeholder="••••••••" id="logoutAllPwInput"
style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border-color,#333);border-radius:10px;padding:11px 14px;color:var(--text-primary,#fff);font-size:14px;">
@error('logout_password')
<div style="color:#f87171;font-size:12px;margin-top:5px;">{{ $message }}</div>
@enderror
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="action-btn" onclick="closeLogoutAllModal()">Cancel</button>
<button type="submit" class="action-btn action-btn-danger">
<i class="bi bi-box-arrow-right"></i> <span>Log Out All Devices</span>
</button>
</div>
</form>
</div>
</div>
</template>
@endif
</div>
</div>
</div>
{{-- ──────────────────────── TABS ───────────────────── --}}
<div class="ch-tabs-wrap">
<div class="ch-tabs" role="tablist">
<button class="ch-tab active" data-tab="wall" onclick="switchTab('wall', this)">
<i class="bi bi-layout-text-sidebar-reverse"></i>
Wall
@if($posts->count() > 0)
<span class="ch-tab-badge">{{ $posts->count() }}</span>
@endif
</button>
<button class="ch-tab" data-tab="videos" onclick="switchTab('videos', this)">
Videos
@if($videos->count() > 0)
<span class="ch-tab-badge">{{ $videos->count() }}</span>
@endif
</button>
@if($shorts->count() > 0)
<button class="ch-tab" data-tab="shorts" onclick="switchTab('shorts', this)">
Shorts
<span class="ch-tab-badge">{{ $shorts->count() }}</span>
</button>
@endif
@if(($playlists && $playlists->count() > 0) || $isOwner)
<button class="ch-tab" data-tab="playlists" onclick="switchTab('playlists', this)">
Playlists
@if($playlists && $playlists->count() > 0)
<span class="ch-tab-badge">{{ $playlists->count() }}</span>
@endif
</button>
@endif
<button class="ch-tab" data-tab="about" onclick="switchTab('about', this)">About</button>
@if($isOwner)
<button class="ch-tab" data-tab="settings" onclick="switchTab('settings', this)">
<i class="bi bi-gear" style="font-size:13px;"></i> Settings
</button>
@endif
</div>
</div>
{{-- spacer pushes content below the fixed tabs bar on mobile --}}
<div class="ch-tabs-spacer" style="display:none;"></div>
{{-- ─────────────────────── WALL TAB ────────────────── --}}
<div class="ch-tab-content active" id="tab-wall">
<div class="ch-wall-layout">
{{-- ── LEFT: composer + feed ── --}}
<div>
{{-- Composer (owner only) --}}
@auth
@if(Auth::id() === $user->id)
<div class="ch-composer">
<form action="{{ route('posts.store', $user->id) }}" method="POST" enctype="multipart/form-data" id="postForm">
@csrf
<div class="ch-composer-top">
<img src="{{ Auth::user()->avatar_url }}" alt="" class="ch-composer-avatar">
<div class="ch-composer-right">
<div class="ch-composer-name">{{ Auth::user()->name }}</div>
<textarea name="body" id="postBody"
placeholder="What's on your mind? Share with your community…"
maxlength="2000"
oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea>
</div>
</div>
{{-- Multi-image preview grid --}}
<div id="imagePreviewGrid" class="ch-composer-img-grid" style="display:none;"></div>
<input type="file" name="images[]" id="postImages" accept="image/*" multiple style="display:none;" onchange="handlePostImages(this)">
{{-- Selected video chips --}}
<div id="postVideoChips" class="ch-composer-video-chips" style="display:none;"></div>
<div class="ch-composer-bar">
<div class="ch-composer-media">
<button type="button" class="ch-composer-media-btn photo" title="Add photos" onclick="document.getElementById('postImages').click()">
<i class="bi bi-image-fill"></i> Photo
</button>
@if($user->videos->count() > 0)
<button type="button" class="ch-composer-media-btn video" onclick="openVideoPicker()" title="Share videos">
<i class="bi bi-play-btn-fill"></i> Video
</button>
@endif
</div>
<button type="submit" class="ch-composer-post-btn">
<i class="bi bi-send-fill"></i> Post
</button>
</div>
</form>
</div>
@endif
@endauth
{{-- Posts feed --}}
@forelse($posts as $post)
@php
$postLiked = Auth::check() && $post->isLikedBy(Auth::user());
$bodyLen = strlen($post->body ?? '');
$isShort = $bodyLen > 0 && $bodyLen <= 80 && !$post->image && !$post->video_id && !$post->postImages->count() && !$post->postVideos->count();
@endphp
<div class="ch-post" id="post-{{ $post->id }}">
{{-- Header --}}
<div class="ch-post-header">
<a href="{{ $post->user ? route('channel', $post->user->channel) : '#' }}" class="ch-post-avatar-link">
<img src="{{ $post->user->avatar_url }}" alt="{{ $post->user->name }}" class="ch-post-avatar">
</a>
<div class="ch-post-meta">
<div class="ch-post-name">{{ $post->user->name }}</div>
<div class="ch-post-time">
<i class="bi bi-clock"></i>
<span title="{{ $post->created_at->format('D, M j Y — g:i A') }}">{{ $post->created_at->diffForHumans() }}</span>
</div>
</div>
@auth
@if(Auth::id() === $post->user_id || Auth::user()->isAdmin())
<div class="ch-post-menu-wrap">
<button class="ch-post-menu-btn" onclick="togglePostMenu(this)" title="More options">
<i class="bi bi-three-dots"></i>
</button>
<div class="ch-post-dropdown">
<form action="{{ route('posts.destroy', $post->id) }}" method="POST">
@csrf @method('DELETE')
<button type="submit" class="danger">
<i class="bi bi-trash3"></i> Delete post
</button>
</form>
</div>
</div>
@endif
@endauth
</div>
{{-- Body --}}
@if($post->body)
<div class="{{ $isShort ? 'ch-post-body-lg' : 'ch-post-body' }}">{{ $post->body }}</div>
@endif
{{-- Images (new multi-image) --}}
@php
$postImgs = $post->postImages;
$legacyImg = (!$postImgs->count() && $post->image) ? $post->image_url : null;
$imgCount = $postImgs->count() ?: ($legacyImg ? 1 : 0);
$countClass = $imgCount === 1 ? 'count-1' : ($imgCount === 2 ? 'count-2' : ($imgCount === 3 ? 'count-3' : ($imgCount === 4 ? 'count-4' : ($imgCount > 4 ? 'count-more' : ''))));
@endphp
@if($imgCount > 0)
<div class="ch-post-img-grid {{ $countClass }}">
@if($legacyImg)
<div class="ch-post-img-item" onclick="openLightbox('{{ $legacyImg }}')">
<img src="{{ $legacyImg }}" alt="" loading="lazy">
</div>
@else
@foreach($postImgs->take(4) as $idx => $img)
<div class="ch-post-img-item" onclick="openLightbox('{{ $img->image_url }}')">
<img src="{{ $img->image_url }}" alt="" loading="lazy">
@if($idx === 3 && $imgCount > 4)
<div class="ch-post-img-more">+{{ $imgCount - 4 }}</div>
@endif
</div>
@endforeach
@endif
</div>
@endif
{{-- Videos (new multi-video) --}}
@php
$postVids = $post->postVideos->filter(fn($pv) => $pv->video?->exists);
$legacyVid = (!$postVids->count() && $post->video_id && $post->video?->exists) ? $post->video : null;
$allVids = $legacyVid ? collect([$legacyVid]) : $postVids->map(fn($pv) => $pv->video);
@endphp
@if($allVids->count())
<div class="ch-post-video-wrap">
@foreach($allVids as $v)
<a href="{{ route('videos.show', $v) }}" class="ch-post-video-card" style="{{ !$loop->first ? 'margin-top:8px;' : '' }}">
<div class="ch-post-video-thumb-wrap">
<img src="{{ $v->thumbnail ? route('media.thumbnail', $v->thumbnail) : '' }}"
alt="" onerror="this.style.display='none'">
<div class="ch-post-video-play"><i class="bi bi-play-circle-fill"></i></div>
</div>
<div class="ch-post-video-info">
<div class="ch-post-video-title">{{ $v->title }}</div>
<div class="ch-post-video-meta">
<span class="ch-post-video-badge"><i class="bi bi-eye"></i> {{ \Illuminate\Support\Number::abbreviate($v->view_count, precision: 1) }}</span>
@if($v->duration)<span class="ch-post-video-badge"><i class="bi bi-clock"></i> {{ $v->formatted_duration }}</span>@endif
@if($v->is_shorts)<span class="ch-post-video-badge" style="color:#e33;"><i class="bi bi-phone"></i> Short</span>@endif
</div>
</div>
</a>
@endforeach
</div>
@endif
{{-- Actions --}}
<div class="ch-post-actions">
@auth
<button class="ch-post-action-btn {{ $postLiked ? 'liked' : '' }}"
data-post-id="{{ $post->id }}"
data-react-url="{{ route('posts.react', $post->id) }}"
onclick="reactPost(this)">
<span class="ch-post-like-anim">
<i class="bi {{ $postLiked ? 'bi-heart-fill' : 'bi-heart' }}"></i>
</span>
<span class="post-like-count">{{ $post->reaction_count > 0 ? $post->reaction_count : '' }}</span>
<span style="margin-left:2px;">{{ $postLiked ? 'Liked' : 'Like' }}</span>
</button>
@else
<span class="ch-post-action-btn" style="cursor:default;">
<i class="bi bi-heart"></i>
{{ $post->reaction_count > 0 ? $post->reaction_count : '' }}
<span style="margin-left:2px;">Like</span>
</span>
@endauth
<div class="ch-post-action-sep"></div>
<button class="ch-post-action-btn"
onclick="navigator.clipboard.writeText('{{ url()->current() }}').then(()=>showToast('Link copied!','success'))">
<i class="bi bi-share"></i> Share
</button>
</div>
</div>
@empty
{{-- Empty state --}}
<div class="ch-wall-empty">
<div class="ch-wall-empty-icon"></div>
<h3>Nothing here yet</h3>
@auth
@if(Auth::id() === $user->id)
<p>This is your wall. Share a thought, a photo, or one of your videos with your community!</p>
@else
<p>{{ $user->name }} hasn't shared anything yet — check back soon!</p>
@endif
@else
<p>{{ $user->name }} hasn't shared anything yet. Sign in to be notified when they do.</p>
@endauth
</div>
@endforelse
</div>
{{-- ── RIGHT: sidebar ── --}}
<div class="ch-wall-sidebar">
@if(!$horoscope && $isOwner)
<div class="ch-sw-card" style="text-align:center;padding:22px 16px;">
<div style="font-size:32px;margin-bottom:8px;">🔮</div>
<div style="font-size:14px;font-weight:700;color:var(--text-primary);margin-bottom:8px;">Your Horoscope Awaits</div>
<p style="font-size:12px;color:var(--text-secondary);line-height:1.6;margin-bottom:14px;">Add your birthday to unlock your zodiac sign, age, personality traits, and compatibility with visitors.</p>
<button type="button" class="action-btn action-btn-primary" style="width:100%;" onclick="switchTab('settings', document.querySelector('[data-tab=settings]')); setTimeout(()=>document.querySelector('[name=birthday]')?.closest('.ch-settings-field')?.scrollIntoView({behavior:'smooth',block:'center'}),400);">
<i class="bi bi-calendar-heart"></i> <span>Add Birthday</span>
</button>
</div>
@elseif($horoscope)
@php
$swHColor = \App\Helpers\Horoscope::elementColor($horoscope['element']);
$swPct = $compatibility;
$swCColor = $swPct !== null ? ($swPct >= 75 ? '#4ade80' : ($swPct >= 55 ? '#fbbf24' : '#f87171')) : null;
$swLabel = $swPct !== null ? ($swPct >= 85 ? 'Soulmates' : ($swPct >= 70 ? 'Great Match' : ($swPct >= 55 ? 'Good Vibes' : ($swPct >= 40 ? 'Some Friction' : 'Tough Combo')))) : null;
$swEmoji = $swPct !== null ? ($swPct >= 85 ? '✨' : ($swPct >= 70 ? '💛' : ($swPct >= 55 ? '🙂' : ($swPct >= 40 ? '🤔' : '⚡')))) : null;
@endphp
<div class="ch-sw-card" style="--sw-h:{{ $swHColor }};">
{{-- Sign row --}}
<div class="ch-sw-sign-row">
<span class="ch-sw-emoji" style="filter:drop-shadow(0 0 10px {{ $swHColor }});">{{ $horoscope['emoji'] }}</span>
<div class="ch-sw-sign-info">
<div class="ch-sw-sign-name">{{ $horoscope['name'] }} <span style="opacity:.5;font-weight:500;">{{ $horoscope['symbol'] }}</span></div>
<div class="ch-sw-element" style="color:{{ $swHColor }};">{{ $horoscope['element'] }} sign</div>
</div>
</div>
{{-- Traits --}}
<div class="ch-sw-traits">
@foreach($horoscope['traits'] as $trait)
<span class="ch-sw-trait">{{ $trait }}</span>
@endforeach
</div>
{{-- Compat section --}}
@if($swPct !== null && $viewerSign)
<div class="ch-sw-compat">
<div class="ch-sw-compat-signs">
<span class="ch-sw-compat-sign">{{ $viewerSign['emoji'] }}<small>You</small></span>
<span class="ch-sw-compat-heart">❤️</span>
<span class="ch-sw-compat-sign">{{ $horoscope['emoji'] }}<small>{{ explode(' ', $user->name)[0] }}</small></span>
<div style="flex:1;"></div>
<span class="ch-sw-compat-pct" style="color:{{ $swCColor }};">{{ $swPct }}%</span>
<span class="ch-sw-compat-lbl">{{ $swEmoji }} {{ $swLabel }}</span>
</div>
<div class="ch-sw-compat-bar">
<div class="ch-sw-compat-fill" id="compatBar" style="width:0%;background:{{ $swCColor }};"></div>
</div>
</div>
@elseif(auth()->check() && !auth()->user()->birthday && auth()->id() !== $user->id)
<div class="ch-sw-compat-prompt">
🔮 <a href="{{ route('channel') }}#settings">Add your birthday</a> to see your cosmic match with {{ explode(' ', $user->name)[0] }}
</div>
@endif
</div>
@endif
</div>{{-- .ch-wall-sidebar --}}
</div>
</div>
{{-- ─────────────────────── VIDEOS TAB ──────────────── --}}
<div class="ch-tab-content" id="tab-videos">
@if($videos->count() > 0)
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:20px;">
<div class="ch-vid-search-wrap" style="margin-bottom:0; flex:1; min-width:180px;">
<i class="bi bi-search"></i>
<input type="search" class="ch-vid-search" id="chVidSearch" placeholder="Search videos…" autocomplete="off">
</div>
<div class="ch-sort-bar" style="margin-bottom:0; flex-shrink:0;">
<span class="ch-sort-label">Sort</span>
<div class="ch-sort-pills">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'latest']) }}"
class="ch-sort-pill {{ $sort === 'latest' ? 'active' : '' }}">Latest</a>
<a href="{{ request()->fullUrlWithQuery(['sort' => 'popular']) }}"
class="ch-sort-pill {{ $sort === 'popular' ? 'active' : '' }}">Popular</a>
<a href="{{ request()->fullUrlWithQuery(['sort' => 'oldest']) }}"
class="ch-sort-pill {{ $sort === 'oldest' ? 'active' : '' }}">Oldest</a>
</div>
</div>
</div>
<div class="yt-video-grid" id="chVidGrid">
@foreach($videos as $video)
@include('components.video-card', ['video' => $video])
@endforeach
<div class="ch-vid-no-results" id="chVidNoResults">
<i class="bi bi-search"></i>
<span>No videos match your search</span>
</div>
</div>
@else
<div class="ch-empty">
<div class="ch-empty-icon"><i class="bi bi-camera-video"></i></div>
<h3>No videos yet</h3>
<p>This channel hasn't uploaded any videos.</p>
@auth
@if(Auth::id() === $user->id)
<button class="ch-btn-primary" onclick="openUploadChooser()">
<i class="bi bi-cloud-upload"></i> Upload your first video
</button>
@endif
@endauth
</div>
@endif
</div>
{{-- ─────────────────────── SHORTS TAB ──────────────── --}}
@if($shorts->count() > 0)
<div class="ch-tab-content" id="tab-shorts">
<div class="ch-shorts-grid">
@foreach($shorts as $short)
<a href="{{ route('videos.show', $short) }}" class="ch-short-card">
<div class="ch-short-thumb">
<img src="{{ $short->thumbnail ? route('media.thumbnail', $short->thumbnail) : 'https://picsum.photos/seed/' . $short->id . '/360/640' }}"
alt="{{ $short->title }}" loading="lazy">
@if($short->duration)
<span class="ch-short-duration">{{ gmdate('i:s', $short->duration) }}</span>
@endif
@if($isOwner && $short->visibility === 'private')
<span class="ch-short-visibility-badge ch-vis-private"><i class="bi bi-lock-fill"></i> Private</span>
@elseif($isOwner && $short->visibility === 'unlisted')
<span class="ch-short-visibility-badge ch-vis-unlisted"><i class="bi bi-link-45deg"></i> Unlisted</span>
@endif
<div class="ch-short-play"><i class="bi bi-play-fill"></i></div>
</div>
<div class="ch-short-title">{{ $short->title }}</div>
<div class="ch-short-meta">{{ number_format($short->view_count) }} views</div>
</a>
@endforeach
</div>
</div>
@endif
{{-- ─────────────────────── PLAYLISTS TAB ───────────── --}}
@if(($playlists && $playlists->count() > 0) || $isOwner)
<div class="ch-tab-content" id="tab-playlists">
@if($isOwner)
<div style="display:flex; justify-content:flex-end; margin-bottom:16px;">
<button class="action-btn action-btn-primary" onclick="openChannelCreatePlaylistModal()">
<i class="bi bi-plus-lg"></i> <span>New Playlist</span>
</button>
</div>
@endif
@if($playlists && $playlists->count() > 0)
<div class="ch-playlists-grid">
@foreach($playlists as $playlist)
@php
$plFirstVid = $playlist->videos->first();
$plCardUrl = $plFirstVid
? route('videos.show', $plFirstVid) . '?playlist=' . $playlist->share_token
: route('playlists.show', $playlist->id);
@endphp
<a href="{{ $plCardUrl }}" class="ch-playlist-card">
<div class="ch-playlist-thumb">
@if($playlist->thumbnail_url)
<img src="{{ $playlist->thumbnail_url }}" alt="{{ $playlist->name }}" loading="lazy">
@else
<div class="ch-pl-placeholder"><i class="bi bi-collection-play"></i></div>
@endif
<div class="ch-playlist-overlay"></div>
<div class="ch-playlist-count">
<i class="bi bi-play-fill"></i>
{{ $playlist->video_count }} videos
</div>
</div>
<div class="ch-playlist-name">{{ $playlist->name }}</div>
<div class="ch-playlist-meta">
@if($playlist->is_default)
Watch Later
@elseif($playlist->visibility === 'private')
<i class="bi bi-lock-fill"></i> Private
@elseif($playlist->visibility === 'unlisted')
<i class="bi bi-link-45deg"></i> Unlisted
@else
<i class="bi bi-globe"></i> Public
@endif
· <i class="bi bi-eye"></i> {{ \Illuminate\Support\Number::abbreviate($playlist->view_count, precision: 1) }}
</div>
</a>
@endforeach
</div>
@else
<div style="text-align:center; padding:60px 20px; color:var(--text-secondary);">
<i class="bi bi-collection-play" style="font-size:48px; margin-bottom:12px; display:block;"></i>
<p style="margin:0 0 16px;">No playlists yet. Create your first one!</p>
</div>
@endif
</div>
@endif
{{-- ─── Create Playlist Modal (channel page) ─────────── --}}
@if($isOwner)
<div id="chCreatePlaylistModal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.7); z-index:9999; align-items:center; justify-content:center; backdrop-filter:blur(2px);">
<div style="background:#282828; border-radius:12px; width:90%; max-width:480px; box-shadow:0 8px 32px rgba(0,0,0,.5); overflow:hidden; animation:chPlModalIn .2s ease;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:20px 24px; border-bottom:1px solid #3f3f3f;">
<h2 style="font-size:18px; font-weight:600; margin:0; color:#fff;">Create new playlist</h2>
<button type="button" onclick="closeChannelCreatePlaylistModal()" style="background:transparent; border:none; color:#aaa; cursor:pointer; font-size:22px; line-height:1; border-radius:50%; width:32px; height:32px; display:flex; align-items:center; justify-content:center;">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div style="padding:24px;">
<form id="chCreatePlaylistForm" enctype="multipart/form-data">
@csrf
<!-- Thumbnail -->
<div style="margin-bottom:20px;">
<label style="display:block; margin-bottom:8px; font-weight:500; font-size:14px; color:#fff;">Thumbnail</label>
<div id="chPlThumbZone" style="border:2px dashed #3f3f3f; border-radius:12px; padding:20px; text-align:center; cursor:pointer; background:#1a1a1a; transition:all .2s;"
onmouseover="this.style.borderColor='#e61e1e'; this.style.background='rgba(230,30,30,0.05)'"
onmouseout="this.style.borderColor='#3f3f3f'; this.style.background='#1a1a1a'">
<input type="file" name="thumbnail" id="chPlThumbInput" accept="image/*" style="display:none;">
<div id="chPlThumbDefault">
<div style="font-size:36px; color:#666; margin-bottom:8px;"><i class="bi bi-card-image"></i></div>
<p style="color:#aaa; font-size:13px; margin:0 0 4px;">Click to upload thumbnail</p>
<p style="color:#555; font-size:11px; margin:0;">JPG, PNG, GIF, WebP (max 5MB)</p>
</div>
<div id="chPlThumbPreview" style="display:none; position:relative;">
<img id="chPlThumbImg" src="" alt="" style="max-width:100%; max-height:160px; border-radius:8px; object-fit:cover;">
<button type="button" onclick="chRemovePlThumb(event)" style="position:absolute; top:-8px; right:-8px; width:24px; height:24px; background:#e61e1e; color:#fff; border:none; border-radius:50%; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:14px;"><i class="bi bi-x"></i></button>
</div>
</div>
</div>
<!-- Name -->
<div style="margin-bottom:20px;">
<label style="display:block; margin-bottom:8px; font-weight:500; font-size:14px; color:#fff;">Name *</label>
<input type="text" name="name" required placeholder="Enter playlist name"
style="width:100%; padding:12px 14px; border:1px solid #3f3f3f; border-radius:8px; background:#121212; color:#fff; font-size:14px; outline:none; box-sizing:border-box;"
onfocus="this.style.borderColor='#e61e1e'" onblur="this.style.borderColor='#3f3f3f'">
</div>
<!-- Description -->
<div style="margin-bottom:20px;">
<label style="display:block; margin-bottom:8px; font-weight:500; font-size:14px; color:#fff;">Description</label>
<textarea name="description" rows="3" placeholder="Add a description (optional)"
style="width:100%; padding:12px 14px; border:1px solid #3f3f3f; border-radius:8px; background:#121212; color:#fff; font-size:14px; outline:none; resize:none; box-sizing:border-box;"
onfocus="this.style.borderColor='#e61e1e'" onblur="this.style.borderColor='#3f3f3f'"></textarea>
</div>
<!-- Visibility -->
<div style="margin-bottom:24px;">
<label style="display:block; margin-bottom:10px; font-weight:500; font-size:14px; color:#fff;">Visibility</label>
<div style="display:flex; flex-direction:column; gap:8px;">
<label style="display:flex; align-items:center; gap:12px; cursor:pointer; padding:10px 12px; border-radius:8px; border:1px solid #3f3f3f; background:#1a1a1a;">
<input type="radio" name="visibility" value="public" style="accent-color:#e61e1e; width:16px; height:16px;">
<div>
<div style="display:flex; align-items:center; gap:6px; color:#fff; font-size:14px; font-weight:500;"><i class="bi bi-globe"></i> Public</div>
<div style="color:#aaa; font-size:12px; margin-top:2px;">Anyone can search for and view this playlist</div>
</div>
</label>
<label style="display:flex; align-items:center; gap:12px; cursor:pointer; padding:10px 12px; border-radius:8px; border:1px solid #3f3f3f; background:#1a1a1a;">
<input type="radio" name="visibility" value="unlisted" style="accent-color:#e61e1e; width:16px; height:16px;">
<div>
<div style="display:flex; align-items:center; gap:6px; color:#fff; font-size:14px; font-weight:500;"><i class="bi bi-link-45deg"></i> Unlisted</div>
<div style="color:#aaa; font-size:12px; margin-top:2px;">Anyone with the link can view, but won't appear in search</div>
</div>
</label>
<label style="display:flex; align-items:center; gap:12px; cursor:pointer; padding:10px 12px; border-radius:8px; border:1px solid #3f3f3f; background:#1a1a1a;">
<input type="radio" name="visibility" value="private" checked style="accent-color:#e61e1e; width:16px; height:16px;">
<div>
<div style="display:flex; align-items:center; gap:6px; color:#fff; font-size:14px; font-weight:500;"><i class="bi bi-lock-fill"></i> Private</div>
<div style="color:#aaa; font-size:12px; margin-top:2px;">Only you can view this playlist</div>
</div>
</label>
</div>
</div>
<div style="display:flex; gap:12px; justify-content:flex-end;">
<button type="button" onclick="closeChannelCreatePlaylistModal()" class="action-btn">Cancel</button>
<button type="submit" class="action-btn action-btn-primary"><i class="bi bi-plus-lg"></i> <span>Create</span></button>
</div>
</form>
</div>
</div>
</div>
<style>
@keyframes chPlModalIn {
from { opacity:0; transform:translateY(-20px) scale(.95); }
to { opacity:1; transform:translateY(0) scale(1); }
}
</style>
<script>
(function() {
const modal = document.getElementById('chCreatePlaylistModal');
const form = document.getElementById('chCreatePlaylistForm');
const thumbZone = document.getElementById('chPlThumbZone');
const thumbInput = document.getElementById('chPlThumbInput');
const thumbDef = document.getElementById('chPlThumbDefault');
const thumbPrev = document.getElementById('chPlThumbPreview');
const thumbImg = document.getElementById('chPlThumbImg');
thumbZone.addEventListener('click', function(e) { if (!e.target.closest('button')) thumbInput.click(); });
thumbInput.addEventListener('change', function() { chHandleThumb(this); });
thumbZone.addEventListener('dragover', function(e) { e.preventDefault(); this.style.borderColor='#e61e1e'; });
thumbZone.addEventListener('dragleave', function(e) { e.preventDefault(); this.style.borderColor='#3f3f3f'; });
thumbZone.addEventListener('drop', function(e) {
e.preventDefault(); this.style.borderColor='#3f3f3f';
if (e.dataTransfer.files.length) { thumbInput.files = e.dataTransfer.files; chHandleThumb(thumbInput); }
});
function chHandleThumb(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
if (!['image/jpeg','image/png','image/gif','image/webp'].includes(file.type)) {
showToast('Invalid image format. Use JPG, PNG, GIF or WebP.', 'error'); return;
}
if (file.size > 5 * 1024 * 1024) { showToast('Image exceeds 5 MB limit.', 'error'); return; }
const reader = new FileReader();
reader.onload = function(e) {
thumbImg.src = e.target.result;
thumbDef.style.display = 'none';
thumbPrev.style.display = 'block';
};
reader.readAsDataURL(file);
}
window.chRemovePlThumb = function(e) {
e.preventDefault(); e.stopPropagation();
thumbInput.value = '';
thumbDef.style.display = 'block';
thumbPrev.style.display = 'none';
thumbImg.src = '';
};
window.openChannelCreatePlaylistModal = function() { modal.style.display = 'flex'; };
window.closeChannelCreatePlaylistModal = function() {
modal.style.display = 'none';
form.reset();
thumbDef.style.display = 'block';
thumbPrev.style.display = 'none';
thumbImg.src = '';
thumbInput.value = '';
};
modal.addEventListener('click', function(e) { if (e.target === modal) closeChannelCreatePlaylistModal(); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.style.display === 'flex') closeChannelCreatePlaylistModal();
});
form.addEventListener('submit', function(e) {
e.preventDefault();
const btn = form.querySelector('[type=submit]');
btn.disabled = true;
fetch('{{ route("playlists.store") }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' },
body: new FormData(form)
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = '{{ route("playlists.show", "") }}/' + data.playlist.id;
} else {
showToast(data.message || 'Failed to create playlist.', 'error');
btn.disabled = false;
}
})
.catch(() => { showToast('Failed to create playlist. Please try again.', 'error'); btn.disabled = false; });
});
})();
</script>
@endif
{{-- ─────────────────────── ABOUT TAB ───────────────── --}}
<div class="ch-tab-content" id="tab-about">
<div class="ch-about-layout">
{{-- Left column --}}
<div>
@if($user->bio)
<div class="ch-about-card">
<div class="ch-about-card-title"><i class="bi bi-person-lines-fill"></i> About</div>
<p class="ch-about-desc">{{ $user->bio }}</p>
</div>
@endif
@if(!$horoscope && $isOwner)
<div class="ch-about-card" style="text-align:center;padding:24px 20px;">
<div style="font-size:36px;margin-bottom:10px;">🔮</div>
<div style="font-size:15px;font-weight:700;color:var(--text-primary);margin-bottom:8px;">Unlock Your Horoscope</div>
<p style="font-size:13px;color:var(--text-secondary);line-height:1.6;margin-bottom:16px;">Add your birthday to see your zodiac sign, age, traits, and compatibility with visitors.</p>
<button type="button" class="action-btn action-btn-primary" onclick="switchTab('settings', document.querySelector('[data-tab=settings]')); setTimeout(()=>document.querySelector('[name=birthday]')?.closest('.ch-settings-field')?.scrollIntoView({behavior:'smooth',block:'center'}),400);">
<i class="bi bi-calendar-heart"></i> <span>Add Birthday</span>
</button>
</div>
@elseif($horoscope)
@php $hColor = \App\Helpers\Horoscope::elementColor($horoscope['element']); @endphp
<div class="ch-about-card" style="--horoscope-color:{{ $hColor }};">
<div class="ch-about-card-title"><i class="bi bi-stars"></i> Horoscope</div>
<div class="ch-horoscope-sign-row" style="margin-bottom:12px;">
<div class="ch-horoscope-symbol" style="font-size:36px;">{{ $horoscope['emoji'] }}</div>
<div>
<div class="ch-horoscope-name" style="font-size:17px;">{{ $horoscope['name'] }} {{ $horoscope['symbol'] }}</div>
<div class="ch-horoscope-element" style="color:{{ $hColor }};margin-top:4px;">
<i class="bi bi-gem"></i> {{ $horoscope['element'] }} sign
</div>
</div>
</div>
<div class="ch-horoscope-traits">
@foreach($horoscope['traits'] as $trait)
<span class="ch-horoscope-trait">{{ $trait }}</span>
@endforeach
</div>
</div>
@endif
@if($socialLinks->isNotEmpty())
@php
$chLinkMap = [
'twitter' => ['icon' => 'bi-twitter-x', 'color' => '#1d9bf0', 'bg' => 'rgba(29,155,240,.15)',
'href' => fn($v) => "https://twitter.com/{$v}",
'label' => fn($v) => "@{$v}"],
'instagram' => ['icon' => 'bi-instagram', 'color' => '#e1306c', 'bg' => 'rgba(225,48,108,.15)',
'href' => fn($v) => "https://instagram.com/{$v}",
'label' => fn($v) => "@{$v}"],
'facebook' => ['icon' => 'bi-facebook', 'color' => '#1877f2', 'bg' => 'rgba(24,119,242,.15)',
'href' => fn($v) => "https://facebook.com/{$v}",
'label' => fn($v) => $v],
'youtube' => ['icon' => 'bi-youtube', 'color' => '#ff0000', 'bg' => 'rgba(255,0,0,.15)',
'href' => fn($v) => "https://youtube.com/@{$v}",
'label' => fn($v) => "@{$v}"],
'linkedin' => ['icon' => 'bi-linkedin', 'color' => '#0a66c2', 'bg' => 'rgba(10,102,194,.15)',
'href' => fn($v) => "https://linkedin.com/in/{$v}",
'label' => fn($v) => $v],
'tiktok' => ['icon' => 'bi-tiktok', 'color' => '#ff0050', 'bg' => 'rgba(255,0,80,.15)',
'href' => fn($v) => "https://tiktok.com/@{$v}",
'label' => fn($v) => "@{$v}"],
'whatsapp' => ['icon' => 'bi-whatsapp', 'color' => '#25d366', 'bg' => 'rgba(37,211,102,.15)',
'href' => fn($v) => "https://wa.me/" . preg_replace('/\D/','',$v),
'label' => fn($v) => $v],
'website' => ['icon' => 'bi-globe2', 'color' => '#a78bfa', 'bg' => 'rgba(167,139,250,.15)',
'href' => fn($v) => preg_match('/^https?:\/\//', $v) ? $v : "https://{$v}",
'label' => fn($v) => parse_url((str_starts_with($v,'http') ? $v : "https://{$v}"), PHP_URL_HOST) ?: $v],
'google_location' => ['icon' => 'bi-geo-alt-fill', 'color' => '#ea4335', 'bg' => 'rgba(234,67,53,.15)',
'href' => fn($v) => $v,
'label' => fn($v) => 'View on Maps'],
'social_phone' => ['icon' => 'bi-telephone-fill', 'color' => '#34d399', 'bg' => 'rgba(52,211,153,.15)',
'href' => fn($v) => "tel:{$v}",
'label' => fn($v) => $v],
'social_email' => ['icon' => 'bi-envelope-fill', 'color' => '#fb923c', 'bg' => 'rgba(251,146,60,.15)',
'href' => fn($v) => "mailto:{$v}",
'label' => fn($v) => $v],
];
@endphp
<div class="ch-about-card">
<div class="ch-about-card-title"><i class="bi bi-link-45deg"></i> Links</div>
@foreach($socialLinks as $link)
@if(isset($chLinkMap[$link->platform]))
@php $m = $chLinkMap[$link->platform]; @endphp
<a href="{{ $m['href']($link->value) }}"
target="{{ in_array($link->platform, ['social_phone','social_email']) ? '_self' : '_blank' }}"
rel="noopener" class="ch-link-card">
<span class="ch-link-card-icon" style="background:{{ $m['bg'] }};color:{{ $m['color'] }};">
<i class="bi {{ $m['icon'] }}"></i>
</span>
<span class="ch-link-card-label">{{ $m['label']($link->value) }}</span>
<i class="bi bi-chevron-right"></i>
</a>
@endif
@endforeach
</div>
@endif
</div>
{{-- Right column: Stats + Compatibility --}}
<div>
<div class="ch-about-card">
<div class="ch-about-card-title"><i class="bi bi-bar-chart-fill"></i> Stats</div>
<div class="ch-stat-cards">
<div class="ch-stat-card">
<div class="ch-stat-card-val">{{ \Illuminate\Support\Number::abbreviate($totalViews, precision: 1) }}</div>
<div class="ch-stat-card-key">Total Views</div>
</div>
<div class="ch-stat-card">
<div class="ch-stat-card-val">{{ $videos->count() + $shorts->count() }}</div>
<div class="ch-stat-card-key">Videos</div>
</div>
</div>
<div class="ch-stat-item">
<div class="ch-stat-item-icon"><i class="bi bi-calendar3"></i></div>
<span>Joined {{ $user->created_at->format('F j, Y') }}</span>
</div>
@if($user->location)
<div class="ch-stat-item">
<div class="ch-stat-item-icon"><i class="bi bi-geo-alt-fill"></i></div>
<span>{{ $user->location }}</span>
</div>
@endif
@if($playlists && $playlists->count() > 0)
<div class="ch-stat-item">
<div class="ch-stat-item-icon"><i class="bi bi-collection-play-fill"></i></div>
<span>{{ $playlists->count() }} playlists</span>
</div>
@endif
@if($shorts->count() > 0)
<div class="ch-stat-item">
<div class="ch-stat-item-icon"><i class="bi bi-phone-fill"></i></div>
<span>{{ $shorts->count() }} shorts</span>
</div>
@endif
</div>
@if($compatibility !== null && $viewerSign)
@php
$pct = $compatibility;
$compatColor = $pct >= 75 ? '#22c55e' : ($pct >= 55 ? '#eab308' : '#ef4444');
$compatLabel = $pct >= 85 ? 'Soulmates' : ($pct >= 70 ? 'Great match' : ($pct >= 55 ? 'Good vibes' : ($pct >= 40 ? 'Some friction' : 'Tough combo')));
@endphp
<div class="ch-compat-card">
<div class="ch-compat-label"><i class="bi bi-hearts" style="color:#e33;"></i> Compatibility</div>
<div class="ch-compat-faces">
<div class="ch-compat-sign-block">
<div class="ch-compat-sign-sym">{{ $viewerSign['emoji'] }}</div>
<div class="ch-compat-sign-name">You</div>
</div>
<div class="ch-compat-heart">❤️</div>
<div class="ch-compat-sign-block">
<div class="ch-compat-sign-sym">{{ $horoscope['emoji'] }}</div>
<div class="ch-compat-sign-name">{{ explode(' ', $user->name)[0] }}</div>
</div>
</div>
<div class="ch-compat-pct" style="color:{{ $compatColor }};">{{ $pct }}%</div>
<div class="ch-compat-bar-wrap" style="margin:8px 0;">
<div class="ch-compat-bar" id="compatBarAbout" style="width:0%;background:{{ $compatColor }};"></div>
</div>
<div class="ch-compat-desc">{{ $compatLabel }}</div>
</div>
@endif
</div>
</div>
</div>
{{-- ─────────────────────── SETTINGS TAB ────────────── --}}
@if($isOwner)
@php
$oldLinks = old('slink');
if ($oldLinks) {
$socialExisting = collect($oldLinks)
->filter(fn($e) => !empty($e['platform']) && !empty($e['value']))
->map(fn($e) => ['platform' => $e['platform'], 'value' => $e['value'], 'visibility' => $e['visibility'] ?? 'public'])
->values()->all();
} else {
$socialExisting = $user->socialLinks()->orderBy('sort_order')->get()
->map(fn($l) => ['platform' => $l->platform, 'value' => $l->value, 'visibility' => $l->visibility])
->all();
}
@endphp
<div class="ch-tab-content" id="tab-settings">
<div class="ch-settings-layout">
{{-- Profile Edit Form --}}
<div>
<div class="ch-settings-card">
<div class="ch-settings-card-title"><i class="bi bi-person-fill"></i> Profile Info</div>
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf @method('PUT')
<input type="hidden" name="_edit_user_id" value="{{ $user->id }}">
{{-- Avatar --}}
<div class="ch-settings-avatar-row">
<div class="ch-settings-avatar-wrap">
<img src="{{ $user->avatar_url }}" alt="" class="ch-settings-avatar" id="chSettingsAvatarPreview">
<label class="ch-settings-avatar-btn" title="Change photo">
<i class="bi bi-camera-fill"></i>
<input type="file" name="avatar" accept="image/*" style="display:none;"
onchange="previewSettingsAvatar(this)">
</label>
</div>
<div style="font-size:13px;color:var(--text-secondary);line-height:1.7;">
<strong style="color:var(--text-primary);">Profile Photo</strong><br>
JPG, PNG or WebP · Max 5 MB<br>
Recommended: 200×200 px minimum
</div>
</div>
<div class="ch-settings-2col">
<div class="ch-settings-field">
<label class="ch-settings-label">Name</label>
<input type="text" name="name" class="ch-settings-input" value="{{ old('name', $user->name) }}" required>
</div>
<div class="ch-settings-field">
<label class="ch-settings-label">Gender</label>
<select name="gender" class="ch-settings-input" style="cursor:pointer;">
<option value="">Prefer not to say</option>
<option value="male" {{ old('gender', $user->gender) === 'male' ? 'selected' : '' }}>Male</option>
<option value="female" {{ old('gender', $user->gender) === 'female' ? 'selected' : '' }}>Female</option>
<option value="prefer_not_to_say" {{ old('gender', $user->gender) === 'prefer_not_to_say' ? 'selected' : '' }}>Prefer not to say</option>
</select>
</div>
</div>
<div class="ch-settings-field">
<label class="ch-settings-label">Bio</label>
<textarea name="bio" class="ch-settings-input ch-settings-textarea" placeholder="Tell the world about yourself…">{{ old('bio', $user->bio) }}</textarea>
</div>
<div class="ch-settings-2col">
<div class="ch-settings-field">
<x-date-picker
name="birthday"
label="Birthday"
value="{{ old('birthday', $user->birthday) }}"
:max-year="(int) date('Y')"
:min-year="1900"
/>
</div>
<div class="ch-settings-field">
<label class="ch-settings-label">Location</label>
<input type="text" name="location" class="ch-settings-input" value="{{ old('location', $user->location) }}" maxlength="100" placeholder="City, Country">
</div>
</div>
<div class="ch-settings-2col">
<div class="ch-settings-field">
<x-country-select
name="nationality"
label="Nationality"
placeholder="Select nationality"
value="{{ old('nationality', $user->nationality) }}"
/>
</div>
<div class="ch-settings-field">
<x-timezone-select
name="timezone"
label="Timezone"
placeholder="Select timezone"
value="{{ old('timezone', $user->timezone) }}"
/>
</div>
</div>
<div class="ch-settings-footer">
<button type="submit" class="action-btn action-btn-primary"><i class="bi bi-check-lg"></i> <span>Save Profile</span></button>
</div>
</form>
</div>
{{-- Social Links --}}
<div class="ch-settings-card">
<div class="ch-settings-card-title"><i class="bi bi-link-45deg"></i> Social Links</div>
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf @method('PUT')
<input type="hidden" name="_edit_user_id" value="{{ $user->id }}">
{{-- Must send name so the controller doesn't clear it --}}
<input type="hidden" name="name" value="{{ $user->name }}">
<x-social-links-editor :existing="$socialExisting" />
<div class="ch-settings-footer">
<button type="submit" class="action-btn action-btn-primary"><i class="bi bi-check-lg"></i> <span>Save Links</span></button>
</div>
</form>
</div>
</div>
{{-- Right: Security --}}
<div>
@if($isOwner)
<div class="ch-settings-card">
<div class="ch-settings-card-title"><i class="bi bi-shield-lock-fill"></i> Change Password</div>
<form method="POST" action="{{ route('settings.update') }}">
@csrf @method('PUT')
<div class="ch-settings-field">
<label class="ch-settings-label">Current Password</label>
<input type="password" name="current_password" class="ch-settings-input" required>
</div>
<div class="ch-settings-field">
<label class="ch-settings-label">New Password</label>
<input type="password" name="new_password" class="ch-settings-input" required minlength="8">
<div class="ch-settings-hint">Minimum 8 characters</div>
</div>
<div class="ch-settings-field">
<label class="ch-settings-label">Confirm New Password</label>
<input type="password" name="new_password_confirmation" class="ch-settings-input" required>
</div>
<div class="ch-settings-footer">
<button type="submit" class="action-btn action-btn-primary"><i class="bi bi-key-fill"></i> <span>Update Password</span></button>
</div>
</form>
</div>
@endif
@if($isOwner)
{{-- 2FA Card --}}
<div class="ch-settings-card" id="twoFactorCard">
<div class="ch-settings-card-title"><i class="bi bi-shield-check-fill"></i> Two-Factor Authentication</div>
@if($user->two_factor_enabled)
{{-- Enabled state --}}
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<span style="background:rgba(34,197,94,.15);border:1px solid #22c55e;color:#22c55e;border-radius:20px;padding:3px 12px;font-size:12px;font-weight:600;">
<i class="bi bi-check-circle-fill"></i> Enabled
</span>
<span style="font-size:13px;color:var(--text-secondary);">Your account is protected.</span>
</div>
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:16px;">
To disable 2FA, confirm your password below.
</p>
<form method="POST" action="{{ route('2fa.disable') }}">
@csrf
<div class="ch-settings-field">
<label class="ch-settings-label">Password</label>
<input type="password" name="password" class="ch-settings-input" required placeholder="••••••••">
</div>
<div class="ch-settings-footer">
<button type="submit" class="action-btn action-btn-danger"><i class="bi bi-shield-x"></i> <span>Disable 2FA</span></button>
</div>
</form>
@else
{{-- Disabled state --}}
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:20px;">
Add an extra layer of security. Once enabled, you'll need your authenticator app each time you sign in.
</p>
{{-- Step 1: Show QR --}}
<div id="tfa-step1" style="display:none;">
<p style="font-size:13px;font-weight:600;margin-bottom:8px;">Step 1 Scan this QR code with your authenticator app</p>
<div id="tfa-qr" style="background:#fff;display:inline-block;padding:10px;border-radius:8px;margin-bottom:12px;">
<img id="tfa-qr-img" src="" alt="QR Code" style="display:block;width:180px;height:180px;">
</div>
<p style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">Or enter the key manually:</p>
<code id="tfa-secret" style="font-size:13px;letter-spacing:.1em;background:rgba(255,255,255,.06);padding:4px 10px;border-radius:6px;display:inline-block;margin-bottom:20px;word-break:break-all;"></code>
<p style="font-size:13px;font-weight:600;margin-bottom:8px;">Step 2 Enter the 6-digit code to confirm</p>
<form method="POST" action="{{ route('2fa.enable') }}" id="tfa-confirm-form">
@csrf
<input type="hidden" name="code" id="tfa-code-hidden">
<div style="display:flex;gap:10px;align-items:center;">
<input type="text" id="tfa-code-input" inputmode="numeric" pattern="[0-9]*" maxlength="6"
placeholder="000000" autocomplete="one-time-code"
style="width:140px;background:rgba(255,255,255,.04);border:1px solid var(--border-color);border-radius:8px;padding:10px 14px;color:var(--text-primary);font-size:18px;letter-spacing:.3em;text-align:center;">
<button type="submit" class="action-btn action-btn-primary"><i class="bi bi-check-lg"></i> <span>Enable 2FA</span></button>
</div>
@if($errors->has('code'))
<p style="color:#ef4444;font-size:12px;margin-top:6px;">{{ $errors->first('code') }}</p>
@endif
</form>
</div>
<div id="tfa-step0">
<button type="button" class="action-btn action-btn-primary" id="tfa-start-btn" onclick="tfaStart()">
<i class="bi bi-shield-plus"></i> <span>Set Up 2FA</span>
</button>
</div>
@endif
</div>
@endif
@if($isOwner)
@php
$notifPrefs = $user->notification_preferences ?? [];
$notifDefs = \App\Models\User::notifDefaults();
function notifVal(array $prefs, array $defs, string $key): bool {
return isset($prefs[$key]) ? (bool)$prefs[$key] : ($defs[$key] ?? true);
}
@endphp
<div class="ch-settings-card" id="notifPrefsCard">
<div class="ch-settings-card-title"><i class="bi bi-bell-fill"></i> Notification Preferences</div>
{{-- Bell (In-App) --}}
<div class="notif-section-label"><i class="bi bi-bell"></i> In-App Notifications</div>
@php
$bellPrefs = [
['notif_new_comment', 'New comment on my video'],
['notif_new_reply', 'Reply to my comment'],
['notif_comment_like', 'Someone liked my comment'],
['notif_video_like', 'Someone liked my video'],
['notif_new_subscriber', 'New subscriber'],
['notif_new_video_from_sub', 'New video from channels I follow'],
['notif_new_post_from_sub', 'New post from channels I follow'],
];
if ($user->isSuperAdmin()) {
$bellPrefs[] = ['notif_new_user_reg', 'New user registration'];
}
@endphp
@foreach($bellPrefs as [$key, $label])
<div class="notif-pref-row">
<span class="notif-pref-label">{{ $label }}</span>
<label class="notif-toggle-wrap">
<input type="checkbox" class="notif-toggle-input"
data-key="{{ $key }}"
{{ notifVal($notifPrefs, $notifDefs, $key) ? 'checked' : '' }}
onchange="saveNotifPref('{{ $key }}', this.checked)">
<span class="notif-toggle-slider"></span>
</label>
</div>
@endforeach
{{-- Email --}}
<div class="notif-section-label" style="margin-top:20px;"><i class="bi bi-envelope"></i> Email Notifications</div>
@php
$emailPrefs = [
['email_new_comment', 'New comment on my video'],
['email_new_reply', 'Reply to my comment'],
['email_comment_like', 'Someone liked my comment'],
['email_video_like', 'Someone liked my video'],
['email_new_subscriber', 'New subscriber'],
['email_new_video_from_sub', 'New video from channels I follow'],
['email_new_post_from_sub', 'New post from channels I follow'],
['email_video_processed', 'My video finished processing'],
['email_weekly_digest', 'Weekly activity digest (every Monday)'],
];
if ($user->isSuperAdmin()) {
$emailPrefs[] = ['email_new_user_reg', 'New user registration'];
}
@endphp
@foreach($emailPrefs as [$key, $label])
<div class="notif-pref-row">
<span class="notif-pref-label">{{ $label }}</span>
<label class="notif-toggle-wrap">
<input type="checkbox" class="notif-toggle-input"
data-key="{{ $key }}"
{{ notifVal($notifPrefs, $notifDefs, $key) ? 'checked' : '' }}
onchange="saveNotifPref('{{ $key }}', this.checked)">
<span class="notif-toggle-slider"></span>
</label>
</div>
@endforeach
<p class="notif-pref-note">Security emails (verification, password reset) are always sent and cannot be disabled.</p>
</div>
@endif
<div class="ch-settings-card">
<div class="ch-settings-card-title"><i class="bi bi-info-circle-fill"></i> Account</div>
<div class="ch-settings-field">
<label class="ch-settings-label">Email</label>
<input type="email" class="ch-settings-input" value="{{ $user->email }}" disabled>
<div class="ch-settings-hint">Email cannot be changed</div>
</div>
<div class="ch-settings-field" style="margin-bottom:0;">
<label class="ch-settings-label">Member Since</label>
<input type="text" class="ch-settings-input" value="{{ $user->created_at->format('F d, Y') }}" disabled>
</div>
</div>
@if($isOwner)
<div class="ch-settings-card">
<div class="ch-settings-card-title"><i class="bi bi-devices"></i> Active Sessions</div>
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:16px;line-height:1.6;">
Sign out of all other browsers and devices where your account is currently logged in.
Your current session will remain active.
</p>
<form method="POST" action="{{ route('settings.logoutAllDevices') }}">
@csrf
<div class="ch-settings-field">
<label class="ch-settings-label">Confirm Password</label>
<input type="password" name="password" class="ch-settings-input" required placeholder="••••••••">
@error('logout_password')
<div style="color:#ef4444;font-size:12px;margin-top:5px;">{{ $message }}</div>
@enderror
</div>
<div class="ch-settings-footer">
<button type="submit" class="action-btn action-btn-danger"><i class="bi bi-box-arrow-right"></i> <span>Log Out All Other Devices</span></button>
</div>
</form>
</div>
@endif
</div>
</div>
</div>
@endif
{{-- Video picker modal (wall composer) --}}
@if(Auth::check() && Auth::id() === $user->id && $user->videos->count() > 0)
@php $pickerVideos = $user->videos()->latest()->get(); @endphp
<div id="videoPickerModal" class="ch-vp-backdrop" style="display:none;" onclick="if(event.target===this)closeVideoPicker()">
<div class="ch-vp-modal" onclick="event.stopPropagation()">
<div class="ch-vp-head">
<h3><i class="bi bi-collection-play-fill" style="color:#60a5fa;margin-right:8px;"></i>Select Videos</h3>
<button class="ch-vp-close" onclick="closeVideoPicker()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="ch-vp-search">
<input type="text" id="vpSearchInput" placeholder="Search your videos…" oninput="filterPickerVideos(this.value)">
</div>
<div class="ch-vp-grid" id="videoPickerGrid">
@foreach($pickerVideos as $v)
<div class="ch-vp-card"
data-id="{{ $v->id }}"
data-title="{{ $v->title }}"
data-thumb="{{ $v->thumbnail ? route('media.thumbnail', $v->thumbnail) : '' }}"
onclick="togglePickerCard(this)">
<div class="ch-vp-card-thumb">
@if($v->thumbnail)
<img src="{{ route('media.thumbnail', $v->thumbnail) }}" alt="" loading="lazy">
@else
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2);font-size:22px;"><i class="bi bi-film"></i></div>
@endif
<div class="ch-vp-card-check"><i class="bi bi-check"></i></div>
</div>
<div class="ch-vp-card-title">{{ $v->title }}</div>
</div>
@endforeach
</div>
<div class="ch-vp-footer">
<span class="ch-vp-count" id="vpSelectedCount">0 videos selected</span>
<button class="ch-vp-confirm" id="vpConfirmBtn" onclick="confirmVideoPicker()" disabled>Add to Post</button>
</div>
</div>
</div>
@endif
@endsection
@section('scripts')
<script>
/* ── Logout All Devices modal ── */
function openLogoutAllModal() {
if (document.getElementById('logoutAllModal')) return;
const tpl = document.getElementById('logoutAllModalTpl');
const node = tpl.content.cloneNode(true);
document.body.appendChild(node);
setTimeout(() => document.getElementById('logoutAllPwInput')?.focus(), 50);
}
function closeLogoutAllModal() {
const el = document.getElementById('logoutAllModal');
if (el) el.remove();
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLogoutAllModal(); });
/* ── Tab switching ── */
function switchTab(tabId, btn) {
document.querySelectorAll('.ch-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.ch-tab-content').forEach(c => c.classList.remove('active'));
if (!btn) btn = document.querySelector('.ch-tab[data-tab="' + tabId + '"]');
if (btn) btn.classList.add('active');
const content = document.getElementById('tab-' + tabId);
if (content) content.classList.add('active');
history.replaceState(null, '', '#' + tabId);
}
window.addEventListener('hashchange', function () {
const h = location.hash.replace('#', '');
if (h) {
const btn = document.querySelector('.ch-tab[data-tab="' + h + '"]');
if (btn) switchTab(h, btn);
}
});
document.addEventListener('DOMContentLoaded', function () {
const hash = location.hash.replace('#', '');
const urlParams = new URLSearchParams(location.search);
if (hash) {
const btn = document.querySelector('.ch-tab[data-tab="' + hash + '"]');
if (btn) switchTab(hash, btn);
} else if (urlParams.has('sort')) {
const btn = document.querySelector('.ch-tab[data-tab="videos"]');
if (btn) switchTab('videos', btn);
}
@if($errors->any() && $isOwner)
switchTab('settings', null);
@elseif(session('_open_tab') === 'settings' && $isOwner)
switchTab('settings', null);
@endif
// Animate compat bars
setTimeout(function () {
@if($compatibility !== null)
var bar = document.getElementById('compatBar');
if (bar) bar.style.width = '{{ $compatibility }}%';
var bar2 = document.getElementById('compatBarAbout');
if (bar2) bar2.style.width = '{{ $compatibility }}%';
var mini = document.getElementById('chCompatMiniBar');
if (mini) mini.style.width = '{{ $compatibility }}%';
@endif
}, 300);
// Video search filter
var vidSearch = document.getElementById('chVidSearch');
var vidGrid = document.getElementById('chVidGrid');
var noResults = document.getElementById('chVidNoResults');
if (vidSearch && vidGrid) {
vidSearch.addEventListener('input', function () {
var q = this.value.trim().toLowerCase();
var cards = vidGrid.querySelectorAll('.yt-video-card');
var visible = 0;
cards.forEach(function (card) {
var titleEl = card.querySelector('.yt-video-title');
var title = titleEl ? titleEl.textContent.toLowerCase() : '';
var show = !q || title.includes(q);
card.style.display = show ? '' : 'none';
if (show) visible++;
});
if (noResults) noResults.style.display = visible === 0 ? 'block' : 'none';
});
}
});
/* ── 2FA setup ── */
function tfaStart() {
const btn = document.getElementById('tfa-start-btn');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="bi bi-hourglass-split"></i> <span>Loading…</span>'; }
fetch('{{ route('2fa.setup') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
})
.then(r => r.json())
.then(data => {
document.getElementById('tfa-qr-img').src = 'data:image/svg+xml;base64,' + data.qr;
document.getElementById('tfa-secret').textContent = data.secret;
document.getElementById('tfa-step0').style.display = 'none';
document.getElementById('tfa-step1').style.display = '';
document.getElementById('tfa-code-input').focus();
})
.catch(() => {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-shield-plus"></i> <span>Set Up 2FA</span>'; }
});
}
document.addEventListener('DOMContentLoaded', function () {
const codeInput = document.getElementById('tfa-code-input');
const codeHidden = document.getElementById('tfa-code-hidden');
if (codeInput && codeHidden) {
document.getElementById('tfa-confirm-form').addEventListener('submit', function () {
codeHidden.value = codeInput.value;
});
}
});
/* ── Bio expand/collapse ── */
function toggleBio() {
const short = document.getElementById('ch-bio-short');
const full = document.getElementById('ch-bio-full');
const btn = document.getElementById('ch-bio-btn');
const isShort = full.style.display === 'none' || !full.style.display;
short.style.display = isShort ? 'none' : '';
full.style.display = isShort ? 'inline' : 'none';
btn.textContent = isShort ? ' Show less' : '…more';
}
/* ── Post composer — multi-image & video picker ── */
let _postImages = []; // array of {file, dataUrl}
function handlePostImages(input) {
const files = Array.from(input.files || []);
if (!files.length) return;
const remaining = 10 - _postImages.length;
files.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
_postImages.push({ file, dataUrl: e.target.result });
renderImageGrid();
};
reader.readAsDataURL(file);
});
input.value = '';
}
function renderImageGrid() {
const grid = document.getElementById('imagePreviewGrid');
if (!grid) return;
if (!_postImages.length) { grid.style.display = 'none'; grid.innerHTML = ''; return; }
const n = _postImages.length;
const cls = n === 1 ? 'count-1' : n === 2 ? 'count-2' : n === 3 ? 'count-3' : n === 4 ? 'count-4' : 'count-more';
grid.className = 'ch-composer-img-grid ' + cls;
grid.style.display = '';
grid.innerHTML = _postImages.map((img, i) => `
<div class="ch-composer-img-item">
<img src="${img.dataUrl}" alt="">
<button type="button" class="ch-composer-img-remove" onclick="removePostImage(${i})">
<i class="bi bi-x"></i>
</button>
</div>`).join('');
}
function removePostImage(idx) {
_postImages.splice(idx, 1);
renderImageGrid();
}
/* ── Video picker ── */
let _pickerSelected = {}; // {id: {id, title, thumb}}
function openVideoPicker() {
_pickerSelected = {};
document.querySelectorAll('.ch-vp-card').forEach(c => c.classList.remove('selected'));
document.getElementById('vpSelectedCount').textContent = '0 videos selected';
document.getElementById('vpConfirmBtn').disabled = true;
document.getElementById('vpSearchInput').value = '';
filterPickerVideos('');
document.getElementById('videoPickerModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
// restore already-selected chips
document.querySelectorAll('#postVideoChips [data-vid]').forEach(chip => {
const id = chip.dataset.vid;
const card = document.querySelector(`.ch-vp-card[data-id="${id}"]`);
if (card) {
card.classList.add('selected');
_pickerSelected[id] = { id, title: card.dataset.title, thumb: card.dataset.thumb };
}
});
updatePickerCount();
}
function closeVideoPicker() {
document.getElementById('videoPickerModal').style.display = 'none';
document.body.style.overflow = '';
}
function togglePickerCard(card) {
const id = card.dataset.id;
if (card.classList.contains('selected')) {
card.classList.remove('selected');
delete _pickerSelected[id];
} else {
card.classList.add('selected');
_pickerSelected[id] = { id, title: card.dataset.title, thumb: card.dataset.thumb };
}
updatePickerCount();
}
function updatePickerCount() {
const n = Object.keys(_pickerSelected).length;
document.getElementById('vpSelectedCount').textContent = n === 0 ? '0 videos selected' : `${n} video${n > 1 ? 's' : ''} selected`;
document.getElementById('vpConfirmBtn').disabled = n === 0;
}
function filterPickerVideos(q) {
const lower = q.toLowerCase();
document.querySelectorAll('.ch-vp-card').forEach(card => {
card.style.display = card.dataset.title.toLowerCase().includes(lower) ? '' : 'none';
});
}
function confirmVideoPicker() {
closeVideoPicker();
renderVideoChips();
}
function renderVideoChips() {
const wrap = document.getElementById('postVideoChips');
if (!wrap) return;
const ids = Object.values(_pickerSelected);
if (!ids.length) { wrap.style.display = 'none'; wrap.innerHTML = ''; return; }
wrap.style.display = '';
wrap.innerHTML = ids.map(v => `
<div class="ch-composer-video-chip" data-vid="${v.id}">
${v.thumb ? `<img class="ch-composer-video-chip-thumb" src="${v.thumb}" alt="">` : '<i class="bi bi-film" style="font-size:14px;flex-shrink:0;"></i>'}
<span>${v.title}</span>
<button type="button" class="ch-composer-video-chip-remove" onclick="removeVideoChip('${v.id}')">
<i class="bi bi-x"></i>
</button>
</div>`).join('');
}
function removeVideoChip(id) {
delete _pickerSelected[id];
renderVideoChips();
}
/* Intercept form submit — use fetch/FormData to send files + video ids */
(function () {
const form = document.getElementById('postForm');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
const fd = new FormData(this);
// Remove placeholder inputs we don't need
fd.delete('images[]');
fd.delete('video_ids[]');
// Add staged images
_postImages.forEach(img => fd.append('images[]', img.file));
// Add selected video ids
Object.keys(_pickerSelected).forEach(id => fd.append('video_ids[]', id));
const btn = this.querySelector('.ch-composer-post-btn');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Posting…'; }
fetch(this.action, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: fd,
credentials: 'same-origin',
})
.then(r => {
if (r.redirected) { window.location.href = r.url; return; }
if (!r.ok) return r.text().then(t => { throw new Error(t); });
window.location.reload();
})
.catch(err => {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-send-fill"></i> Post'; }
if (typeof showToast === 'function') showToast('Failed to post. Please try again.', 'error');
console.error(err);
});
});
})();
/* Escape closes video picker */
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
const modal = document.getElementById('videoPickerModal');
if (modal && modal.style.display !== 'none') closeVideoPicker();
}
});
/* ── Post three-dot menu ── */
function togglePostMenu(btn) {
const dropdown = btn.nextElementSibling;
const isOpen = dropdown.classList.contains('open');
// close all
document.querySelectorAll('.ch-post-dropdown.open').forEach(d => d.classList.remove('open'));
if (!isOpen) dropdown.classList.add('open');
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.ch-post-menu-wrap')) {
document.querySelectorAll('.ch-post-dropdown.open').forEach(d => d.classList.remove('open'));
}
});
/* ── Post like ── */
var csrfToken = '{{ csrf_token() }}';
function reactPost(btn) {
var url = btn.dataset.reactUrl;
var liked = btn.classList.contains('liked');
var anim = btn.querySelector('.ch-post-like-anim');
var icon = btn.querySelector('i');
var count = btn.querySelector('.post-like-count');
var label = btn.querySelector('span:last-child');
var newLiked = !liked;
// Optimistic update
btn.classList.toggle('liked', newLiked);
if (icon) icon.className = newLiked ? 'bi bi-heart-fill' : 'bi bi-heart';
if (label) label.textContent = newLiked ? 'Liked' : 'Like';
if (count) {
var n = parseInt(count.textContent) || 0;
count.textContent = (newLiked ? n + 1 : Math.max(0, n - 1)) || '';
}
if (anim && newLiked) { anim.classList.remove('pop'); void anim.offsetWidth; anim.classList.add('pop'); }
fetch(url, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' },
credentials: 'same-origin',
})
.then(r => r.json())
.then(data => {
btn.classList.toggle('liked', data.liked);
if (icon) icon.className = data.liked ? 'bi bi-heart-fill' : 'bi bi-heart';
if (label) label.textContent = data.liked ? 'Liked' : 'Like';
if (count) count.textContent = data.count || '';
})
.catch(() => {
btn.classList.toggle('liked', liked);
if (icon) icon.className = liked ? 'bi bi-heart-fill' : 'bi bi-heart';
if (label) label.textContent = liked ? 'Liked' : 'Like';
});
}
/* ── Image lightbox ── */
function openLightbox(src) {
var lb = document.getElementById('wallLightbox');
if (!lb) {
lb = document.createElement('div');
lb.id = 'wallLightbox';
lb.style.cssText = 'position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.92);display:flex;align-items:center;justify-content:center;cursor:zoom-out;backdrop-filter:blur(6px);';
lb.innerHTML = '<img style="max-width:92vw;max-height:92vh;border-radius:8px;box-shadow:0 32px 80px rgba(0,0,0,.7);object-fit:contain;" /><button style="position:absolute;top:20px;right:24px;background:rgba(255,255,255,.15);border:none;color:#fff;font-size:22px;width:44px;height:44px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;" onclick="this.parentElement.remove()"><i class="bi bi-x-lg"></i></button>';
lb.addEventListener('click', function(e) { if (e.target === lb) lb.remove(); });
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { var l = document.getElementById('wallLightbox'); if(l) l.remove(); } }, { once: true });
document.body.appendChild(lb);
}
lb.querySelector('img').src = src;
lb.style.display = 'flex';
}
/* Video hover-preview is wired by the video-card component's inline onmouseenter/onmouseleave (playVideo/stopVideo). Those handlers are audio-aware and keep the thumbnail visible for music tracks. Don't add a second listener here that overrides them. */
/* ── Subscribe toggle ── */
(function () {
document.addEventListener('click', function (e) {
var btn = e.target.closest('.subscribe-toggle-btn');
if (!btn) return;
var url = btn.dataset.subscribeUrl;
var subscribed = btn.dataset.subscribed === 'true';
var channelId = btn.dataset.channelId;
var nowSub = !subscribed;
btn.dataset.subscribed = nowSub ? 'true' : 'false';
btn.classList.toggle('subscribed-ch', nowSub);
var icon = btn.querySelector('i');
var label = btn.querySelector('.subscribe-label');
if (icon) icon.className = 'bi ' + (nowSub ? 'bi-bell-fill' : 'bi-person-plus-fill');
if (label) label.textContent = nowSub ? 'Subscribed' : 'Subscribe';
document.querySelectorAll('.channel-subs[data-channel-id="' + channelId + '"]').forEach(function (el) {
var n = parseInt(el.dataset.count || 0);
el.dataset.count = nowSub ? n + 1 : Math.max(0, n - 1);
el.innerHTML = '<i class="bi bi-people-fill"></i> ' + Number(el.dataset.count).toLocaleString() + ' ' + (el.dataset.count == 1 ? 'subscriber' : 'subscribers');
});
fetch(url, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' },
credentials: 'same-origin',
})
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(function (data) {
btn.dataset.subscribed = data.subscribed ? 'true' : 'false';
btn.classList.toggle('subscribed-ch', data.subscribed);
if (icon) icon.className = 'bi ' + (data.subscribed ? 'bi-bell-fill' : 'bi-person-plus-fill');
if (label) label.textContent = data.subscribed ? 'Subscribed' : 'Subscribe';
document.querySelectorAll('.channel-subs[data-channel-id="' + channelId + '"]').forEach(function (el) {
el.dataset.count = data.subscriber_count;
el.innerHTML = '<i class="bi bi-people-fill"></i> ' + Number(data.subscriber_count).toLocaleString() + ' ' + (data.subscriber_count == 1 ? 'subscriber' : 'subscribers');
});
})
.catch(function () {
btn.dataset.subscribed = subscribed ? 'true' : 'false';
btn.classList.toggle('subscribed-ch', subscribed);
if (icon) icon.className = 'bi ' + (subscribed ? 'bi-bell-fill' : 'bi-person-plus-fill');
if (label) label.textContent = subscribed ? 'Subscribed' : 'Subscribe';
if (typeof showToast === 'function') showToast('Could not update subscription. Please try again.', 'error');
});
});
})();
/* ── Settings avatar preview ── */
function previewSettingsAvatar(input) {
if (!input.files || !input.files[0]) return;
var reader = new FileReader();
reader.onload = function(e) {
var prev = document.getElementById('chSettingsAvatarPreview');
if (prev) prev.src = e.target.result;
// Also update the channel header avatar
var chAvatar = document.querySelector('.ch-avatar');
if (chAvatar) chAvatar.src = e.target.result;
};
reader.readAsDataURL(input.files[0]);
}
/* ── Delete video (own channel) ── */
function deleteVideo(videoId, videoTitle) {
showConfirm('Delete "' + videoTitle + '"? This cannot be undone.', function () {
fetch('/videos/' + videoId, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (data.success || data.redirect) window.location.reload();
else showToast('Failed to delete video.', 'error');
})
.catch(() => showToast('Failed to delete video.', 'error'));
}, 'Delete');
}
/* ── Cropper callbacks ── */
function onAvatarSaved(url) {
var img = document.getElementById('chAvatarImg');
if (img) img.src = url + '?t=' + Date.now();
// Also update header avatar in settings tab
var prev = document.getElementById('chSettingsAvatarPreview');
if (prev) prev.src = url + '?t=' + Date.now();
}
function onBannerSaved(url) {
var img = document.getElementById('chBannerImg');
if (img) {
img.src = url + '?t=' + Date.now();
} else {
var newImg = document.createElement('img');
newImg.id = 'chBannerImg';
newImg.className = 'ch-banner-img';
newImg.alt = '';
newImg.src = url + '?t=' + Date.now();
var banner = document.getElementById('chBanner');
if (banner) banner.insertBefore(newImg, banner.firstChild);
}
}
// ── Notification preference toggle ────────────────────────────────────────
function saveNotifPref(key, value) {
var csrf = document.querySelector('meta[name="csrf-token"]');
fetch('{{ route("settings.notifications") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrf ? csrf.getAttribute('content') : '',
'Accept': 'application/json',
},
body: JSON.stringify({ key: key, value: value }),
}).then(function(r) {
if (!r.ok) throw new Error('save failed');
}).catch(function() {
// Revert toggle on failure
var el = document.querySelector('[data-key="' + key + '"]');
if (el) el.checked = !value;
if (window.showToast) showToast('Could not save preference.', 'error');
});
}
</script>
@if($isOwner && !$preview)
<x-image-cropper
id="avatar"
:width="300"
:height="300"
shape="square"
folder="avatars"
:filename="'avatar_' . $user->id"
callback="onAvatarSaved"
update-url="{{ route('profile.updateAvatar') }}"
title="Change Profile Photo"
output-width="600"
/>
<x-image-cropper
id="banner"
:width="500"
:height="160"
shape="square"
folder="banners"
:filename="'banner_' . $user->id"
callback="onBannerSaved"
update-url="{{ route('profile.updateBanner') }}"
title="Change Banner Image"
output-width="1500"
/>
@endif
@endsection