@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ext-support",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Support ticket system extension for DjangoCFG",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"types": "./dist/config.d.ts",
|
|
47
47
|
"import": "./dist/config.js",
|
|
48
48
|
"require": "./dist/config.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./i18n": {
|
|
51
|
+
"types": "./dist/i18n.d.ts",
|
|
52
|
+
"import": "./dist/i18n.js",
|
|
53
|
+
"require": "./dist/i18n.cjs"
|
|
49
54
|
}
|
|
50
55
|
},
|
|
51
56
|
"files": [
|
|
@@ -59,9 +64,10 @@
|
|
|
59
64
|
"check": "tsc --noEmit"
|
|
60
65
|
},
|
|
61
66
|
"peerDependencies": {
|
|
62
|
-
"@djangocfg/api": "^2.1.
|
|
63
|
-
"@djangocfg/ext-base": "^1.0.
|
|
64
|
-
"@djangocfg/
|
|
67
|
+
"@djangocfg/api": "^2.1.111",
|
|
68
|
+
"@djangocfg/ext-base": "^1.0.17",
|
|
69
|
+
"@djangocfg/i18n": "^2.1.111",
|
|
70
|
+
"@djangocfg/ui-core": "^2.1.111",
|
|
65
71
|
"consola": "^3.4.2",
|
|
66
72
|
"lucide-react": "^0.545.0",
|
|
67
73
|
"moment": "^2.30.1",
|
|
@@ -74,10 +80,11 @@
|
|
|
74
80
|
"zod": "^4.3.4"
|
|
75
81
|
},
|
|
76
82
|
"devDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/ext-base": "^1.0.
|
|
79
|
-
"@djangocfg/
|
|
80
|
-
"@djangocfg/
|
|
83
|
+
"@djangocfg/api": "^2.1.111",
|
|
84
|
+
"@djangocfg/ext-base": "^1.0.17",
|
|
85
|
+
"@djangocfg/i18n": "^2.1.111",
|
|
86
|
+
"@djangocfg/ui-core": "^2.1.111",
|
|
87
|
+
"@djangocfg/typescript-config": "^2.1.111",
|
|
81
88
|
"@types/node": "^24.7.2",
|
|
82
89
|
"@types/react": "^19.0.0",
|
|
83
90
|
"consola": "^3.4.2",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support Extension I18n
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createExtensionI18n } from '@djangocfg/ext-base/i18n';
|
|
6
|
+
import type { SupportTranslations } from './types';
|
|
7
|
+
import { en } from './locales/en';
|
|
8
|
+
import { ru } from './locales/ru';
|
|
9
|
+
import { ko } from './locales/ko';
|
|
10
|
+
|
|
11
|
+
/** Support extension namespace */
|
|
12
|
+
export const SUPPORT_NAMESPACE = 'support' as const;
|
|
13
|
+
|
|
14
|
+
export const supportI18n = createExtensionI18n<SupportTranslations>({
|
|
15
|
+
namespace: SUPPORT_NAMESPACE,
|
|
16
|
+
defaultLocale: 'en',
|
|
17
|
+
locales: { en, ru, ko },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const supportTranslations = supportI18n.getAllTranslations();
|
|
21
|
+
|
|
22
|
+
// Types
|
|
23
|
+
export type { SupportTranslations, SupportKeys, SupportLocalKeys } from './types';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SupportTranslations } from '../types';
|
|
2
|
+
|
|
3
|
+
export const en: SupportTranslations = {
|
|
4
|
+
layout: {
|
|
5
|
+
title: 'Support Center',
|
|
6
|
+
titleShort: 'Support',
|
|
7
|
+
subtitle: 'Get help from our support team',
|
|
8
|
+
newTicket: 'New Ticket',
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
status: {
|
|
12
|
+
open: 'Open',
|
|
13
|
+
waitingForUser: 'Waiting for you',
|
|
14
|
+
waitingForAdmin: 'Waiting for support',
|
|
15
|
+
resolved: 'Resolved',
|
|
16
|
+
closed: 'Closed',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
ticketList: {
|
|
20
|
+
noTickets: 'No tickets yet',
|
|
21
|
+
noTicketsDescription: 'Create your first support ticket to get help',
|
|
22
|
+
loadingMore: 'Loading more...',
|
|
23
|
+
loadMore: 'Load more',
|
|
24
|
+
allLoaded: 'All {count} tickets loaded',
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
createTicket: {
|
|
28
|
+
title: 'New Support Ticket',
|
|
29
|
+
description: 'Describe your issue and we will help you',
|
|
30
|
+
subjectLabel: 'Subject',
|
|
31
|
+
subjectPlaceholder: 'Brief description of your issue',
|
|
32
|
+
messageLabel: 'Message',
|
|
33
|
+
messagePlaceholder: 'Describe your issue in detail...',
|
|
34
|
+
cancel: 'Cancel',
|
|
35
|
+
creating: 'Creating...',
|
|
36
|
+
create: 'Create Ticket',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
validation: {
|
|
40
|
+
subjectRequired: 'Subject is required',
|
|
41
|
+
subjectTooLong: 'Subject is too long (max 200 characters)',
|
|
42
|
+
messageRequired: 'Message is required',
|
|
43
|
+
messageTooLong: 'Message is too long (max 5000 characters)',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
messages: {
|
|
47
|
+
ticketCreated: 'Ticket created successfully',
|
|
48
|
+
ticketCreateFailed: 'Failed to create ticket',
|
|
49
|
+
messageSent: 'Message sent',
|
|
50
|
+
messageSendFailed: 'Failed to send message',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
messageInput: {
|
|
54
|
+
placeholder: 'Type your message...',
|
|
55
|
+
ticketClosed: 'Ticket closed',
|
|
56
|
+
ticketClosedDescription: 'This ticket has been closed. Create a new ticket if you need further assistance.',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
messageList: {
|
|
60
|
+
noTicketSelected: 'No ticket selected',
|
|
61
|
+
noTicketSelectedDescription: 'Select a ticket from the list to view messages',
|
|
62
|
+
noMessages: 'No messages yet',
|
|
63
|
+
noMessagesDescription: 'Start the conversation by sending a message',
|
|
64
|
+
loadingOlder: 'Loading older messages...',
|
|
65
|
+
loadOlder: 'Load older messages',
|
|
66
|
+
supportTeam: 'Support Team',
|
|
67
|
+
staff: 'Staff',
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
time: {
|
|
71
|
+
justNow: 'Just now',
|
|
72
|
+
minutesAgo: '{count} min ago',
|
|
73
|
+
hoursAgo: '{count}h ago',
|
|
74
|
+
daysAgo: '{count}d ago',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SupportTranslations } from '../types';
|
|
2
|
+
|
|
3
|
+
export const ko: SupportTranslations = {
|
|
4
|
+
layout: {
|
|
5
|
+
title: '고객지원 센터',
|
|
6
|
+
titleShort: '고객지원',
|
|
7
|
+
subtitle: '고객지원팀의 도움을 받으세요',
|
|
8
|
+
newTicket: '새 문의',
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
status: {
|
|
12
|
+
open: '열림',
|
|
13
|
+
waitingForUser: '회신 대기',
|
|
14
|
+
waitingForAdmin: '지원팀 대기',
|
|
15
|
+
resolved: '해결됨',
|
|
16
|
+
closed: '종료',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
ticketList: {
|
|
20
|
+
noTickets: '문의 내역 없음',
|
|
21
|
+
noTicketsDescription: '첫 번째 문의를 등록하여 도움을 받으세요',
|
|
22
|
+
loadingMore: '불러오는 중...',
|
|
23
|
+
loadMore: '더 보기',
|
|
24
|
+
allLoaded: '총 {count}개의 문의가 로드됨',
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
createTicket: {
|
|
28
|
+
title: '새 문의',
|
|
29
|
+
description: '문제를 설명해 주시면 도움을 드리겠습니다',
|
|
30
|
+
subjectLabel: '제목',
|
|
31
|
+
subjectPlaceholder: '문제에 대한 간략한 설명',
|
|
32
|
+
messageLabel: '내용',
|
|
33
|
+
messagePlaceholder: '문제를 자세히 설명해 주세요...',
|
|
34
|
+
cancel: '취소',
|
|
35
|
+
creating: '생성 중...',
|
|
36
|
+
create: '문의 등록',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
validation: {
|
|
40
|
+
subjectRequired: '제목을 입력해 주세요',
|
|
41
|
+
subjectTooLong: '제목이 너무 깁니다 (최대 200자)',
|
|
42
|
+
messageRequired: '내용을 입력해 주세요',
|
|
43
|
+
messageTooLong: '내용이 너무 깁니다 (최대 5000자)',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
messages: {
|
|
47
|
+
ticketCreated: '문의가 등록되었습니다',
|
|
48
|
+
ticketCreateFailed: '문의 등록에 실패했습니다',
|
|
49
|
+
messageSent: '메시지가 전송되었습니다',
|
|
50
|
+
messageSendFailed: '메시지 전송에 실패했습니다',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
messageInput: {
|
|
54
|
+
placeholder: '메시지를 입력하세요...',
|
|
55
|
+
ticketClosed: '문의 종료됨',
|
|
56
|
+
ticketClosedDescription: '이 문의는 종료되었습니다. 추가 도움이 필요하시면 새 문의를 등록해 주세요.',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
messageList: {
|
|
60
|
+
noTicketSelected: '문의가 선택되지 않음',
|
|
61
|
+
noTicketSelectedDescription: '메시지를 보려면 목록에서 문의를 선택하세요',
|
|
62
|
+
noMessages: '메시지 없음',
|
|
63
|
+
noMessagesDescription: '메시지를 보내 대화를 시작하세요',
|
|
64
|
+
loadingOlder: '이전 메시지 불러오는 중...',
|
|
65
|
+
loadOlder: '이전 메시지 보기',
|
|
66
|
+
supportTeam: '고객지원팀',
|
|
67
|
+
staff: '담당자',
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
time: {
|
|
71
|
+
justNow: '방금 전',
|
|
72
|
+
minutesAgo: '{count}분 전',
|
|
73
|
+
hoursAgo: '{count}시간 전',
|
|
74
|
+
daysAgo: '{count}일 전',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SupportTranslations } from '../types';
|
|
2
|
+
|
|
3
|
+
export const ru: SupportTranslations = {
|
|
4
|
+
layout: {
|
|
5
|
+
title: 'Центр поддержки',
|
|
6
|
+
titleShort: 'Поддержка',
|
|
7
|
+
subtitle: 'Получите помощь от нашей команды поддержки',
|
|
8
|
+
newTicket: 'Новое обращение',
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
status: {
|
|
12
|
+
open: 'Открыт',
|
|
13
|
+
waitingForUser: 'Ожидает вас',
|
|
14
|
+
waitingForAdmin: 'Ожидает поддержку',
|
|
15
|
+
resolved: 'Решён',
|
|
16
|
+
closed: 'Закрыт',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
ticketList: {
|
|
20
|
+
noTickets: 'Нет обращений',
|
|
21
|
+
noTicketsDescription: 'Создайте первое обращение, чтобы получить помощь',
|
|
22
|
+
loadingMore: 'Загрузка...',
|
|
23
|
+
loadMore: 'Загрузить ещё',
|
|
24
|
+
allLoaded: 'Все {count} обращений загружено',
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
createTicket: {
|
|
28
|
+
title: 'Новое обращение',
|
|
29
|
+
description: 'Опишите вашу проблему, и мы поможем вам',
|
|
30
|
+
subjectLabel: 'Тема',
|
|
31
|
+
subjectPlaceholder: 'Краткое описание проблемы',
|
|
32
|
+
messageLabel: 'Сообщение',
|
|
33
|
+
messagePlaceholder: 'Опишите вашу проблему подробно...',
|
|
34
|
+
cancel: 'Отмена',
|
|
35
|
+
creating: 'Создание...',
|
|
36
|
+
create: 'Создать обращение',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
validation: {
|
|
40
|
+
subjectRequired: 'Тема обязательна',
|
|
41
|
+
subjectTooLong: 'Тема слишком длинная (макс. 200 символов)',
|
|
42
|
+
messageRequired: 'Сообщение обязательно',
|
|
43
|
+
messageTooLong: 'Сообщение слишком длинное (макс. 5000 символов)',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
messages: {
|
|
47
|
+
ticketCreated: 'Обращение создано',
|
|
48
|
+
ticketCreateFailed: 'Не удалось создать обращение',
|
|
49
|
+
messageSent: 'Сообщение отправлено',
|
|
50
|
+
messageSendFailed: 'Не удалось отправить сообщение',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
messageInput: {
|
|
54
|
+
placeholder: 'Введите сообщение...',
|
|
55
|
+
ticketClosed: 'Обращение закрыто',
|
|
56
|
+
ticketClosedDescription: 'Это обращение закрыто. Создайте новое, если вам нужна помощь.',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
messageList: {
|
|
60
|
+
noTicketSelected: 'Обращение не выбрано',
|
|
61
|
+
noTicketSelectedDescription: 'Выберите обращение из списка для просмотра сообщений',
|
|
62
|
+
noMessages: 'Нет сообщений',
|
|
63
|
+
noMessagesDescription: 'Начните диалог, отправив сообщение',
|
|
64
|
+
loadingOlder: 'Загрузка старых сообщений...',
|
|
65
|
+
loadOlder: 'Загрузить старые',
|
|
66
|
+
supportTeam: 'Служба поддержки',
|
|
67
|
+
staff: 'Сотрудник',
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
time: {
|
|
71
|
+
justNow: 'Только что',
|
|
72
|
+
minutesAgo: '{count} мин назад',
|
|
73
|
+
hoursAgo: '{count}ч назад',
|
|
74
|
+
daysAgo: '{count}д назад',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support Extension I18n Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionKeys, PathKeys } from '@djangocfg/ext-base/i18n';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* All valid keys for support translations (with namespace)
|
|
9
|
+
*/
|
|
10
|
+
export type SupportKeys = ExtensionKeys<'support', SupportTranslations>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keys without namespace prefix (for createTypedExtensionT)
|
|
14
|
+
*/
|
|
15
|
+
export type SupportLocalKeys = PathKeys<SupportTranslations>;
|
|
16
|
+
|
|
17
|
+
export interface SupportTranslations {
|
|
18
|
+
/** Layout */
|
|
19
|
+
layout: {
|
|
20
|
+
title: string;
|
|
21
|
+
titleShort: string;
|
|
22
|
+
subtitle: string;
|
|
23
|
+
newTicket: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Ticket statuses */
|
|
27
|
+
status: {
|
|
28
|
+
open: string;
|
|
29
|
+
waitingForUser: string;
|
|
30
|
+
waitingForAdmin: string;
|
|
31
|
+
resolved: string;
|
|
32
|
+
closed: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Ticket list */
|
|
36
|
+
ticketList: {
|
|
37
|
+
noTickets: string;
|
|
38
|
+
noTicketsDescription: string;
|
|
39
|
+
loadingMore: string;
|
|
40
|
+
loadMore: string;
|
|
41
|
+
allLoaded: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Create ticket dialog */
|
|
45
|
+
createTicket: {
|
|
46
|
+
title: string;
|
|
47
|
+
description: string;
|
|
48
|
+
subjectLabel: string;
|
|
49
|
+
subjectPlaceholder: string;
|
|
50
|
+
messageLabel: string;
|
|
51
|
+
messagePlaceholder: string;
|
|
52
|
+
cancel: string;
|
|
53
|
+
creating: string;
|
|
54
|
+
create: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Validation */
|
|
58
|
+
validation: {
|
|
59
|
+
subjectRequired: string;
|
|
60
|
+
subjectTooLong: string;
|
|
61
|
+
messageRequired: string;
|
|
62
|
+
messageTooLong: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Messages */
|
|
66
|
+
messages: {
|
|
67
|
+
ticketCreated: string;
|
|
68
|
+
ticketCreateFailed: string;
|
|
69
|
+
messageSent: string;
|
|
70
|
+
messageSendFailed: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Message input */
|
|
74
|
+
messageInput: {
|
|
75
|
+
placeholder: string;
|
|
76
|
+
ticketClosed: string;
|
|
77
|
+
ticketClosedDescription: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Message list */
|
|
81
|
+
messageList: {
|
|
82
|
+
noTicketSelected: string;
|
|
83
|
+
noTicketSelectedDescription: string;
|
|
84
|
+
noMessages: string;
|
|
85
|
+
noMessagesDescription: string;
|
|
86
|
+
loadingOlder: string;
|
|
87
|
+
loadOlder: string;
|
|
88
|
+
supportTeam: string;
|
|
89
|
+
staff: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Time labels */
|
|
93
|
+
time: {
|
|
94
|
+
justNow: string;
|
|
95
|
+
minutesAgo: string;
|
|
96
|
+
hoursAgo: string;
|
|
97
|
+
daysAgo: string;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
9
|
import { ArrowLeft, LifeBuoy, Plus } from 'lucide-react';
|
|
10
|
-
import React from 'react';
|
|
10
|
+
import React, { useMemo } 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 { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@djangocfg/ui-core';
|
|
13
16
|
|
|
14
17
|
import { SupportProvider } from '../../contexts/SupportContext';
|
|
@@ -20,10 +23,19 @@ import { SupportLayoutProvider, useSupportLayoutContext } from './context';
|
|
|
20
23
|
// ─────────────────────────────────────────────────────────────────────────
|
|
21
24
|
|
|
22
25
|
const SupportLayoutContent: React.FC = () => {
|
|
26
|
+
const baseT = useT();
|
|
27
|
+
const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
|
|
23
28
|
const { selectedTicket, selectTicket, openCreateDialog, getUnreadCount } =
|
|
24
29
|
useSupportLayoutContext();
|
|
25
30
|
const [isMobile, setIsMobile] = React.useState(false);
|
|
26
|
-
|
|
31
|
+
|
|
32
|
+
const labels = useMemo(() => ({
|
|
33
|
+
title: st('layout.title'),
|
|
34
|
+
titleShort: st('layout.titleShort'),
|
|
35
|
+
subtitle: st('layout.subtitle'),
|
|
36
|
+
newTicket: st('layout.newTicket'),
|
|
37
|
+
}), [st]);
|
|
38
|
+
|
|
27
39
|
React.useEffect(() => {
|
|
28
40
|
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
|
29
41
|
checkMobile();
|
|
@@ -53,7 +65,7 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
53
65
|
<LifeBuoy className="h-6 w-6 text-primary" />
|
|
54
66
|
)}
|
|
55
67
|
<h1 className="text-xl font-semibold">
|
|
56
|
-
{selectedTicket ? selectedTicket.subject :
|
|
68
|
+
{selectedTicket ? selectedTicket.subject : labels.titleShort}
|
|
57
69
|
</h1>
|
|
58
70
|
{unreadCount > 0 && !selectedTicket && (
|
|
59
71
|
<div className="h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
@@ -64,7 +76,7 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
64
76
|
{!selectedTicket && (
|
|
65
77
|
<Button onClick={openCreateDialog} size="sm">
|
|
66
78
|
<Plus className="h-4 w-4 mr-2" />
|
|
67
|
-
|
|
79
|
+
{labels.newTicket}
|
|
68
80
|
</Button>
|
|
69
81
|
)}
|
|
70
82
|
</div>
|
|
@@ -101,8 +113,8 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
101
113
|
<div className="flex items-center gap-3">
|
|
102
114
|
<LifeBuoy className="h-7 w-7 text-primary" />
|
|
103
115
|
<div>
|
|
104
|
-
<h1 className="text-2xl font-bold">
|
|
105
|
-
<p className="text-sm text-muted-foreground">
|
|
116
|
+
<h1 className="text-2xl font-bold">{labels.title}</h1>
|
|
117
|
+
<p className="text-sm text-muted-foreground">{labels.subtitle}</p>
|
|
106
118
|
</div>
|
|
107
119
|
{unreadCount > 0 && (
|
|
108
120
|
<div className="h-6 w-6 bg-red-500 text-white text-sm rounded-full flex items-center justify-center">
|
|
@@ -113,7 +125,7 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
113
125
|
|
|
114
126
|
<Button onClick={openCreateDialog}>
|
|
115
127
|
<Plus className="h-4 w-4 mr-2" />
|
|
116
|
-
|
|
128
|
+
{labels.newTicket}
|
|
117
129
|
</Button>
|
|
118
130
|
</div>
|
|
119
131
|
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
9
|
import { Loader2, Plus } from 'lucide-react';
|
|
10
|
-
import React from 'react';
|
|
10
|
+
import React, { useMemo } from 'react';
|
|
11
11
|
import { useForm } from 'react-hook-form';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
|
|
14
|
+
import { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
|
|
15
|
+
import { useT } from '@djangocfg/i18n';
|
|
16
|
+
import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
|
|
14
17
|
import {
|
|
15
18
|
Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Form, FormControl,
|
|
16
19
|
FormField, FormItem, FormLabel, FormMessage, Input, Textarea, useToast
|
|
@@ -22,16 +25,38 @@ import { useSupportLayoutContext } from '../context';
|
|
|
22
25
|
|
|
23
26
|
import type { TicketFormData } from '../types';
|
|
24
27
|
|
|
25
|
-
const createTicketSchema = z.object({
|
|
26
|
-
subject: z.string().min(1, 'Subject is required').max(200, 'Subject too long'),
|
|
27
|
-
message: z.string().min(1, 'Message is required').max(5000, 'Message too long'),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
28
|
export const CreateTicketDialog: React.FC = () => {
|
|
29
|
+
const baseT = useT();
|
|
30
|
+
const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
|
|
31
31
|
const { uiState, createTicket, closeCreateDialog } = useSupportLayoutContext();
|
|
32
32
|
const { toast } = useToast();
|
|
33
33
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
34
34
|
|
|
35
|
+
const labels = useMemo(() => ({
|
|
36
|
+
title: st('createTicket.title'),
|
|
37
|
+
description: st('createTicket.description'),
|
|
38
|
+
subjectLabel: st('createTicket.subjectLabel'),
|
|
39
|
+
subjectPlaceholder: st('createTicket.subjectPlaceholder'),
|
|
40
|
+
messageLabel: st('createTicket.messageLabel'),
|
|
41
|
+
messagePlaceholder: st('createTicket.messagePlaceholder'),
|
|
42
|
+
cancel: st('createTicket.cancel'),
|
|
43
|
+
creating: st('createTicket.creating'),
|
|
44
|
+
create: st('createTicket.create'),
|
|
45
|
+
ticketCreated: st('messages.ticketCreated'),
|
|
46
|
+
ticketCreateFailed: st('messages.ticketCreateFailed'),
|
|
47
|
+
validation: {
|
|
48
|
+
subjectRequired: st('validation.subjectRequired'),
|
|
49
|
+
subjectTooLong: st('validation.subjectTooLong'),
|
|
50
|
+
messageRequired: st('validation.messageRequired'),
|
|
51
|
+
messageTooLong: st('validation.messageTooLong'),
|
|
52
|
+
},
|
|
53
|
+
}), [st]);
|
|
54
|
+
|
|
55
|
+
const createTicketSchema = useMemo(() => z.object({
|
|
56
|
+
subject: z.string().min(1, labels.validation.subjectRequired).max(200, labels.validation.subjectTooLong),
|
|
57
|
+
message: z.string().min(1, labels.validation.messageRequired).max(5000, labels.validation.messageTooLong),
|
|
58
|
+
}), [labels.validation]);
|
|
59
|
+
|
|
35
60
|
const form = useForm<TicketFormData>({
|
|
36
61
|
resolver: zodResolver(createTicketSchema),
|
|
37
62
|
defaultValues: {
|
|
@@ -45,10 +70,10 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
45
70
|
try {
|
|
46
71
|
await createTicket(data);
|
|
47
72
|
form.reset();
|
|
48
|
-
toast.success(
|
|
73
|
+
toast.success(labels.ticketCreated);
|
|
49
74
|
} catch (error) {
|
|
50
75
|
supportLogger.error('Failed to create ticket:', error);
|
|
51
|
-
toast.error(
|
|
76
|
+
toast.error(labels.ticketCreateFailed);
|
|
52
77
|
} finally {
|
|
53
78
|
setIsSubmitting(false);
|
|
54
79
|
}
|
|
@@ -65,10 +90,10 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
65
90
|
<DialogHeader>
|
|
66
91
|
<DialogTitle className="flex items-center gap-2">
|
|
67
92
|
<Plus className="h-5 w-5" />
|
|
68
|
-
|
|
93
|
+
{labels.title}
|
|
69
94
|
</DialogTitle>
|
|
70
95
|
<DialogDescription>
|
|
71
|
-
|
|
96
|
+
{labels.description}
|
|
72
97
|
</DialogDescription>
|
|
73
98
|
</DialogHeader>
|
|
74
99
|
|
|
@@ -79,9 +104,9 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
79
104
|
name="subject"
|
|
80
105
|
render={({ field }) => (
|
|
81
106
|
<FormItem>
|
|
82
|
-
<FormLabel>
|
|
107
|
+
<FormLabel>{labels.subjectLabel}</FormLabel>
|
|
83
108
|
<FormControl>
|
|
84
|
-
<Input placeholder=
|
|
109
|
+
<Input placeholder={labels.subjectPlaceholder} {...field} />
|
|
85
110
|
</FormControl>
|
|
86
111
|
<FormMessage />
|
|
87
112
|
</FormItem>
|
|
@@ -93,10 +118,10 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
93
118
|
name="message"
|
|
94
119
|
render={({ field }) => (
|
|
95
120
|
<FormItem>
|
|
96
|
-
<FormLabel>
|
|
121
|
+
<FormLabel>{labels.messageLabel}</FormLabel>
|
|
97
122
|
<FormControl>
|
|
98
123
|
<Textarea
|
|
99
|
-
placeholder=
|
|
124
|
+
placeholder={labels.messagePlaceholder}
|
|
100
125
|
className="min-h-[120px]"
|
|
101
126
|
{...field}
|
|
102
127
|
/>
|
|
@@ -113,18 +138,18 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
113
138
|
onClick={handleClose}
|
|
114
139
|
disabled={isSubmitting}
|
|
115
140
|
>
|
|
116
|
-
|
|
141
|
+
{labels.cancel}
|
|
117
142
|
</Button>
|
|
118
143
|
<Button type="submit" disabled={isSubmitting}>
|
|
119
144
|
{isSubmitting ? (
|
|
120
145
|
<>
|
|
121
146
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
122
|
-
|
|
147
|
+
{labels.creating}
|
|
123
148
|
</>
|
|
124
149
|
) : (
|
|
125
150
|
<>
|
|
126
151
|
<Plus className="h-4 w-4 mr-2" />
|
|
127
|
-
|
|
152
|
+
{labels.create}
|
|
128
153
|
</>
|
|
129
154
|
)}
|
|
130
155
|
</Button>
|
|
@@ -6,8 +6,11 @@
|
|
|
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 { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
|
|
12
|
+
import { useT } from '@djangocfg/i18n';
|
|
13
|
+
import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
|
|
11
14
|
import { Button, Textarea } from '@djangocfg/ui-core';
|
|
12
15
|
import { useToast } from '@djangocfg/ui-core/hooks';
|
|
13
16
|
|
|
@@ -15,24 +18,34 @@ import { supportLogger } from '../../../utils/logger';
|
|
|
15
18
|
import { useSupportLayoutContext } from '../context';
|
|
16
19
|
|
|
17
20
|
export const MessageInput: React.FC = () => {
|
|
21
|
+
const baseT = useT();
|
|
22
|
+
const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
|
|
18
23
|
const { selectedTicket, sendMessage } = useSupportLayoutContext();
|
|
19
24
|
const { toast } = useToast();
|
|
20
25
|
const [message, setMessage] = useState('');
|
|
21
26
|
const [isSending, setIsSending] = useState(false);
|
|
22
27
|
|
|
28
|
+
const labels = useMemo(() => ({
|
|
29
|
+
placeholder: st('messageInput.placeholder'),
|
|
30
|
+
ticketClosed: st('messageInput.ticketClosed'),
|
|
31
|
+
ticketClosedDescription: st('messageInput.ticketClosedDescription'),
|
|
32
|
+
messageSent: st('messages.messageSent'),
|
|
33
|
+
messageSendFailed: st('messages.messageSendFailed'),
|
|
34
|
+
}), [st]);
|
|
35
|
+
|
|
23
36
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
24
37
|
e.preventDefault();
|
|
25
|
-
|
|
38
|
+
|
|
26
39
|
if (!message.trim() || !selectedTicket) return;
|
|
27
40
|
|
|
28
41
|
setIsSending(true);
|
|
29
42
|
try {
|
|
30
43
|
await sendMessage(message.trim());
|
|
31
44
|
setMessage('');
|
|
32
|
-
toast.success(
|
|
45
|
+
toast.success(labels.messageSent);
|
|
33
46
|
} catch (error) {
|
|
34
47
|
supportLogger.error('Failed to send message:', error);
|
|
35
|
-
toast.error(
|
|
48
|
+
toast.error(labels.messageSendFailed);
|
|
36
49
|
} finally {
|
|
37
50
|
setIsSending(false);
|
|
38
51
|
}
|
|
@@ -58,12 +71,8 @@ export const MessageInput: React.FC = () => {
|
|
|
58
71
|
value={message}
|
|
59
72
|
onChange={(e) => setMessage(e.target.value)}
|
|
60
73
|
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
|
|
74
|
+
placeholder={canSendMessage ? labels.placeholder : labels.ticketClosed}
|
|
75
|
+
className="min-h-[60px] max-h-[200px] transition-all duration-200
|
|
67
76
|
focus:ring-2 focus:ring-primary/20"
|
|
68
77
|
disabled={!canSendMessage || isSending}
|
|
69
78
|
/>
|
|
@@ -79,7 +88,7 @@ export const MessageInput: React.FC = () => {
|
|
|
79
88
|
</div>
|
|
80
89
|
{!canSendMessage && (
|
|
81
90
|
<p className="text-xs text-muted-foreground mt-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
82
|
-
|
|
91
|
+
{labels.ticketClosedDescription}
|
|
83
92
|
</p>
|
|
84
93
|
)}
|
|
85
94
|
</form>
|