@djangocfg/ext-support 1.0.21 → 1.0.22
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 +8 -1
- package/dist/config.js +8 -1
- package/dist/hooks.cjs +389 -91
- package/dist/hooks.js +361 -63
- package/dist/i18n.cjs +246 -0
- package/dist/i18n.d.cts +99 -0
- package/dist/i18n.d.ts +99 -0
- package/dist/i18n.js +220 -0
- package/dist/index.cjs +389 -91
- package/dist/index.js +361 -63
- package/package.json +15 -8
- package/src/i18n/index.ts +23 -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 +99 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +19 -7
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +42 -17
- package/src/layouts/SupportLayout/components/MessageInput.tsx +20 -11
- package/src/layouts/SupportLayout/components/MessageList.tsx +32 -11
- package/src/layouts/SupportLayout/components/TicketCard.tsx +37 -14
- package/src/layouts/SupportLayout/components/TicketList.tsx +19 -6
|
@@ -7,9 +7,12 @@
|
|
|
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 { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
|
|
14
|
+
import { useT } from '@djangocfg/i18n';
|
|
15
|
+
import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
|
|
13
16
|
import {
|
|
14
17
|
Avatar, AvatarFallback, AvatarImage, Button, Card, CardContent, ScrollArea, Skeleton
|
|
15
18
|
} from '@djangocfg/ui-core';
|
|
@@ -33,9 +36,13 @@ interface MessageBubbleProps {
|
|
|
33
36
|
message: Message;
|
|
34
37
|
isFromUser: boolean;
|
|
35
38
|
currentUser: any;
|
|
39
|
+
labels: {
|
|
40
|
+
supportTeam: string;
|
|
41
|
+
staff: string;
|
|
42
|
+
};
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, currentUser }) => {
|
|
45
|
+
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, currentUser, labels }) => {
|
|
39
46
|
const sender = message.sender;
|
|
40
47
|
|
|
41
48
|
// Pre-compute initials
|
|
@@ -56,7 +63,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, curr
|
|
|
56
63
|
{!isFromUser && (
|
|
57
64
|
<Avatar className="h-8 w-8 shrink-0">
|
|
58
65
|
{sender?.avatar ? (
|
|
59
|
-
<AvatarImage src={sender.avatar} alt={sender.display_username ||
|
|
66
|
+
<AvatarImage src={sender.avatar} alt={sender.display_username || labels.supportTeam} />
|
|
60
67
|
) : (
|
|
61
68
|
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
62
69
|
{sender?.is_staff ? (
|
|
@@ -76,8 +83,8 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, curr
|
|
|
76
83
|
{/* Sender name (for support messages) */}
|
|
77
84
|
{!isFromUser && sender && (
|
|
78
85
|
<span className="text-xs text-muted-foreground px-1">
|
|
79
|
-
{sender.display_username || sender.email ||
|
|
80
|
-
{sender.is_staff &&
|
|
86
|
+
{sender.display_username || sender.email || labels.supportTeam}
|
|
87
|
+
{sender.is_staff && ` (${labels.staff})`}
|
|
81
88
|
</span>
|
|
82
89
|
)}
|
|
83
90
|
|
|
@@ -117,9 +124,22 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, curr
|
|
|
117
124
|
};
|
|
118
125
|
|
|
119
126
|
export const MessageList: React.FC = () => {
|
|
127
|
+
const baseT = useT();
|
|
128
|
+
const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
|
|
120
129
|
const { selectedTicket } = useSupportLayoutContext();
|
|
121
130
|
const { user } = useAuth();
|
|
122
131
|
|
|
132
|
+
const labels = useMemo(() => ({
|
|
133
|
+
noTicketSelected: st('messageList.noTicketSelected'),
|
|
134
|
+
noTicketSelectedDescription: st('messageList.noTicketSelectedDescription'),
|
|
135
|
+
noMessages: st('messageList.noMessages'),
|
|
136
|
+
noMessagesDescription: st('messageList.noMessagesDescription'),
|
|
137
|
+
loadingOlder: st('messageList.loadingOlder'),
|
|
138
|
+
loadOlder: st('messageList.loadOlder'),
|
|
139
|
+
supportTeam: st('messageList.supportTeam'),
|
|
140
|
+
staff: st('messageList.staff'),
|
|
141
|
+
}), [st]);
|
|
142
|
+
|
|
123
143
|
const {
|
|
124
144
|
messages,
|
|
125
145
|
isLoading,
|
|
@@ -193,9 +213,9 @@ export const MessageList: React.FC = () => {
|
|
|
193
213
|
return (
|
|
194
214
|
<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
215
|
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
196
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
216
|
+
<h3 className="text-lg font-semibold mb-2">{labels.noTicketSelected}</h3>
|
|
197
217
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
198
|
-
|
|
218
|
+
{labels.noTicketSelectedDescription}
|
|
199
219
|
</p>
|
|
200
220
|
</div>
|
|
201
221
|
);
|
|
@@ -222,9 +242,9 @@ export const MessageList: React.FC = () => {
|
|
|
222
242
|
return (
|
|
223
243
|
<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
244
|
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
225
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
245
|
+
<h3 className="text-lg font-semibold mb-2">{labels.noMessages}</h3>
|
|
226
246
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
227
|
-
|
|
247
|
+
{labels.noMessagesDescription}
|
|
228
248
|
</p>
|
|
229
249
|
</div>
|
|
230
250
|
);
|
|
@@ -241,7 +261,7 @@ export const MessageList: React.FC = () => {
|
|
|
241
261
|
<div className="flex justify-center py-4">
|
|
242
262
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
243
263
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
244
|
-
<span className="text-sm">
|
|
264
|
+
<span className="text-sm">{labels.loadingOlder}</span>
|
|
245
265
|
</div>
|
|
246
266
|
</div>
|
|
247
267
|
)}
|
|
@@ -255,7 +275,7 @@ export const MessageList: React.FC = () => {
|
|
|
255
275
|
onClick={handleLoadMore}
|
|
256
276
|
className="text-xs"
|
|
257
277
|
>
|
|
258
|
-
|
|
278
|
+
{labels.loadOlder} ({totalCount > 0 ? `${messages.length}/${totalCount}` : ''})
|
|
259
279
|
</Button>
|
|
260
280
|
</div>
|
|
261
281
|
)}
|
|
@@ -304,6 +324,7 @@ export const MessageList: React.FC = () => {
|
|
|
304
324
|
message={message}
|
|
305
325
|
isFromUser={!!isFromUser}
|
|
306
326
|
currentUser={user}
|
|
327
|
+
labels={{ supportTeam: labels.supportTeam, staff: labels.staff }}
|
|
307
328
|
/>
|
|
308
329
|
</div>
|
|
309
330
|
</React.Fragment>
|
|
@@ -7,8 +7,11 @@
|
|
|
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 { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
|
|
13
|
+
import { useT } from '@djangocfg/i18n';
|
|
14
|
+
import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
|
|
12
15
|
import { Badge, Card, CardContent } from '@djangocfg/ui-core';
|
|
13
16
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
17
|
|
|
@@ -39,22 +42,42 @@ const getStatusBadgeVariant = (
|
|
|
39
42
|
}
|
|
40
43
|
};
|
|
41
44
|
|
|
42
|
-
const
|
|
43
|
-
|
|
45
|
+
export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onClick }) => {
|
|
46
|
+
const baseT = useT();
|
|
47
|
+
const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
|
|
44
48
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const labels = useMemo(() => ({
|
|
50
|
+
status: {
|
|
51
|
+
open: st('status.open'),
|
|
52
|
+
waiting_for_user: st('status.waitingForUser'),
|
|
53
|
+
waiting_for_admin: st('status.waitingForAdmin'),
|
|
54
|
+
resolved: st('status.resolved'),
|
|
55
|
+
closed: st('status.closed'),
|
|
56
|
+
},
|
|
57
|
+
time: {
|
|
58
|
+
justNow: st('time.justNow'),
|
|
59
|
+
minutesAgo: st('time.minutesAgo'),
|
|
60
|
+
hoursAgo: st('time.hoursAgo'),
|
|
61
|
+
daysAgo: st('time.daysAgo'),
|
|
62
|
+
},
|
|
63
|
+
}), [st]);
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
52
|
-
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
65
|
+
const formatRelativeTime = useCallback((date: string | null | undefined): string => {
|
|
66
|
+
if (!date) return 'N/A';
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
const m = moment.utc(date).local();
|
|
69
|
+
const now = moment();
|
|
70
|
+
const diffInSeconds = now.diff(m, 'seconds');
|
|
56
71
|
|
|
57
|
-
|
|
72
|
+
if (diffInSeconds < 60) return labels.time.justNow;
|
|
73
|
+
if (diffInSeconds < 3600) return labels.time.minutesAgo.replace('{count}', String(Math.floor(diffInSeconds / 60)));
|
|
74
|
+
if (diffInSeconds < 86400) return labels.time.hoursAgo.replace('{count}', String(Math.floor(diffInSeconds / 3600)));
|
|
75
|
+
if (diffInSeconds < 604800) return labels.time.daysAgo.replace('{count}', String(Math.floor(diffInSeconds / 86400)));
|
|
76
|
+
|
|
77
|
+
return m.format('MMM D, YYYY');
|
|
78
|
+
}, [labels.time]);
|
|
79
|
+
|
|
80
|
+
const statusLabel = labels.status[ticket.status as keyof typeof labels.status] || ticket.status || labels.status.open;
|
|
58
81
|
return (
|
|
59
82
|
<Card
|
|
60
83
|
className={cn(
|
|
@@ -81,7 +104,7 @@ export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onCl
|
|
|
81
104
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
82
105
|
<div className="flex items-center gap-3">
|
|
83
106
|
<Badge variant={getStatusBadgeVariant(ticket.status || 'open')} className="text-xs">
|
|
84
|
-
{
|
|
107
|
+
{statusLabel}
|
|
85
108
|
</Badge>
|
|
86
109
|
<div className="flex items-center gap-1">
|
|
87
110
|
<Clock className="h-3 w-3" />
|
|
@@ -6,8 +6,11 @@
|
|
|
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 { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
|
|
12
|
+
import { useT } from '@djangocfg/i18n';
|
|
13
|
+
import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
|
|
11
14
|
import { Button, ScrollArea, Skeleton } from '@djangocfg/ui-core';
|
|
12
15
|
|
|
13
16
|
import { useSupportLayoutContext } from '../context';
|
|
@@ -16,6 +19,8 @@ import { useInfiniteTickets } from '../hooks';
|
|
|
16
19
|
import { TicketCard } from './TicketCard';
|
|
17
20
|
|
|
18
21
|
export const TicketList: React.FC = () => {
|
|
22
|
+
const baseT = useT();
|
|
23
|
+
const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
|
|
19
24
|
const { selectedTicket, selectTicket } = useSupportLayoutContext();
|
|
20
25
|
const {
|
|
21
26
|
tickets,
|
|
@@ -27,6 +32,14 @@ export const TicketList: React.FC = () => {
|
|
|
27
32
|
refresh
|
|
28
33
|
} = useInfiniteTickets();
|
|
29
34
|
|
|
35
|
+
const labels = useMemo(() => ({
|
|
36
|
+
noTickets: st('ticketList.noTickets'),
|
|
37
|
+
noTicketsDescription: st('ticketList.noTicketsDescription'),
|
|
38
|
+
loadingMore: st('ticketList.loadingMore'),
|
|
39
|
+
loadMore: st('ticketList.loadMore'),
|
|
40
|
+
allLoaded: st('ticketList.allLoaded'),
|
|
41
|
+
}), [st]);
|
|
42
|
+
|
|
30
43
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
31
44
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
32
45
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
@@ -90,9 +103,9 @@ export const TicketList: React.FC = () => {
|
|
|
90
103
|
return (
|
|
91
104
|
<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
105
|
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
93
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
106
|
+
<h3 className="text-lg font-semibold mb-2">{labels.noTickets}</h3>
|
|
94
107
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
95
|
-
|
|
108
|
+
{labels.noTicketsDescription}
|
|
96
109
|
</p>
|
|
97
110
|
</div>
|
|
98
111
|
);
|
|
@@ -123,7 +136,7 @@ export const TicketList: React.FC = () => {
|
|
|
123
136
|
<div className="flex justify-center py-4">
|
|
124
137
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
125
138
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
126
|
-
<span className="text-sm">
|
|
139
|
+
<span className="text-sm">{labels.loadingMore}</span>
|
|
127
140
|
</div>
|
|
128
141
|
</div>
|
|
129
142
|
)}
|
|
@@ -137,7 +150,7 @@ export const TicketList: React.FC = () => {
|
|
|
137
150
|
onClick={loadMore}
|
|
138
151
|
className="text-xs"
|
|
139
152
|
>
|
|
140
|
-
|
|
153
|
+
{labels.loadMore} ({totalCount > 0 ? `${tickets.length}/${totalCount}` : ''})
|
|
141
154
|
</Button>
|
|
142
155
|
</div>
|
|
143
156
|
)}
|
|
@@ -145,7 +158,7 @@ export const TicketList: React.FC = () => {
|
|
|
145
158
|
{/* End message */}
|
|
146
159
|
{!hasMore && tickets.length > 0 && (
|
|
147
160
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
|
148
|
-
|
|
161
|
+
{labels.allLoaded.replace('{count}', String(totalCount))}
|
|
149
162
|
</div>
|
|
150
163
|
)}
|
|
151
164
|
</div>
|