123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import type { Route } from './+types/route'
|
|
|
|
import { Grid2X2Icon, ListIcon } from 'lucide-react'
|
|
import { Suspense, useMemo } from 'react'
|
|
import { Await, useSearchParams } from 'react-router'
|
|
|
|
import { SearchForm } from '@repo/ui/components/search-form'
|
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
|
import { cloudflareContext } from '@repo/auth/context'
|
|
import { createSearch } from '@repo/util/meili'
|
|
import { request as req } from '@repo/util/request'
|
|
import {
|
|
ToggleGroup,
|
|
ToggleGroupItem
|
|
} from '@repo/ui/components/ui/toggle-group'
|
|
|
|
import type { CustomPricing, Course } from './data'
|
|
import { List } from './list'
|
|
import { Grid } from './grid'
|
|
|
|
export function meta({}: Route.MetaArgs) {
|
|
return [{ title: 'Catálogo de cursos' }]
|
|
}
|
|
|
|
export async function loader({ context, request, params }: Route.LoaderArgs) {
|
|
const cloudflare = context.get(cloudflareContext)
|
|
const courses = createSearch({
|
|
index: 'saladeaula_courses',
|
|
sort: ['created_at:desc'],
|
|
filter: 'unlisted = false',
|
|
hitsPerPage: 100,
|
|
env: cloudflare.env
|
|
})
|
|
|
|
const customPricing = req({
|
|
url: `/orgs/${params.orgid}/custom-pricing`,
|
|
context,
|
|
request
|
|
}).then((r) => r.json() as Promise<{ items: CustomPricing[] }>)
|
|
|
|
return {
|
|
data: Promise.all([courses, customPricing])
|
|
}
|
|
}
|
|
|
|
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const search = searchParams.get('s') as string
|
|
const view = searchParams.get('view') || 'grid'
|
|
|
|
return (
|
|
<Suspense fallback={<Skeleton />}>
|
|
<div className="space-y-0.5 mb-8">
|
|
<h1 className="text-2xl font-bold tracking-tight">
|
|
Catálogo de cursos
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Explore o catálogo de cursos disponíveis para sua empresa.
|
|
</p>
|
|
</div>
|
|
|
|
<Await resolve={data}>
|
|
{([{ hits }, { items }]) => {
|
|
const hits_ = hits.filter(
|
|
({ metadata__unit_price = 0 }) => metadata__unit_price > 0
|
|
) as Course[]
|
|
|
|
return (
|
|
<>
|
|
<div className="flex lg:flex-row justify-between gap-2.5 mb-2.5">
|
|
<div className="2xl:w-92">
|
|
<SearchForm
|
|
placeholder={
|
|
<>
|
|
Digite <Kbd className="border font-mono">/</Kbd> para
|
|
pesquisar
|
|
</>
|
|
}
|
|
defaultValue={search}
|
|
onChange={(search) => {
|
|
setSearchParams((searchParams) => {
|
|
searchParams.set('s', String(search))
|
|
return searchParams
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<ToggleGroup
|
|
variant="outline"
|
|
type="single"
|
|
value={view}
|
|
className="**:cursor-pointer"
|
|
onValueChange={(value) => {
|
|
setSearchParams((searchParams) => {
|
|
searchParams.set('view', value)
|
|
return searchParams
|
|
})
|
|
}}
|
|
>
|
|
<ToggleGroupItem value="list">
|
|
<ListIcon />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="grid">
|
|
<Grid2X2Icon />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
</div>
|
|
|
|
{view === 'list' ? (
|
|
<List search={search} hits={hits_} customPricing={items} />
|
|
) : (
|
|
<Grid search={search} hits={hits_} customPricing={items} />
|
|
)}
|
|
</>
|
|
)
|
|
}}
|
|
</Await>
|
|
</Suspense>
|
|
)
|
|
}
|