@greatapps/common 1.1.71 → 1.1.75
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/dist/components/layouts/AppNavBar.mjs +0 -2
- package/dist/components/layouts/AppNavBar.mjs.map +1 -1
- package/dist/components/layouts/NotificationsPopover.mjs +24 -14
- package/dist/components/layouts/NotificationsPopover.mjs.map +1 -1
- package/dist/components/pages/notifications/Notifications.mjs +106 -0
- package/dist/components/pages/notifications/Notifications.mjs.map +1 -0
- package/dist/components/widgets/notifications/NotificationCard.mjs +53 -0
- package/dist/components/widgets/notifications/NotificationCard.mjs.map +1 -0
- package/dist/index.mjs +177 -47
- package/dist/index.mjs.map +1 -1
- package/dist/modules/auth/actions/login.action.mjs +1 -1
- package/dist/modules/auth/actions/login.action.mjs.map +1 -1
- package/dist/modules/auth/hooks/login.hook.mjs +5 -1
- package/dist/modules/auth/hooks/login.hook.mjs.map +1 -1
- package/dist/modules/auth/hooks/useUserQuery.mjs +2 -1
- package/dist/modules/auth/hooks/useUserQuery.mjs.map +1 -1
- package/dist/modules/auth/services/auth.service.mjs +2 -2
- package/dist/modules/auth/services/auth.service.mjs.map +1 -1
- package/dist/modules/users/action/list-messages.action.mjs +9 -0
- package/dist/modules/users/action/list-messages.action.mjs.map +1 -0
- package/dist/modules/users/action/mark-all-messages-as-read.action.mjs +9 -0
- package/dist/modules/users/action/mark-all-messages-as-read.action.mjs.map +1 -0
- package/dist/modules/users/action/mark-message-as-read.action.mjs +9 -0
- package/dist/modules/users/action/mark-message-as-read.action.mjs.map +1 -0
- package/dist/modules/users/hooks/messages.hook.mjs +38 -0
- package/dist/modules/users/hooks/messages.hook.mjs.map +1 -0
- package/dist/modules/users/schema/messages.schema.mjs +21 -0
- package/dist/modules/users/schema/messages.schema.mjs.map +1 -0
- package/dist/modules/users/services/messages.service.mjs +35 -0
- package/dist/modules/users/services/messages.service.mjs.map +1 -0
- package/dist/providers/auth.provider.mjs +3 -5
- package/dist/providers/auth.provider.mjs.map +1 -1
- package/dist/utils/safeServerAction.mjs +22 -0
- package/dist/utils/safeServerAction.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/layouts/AppNavBar.tsx +0 -4
- package/src/components/layouts/NotificationsPopover.tsx +27 -24
- package/src/components/pages/notifications/Notifications.tsx +106 -0
- package/src/components/widgets/notifications/NotificationCard.tsx +66 -0
- package/src/index.ts +267 -117
- package/src/modules/auth/actions/login.action.ts +1 -1
- package/src/modules/auth/hooks/login.hook.tsx +6 -1
- package/src/modules/auth/hooks/useUserQuery.ts +2 -1
- package/src/modules/auth/schema.ts +6 -4
- package/src/modules/auth/services/auth.service.ts +2 -2
- package/src/modules/users/action/list-messages.action.ts +10 -0
- package/src/modules/users/action/mark-all-messages-as-read.action.ts +8 -0
- package/src/modules/users/action/mark-message-as-read.action.ts +8 -0
- package/src/modules/users/hooks/messages.hook.ts +39 -0
- package/src/modules/users/schema/messages.schema.ts +47 -0
- package/src/modules/users/services/messages.service.ts +48 -0
- package/src/providers/auth.provider.tsx +6 -9
- package/src/utils/safeServerAction.ts +27 -0
- package/dist/components/layouts/NotificationItem.mjs +0 -14
- package/dist/components/layouts/NotificationItem.mjs.map +0 -1
- package/src/components/layouts/NotificationItem.tsx +0 -26
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { messagesService } from "../services/messages.service";
|
|
3
|
+
async function markMessageAsReadAction(messageId) {
|
|
4
|
+
return messagesService.markAsRead(messageId);
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
markMessageAsReadAction
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=mark-message-as-read.action.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/users/action/mark-message-as-read.action.ts"],"sourcesContent":["'use server';\r\n\r\nimport { messagesService } from '../services/messages.service';\r\nimport type { MarkAsReadApiResponse } from '../schema/messages.schema';\r\n\r\nexport async function markMessageAsReadAction(messageId: string): Promise<MarkAsReadApiResponse> {\r\n return messagesService.markAsRead(messageId);\r\n}\r\n"],"mappings":";AAEA,SAAS,uBAAuB;AAGhC,eAAsB,wBAAwB,WAAmD;AAC/F,SAAO,gBAAgB,WAAW,SAAS;AAC7C;","names":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { listMessagesAction } from "../action/list-messages.action";
|
|
4
|
+
import { markMessageAsReadAction } from "../action/mark-message-as-read.action";
|
|
5
|
+
import { markAllMessagesAsReadAction } from "../action/mark-all-messages-as-read.action";
|
|
6
|
+
import { safeServerAction } from "../../../utils/safeServerAction";
|
|
7
|
+
const MESSAGES_QUERY_KEY = ["messages"];
|
|
8
|
+
function useMessages(params) {
|
|
9
|
+
return useQuery({
|
|
10
|
+
queryKey: [...MESSAGES_QUERY_KEY, params],
|
|
11
|
+
queryFn: () => safeServerAction(listMessagesAction, params)
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function useMarkMessageAsRead() {
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
return useMutation({
|
|
17
|
+
mutationFn: (messageId) => safeServerAction(markMessageAsReadAction, messageId),
|
|
18
|
+
onSuccess: () => {
|
|
19
|
+
queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEY });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function useMarkAllMessagesAsRead() {
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
return useMutation({
|
|
26
|
+
mutationFn: () => safeServerAction(markAllMessagesAsReadAction),
|
|
27
|
+
onSuccess: () => {
|
|
28
|
+
queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEY });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
MESSAGES_QUERY_KEY,
|
|
34
|
+
useMarkAllMessagesAsRead,
|
|
35
|
+
useMarkMessageAsRead,
|
|
36
|
+
useMessages
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=messages.hook.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/users/hooks/messages.hook.ts"],"sourcesContent":["'use client';\r\n\r\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\r\nimport { listMessagesAction } from '../action/list-messages.action';\r\nimport { markMessageAsReadAction } from '../action/mark-message-as-read.action';\r\nimport { markAllMessagesAsReadAction } from '../action/mark-all-messages-as-read.action';\r\nimport { safeServerAction } from '../../../utils/safeServerAction';\r\nimport type { FindMessagesParams } from '../schema/messages.schema';\r\n\r\nexport const MESSAGES_QUERY_KEY = ['messages'];\r\n\r\nexport function useMessages(params?: Partial<FindMessagesParams>) {\r\n return useQuery({\r\n queryKey: [...MESSAGES_QUERY_KEY, params],\r\n queryFn: () => safeServerAction(listMessagesAction, params),\r\n });\r\n}\r\n\r\nexport function useMarkMessageAsRead() {\r\n const queryClient = useQueryClient();\r\n\r\n return useMutation({\r\n mutationFn: (messageId: string) => safeServerAction(markMessageAsReadAction, messageId),\r\n onSuccess: () => {\r\n queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEY });\r\n },\r\n });\r\n}\r\n\r\nexport function useMarkAllMessagesAsRead() {\r\n const queryClient = useQueryClient();\r\n\r\n return useMutation({\r\n mutationFn: () => safeServerAction(markAllMessagesAsReadAction),\r\n onSuccess: () => {\r\n queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEY });\r\n },\r\n });\r\n}\r\n"],"mappings":";AAEA,SAAS,aAAa,UAAU,sBAAsB;AACtD,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAC5C,SAAS,wBAAwB;AAG1B,MAAM,qBAAqB,CAAC,UAAU;AAEtC,SAAS,YAAY,QAAsC;AAChE,SAAO,SAAS;AAAA,IACd,UAAU,CAAC,GAAG,oBAAoB,MAAM;AAAA,IACxC,SAAS,MAAM,iBAAiB,oBAAoB,MAAM;AAAA,EAC5D,CAAC;AACH;AAEO,SAAS,uBAAuB;AACrC,QAAM,cAAc,eAAe;AAEnC,SAAO,YAAY;AAAA,IACjB,YAAY,CAAC,cAAsB,iBAAiB,yBAAyB,SAAS;AAAA,IACtF,WAAW,MAAM;AACf,kBAAY,kBAAkB,EAAE,UAAU,mBAAmB,CAAC;AAAA,IAChE;AAAA,EACF,CAAC;AACH;AAEO,SAAS,2BAA2B;AACzC,QAAM,cAAc,eAAe;AAEnC,SAAO,YAAY;AAAA,IACjB,YAAY,MAAM,iBAAiB,2BAA2B;AAAA,IAC9D,WAAW,MAAM;AACf,kBAAY,kBAAkB,EAAE,UAAU,mBAAmB,CAAC;AAAA,IAChE;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const MessageSchema = z.object({
|
|
3
|
+
id: z.string(),
|
|
4
|
+
deleted: z.number(),
|
|
5
|
+
datetime_alt: z.string().nullable(),
|
|
6
|
+
datetime_del: z.string().nullable(),
|
|
7
|
+
datetime_add: z.string(),
|
|
8
|
+
id_wl: z.number(),
|
|
9
|
+
id_account: z.number(),
|
|
10
|
+
id_user: z.number(),
|
|
11
|
+
status: z.number().nullable(),
|
|
12
|
+
type: z.string(),
|
|
13
|
+
subject: z.string(),
|
|
14
|
+
message: z.string(),
|
|
15
|
+
to: z.string(),
|
|
16
|
+
url: z.string()
|
|
17
|
+
});
|
|
18
|
+
export {
|
|
19
|
+
MessageSchema
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=messages.schema.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/users/schema/messages.schema.ts"],"sourcesContent":["import { z } from 'zod';\r\n\r\nexport const MessageSchema = z.object({\r\n id: z.string(),\r\n deleted: z.number(),\r\n datetime_alt: z.string().nullable(),\r\n datetime_del: z.string().nullable(),\r\n datetime_add: z.string(),\r\n id_wl: z.number(),\r\n id_account: z.number(),\r\n id_user: z.number(),\r\n status: z.number().nullable(),\r\n type: z.string(),\r\n subject: z.string(),\r\n message: z.string(),\r\n to: z.string(),\r\n url: z.string(),\r\n});\r\n\r\nexport type Message = z.infer<typeof MessageSchema>;\r\n\r\nexport interface FindMessagesParams {\r\n id_user: number;\r\n type: string;\r\n limit: number;\r\n page: number;\r\n status: number;\r\n order: 'asc' | 'desc';\r\n}\r\n\r\nexport interface ListMessagesApiResponse {\r\n status: 0 | 1;\r\n total: number;\r\n data: Message[];\r\n totalUnread: number;\r\n message?: string;\r\n}\r\n\r\nexport interface MarkAsReadApiResponse {\r\n status: 0 | 1;\r\n message?: string;\r\n}\r\n\r\nexport interface MarkAllAsReadApiResponse {\r\n status: 0 | 1;\r\n message?: string;\r\n}\r\n"],"mappings":"AAAA,SAAS,SAAS;AAEX,MAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,IAAI,EAAE,OAAO;AAAA,EACb,SAAS,EAAE,OAAO;AAAA,EAClB,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,cAAc,EAAE,OAAO;AAAA,EACvB,OAAO,EAAE,OAAO;AAAA,EAChB,YAAY,EAAE,OAAO;AAAA,EACrB,SAAS,EAAE,OAAO;AAAA,EAClB,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,MAAM,EAAE,OAAO;AAAA,EACf,SAAS,EAAE,OAAO;AAAA,EAClB,SAAS,EAAE,OAAO;AAAA,EAClB,IAAI,EAAE,OAAO;AAAA,EACb,KAAK,EAAE,OAAO;AAChB,CAAC;","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { api } from "../../../infra/api/client";
|
|
3
|
+
import { buildQueryParams } from "../../../infra/utils/params";
|
|
4
|
+
import { getUserContext } from "../../auth/utils/get-user-context";
|
|
5
|
+
class MessagesService {
|
|
6
|
+
async listMessages(params) {
|
|
7
|
+
const { id_account, id_user } = await getUserContext();
|
|
8
|
+
const query = buildQueryParams({
|
|
9
|
+
id_account,
|
|
10
|
+
id_user,
|
|
11
|
+
type: "notification",
|
|
12
|
+
limit: 20,
|
|
13
|
+
page: 1,
|
|
14
|
+
order: "desc",
|
|
15
|
+
...params
|
|
16
|
+
});
|
|
17
|
+
const url = `/accounts/${id_account}/messages${query ? `?${query}` : ""}`;
|
|
18
|
+
return api.apps.get(url);
|
|
19
|
+
}
|
|
20
|
+
async markAsRead(messageId) {
|
|
21
|
+
const { id_account } = await getUserContext();
|
|
22
|
+
const url = `/accounts/${id_account}/messages/${messageId}/action/markAsRead`;
|
|
23
|
+
return api.apps.put(url);
|
|
24
|
+
}
|
|
25
|
+
async markAllAsRead() {
|
|
26
|
+
const { id_account, id_user } = await getUserContext();
|
|
27
|
+
const url = `/accounts/${id_account}/messages/action/markAllAsRead`;
|
|
28
|
+
return api.apps.put(url, { id_user });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const messagesService = new MessagesService();
|
|
32
|
+
export {
|
|
33
|
+
messagesService
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=messages.service.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/users/services/messages.service.ts"],"sourcesContent":["import 'server-only';\n\nimport { api } from '../../../infra/api/client';\nimport { buildQueryParams } from '../../../infra/utils/params';\nimport { getUserContext } from '../../auth/utils/get-user-context';\nimport {\n FindMessagesParams,\n ListMessagesApiResponse,\n MarkAsReadApiResponse,\n MarkAllAsReadApiResponse,\n} from '../schema/messages.schema';\n\nclass MessagesService {\n async listMessages(\n params?: Partial<FindMessagesParams>\n ): Promise<ListMessagesApiResponse> {\n const { id_account, id_user } = await getUserContext();\n\n const query = buildQueryParams({\n id_account,\n id_user,\n type: 'notification',\n limit: 20,\n page: 1,\n order: 'desc',\n ...params,\n });\n\n const url = `/accounts/${id_account}/messages${query ? `?${query}` : ''}`;\n return api.apps.get<ListMessagesApiResponse>(url);\n }\n\n async markAsRead(messageId: string): Promise<MarkAsReadApiResponse> {\n const { id_account } = await getUserContext();\n\n const url = `/accounts/${id_account}/messages/${messageId}/action/markAsRead`;\n return api.apps.put<MarkAsReadApiResponse>(url);\n }\n\n async markAllAsRead(): Promise<MarkAllAsReadApiResponse> {\n const { id_account, id_user } = await getUserContext();\n\n const url = `/accounts/${id_account}/messages/action/markAllAsRead`;\n return api.apps.put<MarkAllAsReadApiResponse>(url, { id_user });\n }\n}\n\nexport const messagesService = new MessagesService();\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,WAAW;AACpB,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAQ/B,MAAM,gBAAgB;AAAA,EACpB,MAAM,aACJ,QACkC;AAClC,UAAM,EAAE,YAAY,QAAQ,IAAI,MAAM,eAAe;AAErD,UAAM,QAAQ,iBAAiB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO;AAAA,MACP,GAAG;AAAA,IACL,CAAC;AAED,UAAM,MAAM,aAAa,UAAU,YAAY,QAAQ,IAAI,KAAK,KAAK,EAAE;AACvE,WAAO,IAAI,KAAK,IAA6B,GAAG;AAAA,EAClD;AAAA,EAEA,MAAM,WAAW,WAAmD;AAClE,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,MAAM,aAAa,UAAU,aAAa,SAAS;AACzD,WAAO,IAAI,KAAK,IAA2B,GAAG;AAAA,EAChD;AAAA,EAEA,MAAM,gBAAmD;AACvD,UAAM,EAAE,YAAY,QAAQ,IAAI,MAAM,eAAe;AAErD,UAAM,MAAM,aAAa,UAAU;AACnC,WAAO,IAAI,KAAK,IAA8B,KAAK,EAAE,QAAQ,CAAC;AAAA,EAChE;AACF;AAEO,MAAM,kBAAkB,IAAI,gBAAgB;","names":[]}
|
|
@@ -39,15 +39,13 @@ function AuthProvider({ children }) {
|
|
|
39
39
|
const login = useCallback(
|
|
40
40
|
async (credentials) => {
|
|
41
41
|
const data = await loginMutate(credentials);
|
|
42
|
-
if (data.
|
|
43
|
-
return {
|
|
44
|
-
if (data.twoFactorRequired) {
|
|
45
|
-
return { status: "two_factor_required", cookie: data.cookie };
|
|
42
|
+
if (data.result === "two_factor_required") {
|
|
43
|
+
return { result: "two_factor_required", cookie: data.cookie };
|
|
46
44
|
}
|
|
47
45
|
invalidateUser();
|
|
48
46
|
invalidateAccount();
|
|
49
47
|
setUserData(data.user);
|
|
50
|
-
return {
|
|
48
|
+
return { result: "success" };
|
|
51
49
|
},
|
|
52
50
|
[setUserData, invalidateUser, invalidateAccount]
|
|
53
51
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/providers/auth.provider.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport {\r\n createContext,\r\n ReactNode,\r\n useCallback,\r\n useContext,\r\n useMemo,\r\n} from \"react\";\r\nimport {\r\n useUserQuery,\r\n useSetUserData,\r\n useInvalidateUser,\r\n useUserValidateSession,\r\n} from \"../modules/auth/hooks/useUserQuery\";\r\nimport { useLogin } from \"../modules/auth/hooks/login.hook\";\r\nimport { useRegister } from \"../modules/auth/hooks/register.hook\";\r\nimport { useLogout } from \"../modules/auth/hooks/logout.hook\";\r\nimport {\r\n AuthState,\r\n LoginRequest,\r\n RegisterRequest,\r\n} from \"../modules/auth/schema\";\r\n\r\nexport type LoginResult =\r\n | {
|
|
1
|
+
{"version":3,"sources":["../../src/providers/auth.provider.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport {\r\n createContext,\r\n ReactNode,\r\n useCallback,\r\n useContext,\r\n useMemo,\r\n} from \"react\";\r\nimport {\r\n useUserQuery,\r\n useSetUserData,\r\n useInvalidateUser,\r\n useUserValidateSession,\r\n} from \"../modules/auth/hooks/useUserQuery\";\r\nimport { useLogin } from \"../modules/auth/hooks/login.hook\";\r\nimport { useRegister } from \"../modules/auth/hooks/register.hook\";\r\nimport { useLogout } from \"../modules/auth/hooks/logout.hook\";\r\nimport {\r\n AuthState,\r\n LoginRequest,\r\n RegisterRequest,\r\n} from \"../modules/auth/schema\";\r\n\r\nexport type LoginResult =\r\n | { result: \"success\" }\r\n | { result: \"two_factor_required\"; cookie: string }\r\n | { result: \"error\"; message: string };\r\nimport {\r\n useCurrentAccount,\r\n useInvalidateAccount,\r\n} from \"../modules/accounts/hooks/current-account.hook\";\r\nimport { useRouter } from \"next/navigation\";\r\n\r\ninterface AuthContextProps extends AuthState {\r\n login: (credentials: LoginRequest) => Promise<LoginResult>;\r\n register: (data: RegisterRequest) => Promise<void>;\r\n logout: () => Promise<void>;\r\n}\r\n\r\nexport const AuthContext = createContext<AuthContextProps | undefined>(\r\n undefined\r\n);\r\n\r\ninterface AuthProviderProps {\r\n children: ReactNode;\r\n}\r\n\r\nexport function AuthProvider({ children }: AuthProviderProps) {\r\n const { data: user, isLoading: isQueryLoading } = useUserQuery();\r\n const { data: account, isLoading: isAccountLoading } = useCurrentAccount();\r\n const { isLoading: isSessionLoading } = useUserValidateSession();\r\n\r\n const setUserData = useSetUserData();\r\n const invalidateUser = useInvalidateUser();\r\n const invalidateAccount = useInvalidateAccount();\r\n\r\n const router = useRouter();\r\n\r\n const { mutateAsync: loginMutate, isPending: isLogging } = useLogin();\r\n const { mutateAsync: registerMutate, isPending: isRegistering } =\r\n useRegister();\r\n const { mutateAsync: logoutMutate, isPending: isLoggingOut } = useLogout();\r\n\r\n const isAuthenticated = !!user;\r\n const isLoading =\r\n isLogging ||\r\n isRegistering ||\r\n isLoggingOut ||\r\n isQueryLoading ||\r\n isAccountLoading ||\r\n isSessionLoading;\r\n\r\n const login = useCallback(\r\n async (credentials: LoginRequest): Promise<LoginResult> => {\r\n const data = await loginMutate(credentials);\r\n\r\n if (data.result === \"two_factor_required\") {\r\n return { result: \"two_factor_required\", cookie: data.cookie };\r\n }\r\n\r\n invalidateUser();\r\n invalidateAccount();\r\n\r\n setUserData(data.user);\r\n\r\n return { result: \"success\" };\r\n },\r\n [setUserData, invalidateUser, invalidateAccount]\r\n );\r\n\r\n const register = useCallback(\r\n async (data: RegisterRequest): Promise<void> => {\r\n await registerMutate(data, {\r\n onSuccess: (data) => {\r\n invalidateUser();\r\n invalidateAccount();\r\n setUserData(data.user);\r\n },\r\n });\r\n },\r\n [setUserData, invalidateUser, invalidateAccount]\r\n );\r\n\r\n const logout = useCallback(async (): Promise<void> => {\r\n await logoutMutate(undefined, {\r\n onSuccess: () => {\r\n invalidateUser();\r\n router.push(\"/login\");\r\n },\r\n });\r\n }, [setUserData]);\r\n\r\n const value: AuthContextProps = useMemo(\r\n () => ({\r\n user: user ?? null,\r\n account: account ?? null,\r\n isAuthenticated,\r\n isLoading,\r\n login,\r\n register,\r\n logout,\r\n }),\r\n [user, isAuthenticated, isLoading, login, register, logout]\r\n );\r\n\r\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\r\n}\r\n\r\nexport function useAuth(): AuthContextProps {\r\n const context = useContext(AuthContext);\r\n\r\n if (context === undefined) {\r\n throw new Error(\"useAuth deve ser usado dentro de um AuthProvider\");\r\n }\r\n\r\n return context;\r\n}\r\n"],"mappings":";AA8HS;AA5HT;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAW1B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAQnB,MAAM,cAAc;AAAA,EACzB;AACF;AAMO,SAAS,aAAa,EAAE,SAAS,GAAsB;AAC5D,QAAM,EAAE,MAAM,MAAM,WAAW,eAAe,IAAI,aAAa;AAC/D,QAAM,EAAE,MAAM,SAAS,WAAW,iBAAiB,IAAI,kBAAkB;AACzE,QAAM,EAAE,WAAW,iBAAiB,IAAI,uBAAuB;AAE/D,QAAM,cAAc,eAAe;AACnC,QAAM,iBAAiB,kBAAkB;AACzC,QAAM,oBAAoB,qBAAqB;AAE/C,QAAM,SAAS,UAAU;AAEzB,QAAM,EAAE,aAAa,aAAa,WAAW,UAAU,IAAI,SAAS;AACpE,QAAM,EAAE,aAAa,gBAAgB,WAAW,cAAc,IAC5D,YAAY;AACd,QAAM,EAAE,aAAa,cAAc,WAAW,aAAa,IAAI,UAAU;AAEzE,QAAM,kBAAkB,CAAC,CAAC;AAC1B,QAAM,YACJ,aACA,iBACA,gBACA,kBACA,oBACA;AAEF,QAAM,QAAQ;AAAA,IACZ,OAAO,gBAAoD;AACzD,YAAM,OAAO,MAAM,YAAY,WAAW;AAE1C,UAAI,KAAK,WAAW,uBAAuB;AACzC,eAAO,EAAE,QAAQ,uBAAuB,QAAQ,KAAK,OAAO;AAAA,MAC9D;AAEA,qBAAe;AACf,wBAAkB;AAElB,kBAAY,KAAK,IAAI;AAErB,aAAO,EAAE,QAAQ,UAAU;AAAA,IAC7B;AAAA,IACA,CAAC,aAAa,gBAAgB,iBAAiB;AAAA,EACjD;AAEA,QAAM,WAAW;AAAA,IACf,OAAO,SAAyC;AAC9C,YAAM,eAAe,MAAM;AAAA,QACzB,WAAW,CAACA,UAAS;AACnB,yBAAe;AACf,4BAAkB;AAClB,sBAAYA,MAAK,IAAI;AAAA,QACvB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,aAAa,gBAAgB,iBAAiB;AAAA,EACjD;AAEA,QAAM,SAAS,YAAY,YAA2B;AACpD,UAAM,aAAa,QAAW;AAAA,MAC5B,WAAW,MAAM;AACf,uBAAe;AACf,eAAO,KAAK,QAAQ;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,QAA0B;AAAA,IAC9B,OAAO;AAAA,MACL,MAAM,QAAQ;AAAA,MACd,SAAS,WAAW;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,MAAM,iBAAiB,WAAW,OAAO,UAAU,MAAM;AAAA,EAC5D;AAEA,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,UAA4B;AAC1C,QAAM,UAAU,WAAW,WAAW;AAEtC,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AAEA,SAAO;AACT;","names":["data"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class ServerActionError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(message, status) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ServerActionError";
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function safeServerAction(action, ...args) {
|
|
10
|
+
const result = await action(...args);
|
|
11
|
+
if (result.status === 0 || result.result === "error" || result.error) {
|
|
12
|
+
const msg = result.message || result.error || "Erro na a\xE7\xE3o";
|
|
13
|
+
throw new ServerActionError(msg, result.status || 0);
|
|
14
|
+
}
|
|
15
|
+
const { error, ...successData } = result;
|
|
16
|
+
return successData;
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
ServerActionError,
|
|
20
|
+
safeServerAction
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=safeServerAction.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safeServerAction.ts"],"sourcesContent":["export class ServerActionError extends Error {\n status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.name = 'ServerActionError';\n this.status = status;\n }\n}\n\nexport async function safeServerAction<\n TArgs extends any[],\n TResult extends Record<string, any>,\n>(\n action: (...args: TArgs) => Promise<TResult>,\n ...args: TArgs\n): Promise<Omit<TResult, 'error'>> {\n const result: TResult = await action(...args);\n\n if (result.status === 0 || result.result === 'error' || result.error) {\n const msg = result.message || result.error || 'Erro na ação';\n throw new ServerActionError(msg, result.status || 0);\n }\n\n const { error, ...successData } = result;\n return successData as Omit<TResult, 'error'>;\n}\n"],"mappings":"AAAO,MAAM,0BAA0B,MAAM;AAAA,EAC3C;AAAA,EAEA,YAAY,SAAiB,QAAgB;AAC3C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,eAAsB,iBAIpB,WACG,MAC8B;AACjC,QAAM,SAAkB,MAAM,OAAO,GAAG,IAAI;AAE5C,MAAI,OAAO,WAAW,KAAK,OAAO,WAAW,WAAW,OAAO,OAAO;AACpE,UAAM,MAAM,OAAO,WAAW,OAAO,SAAS;AAC9C,UAAM,IAAI,kBAAkB,KAAK,OAAO,UAAU,CAAC;AAAA,EACrD;AAEA,QAAM,EAAE,OAAO,GAAG,YAAY,IAAI;AAClC,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -8,7 +8,6 @@ import { NotificationsPopover } from './NotificationsPopover';
|
|
|
8
8
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/overlay/Tooltip';
|
|
9
9
|
import { useAccountModals } from '../../store/useAccountModals';
|
|
10
10
|
import type { ProfileMenuItem } from './ProfilePopover';
|
|
11
|
-
import type { NotificationData } from './NotificationsPopover';
|
|
12
11
|
|
|
13
12
|
export interface AppNavBarProps {
|
|
14
13
|
onLogoClick?: () => void;
|
|
@@ -21,7 +20,6 @@ export interface AppNavBarProps {
|
|
|
21
20
|
onExpandClick?: () => void;
|
|
22
21
|
menuItems?: ProfileMenuItem[];
|
|
23
22
|
onCreditsClick?: () => void;
|
|
24
|
-
notifications?: NotificationData[];
|
|
25
23
|
onNotificationsViewAll?: () => void;
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -35,7 +33,6 @@ export function AppNavBar({
|
|
|
35
33
|
onExpandClick,
|
|
36
34
|
menuItems = [],
|
|
37
35
|
onCreditsClick,
|
|
38
|
-
notifications,
|
|
39
36
|
onNotificationsViewAll,
|
|
40
37
|
}: AppNavBarProps) {
|
|
41
38
|
const router = useRouter();
|
|
@@ -73,7 +70,6 @@ export function AppNavBar({
|
|
|
73
70
|
}
|
|
74
71
|
>
|
|
75
72
|
<NotificationsPopover
|
|
76
|
-
notifications={notifications}
|
|
77
73
|
onViewAll={onNotificationsViewAll}
|
|
78
74
|
/>
|
|
79
75
|
<ProfilePopover
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "react";
|
|
4
4
|
import { Bell } from "lucide-react";
|
|
5
5
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
6
6
|
import { Popover } from "../ui/overlay/Popover";
|
|
7
7
|
import { Button } from "../ui/buttons/Button";
|
|
8
|
-
import
|
|
8
|
+
import NotificationCard from "../widgets/notifications/NotificationCard";
|
|
9
9
|
import { cn } from "../../infra/utils/clsx";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
iconBgColor: string;
|
|
14
|
-
title: string;
|
|
15
|
-
time: string;
|
|
16
|
-
}
|
|
10
|
+
import { useMessages, useMarkAllMessagesAsRead } from "../../modules/users/hooks/messages.hook";
|
|
11
|
+
import { formatDistanceToNow } from "date-fns";
|
|
12
|
+
import { ptBR } from "date-fns/locale";
|
|
17
13
|
|
|
18
14
|
export interface NotificationsPopoverProps {
|
|
19
|
-
notifications?: NotificationData[];
|
|
20
15
|
onViewAll?: () => void;
|
|
21
16
|
side?: "top" | "bottom" | "left" | "right";
|
|
22
17
|
align?: "start" | "center" | "end";
|
|
@@ -25,7 +20,6 @@ export interface NotificationsPopoverProps {
|
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
function NotificationsPopover({
|
|
28
|
-
notifications = [],
|
|
29
23
|
onViewAll,
|
|
30
24
|
side,
|
|
31
25
|
align,
|
|
@@ -33,11 +27,17 @@ function NotificationsPopover({
|
|
|
33
27
|
badgeClassName = "bg-pages-400",
|
|
34
28
|
}: NotificationsPopoverProps) {
|
|
35
29
|
const [open, setOpen] = useState(false);
|
|
36
|
-
const
|
|
30
|
+
const { data: messagesData } = useMessages({ limit: 5 });
|
|
31
|
+
const { mutate: markAllAsRead } = useMarkAllMessagesAsRead();
|
|
32
|
+
|
|
33
|
+
const messages = messagesData?.data ?? [];
|
|
34
|
+
const totalUnread = messagesData?.totalUnread ?? 0;
|
|
37
35
|
|
|
38
36
|
const handleOpenChange = (isOpen: boolean) => {
|
|
39
37
|
setOpen(isOpen);
|
|
40
|
-
if (isOpen)
|
|
38
|
+
if (isOpen && totalUnread > 0) {
|
|
39
|
+
markAllAsRead();
|
|
40
|
+
}
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
return (
|
|
@@ -47,7 +47,7 @@ function NotificationsPopover({
|
|
|
47
47
|
className={`relative rounded-full p-2.5 ${open ? "bg-gray-100" : "hover:bg-gray-100"}`}
|
|
48
48
|
>
|
|
49
49
|
<Bell size={20} className="text-gray-500" />
|
|
50
|
-
{
|
|
50
|
+
{totalUnread > 0 && (
|
|
51
51
|
<span
|
|
52
52
|
className={cn(
|
|
53
53
|
"absolute top-2 right-2 size-2.5 rounded-full",
|
|
@@ -60,7 +60,7 @@ function NotificationsPopover({
|
|
|
60
60
|
<PopoverPrimitive.Portal>
|
|
61
61
|
<PopoverPrimitive.Content
|
|
62
62
|
className={cn(
|
|
63
|
-
"absolute bottom-2 left-[26px] w-[308px] p-0 pt-4 bg-white rounded-2xl shadow-md border border-gray-200",
|
|
63
|
+
"absolute bottom-2 left-[26px] z-[9999] w-[308px] p-0 pt-4 bg-white rounded-2xl shadow-md border border-gray-200",
|
|
64
64
|
contentClassName,
|
|
65
65
|
)}
|
|
66
66
|
side={side}
|
|
@@ -71,15 +71,18 @@ function NotificationsPopover({
|
|
|
71
71
|
Últimas notificações
|
|
72
72
|
</span>
|
|
73
73
|
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
key={
|
|
78
|
-
icon={
|
|
79
|
-
iconBgColor=
|
|
80
|
-
title={
|
|
81
|
-
time={
|
|
82
|
-
|
|
74
|
+
{messages.length > 0 ? (
|
|
75
|
+
messages.map((message, index) => (
|
|
76
|
+
<NotificationCard
|
|
77
|
+
key={message.id}
|
|
78
|
+
icon={<Bell size={16} />}
|
|
79
|
+
iconBgColor="bg-gray-100"
|
|
80
|
+
title={message.subject}
|
|
81
|
+
time={formatDistanceToNow(new Date(message.datetime_add), {
|
|
82
|
+
addSuffix: true,
|
|
83
|
+
locale: ptBR,
|
|
84
|
+
})}
|
|
85
|
+
showBorder={index < messages.length - 1}
|
|
83
86
|
/>
|
|
84
87
|
))
|
|
85
88
|
) : (
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Bell, Check } from "lucide-react";
|
|
2
|
+
import { Button } from "../../ui/buttons/Button";
|
|
3
|
+
import {
|
|
4
|
+
Tabs,
|
|
5
|
+
TabsContent,
|
|
6
|
+
TabsList,
|
|
7
|
+
TabsTrigger,
|
|
8
|
+
} from "../../ui/data-display/Tabs";
|
|
9
|
+
import { useMessages } from "../../../modules/users/hooks/messages.hook";
|
|
10
|
+
import NotificationCard from "../../widgets/notifications/NotificationCard";
|
|
11
|
+
import { formatDistanceToNow } from "date-fns";
|
|
12
|
+
import { ptBR } from "date-fns/locale";
|
|
13
|
+
|
|
14
|
+
export function Notifications() {
|
|
15
|
+
const { data: unreadedNotifications } = useMessages({
|
|
16
|
+
page: 1,
|
|
17
|
+
limit: 10,
|
|
18
|
+
status: 1,
|
|
19
|
+
type: "notification",
|
|
20
|
+
order: "desc",
|
|
21
|
+
});
|
|
22
|
+
const { data: allNotifications } = useMessages({
|
|
23
|
+
page: 1,
|
|
24
|
+
limit: 10,
|
|
25
|
+
type: "notification",
|
|
26
|
+
order: "desc",
|
|
27
|
+
});
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-col gap-6 lg:gap-10 px-4 py-6 lg:px-20 lg:py-14">
|
|
30
|
+
<div className="flex items-center justify-between gap-4">
|
|
31
|
+
<div className="flex flex-col gap-2">
|
|
32
|
+
<h1 className="paragraph-xlarge-semibold lg:title-large-semibold text-gray-950">
|
|
33
|
+
Notificações
|
|
34
|
+
</h1>
|
|
35
|
+
<p className="paragraph-medium-regular text-gray-600 hidden lg:block">
|
|
36
|
+
Tenha controle total sobre páginas, experiências e conversões.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<Button variant={"secondary"} className="h-10! lg:h-12! shrink-0">
|
|
40
|
+
<Check size={20} />
|
|
41
|
+
<span className="lg:block hidden">Marcar todos como lidos</span>
|
|
42
|
+
<span className="lg:hidden">Marcar como lidos</span>
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
<Tabs
|
|
46
|
+
defaultValue="all"
|
|
47
|
+
className="flex flex-col w-full bg-transparent gap-6 lg:gap-8"
|
|
48
|
+
>
|
|
49
|
+
<TabsList className="h-auto pb-0 bg-transparent">
|
|
50
|
+
<TabsTrigger className="gap-2" value="all">
|
|
51
|
+
Todas as notificações{" "}
|
|
52
|
+
<span className="text-gray-500">
|
|
53
|
+
({allNotifications?.data.length})
|
|
54
|
+
</span>
|
|
55
|
+
</TabsTrigger>
|
|
56
|
+
<TabsTrigger className="gap-2" value="unreads">
|
|
57
|
+
Não lidas{" "}
|
|
58
|
+
<span className="text-gray-500">
|
|
59
|
+
({unreadedNotifications?.data.length})
|
|
60
|
+
</span>
|
|
61
|
+
</TabsTrigger>
|
|
62
|
+
</TabsList>
|
|
63
|
+
|
|
64
|
+
<TabsContent value="all">
|
|
65
|
+
<div className="flex flex-col border border-gray-200 rounded-lg w-full bg-white">
|
|
66
|
+
{allNotifications?.data.map((notification, index) => (
|
|
67
|
+
<NotificationCard
|
|
68
|
+
key={notification.id}
|
|
69
|
+
icon={<Bell size={20} className="text-pages-600" />}
|
|
70
|
+
iconBgColor="bg-pages-100"
|
|
71
|
+
title={notification.subject}
|
|
72
|
+
description={notification.message}
|
|
73
|
+
time={formatDistanceToNow(new Date(notification.datetime_add), {
|
|
74
|
+
addSuffix: true,
|
|
75
|
+
locale: ptBR,
|
|
76
|
+
})}
|
|
77
|
+
isUnread={notification.status === 1}
|
|
78
|
+
showBorder={index < allNotifications.data.length - 1}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</TabsContent>
|
|
83
|
+
|
|
84
|
+
<TabsContent value="unreads">
|
|
85
|
+
<div className="flex flex-col border border-gray-200 rounded-lg w-full bg-white">
|
|
86
|
+
{unreadedNotifications?.data.map((notification, index) => (
|
|
87
|
+
<NotificationCard
|
|
88
|
+
key={notification.id}
|
|
89
|
+
icon={<Bell size={20} className="text-pages-600" />}
|
|
90
|
+
iconBgColor="bg-pages-100"
|
|
91
|
+
title={notification.subject}
|
|
92
|
+
description={notification.message}
|
|
93
|
+
time={formatDistanceToNow(new Date(notification.datetime_add), {
|
|
94
|
+
addSuffix: true,
|
|
95
|
+
locale: ptBR,
|
|
96
|
+
})}
|
|
97
|
+
isUnread={notification.status === 1}
|
|
98
|
+
showBorder={index < unreadedNotifications.data.length - 1}
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</TabsContent>
|
|
103
|
+
</Tabs>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Clock } from 'lucide-react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { cn } from '../../../infra/utils/clsx';
|
|
4
|
+
|
|
5
|
+
type NotificationCardProps = {
|
|
6
|
+
icon: ReactNode;
|
|
7
|
+
iconBgColor?: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
time: string;
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
isUnread?: boolean;
|
|
13
|
+
showBorder?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default function NotificationCard({
|
|
18
|
+
icon,
|
|
19
|
+
iconBgColor,
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
time,
|
|
23
|
+
actions,
|
|
24
|
+
isUnread = false,
|
|
25
|
+
showBorder = true,
|
|
26
|
+
className,
|
|
27
|
+
}: NotificationCardProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'flex flex-col lg:flex-row items-start p-4 lg:p-6 gap-4 lg:gap-5 relative',
|
|
32
|
+
showBorder && 'border-b border-gray-200',
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
className={cn(
|
|
38
|
+
'flex items-center justify-center size-10 rounded-full shrink-0',
|
|
39
|
+
iconBgColor
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{icon}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="flex flex-col gap-4 flex-1">
|
|
46
|
+
<div className="flex flex-col gap-1">
|
|
47
|
+
<span className="paragraph-small-semibold text-gray-950">{title}</span>
|
|
48
|
+
{description && (
|
|
49
|
+
<span className="paragraph-small-regular text-gray-600">{description}</span>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="flex items-center gap-1.5">
|
|
54
|
+
<Clock size={14} className="text-gray-400" />
|
|
55
|
+
<span className="paragraph-xsmall-medium text-gray-600">{time}</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{isUnread && <div className="size-2 bg-pages-400 rounded-full shrink-0 mt-1" />}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type { NotificationCardProps };
|