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}
+
+
+
+
+ -
+
+ {access_period}d
+
+
+ {cert?.exp_interval && (
+ -
+
+ {cert.exp_interval}d
+
+ )}
+
+ {metadata__unit_price && (
+ <>
+ -
+
+ {metadata__unit_price}
+
+
+ {custom_pricing && (
+
+ {custom_pricing}
+
+ )}
+
+ >
+ )}
+
+
+
+
+
+ )
+}
+
+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}
-
-
-
-
- -
-
- {access_period}d
-
-
- {cert?.exp_interval && (
- -
-
- {cert.exp_interval}d
-
- )}
-
- {metadata__unit_price && (
- <>
- -
-
- {metadata__unit_price}
-
-
- {custom_pricing && (
-
- {custom_pricing}
-
- )}
-
- >
- )}
-
-
-
-
-
- )
-}
-
-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 }