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.
Files changed (112) hide show
  1. package/README.md +129 -0
  2. package/package.json +113 -0
  3. package/src/_services/api/axios.factory.ts +97 -0
  4. package/src/_services/api/list-query.ts +26 -0
  5. package/src/_services/api/schema.d.ts +2527 -0
  6. package/src/_services/auth/index.ts +127 -0
  7. package/src/_utils/advanced-filter.ts +386 -0
  8. package/src/_utils/calendar.ts +103 -0
  9. package/src/_utils/crop-image.ts +65 -0
  10. package/src/_utils/filter.ts +313 -0
  11. package/src/_utils/format.ts +35 -0
  12. package/src/_utils/initials.ts +24 -0
  13. package/src/_utils/password.ts +7 -0
  14. package/src/components/app-shell.tsx +314 -0
  15. package/src/components/breadcrumbs.tsx +55 -0
  16. package/src/components/index.ts +7 -0
  17. package/src/components/language-selector.tsx +51 -0
  18. package/src/components/password-field.tsx +40 -0
  19. package/src/components/theme-toggle.tsx +27 -0
  20. package/src/components/ui/alert.tsx +27 -0
  21. package/src/components/ui/avatar.tsx +50 -0
  22. package/src/components/ui/badge.tsx +36 -0
  23. package/src/components/ui/button.tsx +62 -0
  24. package/src/components/ui/card.tsx +86 -0
  25. package/src/components/ui/checkbox.tsx +30 -0
  26. package/src/components/ui/column-visibility-modal.tsx +110 -0
  27. package/src/components/ui/confirm-dialog.tsx +69 -0
  28. package/src/components/ui/data-table.tsx +401 -0
  29. package/src/components/ui/date-range-picker.tsx +250 -0
  30. package/src/components/ui/dialog.tsx +138 -0
  31. package/src/components/ui/dropdown-menu.tsx +108 -0
  32. package/src/components/ui/field.tsx +20 -0
  33. package/src/components/ui/filter-chips.tsx +88 -0
  34. package/src/components/ui/filter-field.tsx +137 -0
  35. package/src/components/ui/filter-group.tsx +155 -0
  36. package/src/components/ui/filter-rule.tsx +221 -0
  37. package/src/components/ui/filter-sheet.tsx +201 -0
  38. package/src/components/ui/index.ts +30 -0
  39. package/src/components/ui/input.tsx +25 -0
  40. package/src/components/ui/label.tsx +23 -0
  41. package/src/components/ui/row-actions.tsx +32 -0
  42. package/src/components/ui/select.tsx +97 -0
  43. package/src/components/ui/separator.tsx +31 -0
  44. package/src/components/ui/sheet.tsx +119 -0
  45. package/src/components/ui/sonner.tsx +28 -0
  46. package/src/components/ui/spinner.tsx +12 -0
  47. package/src/components/ui/switch.tsx +29 -0
  48. package/src/components/ui/table.tsx +84 -0
  49. package/src/components/ui/tabs.tsx +52 -0
  50. package/src/components/user-avatar.tsx +54 -0
  51. package/src/contexts/auth-context.tsx +90 -0
  52. package/src/contexts/color-mode-context.tsx +105 -0
  53. package/src/contexts/i18n-context.tsx +76 -0
  54. package/src/contexts/index.ts +6 -0
  55. package/src/contexts/socket-context.tsx +153 -0
  56. package/src/contexts/toast-context.tsx +40 -0
  57. package/src/features/audit/audit-screen.tsx +299 -0
  58. package/src/features/audit/hooks/use-audit-trail.ts +68 -0
  59. package/src/features/audit/services/audit.service.ts +24 -0
  60. package/src/features/dashboard/dashboard-screen.tsx +73 -0
  61. package/src/features/login/components/backup-codes-dialog.tsx +66 -0
  62. package/src/features/login/components/index.ts +5 -0
  63. package/src/features/login/components/login-form.tsx +62 -0
  64. package/src/features/login/components/two-factor-setup.tsx +71 -0
  65. package/src/features/login/components/two-factor-verify.tsx +56 -0
  66. package/src/features/login/hooks/use-login-flow.ts +119 -0
  67. package/src/features/login/login-screen.tsx +83 -0
  68. package/src/features/login/validation/schemas.ts +30 -0
  69. package/src/features/logs/hooks/use-error-logs.ts +65 -0
  70. package/src/features/logs/hooks/use-request-logs.ts +65 -0
  71. package/src/features/logs/logs-screen.tsx +31 -0
  72. package/src/features/logs/panels/error-logs-panel.tsx +183 -0
  73. package/src/features/logs/panels/index.ts +3 -0
  74. package/src/features/logs/panels/request-logs-panel.tsx +124 -0
  75. package/src/features/logs/services/logs.service.ts +68 -0
  76. package/src/features/notifications/components/notification-item.tsx +67 -0
  77. package/src/features/notifications/hooks/use-notifications.ts +78 -0
  78. package/src/features/notifications/notifications-center.tsx +119 -0
  79. package/src/features/notifications/services/notifications.service.ts +38 -0
  80. package/src/features/profile/components/avatar-crop-dialog.tsx +133 -0
  81. package/src/features/profile/components/index.ts +2 -0
  82. package/src/features/profile/force-password-change.tsx +114 -0
  83. package/src/features/profile/profile-screen.tsx +385 -0
  84. package/src/features/rbac/panels/actions-panel.tsx +20 -0
  85. package/src/features/rbac/panels/catalog-panel.tsx +243 -0
  86. package/src/features/rbac/panels/groups-panel.tsx +269 -0
  87. package/src/features/rbac/panels/index.ts +8 -0
  88. package/src/features/rbac/panels/permissions-panel.tsx +204 -0
  89. package/src/features/rbac/panels/resources-panel.tsx +20 -0
  90. package/src/features/rbac/panels/roles-panel.tsx +224 -0
  91. package/src/features/rbac/panels/scopes-panel.tsx +20 -0
  92. package/src/features/rbac/rbac-screen.tsx +80 -0
  93. package/src/features/rbac/services/rbac.service.ts +110 -0
  94. package/src/features/recovery/forgot-password-screen.tsx +69 -0
  95. package/src/features/recovery/reset-password-screen.tsx +163 -0
  96. package/src/features/users/components/user-form-dialog.tsx +187 -0
  97. package/src/features/users/hooks/use-users.ts +181 -0
  98. package/src/features/users/services/users.service.ts +83 -0
  99. package/src/features/users/users-screen.tsx +331 -0
  100. package/src/hooks/use-column-visibility.ts +54 -0
  101. package/src/hooks/use-confirm.tsx +70 -0
  102. package/src/hooks/use-filter-schema.ts +85 -0
  103. package/src/hooks/use-filters.ts +148 -0
  104. package/src/hooks/use-list-query.ts +69 -0
  105. package/src/hooks/use-request.ts +114 -0
  106. package/src/i18n/messages/en.ts +296 -0
  107. package/src/i18n/messages/pt.ts +305 -0
  108. package/src/index.ts +60 -0
  109. package/src/lib/utils.ts +6 -0
  110. package/src/middleware.ts +42 -0
  111. package/src/styles/core.css +142 -0
  112. 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
+ }
@@ -0,0 +1,6 @@
1
+ // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
+ export * from "./auth-context";
3
+ export * from "./color-mode-context";
4
+ export * from "./i18n-context";
5
+ export * from "./socket-context";
6
+ export * from "./toast-context";