Initial commit

This commit is contained in:
twotalesanimation
2026-06-11 10:46:09 +02:00
commit 81ad7e4ea9
223 changed files with 39530 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
'use client';
import React, { useEffect, useState } from 'react';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
import { SiteHeader } from '@/components/site-header';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Field,
FieldGroup,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Trash2Icon } from 'lucide-react';
type Course = { id: string; title: string; code?: string };
function CoursesUI() {
const router = useRouter();
const [courses, setCourses] = useState<Course[]>([]);
const [loading, setLoading] = useState(false);
const [courseTitle, setCourseTitle] = useState('');
const [courseCode, setCourseCode] = useState('');
const [deletingId, setDeletingId] = useState<string | null>(null);
useEffect(() => {
fetchCourses();
}, []);
async function fetchCourses() {
try {
const res = await fetch('/api/admin/meta');
if (res.ok) {
const json = await res.json();
setCourses(json.courses || []);
}
} catch (err) {
console.error('Failed to fetch courses', err);
toast.error('Failed to fetch courses');
}
}
async function createCourse(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
try {
const res = await fetch('/api/admin/create-course', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: courseTitle, code: courseCode }),
});
if (res.ok) {
toast.success('Course created');
setCourseTitle('');
setCourseCode('');
await fetchCourses();
router.refresh();
} else {
const txt = await res.text();
toast.error('Failed to create course: ' + txt);
}
} catch (err: any) {
toast.error('Error: ' + String(err.message ?? err));
} finally {
setLoading(false);
}
}
async function deleteCourse(courseId: string) {
setDeletingId(courseId);
try {
const res = await fetch(`/api/admin/delete-course`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ courseId }),
});
if (res.ok) {
toast.success('Course deleted');
await fetchCourses();
router.refresh();
} else {
const txt = await res.text();
toast.error('Failed to delete course: ' + txt);
}
} catch (err: any) {
toast.error('Error: ' + String(err.message ?? err));
} finally {
setDeletingId(null);
}
}
return (
<div className="flex flex-col gap-6 py-4 md:gap-6 md:py-6 px-4 lg:px-16">
<Card>
<CardHeader>
<CardTitle>Create Course</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={createCourse}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="courseTitle">Course title</FieldLabel>
<Input
id="courseTitle"
required
value={courseTitle}
onChange={(e) => setCourseTitle(e.target.value)}
placeholder="3D ANIMATION 300"
/>
</Field>
<Field>
<FieldLabel htmlFor="courseCode">Course code</FieldLabel>
<Input
id="courseCode"
value={courseCode}
onChange={(e) => setCourseCode(e.target.value)}
placeholder="3D100"
/>
</Field>
<Field>
<Button type="submit" disabled={loading}>
Create course
</Button>
</Field>
</FieldGroup>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Courses</CardTitle>
</CardHeader>
<CardContent>
{courses.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No courses yet
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Code</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{courses.map((course) => (
<TableRow key={course.id}>
<TableCell className="font-medium">
{course.title}
</TableCell>
<TableCell>{course.code || '—'}</TableCell>
<TableCell className="text-right">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={deletingId === course.id}
>
<Trash2Icon className="w-4 h-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Course</AlertDialogTitle>
<AlertDialogDescription>
Are you sure? This will delete the course and all
associated playlists and videos.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex gap-2 justify-end">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteCourse(course.id)}
>
Delete
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
);
}
export default function CoursesAdminClient() {
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">
<CoursesUI />
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}
+22
View File
@@ -0,0 +1,22 @@
// app/admin/courses/page.tsx
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth-options';
import { redirect } from 'next/navigation';
import CoursesAdminClient from './admin-client';
export const dynamic = 'force-dynamic';
export default async function CoursesAdminPage() {
const session = await getServerSession(authOptions);
const role = (session as any)?.user?.role ?? null;
if (!session?.user) {
redirect('/login');
}
if (!(role === 'admin' || role === 'superadmin')) {
redirect('/dashboard');
}
return <CoursesAdminClient />;
}