Files
takeone-youtube-clone/routes/web.php
T
ghassanandClaude Opus 4.7 8c6746e547 feat(studio): /studio content table + bulk visibility / bulk delete
Skeleton for the creator studio surface deferred since Phase 6. The
/studio route lands on a filterable table of the signed-in user's
videos — All / Public / Unlisted / Private / Shorts / Processing-or-
Failed — with headline totals (public / unlisted / private counts,
processing + failed, total views) in a stat strip above.

Each row shows thumbnail + title + stripped description preview +
visibility badge + per-video totals. Row-level checkboxes raise a
toolbar when any row is selected; the toolbar supports:
  - Change visibility across the selection (any of the three visibility
    enums).
  - Delete the selection with a confirm prompt.

Server-side the bulk endpoints owner-filter by Auth::id() so a crafted
request body can't flip or delete anyone else's videos. Response is
JSON { updated | deleted : N } so the page can reload / toast cleanly.

Sidebar now carries a persistent "Studio" link under the You section
alongside History / Liked Videos.

Analytics, stepped upload (Details → Visibility), and schedule remain
future work — this ships the content management surface, nothing else.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-10-21 12:35:47 +03:00

372 lines
25 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.
<?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/feed/more', [VideoController::class, 'feedMore'])->name('videos.feed.more');
// Site-wide search. /videos/search is the historical URL; keep it as a
// signed-forward redirect so bookmarks and external links don't break.
Route::get('/search', [\App\Http\Controllers\SearchController::class, 'index'])->name('search');
Route::get('/videos/search', function (\Illuminate\Http\Request $request) {
return redirect()->route('search', $request->only('q'), 301);
})->name('videos.search');
Route::get('/trending', [VideoController::class, 'trending'])->name('videos.trending');
Route::get('/shorts', [VideoController::class, 'shorts'])->name('videos.shorts');
// Deep-link form: /shorts/{encoded-id} loads the feed scrolled to that short.
// The resolveRouteBinding on Video decodes the id; if it's gone, 404 lands
// on the branded errors/404 page.
Route::get('/shorts/{video}', [VideoController::class, 'shortsFocused'])->name('videos.shorts.focused');
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}/description/translate', [VideoController::class, 'translateDescription'])->name('videos.description.translate')->middleware('throttle:30,1');
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');
Route::post('/videos/{video}/toggle-dislike', [UserController::class, 'toggleDislike'])->name('videos.toggleDislike');
});
// 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::post('/comments/{comment}/translate', [CommentController::class, 'translate'])->name('comments.translate');
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');
Route::post('/comments/{comment}/heart', [CommentController::class, 'heart'])->name('comments.heart');
Route::post('/comments/{comment}/pin', [CommentController::class, 'pin'])->name('comments.pin');
});
// 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');
Route::get('/feed/subscriptions', [UserController::class, 'subscriptionsFeed'])->name('subscriptions.feed');
// Creator studio
Route::get('/studio', [\App\Http\Controllers\StudioController::class, 'index'])->name('studio');
Route::post('/studio/bulk/visibility', [\App\Http\Controllers\StudioController::class, 'bulkVisibility'])->name('studio.bulkVisibility');
Route::post('/studio/bulk/delete', [\App\Http\Controllers\StudioController::class, 'bulkDelete'])->name('studio.bulkDelete');
// 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');
Route::get('/channel/{username}/videos/more', [UserController::class, 'channelVideosMore'])
->name('channel.videos.more');
// YouTube-style @handle alias. For now `handle` is just the existing
// `username` slug — if the two ever diverge, flip the controller lookup
// in UserController::channel() to prefer a dedicated `handle` column.
// The {handle} segment must be url-encoded (e.g. /%40alice) OR plain `@alice`
// — Laravel's router accepts both because `@` is allowed in URI path segments.
Route::get('/@{handle}', [UserController::class, 'channel'])
->where('handle', '[A-Za-z0-9][A-Za-z0-9._-]*')
->name('channel.handle');
// Tab aliases: /@handle/videos lands on the Videos tab, /@handle/shorts on
// Shorts, etc. These bridge to the single-page channel view by forwarding
// the tab name as a hash — the channel.blade.php JS already handles
// #wall/#videos/#shorts/#playlists/#about/#posts on load.
Route::get('/@{handle}/{tab}', function (string $handle, string $tab) {
$allowed = ['wall', 'videos', 'shorts', 'playlists', 'posts', 'about'];
if (! in_array($tab, $allowed, true)) {
abort(404);
}
$user = \App\Models\User::where('username', $handle)->firstOrFail();
return redirect(route('channel', $user->channel) . '#' . $tab, 302);
})->where(['handle' => '[A-Za-z0-9][A-Za-z0-9._-]*', 'tab' => '[a-z]+'])
->name('channel.handle.tab');
// 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');