Restores the live streaming controllers / views / model from the
pre-audit WIP that got stashed on 2026-10-21, then applies the Phase 6
polish items:
## /live/new form
Add a Private visibility option that mirrors the upload modal's
wording — "Only you can see this stream. Perfect for testing without
an audience." Validator in LiveStreamController@store now permits
public|unlisted|private (was public|unlisted).
## Watch page ended-state
When the stream status is "ended", the overlay is a solid "Stream
ended" card showing the stream's duration (started_at → ended_at)
and, when a recording was published, a Watch Replay CTA linking to
the saved video. Previously an ended stream showed the generic
"Waiting for the broadcast to start" placeholder forever.
## Channel name link
The uploader's name in the meta row is now a real <a href> to the
channel page.
## Viewer action row
Subscribe + Share buttons below the player, visible to anyone except
the owner. Subscribe for guests uses the Phase 7 ?intended= pattern
so after sign-in they land back on the stream.
## Live chat (DB-backed polling)
New live_chat_messages table (migration 2026_10_21_000003). Viewers
GET /live/{stream}/chat?since=N every 3 seconds; signed-in users POST
to the same path (rate-limited at 60/min). Guests see the panel in
read-only mode with a "Sign in to chat" placeholder.
Private streams reject chat for anyone but the owner (404). Non-live
status rejects chat sends (422). Author name is snapshotted at post
time so a renamed account's old messages keep the name they used.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
90 lines
4.4 KiB
PHP
90 lines
4.4 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'Go Live')
|
|
|
|
@section('extra_styles')
|
|
<style>
|
|
.lc-wrap { max-width: 560px; margin: 0 auto; padding: 24px 16px 60px; }
|
|
.lc-wrap h1 { font-size: 22px; font-weight: 700; margin: 0 0 6px; display: flex; align-items: center; gap: 10px; }
|
|
.lc-sub { font-size: 13px; opacity: .6; margin: 0 0 26px; line-height: 1.6; }
|
|
.lc-field { margin-bottom: 18px; }
|
|
.lc-field label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 7px; }
|
|
.lc-field input[type=text], .lc-field textarea {
|
|
width: 100%; background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.14);
|
|
border-radius: 11px; padding: 11px 13px; color: inherit; font-size: 14px; font-family: inherit;
|
|
}
|
|
.lc-field input:focus, .lc-field textarea:focus { outline: none; border-color: #ef4444; }
|
|
.lc-field textarea { min-height: 88px; resize: vertical; }
|
|
.lc-err { font-size: 12px; color: #f87171; margin-top: 6px; }
|
|
|
|
.lc-vis { display: grid; gap: 10px; }
|
|
.lc-vis label {
|
|
display: flex; gap: 12px; align-items: flex-start; padding: 13px 15px; cursor: pointer;
|
|
border: 1px solid rgba(255,255,255,.14); border-radius: 12px; transition: border-color .15s ease, background .15s ease;
|
|
}
|
|
.lc-vis label:hover { background: rgba(255,255,255,.04); }
|
|
.lc-vis input { margin-top: 3px; accent-color: #ef4444; }
|
|
.lc-vis .t { font-size: 14px; font-weight: 600; }
|
|
.lc-vis .d { font-size: 12px; opacity: .6; margin-top: 2px; line-height: 1.5; }
|
|
.lc-vis input:checked + div .t { color: #ef4444; }
|
|
</style>
|
|
@endsection
|
|
|
|
@section('content')
|
|
<div class="lc-wrap">
|
|
<h1><i class="bi bi-broadcast" style="color:#ef4444;"></i> Go Live</h1>
|
|
<p class="lc-sub">Stream straight from your phone's camera. You'll get a link to share the moment you start.</p>
|
|
|
|
<form method="POST" action="{{ route('live.store') }}">
|
|
@csrf
|
|
|
|
<div class="lc-field">
|
|
<label for="title">Title</label>
|
|
<input type="text" id="title" name="title" value="{{ old('title') }}" maxlength="120" required
|
|
placeholder="What are you streaming?" autofocus>
|
|
@error('title')<div class="lc-err">{{ $message }}</div>@enderror
|
|
</div>
|
|
|
|
<div class="lc-field">
|
|
<label for="description">Description <span style="opacity:.5;font-weight:400;">(optional)</span></label>
|
|
<textarea id="description" name="description" maxlength="2000" placeholder="Tell viewers what to expect">{{ old('description') }}</textarea>
|
|
@error('description')<div class="lc-err">{{ $message }}</div>@enderror
|
|
</div>
|
|
|
|
<div class="lc-field">
|
|
<label>Who can find it</label>
|
|
<div class="lc-vis">
|
|
<label>
|
|
<input type="radio" name="visibility" value="public" {{ old('visibility', 'public') === 'public' ? 'checked' : '' }}>
|
|
<div>
|
|
<div class="t">Public</div>
|
|
<div class="d">Listed on the Live page — anyone can discover and watch it.</div>
|
|
</div>
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="visibility" value="unlisted" {{ old('visibility') === 'unlisted' ? 'checked' : '' }}>
|
|
<div>
|
|
<div class="t">Unlisted</div>
|
|
<div class="d">Not listed anywhere. Only people you send the link to can watch.</div>
|
|
</div>
|
|
</label>
|
|
{{-- Mirror the upload-modal's Private wording: only the owner
|
|
sees it. Useful for test runs and private rehearsals. --}}
|
|
<label>
|
|
<input type="radio" name="visibility" value="private" {{ old('visibility') === 'private' ? 'checked' : '' }}>
|
|
<div>
|
|
<div class="t">Private</div>
|
|
<div class="d">Only you can see this stream. Perfect for testing without an audience.</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
@error('visibility')<div class="lc-err">{{ $message }}</div>@enderror
|
|
</div>
|
|
|
|
<button type="submit" class="action-btn action-btn-primary" style="width:100%;justify-content:center;height:46px;">
|
|
<i class="bi bi-camera-video"></i> <span>Set up camera</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
@endsection
|