@djangocfg/ext-support 1.0.21 → 1.0.23
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/dist/config.cjs +11 -2
- package/dist/config.js +11 -2
- package/dist/hooks.cjs +399 -92
- package/dist/hooks.js +371 -64
- package/dist/i18n.cjs +266 -0
- package/dist/i18n.d.cts +112 -0
- package/dist/i18n.d.ts +112 -0
- package/dist/i18n.js +238 -0
- package/dist/index.cjs +399 -92
- package/dist/index.js +371 -64
- package/package.json +18 -9
- package/src/i18n/index.ts +26 -0
- package/src/i18n/locales/en.ts +76 -0
- package/src/i18n/locales/ko.ts +76 -0
- package/src/i18n/locales/ru.ts +76 -0
- package/src/i18n/types.ts +105 -0
- package/src/i18n/useSupportT.ts +60 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +16 -7
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +39 -17
- package/src/layouts/SupportLayout/components/MessageInput.tsx +17 -11
- package/src/layouts/SupportLayout/components/MessageList.tsx +29 -11
- package/src/layouts/SupportLayout/components/TicketCard.tsx +34 -14
- package/src/layouts/SupportLayout/components/TicketList.tsx +16 -6
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import { Send } from 'lucide-react';
|
|
9
|
-
import React, { useState } from 'react';
|
|
9
|
+
import React, { useState, useMemo } from 'react';
|
|
10
10
|
|
|
11
|
+
import { useSupportT } from '../../../i18n';
|
|
11
12
|
import { Button, Textarea } from '@djangocfg/ui-core';
|
|
12
13
|
import { useToast } from '@djangocfg/ui-core/hooks';
|
|
13
14
|
|
|
@@ -15,24 +16,33 @@ import { supportLogger } from '../../../utils/logger';
|
|
|
15
16
|
import { useSupportLayoutContext } from '../context';
|
|
16
17
|
|
|
17
18
|
export const MessageInput: React.FC = () => {
|
|
19
|
+
const st = useSupportT();
|
|
18
20
|
const { selectedTicket, sendMessage } = useSupportLayoutContext();
|
|
19
21
|
const { toast } = useToast();
|
|
20
22
|
const [message, setMessage] = useState('');
|
|
21
23
|
const [isSending, setIsSending] = useState(false);
|
|
22
24
|
|
|
25
|
+
const labels = useMemo(() => ({
|
|
26
|
+
placeholder: st('messageInput.placeholder'),
|
|
27
|
+
ticketClosed: st('messageInput.ticketClosed'),
|
|
28
|
+
ticketClosedDescription: st('messageInput.ticketClosedDescription'),
|
|
29
|
+
messageSent: st('messages.messageSent'),
|
|
30
|
+
messageSendFailed: st('messages.messageSendFailed'),
|
|
31
|
+
}), [st]);
|
|
32
|
+
|
|
23
33
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
24
34
|
e.preventDefault();
|
|
25
|
-
|
|
35
|
+
|
|
26
36
|
if (!message.trim() || !selectedTicket) return;
|
|
27
37
|
|
|
28
38
|
setIsSending(true);
|
|
29
39
|
try {
|
|
30
40
|
await sendMessage(message.trim());
|
|
31
41
|
setMessage('');
|
|
32
|
-
toast.success(
|
|
42
|
+
toast.success(labels.messageSent);
|
|
33
43
|
} catch (error) {
|
|
34
44
|
supportLogger.error('Failed to send message:', error);
|
|
35
|
-
toast.error(
|
|
45
|
+
toast.error(labels.messageSendFailed);
|
|
36
46
|
} finally {
|
|
37
47
|
setIsSending(false);
|
|
38
48
|
}
|
|
@@ -58,12 +68,8 @@ export const MessageInput: React.FC = () => {
|
|
|
58
68
|
value={message}
|
|
59
69
|
onChange={(e) => setMessage(e.target.value)}
|
|
60
70
|
onKeyDown={handleKeyDown}
|
|
61
|
-
placeholder={
|
|
62
|
-
|
|
63
|
-
? 'Type your message... (Shift+Enter for new line)'
|
|
64
|
-
: 'This ticket is closed'
|
|
65
|
-
}
|
|
66
|
-
className="min-h-[60px] max-h-[200px] transition-all duration-200
|
|
71
|
+
placeholder={canSendMessage ? labels.placeholder : labels.ticketClosed}
|
|
72
|
+
className="min-h-[60px] max-h-[200px] transition-all duration-200
|
|
67
73
|
focus:ring-2 focus:ring-primary/20"
|
|
68
74
|
disabled={!canSendMessage || isSending}
|
|
69
75
|
/>
|
|
@@ -79,7 +85,7 @@ export const MessageInput: React.FC = () => {
|
|
|
79
85
|
</div>
|
|
80
86
|
{!canSendMessage && (
|
|
81
87
|
<p className="text-xs text-muted-foreground mt-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
82
|
-
|
|
88
|
+
{labels.ticketClosedDescription}
|
|
83
89
|
</p>
|
|
84
90
|
)}
|
|
85
91
|
</form>
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { Headphones, Loader2, MessageSquare, User } from 'lucide-react';
|
|
9
9
|
import moment from 'moment';
|
|
10
|
-
import React, { useCallback, useEffect, useRef } from 'react';
|
|
10
|
+
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
|
11
11
|
|
|
12
12
|
import { useAuth } from '@djangocfg/api/auth';
|
|
13
|
+
import { useSupportT } from '../../../i18n';
|
|
13
14
|
import {
|
|
14
15
|
Avatar, AvatarFallback, AvatarImage, Button, Card, CardContent, ScrollArea, Skeleton
|
|
15
16
|
} from '@djangocfg/ui-core';
|
|
@@ -33,9 +34,13 @@ interface MessageBubbleProps {
|
|
|
33
34
|
message: Message;
|
|
34
35
|
isFromUser: boolean;
|
|
35
36
|
currentUser: any;
|
|
37
|
+
labels: {
|
|
38
|
+
supportTeam: string;
|
|
39
|
+
staff: string;
|
|
40
|
+
};
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, currentUser }) => {
|
|
43
|
+
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, currentUser, labels }) => {
|
|
39
44
|
const sender = message.sender;
|
|
40
45
|
|
|
41
46
|
// Pre-compute initials
|
|
@@ -56,7 +61,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, curr
|
|
|
56
61
|
{!isFromUser && (
|
|
57
62
|
<Avatar className="h-8 w-8 shrink-0">
|
|
58
63
|
{sender?.avatar ? (
|
|
59
|
-
<AvatarImage src={sender.avatar} alt={sender.display_username ||
|
|
64
|
+
<AvatarImage src={sender.avatar} alt={sender.display_username || labels.supportTeam} />
|
|
60
65
|
) : (
|
|
61
66
|
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
62
67
|
{sender?.is_staff ? (
|
|
@@ -76,8 +81,8 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, curr
|
|
|
76
81
|
{/* Sender name (for support messages) */}
|
|
77
82
|
{!isFromUser && sender && (
|
|
78
83
|
<span className="text-xs text-muted-foreground px-1">
|
|
79
|
-
{sender.display_username || sender.email ||
|
|
80
|
-
{sender.is_staff &&
|
|
84
|
+
{sender.display_username || sender.email || labels.supportTeam}
|
|
85
|
+
{sender.is_staff && ` (${labels.staff})`}
|
|
81
86
|
</span>
|
|
82
87
|
)}
|
|
83
88
|
|
|
@@ -117,9 +122,21 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, curr
|
|
|
117
122
|
};
|
|
118
123
|
|
|
119
124
|
export const MessageList: React.FC = () => {
|
|
125
|
+
const st = useSupportT();
|
|
120
126
|
const { selectedTicket } = useSupportLayoutContext();
|
|
121
127
|
const { user } = useAuth();
|
|
122
128
|
|
|
129
|
+
const labels = useMemo(() => ({
|
|
130
|
+
noTicketSelected: st('messageList.noTicketSelected'),
|
|
131
|
+
noTicketSelectedDescription: st('messageList.noTicketSelectedDescription'),
|
|
132
|
+
noMessages: st('messageList.noMessages'),
|
|
133
|
+
noMessagesDescription: st('messageList.noMessagesDescription'),
|
|
134
|
+
loadingOlder: st('messageList.loadingOlder'),
|
|
135
|
+
loadOlder: st('messageList.loadOlder'),
|
|
136
|
+
supportTeam: st('messageList.supportTeam'),
|
|
137
|
+
staff: st('messageList.staff'),
|
|
138
|
+
}), [st]);
|
|
139
|
+
|
|
123
140
|
const {
|
|
124
141
|
messages,
|
|
125
142
|
isLoading,
|
|
@@ -193,9 +210,9 @@ export const MessageList: React.FC = () => {
|
|
|
193
210
|
return (
|
|
194
211
|
<div className="flex flex-col items-center justify-center h-full p-8 text-center animate-in fade-in zoom-in-95 duration-300">
|
|
195
212
|
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
196
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
213
|
+
<h3 className="text-lg font-semibold mb-2">{labels.noTicketSelected}</h3>
|
|
197
214
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
198
|
-
|
|
215
|
+
{labels.noTicketSelectedDescription}
|
|
199
216
|
</p>
|
|
200
217
|
</div>
|
|
201
218
|
);
|
|
@@ -222,9 +239,9 @@ export const MessageList: React.FC = () => {
|
|
|
222
239
|
return (
|
|
223
240
|
<div className="flex flex-col items-center justify-center h-full p-8 text-center animate-in fade-in zoom-in-95 duration-300">
|
|
224
241
|
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
225
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
242
|
+
<h3 className="text-lg font-semibold mb-2">{labels.noMessages}</h3>
|
|
226
243
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
227
|
-
|
|
244
|
+
{labels.noMessagesDescription}
|
|
228
245
|
</p>
|
|
229
246
|
</div>
|
|
230
247
|
);
|
|
@@ -241,7 +258,7 @@ export const MessageList: React.FC = () => {
|
|
|
241
258
|
<div className="flex justify-center py-4">
|
|
242
259
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
243
260
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
244
|
-
<span className="text-sm">
|
|
261
|
+
<span className="text-sm">{labels.loadingOlder}</span>
|
|
245
262
|
</div>
|
|
246
263
|
</div>
|
|
247
264
|
)}
|
|
@@ -255,7 +272,7 @@ export const MessageList: React.FC = () => {
|
|
|
255
272
|
onClick={handleLoadMore}
|
|
256
273
|
className="text-xs"
|
|
257
274
|
>
|
|
258
|
-
|
|
275
|
+
{labels.loadOlder} ({totalCount > 0 ? `${messages.length}/${totalCount}` : ''})
|
|
259
276
|
</Button>
|
|
260
277
|
</div>
|
|
261
278
|
)}
|
|
@@ -304,6 +321,7 @@ export const MessageList: React.FC = () => {
|
|
|
304
321
|
message={message}
|
|
305
322
|
isFromUser={!!isFromUser}
|
|
306
323
|
currentUser={user}
|
|
324
|
+
labels={{ supportTeam: labels.supportTeam, staff: labels.staff }}
|
|
307
325
|
/>
|
|
308
326
|
</div>
|
|
309
327
|
</React.Fragment>
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { Clock, MessageSquare } from 'lucide-react';
|
|
9
9
|
import moment from 'moment';
|
|
10
|
-
import React from 'react';
|
|
10
|
+
import React, { useMemo, useCallback } from 'react';
|
|
11
11
|
|
|
12
|
+
import { useSupportT } from '../../../i18n';
|
|
12
13
|
import { Badge, Card, CardContent } from '@djangocfg/ui-core';
|
|
13
14
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
15
|
|
|
@@ -39,22 +40,41 @@ const getStatusBadgeVariant = (
|
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
-
|
|
43
|
+
export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onClick }) => {
|
|
44
|
+
const st = useSupportT();
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const labels = useMemo(() => ({
|
|
47
|
+
status: {
|
|
48
|
+
open: st('status.open'),
|
|
49
|
+
waiting_for_user: st('status.waitingForUser'),
|
|
50
|
+
waiting_for_admin: st('status.waitingForAdmin'),
|
|
51
|
+
resolved: st('status.resolved'),
|
|
52
|
+
closed: st('status.closed'),
|
|
53
|
+
},
|
|
54
|
+
time: {
|
|
55
|
+
justNow: st('time.justNow'),
|
|
56
|
+
minutesAgo: st('time.minutesAgo'),
|
|
57
|
+
hoursAgo: st('time.hoursAgo'),
|
|
58
|
+
daysAgo: st('time.daysAgo'),
|
|
59
|
+
},
|
|
60
|
+
}), [st]);
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
52
|
-
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
62
|
+
const formatRelativeTime = useCallback((date: string | null | undefined): string => {
|
|
63
|
+
if (!date) return 'N/A';
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
const m = moment.utc(date).local();
|
|
66
|
+
const now = moment();
|
|
67
|
+
const diffInSeconds = now.diff(m, 'seconds');
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
if (diffInSeconds < 60) return labels.time.justNow;
|
|
70
|
+
if (diffInSeconds < 3600) return labels.time.minutesAgo.replace('{count}', String(Math.floor(diffInSeconds / 60)));
|
|
71
|
+
if (diffInSeconds < 86400) return labels.time.hoursAgo.replace('{count}', String(Math.floor(diffInSeconds / 3600)));
|
|
72
|
+
if (diffInSeconds < 604800) return labels.time.daysAgo.replace('{count}', String(Math.floor(diffInSeconds / 86400)));
|
|
73
|
+
|
|
74
|
+
return m.format('MMM D, YYYY');
|
|
75
|
+
}, [labels.time]);
|
|
76
|
+
|
|
77
|
+
const statusLabel = labels.status[ticket.status as keyof typeof labels.status] || ticket.status || labels.status.open;
|
|
58
78
|
return (
|
|
59
79
|
<Card
|
|
60
80
|
className={cn(
|
|
@@ -81,7 +101,7 @@ export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onCl
|
|
|
81
101
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
82
102
|
<div className="flex items-center gap-3">
|
|
83
103
|
<Badge variant={getStatusBadgeVariant(ticket.status || 'open')} className="text-xs">
|
|
84
|
-
{
|
|
104
|
+
{statusLabel}
|
|
85
105
|
</Badge>
|
|
86
106
|
<div className="flex items-center gap-1">
|
|
87
107
|
<Clock className="h-3 w-3" />
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import { Loader2, MessageSquare } from 'lucide-react';
|
|
9
|
-
import React, { useEffect, useRef } from 'react';
|
|
9
|
+
import React, { useEffect, useRef, useMemo } from 'react';
|
|
10
10
|
|
|
11
|
+
import { useSupportT } from '../../../i18n';
|
|
11
12
|
import { Button, ScrollArea, Skeleton } from '@djangocfg/ui-core';
|
|
12
13
|
|
|
13
14
|
import { useSupportLayoutContext } from '../context';
|
|
@@ -16,6 +17,7 @@ import { useInfiniteTickets } from '../hooks';
|
|
|
16
17
|
import { TicketCard } from './TicketCard';
|
|
17
18
|
|
|
18
19
|
export const TicketList: React.FC = () => {
|
|
20
|
+
const st = useSupportT();
|
|
19
21
|
const { selectedTicket, selectTicket } = useSupportLayoutContext();
|
|
20
22
|
const {
|
|
21
23
|
tickets,
|
|
@@ -27,6 +29,14 @@ export const TicketList: React.FC = () => {
|
|
|
27
29
|
refresh
|
|
28
30
|
} = useInfiniteTickets();
|
|
29
31
|
|
|
32
|
+
const labels = useMemo(() => ({
|
|
33
|
+
noTickets: st('ticketList.noTickets'),
|
|
34
|
+
noTicketsDescription: st('ticketList.noTicketsDescription'),
|
|
35
|
+
loadingMore: st('ticketList.loadingMore'),
|
|
36
|
+
loadMore: st('ticketList.loadMore'),
|
|
37
|
+
allLoaded: st('ticketList.allLoaded'),
|
|
38
|
+
}), [st]);
|
|
39
|
+
|
|
30
40
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
31
41
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
32
42
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
@@ -90,9 +100,9 @@ export const TicketList: React.FC = () => {
|
|
|
90
100
|
return (
|
|
91
101
|
<div className="flex flex-col items-center justify-center h-full p-8 text-center animate-in fade-in zoom-in-95 duration-300">
|
|
92
102
|
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
93
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
103
|
+
<h3 className="text-lg font-semibold mb-2">{labels.noTickets}</h3>
|
|
94
104
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
95
|
-
|
|
105
|
+
{labels.noTicketsDescription}
|
|
96
106
|
</p>
|
|
97
107
|
</div>
|
|
98
108
|
);
|
|
@@ -123,7 +133,7 @@ export const TicketList: React.FC = () => {
|
|
|
123
133
|
<div className="flex justify-center py-4">
|
|
124
134
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
125
135
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
126
|
-
<span className="text-sm">
|
|
136
|
+
<span className="text-sm">{labels.loadingMore}</span>
|
|
127
137
|
</div>
|
|
128
138
|
</div>
|
|
129
139
|
)}
|
|
@@ -137,7 +147,7 @@ export const TicketList: React.FC = () => {
|
|
|
137
147
|
onClick={loadMore}
|
|
138
148
|
className="text-xs"
|
|
139
149
|
>
|
|
140
|
-
|
|
150
|
+
{labels.loadMore} ({totalCount > 0 ? `${tickets.length}/${totalCount}` : ''})
|
|
141
151
|
</Button>
|
|
142
152
|
</div>
|
|
143
153
|
)}
|
|
@@ -145,7 +155,7 @@ export const TicketList: React.FC = () => {
|
|
|
145
155
|
{/* End message */}
|
|
146
156
|
{!hasMore && tickets.length > 0 && (
|
|
147
157
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
|
148
|
-
|
|
158
|
+
{labels.allLoaded.replace('{count}', String(totalCount))}
|
|
149
159
|
</div>
|
|
150
160
|
)}
|
|
151
161
|
</div>
|