@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.
@@ -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('Message sent successfully');
42
+ toast.success(labels.messageSent);
33
43
  } catch (error) {
34
44
  supportLogger.error('Failed to send message:', error);
35
- toast.error('Failed to send message');
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
- canSendMessage
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
- This ticket is closed. You cannot send new messages.
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 || 'Support'} />
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 || 'Support Team'}
80
- {sender.is_staff && ' (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">No ticket selected</h3>
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
- Select a ticket from the list to view the conversation
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">No messages yet</h3>
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
- Start the conversation by sending a message below
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">Loading older messages...</span>
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
- Load older messages ({totalCount > 0 ? `${messages.length}/${totalCount}` : ''})
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 formatRelativeTime = (date: string | null | undefined): string => {
43
- if (!date) return 'N/A';
43
+ export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onClick }) => {
44
+ const st = useSupportT();
44
45
 
45
- const m = moment.utc(date).local();
46
- const now = moment();
47
- const diffInSeconds = now.diff(m, 'seconds');
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
- 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`;
62
+ const formatRelativeTime = useCallback((date: string | null | undefined): string => {
63
+ if (!date) return 'N/A';
53
64
 
54
- return m.format('MMM D, YYYY');
55
- };
65
+ const m = moment.utc(date).local();
66
+ const now = moment();
67
+ const diffInSeconds = now.diff(m, 'seconds');
56
68
 
57
- export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onClick }) => {
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
- {ticket.status || 'open'}
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">No tickets yet</h3>
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
- Create your first support ticket to get help from our team
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">Loading more tickets...</span>
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
- Load more ({totalCount > 0 ? `${tickets.length}/${totalCount}` : ''})
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
- All {totalCount} tickets loaded
158
+ {labels.allLoaded.replace('{count}', String(totalCount))}
149
159
  </div>
150
160
  )}
151
161
  </div>