This commit is contained in:
2025-11-11 17:06:25 -03:00
parent c00a42ea39
commit 39aedac972
30 changed files with 802 additions and 220 deletions

View File

@@ -12,7 +12,7 @@ import {
} from 'lucide-react'
import { NavMain } from '@/components/nav-main'
import { OrgSwitcher } from '@/components/org-switcher'
import { WorkspaceSwitcher } from '@/components/workspace-switcher'
import {
Sidebar,
SidebarContent,
@@ -42,11 +42,6 @@ const data = {
url: '/admins',
icon: ShieldUserIcon
}
// {
// title: 'Webhooks',
// url: '/webhooks',
// icon: WebhookIcon
// }
],
navContent: [
{
@@ -76,7 +71,7 @@ export function AppSidebar({ orgs = [] }) {
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<OrgSwitcher orgs={orgs} />
<WorkspaceSwitcher />
</SidebarHeader>
<SidebarContent>
<NavMain data={data} />

View File

@@ -29,6 +29,7 @@ import {
TableHeader,
TableRow
} from '@repo/ui/components/ui/table'
import { cn } from '@repo/ui/lib/utils'
import { DataTablePagination } from './pagination'
interface DataTableProps<TData, TValue> {
@@ -145,7 +146,7 @@ export function DataTable<TData, TValue>({
<CardContent>
{children}
<Table_>
<Table_ className="table-auto">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
@@ -154,7 +155,13 @@ export function DataTable<TData, TValue>({
>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="p-2.5">
<TableHead
key={header.id}
className={cn(
'p-2.5',
header.column.columnDef.meta?.className
)}
>
{header.isPlaceholder
? null
: flexRender(
@@ -176,7 +183,13 @@ export function DataTable<TData, TValue>({
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="p-2.5">
<TableCell
key={cell.id}
className={cn(
'p-2.5',
cell.column.columnDef.meta?.className
)}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()

View File

@@ -2,7 +2,7 @@
import { formatCNPJ } from '@brazilian-utils/brazilian-utils'
import { CheckIcon, ChevronsUpDownIcon, PlusIcon } from 'lucide-react'
import { useState } from 'react'
import { createContext, useContext, useState } from 'react'
import { useLocation, useParams } from 'react-router'
import {
@@ -23,23 +23,60 @@ import {
} from '@repo/ui/components/ui/sidebar'
import { initials } from '@repo/ui/lib/utils'
type Org = {
type Workspace = {
id: string
name: string
cnpj: string
}
export function OrgSwitcher({ orgs }: { orgs: Org[] }) {
type WorkspaceContextProps = {
workspaces: Workspace[]
activeWorkspace: Workspace | null
setActiveWorkspace: React.Dispatch<React.SetStateAction<Workspace | null>>
}
const WorkspaceContext = createContext<WorkspaceContextProps | null>(null)
export function useWorksapce() {
const ctx = useContext(WorkspaceContext)
if (!ctx) {
throw new Error('WorkspaceContext is null')
}
return ctx
}
export function WorkspaceProvider({
workspaces,
children
}: {
workspaces: Workspace[]
children: React.ReactNode
}) {
const { orgid } = useParams()
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(
() => workspaces.find((ws) => ws.id === orgid) ?? null
)
return (
<WorkspaceContext
value={{ workspaces, activeWorkspace, setActiveWorkspace }}
>
{children}
</WorkspaceContext>
)
}
export function WorkspaceSwitcher() {
const location = useLocation()
const { isMobile, state } = useSidebar()
const { orgid } = useParams()
const org = orgs.find((org) => org.id === orgid) as Org
const [activeOrg, setActiveOrg] = useState<Org>(org)
const { activeWorkspace, setActiveWorkspace, workspaces } = useWorksapce()
const [, fragment, _] = location.pathname.slice(1).split('/')
const onSelect = (org: Org) => {
setActiveOrg(org)
window.location.assign(`/${org.id}/${fragment}`)
const onSelect = (ws: Workspace) => {
setActiveWorkspace(ws)
window.location.assign(`/${ws.id}/${fragment}`)
}
return (
@@ -55,17 +92,20 @@ export function OrgSwitcher({ orgs }: { orgs: Org[] }) {
className="aria-expanded:border flex aspect-square size-8 items-center justify-center rounded-lg"
aria-expanded={state === 'expanded'}
>
{initials(activeOrg?.name)}
{initials(activeWorkspace?.name)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeOrg?.name}</span>
<span className="truncate font-medium">
{activeWorkspace?.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{formatCNPJ(activeOrg?.cnpj)}
{formatCNPJ(activeWorkspace?.cnpj)}
</span>
</div>
<ChevronsUpDownIcon className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
@@ -75,20 +115,23 @@ export function OrgSwitcher({ orgs }: { orgs: Org[] }) {
<DropdownMenuLabel className="text-muted-foreground text-xs">
Empresas
</DropdownMenuLabel>
{orgs.map((org, index) => (
{workspaces.map((workspace, index) => (
<DropdownMenuItem
key={index}
onClick={() => onSelect(org)}
onClick={() => onSelect(workspace)}
className="group gap-2 p-2 cursor-pointer aria-selected:pointer-events-none"
aria-selected={org.id === activeOrg.id}
aria-selected={workspace.id === activeWorkspace?.id}
>
<div className="flex size-8 items-center justify-center rounded-lg border">
{initials(org?.name)}
{initials(workspace?.name)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{org?.name}</span>
<span className="truncate font-medium">
{workspace?.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{formatCNPJ(org?.cnpj)}
{formatCNPJ(workspace?.cnpj)}
</span>
</div>
<DropdownMenuShortcut className="not-group-aria-selected:hidden">
@@ -96,7 +139,9 @@ export function OrgSwitcher({ orgs }: { orgs: Org[] }) {
</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<PlusIcon className="size-4" />

View File

@@ -43,4 +43,4 @@ export default async function handleRequest(
}
// https://reactrouter.com/how-to/suspense#timeouts
export const streamTimeout = 6_000
export const streamTimeout = 7_000

View File

@@ -31,9 +31,15 @@ export function request({
const requestId = context.get(requestIdContext) as string
const user = context.get(userContext) as User
const url_ = new URL(url, context.cloudflare.env.API_URL)
const headers = new Headers(
Object.assign({ Authorization: `Bearer ${user.accessToken}` }, _headers)
)
const headers = new Headers({
Authorization: `Bearer ${user.accessToken}`
})
if (_headers instanceof Headers) {
_headers.forEach((value, key) => headers.set(key, value))
} else {
Object.entries(_headers).forEach(([key, value]) => headers.set(key, value))
}
console.log(
`[${new Date().toISOString()}] [${requestId}] ${method} ${url_.toString()}`

View File

@@ -119,9 +119,6 @@ export default function Route({ loaderData: { data } }) {
{selectedRows.length ? (
<>
<div className="flex gap-2.5 items-center">
<Button variant="outline">
<TagIcon /> Marcador
</Button>
<DropdownMenuExport
headers={headers}
selectedRows={selectedRows}

View File

@@ -43,7 +43,14 @@ export const columns: ColumnDef<Order>[] = [
}
},
{
header: 'Data de venc.',
header: 'Comprado em',
cell: ({ row }) => {
const createdAt = new Date(row.original.create_date)
return formatted.format(createdAt)
}
},
{
header: 'Vencimento em',
cell: ({ row }) => {
try {
const dueDate = new Date(row.original.due_date)
@@ -54,10 +61,14 @@ export const columns: ColumnDef<Order>[] = [
}
},
{
header: 'Comprado em',
header: 'Pago em',
cell: ({ row }) => {
const createdAt = new Date(row.original.create_date)
return formatted.format(createdAt)
if (row.original.payment_date) {
const createdAt = new Date(row.original.payment_date)
return formatted.format(createdAt)
}
return <></>
}
}
]

View File

@@ -1,12 +1,9 @@
import type { Route } from './+types'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { PatternFormat } from 'react-number-format'
import { Link, useOutletContext } from 'react-router'
import { z } from 'zod'
import type { User } from '@/routes/_.$orgid.users.$id/route'
import { Button } from '@repo/ui/components/ui/button'
import {
Card,
@@ -27,13 +24,8 @@ import { Input } from '@repo/ui/components/ui/input'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { useForm } from 'react-hook-form'
const formSchema = z.object({
name: z.string().trim().nonempty('Digite seu nome'),
email: z.email(),
cpf: z
.string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' })
})
import type { User } from '../_.$orgid.users.$id/route'
import { formSchema, type Schema } from '../_.$orgid.users.add/route'
export default function Route() {
const { user } = useOutletContext() as { user: User }
@@ -43,7 +35,7 @@ export default function Route() {
})
const { handleSubmit, control, formState } = form
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const onSubmit = async (data: Schema) => {
console.log(data)
}
@@ -133,7 +125,7 @@ export default function Route() {
disabled={formState.isSubmitting}
>
{formState.isSubmitting && <Spinner />}
Editar colaborador
Editar
</Button>
</div>
</form>

View File

@@ -1,10 +1,27 @@
import type { Route } from './+types'
import { Suspense } from 'react'
import { Await } from 'react-router'
import { Await, useOutletContext } from 'react-router'
import { request as req } from '@/lib/request'
import { Skeleton } from '@repo/ui/components/skeleton'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { Item, ItemContent, ItemGroup } from '@repo/ui/components/ui/item'
import { Kbd } from '@repo/ui/components/ui/kbd'
import {
NativeSelect,
NativeSelectOption
} from '@repo/ui/components/ui/native-select'
import { Button } from '@repo/ui/components/ui/button'
import type { User } from '../_.$orgid.users.$id/route'
export async function loader({ params, request, context }: Route.LoaderArgs) {
const { id } = params
@@ -18,16 +35,58 @@ export async function loader({ params, request, context }: Route.LoaderArgs) {
}
export default function Route({ loaderData: { data } }) {
const { user } = useOutletContext() as { user: User }
return (
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{({ items = [] }) => (
<ul>
{items.map(({ sk }: { sk: string }, idx: number) => {
const [, email] = sk.split('#')
return <li key={idx}>{email}</li>
})}
</ul>
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">Emails</CardTitle>
<CardDescription>
Podem ser associados vários emails a uma conta. É possível
usar qualquer email para recuperar a senha, mas apenas o email
principal receberá as mensagens.
</CardDescription>
</CardHeader>
<CardContent>
<ul>
{items.map(({ sk }: { sk: string }, idx: number) => {
const [, email] = sk.split('#')
return <li key={idx}>{email}</li>
})}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Email principal</CardTitle>
<CardDescription>
<Kbd className="font-mono border">{user.email}</Kbd> será
usado para mensagens e pode ser usado para redefinições de
senha.
</CardDescription>
</CardHeader>
<CardContent>
<form className="flex gap-1.5">
<NativeSelect value={user.email}>
{items.map(({ sk }: { sk: string }, idx: number) => {
const [, email] = sk.split('#')
return (
<NativeSelectOption key={idx} value={email}>
{email}
</NativeSelectOption>
)
})}
</NativeSelect>
<Button>Mudar</Button>
</form>
</CardContent>
</Card>
</div>
)}
</Await>
</Suspense>

View File

@@ -1,40 +0,0 @@
import { Await } from 'react-router'
import type { Route } from './+types'
import { request as req } from '@/lib/request'
import { Skeleton } from '@repo/ui/components/skeleton'
import { Suspense } from 'react'
export async function loader({ params, request, context }: Route.LoaderArgs) {
const { id } = params
const r = req({
url: `/users/${id}/orgs`,
request,
context
}).then((r) => r.json())
return { data: r }
}
export default function Route({ loaderData: { data } }) {
return (
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{({ items = [] }) => (
<ul>
{items.map(
({ name, cnpj }: { name: string; cnpj: string }, idx: number) => {
return (
<li key={idx}>
{name} {cnpj}
</li>
)
}
)}
</ul>
)}
</Await>
</Suspense>
)
}

View File

@@ -8,6 +8,7 @@ import {
} from 'react-router'
import { request as req } from '@/lib/request'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import {
Breadcrumb,
@@ -90,7 +91,7 @@ export default function Route({
</Avatar>
<ul>
<li className="font-bold">{user.name}</li>
<li className="font-bold text-lg">{user.name}</li>
<li className="text-muted-foreground text-sm">{user.email}</li>
</ul>
</div>

View File

@@ -2,12 +2,23 @@
import { formatCPF } from '@brazilian-utils/brazilian-utils'
import { type ColumnDef } from '@tanstack/react-table'
import { ArrowRight } from 'lucide-react'
import {
ArrowRight,
EllipsisVerticalIcon,
PencilIcon,
UserRoundMinusIcon
} from 'lucide-react'
import { NavLink } from 'react-router'
import { Abbr } from '@/components/abbr'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Button } from '@repo/ui/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { initials } from '@repo/ui/lib/utils'
@@ -65,13 +76,6 @@ export const columns: ColumnDef<User>[] = [
return <></>
}
},
{
header: 'Cadastrado em',
cell: ({ row }) => {
const created_at = new Date(row.original.createDate)
return formatted.format(created_at)
}
},
{
header: 'Último accesso',
cell: ({ row }) => {
@@ -85,30 +89,47 @@ export const columns: ColumnDef<User>[] = [
}
},
{
header: ' ',
header: 'Cadastrado em',
meta: {
className: 'w-1/12'
},
cell: ({ row }) => {
return (
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
className="relative group"
asChild
>
<NavLink to={`${row.original?.id}`}>
{({ isPending }) => (
<>
{isPending && <Spinner className="absolute" />}
<span className="group-[.pending]:invisible">
Editar
</span>{' '}
<ArrowRight className="group-[.pending]:invisible" />
</>
)}
</NavLink>
</Button>
</div>
)
const created_at = new Date(row.original.createDate)
return formatted.format(created_at)
}
},
{
id: 'actions',
cell: ({ row }) => (
<div className="flex justify-end items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-36 *:cursor-pointer">
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<NavLink to={`${row.id}`}>
{({ isPending }) => (
<>
{isPending ? <Spinner /> : <PencilIcon />}
Editar
</>
)}
</NavLink>
</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
<UserRoundMinusIcon /> Desvincular
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
]

View File

@@ -61,6 +61,7 @@ export default function Route({ loaderData: { data } }) {
{({ hits, page, hitsPerPage, totalHits }) => {
return (
<DataTable
sort={[{ id: 'created_at', desc: true }]}
columns={columns}
data={hits as User[]}
pageIndex={page - 1}

View File

@@ -1,4 +1,12 @@
import { Link } from 'react-router'
import type { Route } from './+types'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { PatternFormat } from 'react-number-format'
import { Link, useFetcher } from 'react-router'
import { toast } from 'sonner'
import { z } from 'zod'
import {
Breadcrumb,
@@ -8,6 +16,7 @@ import {
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Button } from '@repo/ui/components/ui/button'
import {
Card,
CardContent,
@@ -15,8 +24,91 @@ import {
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input'
import { Label } from '@repo/ui/components/ui/label'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { useWorksapce } from '@/components/workspace-switcher'
import { HttpMethod, request as req } from '@/lib/request'
import { useEffect } from 'react'
const isName = (name: string) => name && name.includes(' ')
export const formSchema = z.object({
name: z
.string()
.trim()
.nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }),
email: z.email('Email inválido').trim().toLowerCase(),
cpf: z
.string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' })
})
export type Schema = z.infer<typeof formSchema>
export function meta({}: Route.MetaArgs) {
return [{ title: 'Adicionar colaborador' }]
}
export async function action({ request, context }: Route.ActionArgs) {
const body = await request.json()
const r = await req({
url: `users`,
headers: new Headers({ 'Content-Type': 'application/json' }),
method: HttpMethod.POST,
body: JSON.stringify(body),
request,
context
})
console.log(r)
if (!r.ok) {
const error = await r.json().catch(() => ({}))
return { ok: false, error }
}
return { ok: true }
}
export default function Route() {
const fetcher = useFetcher()
const { activeWorkspace } = useWorksapce()
const form = useForm({
resolver: zodResolver(formSchema)
})
const { handleSubmit, control, formState, reset } = form
const onSubmit = async (user: Schema) => {
await fetcher.submit(JSON.stringify({ user, org: activeWorkspace }), {
method: 'post',
encType: 'application/json'
})
}
useEffect(() => {
if (fetcher.data?.ok) {
toast.success('O colaborador foi adicionado')
return reset()
}
switch (fetcher.data?.error?.type) {
case 'UserConflictError':
toast.error('O colaborador já foi vinculado anteriormente')
}
}, [fetcher.data])
return (
<div className="space-y-2.5">
<Breadcrumb>
@@ -33,17 +125,97 @@ export default function Route() {
</BreadcrumbList>
</Breadcrumb>
<div className="lg:max-w-2xl mx-auto space-y-2.5">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Adicionar colaborador</CardTitle>
<CardDescription>
Siga os passos abaixo para cadastrar um novo colaborador
</CardDescription>
</CardHeader>
<div className="lg:max-w-2xl mx-auto">
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-2xl">
Adicionar colaborador
</CardTitle>
<CardDescription>
Siga os passos abaixo para cadastrar um novo colaborador
</CardDescription>
</CardHeader>
<CardContent></CardContent>
</Card>
<CardContent className="space-y-4">
<FormField
control={control}
name="name"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-2.5">
<FormField
control={control}
name="email"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center gap-2">
<Checkbox id="terms" tabIndex={-1} />
<Label htmlFor="terms">
Usar um email fornecido pela plataforma.
</Label>
</div>
</div>
<FormField
control={control}
name="cpf"
defaultValue=""
render={({ field: { onChange, ref, ...props } }) => (
<FormItem>
<FormLabel>CPF</FormLabel>
<FormControl>
<PatternFormat
format="###.###.###-##"
mask="_"
placeholder="___.___.___-__"
customInput={Input}
getInputRef={ref}
onValueChange={({ value }) => {
onChange(value)
}}
{...props}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<div className="flex justify-end">
<Button
type="submit"
className="cursor-pointer"
disabled={formState.isSubmitting}
>
{formState.isSubmitting && <Spinner />}
Adicionar
</Button>
</div>
</form>
</Form>
</div>
</div>
)

View File

@@ -6,6 +6,7 @@ import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
import { AppSidebar } from '@/components/app-sidebar'
import { request as req } from '@/lib/request'
import { WorkspaceProvider } from '@/components/workspace-switcher'
import { userContext } from '@repo/auth/context'
import { authMiddleware } from '@repo/auth/middleware/auth'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
@@ -15,6 +16,7 @@ import {
SidebarProvider,
SidebarTrigger
} from '@repo/ui/components/ui/sidebar'
import { Toaster } from '@repo/ui/components/ui/sonner'
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
@@ -59,31 +61,34 @@ export default function Route({ loaderData }: Route.ComponentProps) {
const { user, orgs, sidebar_state } = loaderData
return (
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">
<AppSidebar orgs={orgs} />
<WorkspaceProvider workspaces={orgs}>
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">
<AppSidebar />
<SidebarInset className="relative flex flex-col flex-1 min-w-0">
<header
className="bg-background/15 backdrop-blur-sm
px-4 py-2 lg:py-4 sticky top-0 z-5"
>
<div className="container mx-auto flex items-center">
<SidebarTrigger className="md:hidden" />
<ThemedImage className="max-md:hidden" />
<SidebarInset className="relative flex flex-col flex-1 min-w-0">
<header
className="bg-background/15 backdrop-blur-sm
px-4 py-2 lg:py-4 sticky top-0 z-5"
>
<div className="container mx-auto flex items-center">
<SidebarTrigger className="md:hidden" />
<ThemedImage className="max-md:hidden" />
<div className="ml-auto flex gap-2.5 items-center">
<ModeToggle />
<NavUser user={user} />
<div className="ml-auto flex gap-2.5 items-center">
<ModeToggle />
<NavUser user={user} />
</div>
</div>
</div>
</header>
</header>
<main className="p-4">
<div className="container mx-auto">
<Outlet />
</div>
</main>
</SidebarInset>
</SidebarProvider>
<main className="p-4">
<div className="container mx-auto relative">
<Outlet />
<Toaster position="top-center" richColors={true} />
</div>
</main>
</SidebarInset>
</SidebarProvider>
</WorkspaceProvider>
)
}