'use client' import { useCallback, useEffect, useState } from 'react' import { useRouter } from 'next/navigation' 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet' import { toast } from 'sonner' import { ArrowLeft, Save, Loader2, Settings, Eye, } 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' }, ] type AccessRule = | { type: 'everyone' } | { type: 'roles'; roles: string[] } | { type: 'jury_group'; juryGroupIds: string[] } | { type: 'round'; roundIds: string[] } export default function NewLearningResourcePage() { const router = useRouter() // 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 createResource = trpc.learningResource.create.useMutation({ onSuccess: () => utils.learningResource.list.invalidate(), }) const getUploadUrl = trpc.learningResource.getUploadUrl.useMutation() // 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 createResource.mutateAsync({ programId, title, description: description || undefined, contentJson: contentJson ? JSON.parse(contentJson) : undefined, accessJson: buildAccessJson(), externalUrl: externalUrl || undefined, isPublished, }) toast.success('Resource created successfully') router.push('/admin/learning') } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to create resource') } }, [title, description, contentJson, externalUrl, isPublished, programId, accessMode, selectedRoles]) // 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]) 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

{/* Content area */}
{previewing ? ( ) : (
{/* Inline title */} setTitle(e.target.value)} placeholder="Untitled" autoFocus 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 */}
)}
) }