@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ext-support",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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,29 +64,33 @@
|
|
|
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.124",
|
|
68
|
+
"@djangocfg/ext-base": "^1.0.18",
|
|
69
|
+
"@djangocfg/i18n": "^2.1.124",
|
|
70
|
+
"@djangocfg/ui-core": "^2.1.124",
|
|
71
|
+
"@hookform/resolvers": "^5.2.2",
|
|
65
72
|
"consola": "^3.4.2",
|
|
66
73
|
"lucide-react": "^0.545.0",
|
|
67
74
|
"moment": "^2.30.1",
|
|
68
75
|
"next": "^16",
|
|
76
|
+
"next-intl": "^4",
|
|
69
77
|
"p-retry": "^7.0.0",
|
|
70
78
|
"react": "^19",
|
|
71
79
|
"react-hook-form": "^7.69.0",
|
|
72
|
-
"@hookform/resolvers": "^5.2.2",
|
|
73
80
|
"swr": "^2.3.7",
|
|
74
81
|
"zod": "^4.3.4"
|
|
75
82
|
},
|
|
76
83
|
"devDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/ext-base": "^1.0.
|
|
79
|
-
"@djangocfg/
|
|
80
|
-
"@djangocfg/
|
|
84
|
+
"@djangocfg/api": "^2.1.124",
|
|
85
|
+
"@djangocfg/ext-base": "^1.0.18",
|
|
86
|
+
"@djangocfg/i18n": "^2.1.124",
|
|
87
|
+
"@djangocfg/ui-core": "^2.1.124",
|
|
88
|
+
"@djangocfg/typescript-config": "^2.1.124",
|
|
81
89
|
"@types/node": "^24.7.2",
|
|
82
90
|
"@types/react": "^19.0.0",
|
|
83
91
|
"consola": "^3.4.2",
|
|
84
92
|
"moment": "^2.30.1",
|
|
93
|
+
"next-intl": "^4.1.0",
|
|
85
94
|
"p-retry": "^7.0.0",
|
|
86
95
|
"swr": "^2.3.7",
|
|
87
96
|
"tsup": "^8.5.0",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support Extension I18n
|
|
3
|
+
*
|
|
4
|
+
* Self-contained translations - no app configuration needed.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { useSupportT } from '@djangocfg/ext-support/i18n';
|
|
9
|
+
*
|
|
10
|
+
* function MyComponent() {
|
|
11
|
+
* const t = useSupportT();
|
|
12
|
+
* return <h1>{t('layout.title')}</h1>;
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Self-contained hook (recommended)
|
|
18
|
+
export { useSupportT } from './useSupportT';
|
|
19
|
+
|
|
20
|
+
// Types
|
|
21
|
+
export type { SupportTranslations, SupportLocalKeys } from './types';
|
|
22
|
+
|
|
23
|
+
// Locales (for direct access if needed)
|
|
24
|
+
export { en } from './locales/en';
|
|
25
|
+
export { ru } from './locales/ru';
|
|
26
|
+
export { ko } from './locales/ko';
|
|
@@ -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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support Extension I18n Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper type to get dot-notation paths from nested object
|
|
7
|
+
*/
|
|
8
|
+
type PathKeys<T, Prefix extends string = ''> = T extends object
|
|
9
|
+
? {
|
|
10
|
+
[K in keyof T]: K extends string
|
|
11
|
+
? T[K] extends object
|
|
12
|
+
? PathKeys<T[K], `${Prefix}${K}.`>
|
|
13
|
+
: `${Prefix}${K}`
|
|
14
|
+
: never;
|
|
15
|
+
}[keyof T]
|
|
16
|
+
: never;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Keys for support translations
|
|
20
|
+
*/
|
|
21
|
+
export type SupportLocalKeys = PathKeys<SupportTranslations>;
|
|
22
|
+
|
|
23
|
+
export interface SupportTranslations {
|
|
24
|
+
/** Layout */
|
|
25
|
+
layout: {
|
|
26
|
+
title: string;
|
|
27
|
+
titleShort: string;
|
|
28
|
+
subtitle: string;
|
|
29
|
+
newTicket: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Ticket statuses */
|
|
33
|
+
status: {
|
|
34
|
+
open: string;
|
|
35
|
+
waitingForUser: string;
|
|
36
|
+
waitingForAdmin: string;
|
|
37
|
+
resolved: string;
|
|
38
|
+
closed: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Ticket list */
|
|
42
|
+
ticketList: {
|
|
43
|
+
noTickets: string;
|
|
44
|
+
noTicketsDescription: string;
|
|
45
|
+
loadingMore: string;
|
|
46
|
+
loadMore: string;
|
|
47
|
+
allLoaded: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Create ticket dialog */
|
|
51
|
+
createTicket: {
|
|
52
|
+
title: string;
|
|
53
|
+
description: string;
|
|
54
|
+
subjectLabel: string;
|
|
55
|
+
subjectPlaceholder: string;
|
|
56
|
+
messageLabel: string;
|
|
57
|
+
messagePlaceholder: string;
|
|
58
|
+
cancel: string;
|
|
59
|
+
creating: string;
|
|
60
|
+
create: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Validation */
|
|
64
|
+
validation: {
|
|
65
|
+
subjectRequired: string;
|
|
66
|
+
subjectTooLong: string;
|
|
67
|
+
messageRequired: string;
|
|
68
|
+
messageTooLong: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Messages */
|
|
72
|
+
messages: {
|
|
73
|
+
ticketCreated: string;
|
|
74
|
+
ticketCreateFailed: string;
|
|
75
|
+
messageSent: string;
|
|
76
|
+
messageSendFailed: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Message input */
|
|
80
|
+
messageInput: {
|
|
81
|
+
placeholder: string;
|
|
82
|
+
ticketClosed: string;
|
|
83
|
+
ticketClosedDescription: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Message list */
|
|
87
|
+
messageList: {
|
|
88
|
+
noTicketSelected: string;
|
|
89
|
+
noTicketSelectedDescription: string;
|
|
90
|
+
noMessages: string;
|
|
91
|
+
noMessagesDescription: string;
|
|
92
|
+
loadingOlder: string;
|
|
93
|
+
loadOlder: string;
|
|
94
|
+
supportTeam: string;
|
|
95
|
+
staff: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Time labels */
|
|
99
|
+
time: {
|
|
100
|
+
justNow: string;
|
|
101
|
+
minutesAgo: string;
|
|
102
|
+
hoursAgo: string;
|
|
103
|
+
daysAgo: string;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Self-contained translation hook for ext-support
|
|
5
|
+
*
|
|
6
|
+
* Uses built-in translations, no app configuration needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useLocale } from 'next-intl';
|
|
10
|
+
import { useMemo, useCallback } from 'react';
|
|
11
|
+
|
|
12
|
+
import type { SupportTranslations, SupportLocalKeys } from './types';
|
|
13
|
+
import { en } from './locales/en';
|
|
14
|
+
import { ru } from './locales/ru';
|
|
15
|
+
import { ko } from './locales/ko';
|
|
16
|
+
|
|
17
|
+
const translations: Record<string, SupportTranslations> = { en, ru, ko };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get nested value from object by dot-notation path
|
|
21
|
+
*/
|
|
22
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
|
23
|
+
const keys = path.split('.');
|
|
24
|
+
let result: unknown = obj;
|
|
25
|
+
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
if (result && typeof result === 'object' && key in result) {
|
|
28
|
+
result = (result as Record<string, unknown>)[key];
|
|
29
|
+
} else {
|
|
30
|
+
return path; // Return key if not found
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return typeof result === 'string' ? result : path;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Self-contained translation hook for support extension
|
|
39
|
+
*
|
|
40
|
+
* Uses built-in translations based on current locale from next-intl.
|
|
41
|
+
* No need to add translations to app's i18n config.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function TicketList() {
|
|
46
|
+
* const t = useSupportT();
|
|
47
|
+
* return <h1>{t('layout.title')}</h1>;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function useSupportT(): (key: SupportLocalKeys) => string {
|
|
52
|
+
const locale = useLocale();
|
|
53
|
+
|
|
54
|
+
const t = useMemo(() => translations[locale] || translations.en, [locale]);
|
|
55
|
+
|
|
56
|
+
return useCallback(
|
|
57
|
+
(key: SupportLocalKeys): string => getNestedValue(t as unknown as Record<string, unknown>, key),
|
|
58
|
+
[t]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -7,8 +7,9 @@
|
|
|
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 { useSupportT } from '../../i18n';
|
|
12
13
|
import { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@djangocfg/ui-core';
|
|
13
14
|
|
|
14
15
|
import { SupportProvider } from '../../contexts/SupportContext';
|
|
@@ -20,10 +21,18 @@ import { SupportLayoutProvider, useSupportLayoutContext } from './context';
|
|
|
20
21
|
// ─────────────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
22
23
|
const SupportLayoutContent: React.FC = () => {
|
|
24
|
+
const st = useSupportT();
|
|
23
25
|
const { selectedTicket, selectTicket, openCreateDialog, getUnreadCount } =
|
|
24
26
|
useSupportLayoutContext();
|
|
25
27
|
const [isMobile, setIsMobile] = React.useState(false);
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
const labels = useMemo(() => ({
|
|
30
|
+
title: st('layout.title'),
|
|
31
|
+
titleShort: st('layout.titleShort'),
|
|
32
|
+
subtitle: st('layout.subtitle'),
|
|
33
|
+
newTicket: st('layout.newTicket'),
|
|
34
|
+
}), [st]);
|
|
35
|
+
|
|
27
36
|
React.useEffect(() => {
|
|
28
37
|
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
|
29
38
|
checkMobile();
|
|
@@ -53,7 +62,7 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
53
62
|
<LifeBuoy className="h-6 w-6 text-primary" />
|
|
54
63
|
)}
|
|
55
64
|
<h1 className="text-xl font-semibold">
|
|
56
|
-
{selectedTicket ? selectedTicket.subject :
|
|
65
|
+
{selectedTicket ? selectedTicket.subject : labels.titleShort}
|
|
57
66
|
</h1>
|
|
58
67
|
{unreadCount > 0 && !selectedTicket && (
|
|
59
68
|
<div className="h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
@@ -64,7 +73,7 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
64
73
|
{!selectedTicket && (
|
|
65
74
|
<Button onClick={openCreateDialog} size="sm">
|
|
66
75
|
<Plus className="h-4 w-4 mr-2" />
|
|
67
|
-
|
|
76
|
+
{labels.newTicket}
|
|
68
77
|
</Button>
|
|
69
78
|
)}
|
|
70
79
|
</div>
|
|
@@ -101,8 +110,8 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
101
110
|
<div className="flex items-center gap-3">
|
|
102
111
|
<LifeBuoy className="h-7 w-7 text-primary" />
|
|
103
112
|
<div>
|
|
104
|
-
<h1 className="text-2xl font-bold">
|
|
105
|
-
<p className="text-sm text-muted-foreground">
|
|
113
|
+
<h1 className="text-2xl font-bold">{labels.title}</h1>
|
|
114
|
+
<p className="text-sm text-muted-foreground">{labels.subtitle}</p>
|
|
106
115
|
</div>
|
|
107
116
|
{unreadCount > 0 && (
|
|
108
117
|
<div className="h-6 w-6 bg-red-500 text-white text-sm rounded-full flex items-center justify-center">
|
|
@@ -113,7 +122,7 @@ const SupportLayoutContent: React.FC = () => {
|
|
|
113
122
|
|
|
114
123
|
<Button onClick={openCreateDialog}>
|
|
115
124
|
<Plus className="h-4 w-4 mr-2" />
|
|
116
|
-
|
|
125
|
+
{labels.newTicket}
|
|
117
126
|
</Button>
|
|
118
127
|
</div>
|
|
119
128
|
|
|
@@ -7,10 +7,11 @@
|
|
|
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 { useSupportT } from '../../../i18n';
|
|
14
15
|
import {
|
|
15
16
|
Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Form, FormControl,
|
|
16
17
|
FormField, FormItem, FormLabel, FormMessage, Input, Textarea, useToast
|
|
@@ -22,16 +23,37 @@ import { useSupportLayoutContext } from '../context';
|
|
|
22
23
|
|
|
23
24
|
import type { TicketFormData } from '../types';
|
|
24
25
|
|
|
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
26
|
export const CreateTicketDialog: React.FC = () => {
|
|
27
|
+
const st = useSupportT();
|
|
31
28
|
const { uiState, createTicket, closeCreateDialog } = useSupportLayoutContext();
|
|
32
29
|
const { toast } = useToast();
|
|
33
30
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
34
31
|
|
|
32
|
+
const labels = useMemo(() => ({
|
|
33
|
+
title: st('createTicket.title'),
|
|
34
|
+
description: st('createTicket.description'),
|
|
35
|
+
subjectLabel: st('createTicket.subjectLabel'),
|
|
36
|
+
subjectPlaceholder: st('createTicket.subjectPlaceholder'),
|
|
37
|
+
messageLabel: st('createTicket.messageLabel'),
|
|
38
|
+
messagePlaceholder: st('createTicket.messagePlaceholder'),
|
|
39
|
+
cancel: st('createTicket.cancel'),
|
|
40
|
+
creating: st('createTicket.creating'),
|
|
41
|
+
create: st('createTicket.create'),
|
|
42
|
+
ticketCreated: st('messages.ticketCreated'),
|
|
43
|
+
ticketCreateFailed: st('messages.ticketCreateFailed'),
|
|
44
|
+
validation: {
|
|
45
|
+
subjectRequired: st('validation.subjectRequired'),
|
|
46
|
+
subjectTooLong: st('validation.subjectTooLong'),
|
|
47
|
+
messageRequired: st('validation.messageRequired'),
|
|
48
|
+
messageTooLong: st('validation.messageTooLong'),
|
|
49
|
+
},
|
|
50
|
+
}), [st]);
|
|
51
|
+
|
|
52
|
+
const createTicketSchema = useMemo(() => z.object({
|
|
53
|
+
subject: z.string().min(1, labels.validation.subjectRequired).max(200, labels.validation.subjectTooLong),
|
|
54
|
+
message: z.string().min(1, labels.validation.messageRequired).max(5000, labels.validation.messageTooLong),
|
|
55
|
+
}), [labels.validation]);
|
|
56
|
+
|
|
35
57
|
const form = useForm<TicketFormData>({
|
|
36
58
|
resolver: zodResolver(createTicketSchema),
|
|
37
59
|
defaultValues: {
|
|
@@ -45,10 +67,10 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
45
67
|
try {
|
|
46
68
|
await createTicket(data);
|
|
47
69
|
form.reset();
|
|
48
|
-
toast.success(
|
|
70
|
+
toast.success(labels.ticketCreated);
|
|
49
71
|
} catch (error) {
|
|
50
72
|
supportLogger.error('Failed to create ticket:', error);
|
|
51
|
-
toast.error(
|
|
73
|
+
toast.error(labels.ticketCreateFailed);
|
|
52
74
|
} finally {
|
|
53
75
|
setIsSubmitting(false);
|
|
54
76
|
}
|
|
@@ -65,10 +87,10 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
65
87
|
<DialogHeader>
|
|
66
88
|
<DialogTitle className="flex items-center gap-2">
|
|
67
89
|
<Plus className="h-5 w-5" />
|
|
68
|
-
|
|
90
|
+
{labels.title}
|
|
69
91
|
</DialogTitle>
|
|
70
92
|
<DialogDescription>
|
|
71
|
-
|
|
93
|
+
{labels.description}
|
|
72
94
|
</DialogDescription>
|
|
73
95
|
</DialogHeader>
|
|
74
96
|
|
|
@@ -79,9 +101,9 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
79
101
|
name="subject"
|
|
80
102
|
render={({ field }) => (
|
|
81
103
|
<FormItem>
|
|
82
|
-
<FormLabel>
|
|
104
|
+
<FormLabel>{labels.subjectLabel}</FormLabel>
|
|
83
105
|
<FormControl>
|
|
84
|
-
<Input placeholder=
|
|
106
|
+
<Input placeholder={labels.subjectPlaceholder} {...field} />
|
|
85
107
|
</FormControl>
|
|
86
108
|
<FormMessage />
|
|
87
109
|
</FormItem>
|
|
@@ -93,10 +115,10 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
93
115
|
name="message"
|
|
94
116
|
render={({ field }) => (
|
|
95
117
|
<FormItem>
|
|
96
|
-
<FormLabel>
|
|
118
|
+
<FormLabel>{labels.messageLabel}</FormLabel>
|
|
97
119
|
<FormControl>
|
|
98
120
|
<Textarea
|
|
99
|
-
placeholder=
|
|
121
|
+
placeholder={labels.messagePlaceholder}
|
|
100
122
|
className="min-h-[120px]"
|
|
101
123
|
{...field}
|
|
102
124
|
/>
|
|
@@ -113,18 +135,18 @@ export const CreateTicketDialog: React.FC = () => {
|
|
|
113
135
|
onClick={handleClose}
|
|
114
136
|
disabled={isSubmitting}
|
|
115
137
|
>
|
|
116
|
-
|
|
138
|
+
{labels.cancel}
|
|
117
139
|
</Button>
|
|
118
140
|
<Button type="submit" disabled={isSubmitting}>
|
|
119
141
|
{isSubmitting ? (
|
|
120
142
|
<>
|
|
121
143
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
122
|
-
|
|
144
|
+
{labels.creating}
|
|
123
145
|
</>
|
|
124
146
|
) : (
|
|
125
147
|
<>
|
|
126
148
|
<Plus className="h-4 w-4 mr-2" />
|
|
127
|
-
|
|
149
|
+
{labels.create}
|
|
128
150
|
</>
|
|
129
151
|
)}
|
|
130
152
|
</Button>
|