refactor(episode): migrate episode upload to Livewire with real-time CDN verification
Replace the server-rendered episode upload form with a Livewire component, introducing an interactive admin experience that validates the new episode's stream path against all configured mirrors before saving. The controller-based `store` method and its route are removed, and episode creation now flows through the `AdminEpisodeForm` component with frontend upload handling and dynamic quality checks. Key changes: - Add `AdminEpisodeForm` Livewire component with file uploads, tag handling, and debounced stream/base-url validation. - Extend `CdnPathValidator::validateStream` to accept an episode number so the manifest path can point to the specific episode being added. - Remove the `store` method from `EpisodeController` and its POST route. - Delete the legacy `createEpisode` method from `EpisodeService`. - Replace the Blade upload modal with the Livewire view, including progress indicators and validation badges. - Add feature tests for the Livewire component and update unit tests for the CDN validator's episode-specific manifest check.
This commit is contained in:
@@ -6,76 +6,8 @@
|
||||
|
||||
<!--Modal body-->
|
||||
<div class="relative p-4 pt-0">
|
||||
<form method="POST" action="{{ route('admin.upload.episode') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<input name="episode_id" id="episode_id" type="hidden"/>
|
||||
|
||||
<!-- Episodes -->
|
||||
<div class="grid grid-cols-2">
|
||||
<!-- Cover -->
|
||||
<div class="p-4">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodecover1">Cover:</label>
|
||||
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" type="file" name="episodecover1" id="episodecover1" required>
|
||||
</div>
|
||||
<!-- Thumbs -->
|
||||
<div class="p-4">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodegallery1">Gallery:</label>
|
||||
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" type="file" name="episodegallery1[]" id="episodegallery1" multiple="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="description">Description:</label>
|
||||
<textarea rows="4" cols="50" id="description" name="description" class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm" required>{{ $episode->description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="baseurl">Stream:</label>
|
||||
<x-text-input id="baseurl" class="block w-full" type="text" name="baseurl" required />
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurl1">Download 1080p:</label>
|
||||
<x-text-input id="episodedlurl1" class="block w-full" type="text" name="episodedlurl1" required />
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurlinterpolated1">Download 1080p 48fps:</label>
|
||||
<x-text-input id="episodedlurlinterpolated1" class="block w-full" type="text" name="episodedlurlinterpolated1" />
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurl4k1">Download 4k:</label>
|
||||
<x-text-input id="episodedlurl4k1" class="block w-full" type="text" name="episodedlurl4k1" />
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="downloadUHDi1">Download 4k 48fps:</label>
|
||||
<x-text-input id="downloadUHDi1" class="block w-full" type="text" name="downloadUHDi1" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 flex-wrap items-center justify-end rounded-b-md p-4">
|
||||
<div class="inline-block mr-2">
|
||||
<input class="w-4 h-4 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
type="checkbox" value="true" id="censored" name="censored" />
|
||||
<label class="inline-block hover:cursor-pointer dark:text-white" for="censored">
|
||||
Censored Notification
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="inline-block rounded bg-primary-100 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-primary-700 transition duration-150 ease-in-out hover:bg-primary-accent-100 focus:bg-primary-accent-100 focus:outline-none focus:ring-0 active:bg-primary-accent-200" data-te-modal-dismiss data-te-ripple-init data-te-ripple-color="light">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="ml-1 inline-block rounded bg-rose-600 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600" data-te-ripple-init data-te-ripple-color="light">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@livewire('admin-episode-form', ['episodeId' => $episode->id])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modals JS -->
|
||||
<script>
|
||||
document.getElementById('episode_id').value = document.getElementById('e_id').value;
|
||||
</script>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user