@djangocfg/ext-support 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.
Files changed (65) hide show
  1. package/README.md +233 -0
  2. package/dist/chunk-AZ4LWZB7.js +2630 -0
  3. package/dist/hooks.cjs +2716 -0
  4. package/dist/hooks.d.cts +255 -0
  5. package/dist/hooks.d.ts +255 -0
  6. package/dist/hooks.js +1 -0
  7. package/dist/index.cjs +2693 -0
  8. package/dist/index.d.cts +1392 -0
  9. package/dist/index.d.ts +1392 -0
  10. package/dist/index.js +1 -0
  11. package/package.json +80 -0
  12. package/src/api/generated/ext_support/_utils/fetchers/ext_support__support.ts +642 -0
  13. package/src/api/generated/ext_support/_utils/fetchers/index.ts +28 -0
  14. package/src/api/generated/ext_support/_utils/hooks/ext_support__support.ts +237 -0
  15. package/src/api/generated/ext_support/_utils/hooks/index.ts +28 -0
  16. package/src/api/generated/ext_support/_utils/schemas/Message.schema.ts +21 -0
  17. package/src/api/generated/ext_support/_utils/schemas/MessageCreate.schema.ts +15 -0
  18. package/src/api/generated/ext_support/_utils/schemas/MessageCreateRequest.schema.ts +15 -0
  19. package/src/api/generated/ext_support/_utils/schemas/MessageRequest.schema.ts +15 -0
  20. package/src/api/generated/ext_support/_utils/schemas/PaginatedMessageList.schema.ts +24 -0
  21. package/src/api/generated/ext_support/_utils/schemas/PaginatedTicketList.schema.ts +24 -0
  22. package/src/api/generated/ext_support/_utils/schemas/PatchedMessageRequest.schema.ts +15 -0
  23. package/src/api/generated/ext_support/_utils/schemas/PatchedTicketRequest.schema.ts +18 -0
  24. package/src/api/generated/ext_support/_utils/schemas/Sender.schema.ts +21 -0
  25. package/src/api/generated/ext_support/_utils/schemas/Ticket.schema.ts +21 -0
  26. package/src/api/generated/ext_support/_utils/schemas/TicketRequest.schema.ts +18 -0
  27. package/src/api/generated/ext_support/_utils/schemas/index.ts +29 -0
  28. package/src/api/generated/ext_support/api-instance.ts +131 -0
  29. package/src/api/generated/ext_support/client.ts +301 -0
  30. package/src/api/generated/ext_support/enums.ts +45 -0
  31. package/src/api/generated/ext_support/errors.ts +116 -0
  32. package/src/api/generated/ext_support/ext_support__support/client.ts +151 -0
  33. package/src/api/generated/ext_support/ext_support__support/index.ts +2 -0
  34. package/src/api/generated/ext_support/ext_support__support/models.ts +165 -0
  35. package/src/api/generated/ext_support/http.ts +103 -0
  36. package/src/api/generated/ext_support/index.ts +273 -0
  37. package/src/api/generated/ext_support/logger.ts +259 -0
  38. package/src/api/generated/ext_support/retry.ts +175 -0
  39. package/src/api/generated/ext_support/schema.json +1049 -0
  40. package/src/api/generated/ext_support/storage.ts +161 -0
  41. package/src/api/generated/ext_support/validation-events.ts +133 -0
  42. package/src/api/index.ts +9 -0
  43. package/src/config.ts +20 -0
  44. package/src/contexts/SupportContext.tsx +250 -0
  45. package/src/contexts/SupportExtensionProvider.tsx +38 -0
  46. package/src/contexts/types.ts +26 -0
  47. package/src/hooks/index.ts +33 -0
  48. package/src/index.ts +39 -0
  49. package/src/layouts/SupportLayout/README.md +91 -0
  50. package/src/layouts/SupportLayout/SupportLayout.tsx +179 -0
  51. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +155 -0
  52. package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
  53. package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
  54. package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
  55. package/src/layouts/SupportLayout/components/TicketList.tsx +153 -0
  56. package/src/layouts/SupportLayout/components/index.ts +6 -0
  57. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +258 -0
  58. package/src/layouts/SupportLayout/context/index.ts +2 -0
  59. package/src/layouts/SupportLayout/events.ts +33 -0
  60. package/src/layouts/SupportLayout/hooks/index.ts +2 -0
  61. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +115 -0
  62. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +88 -0
  63. package/src/layouts/SupportLayout/index.ts +6 -0
  64. package/src/layouts/SupportLayout/types.ts +21 -0
  65. package/src/utils/logger.ts +14 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Support Layout Context
3
+ * Wrapper around SupportContext with UI state, event handling and infinite scroll
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
9
+ import { useSupportContext, type Ticket, type Message, type MessageCreateRequest } from '../../../contexts/SupportContext';
10
+ import { useAuth } from '@djangocfg/api/auth';
11
+ import { SUPPORT_LAYOUT_EVENTS } from '../events';
12
+ import { useInfiniteMessages } from '../hooks';
13
+ import type { SupportUIState, TicketFormData } from '../types';
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────
16
+ // Context Type
17
+ // ─────────────────────────────────────────────────────────────────────────
18
+
19
+ export interface SupportLayoutContextValue {
20
+ // From API context
21
+ tickets: Ticket[] | undefined;
22
+ isLoadingTickets: boolean;
23
+ ticketsError: Error | undefined;
24
+
25
+ // Selected ticket data
26
+ selectedTicket: Ticket | undefined;
27
+ selectedTicketMessages: Message[] | undefined;
28
+ isLoadingMessages: boolean;
29
+
30
+ // Infinite scroll for messages
31
+ isLoadingMoreMessages: boolean;
32
+ hasMoreMessages: boolean;
33
+ totalMessagesCount: number;
34
+ loadMoreMessages: () => void;
35
+
36
+ // UI state
37
+ uiState: SupportUIState;
38
+
39
+ // Actions
40
+ selectTicket: (ticket: Ticket | null) => void;
41
+ createTicket: (data: TicketFormData) => Promise<void>;
42
+ sendMessage: (message: string) => Promise<void>;
43
+ refreshTickets: () => Promise<void>;
44
+ refreshMessages: () => Promise<void>;
45
+
46
+ // Dialog actions
47
+ openCreateDialog: () => void;
48
+ closeCreateDialog: () => void;
49
+
50
+ // Utilities
51
+ getUnreadCount: () => number;
52
+ }
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────
55
+ // Context
56
+ // ─────────────────────────────────────────────────────────────────────────
57
+
58
+ const SupportLayoutContext = createContext<SupportLayoutContextValue | undefined>(undefined);
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────
61
+ // Provider
62
+ // ─────────────────────────────────────────────────────────────────────────
63
+
64
+ interface SupportLayoutProviderProps {
65
+ children: ReactNode;
66
+ }
67
+
68
+ export function SupportLayoutProvider({ children }: SupportLayoutProviderProps) {
69
+ const support = useSupportContext();
70
+ const { user } = useAuth();
71
+
72
+ // UI state
73
+ const [uiState, setUIState] = useState<SupportUIState>({
74
+ selectedTicketUuid: null,
75
+ isCreateDialogOpen: false,
76
+ viewMode: 'list',
77
+ });
78
+
79
+ // Selected ticket
80
+ const selectedTicket = support.tickets?.find(t => t.uuid === uiState.selectedTicketUuid);
81
+
82
+ // Use infinite scroll hook for messages
83
+ const {
84
+ messages: selectedTicketMessages,
85
+ isLoading: isLoadingMessages,
86
+ isLoadingMore: isLoadingMoreMessages,
87
+ hasMore: hasMoreMessages,
88
+ totalCount: totalMessagesCount,
89
+ loadMore: loadMoreMessages,
90
+ refresh: refreshMessages,
91
+ addMessage: addMessageOptimistically,
92
+ } = useInfiniteMessages(selectedTicket?.uuid || null);
93
+
94
+ // Select ticket
95
+ const selectTicket = useCallback(async (ticket: Ticket | null) => {
96
+ setUIState(prev => ({ ...prev, selectedTicketUuid: ticket?.uuid || null }));
97
+
98
+ if (ticket?.uuid) {
99
+ // The messages will be loaded automatically by the useInfiniteMessages hook
100
+ // when the ticket UUID changes
101
+
102
+ // Dispatch event
103
+ window.dispatchEvent(
104
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.TICKET_SELECTED, { detail: { ticket } })
105
+ );
106
+ }
107
+ }, []);
108
+
109
+ // Create ticket
110
+ const createTicket = useCallback(async (data: TicketFormData) => {
111
+ if (!user?.id) {
112
+ throw new Error('User must be authenticated to create tickets');
113
+ }
114
+
115
+ const ticket = await support.createTicket({
116
+ user: user.id,
117
+ subject: data.subject,
118
+ });
119
+
120
+ // Send initial message if provided
121
+ if (ticket.uuid && data.message) {
122
+ await support.createMessage(ticket.uuid, {
123
+ text: data.message,
124
+ });
125
+ }
126
+
127
+ // Close dialog first for better UX
128
+ setUIState(prev => ({ ...prev, isCreateDialogOpen: false }));
129
+
130
+ // Refresh tickets list to show the new ticket
131
+ await support.refreshTickets();
132
+
133
+ // Dispatch event
134
+ window.dispatchEvent(
135
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.TICKET_CREATED, { detail: { ticket } })
136
+ );
137
+
138
+ // Auto-select the newly created ticket after refresh
139
+ // Use the ticket UUID directly to ensure selection works even if the ticket object changes
140
+ setUIState(prev => ({ ...prev, selectedTicketUuid: ticket.uuid }));
141
+
142
+ // Dispatch selection event
143
+ window.dispatchEvent(
144
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.TICKET_SELECTED, { detail: { ticket } })
145
+ );
146
+ }, [support, user]);
147
+
148
+ // Send message
149
+ const sendMessage = useCallback(async (message: string) => {
150
+ if (!selectedTicket?.uuid) return;
151
+
152
+ // Create the message object
153
+ const messageData: MessageCreateRequest = {
154
+ text: message,
155
+ };
156
+
157
+ // Send message to backend
158
+ const newMessage = await support.createMessage(selectedTicket.uuid, messageData);
159
+
160
+ // Add message optimistically to the UI (it will be replaced when refreshing)
161
+ if (newMessage) {
162
+ const fullMessage: Message = {
163
+ uuid: newMessage.uuid || `temp-${Date.now()}`,
164
+ ticket: selectedTicket.uuid,
165
+ sender: {
166
+ id: user?.id || 0,
167
+ display_username: user?.display_username || '',
168
+ email: user?.email || '',
169
+ avatar: user?.avatar || null,
170
+ initials: user?.initials || '',
171
+ is_staff: user?.is_staff || false,
172
+ is_superuser: user?.is_superuser || false,
173
+ },
174
+ is_from_author: true,
175
+ text: message,
176
+ created_at: new Date().toISOString(),
177
+ };
178
+
179
+ addMessageOptimistically(fullMessage);
180
+ }
181
+
182
+ // Refresh messages to get the latest state
183
+ await refreshMessages();
184
+
185
+ // Dispatch event
186
+ window.dispatchEvent(
187
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.MESSAGE_SENT, { detail: { message: newMessage } })
188
+ );
189
+ }, [selectedTicket, support, user, addMessageOptimistically, refreshMessages]);
190
+
191
+ // Dialog actions
192
+ const openCreateDialog = useCallback(() => {
193
+ setUIState(prev => ({ ...prev, isCreateDialogOpen: true }));
194
+ }, []);
195
+
196
+ const closeCreateDialog = useCallback(() => {
197
+ setUIState(prev => ({ ...prev, isCreateDialogOpen: false }));
198
+ }, []);
199
+
200
+ // Get unread count
201
+ const getUnreadCount = useCallback(() => {
202
+ return support.tickets?.reduce((count, ticket) => count + (ticket.unanswered_messages_count || 0), 0) || 0;
203
+ }, [support.tickets]);
204
+
205
+ // Event listeners
206
+ useEffect(() => {
207
+ const handleOpenDialog = () => openCreateDialog();
208
+ const handleCloseDialog = () => closeCreateDialog();
209
+
210
+ window.addEventListener(SUPPORT_LAYOUT_EVENTS.OPEN_CREATE_DIALOG, handleOpenDialog);
211
+ window.addEventListener(SUPPORT_LAYOUT_EVENTS.CLOSE_CREATE_DIALOG, handleCloseDialog);
212
+
213
+ return () => {
214
+ window.removeEventListener(SUPPORT_LAYOUT_EVENTS.OPEN_CREATE_DIALOG, handleOpenDialog);
215
+ window.removeEventListener(SUPPORT_LAYOUT_EVENTS.CLOSE_CREATE_DIALOG, handleCloseDialog);
216
+ };
217
+ }, [openCreateDialog, closeCreateDialog]);
218
+
219
+ const value: SupportLayoutContextValue = {
220
+ tickets: support.tickets,
221
+ isLoadingTickets: support.isLoadingTickets,
222
+ ticketsError: support.ticketsError,
223
+ selectedTicket,
224
+ selectedTicketMessages,
225
+ isLoadingMessages,
226
+ isLoadingMoreMessages,
227
+ hasMoreMessages,
228
+ totalMessagesCount,
229
+ loadMoreMessages,
230
+ uiState,
231
+ selectTicket,
232
+ createTicket,
233
+ sendMessage,
234
+ refreshTickets: support.refreshTickets,
235
+ refreshMessages,
236
+ openCreateDialog,
237
+ closeCreateDialog,
238
+ getUnreadCount,
239
+ };
240
+
241
+ return (
242
+ <SupportLayoutContext.Provider value={value}>
243
+ {children}
244
+ </SupportLayoutContext.Provider>
245
+ );
246
+ }
247
+
248
+ // ─────────────────────────────────────────────────────────────────────────
249
+ // Hook
250
+ // ─────────────────────────────────────────────────────────────────────────
251
+
252
+ export function useSupportLayoutContext(): SupportLayoutContextValue {
253
+ const context = useContext(SupportLayoutContext);
254
+ if (!context) {
255
+ throw new Error('useSupportLayoutContext must be used within SupportLayoutProvider');
256
+ }
257
+ return context;
258
+ }
@@ -0,0 +1,2 @@
1
+ export * from './SupportLayoutContext';
2
+
@@ -0,0 +1,33 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Support Layout Events
5
+ * Event system for SupportLayout
6
+ */
7
+
8
+ export const SUPPORT_LAYOUT_EVENTS = {
9
+ // Dialog events
10
+ OPEN_CREATE_DIALOG: 'support-layout:open-create-dialog',
11
+ CLOSE_CREATE_DIALOG: 'support-layout:close-create-dialog',
12
+
13
+ // Ticket events
14
+ TICKET_SELECTED: 'support-layout:ticket-selected',
15
+ TICKET_CREATED: 'support-layout:ticket-created',
16
+
17
+ // Message events
18
+ MESSAGE_SENT: 'support-layout:message-sent',
19
+ } as const;
20
+
21
+ // Event publishers
22
+ export const openCreateTicketDialog = () => {
23
+ if (typeof window !== 'undefined') {
24
+ window.dispatchEvent(new CustomEvent(SUPPORT_LAYOUT_EVENTS.OPEN_CREATE_DIALOG));
25
+ }
26
+ };
27
+
28
+ export const closeCreateTicketDialog = () => {
29
+ if (typeof window !== 'undefined') {
30
+ window.dispatchEvent(new CustomEvent(SUPPORT_LAYOUT_EVENTS.CLOSE_CREATE_DIALOG));
31
+ }
32
+ };
33
+
@@ -0,0 +1,2 @@
1
+ export * from './useInfiniteTickets';
2
+ export * from './useInfiniteMessages';
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Hook for infinite scroll support messages
3
+ * Uses SWR Infinite for pagination
4
+ */
5
+
6
+ import useSWRInfinite from 'swr/infinite';
7
+ import * as Fetchers from '../../../api/generated/ext_support/_utils/fetchers';
8
+ import type { PaginatedMessageList, Message } from '../../../api/generated/ext_support/_utils/schemas';
9
+
10
+ const PAGE_SIZE = 20;
11
+
12
+ interface UseInfiniteMessagesReturn {
13
+ messages: Message[];
14
+ isLoading: boolean;
15
+ isLoadingMore: boolean;
16
+ error: Error | undefined;
17
+ hasMore: boolean;
18
+ totalCount: number;
19
+ loadMore: () => void;
20
+ refresh: () => Promise<void>;
21
+ addMessage: (message: Message) => void;
22
+ }
23
+
24
+ export function useInfiniteMessages(ticketUuid: string | null): UseInfiniteMessagesReturn {
25
+ const getKey = (pageIndex: number, previousPageData: PaginatedMessageList | null) => {
26
+ // No ticket selected
27
+ if (!ticketUuid) return null;
28
+
29
+ // Reached the end
30
+ if (previousPageData && !previousPageData.has_next) return null;
31
+
32
+ // First page, no previous data
33
+ if (pageIndex === 0) return ['cfg-support-messages-infinite', ticketUuid, 1, PAGE_SIZE];
34
+
35
+ // Add the page number to the SWR key
36
+ return ['cfg-support-messages-infinite', ticketUuid, pageIndex + 1, PAGE_SIZE];
37
+ };
38
+
39
+ const fetcher = async ([, ticket_uuid, page, pageSize]: [string, string, number, number]) => {
40
+ return Fetchers.getSupportTicketsMessagesList(
41
+ ticket_uuid,
42
+ { page, page_size: pageSize }
43
+ );
44
+ };
45
+
46
+ const {
47
+ data,
48
+ error,
49
+ isLoading,
50
+ isValidating,
51
+ size,
52
+ setSize,
53
+ mutate,
54
+ } = useSWRInfinite<PaginatedMessageList>(getKey, fetcher, {
55
+ revalidateFirstPage: false,
56
+ parallel: false,
57
+ });
58
+
59
+ // Flatten all pages into single array (reversed for chat display)
60
+ const messages: Message[] = data ? data.flatMap((page) => page.results) : [];
61
+
62
+ // Check if there are more pages
63
+ const hasMore = data && data[data.length - 1]?.has_next;
64
+
65
+ // Total count from last page
66
+ const totalCount = data && data[data.length - 1]?.count;
67
+
68
+ // Loading more state
69
+ const isLoadingMore = !!(isValidating && data && typeof data[size - 1] !== 'undefined');
70
+
71
+ // Function to load next page
72
+ const loadMore = () => {
73
+ if (hasMore && !isLoadingMore) {
74
+ setSize(size + 1);
75
+ }
76
+ };
77
+
78
+ // Refresh all pages
79
+ const refresh = async () => {
80
+ await mutate();
81
+ };
82
+
83
+ // Add new message optimistically
84
+ const addMessage = (message: Message) => {
85
+ if (!data || !data[0]) return;
86
+
87
+ // Add the message to the first page
88
+ const newData = [...data];
89
+ const firstPage = newData[0];
90
+
91
+ if (firstPage) {
92
+ newData[0] = {
93
+ ...firstPage,
94
+ results: [message, ...firstPage.results],
95
+ count: firstPage.count + 1,
96
+ page: firstPage.page || 1,
97
+ pages: firstPage.pages || 1,
98
+ };
99
+ }
100
+
101
+ mutate(newData, false);
102
+ };
103
+
104
+ return {
105
+ messages,
106
+ isLoading,
107
+ isLoadingMore: isLoadingMore || false,
108
+ error,
109
+ hasMore: hasMore || false,
110
+ totalCount: totalCount || 0,
111
+ loadMore,
112
+ refresh,
113
+ addMessage,
114
+ };
115
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Hook for infinite scroll support tickets
3
+ * Uses SWR Infinite for pagination
4
+ */
5
+
6
+ import useSWRInfinite from 'swr/infinite';
7
+ import * as Fetchers from '../../../api/generated/ext_support/_utils/fetchers';
8
+ import type { PaginatedTicketList, Ticket } from '../../../api/generated/ext_support/_utils/schemas';
9
+
10
+ const PAGE_SIZE = 20;
11
+
12
+ interface UseInfiniteTicketsReturn {
13
+ tickets: Ticket[];
14
+ isLoading: boolean;
15
+ isLoadingMore: boolean;
16
+ error: any;
17
+ hasMore: boolean;
18
+ totalCount: number;
19
+ loadMore: () => void;
20
+ refresh: () => Promise<void>;
21
+ }
22
+
23
+ export function useInfiniteTickets(): UseInfiniteTicketsReturn {
24
+ const getKey = (pageIndex: number, previousPageData: PaginatedTicketList | null) => {
25
+ // Reached the end
26
+ if (previousPageData && !previousPageData.has_next) return null;
27
+
28
+ // First page, no previous data
29
+ if (pageIndex === 0) return ['cfg-support-tickets-infinite', 1, PAGE_SIZE];
30
+
31
+ // Add the page number to the SWR key
32
+ return ['cfg-support-tickets-infinite', pageIndex + 1, PAGE_SIZE];
33
+ };
34
+
35
+ const fetcher = async ([, page, pageSize]: [string, number, number]) => {
36
+ return Fetchers.getSupportTicketsList(
37
+ { page, page_size: pageSize }
38
+ );
39
+ };
40
+
41
+ const {
42
+ data,
43
+ error,
44
+ isLoading,
45
+ isValidating,
46
+ size,
47
+ setSize,
48
+ mutate,
49
+ } = useSWRInfinite<PaginatedTicketList>(getKey, fetcher, {
50
+ revalidateFirstPage: false,
51
+ parallel: false,
52
+ });
53
+
54
+ // Flatten all pages into single array
55
+ const tickets: Ticket[] = data ? data.flatMap((page) => page.results) : [];
56
+
57
+ // Check if there are more pages
58
+ const hasMore = data && data[data.length - 1]?.has_next;
59
+
60
+ // Total count from last page
61
+ const totalCount = data && data[data.length - 1]?.count;
62
+
63
+ // Loading more state
64
+ const isLoadingMore = isValidating && data && typeof data[size - 1] !== 'undefined';
65
+
66
+ // Function to load next page
67
+ const loadMore = () => {
68
+ if (hasMore && !isLoadingMore) {
69
+ setSize(size + 1);
70
+ }
71
+ };
72
+
73
+ // Refresh all pages
74
+ const refresh = async () => {
75
+ await mutate();
76
+ };
77
+
78
+ return {
79
+ tickets,
80
+ isLoading,
81
+ isLoadingMore: isLoadingMore || false,
82
+ error,
83
+ hasMore: hasMore || false,
84
+ totalCount: totalCount || 0,
85
+ loadMore,
86
+ refresh,
87
+ };
88
+ }
@@ -0,0 +1,6 @@
1
+ export * from './SupportLayout';
2
+ export * from './context';
3
+ export * from './events';
4
+ export * from './types';
5
+ export * from './components';
6
+
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Support Layout Types
3
+ * UI state types for SupportLayout
4
+ */
5
+
6
+ // Note: API types (Ticket, Message) are exported from the generated API:
7
+ // import type { Ticket, Message } from '../../../api/generated/ext_support/_utils/schemas';
8
+
9
+ // UI State
10
+ export interface SupportUIState {
11
+ selectedTicketUuid: string | null;
12
+ isCreateDialogOpen: boolean;
13
+ viewMode: 'list' | 'grid';
14
+ }
15
+
16
+ // Form types
17
+ export interface TicketFormData {
18
+ subject: string;
19
+ message: string;
20
+ }
21
+
@@ -0,0 +1,14 @@
1
+ import { createConsola } from 'consola';
2
+
3
+ /**
4
+ * Logger for @djangocfg/ext-support
5
+ */
6
+ const isDevelopment = process.env.NODE_ENV === 'development';
7
+ const showLogs = isDevelopment;
8
+
9
+ export const logger = createConsola({
10
+ level: showLogs ? 4 : 1,
11
+ }).withTag('ext-support');
12
+
13
+ export const supportLogger = logger;
14
+ export default logger;