335 lines
14 KiB
TypeScript
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>
|
|
);
|
|
} |