Files
saladeaula.digital/apps/studio.saladeaula.digital/app/routes/index.tsx
2025-11-26 15:14:29 -03:00

209 lines
5.5 KiB
TypeScript

import type { Route } from './+types/index'
import Fuse from 'fuse.js'
import {
AwardIcon,
BanIcon,
FileBadgeIcon,
HatGlassesIcon,
LaptopIcon
} from 'lucide-react'
import { Suspense, useMemo } from 'react'
import { Await, NavLink, useSearchParams } from 'react-router'
import { createSearch } from '@repo/util/meili'
import { SearchForm } from '@repo/ui/components/search-form'
import { Skeleton } from '@repo/ui/components/skeleton'
import {
Card,
CardFooter,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@repo/ui/components/ui/empty'
import { Kbd } from '@repo/ui/components/ui/kbd'
import { Spinner } from '@repo/ui/components/ui/spinner'
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from '@repo/ui/components/ui/tooltip'
import type { Course } from './edit'
import placeholder from '@/assets/placeholder.webp'
import { cn } from '@repo/ui/lib/utils'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Gerenciar seus cursos' }]
}
export const loader = async ({ context }: Route.ActionArgs) => {
const courses = createSearch({
index: 'saladeaula_courses',
sort: ['created_at:desc'],
hitsPerPage: 100,
env: context.cloudflare.env
})
return {
data: courses
}
}
export default function Component({ loaderData: { data } }) {
const [searchParams, setSearchParams] = useSearchParams()
const term = searchParams.get('term') as string
return (
<Suspense fallback={<Skeleton />}>
<div className="space-y-4">
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">Cursos</h1>
<p className="text-muted-foreground">
Gerencie seus cursos com facilidade e organize seu conteúdo.
</p>
</div>
<div className="w-full xl:w-1/3">
<SearchForm
placeholder={
<>
Digite <Kbd>/</Kbd> para pesquisar
</>
}
defaultValue={term}
onChange={(term) => {
setSearchParams({ term })
}}
/>
</div>
<Await resolve={data}>
{({ hits = [] }) => <List term={term} hits={hits} />}
</Await>
</div>
</Suspense>
)
}
function List({ term, hits = [] }: { term: string; hits: Course[] }) {
const fuse = useMemo(() => {
return new Fuse(hits, {
keys: ['name'],
threshold: 0.3,
includeMatches: true
})
}, [hits])
const hits_ = useMemo(() => {
if (!term) {
return hits
}
return fuse.search(term).map(({ item }) => item)
}, [term, fuse, hits])
if (hits_.length === 0) {
return (
<Empty className="border border-dashed">
<EmptyHeader>
<EmptyMedia variant="icon">
<BanIcon />
</EmptyMedia>
<EmptyTitle>Nada encontrado</EmptyTitle>
<EmptyDescription>
Nenhum resultado para <mark>{term}</mark>.
</EmptyDescription>
</EmptyHeader>
</Empty>
)
}
return (
<div className="grid lg:grid-cols-4 gap-5">
{hits_.map((props: Course, idx) => {
return <Course key={idx} {...props} />
})}
</div>
)
}
function Course({ id, name, access_period, cert, draft }: Course) {
return (
<NavLink to={`/edit/${id}`} className="hover:scale-105 transition">
{({ isPending }) => (
<Card
className={cn(
'overflow-hidden relative h-96',
draft && 'border-dashed'
)}
>
{isPending && (
<div className="absolute bottom-0 right-0 p-6 z-1">
<Spinner className="size-6" />
</div>
)}
<CardHeader className="z-1 relative">
<CardTitle className="text-xl/6">{name}</CardTitle>
</CardHeader>
<CardFooter className="text-gray-300 text-sm absolute z-1 bottom-6 w-full flex gap-1.5">
<ul className="flex gap-2.5">
<li>
<Tooltip>
<TooltipTrigger className="flex gap-0.5 items-center">
<LaptopIcon className="size-4" />
<span>{access_period}d</span>
</TooltipTrigger>
<TooltipContent>
<p>Tempo de acesso ao curso</p>
</TooltipContent>
</Tooltip>
</li>
{cert?.exp_interval && (
<li>
<Tooltip>
<TooltipTrigger className="flex gap-0.5 items-center">
<AwardIcon className="size-4" />
<span>{cert.exp_interval}d</span>
</TooltipTrigger>
<TooltipContent>
<p>Perído de validade do certificado</p>
</TooltipContent>
</Tooltip>
</li>
)}
{cert?.s3_uri && (
<li className="flex items-center">
<FileBadgeIcon className="size-4" />
</li>
)}
{draft && (
<li className="flex items-center">
<HatGlassesIcon className="size-4" />
</li>
)}
</ul>
</CardFooter>
<img
src={placeholder}
alt={name}
className="absolute bottom-0 opacity-75"
/>
</Card>
)}
</NavLink>
)
}