@carlonicora/nextjs-jsonapi 1.0.3 → 1.0.4
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/package.json +2 -1
- package/src/atoms/index.ts +1 -0
- package/src/atoms/recentPagesAtom.ts +10 -0
- package/src/client/context/JsonApiContext.ts +61 -0
- package/src/client/context/JsonApiProvider.tsx +27 -0
- package/src/client/context/index.ts +2 -0
- package/src/client/hooks/index.ts +3 -0
- package/src/client/hooks/useJsonApiGet.ts +188 -0
- package/src/client/hooks/useJsonApiMutation.ts +193 -0
- package/src/client/hooks/useRehydration.ts +47 -0
- package/src/client/index.ts +11 -0
- package/src/client/request.ts +97 -0
- package/src/client/token.ts +10 -0
- package/src/components/containers/PageContainer.tsx +15 -0
- package/src/components/containers/ReactMarkdownContainer.tsx +119 -0
- package/src/components/containers/TabsContainer.tsx +93 -0
- package/src/components/containers/index.ts +3 -0
- package/src/components/contents/AttributeElement.tsx +20 -0
- package/src/components/contents/index.ts +1 -0
- package/src/components/details/AllowedUsersDetails.tsx +23 -0
- package/src/components/details/index.ts +1 -0
- package/src/components/editors/BlockNoteDiffInlineContent.tsx +152 -0
- package/src/components/editors/BlockNoteEditor.tsx +404 -0
- package/src/components/editors/BlockNoteEditorContainer.tsx +13 -0
- package/src/components/editors/BlockNoteEditorFormattingToolbar.tsx +38 -0
- package/src/components/editors/index.ts +1 -0
- package/src/components/errors/ErrorDetails.tsx +41 -0
- package/src/components/errors/errorToast.ts +9 -0
- package/src/components/errors/index.ts +2 -0
- package/src/components/forms/CommonAssociationForm.tsx +162 -0
- package/src/components/forms/CommonDeleter.tsx +94 -0
- package/src/components/forms/CommonEditorButtons.tsx +30 -0
- package/src/components/forms/CommonEditorHeader.tsx +35 -0
- package/src/components/forms/CommonEditorTrigger.tsx +26 -0
- package/src/components/forms/DatePickerPopover.tsx +219 -0
- package/src/components/forms/DateRangeSelector.tsx +110 -0
- package/src/components/forms/FileUploader.tsx +324 -0
- package/src/components/forms/FormCheckbox.tsx +66 -0
- package/src/components/forms/FormContainerGeneric.tsx +39 -0
- package/src/components/forms/FormDate.tsx +247 -0
- package/src/components/forms/FormDateTime.tsx +231 -0
- package/src/components/forms/FormInput.tsx +110 -0
- package/src/components/forms/FormPassword.tsx +54 -0
- package/src/components/forms/FormPlaceAutocomplete.tsx +286 -0
- package/src/components/forms/FormSelect.tsx +72 -0
- package/src/components/forms/FormSlider.tsx +51 -0
- package/src/components/forms/FormSwitch.tsx +25 -0
- package/src/components/forms/FormTextarea.tsx +44 -0
- package/src/components/forms/MultiFileUploader.tsx +107 -0
- package/src/components/forms/PasswordInput.tsx +47 -0
- package/src/components/forms/index.ts +21 -0
- package/src/components/index.ts +11 -0
- package/src/components/navigations/Breadcrumb.tsx +83 -0
- package/src/components/navigations/ContentTitle.tsx +39 -0
- package/src/components/navigations/Header.tsx +27 -0
- package/src/components/navigations/ModeToggleSwitch.tsx +25 -0
- package/src/components/navigations/PageSection.tsx +64 -0
- package/src/components/navigations/RecentPagesNavigator.tsx +52 -0
- package/src/components/navigations/index.ts +6 -0
- package/src/components/pages/PageContainerContentDetails.tsx +76 -0
- package/src/components/pages/PageContentContainer.tsx +31 -0
- package/src/components/pages/index.ts +2 -0
- package/src/components/tables/ContentListTable.tsx +165 -0
- package/src/components/tables/ContentTableSearch.tsx +105 -0
- package/src/components/tables/cells/cell.component.tsx +18 -0
- package/src/components/tables/cells/cell.date.tsx +16 -0
- package/src/components/tables/cells/cell.id.tsx +27 -0
- package/src/components/tables/cells/cell.link.tsx +18 -0
- package/src/components/tables/cells/cell.text.tsx +12 -0
- package/src/components/tables/cells/cell.url.tsx +13 -0
- package/src/components/tables/cells/index.ts +5 -0
- package/src/components/tables/index.ts +3 -0
- package/src/contexts/SharedContext.tsx +35 -0
- package/src/contexts/index.ts +2 -0
- package/src/core/abstracts/AbstractApiData.ts +138 -0
- package/src/core/abstracts/AbstractService.ts +263 -0
- package/src/core/abstracts/index.ts +2 -0
- package/src/core/endpoint/EndpointCreator.ts +97 -0
- package/src/core/endpoint/index.ts +1 -0
- package/src/core/factories/JsonApiDataFactory.ts +12 -0
- package/src/core/factories/RehydrationFactory.ts +30 -0
- package/src/core/factories/index.ts +2 -0
- package/src/core/fields/FieldSelector.ts +15 -0
- package/src/core/fields/index.ts +1 -0
- package/src/core/index.ts +20 -0
- package/src/core/interfaces/ApiData.ts +8 -0
- package/src/core/interfaces/ApiDataInterface.ts +15 -0
- package/src/core/interfaces/ApiRequestDataTypeInterface.ts +14 -0
- package/src/core/interfaces/ApiResponseInterface.ts +17 -0
- package/src/core/interfaces/JsonApiHydratedDataInterface.ts +5 -0
- package/src/core/interfaces/index.ts +5 -0
- package/src/core/registry/DataClassRegistry.ts +51 -0
- package/src/core/registry/ModuleRegistrar.ts +43 -0
- package/src/core/registry/ModuleRegistry.ts +64 -0
- package/src/core/registry/index.ts +3 -0
- package/src/core/utils/index.ts +2 -0
- package/src/core/utils/rehydrate.ts +24 -0
- package/src/core/utils/translateResponse.ts +125 -0
- package/src/features/auth/auth.module.ts +9 -0
- package/src/features/auth/config.ts +57 -0
- package/src/features/auth/data/auth.interface.ts +31 -0
- package/src/features/auth/data/auth.service.ts +159 -0
- package/src/features/auth/data/auth.ts +54 -0
- package/src/features/auth/data/index.ts +3 -0
- package/src/features/auth/index.ts +3 -0
- package/src/features/company/company.module.ts +10 -0
- package/src/features/company/data/company.fields.ts +6 -0
- package/src/features/company/data/company.interface.ts +28 -0
- package/src/features/company/data/company.service.ts +73 -0
- package/src/features/company/data/company.ts +104 -0
- package/src/features/company/data/index.ts +4 -0
- package/src/features/company/index.ts +2 -0
- package/src/features/content/content.module.ts +20 -0
- package/src/features/content/data/content.fields.ts +13 -0
- package/src/features/content/data/content.interface.ts +23 -0
- package/src/features/content/data/content.service.ts +75 -0
- package/src/features/content/data/content.ts +85 -0
- package/src/features/content/data/index.ts +4 -0
- package/src/features/content/index.ts +2 -0
- package/src/features/feature/components/forms/FormFeatures.tsx +149 -0
- package/src/features/feature/components/index.ts +1 -0
- package/src/features/feature/data/feature.interface.ts +9 -0
- package/src/features/feature/data/feature.service.ts +19 -0
- package/src/features/feature/data/feature.ts +33 -0
- package/src/features/feature/data/index.ts +3 -0
- package/src/features/feature/feature.module.ts +10 -0
- package/src/features/feature/index.ts +3 -0
- package/src/features/index.ts +12 -0
- package/src/features/module/data/index.ts +2 -0
- package/src/features/module/data/module.interface.ts +12 -0
- package/src/features/module/data/module.ts +42 -0
- package/src/features/module/index.ts +2 -0
- package/src/features/module/module.module.ts +10 -0
- package/src/features/notification/data/index.ts +4 -0
- package/src/features/notification/data/notification.fields.ts +8 -0
- package/src/features/notification/data/notification.interface.ts +14 -0
- package/src/features/notification/data/notification.service.ts +34 -0
- package/src/features/notification/data/notification.ts +51 -0
- package/src/features/notification/index.ts +2 -0
- package/src/features/notification/notification.module.ts +10 -0
- package/src/features/push/data/index.ts +3 -0
- package/src/features/push/data/push.interface.ts +8 -0
- package/src/features/push/data/push.service.ts +17 -0
- package/src/features/push/data/push.ts +18 -0
- package/src/features/push/index.ts +2 -0
- package/src/features/push/push.module.ts +10 -0
- package/src/features/role/data/index.ts +4 -0
- package/src/features/role/data/role.fields.ts +8 -0
- package/src/features/role/data/role.interface.ts +16 -0
- package/src/features/role/data/role.service.ts +117 -0
- package/src/features/role/data/role.ts +62 -0
- package/src/features/role/index.ts +2 -0
- package/src/features/role/role.module.ts +10 -0
- package/src/features/s3/data/index.ts +3 -0
- package/src/features/s3/data/s3.interface.ts +11 -0
- package/src/features/s3/data/s3.service.ts +30 -0
- package/src/features/s3/data/s3.ts +60 -0
- package/src/features/s3/index.ts +2 -0
- package/src/features/s3/s3.module.ts +10 -0
- package/src/features/search/index.ts +1 -0
- package/src/features/search/interfaces/index.ts +1 -0
- package/src/features/search/interfaces/search.result.interface.ts +3 -0
- package/src/features/user/author.module.ts +10 -0
- package/src/features/user/components/index.ts +2 -0
- package/src/features/user/components/lists/ContributorsList.tsx +41 -0
- package/src/features/user/components/lists/index.ts +1 -0
- package/src/features/user/components/widgets/UserAvatar.tsx +86 -0
- package/src/features/user/components/widgets/index.ts +1 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +156 -0
- package/src/features/user/contexts/index.ts +1 -0
- package/src/features/user/data/index.ts +4 -0
- package/src/features/user/data/user.fields.ts +8 -0
- package/src/features/user/data/user.interface.ts +41 -0
- package/src/features/user/data/user.service.ts +246 -0
- package/src/features/user/data/user.ts +162 -0
- package/src/features/user/index.ts +4 -0
- package/src/features/user/user.module.ts +21 -0
- package/src/hooks/TableGeneratorRegistry.ts +53 -0
- package/src/hooks/index.ts +33 -0
- package/src/hooks/types.ts +35 -0
- package/src/hooks/url.rewriter.ts +22 -0
- package/src/hooks/useCustomD3Graph.tsx +705 -0
- package/src/hooks/useDataListRetriever.ts +349 -0
- package/src/hooks/useDebounce.ts +33 -0
- package/src/hooks/usePageUrlGenerator.ts +50 -0
- package/src/hooks/useTableGenerator.ts +16 -0
- package/src/i18n/config.ts +73 -0
- package/src/i18n/index.ts +18 -0
- package/src/index.ts +16 -0
- package/src/interfaces/breadcrumb.item.data.interface.ts +4 -0
- package/src/interfaces/d3.link.interface.ts +7 -0
- package/src/interfaces/d3.node.interface.ts +12 -0
- package/src/interfaces/index.ts +3 -0
- package/src/permissions/check.ts +127 -0
- package/src/permissions/index.ts +2 -0
- package/src/permissions/types.ts +109 -0
- package/src/roles/config.ts +46 -0
- package/src/roles/index.ts +1 -0
- package/src/server/cache.ts +28 -0
- package/src/server/index.ts +3 -0
- package/src/server/request.ts +113 -0
- package/src/server/token.ts +10 -0
- package/src/shadcnui/custom/kanban.tsx +1001 -0
- package/src/shadcnui/custom/link.tsx +18 -0
- package/src/shadcnui/custom/multi-select.tsx +382 -0
- package/src/shadcnui/index.ts +49 -0
- package/src/shadcnui/ui/accordion.tsx +52 -0
- package/src/shadcnui/ui/alert-dialog.tsx +141 -0
- package/src/shadcnui/ui/alert.tsx +43 -0
- package/src/shadcnui/ui/avatar.tsx +50 -0
- package/src/shadcnui/ui/badge.tsx +40 -0
- package/src/shadcnui/ui/breadcrumb.tsx +115 -0
- package/src/shadcnui/ui/button.tsx +51 -0
- package/src/shadcnui/ui/calendar.tsx +73 -0
- package/src/shadcnui/ui/card.tsx +43 -0
- package/src/shadcnui/ui/carousel.tsx +225 -0
- package/src/shadcnui/ui/chart.tsx +320 -0
- package/src/shadcnui/ui/checkbox.tsx +29 -0
- package/src/shadcnui/ui/collapsible.tsx +11 -0
- package/src/shadcnui/ui/command.tsx +155 -0
- package/src/shadcnui/ui/context-menu.tsx +179 -0
- package/src/shadcnui/ui/dialog.tsx +96 -0
- package/src/shadcnui/ui/drawer.tsx +89 -0
- package/src/shadcnui/ui/dropdown-menu.tsx +205 -0
- package/src/shadcnui/ui/form.tsx +138 -0
- package/src/shadcnui/ui/hover-card.tsx +29 -0
- package/src/shadcnui/ui/input.tsx +21 -0
- package/src/shadcnui/ui/label.tsx +26 -0
- package/src/shadcnui/ui/navigation-menu.tsx +168 -0
- package/src/shadcnui/ui/popover.tsx +33 -0
- package/src/shadcnui/ui/progress.tsx +25 -0
- package/src/shadcnui/ui/radio-group.tsx +37 -0
- package/src/shadcnui/ui/resizable.tsx +47 -0
- package/src/shadcnui/ui/scroll-area.tsx +40 -0
- package/src/shadcnui/ui/select.tsx +164 -0
- package/src/shadcnui/ui/separator.tsx +28 -0
- package/src/shadcnui/ui/sheet.tsx +139 -0
- package/src/shadcnui/ui/sidebar.tsx +677 -0
- package/src/shadcnui/ui/skeleton.tsx +13 -0
- package/src/shadcnui/ui/slider.tsx +25 -0
- package/src/shadcnui/ui/sonner.tsx +25 -0
- package/src/shadcnui/ui/switch.tsx +31 -0
- package/src/shadcnui/ui/table.tsx +120 -0
- package/src/shadcnui/ui/tabs.tsx +55 -0
- package/src/shadcnui/ui/textarea.tsx +24 -0
- package/src/shadcnui/ui/toggle.tsx +39 -0
- package/src/shadcnui/ui/tooltip.tsx +61 -0
- package/src/unified/JsonApiRequest.ts +325 -0
- package/src/unified/index.ts +1 -0
- package/src/utils/blocknote-diff.util.ts +815 -0
- package/src/utils/blocknote-word-diff-renderer.util.ts +413 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/compose-refs.ts +61 -0
- package/src/utils/date-formatter.ts +53 -0
- package/src/utils/exists.ts +7 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/schemas/entity.object.schema.ts +8 -0
- package/src/utils/schemas/index.ts +2 -0
- package/src/utils/schemas/user.object.schema.ts +9 -0
- package/src/utils/table-options.ts +67 -0
- package/src/utils/use-mobile.tsx +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlonicora/nextjs-jsonapi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Next.js JSON:API client with server/client support and caching",
|
|
5
5
|
"author": "Carlo Nicora",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
},
|
|
92
92
|
"files": [
|
|
93
93
|
"dist",
|
|
94
|
+
"src",
|
|
94
95
|
"README.md"
|
|
95
96
|
],
|
|
96
97
|
"peerDependencies": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./recentPagesAtom";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
export type CacheProfile = "seconds" | "minutes" | "hours" | "days" | "weeks" | "max" | "default";
|
|
6
|
+
|
|
7
|
+
export interface JsonApiConfig {
|
|
8
|
+
/**
|
|
9
|
+
* The base URL for API requests (e.g., https://api.example.com)
|
|
10
|
+
*/
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom token getter function. If not provided, will use default cookie-based token retrieval.
|
|
15
|
+
*/
|
|
16
|
+
tokenGetter?: () => Promise<string | undefined>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Custom language getter function. If not provided, will use browser locale or next-intl.
|
|
20
|
+
*/
|
|
21
|
+
languageGetter?: () => Promise<string>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default headers to include in all requests
|
|
25
|
+
*/
|
|
26
|
+
defaultHeaders?: Record<string, string>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Global error handler for failed requests (client-side only)
|
|
30
|
+
*/
|
|
31
|
+
onError?: (status: number, message: string) => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cache configuration
|
|
35
|
+
*/
|
|
36
|
+
cacheConfig?: {
|
|
37
|
+
defaultProfile: CacheProfile;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Function to bootstrap the data class registry.
|
|
42
|
+
* Will be called automatically when needed.
|
|
43
|
+
*/
|
|
44
|
+
bootstrapper?: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const JsonApiContext = createContext<JsonApiConfig | null>(null);
|
|
48
|
+
|
|
49
|
+
export function useJsonApiConfig(): JsonApiConfig {
|
|
50
|
+
const config = useContext(JsonApiContext);
|
|
51
|
+
if (!config) {
|
|
52
|
+
throw new Error("useJsonApiConfig must be used within a JsonApiProvider");
|
|
53
|
+
}
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useJsonApiConfigOptional(): JsonApiConfig | null {
|
|
58
|
+
return useContext(JsonApiContext);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { JsonApiContext };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo } from "react";
|
|
4
|
+
import { JsonApiConfig, JsonApiContext } from "./JsonApiContext";
|
|
5
|
+
|
|
6
|
+
export interface JsonApiProviderProps {
|
|
7
|
+
config: JsonApiConfig;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function JsonApiProvider({ config, children }: JsonApiProviderProps) {
|
|
12
|
+
// Run bootstrapper on mount if provided
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (config.bootstrapper) {
|
|
15
|
+
config.bootstrapper();
|
|
16
|
+
}
|
|
17
|
+
}, [config.bootstrapper]);
|
|
18
|
+
|
|
19
|
+
// Memoize config to prevent unnecessary re-renders
|
|
20
|
+
const memoizedConfig = useMemo(() => config, [config]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<JsonApiContext.Provider value={memoizedConfig}>
|
|
24
|
+
{children}
|
|
25
|
+
</JsonApiContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
import { ApiDataInterface } from "../../core/interfaces/ApiDataInterface";
|
|
5
|
+
import { ApiRequestDataTypeInterface } from "../../core/interfaces/ApiRequestDataTypeInterface";
|
|
6
|
+
import { ApiResponseInterface } from "../../core/interfaces/ApiResponseInterface";
|
|
7
|
+
|
|
8
|
+
export interface UseJsonApiGetOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Whether to enable the query. If false, the query won't run.
|
|
11
|
+
*/
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Dependencies that trigger a refetch when changed.
|
|
15
|
+
*/
|
|
16
|
+
deps?: any[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseJsonApiGetResult<T> {
|
|
20
|
+
/**
|
|
21
|
+
* The fetched data, or null if not yet fetched.
|
|
22
|
+
*/
|
|
23
|
+
data: T | null;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the query is currently loading.
|
|
26
|
+
*/
|
|
27
|
+
loading: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Error message if the query failed.
|
|
30
|
+
*/
|
|
31
|
+
error: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The full API response (includes raw data, pagination, etc.)
|
|
34
|
+
*/
|
|
35
|
+
response: ApiResponseInterface | null;
|
|
36
|
+
/**
|
|
37
|
+
* Function to manually refetch the data.
|
|
38
|
+
*/
|
|
39
|
+
refetch: () => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Whether there is a next page available.
|
|
42
|
+
*/
|
|
43
|
+
hasNextPage: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether there is a previous page available.
|
|
46
|
+
*/
|
|
47
|
+
hasPreviousPage: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Function to fetch the next page.
|
|
50
|
+
*/
|
|
51
|
+
fetchNextPage: () => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Function to fetch the previous page.
|
|
54
|
+
*/
|
|
55
|
+
fetchPreviousPage: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Hook for fetching data from a JSON:API endpoint.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const { data, loading, error, refetch } = useJsonApiGet<Article>({
|
|
64
|
+
* classKey: Modules.Article,
|
|
65
|
+
* endpoint: `/articles/${id}`,
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* if (loading) return <Loading />;
|
|
69
|
+
* if (error) return <Error message={error} />;
|
|
70
|
+
* return <ArticleView article={data} />;
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function useJsonApiGet<T extends ApiDataInterface>(params: {
|
|
74
|
+
classKey: ApiRequestDataTypeInterface;
|
|
75
|
+
endpoint: string;
|
|
76
|
+
companyId?: string;
|
|
77
|
+
options?: UseJsonApiGetOptions;
|
|
78
|
+
}): UseJsonApiGetResult<T> {
|
|
79
|
+
const [data, setData] = useState<T | null>(null);
|
|
80
|
+
const [loading, setLoading] = useState(false);
|
|
81
|
+
const [error, setError] = useState<string | null>(null);
|
|
82
|
+
const [response, setResponse] = useState<ApiResponseInterface | null>(null);
|
|
83
|
+
const isMounted = useRef(true);
|
|
84
|
+
|
|
85
|
+
const fetchData = useCallback(async () => {
|
|
86
|
+
if (params.options?.enabled === false) return;
|
|
87
|
+
|
|
88
|
+
setLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const { JsonApiGet } = await import("../../unified/JsonApiRequest");
|
|
93
|
+
const language = navigator.language.split("-")[0] || "en";
|
|
94
|
+
|
|
95
|
+
const apiResponse = await JsonApiGet({
|
|
96
|
+
classKey: params.classKey,
|
|
97
|
+
endpoint: params.endpoint,
|
|
98
|
+
companyId: params.companyId,
|
|
99
|
+
language,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!isMounted.current) return;
|
|
103
|
+
|
|
104
|
+
setResponse(apiResponse);
|
|
105
|
+
|
|
106
|
+
if (apiResponse.ok) {
|
|
107
|
+
setData(apiResponse.data as T);
|
|
108
|
+
} else {
|
|
109
|
+
setError(apiResponse.error);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (!isMounted.current) return;
|
|
113
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
114
|
+
} finally {
|
|
115
|
+
if (isMounted.current) {
|
|
116
|
+
setLoading(false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [params.classKey, params.endpoint, params.companyId, params.options?.enabled]);
|
|
120
|
+
|
|
121
|
+
const fetchNextPage = useCallback(async () => {
|
|
122
|
+
if (!response?.nextPage) return;
|
|
123
|
+
|
|
124
|
+
setLoading(true);
|
|
125
|
+
try {
|
|
126
|
+
const nextResponse = await response.nextPage();
|
|
127
|
+
if (!isMounted.current) return;
|
|
128
|
+
|
|
129
|
+
setResponse(nextResponse);
|
|
130
|
+
if (nextResponse.ok) {
|
|
131
|
+
setData(nextResponse.data as T);
|
|
132
|
+
} else {
|
|
133
|
+
setError(nextResponse.error);
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (!isMounted.current) return;
|
|
137
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
138
|
+
} finally {
|
|
139
|
+
if (isMounted.current) {
|
|
140
|
+
setLoading(false);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}, [response]);
|
|
144
|
+
|
|
145
|
+
const fetchPreviousPage = useCallback(async () => {
|
|
146
|
+
if (!response?.prevPage) return;
|
|
147
|
+
|
|
148
|
+
setLoading(true);
|
|
149
|
+
try {
|
|
150
|
+
const prevResponse = await response.prevPage();
|
|
151
|
+
if (!isMounted.current) return;
|
|
152
|
+
|
|
153
|
+
setResponse(prevResponse);
|
|
154
|
+
if (prevResponse.ok) {
|
|
155
|
+
setData(prevResponse.data as T);
|
|
156
|
+
} else {
|
|
157
|
+
setError(prevResponse.error);
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (!isMounted.current) return;
|
|
161
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
162
|
+
} finally {
|
|
163
|
+
if (isMounted.current) {
|
|
164
|
+
setLoading(false);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, [response]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
isMounted.current = true;
|
|
171
|
+
fetchData();
|
|
172
|
+
return () => {
|
|
173
|
+
isMounted.current = false;
|
|
174
|
+
};
|
|
175
|
+
}, [fetchData, ...(params.options?.deps || [])]);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
data,
|
|
179
|
+
loading,
|
|
180
|
+
error,
|
|
181
|
+
response,
|
|
182
|
+
refetch: fetchData,
|
|
183
|
+
hasNextPage: !!response?.next,
|
|
184
|
+
hasPreviousPage: !!response?.prev,
|
|
185
|
+
fetchNextPage,
|
|
186
|
+
fetchPreviousPage,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { ApiDataInterface } from "../../core/interfaces/ApiDataInterface";
|
|
5
|
+
import { ApiRequestDataTypeInterface } from "../../core/interfaces/ApiRequestDataTypeInterface";
|
|
6
|
+
import { ApiResponseInterface } from "../../core/interfaces/ApiResponseInterface";
|
|
7
|
+
|
|
8
|
+
export type MutationMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
9
|
+
|
|
10
|
+
export interface UseJsonApiMutationResult<T> {
|
|
11
|
+
/**
|
|
12
|
+
* The result data from the mutation, or null if not yet executed.
|
|
13
|
+
*/
|
|
14
|
+
data: T | null;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the mutation is currently in progress.
|
|
17
|
+
*/
|
|
18
|
+
loading: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Error message if the mutation failed.
|
|
21
|
+
*/
|
|
22
|
+
error: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* The full API response.
|
|
25
|
+
*/
|
|
26
|
+
response: ApiResponseInterface | null;
|
|
27
|
+
/**
|
|
28
|
+
* Execute the mutation.
|
|
29
|
+
*/
|
|
30
|
+
mutate: (params: MutationParams) => Promise<T | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Reset the mutation state.
|
|
33
|
+
*/
|
|
34
|
+
reset: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MutationParams {
|
|
38
|
+
/**
|
|
39
|
+
* The endpoint to call.
|
|
40
|
+
*/
|
|
41
|
+
endpoint: string;
|
|
42
|
+
/**
|
|
43
|
+
* The request body.
|
|
44
|
+
*/
|
|
45
|
+
body?: any;
|
|
46
|
+
/**
|
|
47
|
+
* Files to upload.
|
|
48
|
+
*/
|
|
49
|
+
files?: { [key: string]: File | Blob } | File | Blob;
|
|
50
|
+
/**
|
|
51
|
+
* Company ID for multi-tenant requests.
|
|
52
|
+
*/
|
|
53
|
+
companyId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Override the default JSON:API body creation.
|
|
56
|
+
*/
|
|
57
|
+
overridesJsonApiCreation?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Response type if different from the request type.
|
|
60
|
+
*/
|
|
61
|
+
responseType?: ApiRequestDataTypeInterface;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hook for making mutations (POST, PUT, PATCH, DELETE) to a JSON:API endpoint.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const { mutate, loading, error } = useJsonApiMutation<Article>({
|
|
70
|
+
* method: "POST",
|
|
71
|
+
* classKey: Modules.Article,
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* const handleSubmit = async (data: ArticleInput) => {
|
|
75
|
+
* const result = await mutate({
|
|
76
|
+
* endpoint: "/articles",
|
|
77
|
+
* body: data,
|
|
78
|
+
* });
|
|
79
|
+
* if (result) {
|
|
80
|
+
* // Success!
|
|
81
|
+
* }
|
|
82
|
+
* };
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function useJsonApiMutation<T extends ApiDataInterface>(config: {
|
|
86
|
+
method: MutationMethod;
|
|
87
|
+
classKey: ApiRequestDataTypeInterface;
|
|
88
|
+
onSuccess?: (data: T) => void;
|
|
89
|
+
onError?: (error: string) => void;
|
|
90
|
+
}): UseJsonApiMutationResult<T> {
|
|
91
|
+
const [data, setData] = useState<T | null>(null);
|
|
92
|
+
const [loading, setLoading] = useState(false);
|
|
93
|
+
const [error, setError] = useState<string | null>(null);
|
|
94
|
+
const [response, setResponse] = useState<ApiResponseInterface | null>(null);
|
|
95
|
+
|
|
96
|
+
const reset = useCallback(() => {
|
|
97
|
+
setData(null);
|
|
98
|
+
setLoading(false);
|
|
99
|
+
setError(null);
|
|
100
|
+
setResponse(null);
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const mutate = useCallback(
|
|
104
|
+
async (params: MutationParams): Promise<T | null> => {
|
|
105
|
+
setLoading(true);
|
|
106
|
+
setError(null);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const { JsonApiPost, JsonApiPut, JsonApiPatch, JsonApiDelete } = await import("../../unified/JsonApiRequest");
|
|
110
|
+
const language = navigator.language.split("-")[0] || "en";
|
|
111
|
+
|
|
112
|
+
let apiResponse: ApiResponseInterface;
|
|
113
|
+
|
|
114
|
+
switch (config.method) {
|
|
115
|
+
case "POST":
|
|
116
|
+
apiResponse = await JsonApiPost({
|
|
117
|
+
classKey: config.classKey,
|
|
118
|
+
endpoint: params.endpoint,
|
|
119
|
+
companyId: params.companyId,
|
|
120
|
+
body: params.body,
|
|
121
|
+
overridesJsonApiCreation: params.overridesJsonApiCreation,
|
|
122
|
+
files: params.files,
|
|
123
|
+
language,
|
|
124
|
+
responseType: params.responseType,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
case "PUT":
|
|
128
|
+
apiResponse = await JsonApiPut({
|
|
129
|
+
classKey: config.classKey,
|
|
130
|
+
endpoint: params.endpoint,
|
|
131
|
+
companyId: params.companyId,
|
|
132
|
+
body: params.body,
|
|
133
|
+
files: params.files,
|
|
134
|
+
language,
|
|
135
|
+
responseType: params.responseType,
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
case "PATCH":
|
|
139
|
+
apiResponse = await JsonApiPatch({
|
|
140
|
+
classKey: config.classKey,
|
|
141
|
+
endpoint: params.endpoint,
|
|
142
|
+
companyId: params.companyId,
|
|
143
|
+
body: params.body,
|
|
144
|
+
overridesJsonApiCreation: params.overridesJsonApiCreation,
|
|
145
|
+
files: params.files,
|
|
146
|
+
language,
|
|
147
|
+
responseType: params.responseType,
|
|
148
|
+
});
|
|
149
|
+
break;
|
|
150
|
+
case "DELETE":
|
|
151
|
+
apiResponse = await JsonApiDelete({
|
|
152
|
+
classKey: config.classKey,
|
|
153
|
+
endpoint: params.endpoint,
|
|
154
|
+
companyId: params.companyId,
|
|
155
|
+
language,
|
|
156
|
+
responseType: params.responseType,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setResponse(apiResponse);
|
|
162
|
+
|
|
163
|
+
if (apiResponse.ok) {
|
|
164
|
+
const resultData = apiResponse.data as T;
|
|
165
|
+
setData(resultData);
|
|
166
|
+
config.onSuccess?.(resultData);
|
|
167
|
+
return resultData;
|
|
168
|
+
} else {
|
|
169
|
+
setError(apiResponse.error);
|
|
170
|
+
config.onError?.(apiResponse.error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
175
|
+
setError(errorMessage);
|
|
176
|
+
config.onError?.(errorMessage);
|
|
177
|
+
return null;
|
|
178
|
+
} finally {
|
|
179
|
+
setLoading(false);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
[config],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
data,
|
|
187
|
+
loading,
|
|
188
|
+
error,
|
|
189
|
+
response,
|
|
190
|
+
mutate,
|
|
191
|
+
reset,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { ApiDataInterface } from "../../core/interfaces/ApiDataInterface";
|
|
5
|
+
import { ApiRequestDataTypeInterface } from "../../core/interfaces/ApiRequestDataTypeInterface";
|
|
6
|
+
import { JsonApiHydratedDataInterface } from "../../core/interfaces/JsonApiHydratedDataInterface";
|
|
7
|
+
import { RehydrationFactory } from "../../core/factories/RehydrationFactory";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to rehydrate server-passed data into typed objects.
|
|
11
|
+
* Use this when passing data from server components to client components.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // In server component
|
|
16
|
+
* const article = await ArticleService.findOne(id);
|
|
17
|
+
* return <ArticleDetails data={article.dehydrate()} />;
|
|
18
|
+
*
|
|
19
|
+
* // In client component
|
|
20
|
+
* function ArticleDetails({ data }: { data: JsonApiHydratedDataInterface }) {
|
|
21
|
+
* const article = useRehydration<Article>(Modules.Article, data);
|
|
22
|
+
* return <div>{article.title}</div>;
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useRehydration<T extends ApiDataInterface>(
|
|
27
|
+
classKey: ApiRequestDataTypeInterface,
|
|
28
|
+
data: JsonApiHydratedDataInterface | null | undefined,
|
|
29
|
+
): T | null {
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
if (!data) return null;
|
|
32
|
+
return RehydrationFactory.rehydrate<T>(classKey, data);
|
|
33
|
+
}, [classKey, data]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hook to rehydrate a list of server-passed data into typed objects.
|
|
38
|
+
*/
|
|
39
|
+
export function useRehydrationList<T extends ApiDataInterface>(
|
|
40
|
+
classKey: ApiRequestDataTypeInterface,
|
|
41
|
+
data: JsonApiHydratedDataInterface[] | null | undefined,
|
|
42
|
+
): T[] {
|
|
43
|
+
return useMemo(() => {
|
|
44
|
+
if (!data || data.length === 0) return [];
|
|
45
|
+
return RehydrationFactory.rehydrateList<T>(classKey, data);
|
|
46
|
+
}, [classKey, data]);
|
|
47
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ApiData } from "../core/interfaces/ApiData";
|
|
4
|
+
|
|
5
|
+
export interface DirectFetchParams {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
body?: any;
|
|
10
|
+
files?: { [key: string]: File | Blob } | File | Blob;
|
|
11
|
+
companyId?: string;
|
|
12
|
+
language: string;
|
|
13
|
+
additionalHeaders?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Client-side direct fetch to bypass server action overhead.
|
|
18
|
+
* Use this for client-side API calls.
|
|
19
|
+
*/
|
|
20
|
+
export async function directFetch(params: DirectFetchParams): Promise<ApiData> {
|
|
21
|
+
const response: ApiData = {
|
|
22
|
+
data: undefined,
|
|
23
|
+
ok: false,
|
|
24
|
+
status: 0,
|
|
25
|
+
statusText: "",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const additionalHeaders: Record<string, string> = { ...params.additionalHeaders };
|
|
29
|
+
|
|
30
|
+
if (params.companyId) {
|
|
31
|
+
additionalHeaders["x-companyid"] = params.companyId;
|
|
32
|
+
}
|
|
33
|
+
additionalHeaders["x-language"] = params.language;
|
|
34
|
+
|
|
35
|
+
let requestBody: BodyInit | undefined = undefined;
|
|
36
|
+
|
|
37
|
+
if (params.files) {
|
|
38
|
+
const formData = new FormData();
|
|
39
|
+
if (params.body && typeof params.body === "object") {
|
|
40
|
+
for (const key in params.body) {
|
|
41
|
+
if (Object.prototype.hasOwnProperty.call(params.body, key)) {
|
|
42
|
+
formData.append(
|
|
43
|
+
key,
|
|
44
|
+
typeof params.body[key] === "object" ? JSON.stringify(params.body[key]) : params.body[key],
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (params.files instanceof Blob) {
|
|
51
|
+
formData.append("file", params.files);
|
|
52
|
+
} else if (typeof params.files === "object" && params.files !== null) {
|
|
53
|
+
for (const key in params.files) {
|
|
54
|
+
if (Object.prototype.hasOwnProperty.call(params.files, key)) {
|
|
55
|
+
formData.append(key, params.files[key]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
requestBody = formData;
|
|
61
|
+
} else if (params.body !== undefined) {
|
|
62
|
+
requestBody = JSON.stringify(params.body);
|
|
63
|
+
additionalHeaders["Content-Type"] = "application/json";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const options: RequestInit = {
|
|
67
|
+
method: params.method,
|
|
68
|
+
headers: { Accept: "application/json", ...additionalHeaders },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (requestBody !== undefined) {
|
|
72
|
+
options.body = requestBody;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (params.token) {
|
|
76
|
+
options.headers = { ...options.headers, Authorization: `Bearer ${params.token}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const apiResponse = await fetch(params.url, options);
|
|
81
|
+
|
|
82
|
+
response.ok = apiResponse.ok;
|
|
83
|
+
response.status = apiResponse.status;
|
|
84
|
+
response.statusText = apiResponse.statusText;
|
|
85
|
+
try {
|
|
86
|
+
response.data = await apiResponse.json();
|
|
87
|
+
} catch {
|
|
88
|
+
response.data = undefined;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
response.ok = false;
|
|
92
|
+
response.status = 500;
|
|
93
|
+
response.data = undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils";
|
|
4
|
+
import { Header } from "../navigations";
|
|
5
|
+
|
|
6
|
+
type PageContainerProps = { children: React.ReactNode; testId?: string; className?: string };
|
|
7
|
+
|
|
8
|
+
export function PageContainer({ children, testId, className }: PageContainerProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={`flex h-full w-full flex-col`} data-testid={testId}>
|
|
11
|
+
<Header />
|
|
12
|
+
<main className={cn(`flex w-full flex-1 flex-col gap-y-4 pt-4 pl-4 pr-4`, className)}>{children}</main>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|