- Fixed theme selector dropdown positioning issues with proper Tailwind classes - Replaced custom CSS classes with standard Tailwind utilities for v4 compatibility - Implemented dynamic theme-aware styling system for dropdown appearance - Added theme-specific styling for Light, Dark, High Contrast Light, High Contrast Dark - Enhanced UX with professional dropdown interface and visual consistency - Maintained 100% theme compliance and accessibility standards - Updated memory bank documentation with Phase 7 completion Key improvements: - Professional dropdown with proper shadows, borders, and backgrounds - Dynamic styling that adapts to current website theme - Enhanced accessibility with ARIA support and keyboard navigation - Seamless visual integration across all theme modes - Production-ready theme selector with polished UI
378 lines
16 KiB
TypeScript
378 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter, useParams } from 'next/navigation';
|
|
import { useVideos, Video } from '../../../../hooks/useVideos';
|
|
|
|
export default function VideoDetailsPage() {
|
|
const params = useParams();
|
|
const id = params.id as string;
|
|
const router = useRouter();
|
|
const { getVideoById, deleteVideo, publishVideo } = useVideos();
|
|
|
|
const [video, setVideo] = useState<Video | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
|
|
// Fetch video data
|
|
useEffect(() => {
|
|
const fetchVideo = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getVideoById(id);
|
|
setVideo(data);
|
|
} catch (err) {
|
|
console.error('Error fetching video:', err);
|
|
setError('Failed to load video details. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchVideo();
|
|
}, [id, getVideoById]);
|
|
|
|
// Handle video deletion
|
|
const handleDelete = async () => {
|
|
if (!deleteConfirm) {
|
|
setDeleteConfirm(true);
|
|
return;
|
|
}
|
|
|
|
setActionLoading(true);
|
|
try {
|
|
const success = await deleteVideo(id);
|
|
if (success) {
|
|
router.push('/admin/videos');
|
|
} else {
|
|
setError('Failed to delete video. Please try again.');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting video:', err);
|
|
setError('Failed to delete video. Please try again.');
|
|
} finally {
|
|
setActionLoading(false);
|
|
setDeleteConfirm(false);
|
|
}
|
|
};
|
|
|
|
// Handle publish/unpublish
|
|
const handlePublishToggle = async () => {
|
|
if (!video) return;
|
|
|
|
setActionLoading(true);
|
|
try {
|
|
const result = await publishVideo(id, !video.published);
|
|
if (result) {
|
|
setVideo(result);
|
|
} else {
|
|
setError(`Failed to ${video.published ? 'unpublish' : 'publish'} video. Please try again.`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error toggling publish status:', err);
|
|
setError(`Failed to ${video.published ? 'unpublish' : 'publish'} video. Please try again.`);
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
// Format date for display
|
|
const formatDate = (dateString: string | Date): string => {
|
|
if (!dateString) return 'N/A';
|
|
|
|
const date = new Date(dateString);
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
}).format(date);
|
|
};
|
|
|
|
// Format duration for display (MM:SS)
|
|
const formatDuration = (seconds: number): string => {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = Math.round(seconds % 60);
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="admin-card" style={{backgroundColor: 'var(--admin-error)', color: 'white', border: '1px solid var(--admin-error)'}}>
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium">
|
|
{error}
|
|
</h3>
|
|
<div className="mt-2">
|
|
<button
|
|
onClick={() => router.push('/admin/videos')}
|
|
className="text-sm font-medium underline"
|
|
>
|
|
Return to Videos List
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!video) {
|
|
return (
|
|
<div className="admin-card" style={{backgroundColor: 'var(--admin-warning)', color: 'white', border: '1px solid var(--admin-warning)'}}>
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium">
|
|
Video not found
|
|
</h3>
|
|
<div className="mt-2">
|
|
<button
|
|
onClick={() => router.push('/admin/videos')}
|
|
className="text-sm font-medium underline"
|
|
>
|
|
Return to Videos List
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Header */}
|
|
<div className="sm:flex sm:items-center sm:justify-between mb-6">
|
|
<div>
|
|
<h1 className="admin-page-header-title">
|
|
{video.title}
|
|
</h1>
|
|
<p className="mt-1 text-sm admin-text-light">
|
|
Uploaded on {formatDate(video.uploadDate)}
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 sm:mt-0 flex space-x-3">
|
|
<Link
|
|
href="/admin/videos"
|
|
className="admin-btn-secondary inline-flex items-center"
|
|
>
|
|
<svg className="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to Videos
|
|
</Link>
|
|
<Link
|
|
href={`/admin/videos/${id}/edit`}
|
|
className="admin-btn-primary inline-flex items-center"
|
|
>
|
|
<svg className="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
</svg>
|
|
Edit Video
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status and Quick Actions */}
|
|
<div className="admin-card mb-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex items-center mb-4 sm:mb-0">
|
|
<span className="text-sm font-medium admin-text mr-2">Status:</span>
|
|
<span className={`admin-badge ${video.published ? 'admin-badge-success' : 'admin-badge-warning'}`}>
|
|
{video.published ? 'Published' : 'Draft'}
|
|
</span>
|
|
<span className="ml-4 text-sm font-medium admin-text mr-2">Accessibility:</span>
|
|
<span className={`admin-badge ${video.isAccessible ? 'admin-badge-social' : 'admin-badge-default'}`}>
|
|
{video.isAccessible ? 'Accessible' : 'Not Accessible'}
|
|
</span>
|
|
</div>
|
|
<div className="flex space-x-3">
|
|
<button
|
|
onClick={handlePublishToggle}
|
|
disabled={actionLoading}
|
|
className={`inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white ${
|
|
video.published
|
|
? 'bg-yellow-600 hover:bg-yellow-700 focus:ring-yellow-500'
|
|
: 'bg-green-600 hover:bg-green-700 focus:ring-green-500'
|
|
} focus:outline-none focus:ring-2 focus:ring-offset-2`}
|
|
>
|
|
{actionLoading ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Processing...
|
|
</>
|
|
) : (
|
|
<>{video.published ? 'Unpublish' : 'Publish'}</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleDelete}
|
|
disabled={actionLoading}
|
|
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
|
>
|
|
{deleteConfirm ? 'Confirm Delete' : 'Delete Video'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Video Preview */}
|
|
<div className="admin-card mb-6">
|
|
<h2 className="text-lg font-semibold mb-4 admin-text">Video Preview</h2>
|
|
<div className="aspect-video admin-info-panel rounded-md overflow-hidden">
|
|
<video
|
|
src={`/api/uploads/${video.videoPath}`}
|
|
controls
|
|
className="w-full h-full"
|
|
poster={video.thumbnailPath ? `/api/uploads/${video.thumbnailPath}` : undefined}
|
|
></video>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Video Details */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Basic Information */}
|
|
<div className="admin-card">
|
|
<h2 className="text-lg font-semibold mb-4 admin-text">Basic Information</h2>
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-2">
|
|
<dt className="text-sm font-medium admin-text-light">Title</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.title}</dd>
|
|
</div>
|
|
<div className="sm:col-span-2">
|
|
<dt className="text-sm font-medium admin-text-light">Description</dt>
|
|
<dd className="mt-1 text-sm admin-text whitespace-pre-line">{video.description}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Category</dt>
|
|
<dd className="mt-1 text-sm admin-text capitalize">{video.category}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Duration</dt>
|
|
<dd className="mt-1 text-sm admin-text">{formatDuration(video.duration)}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Upload Date</dt>
|
|
<dd className="mt-1 text-sm admin-text">{formatDate(video.uploadDate)}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">View Count</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.viewCount}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Original Filename</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.originalFilename || 'N/A'}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Accessibility Information */}
|
|
<div className="admin-card">
|
|
<h2 className="text-lg font-semibold mb-4 admin-text">Accessibility Features</h2>
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Has Subtitles</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.hasSubtitles ? 'Yes' : 'No'}</dd>
|
|
</div>
|
|
{video.hasSubtitles && video.subtitlesPath && (
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Subtitles Language</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.subtitlesLanguage || 'English'}</dd>
|
|
</div>
|
|
)}
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Has Transcript</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.hasTranscript ? 'Yes' : 'No'}</dd>
|
|
</div>
|
|
{video.hasTranscript && video.transcriptPath && (
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium admin-text-light">Transcript Language</dt>
|
|
<dd className="mt-1 text-sm admin-text">{video.transcriptLanguage || 'English'}</dd>
|
|
</div>
|
|
)}
|
|
<div className="sm:col-span-2">
|
|
<dt className="text-sm font-medium admin-text-light">Accessibility Status</dt>
|
|
<dd className="mt-1">
|
|
<span className={`admin-badge ${video.isAccessible ? 'admin-badge-social' : 'admin-badge-default'}`}>
|
|
{video.isAccessible ? 'Accessible' : 'Not Accessible'}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
{/* Accessibility Files */}
|
|
<div className="mt-6">
|
|
<h3 className="text-md font-medium mb-2 admin-text">Accessibility Files</h3>
|
|
<div className="space-y-4">
|
|
{video.subtitlesPath ? (
|
|
<div className="flex items-center justify-between admin-info-panel p-3 rounded-md">
|
|
<div>
|
|
<p className="text-sm font-medium admin-text">Subtitles File</p>
|
|
<p className="text-xs admin-text-light">{video.subtitlesPath.split('/').pop()}</p>
|
|
</div>
|
|
<a
|
|
href={`/api/uploads/${video.subtitlesPath}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="admin-action-link"
|
|
>
|
|
Download
|
|
</a>
|
|
</div>
|
|
) : (
|
|
<div className="admin-info-panel p-3 rounded-md">
|
|
<p className="text-sm admin-text-light">No subtitles file available</p>
|
|
</div>
|
|
)}
|
|
|
|
{video.transcriptPath ? (
|
|
<div className="flex items-center justify-between admin-info-panel p-3 rounded-md">
|
|
<div>
|
|
<p className="text-sm font-medium admin-text">Transcript File</p>
|
|
<p className="text-xs admin-text-light">{video.transcriptPath.split('/').pop()}</p>
|
|
</div>
|
|
<a
|
|
href={`/api/uploads/${video.transcriptPath}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="admin-action-link"
|
|
>
|
|
Download
|
|
</a>
|
|
</div>
|
|
) : (
|
|
<div className="admin-info-panel p-3 rounded-md">
|
|
<p className="text-sm admin-text-light">No transcript file available</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|