import type { Route } from './+types/edit' import { zodResolver } from '@hookform/resolvers/zod' import { FileBadgeIcon, FileCode2Icon, MoreHorizontalIcon } from 'lucide-react' import { Suspense, useState, type ReactNode } from 'react' import { useForm } from 'react-hook-form' import { Await, useAsyncValue, useFetcher } from 'react-router' import { toast } from 'sonner' import { z } from 'zod' import { Skeleton } from '@repo/ui/components/skeleton' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@repo/ui/components/ui/breadcrumb' import { Button } from '@repo/ui/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/ui/components/ui/card' import { Checkbox } from '@repo/ui/components/ui/checkbox' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@repo/ui/components/ui/dropdown-menu' import { FieldGroup, FieldLegend, FieldSet } from '@repo/ui/components/ui/field' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@repo/ui/components/ui/form' import { Input } from '@repo/ui/components/ui/input' import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@repo/ui/components/ui/input-group' import { Spinner } from '@repo/ui/components/ui/spinner' import { Switch } from '@repo/ui/components/ui/switch' import { HttpMethod, request as req } from '@repo/util/request' const formSchema = z .object({ given_cert: z.coerce.boolean(), never_expires: z.coerce.boolean(), name: z.string().min(1), access_period: z.coerce.number().min(1), cert: z.object({ exp_interval: z.coerce.number() }), rawfile: z .instanceof(File, { message: 'Anexe um arquivo HTML' }) .optional(), unlisted: z.boolean() }) .refine( (data) => { if (data?.never_expires) { return true } return data?.cert?.exp_interval > 0 }, { message: 'Deve ser maior ou igual a 1', path: ['cert', 'exp_interval'] } ) type Schema = z.infer type Cert = { exp_interval: number s3_uri?: string } export type Course = { id: string name: string access_period: number cert?: Cert unlisted?: boolean } export function meta({}: Route.MetaArgs) { return [{ title: 'Editar curso' }] } export const loader = async ({ params, context, request }: Route.LoaderArgs) => { const r = await req({ url: `/courses/${params.id}`, context, request }) if (!r.ok) { throw new Response(null, { status: r.status }) } return { data: r.json() as Promise } } export async function action({ params, request, context }: Route.ActionArgs) { const formData = await request.formData() const r = await req({ url: `courses/${params.id}`, method: HttpMethod.PUT, body: formData, request, context }) return { ok: r.status === 200 } } export default function Component({ loaderData: { data } }: Route.ComponentProps) { return ( }>
Cursos Editar curso
) } function Editing() { const course = useAsyncValue() as Course const fetcher = useFetcher() const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { unlisted: false, given_cert: !!course?.cert, never_expires: !course?.cert?.exp_interval, ...course } }) const { handleSubmit, formState, watch } = form const givenCert = watch('given_cert') const neverExpires = watch('never_expires') as boolean const onSubmit = async ({ given_cert, never_expires, ...data }: Schema) => { const formData = new FormData() const data_ = flattenObject(data, '.') for (const k in data_) { // @ts-ignore const v = data_[k] if (v) { formData.append(k, v) } } // If `s3_uri` exists, restore it if (course?.cert?.s3_uri) { formData.append('cert.s3_uri', course.cert.s3_uri) } await fetcher.submit(formData, { method: 'post', encType: 'multipart/form-data' }) toast.success('O curso foi atualizado') } return (
Editar curso Configurar as informações gerais para este curso ( Curso )} /> ( Tempo de acesso (em dias) )} /> (
Habilitar certificação Emita automaticamente o certificado de conclusão para os participantes.
)} /> {givenCert ? (
Configurações do certificado
( Período de validade (em dias) )} /> { return ( O certificado não possui prazo de validade. ) }} />
( Template do certificado { const file = e.target.files?.[0] if (file) { onChange(file) } }} {...field} /> {course?.cert?.s3_uri ? ( ) : null} Anexe o arquivo HTML que será utilizado na emissão dos certificados. )} />
) : null} ( Não listar no catálogo de cursos )} />
) } type DownloadMenuProps = { course_id: string s3_uri: string } function DownloadMenu({ course_id, s3_uri }: DownloadMenuProps) { return ( {({ isLoading }) => ( <>{isLoading ? : } Baixar amostra )} {({ isLoading }) => ( <> {isLoading ? : } Baixar template (html) )} ) } type DownloadMenuItemProps = { course_id: string url: string s3_uri: string children: (props: { isLoading: boolean }) => ReactNode } function DownloadMenuItem({ children, course_id, url, s3_uri }: DownloadMenuItemProps) { const [isLoading, setIsLoading] = useState(false) const handleClick = async (e: React.MouseEvent) => { e.preventDefault() setIsLoading(true) const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ s3_uri }) }) setIsLoading(false) if (r.ok) { const url = URL.createObjectURL(await r.blob()) const link = document.createElement('a') link.href = url link.download = course_id document.body.appendChild(link) link.click() link.remove() URL.revokeObjectURL(url) } } return ( ) } export function flattenObject< T extends Record, R extends Record = Record >(obj: T, delimiter: string = '.', prefix: string = ''): R { return Object.keys(obj).reduce((acc, key) => { const value = obj[key] const newKey = prefix ? `${prefix}${delimiter}${key}` : key if ( typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0 ) { Object.assign(acc, flattenObject(value, delimiter, newKey)) } else { ;(acc as any)[newKey] = value } return acc }, {} as R) }