@djangocfg/layouts 1.0.1

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/package.json +86 -0
  4. package/src/auth/README.md +962 -0
  5. package/src/auth/context/AuthContext.tsx +458 -0
  6. package/src/auth/context/index.ts +2 -0
  7. package/src/auth/context/types.ts +63 -0
  8. package/src/auth/hooks/index.ts +6 -0
  9. package/src/auth/hooks/useAuthForm.ts +329 -0
  10. package/src/auth/hooks/useAuthGuard.ts +23 -0
  11. package/src/auth/hooks/useAuthRedirect.ts +51 -0
  12. package/src/auth/hooks/useAutoAuth.ts +42 -0
  13. package/src/auth/hooks/useLocalStorage.ts +211 -0
  14. package/src/auth/hooks/useSessionStorage.ts +186 -0
  15. package/src/auth/index.ts +10 -0
  16. package/src/auth/middlewares/index.ts +1 -0
  17. package/src/auth/middlewares/proxy.ts +24 -0
  18. package/src/auth/server.ts +6 -0
  19. package/src/auth/utils/errors.ts +34 -0
  20. package/src/auth/utils/index.ts +2 -0
  21. package/src/auth/utils/validation.ts +14 -0
  22. package/src/index.ts +15 -0
  23. package/src/layouts/AppLayout/AppLayout.tsx +123 -0
  24. package/src/layouts/AppLayout/README.md +204 -0
  25. package/src/layouts/AppLayout/SUMMARY.md +240 -0
  26. package/src/layouts/AppLayout/USAGE.md +312 -0
  27. package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
  28. package/src/layouts/AppLayout/components/Seo.tsx +87 -0
  29. package/src/layouts/AppLayout/components/index.ts +6 -0
  30. package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
  31. package/src/layouts/AppLayout/context/index.ts +5 -0
  32. package/src/layouts/AppLayout/hooks/index.ts +6 -0
  33. package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
  34. package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
  35. package/src/layouts/AppLayout/index.ts +31 -0
  36. package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
  37. package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
  38. package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
  39. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
  40. package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
  41. package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
  42. package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
  43. package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
  44. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
  45. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
  46. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
  47. package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
  48. package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
  49. package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
  50. package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
  51. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
  52. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
  53. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
  54. package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
  55. package/src/layouts/AppLayout/layouts/index.ts +7 -0
  56. package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
  57. package/src/layouts/AppLayout/providers/index.ts +5 -0
  58. package/src/layouts/AppLayout/types/config.ts +40 -0
  59. package/src/layouts/AppLayout/types/index.ts +10 -0
  60. package/src/layouts/AppLayout/types/layout.ts +47 -0
  61. package/src/layouts/AppLayout/types/navigation.ts +41 -0
  62. package/src/layouts/AppLayout/types/routes.ts +45 -0
  63. package/src/layouts/AppLayout/utils/index.ts +5 -0
  64. package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
  65. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
  66. package/src/layouts/PaymentsLayout/README.md +133 -0
  67. package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
  68. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
  69. package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
  70. package/src/layouts/PaymentsLayout/components/index.ts +4 -0
  71. package/src/layouts/PaymentsLayout/events.ts +106 -0
  72. package/src/layouts/PaymentsLayout/index.ts +20 -0
  73. package/src/layouts/PaymentsLayout/types.ts +19 -0
  74. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
  75. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
  76. package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
  77. package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
  78. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
  79. package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
  80. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
  81. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
  82. package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
  83. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
  84. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
  85. package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
  86. package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
  87. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
  88. package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
  89. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
  90. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
  91. package/src/layouts/ProfileLayout/components/index.ts +3 -0
  92. package/src/layouts/ProfileLayout/index.ts +3 -0
  93. package/src/layouts/SupportLayout/README.md +91 -0
  94. package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
  95. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
  96. package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
  97. package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
  98. package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
  99. package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
  100. package/src/layouts/SupportLayout/components/index.ts +6 -0
  101. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
  102. package/src/layouts/SupportLayout/context/index.ts +2 -0
  103. package/src/layouts/SupportLayout/events.ts +31 -0
  104. package/src/layouts/SupportLayout/hooks/index.ts +2 -0
  105. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
  106. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
  107. package/src/layouts/SupportLayout/index.ts +6 -0
  108. package/src/layouts/SupportLayout/types.ts +23 -0
  109. package/src/layouts/index.ts +9 -0
  110. package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
  111. package/src/snippets/AuthDialog/events.ts +21 -0
  112. package/src/snippets/AuthDialog/index.ts +3 -0
  113. package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
  114. package/src/snippets/Breadcrumbs.tsx +80 -0
  115. package/src/snippets/Chat/ChatUIContext.tsx +110 -0
  116. package/src/snippets/Chat/ChatWidget.tsx +476 -0
  117. package/src/snippets/Chat/README.md +122 -0
  118. package/src/snippets/Chat/components/MessageInput.tsx +124 -0
  119. package/src/snippets/Chat/components/MessageList.tsx +168 -0
  120. package/src/snippets/Chat/components/SessionList.tsx +192 -0
  121. package/src/snippets/Chat/components/index.ts +9 -0
  122. package/src/snippets/Chat/hooks/index.ts +6 -0
  123. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
  124. package/src/snippets/Chat/index.tsx +44 -0
  125. package/src/snippets/Chat/types.ts +79 -0
  126. package/src/snippets/VideoPlayer/README.md +203 -0
  127. package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
  128. package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
  129. package/src/snippets/VideoPlayer/index.ts +8 -0
  130. package/src/snippets/VideoPlayer/types.ts +61 -0
  131. package/src/snippets/index.ts +10 -0
  132. package/src/styles/dashboard.css +41 -0
  133. package/src/styles/index.css +20 -0
  134. package/src/styles/sources.css +6 -0
  135. package/src/types/index.ts +1 -0
  136. package/src/types/pageConfig.ts +103 -0
  137. package/src/utils/index.ts +6 -0
  138. package/src/utils/logger.ts +57 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Ticket List Component
3
+ * Displays a list of support tickets with infinite scroll
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { useEffect, useRef } from 'react';
9
+ import { ScrollArea, Skeleton, Button } from '@djangocfg/ui';
10
+ import { TicketCard } from './TicketCard';
11
+ import { useSupportLayoutContext } from '../context';
12
+ import { MessageSquare, Loader2 } from 'lucide-react';
13
+ import { useInfiniteTickets } from '../hooks';
14
+ import { SUPPORT_LAYOUT_EVENTS } from '../events';
15
+
16
+ export const TicketList: React.FC = () => {
17
+ const { selectedTicket, selectTicket } = useSupportLayoutContext();
18
+ const {
19
+ tickets,
20
+ isLoading,
21
+ isLoadingMore,
22
+ hasMore,
23
+ loadMore,
24
+ totalCount,
25
+ refresh
26
+ } = useInfiniteTickets();
27
+
28
+ const scrollRef = useRef<HTMLDivElement>(null);
29
+ const observerRef = useRef<IntersectionObserver | null>(null);
30
+ const loadMoreRef = useRef<HTMLDivElement>(null);
31
+
32
+ // Listen for ticket creation events to refresh the list
33
+ useEffect(() => {
34
+ const handleTicketCreated = () => {
35
+ // Refresh the tickets list when a new ticket is created
36
+ refresh();
37
+ };
38
+
39
+ window.addEventListener(SUPPORT_LAYOUT_EVENTS.TICKET_CREATED, handleTicketCreated);
40
+
41
+ return () => {
42
+ window.removeEventListener(SUPPORT_LAYOUT_EVENTS.TICKET_CREATED, handleTicketCreated);
43
+ };
44
+ }, [refresh]);
45
+
46
+ // Set up intersection observer for infinite scroll
47
+ useEffect(() => {
48
+ if (observerRef.current) {
49
+ observerRef.current.disconnect();
50
+ }
51
+
52
+ observerRef.current = new IntersectionObserver(
53
+ (entries) => {
54
+ if (entries[0]?.isIntersecting && hasMore && !isLoadingMore) {
55
+ loadMore();
56
+ }
57
+ },
58
+ { threshold: 0.1 }
59
+ );
60
+
61
+ if (loadMoreRef.current) {
62
+ observerRef.current.observe(loadMoreRef.current);
63
+ }
64
+
65
+ return () => {
66
+ if (observerRef.current) {
67
+ observerRef.current.disconnect();
68
+ }
69
+ };
70
+ }, [hasMore, isLoadingMore, loadMore]);
71
+
72
+ if (isLoading) {
73
+ return (
74
+ <div className="p-4 space-y-2">
75
+ {[1, 2, 3, 4, 5].map((i) => (
76
+ <Skeleton
77
+ key={i}
78
+ className="h-24 w-full animate-pulse"
79
+ style={{ animationDelay: `${i * 100}ms` }}
80
+ />
81
+ ))}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ if (!tickets || tickets.length === 0) {
87
+ return (
88
+ <div className="flex flex-col items-center justify-center h-full p-8 text-center animate-in fade-in zoom-in-95 duration-300">
89
+ <MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
90
+ <h3 className="text-lg font-semibold mb-2">No tickets yet</h3>
91
+ <p className="text-sm text-muted-foreground max-w-sm">
92
+ Create your first support ticket to get help from our team
93
+ </p>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ return (
99
+ <ScrollArea className="h-full" ref={scrollRef}>
100
+ <div className="p-4 space-y-2">
101
+ {tickets.map((ticket, index) => (
102
+ <div
103
+ key={ticket.uuid}
104
+ className="animate-in fade-in slide-in-from-left-2 duration-300"
105
+ style={{ animationDelay: `${Math.min(index, 10) * 50}ms` }}
106
+ >
107
+ <TicketCard
108
+ ticket={ticket}
109
+ isSelected={selectedTicket?.uuid === ticket.uuid}
110
+ onClick={() => selectTicket(ticket)}
111
+ />
112
+ </div>
113
+ ))}
114
+
115
+ {/* Load more trigger */}
116
+ <div ref={loadMoreRef} className="h-2" />
117
+
118
+ {/* Loading indicator */}
119
+ {isLoadingMore && (
120
+ <div className="flex justify-center py-4">
121
+ <div className="flex items-center gap-2 text-muted-foreground">
122
+ <Loader2 className="h-4 w-4 animate-spin" />
123
+ <span className="text-sm">Loading more tickets...</span>
124
+ </div>
125
+ </div>
126
+ )}
127
+
128
+ {/* Manual load button if needed */}
129
+ {hasMore && !isLoadingMore && (
130
+ <div className="flex justify-center pt-2 pb-4">
131
+ <Button
132
+ variant="outline"
133
+ size="sm"
134
+ onClick={loadMore}
135
+ className="text-xs"
136
+ >
137
+ Load more ({totalCount > 0 ? `${tickets.length}/${totalCount}` : ''})
138
+ </Button>
139
+ </div>
140
+ )}
141
+
142
+ {/* End message */}
143
+ {!hasMore && tickets.length > 0 && (
144
+ <div className="text-center py-4 text-sm text-muted-foreground">
145
+ All {totalCount} tickets loaded
146
+ </div>
147
+ )}
148
+ </div>
149
+ </ScrollArea>
150
+ );
151
+ };
152
+
@@ -0,0 +1,6 @@
1
+ export * from './TicketCard';
2
+ export * from './TicketList';
3
+ export * from './MessageList';
4
+ export * from './MessageInput';
5
+ export * from './CreateTicketDialog';
6
+
@@ -0,0 +1,260 @@
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 } from '@djangocfg/api/cfg/contexts';
10
+ import type { Message, MessageCreateRequest } from '@djangocfg/api/cfg/generated/schemas';
11
+ import { supportLogger } from '../../../utils/logger';
12
+ import { useAuth } from '../../../auth';
13
+ import { SUPPORT_LAYOUT_EVENTS } from '../events';
14
+ import { useInfiniteMessages } from '../hooks';
15
+ import type { SupportUIState, TicketFormData } from '../types';
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────
18
+ // Context Type
19
+ // ─────────────────────────────────────────────────────────────────────────
20
+
21
+ export interface SupportLayoutContextValue {
22
+ // From API context
23
+ tickets: Ticket[] | undefined;
24
+ isLoadingTickets: boolean;
25
+ ticketsError: Error | undefined;
26
+
27
+ // Selected ticket data
28
+ selectedTicket: Ticket | undefined;
29
+ selectedTicketMessages: Message[] | undefined;
30
+ isLoadingMessages: boolean;
31
+
32
+ // Infinite scroll for messages
33
+ isLoadingMoreMessages: boolean;
34
+ hasMoreMessages: boolean;
35
+ totalMessagesCount: number;
36
+ loadMoreMessages: () => void;
37
+
38
+ // UI state
39
+ uiState: SupportUIState;
40
+
41
+ // Actions
42
+ selectTicket: (ticket: Ticket | null) => void;
43
+ createTicket: (data: TicketFormData) => Promise<void>;
44
+ sendMessage: (message: string) => Promise<void>;
45
+ refreshTickets: () => Promise<void>;
46
+ refreshMessages: () => Promise<void>;
47
+
48
+ // Dialog actions
49
+ openCreateDialog: () => void;
50
+ closeCreateDialog: () => void;
51
+
52
+ // Utilities
53
+ getUnreadCount: () => number;
54
+ }
55
+
56
+ // ─────────────────────────────────────────────────────────────────────────
57
+ // Context
58
+ // ─────────────────────────────────────────────────────────────────────────
59
+
60
+ const SupportLayoutContext = createContext<SupportLayoutContextValue | undefined>(undefined);
61
+
62
+ // ─────────────────────────────────────────────────────────────────────────
63
+ // Provider
64
+ // ─────────────────────────────────────────────────────────────────────────
65
+
66
+ interface SupportLayoutProviderProps {
67
+ children: ReactNode;
68
+ }
69
+
70
+ export function SupportLayoutProvider({ children }: SupportLayoutProviderProps) {
71
+ const support = useSupportContext();
72
+ const { user } = useAuth();
73
+
74
+ // UI state
75
+ const [uiState, setUIState] = useState<SupportUIState>({
76
+ selectedTicketUuid: null,
77
+ isCreateDialogOpen: false,
78
+ viewMode: 'list',
79
+ });
80
+
81
+ // Selected ticket
82
+ const selectedTicket = support.tickets?.find(t => t.uuid === uiState.selectedTicketUuid);
83
+
84
+ // Use infinite scroll hook for messages
85
+ const {
86
+ messages: selectedTicketMessages,
87
+ isLoading: isLoadingMessages,
88
+ isLoadingMore: isLoadingMoreMessages,
89
+ hasMore: hasMoreMessages,
90
+ totalCount: totalMessagesCount,
91
+ loadMore: loadMoreMessages,
92
+ refresh: refreshMessages,
93
+ addMessage: addMessageOptimistically,
94
+ } = useInfiniteMessages(selectedTicket?.uuid || null);
95
+
96
+ // Select ticket
97
+ const selectTicket = useCallback(async (ticket: Ticket | null) => {
98
+ setUIState(prev => ({ ...prev, selectedTicketUuid: ticket?.uuid || null }));
99
+
100
+ if (ticket?.uuid) {
101
+ // The messages will be loaded automatically by the useInfiniteMessages hook
102
+ // when the ticket UUID changes
103
+
104
+ // Dispatch event
105
+ window.dispatchEvent(
106
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.TICKET_SELECTED, { detail: { ticket } })
107
+ );
108
+ }
109
+ }, []);
110
+
111
+ // Create ticket
112
+ const createTicket = useCallback(async (data: TicketFormData) => {
113
+ if (!user?.id) {
114
+ throw new Error('User must be authenticated to create tickets');
115
+ }
116
+
117
+ const ticket = await support.createTicket({
118
+ user: user.id,
119
+ subject: data.subject,
120
+ });
121
+
122
+ // Send initial message if provided
123
+ if (ticket.uuid && data.message) {
124
+ await support.createMessage(ticket.uuid, {
125
+ text: data.message,
126
+ });
127
+ }
128
+
129
+ // Close dialog first for better UX
130
+ setUIState(prev => ({ ...prev, isCreateDialogOpen: false }));
131
+
132
+ // Refresh tickets list to show the new ticket
133
+ await support.refreshTickets();
134
+
135
+ // Dispatch event
136
+ window.dispatchEvent(
137
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.TICKET_CREATED, { detail: { ticket } })
138
+ );
139
+
140
+ // Auto-select the newly created ticket after refresh
141
+ // Use the ticket UUID directly to ensure selection works even if the ticket object changes
142
+ setUIState(prev => ({ ...prev, selectedTicketUuid: ticket.uuid }));
143
+
144
+ // Dispatch selection event
145
+ window.dispatchEvent(
146
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.TICKET_SELECTED, { detail: { ticket } })
147
+ );
148
+ }, [support, user]);
149
+
150
+ // Send message
151
+ const sendMessage = useCallback(async (message: string) => {
152
+ if (!selectedTicket?.uuid) return;
153
+
154
+ // Create the message object
155
+ const messageData: MessageCreateRequest = {
156
+ text: message,
157
+ };
158
+
159
+ // Send message to backend
160
+ const newMessage = await support.createMessage(selectedTicket.uuid, messageData);
161
+
162
+ // Add message optimistically to the UI (it will be replaced when refreshing)
163
+ if (newMessage) {
164
+ const fullMessage: Message = {
165
+ uuid: newMessage.uuid || `temp-${Date.now()}`,
166
+ ticket: selectedTicket.uuid,
167
+ sender: {
168
+ id: user?.id || 0,
169
+ display_username: user?.display_username || '',
170
+ email: user?.email || '',
171
+ avatar: user?.avatar || null,
172
+ initials: user?.initials || '',
173
+ is_staff: user?.is_staff || false,
174
+ is_superuser: user?.is_superuser || false,
175
+ },
176
+ is_from_author: true,
177
+ text: message,
178
+ created_at: new Date().toISOString(),
179
+ };
180
+
181
+ addMessageOptimistically(fullMessage);
182
+ }
183
+
184
+ // Refresh messages to get the latest state
185
+ await refreshMessages();
186
+
187
+ // Dispatch event
188
+ window.dispatchEvent(
189
+ new CustomEvent(SUPPORT_LAYOUT_EVENTS.MESSAGE_SENT, { detail: { message: newMessage } })
190
+ );
191
+ }, [selectedTicket, support, user, addMessageOptimistically, refreshMessages]);
192
+
193
+ // Dialog actions
194
+ const openCreateDialog = useCallback(() => {
195
+ setUIState(prev => ({ ...prev, isCreateDialogOpen: true }));
196
+ }, []);
197
+
198
+ const closeCreateDialog = useCallback(() => {
199
+ setUIState(prev => ({ ...prev, isCreateDialogOpen: false }));
200
+ }, []);
201
+
202
+ // Get unread count
203
+ const getUnreadCount = useCallback(() => {
204
+ return support.tickets?.reduce((count, ticket) => count + (ticket.unanswered_messages_count || 0), 0) || 0;
205
+ }, [support.tickets]);
206
+
207
+ // Event listeners
208
+ useEffect(() => {
209
+ const handleOpenDialog = () => openCreateDialog();
210
+ const handleCloseDialog = () => closeCreateDialog();
211
+
212
+ window.addEventListener(SUPPORT_LAYOUT_EVENTS.OPEN_CREATE_DIALOG, handleOpenDialog);
213
+ window.addEventListener(SUPPORT_LAYOUT_EVENTS.CLOSE_CREATE_DIALOG, handleCloseDialog);
214
+
215
+ return () => {
216
+ window.removeEventListener(SUPPORT_LAYOUT_EVENTS.OPEN_CREATE_DIALOG, handleOpenDialog);
217
+ window.removeEventListener(SUPPORT_LAYOUT_EVENTS.CLOSE_CREATE_DIALOG, handleCloseDialog);
218
+ };
219
+ }, [openCreateDialog, closeCreateDialog]);
220
+
221
+ const value: SupportLayoutContextValue = {
222
+ tickets: support.tickets,
223
+ isLoadingTickets: support.isLoadingTickets,
224
+ ticketsError: support.ticketsError,
225
+ selectedTicket,
226
+ selectedTicketMessages,
227
+ isLoadingMessages,
228
+ isLoadingMoreMessages,
229
+ hasMoreMessages,
230
+ totalMessagesCount,
231
+ loadMoreMessages,
232
+ uiState,
233
+ selectTicket,
234
+ createTicket,
235
+ sendMessage,
236
+ refreshTickets: support.refreshTickets,
237
+ refreshMessages,
238
+ openCreateDialog,
239
+ closeCreateDialog,
240
+ getUnreadCount,
241
+ };
242
+
243
+ return (
244
+ <SupportLayoutContext.Provider value={value}>
245
+ {children}
246
+ </SupportLayoutContext.Provider>
247
+ );
248
+ }
249
+
250
+ // ─────────────────────────────────────────────────────────────────────────
251
+ // Hook
252
+ // ─────────────────────────────────────────────────────────────────────────
253
+
254
+ export function useSupportLayoutContext(): SupportLayoutContextValue {
255
+ const context = useContext(SupportLayoutContext);
256
+ if (!context) {
257
+ throw new Error('useSupportLayoutContext must be used within SupportLayoutProvider');
258
+ }
259
+ return context;
260
+ }
@@ -0,0 +1,2 @@
1
+ export * from './SupportLayoutContext';
2
+
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Support Layout Events
3
+ * Event system for SupportLayout
4
+ */
5
+
6
+ export const SUPPORT_LAYOUT_EVENTS = {
7
+ // Dialog events
8
+ OPEN_CREATE_DIALOG: 'support-layout:open-create-dialog',
9
+ CLOSE_CREATE_DIALOG: 'support-layout:close-create-dialog',
10
+
11
+ // Ticket events
12
+ TICKET_SELECTED: 'support-layout:ticket-selected',
13
+ TICKET_CREATED: 'support-layout:ticket-created',
14
+
15
+ // Message events
16
+ MESSAGE_SENT: 'support-layout:message-sent',
17
+ } as const;
18
+
19
+ // Event publishers
20
+ export const openCreateTicketDialog = () => {
21
+ if (typeof window !== 'undefined') {
22
+ window.dispatchEvent(new CustomEvent(SUPPORT_LAYOUT_EVENTS.OPEN_CREATE_DIALOG));
23
+ }
24
+ };
25
+
26
+ export const closeCreateTicketDialog = () => {
27
+ if (typeof window !== 'undefined') {
28
+ window.dispatchEvent(new CustomEvent(SUPPORT_LAYOUT_EVENTS.CLOSE_CREATE_DIALOG));
29
+ }
30
+ };
31
+
@@ -0,0 +1,2 @@
1
+ export * from './useInfiniteTickets';
2
+ export * from './useInfiniteMessages';
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Hook for infinite scroll support messages
3
+ * Uses SWR Infinite for pagination
4
+ */
5
+
6
+ import useSWRInfinite from 'swr/infinite';
7
+ import { api } from '@djangocfg/api';
8
+ import type { API } from '@djangocfg/api/cfg/generated';
9
+ import { getSupportTicketsMessagesList } from '@djangocfg/api/cfg/generated/fetchers';
10
+ import type { PaginatedMessageList, Message } from '@djangocfg/api/cfg/generated/schemas';
11
+
12
+ const PAGE_SIZE = 20;
13
+
14
+ interface UseInfiniteMessagesReturn {
15
+ messages: Message[];
16
+ isLoading: boolean;
17
+ isLoadingMore: boolean;
18
+ error: any;
19
+ hasMore: boolean;
20
+ totalCount: number;
21
+ loadMore: () => void;
22
+ refresh: () => Promise<void>;
23
+ addMessage: (message: Message) => void;
24
+ }
25
+
26
+ export function useInfiniteMessages(ticketUuid: string | null): UseInfiniteMessagesReturn {
27
+ const getKey = (pageIndex: number, previousPageData: PaginatedMessageList | null) => {
28
+ // No ticket selected
29
+ if (!ticketUuid) return null;
30
+
31
+ // Reached the end
32
+ if (previousPageData && !previousPageData.has_next) return null;
33
+
34
+ // First page, no previous data
35
+ if (pageIndex === 0) return ['cfg-support-messages-infinite', ticketUuid, 1, PAGE_SIZE];
36
+
37
+ // Add the page number to the SWR key
38
+ return ['cfg-support-messages-infinite', ticketUuid, pageIndex + 1, PAGE_SIZE];
39
+ };
40
+
41
+ const fetcher = async ([, ticket_uuid, page, pageSize]: [string, string, number, number]) => {
42
+ return getSupportTicketsMessagesList(
43
+ ticket_uuid,
44
+ { page, page_size: pageSize },
45
+ api as unknown as API
46
+ );
47
+ };
48
+
49
+ const {
50
+ data,
51
+ error,
52
+ isLoading,
53
+ isValidating,
54
+ size,
55
+ setSize,
56
+ mutate,
57
+ } = useSWRInfinite<PaginatedMessageList>(getKey, fetcher, {
58
+ revalidateFirstPage: false,
59
+ parallel: false,
60
+ });
61
+
62
+ // Flatten all pages into single array (reversed for chat display)
63
+ const messages: Message[] = data ? data.flatMap((page) => page.results) : [];
64
+
65
+ // Check if there are more pages
66
+ const hasMore = data && data[data.length - 1]?.has_next;
67
+
68
+ // Total count from last page
69
+ const totalCount = data && data[data.length - 1]?.count;
70
+
71
+ // Loading more state
72
+ const isLoadingMore = !!(isValidating && data && typeof data[size - 1] !== 'undefined');
73
+
74
+ // Function to load next page
75
+ const loadMore = () => {
76
+ if (hasMore && !isLoadingMore) {
77
+ setSize(size + 1);
78
+ }
79
+ };
80
+
81
+ // Refresh all pages
82
+ const refresh = async () => {
83
+ await mutate();
84
+ };
85
+
86
+ // Add new message optimistically
87
+ const addMessage = (message: Message) => {
88
+ if (!data || !data[0]) return;
89
+
90
+ // Add the message to the first page
91
+ const newData = [...data];
92
+ const firstPage = newData[0];
93
+
94
+ if (firstPage) {
95
+ newData[0] = {
96
+ ...firstPage,
97
+ results: [message, ...firstPage.results],
98
+ count: firstPage.count + 1,
99
+ page: firstPage.page || 1,
100
+ pages: firstPage.pages || 1,
101
+ };
102
+ }
103
+
104
+ mutate(newData, false);
105
+ };
106
+
107
+ return {
108
+ messages,
109
+ isLoading,
110
+ isLoadingMore: isLoadingMore || false,
111
+ error,
112
+ hasMore: hasMore || false,
113
+ totalCount: totalCount || 0,
114
+ loadMore,
115
+ refresh,
116
+ addMessage,
117
+ };
118
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Hook for infinite scroll support tickets
3
+ * Uses SWR Infinite for pagination
4
+ */
5
+
6
+ import useSWRInfinite from 'swr/infinite';
7
+ import { api } from '@djangocfg/api';
8
+ import type { API } from '@djangocfg/api/cfg/generated';
9
+ import { getSupportTicketsList } from '@djangocfg/api/cfg/generated/fetchers';
10
+ import type { PaginatedTicketList, Ticket } from '@djangocfg/api/cfg/generated/schemas';
11
+
12
+ const PAGE_SIZE = 20;
13
+
14
+ interface UseInfiniteTicketsReturn {
15
+ tickets: Ticket[];
16
+ isLoading: boolean;
17
+ isLoadingMore: boolean;
18
+ error: any;
19
+ hasMore: boolean;
20
+ totalCount: number;
21
+ loadMore: () => void;
22
+ refresh: () => Promise<void>;
23
+ }
24
+
25
+ export function useInfiniteTickets(): UseInfiniteTicketsReturn {
26
+ const getKey = (pageIndex: number, previousPageData: PaginatedTicketList | null) => {
27
+ // Reached the end
28
+ if (previousPageData && !previousPageData.has_next) return null;
29
+
30
+ // First page, no previous data
31
+ if (pageIndex === 0) return ['cfg-support-tickets-infinite', 1, PAGE_SIZE];
32
+
33
+ // Add the page number to the SWR key
34
+ return ['cfg-support-tickets-infinite', pageIndex + 1, PAGE_SIZE];
35
+ };
36
+
37
+ const fetcher = async ([, page, pageSize]: [string, number, number]) => {
38
+ return getSupportTicketsList(
39
+ { page, page_size: pageSize },
40
+ api as unknown as API
41
+ );
42
+ };
43
+
44
+ const {
45
+ data,
46
+ error,
47
+ isLoading,
48
+ isValidating,
49
+ size,
50
+ setSize,
51
+ mutate,
52
+ } = useSWRInfinite<PaginatedTicketList>(getKey, fetcher, {
53
+ revalidateFirstPage: false,
54
+ parallel: false,
55
+ });
56
+
57
+ // Flatten all pages into single array
58
+ const tickets: Ticket[] = data ? data.flatMap((page) => page.results) : [];
59
+
60
+ // Check if there are more pages
61
+ const hasMore = data && data[data.length - 1]?.has_next;
62
+
63
+ // Total count from last page
64
+ const totalCount = data && data[data.length - 1]?.count;
65
+
66
+ // Loading more state
67
+ const isLoadingMore = isValidating && data && typeof data[size - 1] !== 'undefined';
68
+
69
+ // Function to load next page
70
+ const loadMore = () => {
71
+ if (hasMore && !isLoadingMore) {
72
+ setSize(size + 1);
73
+ }
74
+ };
75
+
76
+ // Refresh all pages
77
+ const refresh = async () => {
78
+ await mutate();
79
+ };
80
+
81
+ return {
82
+ tickets,
83
+ isLoading,
84
+ isLoadingMore: isLoadingMore || false,
85
+ error,
86
+ hasMore: hasMore || false,
87
+ totalCount: totalCount || 0,
88
+ loadMore,
89
+ refresh,
90
+ };
91
+ }
@@ -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,23 @@
1
+ /**
2
+ * Support Layout Types
3
+ * Types for SupportLayout - combines API types with UI state
4
+ */
5
+
6
+ import type { Ticket, Message } from '@djangocfg/api/cfg/contexts';
7
+
8
+ // Re-export API types
9
+ export type { Ticket, Message };
10
+
11
+ // UI State
12
+ export interface SupportUIState {
13
+ selectedTicketUuid: string | null;
14
+ isCreateDialogOpen: boolean;
15
+ viewMode: 'list' | 'grid';
16
+ }
17
+
18
+ // Form types
19
+ export interface TicketFormData {
20
+ subject: string;
21
+ message: string;
22
+ }
23
+