thumbnail update feature
This commit is contained in:
@@ -90,8 +90,19 @@ class CreatorDashboardController extends Controller
|
||||
'slug' => ['required', 'string', 'min:3', 'max:40', 'alpha_dash:ascii', 'lowercase', Rule::unique('channels', 'slug')->ignore($channel)],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'category_id' => ['nullable', 'exists:categories,id'],
|
||||
'thumbnail' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
||||
]);
|
||||
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
if ($channel->thumbnail_path) {
|
||||
Storage::disk('public')->delete($channel->thumbnail_path);
|
||||
}
|
||||
|
||||
$validated['thumbnail_path'] = $request->file('thumbnail')->store("channels/{$channel->id}/thumbnails", 'public');
|
||||
}
|
||||
|
||||
unset($validated['thumbnail']);
|
||||
|
||||
$channel->update([
|
||||
...$validated,
|
||||
'display_name' => trim($validated['display_name']),
|
||||
|
||||
@@ -33,7 +33,7 @@ return [
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'serve' => false,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Check,
|
||||
Copy,
|
||||
Eye,
|
||||
ImagePlus,
|
||||
Radio,
|
||||
RotateCw,
|
||||
Save,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
Video,
|
||||
} from 'lucide-react';
|
||||
import type { FormEvent, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -73,6 +74,10 @@ export default function Dashboard({
|
||||
}: Props) {
|
||||
const [copiedText, copy] = useClipboard();
|
||||
const [stopTarget, setStopTarget] = useState<BroadcastSummary | null>(null);
|
||||
const [thumbnailPreviewUrl, setThumbnailPreviewUrl] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const thumbnailInputRef = useRef<HTMLInputElement>(null);
|
||||
const liveBroadcast = channel?.live_broadcast ?? null;
|
||||
const pendingBroadcast =
|
||||
recentBroadcasts.find((broadcast) => broadcast.status === 'pending') ??
|
||||
@@ -84,16 +89,26 @@ export default function Dashboard({
|
||||
category_id: '',
|
||||
});
|
||||
const channelForm = useForm({
|
||||
_method: 'PATCH',
|
||||
display_name: channel?.display_name ?? '',
|
||||
slug: channel?.slug ?? '',
|
||||
description: channel?.description ?? '',
|
||||
category_id: channel?.category_id ? String(channel.category_id) : '',
|
||||
thumbnail: null as File | null,
|
||||
});
|
||||
const broadcastForm = useForm({
|
||||
title: '',
|
||||
recording_enabled: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (thumbnailPreviewUrl) {
|
||||
URL.revokeObjectURL(thumbnailPreviewUrl);
|
||||
}
|
||||
};
|
||||
}, [thumbnailPreviewUrl]);
|
||||
|
||||
function createChannel(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
createForm.submit(storeChannelRoute());
|
||||
@@ -101,7 +116,22 @@ export default function Dashboard({
|
||||
|
||||
function updateChannel(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
channelForm.submit(updateChannelRoute());
|
||||
channelForm.post(updateChannelRoute.url(), {
|
||||
forceFormData: true,
|
||||
onSuccess: () => {
|
||||
channelForm.reset('thumbnail');
|
||||
setThumbnailPreviewUrl(null);
|
||||
|
||||
if (thumbnailInputRef.current) {
|
||||
thumbnailInputRef.current.value = '';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateThumbnail(file: File | null) {
|
||||
channelForm.setData('thumbnail', file);
|
||||
setThumbnailPreviewUrl(file ? URL.createObjectURL(file) : null);
|
||||
}
|
||||
|
||||
function createBroadcast(event: FormEvent) {
|
||||
@@ -390,6 +420,73 @@ export default function Dashboard({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Live thumbnail"
|
||||
error={channelForm.errors.thumbnail}
|
||||
className="md:col-span-2"
|
||||
>
|
||||
<div className="flex flex-col gap-3 rounded-md border bg-background p-3 sm:flex-row sm:items-center">
|
||||
<div className="aspect-video w-full overflow-hidden rounded-md bg-muted sm:w-40 sm:shrink-0">
|
||||
{thumbnailPreviewUrl ||
|
||||
channel.thumbnail_url ? (
|
||||
<img
|
||||
src={
|
||||
thumbnailPreviewUrl ??
|
||||
channel.thumbnail_url ??
|
||||
''
|
||||
}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center text-xs text-muted-foreground">
|
||||
No thumbnail
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid min-w-0 flex-1 gap-2">
|
||||
<Input
|
||||
ref={thumbnailInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
onChange={(event) =>
|
||||
updateThumbnail(
|
||||
event.target
|
||||
.files?.[0] ??
|
||||
null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<ImagePlus className="size-3.5" />
|
||||
<span>
|
||||
Use a 16:9 JPG, PNG, or
|
||||
WebP up to 4 MB.
|
||||
</span>
|
||||
{channelForm.data
|
||||
.thumbnail && (
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{
|
||||
channelForm.data
|
||||
.thumbnail
|
||||
.name
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{channelForm.progress && (
|
||||
<progress
|
||||
value={
|
||||
channelForm.progress
|
||||
.percentage
|
||||
}
|
||||
max="100"
|
||||
className="h-1.5 w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<Field
|
||||
label="Description"
|
||||
error={channelForm.errors.description}
|
||||
|
||||
2
resume
2
resume
@@ -1 +1 @@
|
||||
codex resume 019e28d9-26f3-7ff3-b0f7-d0d833e46d7d
|
||||
codex resume 019e2d2a-4c81-7983-958b-95e5f72e81a2
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
codex resume 019e2c64-42f1-7c71-9e37-c12e55862adf
|
||||
@@ -6,6 +6,8 @@ use App\Models\Broadcast;
|
||||
use App\Models\Channel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -68,4 +70,60 @@ class DashboardTest extends TestCase
|
||||
->where('channel.thumbnail_url', '/storage/channels/demo/thumb.jpg'),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_creator_can_update_channel_thumbnail(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$channel = Channel::factory()->for($user)->create([
|
||||
'thumbnail_path' => 'channels/demo/old-thumb.jpg',
|
||||
]);
|
||||
|
||||
Storage::disk('public')->put($channel->thumbnail_path, 'old thumbnail');
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('creator.channel.update'), [
|
||||
'_method' => 'PATCH',
|
||||
'display_name' => $channel->display_name,
|
||||
'slug' => $channel->slug,
|
||||
'description' => $channel->description,
|
||||
'category_id' => $channel->category_id,
|
||||
'thumbnail' => UploadedFile::fake()->image('thumbnail.jpg', 1280, 720),
|
||||
])
|
||||
->assertRedirect()
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
$channel->refresh();
|
||||
|
||||
$this->assertNotSame('channels/demo/old-thumb.jpg', $channel->thumbnail_path);
|
||||
$this->assertStringStartsWith("channels/{$channel->id}/thumbnails/", $channel->thumbnail_path);
|
||||
Storage::disk('public')->assertMissing('channels/demo/old-thumb.jpg');
|
||||
Storage::disk('public')->assertExists($channel->thumbnail_path);
|
||||
}
|
||||
|
||||
public function test_creator_channel_thumbnail_must_be_an_image(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$channel = Channel::factory()->for($user)->create([
|
||||
'thumbnail_path' => 'channels/demo/current-thumb.jpg',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->from(route('dashboard'))
|
||||
->post(route('creator.channel.update'), [
|
||||
'_method' => 'PATCH',
|
||||
'display_name' => $channel->display_name,
|
||||
'slug' => $channel->slug,
|
||||
'description' => $channel->description,
|
||||
'category_id' => $channel->category_id,
|
||||
'thumbnail' => UploadedFile::fake()->create('thumbnail.txt', 8, 'text/plain'),
|
||||
])
|
||||
->assertRedirect(route('dashboard'))
|
||||
->assertSessionHasErrors('thumbnail');
|
||||
|
||||
$this->assertSame('channels/demo/current-thumb.jpg', $channel->refresh()->thumbnail_path);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user