diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.checkout/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.checkout/route.tsx new file mode 100644 index 0000000..b080ee1 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.checkout/route.tsx @@ -0,0 +1,3 @@ +export default function Route() { + return <>... +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/data.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/data.ts new file mode 100644 index 0000000..df188cc --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/data.ts @@ -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 +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/grid.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/grid.tsx new file mode 100644 index 0000000..9b967d8 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/grid.tsx @@ -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 ( + + + + + + Nada encontrado + + Nenhum resultado para {search}. + + + + ) + } + + return ( +
+ {hits_.map((props: Course, idx) => { + return ( + + ) + })} +
+ ) +} + +function Course({ + name, + access_period, + cert, + metadata__unit_price, + custom_pricing +}: Course & { custom_pricing?: number }) { + return ( + + + {name} + + + + + + + {name} + + ) +} + +const currency = new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL' +}) diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/list.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/list.tsx new file mode 100644 index 0000000..2aceff2 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/list.tsx @@ -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 ( + + + + + + Curso + Tempo de acesso + Validade do cert. + Valor + + + + {hits_.map( + ( + { id, name, access_period, cert, metadata__unit_price }, + index + ) => { + const custom_pricing = customPricingMap.get(id) + + return ( + + {name} + {access_period} dias + + {cert?.exp_interval ? ( + <>{cert.exp_interval} dias + ) : ( + <>N/A + )} + + + {metadata__unit_price ? ( + <> + {custom_pricing ? ( + {custom_pricing} + ) : ( + {metadata__unit_price} + )} + + ) : ( + <>N/A + )} + + + ) + } + )} + + {hits_.length === 0 && ( + + + Nenhum resultado para {search}. + + + )} + +
+
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx index 0d8c2b0..ef8d8df 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx @@ -1,50 +1,23 @@ import type { Route } from './+types/route' -import Fuse from 'fuse.js' -import { AwardIcon, BanIcon, LaptopIcon } from 'lucide-react' +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 { - 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 { cn } from '@repo/ui/lib/utils' 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 placeholder from '@/assets/placeholder.webp' -import { Currency } from '@repo/ui/components/currency' - -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 -} +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' }] @@ -74,6 +47,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) { 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 ( }> @@ -88,6 +62,10 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) { {([{ hits }, { items }]) => { + const hits_ = hits.filter( + ({ metadata__unit_price = 0 }) => metadata__unit_price > 0 + ) as Course[] + return ( <>
@@ -101,17 +79,40 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) { } defaultValue={search} onChange={(search) => { - setSearchParams({ s: String(search) }) + setSearchParams((searchParams) => { + searchParams.set('s', String(search)) + return searchParams + }) }} />
+ + { + setSearchParams((searchParams) => { + searchParams.set('view', value) + return searchParams + }) + }} + > + + + + + + + - + {view === 'list' ? ( + + ) : ( + + )} ) }} @@ -119,132 +120,3 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
) } - -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 ( - - - - - - Nada encontrado - - Nenhum resultado para {search}. - - - - ) - } - - return ( -
- {hits_ - .filter(({ metadata__unit_price = 0 }) => metadata__unit_price > 0) - .map((props: Course, idx) => { - return ( - - ) - })} -
- ) -} - -function Course({ - name, - access_period, - cert, - metadata__unit_price, - custom_pricing -}: Course & { custom_pricing?: number }) { - return ( - - - {name} - - - - - - - {name} - - ) -} - -const currency = new Intl.NumberFormat('pt-BR', { - style: 'currency', - currency: 'BRL' -}) diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx index 7e3fee3..32b8a5a 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx @@ -72,13 +72,13 @@ export default function Route({}: Route.ComponentProps) { useEffect(() => { if (fetcher.data?.ok) { - toast.success('O colaborador foi atualizado.') + toast.success('O colaborador(a) foi atualizado(a).') return } switch (fetcher.data?.error?.type) { case 'RateLimitExceededError': - toast.error('Seu limite diário de atualizações foi atingido.') + toast.error('Limite diário de atualizações atingido.') return case 'CPFConflictError': setError('cpf', { message: 'CPF já está em uso' }) diff --git a/package-lock.json b/package-lock.json index b1f42a8..a4cd9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { "version": "1.2.8", "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-switch": "^1.2.6", "@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", "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/vite": "^4.1.17", diff --git a/packages/ui/package.json b/packages/ui/package.json index e31aa06..4de4484 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -32,6 +32,8 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@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", "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/vite": "^4.1.17", diff --git a/packages/ui/src/components/ui/toggle-group.tsx b/packages/ui/src/components/ui/toggle-group.tsx new file mode 100644 index 0000000..24a4850 --- /dev/null +++ b/packages/ui/src/components/ui/toggle-group.tsx @@ -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 & { + spacing?: number + } +>({ + size: "default", + variant: "default", + spacing: 0, +}) + +function ToggleGroup({ + className, + variant, + size, + spacing = 0, + children, + ...props +}: React.ComponentProps & + VariantProps & { + spacing?: number + }) { + return ( + + + {children} + + + ) +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(ToggleGroupContext) + + return ( + + {children} + + ) +} + +export { ToggleGroup, ToggleGroupItem } diff --git a/packages/ui/src/components/ui/toggle.tsx b/packages/ui/src/components/ui/toggle.tsx new file mode 100644 index 0000000..94ec8f5 --- /dev/null +++ b/packages/ui/src/components/ui/toggle.tsx @@ -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 & + VariantProps) { + return ( + + ) +} + +export { Toggle, toggleVariants }