kreativortex/app/[locale]/(app)/dashboard/videos/page.tsx
Jessica Rekcah 4253483f44 jalan
2025-12-02 00:22:34 +07:00

335 lines
14 KiB
TypeScript

/**
* File: page.tsx
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Video list page for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
'use client';
import { Link } from '@/i18n/routing';
import ActionButton from '@/components/ActionButton';
import { useFetch } from '@/hooks/useFetch';
import { useTranslations } from 'next-intl';
import { useState, useEffect } from 'react';
import { generateYoutubeEmbedUrl } from '@/lib/youtube';
import { PlayIcon } from 'lucide-react';
import { canUploadVideos } from '@/lib/admin';
interface VideoData extends Record<string, unknown> {
id: string;
title: string;
description: string;
videoType: 'YOUTUBE' | 'LOCAL';
viewCount: number;
createdAt: string;
isPublic: boolean;
uploaderId: string;
uploader: {
user: {
name: string;
};
};
}
export default function VideosPage() {
const t = useTranslations('Videos');
const { data: videos, loading } = useFetch<VideoData[]>('/api/videos');
const [userProfile, setUserProfile] = useState<any>(null);
useEffect(() => {
// Fetch user profile to check permissions
const fetchProfile = async () => {
try {
const response = await fetch('/api/user/profile', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setUserProfile(data.data);
}
} catch (error) {
console.error('Error fetching user profile:', error);
}
};
fetchProfile();
}, []);
const handleDelete = async (videoId: string) => {
if (!confirm('Apakah Anda yakin ingin menghapus video ini?')) {
return;
}
try {
const response = await fetch(`/api/videos/${videoId}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
// Refresh the videos list
window.location.reload();
} else {
alert('Gagal menghapus video');
}
} catch (error) {
console.error('Error deleting video:', error);
alert('Gagal menghapus video');
}
};
const renderVideoActions = (video: any, userProfile: any, handleDelete: Function, t: Function) => {
const canEdit = userProfile && (
userProfile.role?.name === 'ADMIN' ||
userProfile.role?.name === 'PENDIDIK' ||
video.uploaderId === userProfile.id
);
const canDelete = userProfile && (
userProfile.role?.name === 'ADMIN' ||
video.uploaderId === userProfile.id
);
return (
<div className="flex space-x-2">
<Link
href={`/dashboard/videos/${video.id}`}
className="inline-flex items-center px-3 py-2 bg-gold-500 hover:bg-gold-600 text-navy-900 rounded-lg text-sm font-medium transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002 2z" />
</svg>
{t('actionView')}
</Link>
{canEdit && (
<Link
href={`/dashboard/videos/${video.id}/edit`}
className="inline-flex items-center px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002 2v-4a1 1 0 011-1h7a1 1 0 011-1v-4a2 2 0 002-2H7a2 2 0 00-2 2v11a2 2 0 002-2z" />
</svg>
{t('actionEdit')}
</Link>
)}
{canDelete && (
<button
onClick={() => handleDelete(video.id)}
className="inline-flex items-center px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-medium transition-colors"
title={t('actionDelete')}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0015-1.414l-7-7A2 2 0 003 7v10a2 2 0 002 2h8a2 2 0 002 2z" />
</svg>
{t('actionDelete')}
</button>
)}
</div>
);
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-white">{t('title')}</h1>
<p className="text-gray-400">{t('subtitle')}</p>
</div>
{canUploadVideos(userProfile) && (
<Link href="/dashboard/videos/new">
<ActionButton>
{t('uploadButton')}
</ActionButton>
</Link>
)}
</div>
{loading ? (
<div className="text-center text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gold-400"></div>
<p className="mt-4">{t('loading')}</p>
</div>
) : videos && videos.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{videos.map((video) => (
<div key={video.id} className="bg-white/10 backdrop-blur-sm rounded-xl border border-white/20 overflow-hidden hover:bg-white/15 transition-all duration-300 group">
{/* Video Thumbnail */}
<div className="aspect-video bg-gray-900 relative overflow-hidden">
{video.videoType === 'YOUTUBE' ? (
<iframe
src={generateYoutubeEmbedUrl(video.videoUrl)}
className="w-full h-full"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-800">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002 2z" />
</svg>
<p className="text-xs text-gray-500 mt-2">Video Preview</p>
</div>
)}
{/* Play Button Overlay */}
<div className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Link href={`/dashboard/videos/${video.id}`} className="text-white hover:text-gold-400 transition-colors">
<PlayIcon className="w-12 h-12" />
</Link>
</div>
{/* Status Badge */}
<div className="absolute top-4 right-4">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
video.isPublic
? 'bg-green-500 text-white'
: 'bg-yellow-500 text-white'
}`}>
{video.isPublic ? 'Public' : 'Private'}
</span>
</div>
{/* Play Button Overlay */}
<div className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Link href={`/dashboard/videos/${video.id}`} className="text-white hover:text-gold-400 transition-colors">
<PlayIcon className="w-12 h-12" />
</Link>
</div>
{/* Status Badge */}
<div className="absolute top-4 right-4">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
video.isPublic
? 'bg-green-500 text-white'
: 'bg-yellow-500 text-white'
}`}>
{video.isPublic ? 'Public' : 'Private'}
</span>
</div>
</div>
{/* Video Info */}
<div className="p-6">
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-semibold text-white group-hover:text-gold-400 transition-colors line-clamp-2">
{video.title}
</h3>
<div className="flex items-center space-x-2 text-sm text-gray-400">
<span className={`px-2 py-1 rounded text-xs font-medium ${
video.videoType === 'YOUTUBE'
? 'bg-red-900/50 text-red-200'
: 'bg-blue-900/50 text-blue-200'
}`}>
{video.videoType}
</span>
<span className="flex items-center">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0l-3 3m0 0a3 3 0 016 0l3-3m6 0a3 3 0 016 0l-3 3" />
</svg>
{video.viewCount || 0} views
</span>
</div>
</div>
<p className="text-gray-300 text-sm line-clamp-3 mb-4">
{video.description}
</p>
{/* Video Actions */}
<div className="flex items-center justify-between pt-4 border-t border-gray-700">
<div className="flex items-center space-x-2 text-sm text-gray-400">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0v4a4 4 0 014 0H6a4 4 0 00-4 4v4a4 4 0 014 0h8a4 4 0 014 0z" />
</svg>
{new Date(video.createdAt).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
<div className="flex space-x-2">
<Link
href={`/dashboard/videos/${video.id}`}
className="inline-flex items-center px-3 py-2 bg-gold-500 hover:bg-gold-600 text-navy-900 rounded-lg text-sm font-medium transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0l-3 3m0 0a3 3 0 016 0l3-3m6 0a3 3 0 016 0l-3 3" />
</svg>
{t('actionView')}
</Link>
{(() => {
const canEdit = userProfile && (
userProfile.role?.name === 'ADMIN' ||
userProfile.role?.name === 'PENDIDIK' ||
video.uploaderId === userProfile.id
);
const canDelete = userProfile && (
userProfile.role?.name === 'ADMIN' ||
video.uploaderId === userProfile.id
);
if (canEdit) {
return (
<Link
href={`/dashboard/videos/${video.id}/edit`}
className="inline-flex items-center px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-4a1 1 0 011-1h7a1 1 0 011 1v4a1 1 0 011-1h7a1 1 0 011-1v-4a2 2 0 00-2 2H7a2 2 0 00-2 2v11a2 2 0 002 2z" />
</svg>
{t('actionEdit')}
</Link>
);
}
if (canDelete) {
return (
<button
onClick={() => handleDelete(video.id)}
className="inline-flex items-center px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-medium transition-colors"
title={t('actionDelete')}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0015-1.414l-7-7A2 2 0 003 7v10a2 2 0 002 2h8a2 2 0 002 2v10a2 2 0 002-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
{t('actionDelete')}
</button>
);
}
return null;
})()}
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<svg className="w-16 h-16 mx-auto mb-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002 2z" />
</svg>
<h3 className="text-xl font-semibold text-white mb-2">{t('noVideos')}</h3>
<p className="text-gray-400 mb-6">{t('noVideosDesc')}</p>
{canUploadVideos(userProfile) && (
<Link href="/dashboard/videos/new">
<ActionButton>
{t('uploadFirst')}
</ActionButton>
</Link>
)}
</div>
</div>
)}
</div>
);
}