'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.
Back to Learning Hub
)
}
return (
{/* Sticky toolbar */}
Back
setPreviewing(!previewing)}
>
{previewing ? 'Edit' : 'Preview'}
Settings
Resource Settings
Configure publishing, access, and metadata
{/* Publish toggle */}
Published
Make visible to users
{/* Program */}
Program
setProgramId(v === 'global' ? null : v)}
>
Global (All Programs)
{programs?.map((program) => (
{program.year} Edition
))}
{/* Access Rules */}
Access Rules
setAccessMode(v as 'everyone' | 'roles')}>
Everyone
By Role
{accessMode === 'roles' && (
{ROLE_OPTIONS.map((role) => (
{
setSelectedRoles(
checked
? [...selectedRoles, role.value]
: selectedRoles.filter((r) => r !== role.value)
)
}}
/>
{role.label}
))}
)}
{/* External URL */}
{/* Statistics */}
{stats && (
Statistics
{stats.totalViews}
Total views
{stats.uniqueUsers}
Unique users
)}
{/* Danger Zone */}
Danger Zone
Delete Resource
Delete Resource
Are you sure you want to delete "{resource.title}"?
This action cannot be undone.
Cancel
{deleteResource.isPending ? (
) : null}
Delete
{updateResource.isPending ? (
) : (
)}
Save
{/* Content area */}
)
}