mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
- Moved registration logic to a new client component for better separation of concerns. - Integrated invite code requirement based on feature flags in the registration process. - Enhanced dashboard functionality to conditionally enable Bunny uploads based on feature flags. - Updated various components and API routes to check for Bunny uploads and Stripe billing feature flags. - Added new feature flag utilities for managing feature toggles in the application.
261 lines
9.3 KiB
TypeScript
261 lines
9.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, type Dispatch, type SetStateAction } from 'react';
|
|
import { toast } from 'sonner';
|
|
import * as tus from 'tus-js-client';
|
|
import { parseVideoUrl, getThumbnailUrl, fetchVideoMetadata, type VideoSource } from '@/lib/video-providers';
|
|
import type { VersionActionsConfig, VideoData } from '@/components/video-page/types';
|
|
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
|
|
|
interface UseVersionActionsParams extends VersionActionsConfig {
|
|
setVideo: Dispatch<SetStateAction<VideoData | null>>;
|
|
activeVersionId: string | null;
|
|
setActiveVersionId: Dispatch<SetStateAction<string | null>>;
|
|
}
|
|
|
|
export function useVersionActions({
|
|
projectId,
|
|
videoId,
|
|
bunnyUploadsEnabled = true,
|
|
setVideo,
|
|
activeVersionId,
|
|
setActiveVersionId,
|
|
}: UseVersionActionsParams) {
|
|
const [showVersionDialog, setShowVersionDialog] = useState(false);
|
|
const [newVersionUrl, setNewVersionUrl] = useState('');
|
|
const [newVersionLabel, setNewVersionLabel] = useState('');
|
|
const [newVersionSource, setNewVersionSource] = useState<VideoSource | null>(null);
|
|
const [newVersionUrlError, setNewVersionUrlError] = useState('');
|
|
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
|
const [newVersionMode, setNewVersionMode] = useState<'url' | 'file'>('url');
|
|
const [newVersionFile, setNewVersionFile] = useState<File | null>(null);
|
|
const [newVersionUploadProgress, setNewVersionUploadProgress] = useState(0);
|
|
const [newVersionUploadStatus, setNewVersionUploadStatus] = useState('');
|
|
|
|
const [showDeleteVersionDialog, setShowDeleteVersionDialog] = useState(false);
|
|
const [versionToDelete, setVersionToDelete] = useState<string | null>(null);
|
|
const [isDeletingVersion, setIsDeletingVersion] = useState(false);
|
|
const bunnyCdnHostname = resolvePublicBunnyCdnHostname();
|
|
|
|
const handleNewVersionUrlChange = (url: string) => {
|
|
setNewVersionUrl(url);
|
|
setNewVersionUrlError('');
|
|
if (!url.trim()) {
|
|
setNewVersionSource(null);
|
|
return;
|
|
}
|
|
const source = parseVideoUrl(url);
|
|
if (source) {
|
|
setNewVersionSource(source);
|
|
} else {
|
|
setNewVersionSource(null);
|
|
if (url.length > 10) setNewVersionUrlError('Unsupported URL');
|
|
}
|
|
};
|
|
|
|
const handleCreateVersion = async () => {
|
|
if (!projectId) return;
|
|
setIsCreatingVersion(true);
|
|
setNewVersionUploadStatus('');
|
|
setNewVersionUploadProgress(0);
|
|
let uploadedBunnyVideoId: string | null = null;
|
|
let uploadedBunnyUploadToken: string | null = null;
|
|
|
|
try {
|
|
let finalVideoUrl = '';
|
|
let finalProviderId = '';
|
|
let finalProviderVideoId = '';
|
|
let finalThumbnailUrl: string | null = null;
|
|
let finalDuration: number | null = null;
|
|
|
|
if (newVersionMode === 'url') {
|
|
if (!newVersionSource) throw new Error('Invalid URL');
|
|
const meta = await fetchVideoMetadata(newVersionSource);
|
|
finalVideoUrl = newVersionSource.originalUrl;
|
|
finalProviderId = newVersionSource.providerId;
|
|
finalProviderVideoId = newVersionSource.videoId;
|
|
finalThumbnailUrl = getThumbnailUrl(newVersionSource, 'large');
|
|
finalDuration = meta?.duration || null;
|
|
} else {
|
|
if (!bunnyUploadsEnabled) throw new Error('Direct uploads are disabled by this host');
|
|
if (!newVersionFile) throw new Error('No file selected');
|
|
let title = newVersionFile.name;
|
|
if (newVersionLabel.trim()) {
|
|
title = newVersionLabel.trim();
|
|
} else {
|
|
title = title.replace(/\.[^/.]+$/, '');
|
|
}
|
|
|
|
setNewVersionUploadStatus('Initializing upload...');
|
|
const initRes = await fetch(`/api/projects/${projectId}/videos/bunny-init`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
|
|
if (!initRes.ok) throw new Error('Failed to initialize upload');
|
|
const { data: { videoId: bunnyVideoId, libraryId, signature, expirationTime, uploadToken } } = await initRes.json();
|
|
uploadedBunnyVideoId = bunnyVideoId;
|
|
uploadedBunnyUploadToken = uploadToken;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
setNewVersionUploadStatus('Uploading video...');
|
|
const upload = new tus.Upload(newVersionFile, {
|
|
endpoint: 'https://video.bunnycdn.com/tusupload',
|
|
retryDelays: [0, 3000, 5000, 10000, 20000],
|
|
headers: {
|
|
AuthorizationSignature: signature,
|
|
AuthorizationExpire: expirationTime.toString(),
|
|
VideoId: bunnyVideoId,
|
|
LibraryId: libraryId,
|
|
},
|
|
metadata: {
|
|
filetype: newVersionFile.type,
|
|
title,
|
|
},
|
|
onError: (error) => reject(new Error(`Upload failed: ${error.message}`)),
|
|
onProgress: (bytesUploaded, bytesTotal) => {
|
|
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
|
|
setNewVersionUploadProgress(Number(percentage));
|
|
setNewVersionUploadStatus(`Uploading... ${percentage}%`);
|
|
},
|
|
onSuccess: () => {
|
|
setNewVersionUploadStatus('Processing video...');
|
|
resolve(true);
|
|
},
|
|
});
|
|
upload.start();
|
|
});
|
|
|
|
finalVideoUrl = `https://iframe.mediadelivery.net/embed/${libraryId}/${bunnyVideoId}`;
|
|
finalProviderId = 'bunny';
|
|
finalProviderVideoId = bunnyVideoId;
|
|
finalThumbnailUrl = bunnyCdnHostname
|
|
? `https://${bunnyCdnHostname}/${bunnyVideoId}/thumbnail.jpg`
|
|
: null;
|
|
}
|
|
|
|
const res = await fetch(`/api/projects/${projectId}/videos/${videoId}/versions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
videoUrl: finalVideoUrl,
|
|
providerId: finalProviderId,
|
|
providerVideoId: finalProviderVideoId,
|
|
uploadToken: uploadedBunnyUploadToken,
|
|
versionLabel: newVersionLabel.trim() || null,
|
|
thumbnailUrl: finalThumbnailUrl,
|
|
duration: finalDuration,
|
|
setActive: true,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => null);
|
|
throw new Error(data?.error || 'Failed to create version');
|
|
}
|
|
|
|
const versionData = await res.json();
|
|
const newVersion = versionData.data;
|
|
setVideo((prev) => {
|
|
if (!prev) return prev;
|
|
const updatedVersions = prev.versions.map((v) => ({ ...v, isActive: false }));
|
|
updatedVersions.unshift({
|
|
...newVersion,
|
|
comments: [],
|
|
});
|
|
return { ...prev, versions: updatedVersions };
|
|
});
|
|
setActiveVersionId(newVersion.id);
|
|
setShowVersionDialog(false);
|
|
setNewVersionUrl('');
|
|
setNewVersionLabel('');
|
|
setNewVersionSource(null);
|
|
setNewVersionFile(null);
|
|
setNewVersionUploadStatus('');
|
|
} catch (err) {
|
|
const errorObj = err as Error;
|
|
if (uploadedBunnyVideoId && uploadedBunnyUploadToken) {
|
|
await fetch(`/api/projects/${projectId}/videos/bunny-init`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ videoId: uploadedBunnyVideoId, uploadToken: uploadedBunnyUploadToken }),
|
|
}).catch((cleanupError) => {
|
|
console.error('Failed to cleanup pending Bunny version upload:', cleanupError);
|
|
});
|
|
}
|
|
console.error('Failed to create version:', errorObj);
|
|
toast.error(errorObj.message || 'Failed to create version');
|
|
} finally {
|
|
setIsCreatingVersion(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteVersion = async () => {
|
|
if (!versionToDelete || !projectId) return;
|
|
setIsDeletingVersion(true);
|
|
try {
|
|
const res = await fetch(
|
|
`/api/projects/${projectId}/videos/${videoId}/versions/${versionToDelete}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
if (res.ok) {
|
|
setVideo((prev) => {
|
|
if (!prev) return prev;
|
|
const remaining = prev.versions.filter((v) => v.id !== versionToDelete);
|
|
return { ...prev, versions: remaining };
|
|
});
|
|
|
|
if (activeVersionId === versionToDelete) {
|
|
setVideo((prev) => {
|
|
if (!prev) return prev;
|
|
const remaining = prev.versions.filter((v) => v.id !== versionToDelete);
|
|
if (remaining.length > 0) {
|
|
setActiveVersionId(remaining[0].id);
|
|
} else {
|
|
setActiveVersionId(null);
|
|
}
|
|
return prev;
|
|
});
|
|
}
|
|
|
|
setShowDeleteVersionDialog(false);
|
|
setVersionToDelete(null);
|
|
} else {
|
|
const data = await res.json();
|
|
toast.error(data.error || 'Failed to delete version');
|
|
}
|
|
} catch {
|
|
toast.error('Failed to delete version');
|
|
} finally {
|
|
setIsDeletingVersion(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
showVersionDialog,
|
|
setShowVersionDialog,
|
|
newVersionUrl,
|
|
newVersionLabel,
|
|
setNewVersionLabel,
|
|
newVersionSource,
|
|
newVersionUrlError,
|
|
isCreatingVersion,
|
|
newVersionMode,
|
|
setNewVersionMode,
|
|
newVersionFile,
|
|
setNewVersionFile,
|
|
newVersionUploadProgress,
|
|
newVersionUploadStatus,
|
|
handleNewVersionUrlChange,
|
|
handleCreateVersion,
|
|
|
|
showDeleteVersionDialog,
|
|
setShowDeleteVersionDialog,
|
|
versionToDelete,
|
|
setVersionToDelete,
|
|
isDeletingVersion,
|
|
handleDeleteVersion,
|
|
};
|
|
}
|