Files
Vault/app/admin/videos/[videoId]/edit/edit-video-client.tsx
T
twotalesanimation 81ad7e4ea9 Initial commit
2026-06-11 10:46:09 +02:00

269 lines
9.3 KiB
TypeScript

'use client';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import Image from 'next/image';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
import { SiteHeader } from '@/components/site-header';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Field,
FieldGroup,
FieldLabel,
FieldDescription,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
interface VideoData {
id: string;
title: string;
description: string;
thumbnail?: string;
playlistTitle: string;
courseTitle: string;
restrictedCourseIds?: string[];
}
interface CourseData {
id: string;
title: string;
}
export default function EditVideoClient({ video }: { video: VideoData }) {
const router = useRouter();
const [title, setTitle] = useState(video.title);
const [description, setDescription] = useState(video.description);
const [thumbFile, setThumbFile] = useState<File | null>(null);
const [thumbPreview, setThumbPreview] = useState(video.thumbnail);
const [loading, setLoading] = useState(false);
const [courses, setCourses] = useState<CourseData[]>([]);
const [coursesLoading, setCoursesLoading] = useState(true);
const [restrictedCourseIds, setRestrictedCourseIds] = useState<string[]>(
video.restrictedCourseIds ?? []
);
useEffect(() => {
let cancelled = false;
setCoursesLoading(true);
fetch('/api/admin/meta')
.then(async (res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) {
setCourses(json.courses ?? []);
}
})
.catch((err) => {
console.error('Failed to load courses', err);
if (!cancelled) {
setCourses([]);
}
})
.finally(() => {
if (!cancelled) setCoursesLoading(false);
});
return () => {
cancelled = true;
};
}, []);
const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setThumbFile(file);
const reader = new FileReader();
reader.onload = (event) => {
setThumbPreview(event.target?.result as string);
};
reader.readAsDataURL(file);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const formData = new FormData();
formData.append('videoId', video.id);
formData.append('title', title);
formData.append('description', description);
if (thumbFile) {
formData.append('thumbnail', thumbFile);
}
formData.append('restrictedCourseIds', JSON.stringify(restrictedCourseIds));
const res = await fetch('/api/admin/update-video', {
method: 'POST',
body: formData,
});
if (res.ok) {
toast.success('Video updated successfully');
router.push('/admin/videos');
} else {
const err = await res.text();
toast.error('Failed to update video: ' + err);
}
} catch (err: any) {
toast.error('Error: ' + String(err.message ?? err));
} finally {
setLoading(false);
}
};
return (
<SidebarProvider
style={
{
'--sidebar-width': 'calc(var(--spacing) * 72)',
'--header-height': 'calc(var(--spacing) * 12)',
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-6 py-4 md:gap-6 md:py-6 px-4 lg:px-16">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">
Edit Video
</h1>
<p className="text-muted-foreground">
{video.courseTitle} {video.playlistTitle}
</p>
</div>
<Card>
<CardHeader>
<CardTitle>{video.title}</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="title">Video Title</FieldLabel>
<Input
id="title"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter video title"
/>
</Field>
<Field>
<FieldLabel htmlFor="description">
Description
</FieldLabel>
<FieldDescription>
Optional description for the video
</FieldDescription>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter video description"
rows={4}
/>
</Field>
<Field>
<FieldLabel>Restricted courses</FieldLabel>
<FieldDescription>
Only users enrolled in the checked courses will see this video. Leave
empty to allow all enrolled courses to access it.
</FieldDescription>
{coursesLoading ? (
<div className="text-sm text-muted-foreground">Loading courses</div>
) : courses.length === 0 ? (
<div className="text-sm text-muted-foreground">No courses available.</div>
) : (
<div className="grid gap-2 text-sm">
{courses.map((course) => (
<label
key={course.id}
className="flex items-center gap-2 text-sm font-medium"
>
<input
type="checkbox"
checked={restrictedCourseIds.includes(course.id)}
onChange={() => {
setRestrictedCourseIds((prev) =>
prev.includes(course.id)
? prev.filter((id) => id !== course.id)
: [...prev, course.id]
);
}}
className="accent-primary"
/>
<span>{course.title}</span>
</label>
))}
</div>
)}
</Field>
<Field>
<FieldLabel htmlFor="thumbnail">Thumbnail</FieldLabel>
<FieldDescription>
Upload a new thumbnail image (optional)
</FieldDescription>
<div className="grid w-full max-w-sm items-center gap-3">
<Input
id="thumbnail"
type="file"
accept="image/*"
onChange={handleThumbnailChange}
className="block w-full text-sm"
/>
{thumbPreview ? (
<div className="relative w-40 h-24">
<Image
src={thumbPreview}
alt="Thumbnail preview"
fill
unoptimized
className="object-cover rounded"
/>
</div>
) : null}
</div>
</Field>
<div className="flex gap-2 pt-4">
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Changes'}
</Button>
<Button
type="button"
variant="outline"
onClick={() => router.push('/admin/videos')}
>
Cancel
</Button>
</div>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}