291 lines
7.2 KiB
TypeScript
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 só 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'
|
|
}
|