rl-core-front 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/package.json +113 -0
- package/src/_services/api/axios.factory.ts +97 -0
- package/src/_services/api/list-query.ts +26 -0
- package/src/_services/api/schema.d.ts +2527 -0
- package/src/_services/auth/index.ts +127 -0
- package/src/_utils/advanced-filter.ts +386 -0
- package/src/_utils/calendar.ts +103 -0
- package/src/_utils/crop-image.ts +65 -0
- package/src/_utils/filter.ts +313 -0
- package/src/_utils/format.ts +35 -0
- package/src/_utils/initials.ts +24 -0
- package/src/_utils/password.ts +7 -0
- package/src/components/app-shell.tsx +314 -0
- package/src/components/breadcrumbs.tsx +55 -0
- package/src/components/index.ts +7 -0
- package/src/components/language-selector.tsx +51 -0
- package/src/components/password-field.tsx +40 -0
- package/src/components/theme-toggle.tsx +27 -0
- package/src/components/ui/alert.tsx +27 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +86 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/column-visibility-modal.tsx +110 -0
- package/src/components/ui/confirm-dialog.tsx +69 -0
- package/src/components/ui/data-table.tsx +401 -0
- package/src/components/ui/date-range-picker.tsx +250 -0
- package/src/components/ui/dialog.tsx +138 -0
- package/src/components/ui/dropdown-menu.tsx +108 -0
- package/src/components/ui/field.tsx +20 -0
- package/src/components/ui/filter-chips.tsx +88 -0
- package/src/components/ui/filter-field.tsx +137 -0
- package/src/components/ui/filter-group.tsx +155 -0
- package/src/components/ui/filter-rule.tsx +221 -0
- package/src/components/ui/filter-sheet.tsx +201 -0
- package/src/components/ui/index.ts +30 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/row-actions.tsx +32 -0
- package/src/components/ui/select.tsx +97 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +119 -0
- package/src/components/ui/sonner.tsx +28 -0
- package/src/components/ui/spinner.tsx +12 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +84 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/user-avatar.tsx +54 -0
- package/src/contexts/auth-context.tsx +90 -0
- package/src/contexts/color-mode-context.tsx +105 -0
- package/src/contexts/i18n-context.tsx +76 -0
- package/src/contexts/index.ts +6 -0
- package/src/contexts/socket-context.tsx +153 -0
- package/src/contexts/toast-context.tsx +40 -0
- package/src/features/audit/audit-screen.tsx +299 -0
- package/src/features/audit/hooks/use-audit-trail.ts +68 -0
- package/src/features/audit/services/audit.service.ts +24 -0
- package/src/features/dashboard/dashboard-screen.tsx +73 -0
- package/src/features/login/components/backup-codes-dialog.tsx +66 -0
- package/src/features/login/components/index.ts +5 -0
- package/src/features/login/components/login-form.tsx +62 -0
- package/src/features/login/components/two-factor-setup.tsx +71 -0
- package/src/features/login/components/two-factor-verify.tsx +56 -0
- package/src/features/login/hooks/use-login-flow.ts +119 -0
- package/src/features/login/login-screen.tsx +83 -0
- package/src/features/login/validation/schemas.ts +30 -0
- package/src/features/logs/hooks/use-error-logs.ts +65 -0
- package/src/features/logs/hooks/use-request-logs.ts +65 -0
- package/src/features/logs/logs-screen.tsx +31 -0
- package/src/features/logs/panels/error-logs-panel.tsx +183 -0
- package/src/features/logs/panels/index.ts +3 -0
- package/src/features/logs/panels/request-logs-panel.tsx +124 -0
- package/src/features/logs/services/logs.service.ts +68 -0
- package/src/features/notifications/components/notification-item.tsx +67 -0
- package/src/features/notifications/hooks/use-notifications.ts +78 -0
- package/src/features/notifications/notifications-center.tsx +119 -0
- package/src/features/notifications/services/notifications.service.ts +38 -0
- package/src/features/profile/components/avatar-crop-dialog.tsx +133 -0
- package/src/features/profile/components/index.ts +2 -0
- package/src/features/profile/force-password-change.tsx +114 -0
- package/src/features/profile/profile-screen.tsx +385 -0
- package/src/features/rbac/panels/actions-panel.tsx +20 -0
- package/src/features/rbac/panels/catalog-panel.tsx +243 -0
- package/src/features/rbac/panels/groups-panel.tsx +269 -0
- package/src/features/rbac/panels/index.ts +8 -0
- package/src/features/rbac/panels/permissions-panel.tsx +204 -0
- package/src/features/rbac/panels/resources-panel.tsx +20 -0
- package/src/features/rbac/panels/roles-panel.tsx +224 -0
- package/src/features/rbac/panels/scopes-panel.tsx +20 -0
- package/src/features/rbac/rbac-screen.tsx +80 -0
- package/src/features/rbac/services/rbac.service.ts +110 -0
- package/src/features/recovery/forgot-password-screen.tsx +69 -0
- package/src/features/recovery/reset-password-screen.tsx +163 -0
- package/src/features/users/components/user-form-dialog.tsx +187 -0
- package/src/features/users/hooks/use-users.ts +181 -0
- package/src/features/users/services/users.service.ts +83 -0
- package/src/features/users/users-screen.tsx +331 -0
- package/src/hooks/use-column-visibility.ts +54 -0
- package/src/hooks/use-confirm.tsx +70 -0
- package/src/hooks/use-filter-schema.ts +85 -0
- package/src/hooks/use-filters.ts +148 -0
- package/src/hooks/use-list-query.ts +69 -0
- package/src/hooks/use-request.ts +114 -0
- package/src/i18n/messages/en.ts +296 -0
- package/src/i18n/messages/pt.ts +305 -0
- package/src/index.ts +60 -0
- package/src/lib/utils.ts +6 -0
- package/src/middleware.ts +42 -0
- package/src/styles/core.css +142 -0
- package/tailwind-preset.ts +103 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "#core/lib/utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Painel que entra pela lateral direita, sobre o Radix Dialog — mesma mecânica
|
|
11
|
+
* de foco, ESC e overlay do `dialog.tsx`, só a posição muda. No mobile ocupa a
|
|
12
|
+
* largura toda, porque um painel de 28rem numa tela de 360px é um modal pior.
|
|
13
|
+
*/
|
|
14
|
+
const Sheet = DialogPrimitive.Root;
|
|
15
|
+
const SheetTrigger = DialogPrimitive.Trigger;
|
|
16
|
+
const SheetClose = DialogPrimitive.Close;
|
|
17
|
+
|
|
18
|
+
const SheetOverlay = React.forwardRef<
|
|
19
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
21
|
+
>(({ className, ...props }, ref) => (
|
|
22
|
+
<DialogPrimitive.Overlay
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn(
|
|
25
|
+
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
));
|
|
31
|
+
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
32
|
+
|
|
33
|
+
const SheetContent = React.forwardRef<
|
|
34
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
35
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
36
|
+
>(({ className, children, ...props }, ref) => (
|
|
37
|
+
<DialogPrimitive.Portal>
|
|
38
|
+
<SheetOverlay />
|
|
39
|
+
<DialogPrimitive.Content
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={cn(
|
|
42
|
+
"fixed inset-y-0 right-0 z-50 flex h-full w-full flex-col gap-4 border-l border-border bg-card p-6 shadow-lg sm:max-w-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right data-[state=closed]:duration-200 data-[state=open]:duration-300",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring">
|
|
49
|
+
<X className="h-4 w-4" />
|
|
50
|
+
<span className="sr-only">Fechar</span>
|
|
51
|
+
</DialogPrimitive.Close>
|
|
52
|
+
</DialogPrimitive.Content>
|
|
53
|
+
</DialogPrimitive.Portal>
|
|
54
|
+
));
|
|
55
|
+
SheetContent.displayName = DialogPrimitive.Content.displayName;
|
|
56
|
+
|
|
57
|
+
const SheetHeader = ({
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element => (
|
|
61
|
+
<div className={cn("flex flex-col space-y-1.5 text-left", className)} {...props} />
|
|
62
|
+
);
|
|
63
|
+
SheetHeader.displayName = "SheetHeader";
|
|
64
|
+
|
|
65
|
+
const SheetBody = ({
|
|
66
|
+
className,
|
|
67
|
+
...props
|
|
68
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element => (
|
|
69
|
+
<div className={cn("flex-1 overflow-y-auto", className)} {...props} />
|
|
70
|
+
);
|
|
71
|
+
SheetBody.displayName = "SheetBody";
|
|
72
|
+
|
|
73
|
+
const SheetFooter = ({
|
|
74
|
+
className,
|
|
75
|
+
...props
|
|
76
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element => (
|
|
77
|
+
<div
|
|
78
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
SheetFooter.displayName = "SheetFooter";
|
|
83
|
+
|
|
84
|
+
const SheetTitle = React.forwardRef<
|
|
85
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
86
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
87
|
+
>(({ className, ...props }, ref) => (
|
|
88
|
+
<DialogPrimitive.Title
|
|
89
|
+
ref={ref}
|
|
90
|
+
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
));
|
|
94
|
+
SheetTitle.displayName = DialogPrimitive.Title.displayName;
|
|
95
|
+
|
|
96
|
+
const SheetDescription = React.forwardRef<
|
|
97
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
98
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
99
|
+
>(({ className, ...props }, ref) => (
|
|
100
|
+
<DialogPrimitive.Description
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
));
|
|
106
|
+
SheetDescription.displayName = DialogPrimitive.Description.displayName;
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
Sheet,
|
|
110
|
+
SheetBody,
|
|
111
|
+
SheetClose,
|
|
112
|
+
SheetContent,
|
|
113
|
+
SheetDescription,
|
|
114
|
+
SheetFooter,
|
|
115
|
+
SheetHeader,
|
|
116
|
+
SheetOverlay,
|
|
117
|
+
SheetTitle,
|
|
118
|
+
SheetTrigger,
|
|
119
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
import { Toaster as SonnerToaster } from "sonner";
|
|
5
|
+
|
|
6
|
+
import { useColorMode } from "#core/contexts";
|
|
7
|
+
|
|
8
|
+
export function Toaster(): JSX.Element {
|
|
9
|
+
const { mode } = useColorMode();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<SonnerToaster
|
|
13
|
+
position="top-right"
|
|
14
|
+
theme={mode}
|
|
15
|
+
richColors
|
|
16
|
+
toastOptions={{
|
|
17
|
+
classNames: {
|
|
18
|
+
toast: "group toast shadow-lg rounded-lg border",
|
|
19
|
+
description: "group-[.toast]:opacity-90",
|
|
20
|
+
actionButton:
|
|
21
|
+
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
|
22
|
+
cancelButton:
|
|
23
|
+
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
|
24
|
+
},
|
|
25
|
+
}}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Loader2 } from "lucide-react";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "#core/lib/utils";
|
|
5
|
+
|
|
6
|
+
export function Spinner({ className }: { className?: string }): JSX.Element {
|
|
7
|
+
return (
|
|
8
|
+
<Loader2
|
|
9
|
+
className={cn("h-5 w-5 animate-spin text-muted-foreground", className)}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "#core/lib/utils";
|
|
7
|
+
|
|
8
|
+
const Switch = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<SwitchPrimitives.Root
|
|
13
|
+
className={cn(
|
|
14
|
+
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
ref={ref}
|
|
19
|
+
>
|
|
20
|
+
<SwitchPrimitives.Thumb
|
|
21
|
+
className={cn(
|
|
22
|
+
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
25
|
+
</SwitchPrimitives.Root>
|
|
26
|
+
));
|
|
27
|
+
Switch.displayName = SwitchPrimitives.Root.displayName;
|
|
28
|
+
|
|
29
|
+
export { Switch };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "#core/lib/utils";
|
|
4
|
+
|
|
5
|
+
const Table = React.forwardRef<
|
|
6
|
+
HTMLTableElement,
|
|
7
|
+
React.HTMLAttributes<HTMLTableElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div className="relative w-full overflow-x-auto">
|
|
10
|
+
<table
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
));
|
|
17
|
+
Table.displayName = "Table";
|
|
18
|
+
|
|
19
|
+
const TableHeader = React.forwardRef<
|
|
20
|
+
HTMLTableSectionElement,
|
|
21
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
24
|
+
));
|
|
25
|
+
TableHeader.displayName = "TableHeader";
|
|
26
|
+
|
|
27
|
+
const TableBody = React.forwardRef<
|
|
28
|
+
HTMLTableSectionElement,
|
|
29
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
30
|
+
>(({ className, ...props }, ref) => (
|
|
31
|
+
<tbody
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
));
|
|
37
|
+
TableBody.displayName = "TableBody";
|
|
38
|
+
|
|
39
|
+
const TableRow = React.forwardRef<
|
|
40
|
+
HTMLTableRowElement,
|
|
41
|
+
React.HTMLAttributes<HTMLTableRowElement>
|
|
42
|
+
>(({ className, ...props }, ref) => (
|
|
43
|
+
<tr
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
"border-b border-border transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
));
|
|
52
|
+
TableRow.displayName = "TableRow";
|
|
53
|
+
|
|
54
|
+
const TableHead = React.forwardRef<
|
|
55
|
+
HTMLTableCellElement,
|
|
56
|
+
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
57
|
+
>(({ className, ...props }, ref) => (
|
|
58
|
+
<th
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn(
|
|
61
|
+
"h-11 px-4 text-left align-middle text-xs font-semibold uppercase tracking-wide text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
));
|
|
67
|
+
TableHead.displayName = "TableHead";
|
|
68
|
+
|
|
69
|
+
const TableCell = React.forwardRef<
|
|
70
|
+
HTMLTableCellElement,
|
|
71
|
+
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
72
|
+
>(({ className, ...props }, ref) => (
|
|
73
|
+
<td
|
|
74
|
+
ref={ref}
|
|
75
|
+
className={cn(
|
|
76
|
+
"px-4 py-3 align-middle [&:has([role=checkbox])]:pr-0",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
));
|
|
82
|
+
TableCell.displayName = "TableCell";
|
|
83
|
+
|
|
84
|
+
export { Table, TableBody, TableCell,TableHead, TableHeader, TableRow };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "#core/lib/utils";
|
|
7
|
+
|
|
8
|
+
const Tabs = TabsPrimitive.Root;
|
|
9
|
+
|
|
10
|
+
const TabsList = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<TabsPrimitive.List
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
"inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
24
|
+
|
|
25
|
+
const TabsTrigger = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<TabsPrimitive.Trigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
));
|
|
38
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
39
|
+
|
|
40
|
+
const TabsContent = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<TabsPrimitive.Content
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn("mt-4 focus-visible:outline-none", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
));
|
|
50
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
51
|
+
|
|
52
|
+
export { Tabs, TabsContent,TabsList, TabsTrigger };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
|
|
5
|
+
import { getInitials } from "#core/_utils/initials";
|
|
6
|
+
import { Avatar, AvatarFallback, AvatarImage } from "#core/components/ui";
|
|
7
|
+
import { cn } from "#core/lib/utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A API guarda o avatar como caminho (`/uploads/avatars/<id>.webp`), servido
|
|
11
|
+
* pela origem da API — que não é a da interface. Daí a necessidade de compor a
|
|
12
|
+
* URL absoluta aqui, em um lugar só.
|
|
13
|
+
*/
|
|
14
|
+
const API_ORIGIN = (process.env.NEXT_PUBLIC_API_BASE_URL ?? "").replace(
|
|
15
|
+
/\/api\/v1\/?$/,
|
|
16
|
+
"",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** Monta a URL absoluta do avatar; caminho vazio devolve `undefined`. */
|
|
20
|
+
export function avatarUrlOf(path?: string | null): string | undefined {
|
|
21
|
+
if (!path) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return path.startsWith("http") ? path : `${API_ORIGIN}${path}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UserAvatarProps {
|
|
28
|
+
name?: string | null;
|
|
29
|
+
lastName?: string | null;
|
|
30
|
+
/** Caminho devolvido pela API, não a URL completa. */
|
|
31
|
+
avatar?: string | null;
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Classe do texto das iniciais (tamanho da fonte acompanha o tamanho). */
|
|
34
|
+
fallbackClassName?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Avatar do usuário com fallback nas iniciais — usado no header e no perfil. */
|
|
38
|
+
export function UserAvatar({
|
|
39
|
+
name,
|
|
40
|
+
lastName,
|
|
41
|
+
avatar,
|
|
42
|
+
className,
|
|
43
|
+
fallbackClassName,
|
|
44
|
+
}: UserAvatarProps): JSX.Element {
|
|
45
|
+
const url = avatarUrlOf(avatar);
|
|
46
|
+
return (
|
|
47
|
+
<Avatar className={className}>
|
|
48
|
+
{url && <AvatarImage src={url} alt="" />}
|
|
49
|
+
<AvatarFallback className={cn(fallbackClassName)}>
|
|
50
|
+
{getInitials(name, lastName)}
|
|
51
|
+
</AvatarFallback>
|
|
52
|
+
</Avatar>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import { authService, Profile } from "#core/_services/auth";
|
|
13
|
+
|
|
14
|
+
export interface AuthContextType {
|
|
15
|
+
user: Profile | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
refreshProfile: () => Promise<void>;
|
|
18
|
+
logout: () => Promise<void>;
|
|
19
|
+
hasPermission: (code: string) => boolean;
|
|
20
|
+
hasAnyPermission: (...codes: string[]) => boolean;
|
|
21
|
+
hasAllPermissions: (...codes: string[]) => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
export function AuthProvider({ children }: { children: React.ReactNode }): JSX.Element {
|
|
27
|
+
const [user, setUser] = useState<Profile | null>(null);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
|
|
30
|
+
const refreshProfile = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const profile = await authService.profile();
|
|
33
|
+
setUser(profile);
|
|
34
|
+
} catch {
|
|
35
|
+
setUser(null);
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const logout = useCallback(async () => {
|
|
42
|
+
try {
|
|
43
|
+
await authService.logout();
|
|
44
|
+
} finally {
|
|
45
|
+
setUser(null);
|
|
46
|
+
if (typeof window !== "undefined") {window.location.href = "/login";}
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const hasPermission = useCallback(
|
|
51
|
+
(code: string) => !!user?.permissions?.includes(code),
|
|
52
|
+
[user],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const hasAnyPermission = useCallback(
|
|
56
|
+
(...codes: string[]) => codes.some((c) => user?.permissions?.includes(c)),
|
|
57
|
+
[user],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const hasAllPermissions = useCallback(
|
|
61
|
+
(...codes: string[]) => codes.every((c) => user?.permissions?.includes(c)),
|
|
62
|
+
[user],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
void refreshProfile();
|
|
67
|
+
}, [refreshProfile]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<AuthContext.Provider
|
|
71
|
+
value={{
|
|
72
|
+
user,
|
|
73
|
+
loading,
|
|
74
|
+
refreshProfile,
|
|
75
|
+
logout,
|
|
76
|
+
hasPermission,
|
|
77
|
+
hasAnyPermission,
|
|
78
|
+
hasAllPermissions,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</AuthContext.Provider>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useAuth(): AuthContextType {
|
|
87
|
+
const ctx = useContext(AuthContext);
|
|
88
|
+
if (!ctx) {throw new Error("useAuth deve ser usado dentro de AuthProvider");}
|
|
89
|
+
return ctx;
|
|
90
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
export const STORAGE_KEY = "color-mode";
|
|
14
|
+
|
|
15
|
+
export type ColorPreference = "light" | "dark" | "system";
|
|
16
|
+
type Mode = "light" | "dark";
|
|
17
|
+
|
|
18
|
+
/** Preferência padrão: seguir o sistema operacional, nunca escuro fixo. */
|
|
19
|
+
const DEFAULT_PREFERENCE: ColorPreference = "system";
|
|
20
|
+
|
|
21
|
+
function isPreference(value: string | null): value is ColorPreference {
|
|
22
|
+
return value === "light" || value === "dark" || value === "system";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function systemMode(): Mode {
|
|
26
|
+
if (typeof window === "undefined" || !window.matchMedia) {return "light";}
|
|
27
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
28
|
+
? "dark"
|
|
29
|
+
: "light";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyClass(mode: Mode): void {
|
|
33
|
+
document.documentElement.classList.toggle("dark", mode === "dark");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ColorModeContextType {
|
|
37
|
+
mode: Mode;
|
|
38
|
+
preference: ColorPreference;
|
|
39
|
+
setPreference: (p: ColorPreference) => void;
|
|
40
|
+
toggle: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ColorModeContext = createContext<ColorModeContextType | undefined>(
|
|
44
|
+
undefined,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export function ColorModeProvider({ children }: { children: React.ReactNode }): JSX.Element {
|
|
48
|
+
const [preference, setPreferenceState] =
|
|
49
|
+
useState<ColorPreference>(DEFAULT_PREFERENCE);
|
|
50
|
+
const [sysMode, setSysMode] = useState<Mode>("light");
|
|
51
|
+
// Antes de ler o localStorage não se sabe o tema real. O script anti-flash do
|
|
52
|
+
// layout já pôs a classe certa no <html>; aplicar classe antes disso só
|
|
53
|
+
// desfaria o trabalho dele e causava piscada de claro→escuro.
|
|
54
|
+
const [resolved, setResolved] = useState(false);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
58
|
+
if (isPreference(saved)) {
|
|
59
|
+
setPreferenceState(saved);
|
|
60
|
+
}
|
|
61
|
+
setSysMode(systemMode());
|
|
62
|
+
setResolved(true);
|
|
63
|
+
|
|
64
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
65
|
+
const onChange = (): void => setSysMode(systemMode());
|
|
66
|
+
mq.addEventListener("change", onChange);
|
|
67
|
+
return () => mq.removeEventListener("change", onChange);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const mode: Mode = preference === "system" ? sysMode : preference;
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (resolved) {
|
|
74
|
+
applyClass(mode);
|
|
75
|
+
}
|
|
76
|
+
}, [mode, resolved]);
|
|
77
|
+
|
|
78
|
+
const setPreference = useCallback((p: ColorPreference) => {
|
|
79
|
+
setPreferenceState(p);
|
|
80
|
+
localStorage.setItem(STORAGE_KEY, p);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const toggle = useCallback(
|
|
84
|
+
() => setPreference(mode === "light" ? "dark" : "light"),
|
|
85
|
+
[mode, setPreference],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const value = useMemo(
|
|
89
|
+
() => ({ mode, preference, setPreference, toggle }),
|
|
90
|
+
[mode, preference, setPreference, toggle],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<ColorModeContext.Provider value={value}>
|
|
95
|
+
{children}
|
|
96
|
+
</ColorModeContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function useColorMode(): ColorModeContextType {
|
|
101
|
+
const ctx = useContext(ColorModeContext);
|
|
102
|
+
if (!ctx)
|
|
103
|
+
{throw new Error("useColorMode deve ser usado dentro de ColorModeProvider");}
|
|
104
|
+
return ctx;
|
|
105
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
import { en } from "#core/i18n/messages/en";
|
|
14
|
+
import { pt } from "#core/i18n/messages/pt";
|
|
15
|
+
|
|
16
|
+
export type Locale = "pt" | "en";
|
|
17
|
+
const MESSAGES = { pt, en } as const;
|
|
18
|
+
const STORAGE_KEY = "locale";
|
|
19
|
+
|
|
20
|
+
export interface I18nContextType {
|
|
21
|
+
locale: Locale;
|
|
22
|
+
setLocale: (l: Locale) => void;
|
|
23
|
+
/** Traduz uma chave em dot-path (ex.: 'auth.loginTitle') com interpolação {param}. */
|
|
24
|
+
t: (key: string, params?: Record<string, string | number>) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const I18nContext = createContext<I18nContextType | undefined>(undefined);
|
|
28
|
+
|
|
29
|
+
function resolve(obj: unknown, path: string): string | undefined {
|
|
30
|
+
return path.split(".").reduce<unknown>((acc, part) => {
|
|
31
|
+
if (acc && typeof acc === "object")
|
|
32
|
+
{return (acc as Record<string, unknown>)[part];}
|
|
33
|
+
return undefined;
|
|
34
|
+
}, obj) as string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function I18nProvider({ children }: { children: React.ReactNode }): JSX.Element {
|
|
38
|
+
const [locale, setLocaleState] = useState<Locale>("pt");
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const saved = (localStorage.getItem(STORAGE_KEY) as Locale) || null;
|
|
42
|
+
if (saved === "pt" || saved === "en") {setLocaleState(saved);}
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const setLocale = useCallback((l: Locale) => {
|
|
46
|
+
setLocaleState(l);
|
|
47
|
+
localStorage.setItem(STORAGE_KEY, l);
|
|
48
|
+
document.cookie = `${STORAGE_KEY}=${l}; path=/; max-age=31536000`;
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const t = useCallback(
|
|
52
|
+
(key: string, params?: Record<string, string | number>) => {
|
|
53
|
+
const raw =
|
|
54
|
+
resolve(MESSAGES[locale], key) ?? resolve(MESSAGES.pt, key) ?? key;
|
|
55
|
+
if (!params) {return raw;}
|
|
56
|
+
return Object.entries(params).reduce(
|
|
57
|
+
(str, [k, v]) => str.replace(new RegExp(`\\{${k}\\}`, "g"), String(v)),
|
|
58
|
+
raw,
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
[locale],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const value = useMemo(
|
|
65
|
+
() => ({ locale, setLocale, t }),
|
|
66
|
+
[locale, setLocale, t],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useI18n(): I18nContextType {
|
|
73
|
+
const ctx = useContext(I18nContext);
|
|
74
|
+
if (!ctx) {throw new Error("useI18n deve ser usado dentro de I18nProvider");}
|
|
75
|
+
return ctx;
|
|
76
|
+
}
|