Sports match share links now show the full VS composition (fighters,
flags, event, weight class, referee/court/match chips) in WhatsApp /
Telegram / Facebook / Twitter previews instead of the raw video frame.
- Browsershot + puppeteer-bundled Chromium screenshots a dedicated
1200x675 (true 16:9) render of the vs-mini composition. Chrome runs
through a small wrapper (bin/browsershot-node.sh) that raises the fd
limit and reads its full env, since PHP-FPM's stripped env otherwise
makes Chromium fail to launch.
- Render is JPEG @ q85 (~100 KB) instead of PNG (~700 KB) — WhatsApp
silently drops OG images above roughly 300 KB, which is why music
thumbnails always showed and match cards didn't.
- SportsMatchController@store/update renders synchronously so the
picture is guaranteed to exist before the user copies the share URL;
edits regenerate and old stamped variants are cleaned. The og:image
URL carries a ?v={updated_at} stamp so scrapers recache on edit.
- accessShare() (/s/{token}) now detects scraper user agents and serves
the video page HTML inline instead of a 302 — some crawlers don't
follow redirects when building link previews.
- og:description / twitter:description built from headerData(): flag
emoji + fighter names (blue vs red), event, weight, match#, court,
referee, view count, upload date.
- Fixed 3-dot menu on video cards being hidden behind the next card
(:has(.dropdown-menu.show) promotes the active card above siblings
and lifts the overflow:hidden clip on .yt-video-info).
- vs-mini: skip the entrance animation on touch devices and expose a
.vs-mini-static class so the OG render always captures the final
frame instead of a random mid-animation moment.
Also adds an admin gallery at /admin/og-previews for spot-checking the
generated preview JPEGs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
326 lines
22 KiB
PHP
326 lines
22 KiB
PHP
<?php
|
||
|
||
use App\Http\Controllers\CommentController;
|
||
use App\Http\Controllers\ImageUploadController;
|
||
use App\Http\Controllers\MatchEventController;
|
||
use App\Http\Controllers\MediaController;
|
||
use App\Http\Controllers\SportsMatchController;
|
||
use App\Http\Controllers\PostController;
|
||
use App\Http\Controllers\SuperAdminController;
|
||
use App\Http\Controllers\UserController;
|
||
use App\Http\Controllers\VideoController;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\Route;
|
||
|
||
// Media asset routes — NAS-aware image serving (public, no auth)
|
||
Route::get('/media/thumbnails/{filename}', [MediaController::class, 'thumbnail'])->name('media.thumbnail')->where('filename', '.+');
|
||
Route::get('/media/avatars/{filename}', [MediaController::class, 'avatar'])->name('media.avatar')->where('filename', '.+');
|
||
Route::get('/media/banners/{filename}', [MediaController::class, 'banner'])->name('media.banner')->where('filename', '.+');
|
||
Route::get('/media/post-images/{filename}', [MediaController::class, 'postImage'])->name('media.post-image')->where('filename', '.+');
|
||
Route::get('/media/sports/{filename}', [MediaController::class, 'sportsImage'])->name('media.sports-image')->where('filename', '.+');
|
||
|
||
// Root route - show videos
|
||
Route::get('/', [VideoController::class, 'index'])->name('home');
|
||
|
||
// Video routes - public
|
||
Route::get('/videos', [VideoController::class, 'index'])->name('videos.index');
|
||
Route::get('/videos/search', [VideoController::class, 'search'])->name('videos.search');
|
||
Route::get('/trending', [VideoController::class, 'trending'])->name('videos.trending');
|
||
Route::get('/shorts', [VideoController::class, 'shorts'])->name('videos.shorts');
|
||
Route::get('/videos/create', [VideoController::class, 'create'])->name('videos.create');
|
||
// Redirect legacy numeric video IDs to encoded URLs (backward compatibility)
|
||
Route::get('/videos/{numericId}', function ($numericId) {
|
||
$video = \App\Models\Video::findOrFail((int) $numericId);
|
||
$url = route('videos.show', $video);
|
||
if (request()->getQueryString()) {
|
||
$url .= '?' . request()->getQueryString();
|
||
}
|
||
return redirect($url, 301);
|
||
})->where('numericId', '[0-9]+');
|
||
|
||
Route::get('/videos/share/{token}', [VideoController::class, 'showByToken'])->name('videos.showByToken');
|
||
Route::get('/videos/{video}', [VideoController::class, 'show'])->name('videos.show');
|
||
Route::get('/videos/{video}/vs-preview', function (\App\Models\Video $video) {
|
||
abort_unless($video->type === 'match' && $video->sportsMatch, 404);
|
||
return view('videos.vs-preview', ['video' => $video]);
|
||
})->name('videos.vsPreview');
|
||
Route::get('/videos/{video}/stream', [VideoController::class, 'stream'])->name('videos.stream');
|
||
Route::get('/videos/{video}/audio-track/{track}', [VideoController::class, 'streamAudioTrack'])->name('videos.audio-track');
|
||
Route::get('/videos/{video}/hls/{file?}', [VideoController::class, 'hls'])->where(['file' => '.*'])->name('videos.hls');
|
||
Route::get('/videos/{video}/download', [VideoController::class, 'download'])->name('videos.download');
|
||
Route::get('/videos/{video}/download-mp3', [VideoController::class, 'downloadMp3'])->name('videos.downloadMp3');
|
||
Route::post('/videos/{video}/slideshow/generate', [VideoController::class, 'slideshowGenerate'])->name('videos.slideshow.generate');
|
||
Route::get('/videos/{video}/slideshow/progress', [VideoController::class, 'slideshowProgress'])->name('videos.slideshow.progress');
|
||
Route::get('/videos/{video}/recommendations', [VideoController::class, 'recommendations'])->name('videos.recommendations');
|
||
Route::get('/videos/{video}/player-data', [VideoController::class, 'playerData'])->name('videos.playerData');
|
||
Route::post('/videos/{video}/lyrics/generate', [VideoController::class, 'generateLyrics'])->name('videos.lyrics.generate')->middleware(['auth', 'throttle:20,1']);
|
||
Route::get('/videos/{video}/lyrics/progress', [VideoController::class, 'lyricsProgress'])->name('videos.lyrics.progress');
|
||
Route::post('/videos/{video}/lyrics/save', [VideoController::class, 'saveLyrics'])->name('videos.lyrics.save')->middleware(['auth', 'throttle:30,1']);
|
||
Route::delete('/videos/{video}/lyrics', [VideoController::class, 'deleteLyrics'])->name('videos.lyrics.delete')->middleware(['auth', 'throttle:20,1']);
|
||
Route::post('/videos/{video}/share', [VideoController::class, 'recordShare'])->name('videos.recordShare');
|
||
Route::post('/videos/{video}/share/email', [VideoController::class, 'shareByEmail'])->name('videos.shareEmail')->middleware(['auth', 'throttle:10,1']);
|
||
Route::post('/videos/{video}/share/members', [VideoController::class, 'shareWithMembers'])->name('videos.shareMembers')->middleware(['auth', 'throttle:20,1']);
|
||
Route::get('/videos/{video}/og-image', [VideoController::class, 'ogImage'])->name('videos.ogImage');
|
||
// vs-og-frame is the Browsershot-only, no-animation, 1200×630 render used by
|
||
// the OG image generator. Distinct from /vs-preview (line 43) which is a
|
||
// full-page interactive preview of the arena intro overlay.
|
||
Route::get('/videos/{video}/vs-og-frame', [VideoController::class, 'vsPreview'])->name('videos.vsOgFrame');
|
||
|
||
// Admin-only gallery of all generated OG preview PNGs (the composed VS
|
||
// cards that WhatsApp/Facebook see when a match video URL is shared).
|
||
Route::get('/admin/og-previews', function () {
|
||
abort_unless(auth()->check() && auth()->user()->isSuperAdmin(), 403);
|
||
$files = collect(glob(storage_path('app/og-cache/match-*.{jpg,png}'), GLOB_BRACE) ?: [])
|
||
->map(fn ($f) => [
|
||
'name' => basename($f),
|
||
'size' => filesize($f),
|
||
'mtime' => filemtime($f),
|
||
'id' => (int) (explode('-', pathinfo($f, PATHINFO_FILENAME))[1] ?? 0),
|
||
])
|
||
->sortByDesc('mtime')
|
||
->values();
|
||
return view('admin.og-previews', ['files' => $files]);
|
||
})->name('admin.og-previews');
|
||
|
||
Route::get('/admin/og-previews/{name}', function (string $name) {
|
||
abort_unless(auth()->check() && auth()->user()->isSuperAdmin(), 403);
|
||
abort_unless(preg_match('/^match-\d+-\d+\.(jpg|png)$/', $name), 404);
|
||
$path = storage_path('app/og-cache/' . $name);
|
||
abort_unless(is_file($path), 404);
|
||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||
return response()->file($path, ['Content-Type' => $ext === 'png' ? 'image/png' : 'image/jpeg']);
|
||
})->name('admin.og-previews.file');
|
||
Route::get('/s/{token}', [VideoController::class, 'accessShare'])->name('share.access');
|
||
Route::get('/videos/{video}/insights', [VideoController::class, 'insights'])->name('videos.insights')->middleware('auth');
|
||
Route::get('/videos/{video}/insights/country/{country}', [VideoController::class, 'insightsCountry'])->name('videos.insights.country')->middleware('auth');
|
||
Route::get('/videos/{video}/insights/day/{date}', [VideoController::class, 'insightsDay'])->name('videos.insights.day')->middleware('auth');
|
||
Route::get('/videos/{video}/insights/downloader/{userId}', [VideoController::class, 'insightsDownloaderHistory'])->name('videos.insights.downloader')->middleware('auth');
|
||
Route::get('/videos/{video}/insights/viewer/{who}', [VideoController::class, 'insightsViewer'])->where('who', '[ug]:[A-Za-z0-9\.:_-]+')->name('videos.insights.viewer')->middleware('auth');
|
||
Route::get('/videos/{video}/insights/share/{token}', [VideoController::class, 'insightsShare'])->where('token', '[A-Za-z0-9]+')->name('videos.insights.share')->middleware('auth');
|
||
Route::post('/videos/{video}/identify', [VideoController::class, 'identify'])->name('videos.identify');
|
||
|
||
// Video routes - auth + verified required
|
||
Route::middleware(['auth', 'verified'])->group(function () {
|
||
Route::get('/videos/create', [VideoController::class, 'create'])->name('videos.create');
|
||
Route::post('/videos', [VideoController::class, 'store'])->name('videos.store');
|
||
Route::post('/videos/upload-chunk', [VideoController::class, 'uploadChunk'])->name('videos.uploadChunk');
|
||
Route::get('/videos/{video}/edit', [VideoController::class, 'edit'])->name('videos.edit');
|
||
Route::put('/videos/{video}', [VideoController::class, 'update'])->name('videos.update');
|
||
Route::post('/videos/{video}/replace-file', [VideoController::class, 'replaceFile'])->name('videos.replaceFile');
|
||
Route::delete('/videos/{video}', [VideoController::class, 'destroy'])->name('videos.destroy');
|
||
Route::delete('/videos/{video}/audio-track/{track}', [VideoController::class, 'deleteAudioTrack'])->name('videos.audio-track.delete');
|
||
|
||
// Music-video merge — fold {source} into {target}. Owner-only.
|
||
Route::get('/videos/{target}/merge/candidates', [\App\Http\Controllers\VideoMergeController::class, 'candidates'])->name('videos.merge.candidates');
|
||
Route::get('/videos/{target}/merge/{source}/preview',[\App\Http\Controllers\VideoMergeController::class, 'preview'])->name('videos.merge.preview');
|
||
Route::post('/videos/{target}/merge/{source}', [\App\Http\Controllers\VideoMergeController::class, 'merge'])->name('videos.merge');
|
||
|
||
// Like/unlike routes
|
||
Route::post('/videos/{video}/like', [UserController::class, 'like'])->name('videos.like');
|
||
Route::post('/videos/{video}/unlike', [UserController::class, 'unlike'])->name('videos.unlike');
|
||
Route::post('/videos/{video}/toggle-like', [UserController::class, 'toggleLike'])->name('videos.toggleLike');
|
||
});
|
||
|
||
// Subscribe + notifications — auth but NOT email verification required
|
||
Route::middleware(['auth'])->group(function () {
|
||
Route::post('/channel/{user}/subscribe', [UserController::class, 'toggleSubscribe'])->name('channel.subscribe');
|
||
Route::get('/notifications/count', [UserController::class, 'notificationCount'])->name('notifications.count');
|
||
Route::get('/notifications/fetch', [UserController::class, 'fetchNotifications'])->name('notifications.fetch');
|
||
Route::post('/notifications/{id}/read', [UserController::class, 'markNotificationRead'])->name('notifications.read');
|
||
Route::post('/notifications/read-all', [UserController::class, 'markAllNotificationsRead'])->name('notifications.read-all');
|
||
Route::get('/users/search', [UserController::class, 'searchUsers'])->name('users.search');
|
||
});
|
||
|
||
// View progress + profile-visit tracking — accept both guests and authed users
|
||
Route::post('/videos/{video}/view-progress', [VideoController::class, 'trackProgress'])->name('videos.viewProgress');
|
||
Route::post('/profile-visits/{user}', [UserController::class, 'recordProfileVisit'])->name('profile.visit');
|
||
|
||
// Comment routes
|
||
Route::get('/videos/{video}/comments', [CommentController::class, 'index'])->name('comments.index');
|
||
Route::middleware(['auth', 'verified'])->group(function () {
|
||
Route::post('/videos/{video}/comments', [CommentController::class, 'store'])->name('comments.store');
|
||
Route::put('/comments/{comment}', [CommentController::class, 'update'])->name('comments.update');
|
||
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
|
||
Route::post('/comments/{comment}/like', [CommentController::class, 'like'])->name('comments.like');
|
||
});
|
||
|
||
// User routes
|
||
Route::middleware(['auth', 'verified'])->group(function () {
|
||
// Profile — redirect to own channel
|
||
Route::get('/profile', fn() => redirect()->route('channel', Auth::user()->channel))->name('profile');
|
||
Route::put('/profile', [UserController::class, 'updateProfile'])->name('profile.update');
|
||
|
||
// Settings
|
||
Route::get('/settings', [UserController::class, 'settings'])->name('settings');
|
||
Route::put('/settings', [UserController::class, 'updateSettings'])->name('settings.update');
|
||
Route::post('/settings/notifications', [UserController::class, 'updateNotificationPreferences'])->name('settings.notifications');
|
||
|
||
// History & Liked
|
||
Route::get('/history', [UserController::class, 'history'])->name('history');
|
||
Route::delete('/history', [UserController::class, 'clearHistory'])->name('history.clear');
|
||
Route::get('/liked', [UserController::class, 'liked'])->name('liked');
|
||
|
||
// Logout all other devices
|
||
Route::post('/settings/logout-all-devices', [UserController::class, 'logoutAllDevices'])->name('settings.logoutAllDevices');
|
||
|
||
// Image upload (cropper)
|
||
Route::post('/image-upload', [ImageUploadController::class, 'upload'])->name('image.upload');
|
||
Route::post('/profile/update-avatar', [UserController::class, 'updateAvatar'])->name('profile.updateAvatar');
|
||
Route::post('/profile/update-banner', [UserController::class, 'updateBanner'])->name('profile.updateBanner');
|
||
});
|
||
|
||
// Channel - public for viewing. {username} is the user's slug (non-sequential, safe to share).
|
||
Route::get('/channel/{username?}', [UserController::class, 'channel'])->name('channel');
|
||
|
||
// Post routes - auth required
|
||
Route::middleware(['auth'])->group(function () {
|
||
Route::post('/channel/{user}/posts', [PostController::class, 'store'])->name('posts.store');
|
||
Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
|
||
Route::post('/posts/{post}/react', [PostController::class, 'react'])->name('posts.react');
|
||
});
|
||
|
||
// Playlist routes
|
||
use App\Http\Controllers\PlaylistController;
|
||
|
||
// Get user playlists (for dropdown) - MUST be before /playlists/{playlist} route
|
||
Route::get('/user/playlists', [PlaylistController::class, 'userPlaylists'])->name('playlists.userPlaylists');
|
||
|
||
// Public playlist routes
|
||
Route::get('/playlists', [PlaylistController::class, 'index'])->name('playlists.index');
|
||
Route::get('/playlists/share/{token}', [PlaylistController::class, 'showByToken'])->name('playlists.showByToken');
|
||
Route::get('/playlists/{playlist}/og-image', [PlaylistController::class, 'ogImage'])->name('playlists.ogImage');
|
||
Route::get('/playlists/{playlist}', [PlaylistController::class, 'show'])->name('playlists.show');
|
||
Route::post('/playlists/{playlist}/share', [PlaylistController::class, 'recordShare'])->name('playlists.recordShare');
|
||
Route::get('/ps/{token}', [PlaylistController::class, 'accessShare'])->name('playlists.accessShare');
|
||
|
||
// Authenticated playlist routes
|
||
Route::middleware(['auth', 'verified'])->group(function () {
|
||
Route::get('/playlists/create', [PlaylistController::class, 'create'])->name('playlists.create');
|
||
Route::post('/playlists', [PlaylistController::class, 'store'])->name('playlists.store');
|
||
Route::get('/playlists/{playlist}/edit', [PlaylistController::class, 'edit'])->name('playlists.edit');
|
||
Route::put('/playlists/{playlist}', [PlaylistController::class, 'update'])->name('playlists.update');
|
||
Route::delete('/playlists/{playlist}', [PlaylistController::class, 'destroy'])->name('playlists.destroy');
|
||
|
||
// Playlist video management
|
||
Route::post('/playlists/{playlist}/videos', [PlaylistController::class, 'addVideo'])->name('playlists.addVideo');
|
||
Route::delete('/playlists/{playlist}/videos/{video}', [PlaylistController::class, 'removeVideo'])->name('playlists.removeVideo');
|
||
// Body-based remove (mirror of addVideo) — used by the add-to-playlist modal
|
||
// where the front-end only has the numeric video id, not the encoded route key.
|
||
Route::delete('/playlists/{playlist}/videos', [PlaylistController::class, 'removeVideoByBody'])->name('playlists.removeVideoByBody');
|
||
Route::put('/playlists/{playlist}/reorder', [PlaylistController::class, 'reorder'])->name('playlists.reorder');
|
||
|
||
// Playlist actions
|
||
Route::get('/playlists/{playlist}/play', [PlaylistController::class, 'playAll'])->name('playlists.playAll');
|
||
Route::get('/playlists/{playlist}/shuffle', [PlaylistController::class, 'shuffle'])->name('playlists.shuffle');
|
||
|
||
// Watch Later
|
||
Route::post('/videos/{video}/watch-later', [PlaylistController::class, 'watchLater'])->name('videos.watchLater');
|
||
});
|
||
|
||
// Two-Factor Authentication
|
||
use App\Http\Controllers\TwoFactorController;
|
||
Route::get('/2fa/challenge', [TwoFactorController::class, 'showChallenge'])->name('2fa.challenge');
|
||
Route::post('/2fa/challenge', [TwoFactorController::class, 'verifyChallenge'])->name('2fa.verify');
|
||
Route::middleware(['auth'])->group(function () {
|
||
Route::post('/2fa/setup', [TwoFactorController::class, 'setup'])->name('2fa.setup');
|
||
Route::post('/2fa/enable', [TwoFactorController::class, 'enable'])->name('2fa.enable');
|
||
Route::post('/2fa/disable', [TwoFactorController::class, 'disable'])->name('2fa.disable');
|
||
});
|
||
|
||
// Sports match records (progressive-disclosure creation flow)
|
||
Route::middleware(['auth'])->group(function () {
|
||
Route::post('/sports-matches', [SportsMatchController::class, 'store'])->name('sports-matches.store');
|
||
Route::get('/sports-matches/{sportsMatch}/edit', [SportsMatchController::class, 'edit'])->name('sports-matches.edit');
|
||
Route::put('/sports-matches/{sportsMatch}', [SportsMatchController::class, 'update'])->name('sports-matches.update');
|
||
});
|
||
// Signed confirmation link — no auth required (user may click from a different device/browser)
|
||
Route::get('/2fa/disable/confirm', [TwoFactorController::class, 'confirmDisable'])->name('2fa.disable.confirm');
|
||
|
||
// Authentication Routes
|
||
require __DIR__.'/auth.php';
|
||
|
||
// Super Admin Routes
|
||
Route::middleware(['auth', 'super_admin'])->prefix('admin')->name('admin.')->group(function () {
|
||
// Dashboard
|
||
Route::get('/dashboard', [SuperAdminController::class, 'dashboard'])->name('dashboard');
|
||
Route::get('/dashboard/modal', [SuperAdminController::class, 'modalData'])->name('dashboard.modal');
|
||
|
||
// User Management
|
||
Route::get('/users', [SuperAdminController::class, 'users'])->name('users');
|
||
Route::get('/users/{user}/edit', [SuperAdminController::class, 'editUser'])->name('users.edit');
|
||
Route::put('/users/{user}', [SuperAdminController::class, 'updateUser'])->name('users.update');
|
||
Route::delete('/users/{user}', [SuperAdminController::class, 'deleteUser'])->name('users.delete');
|
||
Route::post('/users/{user}/impersonate', [SuperAdminController::class, 'impersonate'])->name('users.impersonate');
|
||
Route::post('/users/{user}/verify', [SuperAdminController::class, 'verifyUser'])->name('users.verify');
|
||
|
||
// Video Management
|
||
Route::get('/videos', [SuperAdminController::class, 'videos'])->name('videos');
|
||
Route::get('/videos/{video}/analytics', [SuperAdminController::class, 'videoAnalytics'])->name('videos.analytics');
|
||
Route::get('/videos/{video}/edit', [SuperAdminController::class, 'editVideo'])->name('videos.edit');
|
||
Route::put('/videos/{video}', [SuperAdminController::class, 'updateVideo'])->name('videos.update');
|
||
Route::delete('/videos/{video}', [SuperAdminController::class, 'deleteVideo'])->name('videos.delete');
|
||
|
||
// Orphan cleanup
|
||
Route::post('/cleanup-orphans', [SuperAdminController::class, 'cleanupOrphanedVideos'])->name('admin.cleanup.orphans');
|
||
|
||
// Log viewer
|
||
Route::get('/logs', [SuperAdminController::class, 'logs'])->name('logs');
|
||
|
||
// Audit log center
|
||
Route::get('/audit', [SuperAdminController::class, 'auditLogs'])->name('audit');
|
||
|
||
// System settings
|
||
Route::get('/settings', [SuperAdminController::class, 'settings'])->name('settings');
|
||
Route::post('/settings', [SuperAdminController::class, 'updateSettings'])->name('settings.update');
|
||
Route::get('/settings/detect-gpu', [SuperAdminController::class, 'detectGpu'])->name('settings.detect-gpu');
|
||
Route::post('/settings/llm-test', [SuperAdminController::class, 'llmProviderTest'])->name('settings.llm-test');
|
||
|
||
// Lyrics Pipeline toggles (own page)
|
||
Route::get('/lyrics', [SuperAdminController::class, 'lyrics'])->name('lyrics');
|
||
|
||
// GPU Accelerator (own page)
|
||
Route::get('/gpu', [SuperAdminController::class, 'gpu'])->name('gpu');
|
||
|
||
// Backup & Restore (own page)
|
||
Route::get('/backup', [SuperAdminController::class, 'backup'])->name('backup');
|
||
|
||
// NAS Storage
|
||
Route::get('/nas-storage', [SuperAdminController::class, 'nasStorage'])->name('nas-storage');
|
||
Route::post('/nas-repair', [SuperAdminController::class, 'nasRepair'])->name('admin.nas.repair');
|
||
Route::post('/nas-delete', [SuperAdminController::class, 'nasDelete'])->name('nas.delete');
|
||
|
||
// NAS Disable Flow
|
||
Route::post('/nas/disable', [SuperAdminController::class, 'nasDisable'])->name('nas.disable');
|
||
Route::get('/nas/migrate-progress', [SuperAdminController::class, 'nasMigrateProgress'])->name('nas.migrate-progress');
|
||
|
||
// Backup & Restore
|
||
Route::get('/backup/users-settings', [SuperAdminController::class, 'backupUsersSettings'])->name('backup.users-settings');
|
||
Route::post('/backup/restore', [SuperAdminController::class, 'restoreUsersSettings'])->name('backup.restore');
|
||
});
|
||
|
||
// Exit impersonation — accessible by any authenticated user (the impersonated user hitting exit)
|
||
Route::post('/impersonate/exit', [SuperAdminController::class, 'exitImpersonation'])
|
||
->middleware('auth')
|
||
->name('impersonate.exit');
|
||
|
||
// Match Events Routes (removed auth requirement for demo purposes)
|
||
// In production, wrap with: Route::middleware('auth')->group(function () {
|
||
|
||
// Get match data - public for viewing
|
||
Route::get('/videos/{video}/match-data', [MatchEventController::class, 'getMatchData'])->name('match.getData');
|
||
|
||
// Round CRUD
|
||
Route::post('/videos/{video}/rounds', [MatchEventController::class, 'storeRound'])->name('match.storeRound');
|
||
Route::put('/rounds/{round}', [MatchEventController::class, 'updateRound'])->name('match.updateRound');
|
||
Route::delete('/rounds/{round}', [MatchEventController::class, 'destroyRound'])->name('match.destroyRound');
|
||
|
||
// Point CRUD
|
||
Route::post('/videos/{video}/points', [MatchEventController::class, 'storePoint'])->name('match.storePoint');
|
||
Route::put('/points/{point}', [MatchEventController::class, 'updatePoint'])->name('match.updatePoint');
|
||
Route::delete('/points/{point}', [MatchEventController::class, 'destroyPoint'])->name('match.destroyPoint');
|
||
|
||
// Coach Review CRUD
|
||
Route::post('/videos/{video}/reviews', [MatchEventController::class, 'storeReview'])->name('match.storeReview');
|
||
Route::put('/reviews/{review}', [MatchEventController::class, 'updateReview'])->name('match.updateReview');
|
||
Route::delete('/reviews/{review}', [MatchEventController::class, 'destroyReview'])->name('match.destroyReview');
|