ocd-website/frontend/src/app/admin/videos/[id]/page.tsx
TheMaddax 338f81f7ad Phase 7: Fix theme selector positioning and implement dynamic theme-aware styling
- 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
2025-06-03 17:42:14 -06:00

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>
);
}