Files
saladeaula.digital/apps/saladeaula.digital/app/routes/index.tsx

291 lines
7.2 KiB
TypeScript

import type { Route } from './+types'
// import SHA256 from 'crypto-js/sha256'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@/components/ui/empty'
import {
BanIcon,
CircleCheckIcon,
CircleIcon,
CircleOffIcon,
CircleXIcon,
HelpCircleIcon,
TimerIcon,
type LucideIcon
} from 'lucide-react'
// import lzwCompress from 'lzwcompress'
import Fuse from 'fuse.js'
import { Suspense, useMemo, useState } from 'react'
import { Await, useLoaderData } from 'react-router'
import placeholder from '@/assets/placeholder.webp'
import { FacetedFilter } from '@/components/faceted-filter'
import { SearchForm } from '@/components/search-form'
import { Skeleton } from '@/components/skeleton'
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle
} from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { userContext } from '@/context'
import type { User } from '@/lib/auth'
import { createSearch } from '@/lib/meili'
export const data = [
{
id: 'fbad867a-0022-4605-814f-db8ebe2b17fb',
courseName: 'All Golf',
scormContentPath: 'all-golf-scorm12/shared/launchpage.html'
},
{
id: '5ece4b81-2243-4289-9394-b8d853ed0933',
courseName: 'CIPA',
scormContentPath: 'cipa-pt-1-scorm12/scormdriver/indexAPI.html'
},
{
id: '11ed8481-c6c7-4523-a856-6f7e8bfef022',
courseName: 'NR-18 Sinaleiro e Amarrador de Cargas para Içamento',
scormContentPath: 'nr-18-sinaleiro-pt-1-scorm12/scormdriver/indexAPI.html'
}
]
type Course = {
name: string
}
export type Enrollment = {
id: string
course: Course
status: number
progress: number
}
export function meta({}: Route.MetaArgs) {
return [{ title: 'Meus cursos' }]
}
export const loader = async ({ context }: Route.ActionArgs) => {
const user = context.get(userContext) as User
return {
data: createSearch({
index: 'betaeducacao-prod-enrollments',
filter: `user.id = "${user.sub}"`,
sort: ['created_at:desc'],
env: context.cloudflare.env
})
}
}
export const statuses = [
{
value: 'PENDING',
label: 'Não iniciado',
icon: CircleIcon
},
{
value: 'IN_PROGRESS',
label: 'Em andamento',
icon: TimerIcon
},
{
value: 'COMPLETED',
label: 'Aprovado',
icon: CircleCheckIcon
},
{
value: 'FAILED',
label: 'Reprovado',
icon: CircleXIcon
},
{
value: 'CANCELED',
label: 'Cancelado',
icon: CircleOffIcon
}
]
export default function Component({}: Route.ComponentProps) {
const { data } = useLoaderData()
const [term, setTerm] = useState<string>('')
const [status, setStatus] = useState<string[]>([])
return (
<div className="space-y-4">
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">Meus cursos</h1>
<p className="text-muted-foreground">
Aqui você encontra todos os cursos em que está matriculado,
organizados em um lugar. Acompanhe seu progresso e continue de onde
parou.
</p>
</div>
<Suspense fallback={<Skeleton />}>
<div className="flex gap-2.5">
<div className="w-full xl:w-93">
<SearchForm
onChange={(e) => {
setTerm(e.target.value)
}}
/>
</div>
<FacetedFilter
value={status}
onChange={setStatus}
title="Status"
options={statuses}
/>
</div>
<div className="grid lg:grid-cols-4 gap-5">
<Await resolve={data}>
{({ hits = [] }) => {
return <List term={term} hits={hits} />
}}
</Await>
</div>
</Suspense>
</div>
)
}
function List({ term, hits = [] }: { term: string; hits: Enrollment[] }) {
const fuse = useMemo(() => {
return new Fuse(hits, {
keys: ['course.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>
<EmptyHeader>
<EmptyMedia variant="icon">
<BanIcon />
</EmptyMedia>
<EmptyTitle>Nada encontrado</EmptyTitle>
<EmptyDescription>
Nenhum resultado para <mark>{term}</mark>.
</EmptyDescription>
</EmptyHeader>
</Empty>
)
}
return hits_.map((props: Enrollment, idx) => {
return <Enrollment key={idx} {...props} />
})
}
function Enrollment({
id,
course,
status,
progress
// scormContentPath,
// courseName
}: Enrollment) {
// const status_ = statusTranslate[status] ?? status
// const { icon: Icon, color } = statusIcon?.[status] ?? defaultIcon
// const [mounted, setMounted] = useState(false)
// const [progress, setProgress] = useState<number>(0)
// useEffect(() => {
// setMounted(true)
// const hash = SHA256(scormContentPath).toString()
// const stored = localStorage.getItem(`scormState.${hash}`)
// if (stored) {
// try {
// const scormState = JSON.parse(stored)
// const suspendData = JSON.parse(scormState?.cmi?.suspend_data || '{}')
// const d = lzwCompress.unpack(suspendData?.d)
// const data = JSON.parse(d || '{}')
// setProgress(data?.progress?.p ?? null)
// } catch (err) {
// console.warn('Erro ao carregar progresso:', err)
// }
// }
// }, [scormContentPath])
return (
<a href={`/player/${id}`} className="hover:scale-105 transition">
<Card className="overflow-hidden relative h-96">
<CardHeader className="z-1 relative">
<CardTitle className="text-xl/6">{course.name}</CardTitle>
</CardHeader>
<CardContent className="z-1">
{/* Você pode adicionar algo como tempo total aqui, se quiser */}
</CardContent>
<CardFooter className="absolute z-1 bottom-6 w-full flex gap-1.5">
{/* <Badge variant="secondary" className={color}>
<Icon className="stroke-2" />
{status_}
</Badge>*/}
<Progress value={progress} />
<span className="text-xs">{progress}%</span>
</CardFooter>
<img
src={placeholder}
alt={course.name}
className="absolute bottom-0 opacity-75"
/>
</Card>
</a>
)
}
export const statusIcon: Record<string, { icon: LucideIcon; color: string }> = {
PENDING: {
icon: CircleIcon,
color: 'bg-gray-500/50 border-gray-500'
},
IN_PROGRESS: {
icon: TimerIcon,
color: 'bg-blue-500/50 border-blue-500'
},
COMPLETED: {
icon: CircleCheckIcon,
color: 'bg-green-500/50 border-green-500'
},
FAILED: {
icon: CircleXIcon,
color: 'bg-red-500/50 border-red-500'
},
CANCELED: {
icon: CircleOffIcon,
color: 'bg-orange-500/50 border-orange-500'
}
}
const defaultIcon = {
icon: HelpCircleIcon,
color: 'bg-gray-500 border-gray-500'
}
const statusTranslate: Record<string, string> = {
PENDING: 'Não iniciado',
IN_PROGRESS: 'Em andamento',
COMPLETED: 'Aprovado',
FAILED: 'Reprovado',
CANCELED: 'Cancelado'
}