@djangocfg/ext-base 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +346 -0
- package/dist/api.cjs +41 -0
- package/dist/api.d.cts +35 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +2 -0
- package/dist/auth.cjs +10 -0
- package/dist/auth.d.cts +1 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +2 -0
- package/dist/chunk-3RG5ZIWI.js +8 -0
- package/dist/chunk-MECBWZG4.js +44 -0
- package/dist/chunk-YQGNYUBX.js +67 -0
- package/dist/hooks.cjs +190 -0
- package/dist/hooks.d.cts +96 -0
- package/dist/hooks.d.ts +96 -0
- package/dist/hooks.js +65 -0
- package/dist/index.cjs +131 -0
- package/dist/index.d.cts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +3 -0
- package/package.json +80 -0
- package/src/api/createExtensionAPI.ts +63 -0
- package/src/api/index.ts +5 -0
- package/src/auth/index.ts +13 -0
- package/src/config/env.ts +59 -0
- package/src/config/index.ts +5 -0
- package/src/context/ExtensionProvider.tsx +102 -0
- package/src/context/createExtensionContext.tsx +78 -0
- package/src/context/index.ts +7 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useInfinitePagination.ts +117 -0
- package/src/hooks/usePagination.ts +155 -0
- package/src/hooks.ts +17 -0
- package/src/index.ts +21 -0
- package/src/logger/createExtensionLogger.ts +61 -0
- package/src/logger/index.ts +5 -0
- package/src/types/context.ts +93 -0
- package/src/types/error.ts +12 -0
- package/src/types/index.ts +17 -0
- package/src/types/logger.ts +17 -0
- package/src/types/pagination.ts +47 -0
- package/src/utils/errors.ts +71 -0
- package/src/utils/index.ts +10 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment configuration utilities
|
|
3
|
+
*
|
|
4
|
+
* Safe to use in both client and server contexts.
|
|
5
|
+
* Can be imported from server-safe entry point.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if running in development mode
|
|
10
|
+
*/
|
|
11
|
+
export const isDevelopment = process.env.NODE_ENV === 'development';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if running in production mode
|
|
15
|
+
*/
|
|
16
|
+
export const isProduction = process.env.NODE_ENV === 'production';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if running in test mode
|
|
20
|
+
*/
|
|
21
|
+
export const isTest = process.env.NODE_ENV === 'test';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if this is a static build (Next.js export)
|
|
25
|
+
*/
|
|
26
|
+
export const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if code is running on client side
|
|
30
|
+
* Safe to use in both client and server contexts
|
|
31
|
+
*/
|
|
32
|
+
export const isClient = typeof window !== 'undefined';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if code is running on server side
|
|
36
|
+
* Safe to use in both client and server contexts
|
|
37
|
+
*/
|
|
38
|
+
export const isServer = !isClient;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get API URL from environment
|
|
42
|
+
* Returns empty string if not set
|
|
43
|
+
*/
|
|
44
|
+
export const getApiUrl = (): string => {
|
|
45
|
+
return process.env.NEXT_PUBLIC_API_URL || '';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Environment configuration object
|
|
50
|
+
*/
|
|
51
|
+
export const env = {
|
|
52
|
+
isDevelopment,
|
|
53
|
+
isProduction,
|
|
54
|
+
isTest,
|
|
55
|
+
isStaticBuild,
|
|
56
|
+
isClient,
|
|
57
|
+
isServer,
|
|
58
|
+
getApiUrl,
|
|
59
|
+
} as const;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base provider wrapper with SWR configuration and auth for extensions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use client';
|
|
6
|
+
|
|
7
|
+
import { useEffect } from 'react';
|
|
8
|
+
import { SWRConfig } from 'swr';
|
|
9
|
+
import type { ExtensionContextOptions, ExtensionProviderProps } from '../types';
|
|
10
|
+
import { createExtensionLogger } from '../logger';
|
|
11
|
+
import { isDevelopment } from '../config';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OPTIONS: ExtensionContextOptions = {
|
|
14
|
+
revalidateOnFocus: false,
|
|
15
|
+
revalidateOnReconnect: false,
|
|
16
|
+
revalidateIfStale: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Extension registry for debugging and tracking
|
|
20
|
+
const registeredExtensions = new Set<string>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base provider with SWR configuration for extension contexts
|
|
24
|
+
*
|
|
25
|
+
* Provides:
|
|
26
|
+
* - SWR configuration for data fetching
|
|
27
|
+
* - Extension registration and metadata management
|
|
28
|
+
* - Auth context from @djangocfg/api (automatically available via useAuth)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* // In extension package:
|
|
33
|
+
* export function NewsletterProvider({ children }: { children: ReactNode }) {
|
|
34
|
+
* return (
|
|
35
|
+
* <ExtensionProvider
|
|
36
|
+
* metadata={{
|
|
37
|
+
* name: 'newsletter',
|
|
38
|
+
* version: '1.0.0',
|
|
39
|
+
* author: 'DjangoCFG',
|
|
40
|
+
* displayName: 'Newsletter',
|
|
41
|
+
* description: 'Newsletter subscription management',
|
|
42
|
+
* icon: '📧',
|
|
43
|
+
* license: 'MIT',
|
|
44
|
+
* githubUrl: 'https://github.com/markolofsen/django-cfg',
|
|
45
|
+
* keywords: ['newsletter', 'email', 'subscription'],
|
|
46
|
+
* }}
|
|
47
|
+
* options={{ revalidateOnFocus: true }}
|
|
48
|
+
* >
|
|
49
|
+
* {children}
|
|
50
|
+
* </ExtensionProvider>
|
|
51
|
+
* );
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* // In components:
|
|
55
|
+
* import { useAuth } from '@djangocfg/api/auth';
|
|
56
|
+
*
|
|
57
|
+
* function MyComponent() {
|
|
58
|
+
* const { user, isAuthenticated } = useAuth();
|
|
59
|
+
* // Auth is automatically available!
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function ExtensionProvider({ children, metadata, options = {} }: ExtensionProviderProps) {
|
|
64
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
65
|
+
|
|
66
|
+
// Register extension on mount
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (registeredExtensions.has(metadata.name)) {
|
|
69
|
+
if (isDevelopment) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[ExtensionProvider] Extension "${metadata.name}" is already registered. ` +
|
|
72
|
+
`This might indicate that the extension is mounted multiple times.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
registeredExtensions.add(metadata.name);
|
|
79
|
+
|
|
80
|
+
if (isDevelopment) {
|
|
81
|
+
const logger = createExtensionLogger({ tag: 'ext-base', level: 'info' });
|
|
82
|
+
logger.info(
|
|
83
|
+
`Extension registered: ${metadata.displayName || metadata.name} v${metadata.version}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
registeredExtensions.delete(metadata.name);
|
|
89
|
+
};
|
|
90
|
+
}, [metadata.name, metadata.version, metadata.displayName]);
|
|
91
|
+
|
|
92
|
+
// Note: useAuth from @djangocfg/api is automatically available
|
|
93
|
+
// No need to wrap - extensions can import and use it directly
|
|
94
|
+
return <SWRConfig value={config}>{children}</SWRConfig>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get all registered extensions (for debugging)
|
|
99
|
+
*/
|
|
100
|
+
export function getRegisteredExtensions(): string[] {
|
|
101
|
+
return Array.from(registeredExtensions);
|
|
102
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for creating typed React contexts for extensions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use client';
|
|
6
|
+
|
|
7
|
+
import { createContext, useContext, type Context, type ReactNode, type ReactElement } from 'react';
|
|
8
|
+
|
|
9
|
+
export interface CreateExtensionContextOptions<T> {
|
|
10
|
+
/** Display name for the context (for debugging) */
|
|
11
|
+
displayName: string;
|
|
12
|
+
|
|
13
|
+
/** Error message when hook is used outside provider */
|
|
14
|
+
errorMessage?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExtensionContextReturn<T> {
|
|
18
|
+
/** The React Context */
|
|
19
|
+
Context: Context<T | undefined>;
|
|
20
|
+
|
|
21
|
+
/** Provider component */
|
|
22
|
+
Provider: (props: { value: T; children: ReactNode }) => ReactElement;
|
|
23
|
+
|
|
24
|
+
/** Hook to access context value */
|
|
25
|
+
useContext: () => T;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a typed React context with provider and hook
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* interface MyContextValue {
|
|
34
|
+
* data: string[];
|
|
35
|
+
* isLoading: boolean;
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* const { Provider, useContext } = createExtensionContext<MyContextValue>({
|
|
39
|
+
* displayName: 'MyContext'
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // In provider component:
|
|
43
|
+
* <Provider value={{ data: [], isLoading: false }}>
|
|
44
|
+
* {children}
|
|
45
|
+
* </Provider>
|
|
46
|
+
*
|
|
47
|
+
* // In consumer component:
|
|
48
|
+
* const { data, isLoading } = useContext();
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function createExtensionContext<T>({
|
|
52
|
+
displayName,
|
|
53
|
+
errorMessage,
|
|
54
|
+
}: CreateExtensionContextOptions<T>): ExtensionContextReturn<T> {
|
|
55
|
+
const Context = createContext<T | undefined>(undefined);
|
|
56
|
+
Context.displayName = displayName;
|
|
57
|
+
|
|
58
|
+
const Provider = ({ value, children }: { value: T; children: ReactNode }) => {
|
|
59
|
+
return <Context.Provider value={value}>{children}</Context.Provider>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const useContextHook = (): T => {
|
|
63
|
+
const context = useContext(Context);
|
|
64
|
+
if (context === undefined) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
errorMessage ||
|
|
67
|
+
`use${displayName} must be used within ${displayName}Provider`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return context;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
Context,
|
|
75
|
+
Provider,
|
|
76
|
+
useContext: useContextHook,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context utilities for extension packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createExtensionContext } from './createExtensionContext';
|
|
6
|
+
export { ExtensionProvider } from './ExtensionProvider';
|
|
7
|
+
export type { ExtensionContextReturn, CreateExtensionContextOptions } from './createExtensionContext';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic hook for infinite scroll pagination
|
|
3
|
+
* Works with any paginated API endpoint
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import useSWRInfinite from 'swr/infinite';
|
|
9
|
+
import type {
|
|
10
|
+
PaginatedResponse,
|
|
11
|
+
InfinitePaginationReturn,
|
|
12
|
+
InfinitePaginationOptions,
|
|
13
|
+
} from '../types';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
16
|
+
|
|
17
|
+
interface UseInfinitePaginationParams<T> {
|
|
18
|
+
/** Unique key prefix for SWR cache */
|
|
19
|
+
keyPrefix: string;
|
|
20
|
+
|
|
21
|
+
/** Fetcher function that takes (page, pageSize) and returns paginated data */
|
|
22
|
+
fetcher: (page: number, pageSize: number) => Promise<PaginatedResponse<T>>;
|
|
23
|
+
|
|
24
|
+
/** Options for pagination behavior */
|
|
25
|
+
options?: InfinitePaginationOptions;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook for infinite scroll with automatic pagination
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { items, loadMore, hasMore, isLoading } = useInfinitePagination({
|
|
34
|
+
* keyPrefix: 'tickets',
|
|
35
|
+
* fetcher: (page, pageSize) => apiSupport.tickets.list({ page, page_size: pageSize }),
|
|
36
|
+
* options: { pageSize: 20 }
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useInfinitePagination<T>({
|
|
41
|
+
keyPrefix,
|
|
42
|
+
fetcher,
|
|
43
|
+
options = {},
|
|
44
|
+
}: UseInfinitePaginationParams<T>): InfinitePaginationReturn<T> {
|
|
45
|
+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
46
|
+
|
|
47
|
+
const getKey = (pageIndex: number, previousPageData: PaginatedResponse<T> | null) => {
|
|
48
|
+
// Reached the end
|
|
49
|
+
if (previousPageData && !previousPageData.has_next && !previousPageData.next) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// First page, no previous data
|
|
54
|
+
if (pageIndex === 0) {
|
|
55
|
+
return [keyPrefix, 'infinite', 1, pageSize];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add the page number to the SWR key
|
|
59
|
+
return [keyPrefix, 'infinite', pageIndex + 1, pageSize];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const swrFetcher = async ([, , page, size]: [string, string, number, number]) => {
|
|
63
|
+
return fetcher(page, size);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
data,
|
|
68
|
+
error,
|
|
69
|
+
isLoading,
|
|
70
|
+
isValidating,
|
|
71
|
+
size,
|
|
72
|
+
setSize,
|
|
73
|
+
mutate,
|
|
74
|
+
} = useSWRInfinite<PaginatedResponse<T>>(getKey, swrFetcher, {
|
|
75
|
+
revalidateFirstPage: options.revalidateFirstPage ?? false,
|
|
76
|
+
parallel: options.parallel ?? false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Flatten all pages into single array
|
|
80
|
+
const items: T[] = data ? data.flatMap((page) => page.results) : [];
|
|
81
|
+
|
|
82
|
+
// Check if there are more pages
|
|
83
|
+
const hasMore = data
|
|
84
|
+
? Boolean(data[data.length - 1]?.has_next) || Boolean(data[data.length - 1]?.next)
|
|
85
|
+
: false;
|
|
86
|
+
|
|
87
|
+
// Total count from last page
|
|
88
|
+
const totalCount = data?.[data.length - 1]?.count ?? 0;
|
|
89
|
+
|
|
90
|
+
// Loading more state
|
|
91
|
+
const isLoadingMore = Boolean(
|
|
92
|
+
isValidating && data && typeof data[size - 1] !== 'undefined'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Function to load next page
|
|
96
|
+
const loadMore = () => {
|
|
97
|
+
if (hasMore && !isLoadingMore) {
|
|
98
|
+
setSize(size + 1);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Refresh all pages
|
|
103
|
+
const refresh = async () => {
|
|
104
|
+
await mutate();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
items,
|
|
109
|
+
isLoading,
|
|
110
|
+
isLoadingMore,
|
|
111
|
+
error,
|
|
112
|
+
hasMore,
|
|
113
|
+
totalCount,
|
|
114
|
+
loadMore,
|
|
115
|
+
refresh,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic hook for standard pagination
|
|
3
|
+
* Works with any paginated API endpoint
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback } from 'react';
|
|
9
|
+
import useSWR from 'swr';
|
|
10
|
+
import type { PaginatedResponse, PaginationParams, PaginationState } from '../types';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
13
|
+
|
|
14
|
+
interface UsePaginationParams<T> {
|
|
15
|
+
/** Unique key prefix for SWR cache */
|
|
16
|
+
keyPrefix: string;
|
|
17
|
+
|
|
18
|
+
/** Fetcher function that takes pagination params and returns paginated data */
|
|
19
|
+
fetcher: (params: PaginationParams) => Promise<PaginatedResponse<T>>;
|
|
20
|
+
|
|
21
|
+
/** Initial page number (default: 1) */
|
|
22
|
+
initialPage?: number;
|
|
23
|
+
|
|
24
|
+
/** Items per page (default: 20) */
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
|
|
27
|
+
/** Initial ordering */
|
|
28
|
+
initialOrdering?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface UsePaginationReturn<T> {
|
|
32
|
+
/** Current page items */
|
|
33
|
+
items: T[];
|
|
34
|
+
|
|
35
|
+
/** Pagination state */
|
|
36
|
+
pagination: PaginationState;
|
|
37
|
+
|
|
38
|
+
/** Loading state */
|
|
39
|
+
isLoading: boolean;
|
|
40
|
+
|
|
41
|
+
/** Error state */
|
|
42
|
+
error: any;
|
|
43
|
+
|
|
44
|
+
/** Go to specific page */
|
|
45
|
+
goToPage: (page: number) => void;
|
|
46
|
+
|
|
47
|
+
/** Go to next page */
|
|
48
|
+
nextPage: () => void;
|
|
49
|
+
|
|
50
|
+
/** Go to previous page */
|
|
51
|
+
previousPage: () => void;
|
|
52
|
+
|
|
53
|
+
/** Change page size */
|
|
54
|
+
setPageSize: (size: number) => void;
|
|
55
|
+
|
|
56
|
+
/** Change ordering */
|
|
57
|
+
setOrdering: (ordering: string) => void;
|
|
58
|
+
|
|
59
|
+
/** Refresh current page */
|
|
60
|
+
refresh: () => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hook for standard pagination with page controls
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```tsx
|
|
68
|
+
* const { items, pagination, goToPage, nextPage } = usePagination({
|
|
69
|
+
* keyPrefix: 'campaigns',
|
|
70
|
+
* fetcher: (params) => apiNewsletter.campaigns.list(params),
|
|
71
|
+
* pageSize: 10
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function usePagination<T>({
|
|
76
|
+
keyPrefix,
|
|
77
|
+
fetcher,
|
|
78
|
+
initialPage = 1,
|
|
79
|
+
pageSize: initialPageSize = DEFAULT_PAGE_SIZE,
|
|
80
|
+
initialOrdering,
|
|
81
|
+
}: UsePaginationParams<T>): UsePaginationReturn<T> {
|
|
82
|
+
const [page, setPage] = useState(initialPage);
|
|
83
|
+
const [pageSize, setPageSizeState] = useState(initialPageSize);
|
|
84
|
+
const [ordering, setOrdering] = useState<string | undefined>(initialOrdering);
|
|
85
|
+
|
|
86
|
+
const params: PaginationParams = {
|
|
87
|
+
page,
|
|
88
|
+
page_size: pageSize,
|
|
89
|
+
ordering,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const { data, error, isLoading, mutate } = useSWR(
|
|
93
|
+
[keyPrefix, 'paginated', params],
|
|
94
|
+
() => fetcher(params),
|
|
95
|
+
{
|
|
96
|
+
revalidateOnFocus: false,
|
|
97
|
+
revalidateIfStale: false,
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const items = data?.results ?? [];
|
|
102
|
+
const totalCount = data?.count ?? 0;
|
|
103
|
+
const totalPages = Math.ceil(totalCount / pageSize);
|
|
104
|
+
const hasNext = data?.has_next ?? Boolean(data?.next);
|
|
105
|
+
const hasPrevious = data?.has_previous ?? Boolean(data?.previous);
|
|
106
|
+
|
|
107
|
+
const pagination: PaginationState = {
|
|
108
|
+
page,
|
|
109
|
+
pageSize,
|
|
110
|
+
totalCount,
|
|
111
|
+
totalPages,
|
|
112
|
+
hasNext,
|
|
113
|
+
hasPrevious,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const goToPage = useCallback((newPage: number) => {
|
|
117
|
+
if (newPage >= 1 && newPage <= totalPages) {
|
|
118
|
+
setPage(newPage);
|
|
119
|
+
}
|
|
120
|
+
}, [totalPages]);
|
|
121
|
+
|
|
122
|
+
const nextPage = useCallback(() => {
|
|
123
|
+
if (hasNext) {
|
|
124
|
+
setPage((prev) => prev + 1);
|
|
125
|
+
}
|
|
126
|
+
}, [hasNext]);
|
|
127
|
+
|
|
128
|
+
const previousPage = useCallback(() => {
|
|
129
|
+
if (hasPrevious) {
|
|
130
|
+
setPage((prev) => Math.max(1, prev - 1));
|
|
131
|
+
}
|
|
132
|
+
}, [hasPrevious]);
|
|
133
|
+
|
|
134
|
+
const setPageSize = useCallback((newSize: number) => {
|
|
135
|
+
setPageSizeState(newSize);
|
|
136
|
+
setPage(1); // Reset to first page when changing page size
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const refresh = useCallback(async () => {
|
|
140
|
+
await mutate();
|
|
141
|
+
}, [mutate]);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
items,
|
|
145
|
+
pagination,
|
|
146
|
+
isLoading,
|
|
147
|
+
error,
|
|
148
|
+
goToPage,
|
|
149
|
+
nextPage,
|
|
150
|
+
previousPage,
|
|
151
|
+
setPageSize,
|
|
152
|
+
setOrdering,
|
|
153
|
+
refresh,
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @djangocfg/ext-base/hooks
|
|
3
|
+
*
|
|
4
|
+
* Client-only hooks and components for extension packages.
|
|
5
|
+
* Use this entry point in client components only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
// Re-export everything from main entry (server-safe)
|
|
11
|
+
export * from './index';
|
|
12
|
+
|
|
13
|
+
// Client-only hooks
|
|
14
|
+
export * from './hooks';
|
|
15
|
+
|
|
16
|
+
// Client-only context utilities
|
|
17
|
+
export * from './context';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @djangocfg/ext-base
|
|
3
|
+
*
|
|
4
|
+
* Base utilities for extension packages.
|
|
5
|
+
* Server-safe entry point - can be used in both server and client components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type * from './types';
|
|
10
|
+
|
|
11
|
+
// Config
|
|
12
|
+
export * from './config';
|
|
13
|
+
|
|
14
|
+
// API
|
|
15
|
+
export * from './api';
|
|
16
|
+
|
|
17
|
+
// Utils
|
|
18
|
+
export * from './utils';
|
|
19
|
+
|
|
20
|
+
// Logger
|
|
21
|
+
export * from './logger';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger factory for extension packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createConsola } from 'consola';
|
|
6
|
+
import type { ExtensionLogger, LoggerOptions } from '../types';
|
|
7
|
+
|
|
8
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
9
|
+
|
|
10
|
+
const LEVEL_MAP = {
|
|
11
|
+
debug: 4,
|
|
12
|
+
info: 3,
|
|
13
|
+
warn: 2,
|
|
14
|
+
error: 1,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a tagged logger for an extension
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // In extension package
|
|
23
|
+
* export const logger = createExtensionLogger({
|
|
24
|
+
* tag: 'ext-newsletter',
|
|
25
|
+
* level: 'info'
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Usage
|
|
29
|
+
* logger.info('Campaign created:', campaignId);
|
|
30
|
+
* logger.error('Failed to send campaign:', error);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function createExtensionLogger(options: LoggerOptions): ExtensionLogger {
|
|
34
|
+
const { tag, level = 'info', enabled = true } = options;
|
|
35
|
+
|
|
36
|
+
if (!enabled) {
|
|
37
|
+
// Return no-op logger if disabled
|
|
38
|
+
const noop = () => {};
|
|
39
|
+
return {
|
|
40
|
+
info: noop,
|
|
41
|
+
warn: noop,
|
|
42
|
+
error: noop,
|
|
43
|
+
debug: noop,
|
|
44
|
+
success: noop,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const logLevel = isDevelopment ? LEVEL_MAP[level] : LEVEL_MAP.error;
|
|
49
|
+
|
|
50
|
+
const consola = createConsola({
|
|
51
|
+
level: logLevel,
|
|
52
|
+
}).withTag(tag);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
info: (...args: unknown[]) => consola.info(...(args as [any, ...any[]])),
|
|
56
|
+
warn: (...args: unknown[]) => consola.warn(...(args as [any, ...any[]])),
|
|
57
|
+
error: (...args: unknown[]) => consola.error(...(args as [any, ...any[]])),
|
|
58
|
+
debug: (...args: unknown[]) => consola.debug(...(args as [any, ...any[]])),
|
|
59
|
+
success: (...args: unknown[]) => consola.success(...(args as [any, ...any[]])),
|
|
60
|
+
};
|
|
61
|
+
}
|