fix(channel): grid, scoreboard thumb, sort hash, visitor privacy
Audit fixes against /channel/{slug}:
1. Fluid video grid — repeat(auto-fill, minmax(260px, 1fr)) replaces
the three fixed breakpoints (3/2/4 cols at 1101/1500/… px) that
produced different card widths and could overflow 390 / 1280 /
1920. min-width:0 on every grid child so a long unbroken title
(CJK, "SPARTAWORRIOR") can't widen its track past 1fr.
2. Scoreboard thumbnail — stop including vs-mini + scorebar-mini on
match cards. Those partials leaked fighter names, "AKA/AO",
"ROUND 1", "0:00" into the card DOM, showing stale 0-0 for
finished matches and breaking title layout at narrow widths. The
regular <img> above is the thumbnail now; VS intro + live scorebar
still render on the full match player page where they belong.
3. Sort pills — Latest/Popular/Oldest now append #videos so the page
stays on the Videos tab after reload instead of jumping to Wall.
4. Shorts tab — always render for the owner (so the "My Shorts"
sidebar link always lands somewhere), with an empty-state card
when no shorts exist. Visitors still see it only when shorts > 0.
5. Subscriptions sidebar link — href="#" replaced by a real
/feed/subscriptions route backed by UserController@subscriptionsFeed
that reuses the videos/index view with the subscription feed.
6. Header view count — totalViews now sums video_views over public
videos for visitors (owner still sees the full total). Previously
leaked unlisted and private video view counts.
7. Card redundancy — new hideChannelInfo prop on <x-video-card/>
suppresses the per-card avatar + channel-name row. The channel
page passes it; everywhere else keeps the row.
9. Decorative logo — the second <img alt="Play"> in the responsive
header pair now has alt="" so a screen reader doesn't announce
"Play Play". ⋮ menu button gets aria-label="More actions".
10. Privacy — age, gender, location and horoscope strip now only
render to the owner on the channel header (horoscope strip is
also allowed when the compatibility match-up is being shown to
a signed-in visitor). Joined-month stays public.
11. Notifications dropdown — initial state is "No notifications"
not an indefinite "Loading…". Opening the panel now shows a
spinner while fetching, surfaces a Retry button on error, and
bails out of the "Loading…" state on non-ok HTTP responses.
Title clamp — .yt-video-title is now a 2-line box with
overflow-wrap:anywhere so CJK titles and long words wrap mid-word
and can't widen the card past its track.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
d9f97a672e
commit
a25af30700
9 files changed
+148
-50
No files matched your search
@@ -136,7 +136,14 @@ $headerSocialMap = [
|
||||
<i class="bi bi-people-fill"></i>
|
||||
{{ number_format($user->subscriber_count) }} {{ Str::plural('subscriber', $user->subscriber_count) }}
|
||||
</span>
|
||||
@if($user->location)
|
||||
{{-- Location, age and gender used to render in the public
|
||||
header meta row — visible to every visitor. That's a
|
||||
privacy leak the user flagged; those three now show
|
||||
only to the owner here, and only in the About tab for
|
||||
everyone (via the existing ch-about-card below). Join
|
||||
month stays public because it's channel metadata, not
|
||||
personal data. --}}
|
||||
@if($isOwner && $user->location)
|
||||
<span class="ch-meta-sep">·</span>
|
||||
<span class="ch-meta-item">
|
||||
<i class="bi bi-geo-alt-fill"></i>
|
||||
@@ -148,7 +155,7 @@ $headerSocialMap = [
|
||||
<i class="bi bi-calendar3"></i>
|
||||
Joined {{ $user->created_at->format('M Y') }}
|
||||
</span>
|
||||
@if($user->birthday)
|
||||
@if($isOwner && $user->birthday)
|
||||
@php $age = \Carbon\Carbon::parse($user->birthday)->age; @endphp
|
||||
<span class="ch-meta-sep">·</span>
|
||||
<span class="ch-meta-item">
|
||||
@@ -156,13 +163,13 @@ $headerSocialMap = [
|
||||
{{ $age }} yrs
|
||||
</span>
|
||||
@endif
|
||||
@if($user->gender === 'male')
|
||||
@if($isOwner && $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')
|
||||
@elseif($isOwner && $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>
|
||||
@@ -181,18 +188,25 @@ $headerSocialMap = [
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Horoscope + Compatibility strip & Social buttons side by side --}}
|
||||
@if($horoscope || $socialLinks->isNotEmpty())
|
||||
{{-- Horoscope + Compatibility strip & Social buttons side by side.
|
||||
The horoscope portion is sign-derived from the birthday the
|
||||
owner entered; it only renders to the owner (or to a visitor
|
||||
who's seeing the compatibility match-up). Pure social buttons
|
||||
still show to visitors. --}}
|
||||
@php
|
||||
$hColor = $horoscope ? \App\Helpers\Horoscope::elementColor($horoscope['element']) : null;
|
||||
$showCompat = $horoscope && ($compatibility !== null && $viewerSign && !$isOwner);
|
||||
$showHoroStrip = $horoscope && ($isOwner || $showCompat);
|
||||
@endphp
|
||||
@if($showHoroStrip || $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)
|
||||
@if($showHoroStrip)
|
||||
<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">
|
||||
@@ -361,10 +375,15 @@ $headerSocialMap = [
|
||||
<span class="ch-tab-badge">{{ $videos->count() }}</span>
|
||||
@endif
|
||||
</button>
|
||||
@if($shorts->count() > 0)
|
||||
{{-- Always show the Shorts tab for the owner so the "My Shorts"
|
||||
sidebar link always lands somewhere; for visitors only when
|
||||
there's content to show. --}}
|
||||
@if($isOwner || $shorts->count() > 0)
|
||||
<button class="ch-tab" data-tab="shorts" onclick="switchTab('shorts', this)">
|
||||
Shorts
|
||||
@if($shorts->count() > 0)
|
||||
<span class="ch-tab-badge">{{ $shorts->count() }}</span>
|
||||
@endif
|
||||
</button>
|
||||
@endif
|
||||
@if(($playlists && $playlists->count() > 0) || $isOwner)
|
||||
@@ -663,18 +682,18 @@ $headerSocialMap = [
|
||||
<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']) }}"
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'latest']) . '#videos' }}"
|
||||
class="ch-sort-pill {{ $sort === 'latest' ? 'active' : '' }}">Latest</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'popular']) }}"
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'popular']) . '#videos' }}"
|
||||
class="ch-sort-pill {{ $sort === 'popular' ? 'active' : '' }}">Popular</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'oldest']) }}"
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'oldest']) . '#videos' }}"
|
||||
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])
|
||||
@include('components.video-card', ['video' => $video, 'hideChannelInfo' => true])
|
||||
@endforeach
|
||||
<div class="ch-vid-no-results" id="chVidNoResults">
|
||||
<i class="bi bi-search"></i>
|
||||
@@ -699,8 +718,15 @@ $headerSocialMap = [
|
||||
</div>
|
||||
|
||||
{{-- ─────────────────────── SHORTS TAB ──────────────── --}}
|
||||
@if($shorts->count() > 0)
|
||||
@if($isOwner || $shorts->count() > 0)
|
||||
<div class="ch-tab-content" id="tab-shorts">
|
||||
@if($shorts->isEmpty())
|
||||
<div class="ch-empty">
|
||||
<div class="ch-empty-icon"><i class="bi bi-phone"></i></div>
|
||||
<h3>No shorts yet</h3>
|
||||
<p>Vertical videos under 60 seconds appear here.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="ch-shorts-grid">
|
||||
@foreach($shorts as $short)
|
||||
<a href="{{ route('videos.show', $short) }}" class="ch-short-card">
|
||||
@@ -722,6 +748,7 @@ $headerSocialMap = [
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
Reference in new issue
Block a user