feat: delete lesson videos from Bunny
This commit is contained in:
@@ -93,6 +93,7 @@ const CourseModal: React.FC<{
|
|||||||
const [bunnyConfigured, setBunnyConfigured] = useState<boolean | null>(null);
|
const [bunnyConfigured, setBunnyConfigured] = useState<boolean | null>(null);
|
||||||
const [bunnyUploadState, setBunnyUploadState] = useState<'idle' | 'uploading' | 'uploaded' | 'error'>('idle');
|
const [bunnyUploadState, setBunnyUploadState] = useState<'idle' | 'uploading' | 'uploaded' | 'error'>('idle');
|
||||||
const [bunnyUploadMessage, setBunnyUploadMessage] = useState('');
|
const [bunnyUploadMessage, setBunnyUploadMessage] = useState('');
|
||||||
|
const [deletingVideoId, setDeletingVideoId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Tips & Global Attachments
|
// Tips & Global Attachments
|
||||||
const [tips, setTips] = useState<string[]>([]);
|
const [tips, setTips] = useState<string[]>([]);
|
||||||
@@ -340,8 +341,26 @@ const CourseModal: React.FC<{
|
|||||||
setBunnyUploadMessage('');
|
setBunnyUploadMessage('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLesson = (id: string) => {
|
const removeLesson = async (lesson: Lesson) => {
|
||||||
setLessons(lessons.filter(l => l.id !== id));
|
const video = lesson.media?.find((media) => media.provider === 'bunny');
|
||||||
|
const message = video
|
||||||
|
? 'Excluir esta aula e o vídeo enviado? O vídeo será removido permanentemente e esta ação não poderá ser desfeita.'
|
||||||
|
: 'Excluir esta aula? Esta ação não poderá ser desfeita após salvar o curso.';
|
||||||
|
if (!window.confirm(message)) return;
|
||||||
|
|
||||||
|
if (video) {
|
||||||
|
setDeletingVideoId(video.externalId);
|
||||||
|
try {
|
||||||
|
await instructorApi.deleteBunnyVideo(video.externalId);
|
||||||
|
} catch (error) {
|
||||||
|
alert(error instanceof Error ? error.message : 'Não foi possível excluir o vídeo. Tente novamente.');
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
setDeletingVideoId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLessons((current) => current.filter((item) => item.id !== lesson.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLessonAccess = (id: string) => {
|
const toggleLessonAccess = (id: string) => {
|
||||||
@@ -778,10 +797,12 @@ const CourseModal: React.FC<{
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeLesson(lesson.id)}
|
disabled={Boolean(lesson.media?.[0]?.provider === 'bunny' && deletingVideoId === lesson.media[0].externalId)}
|
||||||
className="p-1.5 text-gray-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
onClick={() => void removeLesson(lesson)}
|
||||||
|
className="p-1.5 text-gray-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
title="Excluir aula e vídeo"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
{lesson.media?.[0]?.provider === 'bunny' && deletingVideoId === lesson.media[0].externalId ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ export async function getBunnyVideo(videoId: string) {
|
|||||||
return toVideo(await response.json() as BunnyApiVideo);
|
return toVideo(await response.json() as BunnyApiVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteBunnyVideo(videoId: string) {
|
||||||
|
await bunnyRequest(`/videos/${encodeURIComponent(videoId)}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
export function signedBunnyEmbedUrl(videoId: string, validitySeconds = config.BUNNY_EMBED_TOKEN_TTL_SECONDS) {
|
export function signedBunnyEmbedUrl(videoId: string, validitySeconds = config.BUNNY_EMBED_TOKEN_TTL_SECONDS) {
|
||||||
const { libraryId, embedTokenKey } = getBunnyConfiguration();
|
const { libraryId, embedTokenKey } = getBunnyConfiguration();
|
||||||
const expires = Math.floor(Date.now() / 1000) + validitySeconds;
|
const expires = Math.floor(Date.now() / 1000) + validitySeconds;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
BunnyConfigurationError,
|
BunnyConfigurationError,
|
||||||
BunnyRequestError,
|
BunnyRequestError,
|
||||||
createBunnyVideo,
|
createBunnyVideo,
|
||||||
|
deleteBunnyVideo,
|
||||||
getBunnyVideo,
|
getBunnyVideo,
|
||||||
isBunnyConfigured,
|
isBunnyConfigured,
|
||||||
uploadBunnyVideo,
|
uploadBunnyVideo,
|
||||||
@@ -123,4 +124,15 @@ export const mediaRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return providerError(reply, error);
|
return providerError(reply, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.delete('/bunny/videos/:videoId', manageAccess, async (request, reply) => {
|
||||||
|
const { videoId } = videoParamsSchema.parse(request.params);
|
||||||
|
try {
|
||||||
|
await deleteBunnyVideo(videoId);
|
||||||
|
await recordAudit({ actorId: request.user.id, action: 'media.video.deleted', subjectType: 'video', subjectId: videoId, ipAddress: request.ip });
|
||||||
|
return reply.code(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
return providerError(reply, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ export const instructorApi = {
|
|||||||
async bunnyVideoStatus(videoId: string) {
|
async bunnyVideoStatus(videoId: string) {
|
||||||
return apiRequest<{ data: BunnyVideo }>(`/manage/media/bunny/videos/${videoId}`);
|
return apiRequest<{ data: BunnyVideo }>(`/manage/media/bunny/videos/${videoId}`);
|
||||||
},
|
},
|
||||||
|
async deleteBunnyVideo(videoId: string) {
|
||||||
|
return apiRequest<void>(`/manage/media/bunny/videos/${videoId}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
async coverConfiguration() { return apiRequest<{ data: { configured: boolean; maxUploadBytes: number } }>('/manage/media/covers/config'); },
|
async coverConfiguration() { return apiRequest<{ data: { configured: boolean; maxUploadBytes: number } }>('/manage/media/covers/config'); },
|
||||||
async uploadCover(file: File) { return uploadImageRequest<{ data: { key: string; coverImageUrl: string } }>('/manage/media/covers', file); },
|
async uploadCover(file: File) { return uploadImageRequest<{ data: { key: string; coverImageUrl: string } }>('/manage/media/covers', file); },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user