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