@djangocfg/ext-support 1.0.20 → 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.
@@ -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 || 'Support'} />
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 || 'Support Team'}
80
- {sender.is_staff && ' (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">No ticket selected</h3>
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
- Select a ticket from the list to view the conversation
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">No messages yet</h3>
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
- Start the conversation by sending a message below
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">Loading older messages...</span>
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
- Load older messages ({totalCount > 0 ? `${messages.length}/${totalCount}` : ''})
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 formatRelativeTime = (date: string | null | undefined): string => {
43
- if (!date) return 'N/A';
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 m = moment.utc(date).local();
46
- const now = moment();
47
- const diffInSeconds = now.diff(m, 'seconds');
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
- if (diffInSeconds < 60) return 'Just now';
50
- if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
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
- return m.format('MMM D, YYYY');
55
- };
68
+ const m = moment.utc(date).local();
69
+ const now = moment();
70
+ const diffInSeconds = now.diff(m, 'seconds');
56
71
 
57
- export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onClick }) => {
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
- {ticket.status || 'open'}
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">No tickets yet</h3>
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
- Create your first support ticket to get help from our team
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">Loading more tickets...</span>
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
- Load more ({totalCount > 0 ? `${tickets.length}/${totalCount}` : ''})
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
- All {totalCount} tickets loaded
161
+ {labels.allLoaded.replace('{count}', String(totalCount))}
149
162
  </div>
150
163
  )}
151
164
  </div>