'use client' import { useCallback, useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import Link from 'next/link' import dynamic from 'next/dynamic' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Checkbox } from '@/components/ui/checkbox' import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/skeleton' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { toast } from 'sonner' import { ArrowLeft, Save, Loader2, Settings, Eye, Trash2, AlertCircle, } from 'lucide-react' // Dynamically import editors to avoid SSR issues const BlockEditor = dynamic( () => import('@/components/shared/block-editor').then((mod) => mod.BlockEditor), { ssr: false, loading: () => (
), } ) const ResourceRenderer = dynamic( () => import('@/components/shared/resource-renderer').then((mod) => mod.ResourceRenderer), { ssr: false, loading: () => (
), } ) const ROLE_OPTIONS = [ { value: 'JURY_MEMBER', label: 'Jury Members' }, { value: 'MENTOR', label: 'Mentors' }, { value: 'OBSERVER', label: 'Observers' }, { value: 'APPLICANT', label: 'Applicants' }, { value: 'AWARD_MASTER', label: 'Award Masters' }, ] type AccessRule = | { type: 'everyone' } | { type: 'roles'; roles: string[] } | { type: 'jury_group'; juryGroupIds: string[] } | { type: 'round'; roundIds: string[] } function parseAccessJson(accessJson: unknown): { mode: 'everyone' | 'roles'; roles: string[] } { if (!accessJson || !Array.isArray(accessJson) || accessJson.length === 0) { return { mode: 'everyone', roles: [] } } const firstRule = accessJson[0] as AccessRule if (firstRule.type === 'roles' && 'roles' in firstRule) { return { mode: 'roles', roles: firstRule.roles } } return { mode: 'everyone', roles: [] } } export default function EditLearningResourcePage() { const params = useParams() const router = useRouter() const resourceId = params.id as string // Fetch resource const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId }) const { data: stats } = trpc.learningResource.getStats.useQuery({ id: resourceId }) // Form state const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [contentJson, setContentJson] = useState('') const [externalUrl, setExternalUrl] = useState('') const [isPublished, setIsPublished] = useState(false) const [programId, setProgramId] = useState(null) const [previewing, setPreviewing] = useState(false) // Access rules state const [accessMode, setAccessMode] = useState<'everyone' | 'roles'>('everyone') const [selectedRoles, setSelectedRoles] = useState([]) // API const { data: programs } = trpc.program.list.useQuery({ status: 'ACTIVE' }) const utils = trpc.useUtils() const updateResource = trpc.learningResource.update.useMutation({ onSuccess: () => { utils.learningResource.get.invalidate({ id: resourceId }) utils.learningResource.list.invalidate() }, }) const deleteResource = trpc.learningResource.delete.useMutation({ onSuccess: () => utils.learningResource.list.invalidate(), }) const getUploadUrl = trpc.learningResource.getUploadUrl.useMutation() // Populate form when resource loads useEffect(() => { if (resource) { setTitle(resource.title) setDescription(resource.description || '') setContentJson(resource.contentJson ? JSON.stringify(resource.contentJson) : '') setExternalUrl(resource.externalUrl || '') setIsPublished(resource.isPublished) setProgramId(resource.programId) const { mode, roles } = parseAccessJson(resource.accessJson) setAccessMode(mode) setSelectedRoles(roles) } }, [resource]) // Handle file upload for BlockNote const handleUploadFile = async (file: File): Promise => { try { const { url, bucket, objectKey } = await getUploadUrl.mutateAsync({ fileName: file.name, mimeType: file.type, }) await fetch(url, { method: 'PUT', body: file, headers: { 'Content-Type': file.type }, }) const minioEndpoint = process.env.NEXT_PUBLIC_MINIO_ENDPOINT || 'http://localhost:9000' return `${minioEndpoint}/${bucket}/${objectKey}` } catch { toast.error('Failed to upload file') throw new Error('Upload failed') } } const buildAccessJson = (): AccessRule[] | null => { if (accessMode === 'everyone') return null if (accessMode === 'roles' && selectedRoles.length > 0) { return [{ type: 'roles', roles: selectedRoles }] } return null } const handleSubmit = useCallback(async () => { if (!title.trim()) { toast.error('Please enter a title') return } try { await updateResource.mutateAsync({ id: resourceId, programId, title, description: description || null, contentJson: contentJson ? JSON.parse(contentJson) : undefined, accessJson: buildAccessJson(), externalUrl: externalUrl || null, isPublished, }) toast.success('Resource updated') } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to update resource') } }, [title, description, contentJson, externalUrl, isPublished, programId, accessMode, selectedRoles, resourceId]) const handleDelete = async () => { try { await deleteResource.mutateAsync({ id: resourceId }) toast.success('Resource deleted') router.push('/admin/learning') } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to delete resource') } } // Ctrl+S save useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault() handleSubmit() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [handleSubmit]) if (isLoading) { return (
) } if (error || !resource) { return (
Resource not found The resource you're looking for does not exist.
) } return (
{/* Sticky toolbar */}
Resource Settings Configure publishing, access, and metadata
{/* Publish toggle */}

Make visible to users

{/* Program */}
{/* Access Rules */}
{accessMode === 'roles' && (
{ROLE_OPTIONS.map((role) => ( ))}
)}
{/* External URL */}
setExternalUrl(e.target.value)} placeholder="https://example.com/resource" />

Optional link to an external resource

{/* Statistics */} {stats && (

{stats.totalViews}

Total views

{stats.uniqueUsers}

Unique users

)} {/* Danger Zone */}
Delete Resource Are you sure you want to delete "{resource.title}"? This action cannot be undone. Cancel {deleteResource.isPending ? ( ) : null} Delete
{/* Content area */}
{previewing ? ( ) : (
{/* Inline title */} setTitle(e.target.value)} placeholder="Untitled" className="w-full border-0 bg-transparent text-3xl font-bold tracking-tight text-foreground placeholder:text-muted-foreground/40 focus:outline-none sm:text-4xl" /> {/* Inline description */} setDescription(e.target.value)} placeholder="Add a description..." className="w-full border-0 bg-transparent text-lg text-muted-foreground placeholder:text-muted-foreground/30 focus:outline-none" /> {/* Divider */}
{/* Block editor */}
)}
) }