diff --git a/apps/admin.saladeaula.digital/README.md b/apps/admin.saladeaula.digital/README.md new file mode 100644 index 0000000..9da0e71 --- /dev/null +++ b/apps/admin.saladeaula.digital/README.md @@ -0,0 +1,3 @@ +# [admin.saladeaula.digital](https://admin.saladeaula.digital) + +O código-fonte para [admin.saladeaula.digital](https://admin.saladeaula.digital), construído com [React Router](https://github.com/remix-run/react-router). diff --git a/apps/admin.saladeaula.digital/app/app.css b/apps/admin.saladeaula.digital/app/app.css new file mode 100644 index 0000000..b77868d --- /dev/null +++ b/apps/admin.saladeaula.digital/app/app.css @@ -0,0 +1,138 @@ +@import 'tailwindcss' source('.'); +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --font-sans: + 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} + +html, +body { + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} + +.dark [data-hide-on-theme='dark'], +.light [data-hide-on-theme='light'] { + @apply hidden; +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.65rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.648 0.2 131.684); + --primary-foreground: oklch(0.986 0.031 120.757); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.841 0.238 128.85); + --chart-1: oklch(0.871 0.15 154.449); + --chart-2: oklch(0.723 0.219 149.579); + --chart-3: oklch(0.627 0.194 149.214); + --chart-4: oklch(0.527 0.154 150.069); + --chart-5: oklch(0.448 0.119 151.328); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.648 0.2 131.684); + --sidebar-primary-foreground: oklch(0.986 0.031 120.757); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.841 0.238 128.85); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.648 0.2 131.684); + --primary-foreground: oklch(0.986 0.031 120.757); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.405 0.101 131.063); + --chart-1: oklch(0.871 0.15 154.449); + --chart-2: oklch(0.723 0.219 149.579); + --chart-3: oklch(0.627 0.194 149.214); + --chart-4: oklch(0.527 0.154 150.069); + --chart-5: oklch(0.448 0.119 151.328); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.768 0.233 130.85); + --sidebar-primary-foreground: oklch(0.986 0.031 120.757); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.405 0.101 131.063); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/admin.saladeaula.digital/app/assets/placeholder.webp b/apps/admin.saladeaula.digital/app/assets/placeholder.webp new file mode 100644 index 0000000..03afc9a Binary files /dev/null and b/apps/admin.saladeaula.digital/app/assets/placeholder.webp differ diff --git a/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx b/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx new file mode 100644 index 0000000..f9e32ae --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx @@ -0,0 +1,81 @@ +'use client' + +import { + BookCopyIcon, + CalendarClockIcon, + DollarSign, + GraduationCap, + LayoutDashboard, + ShieldUserIcon, + UsersIcon +} from 'lucide-react' + +import { NavMain } from '@/components/nav-main' +import { OrgSwitcher } from '@/components/org-switcher' +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader +} from '@/components/ui/sidebar' + +const data = { + navMain: [ + { + title: 'Visão geral', + url: '/main', + icon: LayoutDashboard + }, + { + title: 'Histórico de compras', + url: '/orders', + icon: DollarSign + }, + { + title: 'Colaboradores', + url: '/users', + icon: UsersIcon + }, + { + title: 'Gestores', + url: '/admins', + icon: ShieldUserIcon + } + // { + // title: 'Webhooks', + // url: '/webhooks', + // icon: WebhookIcon + // } + ], + navContent: [ + { + title: 'Matrículas', + url: '/enrollments', + icon: GraduationCap + }, + { + title: 'Agendamentos', + url: '/scheduled', + icon: CalendarClockIcon + }, + { + title: 'Catálogo de cursos', + url: '/courses', + icon: BookCopyIcon + } + ] +} + +export function AppSidebar({ orgs = [] }) { + return ( + + + + + + + + + + ) +} diff --git a/apps/admin.saladeaula.digital/app/components/dark-mode.tsx b/apps/admin.saladeaula.digital/app/components/dark-mode.tsx new file mode 100644 index 0000000..4af28fe --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/dark-mode.tsx @@ -0,0 +1,54 @@ +'use client' + +import { Moon, Sun, SunMoon } from 'lucide-react' +import { useTheme } from 'next-themes' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import dark from './logo-dark.svg' +import light from './logo-light.svg' + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme('light')}> + Claro + + setTheme('dark')}> + Escuro + + setTheme('system')}> + Sistema + + + + ) +} + +export function ThemedImage() { + return ( + <> + + + + ) +} diff --git a/apps/admin.saladeaula.digital/app/components/data-table.tsx b/apps/admin.saladeaula.digital/app/components/data-table.tsx new file mode 100644 index 0000000..dfc0cc4 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/data-table.tsx @@ -0,0 +1,266 @@ +'use client' + +import { + flexRender, + getCoreRowModel, + useReactTable, + type ColumnDef, + type Table as TTable, + type VisibilityState +} from '@tanstack/react-table' +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + Columns2Icon +} from 'lucide-react' +import { createContext, useContext, useState, type ReactNode } from 'react' +import { useSearchParams } from 'react-router' + +import { Card, CardContent } from '@/components/ui/card' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { Label } from '@/components/ui/label' +import { + Pagination, + PaginationContent, + PaginationItem +} from '@/components/ui/pagination' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import { cn } from '@/lib/utils' +import { Button } from './ui/button' + +interface DataTableProps { + children?: ReactNode + columns: ColumnDef[] + data: TData[] + pageIndex: number + pageSize: number + rowCount: number + hiddenColumn?: string[] +} + +const TableContext = createContext<{ table: TTable } | null>(null) + +export function DataTable({ + children, + columns, + data, + pageIndex, + pageSize, + rowCount, + hiddenColumn = [] +}: DataTableProps) { + const [, setSearchParams] = useSearchParams() + const hiddenColumn_ = Object.fromEntries( + hiddenColumn.map((column) => [column, false]) + ) + const [columnVisibility, setColumnVisibility] = + useState(hiddenColumn_) + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + state: { + columnVisibility, + pagination: { + pageIndex, + pageSize + } + }, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const newState = + typeof updater === 'function' + ? updater({ pageIndex, pageSize }) + : updater + + setSearchParams((searchParams) => { + searchParams.set('p', newState?.pageIndex.toString()) + searchParams.set('perPage', newState?.pageSize.toString()) + return searchParams + }) + }, + manualPagination: true, + rowCount + }) + + return ( + +
+ + + {children} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + Nenhum resultado. + + + )} + +
+
+
+ +
+
+ + +
+ +
+ + + + {(pageIndex + 1) * pageSize - pageSize + 1}- + {Math.min((pageIndex + 1) * pageSize, rowCount)} + + + + + + + + + + + +
+
+
+
+ ) +} + +export function CustomizeColumns({ className }: { className?: string }) { + const { table } = useContext(TableContext) + + return ( + + + + + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== 'undefined' && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.columnDef.header} + + ) + })} + + + ) +} diff --git a/apps/admin.saladeaula.digital/app/components/faceted-filter.tsx b/apps/admin.saladeaula.digital/app/components/faceted-filter.tsx new file mode 100644 index 0000000..761e30d --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/faceted-filter.tsx @@ -0,0 +1,148 @@ +import { CheckIcon } from 'lucide-react' +import { useState } from 'react' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { Separator } from '@/components/ui/separator' +import { cn } from '@/lib/utils' + +interface FacetedFilterProps { + value?: string[] + title?: string + icon: React.ComponentType + className?: string + options: { + label: string + value: string + icon?: React.ComponentType<{ className?: string }> + }[] + onChange?: (values: string[]) => void +} + +export function FacetedFilter({ + value = [], + icon: Icon, + title, + options, + onChange, + className +}: FacetedFilterProps) { + const [selectedValues, setSelectedValues] = useState(new Set(value)) + + return ( + + + + + + + + + Nenhum resultado encontrado. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value) + + return ( + { + if (isSelected) { + selectedValues.delete(option.value) + } else { + selectedValues.add(option.value) + } + + setSelectedValues(selectedValues) + onChange?.(Array.from(selectedValues)) + }} + > +
+ +
+ {option.icon && ( + + )} + {option.label} +
+ ) + })} +
+ {selectedValues.size > 0 && ( + <> + + + { + setSelectedValues(new Set()) + onChange?.([]) + }} + className="justify-center text-center cursor-pointer" + > + Limpar + + + + )} +
+
+
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/components/logo-dark.svg b/apps/admin.saladeaula.digital/app/components/logo-dark.svg new file mode 100644 index 0000000..30500d5 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/logo-dark.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/admin.saladeaula.digital/app/components/logo-light.svg b/apps/admin.saladeaula.digital/app/components/logo-light.svg new file mode 100644 index 0000000..fad8611 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/logo-light.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/admin.saladeaula.digital/app/components/nav-main.tsx b/apps/admin.saladeaula.digital/app/components/nav-main.tsx new file mode 100644 index 0000000..51ae8c9 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/nav-main.tsx @@ -0,0 +1,82 @@ +'use client' + +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar +} from '@/components/ui/sidebar' +import { useIsMobile } from '@/hooks/use-mobile' +import { type LucideIcon } from 'lucide-react' +import { NavLink, useParams } from 'react-router' + +type NavItem = { + title: string + url: string + icon?: LucideIcon +} + +export function NavMain({ + data +}: { + data: { + navMain: NavItem[] + navContent: NavItem[] + } +}) { + return ( + <> + + + + {data.navMain.map((props, idx) => ( + + ))} + + + + + + + + Gestão de matrículas + {data.navContent.map((props, idx) => ( + + ))} + + + + + ) +} + +function SidebarMenuItemLink({ title, url, icon: Icon }: NavItem) { + const { orgid } = useParams() + const { toggleSidebar } = useSidebar() + const isMobile = useIsMobile() + + const onToggle = () => (isMobile ? toggleSidebar() : null) + + return ( + + + {({ isActive }) => ( + + + {Icon && } + {title} + + + )} + + + ) +} diff --git a/apps/admin.saladeaula.digital/app/components/nav-user.tsx b/apps/admin.saladeaula.digital/app/components/nav-user.tsx new file mode 100644 index 0000000..74a69fd --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/nav-user.tsx @@ -0,0 +1,140 @@ +'use client' + +import { + CirclePlayIcon, + DollarSignIcon, + GraduationCapIcon, + LogOutIcon, + UserIcon +} from 'lucide-react' +import { Link } from 'react-router' + +import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { initials } from '@/lib/utils' + +export function NavUser({ + user +}: { + user: { + name: string + email: string + scope: string + } +}) { + const scopes = user.scope.split(' ') + + return ( + + + + {initials(user.name)} + + + + +
+ + {initials(user.name)} + +
+ {user.name} + + {user.email} + +
+
+
+ + + + + + + Minha conta + + + + + + Histórico de compras + + + + + + {grantIfHas(['apps:admin', 'apps:studio'], scopes, 'any') && ( + <> + + + Aplicações + + + )} + + + + + Sala de aula + + + + {grantIfHas(['apps:studio'], scopes) && ( + <> + + + + EDUSEG® Estúdio + + + + )} + + + + + + + Sair + + +
+
+ ) +} + +function grantIfHas( + required: string[], + granted: string[], + mode: 'all' | 'any' = 'all' +): boolean { + const grantedSet: Set = new Set(granted) + + if (mode === 'all') { + return required.every((scope) => grantedSet.has(scope)) + } + + return required.some((scope) => grantedSet.has(scope)) +} diff --git a/apps/admin.saladeaula.digital/app/components/org-switcher.tsx b/apps/admin.saladeaula.digital/app/components/org-switcher.tsx new file mode 100644 index 0000000..1290aff --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/org-switcher.tsx @@ -0,0 +1,102 @@ +'use client' + +import { formatCNPJ } from '@brazilian-utils/brazilian-utils' +import { CheckIcon, ChevronsUpDownIcon, PlusIcon } from 'lucide-react' +import { useState } from 'react' +import { useLocation, useParams } from 'react-router' + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar +} from '@/components/ui/sidebar' + +type Org = { + id: string + name: string + cnpj: string +} + +export function OrgSwitcher({ orgs }: { orgs: Org[] }) { + const location = useLocation() + const { isMobile } = useSidebar() + const { orgid } = useParams() + const org = orgs.find((org) => org.id === orgid) as Org + const [activeOrg, setActiveOrg] = useState(org) + const [, fragment, _] = location.pathname.slice(1).split('/') + + const onSelect = (org: Org) => { + setActiveOrg(org) + window.location.assign(`/${org.id}/${fragment}`) + } + + return ( + + + + + +
+ {activeOrg?.name} + + {formatCNPJ(activeOrg?.cnpj)} + +
+ +
+
+ + + Empresas + + {orgs.map((org, index) => ( + onSelect(org)} + className="group gap-2 p-2 cursor-pointer aria-selected:pointer-events-none" + aria-selected={org.id === activeOrg.id} + > +
+ {org?.name} + + {formatCNPJ(org?.cnpj)} + +
+ + + +
+ ))} + + +
+ +
+
+ Adicionar empresa +
+
+
+
+
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/components/range-calendar-filter.tsx b/apps/admin.saladeaula.digital/app/components/range-calendar-filter.tsx new file mode 100644 index 0000000..7fccae8 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/range-calendar-filter.tsx @@ -0,0 +1,119 @@ +'use client' + +import { CalendarIcon } from 'lucide-react' +import { useState, type ReactNode } from 'react' +import { type DateRange } from 'react-day-picker' +import { ptBR } from 'react-day-picker/locale' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Calendar } from '@/components/ui/calendar' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' + +import { Separator } from '@/components/ui/separator' +import { cn } from '@/lib/utils' + +const formatted = new Intl.DateTimeFormat('pt-BR', { + day: '2-digit', + month: '2-digit', + year: '2-digit' +}) + +type RangeCalendarFilterProps = { + children?: ReactNode + value?: DateRange + className?: string + onChange?: (values: DateRange | undefined) => void +} + +export function RangeCalendarFilter({ + children, + value, + className, + onChange +}: RangeCalendarFilterProps) { + const [dateRange, setDateRange] = useState(value) + + return ( + + + + + + {children && ( + <> + {children} + + + )} + + { + if (!dateRange) { + onChange?.(undefined) + return setDateRange(undefined) + } + + if (dateRange.to?.getTime() === dateRange.from?.getTime()) { + const nextDay = new Date(String(dateRange?.from)) + nextDay.setDate(nextDay.getDate() + 6) + dateRange.to = nextDay + } + + onChange?.(dateRange) + setDateRange(dateRange) + }} + /> + + {dateRange && ( + <> + +
+ +
+ + )} +
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/components/search-form.tsx b/apps/admin.saladeaula.digital/app/components/search-form.tsx new file mode 100644 index 0000000..149e480 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/search-form.tsx @@ -0,0 +1,77 @@ +import { debounce } from 'lodash' +import { SearchIcon, XIcon } from 'lucide-react' +import { useRef } from 'react' + +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput +} from '@/components/ui/input-group' + +import { useKeyPress } from '@/hooks/use-keypress' +import { cn } from '@/lib/utils' + +export function SearchForm({ + placeholder, + className, + onChange, + defaultValue = '', + ...props +}: { + placeholder?: React.ReactNode + className?: string + onChange?: (value: string) => void + defaultValue?: string +} & React.HTMLAttributes) { + const inputRef = useRef(null) + + useKeyPress('/', () => { + inputRef.current?.focus() + }) + + const debouncedOnChange = debounce((value: string) => { + onChange?.(value) + }, 200) + + return ( + + debouncedOnChange(e.target.value)} + {...props} + /> + + + + + + {placeholder && ( + + {placeholder} + + )} + + {defaultValue && ( + + { + if (inputRef.current) { + inputRef.current.value = '' + } + onChange?.('') + inputRef.current?.focus() + }} + > + + + + )} + + ) +} diff --git a/apps/admin.saladeaula.digital/app/components/skeleton.tsx b/apps/admin.saladeaula.digital/app/components/skeleton.tsx new file mode 100644 index 0000000..c86dd3d --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/skeleton.tsx @@ -0,0 +1,19 @@ +import { Skeleton as XSkeleton } from '@/components/ui/skeleton' + +export function Skeleton() { + return ( +
+
+ + + + + + + + + +
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/components/theme-provider.tsx b/apps/admin.saladeaula.digital/app/components/theme-provider.tsx new file mode 100644 index 0000000..ec648cb --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/theme-provider.tsx @@ -0,0 +1,10 @@ +'use client' + +import { ThemeProvider as NextThemesProvider } from 'next-themes' + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} diff --git a/apps/admin.saladeaula.digital/app/components/ui/avatar.tsx b/apps/admin.saladeaula.digital/app/components/ui/avatar.tsx new file mode 100644 index 0000000..b7224f0 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/apps/admin.saladeaula.digital/app/components/ui/badge.tsx b/apps/admin.saladeaula.digital/app/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/apps/admin.saladeaula.digital/app/components/ui/breadcrumb.tsx b/apps/admin.saladeaula.digital/app/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..d62407f --- /dev/null +++ b/apps/admin.saladeaula.digital/app/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return