add list to catalog
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Route() {
|
||||||
|
return <>...</>
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export type Cert = {
|
||||||
|
exp_interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Course = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
access_period: string
|
||||||
|
cert: Cert
|
||||||
|
metadata__unit_price?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomPricing = {
|
||||||
|
sk: string
|
||||||
|
unit_price: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { AwardIcon, BanIcon, LaptopIcon } from 'lucide-react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from '@repo/ui/components/ui/card'
|
||||||
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle
|
||||||
|
} from '@repo/ui/components/ui/empty'
|
||||||
|
|
||||||
|
import type { Course, CustomPricing } from './data'
|
||||||
|
import placeholder from '@/assets/placeholder.webp'
|
||||||
|
|
||||||
|
export function Grid({
|
||||||
|
search,
|
||||||
|
hits = [],
|
||||||
|
customPricing = []
|
||||||
|
}: {
|
||||||
|
search: string
|
||||||
|
hits: Course[]
|
||||||
|
customPricing: CustomPricing[]
|
||||||
|
}) {
|
||||||
|
const fuse = useMemo(() => {
|
||||||
|
return new Fuse(hits, {
|
||||||
|
keys: ['name'],
|
||||||
|
threshold: 0.3,
|
||||||
|
includeMatches: true
|
||||||
|
})
|
||||||
|
}, [hits])
|
||||||
|
|
||||||
|
const hits_ = useMemo(() => {
|
||||||
|
if (!search) {
|
||||||
|
return hits
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuse.search(search).map(({ item }) => item)
|
||||||
|
}, [search, fuse, hits])
|
||||||
|
|
||||||
|
const customPricingMap = new Map(
|
||||||
|
customPricing.map((x) => {
|
||||||
|
const [, courseId] = x.sk.split('#')
|
||||||
|
return [courseId, x.unit_price]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
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>{search}</mark>.
|
||||||
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||||
|
{hits_.map((props: Course, idx) => {
|
||||||
|
return (
|
||||||
|
<Course
|
||||||
|
key={idx}
|
||||||
|
custom_pricing={customPricingMap.get(props.id)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Course({
|
||||||
|
name,
|
||||||
|
access_period,
|
||||||
|
cert,
|
||||||
|
metadata__unit_price,
|
||||||
|
custom_pricing
|
||||||
|
}: Course & { custom_pricing?: number }) {
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden relative h-96">
|
||||||
|
<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 className="flex gap-0.5 items-center">
|
||||||
|
<LaptopIcon className="size-4" />
|
||||||
|
<span>{access_period}d</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{cert?.exp_interval && (
|
||||||
|
<li className="flex gap-0.5 items-center">
|
||||||
|
<AwardIcon className="size-4" />
|
||||||
|
<span>{cert.exp_interval}d</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metadata__unit_price && (
|
||||||
|
<>
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span
|
||||||
|
className={cn({
|
||||||
|
'line-through text-muted-foreground': custom_pricing,
|
||||||
|
'font-bold': !custom_pricing
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Currency>{metadata__unit_price}</Currency>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{custom_pricing && (
|
||||||
|
<span className="font-bold">
|
||||||
|
<Currency>{custom_pricing}</Currency>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</CardFooter>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={placeholder}
|
||||||
|
alt={name}
|
||||||
|
className="absolute bottom-0 dark:opacity-75"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currency = new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL'
|
||||||
|
})
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@repo/ui/components/ui/table'
|
||||||
|
|
||||||
|
import type { Course, CustomPricing } from './data'
|
||||||
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
|
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||||
|
|
||||||
|
export function List({
|
||||||
|
search,
|
||||||
|
hits = [],
|
||||||
|
customPricing = []
|
||||||
|
}: {
|
||||||
|
search: string
|
||||||
|
hits: Course[]
|
||||||
|
customPricing: CustomPricing[]
|
||||||
|
}) {
|
||||||
|
const fuse = useMemo(() => {
|
||||||
|
return new Fuse(hits, {
|
||||||
|
keys: ['name'],
|
||||||
|
threshold: 0.3,
|
||||||
|
includeMatches: true
|
||||||
|
})
|
||||||
|
}, [hits])
|
||||||
|
|
||||||
|
const hits_ = useMemo(() => {
|
||||||
|
if (!search) {
|
||||||
|
return hits
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuse.search(search).map(({ item }) => item)
|
||||||
|
}, [search, fuse, hits])
|
||||||
|
|
||||||
|
const customPricingMap = new Map(
|
||||||
|
customPricing.map((x) => {
|
||||||
|
const [, courseId] = x.sk.split('#')
|
||||||
|
return [courseId, x.unit_price]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="pointer-events-none">
|
||||||
|
<TableHead>Curso</TableHead>
|
||||||
|
<TableHead>Tempo de acesso</TableHead>
|
||||||
|
<TableHead>Validade do cert.</TableHead>
|
||||||
|
<TableHead>Valor</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{hits_.map(
|
||||||
|
(
|
||||||
|
{ id, name, access_period, cert, metadata__unit_price },
|
||||||
|
index
|
||||||
|
) => {
|
||||||
|
const custom_pricing = customPricingMap.get(id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>{name}</TableCell>
|
||||||
|
<TableCell>{access_period} dias</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{cert?.exp_interval ? (
|
||||||
|
<>{cert.exp_interval} dias</>
|
||||||
|
) : (
|
||||||
|
<>N/A</>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{metadata__unit_price ? (
|
||||||
|
<>
|
||||||
|
{custom_pricing ? (
|
||||||
|
<Currency>{custom_pricing}</Currency>
|
||||||
|
) : (
|
||||||
|
<Currency>{metadata__unit_price}</Currency>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>N/A</>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hits_.length === 0 && (
|
||||||
|
<TableRow className="pointer-events-none">
|
||||||
|
<TableCell colSpan={4} className="h-20 text-center">
|
||||||
|
Nenhum resultado para <mark>{search}</mark>.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,50 +1,23 @@
|
|||||||
import type { Route } from './+types/route'
|
import type { Route } from './+types/route'
|
||||||
|
|
||||||
import Fuse from 'fuse.js'
|
import { Grid2X2Icon, ListIcon } from 'lucide-react'
|
||||||
import { AwardIcon, BanIcon, LaptopIcon } from 'lucide-react'
|
|
||||||
import { Suspense, useMemo } from 'react'
|
import { Suspense, useMemo } from 'react'
|
||||||
import { Await, useSearchParams } from 'react-router'
|
import { Await, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import { SearchForm } from '@repo/ui/components/search-form'
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
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 { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
|
||||||
import { cloudflareContext } from '@repo/auth/context'
|
import { cloudflareContext } from '@repo/auth/context'
|
||||||
import { createSearch } from '@repo/util/meili'
|
import { createSearch } from '@repo/util/meili'
|
||||||
import { request as req } from '@repo/util/request'
|
import { request as req } from '@repo/util/request'
|
||||||
|
import {
|
||||||
|
ToggleGroup,
|
||||||
|
ToggleGroupItem
|
||||||
|
} from '@repo/ui/components/ui/toggle-group'
|
||||||
|
|
||||||
import placeholder from '@/assets/placeholder.webp'
|
import type { CustomPricing, Course } from './data'
|
||||||
import { Currency } from '@repo/ui/components/currency'
|
import { List } from './list'
|
||||||
|
import { Grid } from './grid'
|
||||||
type Cert = {
|
|
||||||
exp_interval: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Course = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
access_period: string
|
|
||||||
cert: Cert
|
|
||||||
metadata__unit_price?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomPricing = {
|
|
||||||
sk: string
|
|
||||||
unit_price: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Catálogo de cursos' }]
|
return [{ title: 'Catálogo de cursos' }]
|
||||||
@@ -74,6 +47,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
|
|||||||
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const search = searchParams.get('s') as string
|
const search = searchParams.get('s') as string
|
||||||
|
const view = searchParams.get('view') || 'grid'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Skeleton />}>
|
<Suspense fallback={<Skeleton />}>
|
||||||
@@ -88,6 +62,10 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
|
|
||||||
<Await resolve={data}>
|
<Await resolve={data}>
|
||||||
{([{ hits }, { items }]) => {
|
{([{ hits }, { items }]) => {
|
||||||
|
const hits_ = hits.filter(
|
||||||
|
({ metadata__unit_price = 0 }) => metadata__unit_price > 0
|
||||||
|
) as Course[]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col lg:flex-row justify-between gap-2.5 mb-2.5">
|
<div className="flex flex-col lg:flex-row justify-between gap-2.5 mb-2.5">
|
||||||
@@ -101,17 +79,40 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
}
|
}
|
||||||
defaultValue={search}
|
defaultValue={search}
|
||||||
onChange={(search) => {
|
onChange={(search) => {
|
||||||
setSearchParams({ s: String(search) })
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.set('s', String(search))
|
||||||
|
return searchParams
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<List
|
{view === 'list' ? (
|
||||||
search={search}
|
<List search={search} hits={hits_} customPricing={items} />
|
||||||
hits={hits as Course[]}
|
) : (
|
||||||
customPricing={items}
|
<Grid search={search} hits={hits_} customPricing={items} />
|
||||||
/>
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -119,132 +120,3 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function List({
|
|
||||||
search,
|
|
||||||
hits = [],
|
|
||||||
customPricing = []
|
|
||||||
}: {
|
|
||||||
search: string
|
|
||||||
hits: Course[]
|
|
||||||
customPricing: CustomPricing[]
|
|
||||||
}) {
|
|
||||||
const fuse = useMemo(() => {
|
|
||||||
return new Fuse(hits, {
|
|
||||||
keys: ['name'],
|
|
||||||
threshold: 0.3,
|
|
||||||
includeMatches: true
|
|
||||||
})
|
|
||||||
}, [hits])
|
|
||||||
|
|
||||||
const hits_ = useMemo(() => {
|
|
||||||
if (!search) {
|
|
||||||
return hits
|
|
||||||
}
|
|
||||||
|
|
||||||
return fuse.search(search).map(({ item }) => item)
|
|
||||||
}, [search, fuse, hits])
|
|
||||||
|
|
||||||
const customPricingMap = new Map(
|
|
||||||
customPricing.map((x) => {
|
|
||||||
const [, courseId] = x.sk.split('#')
|
|
||||||
return [courseId, x.unit_price]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
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>{search}</mark>.
|
|
||||||
</EmptyDescription>
|
|
||||||
</EmptyHeader>
|
|
||||||
</Empty>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
|
||||||
{hits_
|
|
||||||
.filter(({ metadata__unit_price = 0 }) => metadata__unit_price > 0)
|
|
||||||
.map((props: Course, idx) => {
|
|
||||||
return (
|
|
||||||
<Course
|
|
||||||
key={idx}
|
|
||||||
custom_pricing={customPricingMap.get(props.id)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Course({
|
|
||||||
name,
|
|
||||||
access_period,
|
|
||||||
cert,
|
|
||||||
metadata__unit_price,
|
|
||||||
custom_pricing
|
|
||||||
}: Course & { custom_pricing?: number }) {
|
|
||||||
return (
|
|
||||||
<Card className="overflow-hidden relative h-96">
|
|
||||||
<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 className="flex gap-0.5 items-center">
|
|
||||||
<LaptopIcon className="size-4" />
|
|
||||||
<span>{access_period}d</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{cert?.exp_interval && (
|
|
||||||
<li className="flex gap-0.5 items-center">
|
|
||||||
<AwardIcon className="size-4" />
|
|
||||||
<span>{cert.exp_interval}d</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{metadata__unit_price && (
|
|
||||||
<>
|
|
||||||
<li className="flex gap-1.5">
|
|
||||||
<span
|
|
||||||
className={cn({
|
|
||||||
'line-through text-muted-foreground': custom_pricing,
|
|
||||||
'font-bold': !custom_pricing
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Currency>{metadata__unit_price}</Currency>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{custom_pricing && (
|
|
||||||
<span className="font-bold">
|
|
||||||
<Currency>{custom_pricing}</Currency>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
|
|
||||||
<img
|
|
||||||
src={placeholder}
|
|
||||||
alt={name}
|
|
||||||
className="absolute bottom-0 dark:opacity-75"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currency = new Intl.NumberFormat('pt-BR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'BRL'
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -72,13 +72,13 @@ export default function Route({}: Route.ComponentProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetcher.data?.ok) {
|
if (fetcher.data?.ok) {
|
||||||
toast.success('O colaborador foi atualizado.')
|
toast.success('O colaborador(a) foi atualizado(a).')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (fetcher.data?.error?.type) {
|
switch (fetcher.data?.error?.type) {
|
||||||
case 'RateLimitExceededError':
|
case 'RateLimitExceededError':
|
||||||
toast.error('Seu limite diário de atualizações foi atingido.')
|
toast.error('Limite diário de atualizações atingido.')
|
||||||
return
|
return
|
||||||
case 'CPFConflictError':
|
case 'CPFConflictError':
|
||||||
setError('cpf', { message: 'CPF já está em uso' })
|
setError('cpf', { message: 'CPF já está em uso' })
|
||||||
|
|||||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -3499,6 +3499,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-toggle": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toggle-group": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.11",
|
||||||
|
"@radix-ui/react-toggle": "1.1.10",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||||
@@ -7441,6 +7495,8 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
|||||||
83
packages/ui/src/components/ui/toggle-group.tsx
Normal file
83
packages/ui/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||||
|
import { type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { toggleVariants } from "@/components/ui/toggle"
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
spacing: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
function ToggleGroup({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
spacing = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
data-slot="toggle-group"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
style={{ "--gap": spacing } as React.CSSProperties}
|
||||||
|
className={cn(
|
||||||
|
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToggleGroupItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
const context = React.useContext(ToggleGroupContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={context.variant || variant}
|
||||||
|
data-size={context.size || size}
|
||||||
|
data-spacing={context.spacing}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
||||||
|
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
47
packages/ui/src/components/ui/toggle.tsx
Normal file
47
packages/ui/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-2 min-w-9",
|
||||||
|
sm: "h-8 px-1.5 min-w-8",
|
||||||
|
lg: "h-10 px-2.5 min-w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
data-slot="toggle"
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants }
|
||||||
Reference in New Issue
Block a user