@djangocfg/ext-support 1.0.0
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/README.md +233 -0
- package/dist/chunk-AZ4LWZB7.js +2630 -0
- package/dist/hooks.cjs +2716 -0
- package/dist/hooks.d.cts +255 -0
- package/dist/hooks.d.ts +255 -0
- package/dist/hooks.js +1 -0
- package/dist/index.cjs +2693 -0
- package/dist/index.d.cts +1392 -0
- package/dist/index.d.ts +1392 -0
- package/dist/index.js +1 -0
- package/package.json +80 -0
- package/src/api/generated/ext_support/_utils/fetchers/ext_support__support.ts +642 -0
- package/src/api/generated/ext_support/_utils/fetchers/index.ts +28 -0
- package/src/api/generated/ext_support/_utils/hooks/ext_support__support.ts +237 -0
- package/src/api/generated/ext_support/_utils/hooks/index.ts +28 -0
- package/src/api/generated/ext_support/_utils/schemas/Message.schema.ts +21 -0
- package/src/api/generated/ext_support/_utils/schemas/MessageCreate.schema.ts +15 -0
- package/src/api/generated/ext_support/_utils/schemas/MessageCreateRequest.schema.ts +15 -0
- package/src/api/generated/ext_support/_utils/schemas/MessageRequest.schema.ts +15 -0
- package/src/api/generated/ext_support/_utils/schemas/PaginatedMessageList.schema.ts +24 -0
- package/src/api/generated/ext_support/_utils/schemas/PaginatedTicketList.schema.ts +24 -0
- package/src/api/generated/ext_support/_utils/schemas/PatchedMessageRequest.schema.ts +15 -0
- package/src/api/generated/ext_support/_utils/schemas/PatchedTicketRequest.schema.ts +18 -0
- package/src/api/generated/ext_support/_utils/schemas/Sender.schema.ts +21 -0
- package/src/api/generated/ext_support/_utils/schemas/Ticket.schema.ts +21 -0
- package/src/api/generated/ext_support/_utils/schemas/TicketRequest.schema.ts +18 -0
- package/src/api/generated/ext_support/_utils/schemas/index.ts +29 -0
- package/src/api/generated/ext_support/api-instance.ts +131 -0
- package/src/api/generated/ext_support/client.ts +301 -0
- package/src/api/generated/ext_support/enums.ts +45 -0
- package/src/api/generated/ext_support/errors.ts +116 -0
- package/src/api/generated/ext_support/ext_support__support/client.ts +151 -0
- package/src/api/generated/ext_support/ext_support__support/index.ts +2 -0
- package/src/api/generated/ext_support/ext_support__support/models.ts +165 -0
- package/src/api/generated/ext_support/http.ts +103 -0
- package/src/api/generated/ext_support/index.ts +273 -0
- package/src/api/generated/ext_support/logger.ts +259 -0
- package/src/api/generated/ext_support/retry.ts +175 -0
- package/src/api/generated/ext_support/schema.json +1049 -0
- package/src/api/generated/ext_support/storage.ts +161 -0
- package/src/api/generated/ext_support/validation-events.ts +133 -0
- package/src/api/index.ts +9 -0
- package/src/config.ts +20 -0
- package/src/contexts/SupportContext.tsx +250 -0
- package/src/contexts/SupportExtensionProvider.tsx +38 -0
- package/src/contexts/types.ts +26 -0
- package/src/hooks/index.ts +33 -0
- package/src/index.ts +39 -0
- package/src/layouts/SupportLayout/README.md +91 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +179 -0
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +155 -0
- package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
- package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
- package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
- package/src/layouts/SupportLayout/components/TicketList.tsx +153 -0
- package/src/layouts/SupportLayout/components/index.ts +6 -0
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +258 -0
- package/src/layouts/SupportLayout/context/index.ts +2 -0
- package/src/layouts/SupportLayout/events.ts +33 -0
- package/src/layouts/SupportLayout/hooks/index.ts +2 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +115 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +88 -0
- package/src/layouts/SupportLayout/index.ts +6 -0
- package/src/layouts/SupportLayout/types.ts +21 -0
- package/src/utils/logger.ts +14 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @djangocfg/ext-support
|
|
3
|
+
*
|
|
4
|
+
* Support ticket system extension.
|
|
5
|
+
* This entry point is server-safe (no SWR/React hooks).
|
|
6
|
+
*
|
|
7
|
+
* For React hooks, import from '@djangocfg/ext-support/hooks'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// API client (server-safe)
|
|
11
|
+
export * from './api/generated/ext_support';
|
|
12
|
+
export { API } from './api/generated/ext_support';
|
|
13
|
+
export { apiSupport } from './api';
|
|
14
|
+
|
|
15
|
+
// Layout components
|
|
16
|
+
export { SupportLayout } from './layouts/SupportLayout';
|
|
17
|
+
export type { SupportLayoutProps } from './layouts/SupportLayout/SupportLayout';
|
|
18
|
+
|
|
19
|
+
// Events
|
|
20
|
+
export * from './layouts/SupportLayout/events';
|
|
21
|
+
|
|
22
|
+
// Types
|
|
23
|
+
export type { SupportUIState, TicketFormData } from './layouts/SupportLayout/types';
|
|
24
|
+
|
|
25
|
+
// Re-export API types for convenience
|
|
26
|
+
export type {
|
|
27
|
+
Ticket,
|
|
28
|
+
TicketRequest,
|
|
29
|
+
PatchedTicketRequest,
|
|
30
|
+
Message,
|
|
31
|
+
MessageRequest,
|
|
32
|
+
MessageCreateRequest,
|
|
33
|
+
PatchedMessageRequest,
|
|
34
|
+
PaginatedTicketList,
|
|
35
|
+
PaginatedMessageList,
|
|
36
|
+
} from './api/generated/ext_support/_utils/schemas';
|
|
37
|
+
|
|
38
|
+
// Note: Hooks are NOT exported here to keep this bundle server-safe
|
|
39
|
+
// Use: import { useSupportTicketsList } from '@djangocfg/ext-support/hooks'
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Support Layout
|
|
2
|
+
|
|
3
|
+
Modern support ticket system layout with resizable panels and mobile-optimized interface.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Desktop**: Resizable split-panel view (ticket list | conversation)
|
|
8
|
+
- ✅ **Mobile**: Single-column navigation with back/forward flow
|
|
9
|
+
- ✅ **Real-time**: Auto-refresh messages after sending
|
|
10
|
+
- ✅ **Event-driven**: Dialog management via custom events
|
|
11
|
+
- ✅ **Type-safe**: Full TypeScript support with generated API types
|
|
12
|
+
- ✅ **Smart UI**: Unread counters, status badges, relative timestamps
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
SupportLayout
|
|
18
|
+
├── SupportLayoutProvider (UI state + events wrapper)
|
|
19
|
+
│ └── SupportProvider (API context from @djangocfg/api)
|
|
20
|
+
│ └── AccountsProvider (for user.id in ticket creation)
|
|
21
|
+
└── Components
|
|
22
|
+
├── TicketList (scrollable ticket cards)
|
|
23
|
+
├── MessageList (conversation bubbles)
|
|
24
|
+
├── MessageInput (with keyboard shortcuts)
|
|
25
|
+
└── CreateTicketDialog (event-driven)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { SupportLayout } from '@djangocfg/layouts';
|
|
32
|
+
|
|
33
|
+
export default function SupportPage() {
|
|
34
|
+
return <SupportLayout />;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Event-based Dialog Opening
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { openCreateTicketDialog } from '@djangocfg/layouts';
|
|
42
|
+
|
|
43
|
+
function MyComponent() {
|
|
44
|
+
return (
|
|
45
|
+
<Button onClick={openCreateTicketDialog}>
|
|
46
|
+
Create Ticket
|
|
47
|
+
</Button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API Integration
|
|
53
|
+
|
|
54
|
+
Uses generated SWR hooks from `@djangocfg/api/cfg/contexts`:
|
|
55
|
+
- `useSupportContext()` - Tickets CRUD, messages CRUD
|
|
56
|
+
- Automatic cache revalidation after mutations
|
|
57
|
+
- Type-safe request/response handling
|
|
58
|
+
|
|
59
|
+
## Mobile Optimization
|
|
60
|
+
|
|
61
|
+
- Auto-detects screen width (≤768px)
|
|
62
|
+
- Single-column navigation when mobile
|
|
63
|
+
- Back button to return to ticket list
|
|
64
|
+
- Optimized touch targets
|
|
65
|
+
|
|
66
|
+
## Key Components
|
|
67
|
+
|
|
68
|
+
### TicketCard
|
|
69
|
+
- Status badges with color-coding
|
|
70
|
+
- Unread message counters
|
|
71
|
+
- Relative timestamps
|
|
72
|
+
- Click to select
|
|
73
|
+
|
|
74
|
+
### MessageList
|
|
75
|
+
- Auto-scroll to latest message
|
|
76
|
+
- User vs. Admin message styling
|
|
77
|
+
- Avatar placeholders
|
|
78
|
+
- Timestamp formatting
|
|
79
|
+
|
|
80
|
+
### MessageInput
|
|
81
|
+
- Multi-line support (Shift+Enter)
|
|
82
|
+
- Submit on Enter
|
|
83
|
+
- Disabled when ticket closed
|
|
84
|
+
- Loading states
|
|
85
|
+
|
|
86
|
+
### CreateTicketDialog
|
|
87
|
+
- Subject + initial message
|
|
88
|
+
- Zod validation
|
|
89
|
+
- Auto-selects created ticket
|
|
90
|
+
- Toast notifications
|
|
91
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Support Layout
|
|
4
|
+
* Modern support layout with resizable panels for desktop and mobile-optimized view
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { SupportProvider } from '../../contexts/SupportContext';
|
|
11
|
+
import { SupportLayoutProvider, useSupportLayoutContext } from './context';
|
|
12
|
+
import {
|
|
13
|
+
TicketList,
|
|
14
|
+
MessageList,
|
|
15
|
+
MessageInput,
|
|
16
|
+
CreateTicketDialog,
|
|
17
|
+
} from './components';
|
|
18
|
+
import {
|
|
19
|
+
Button,
|
|
20
|
+
ResizablePanelGroup,
|
|
21
|
+
ResizablePanel,
|
|
22
|
+
ResizableHandle,
|
|
23
|
+
} from '@djangocfg/ui-nextjs';
|
|
24
|
+
import { Plus, LifeBuoy, ArrowLeft } from 'lucide-react';
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Support Layout Content (with context)
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const SupportLayoutContent: React.FC = () => {
|
|
31
|
+
const { selectedTicket, selectTicket, openCreateDialog, getUnreadCount } =
|
|
32
|
+
useSupportLayoutContext();
|
|
33
|
+
const [isMobile, setIsMobile] = React.useState(false);
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
|
37
|
+
checkMobile();
|
|
38
|
+
window.addEventListener('resize', checkMobile);
|
|
39
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const unreadCount = getUnreadCount();
|
|
43
|
+
|
|
44
|
+
if (isMobile) {
|
|
45
|
+
// Mobile layout - single column with navigation
|
|
46
|
+
return (
|
|
47
|
+
<div className="h-screen flex flex-col overflow-hidden">
|
|
48
|
+
{/* Mobile Header */}
|
|
49
|
+
<div className="flex items-center justify-between p-4 border-b bg-background flex-shrink-0">
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
{selectedTicket ? (
|
|
52
|
+
<Button
|
|
53
|
+
variant="ghost"
|
|
54
|
+
size="sm"
|
|
55
|
+
onClick={() => selectTicket(null)}
|
|
56
|
+
className="p-1"
|
|
57
|
+
>
|
|
58
|
+
<ArrowLeft className="h-5 w-5" />
|
|
59
|
+
</Button>
|
|
60
|
+
) : (
|
|
61
|
+
<LifeBuoy className="h-6 w-6 text-primary" />
|
|
62
|
+
)}
|
|
63
|
+
<h1 className="text-xl font-semibold">
|
|
64
|
+
{selectedTicket ? selectedTicket.subject : 'Support'}
|
|
65
|
+
</h1>
|
|
66
|
+
{unreadCount > 0 && !selectedTicket && (
|
|
67
|
+
<div className="h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
68
|
+
{unreadCount}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
{!selectedTicket && (
|
|
73
|
+
<Button onClick={openCreateDialog} size="sm">
|
|
74
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
75
|
+
New Ticket
|
|
76
|
+
</Button>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Mobile Content */}
|
|
81
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
82
|
+
{selectedTicket ? (
|
|
83
|
+
// Show messages when ticket is selected
|
|
84
|
+
<div className="h-full flex flex-col">
|
|
85
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
86
|
+
<MessageList />
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex-shrink-0">
|
|
89
|
+
<MessageInput />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
// Show ticket list when no ticket is selected
|
|
94
|
+
<TicketList />
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Dialog */}
|
|
99
|
+
<CreateTicketDialog />
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Desktop layout - resizable panels
|
|
105
|
+
return (
|
|
106
|
+
<div className="h-screen flex flex-col overflow-hidden">
|
|
107
|
+
{/* Desktop Header */}
|
|
108
|
+
<div className="flex items-center justify-between p-6 border-b bg-background flex-shrink-0">
|
|
109
|
+
<div className="flex items-center gap-3">
|
|
110
|
+
<LifeBuoy className="h-7 w-7 text-primary" />
|
|
111
|
+
<div>
|
|
112
|
+
<h1 className="text-2xl font-bold">Support Center</h1>
|
|
113
|
+
<p className="text-sm text-muted-foreground">Get help from our support team</p>
|
|
114
|
+
</div>
|
|
115
|
+
{unreadCount > 0 && (
|
|
116
|
+
<div className="h-6 w-6 bg-red-500 text-white text-sm rounded-full flex items-center justify-center">
|
|
117
|
+
{unreadCount}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<Button onClick={openCreateDialog}>
|
|
123
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
124
|
+
New Ticket
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Desktop Content */}
|
|
129
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
130
|
+
<ResizablePanelGroup direction="horizontal" className="h-full">
|
|
131
|
+
{/* Ticket List Panel */}
|
|
132
|
+
<ResizablePanel defaultSize={35} minSize={25} maxSize={50}>
|
|
133
|
+
<div className="h-full border-r overflow-hidden">
|
|
134
|
+
<TicketList />
|
|
135
|
+
</div>
|
|
136
|
+
</ResizablePanel>
|
|
137
|
+
|
|
138
|
+
<ResizableHandle withHandle className="hover:bg-accent transition-colors" />
|
|
139
|
+
|
|
140
|
+
{/* Messages Panel */}
|
|
141
|
+
<ResizablePanel defaultSize={65} minSize={50}>
|
|
142
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
143
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
144
|
+
<MessageList />
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex-shrink-0">
|
|
147
|
+
<MessageInput />
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</ResizablePanel>
|
|
151
|
+
</ResizablePanelGroup>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Dialog */}
|
|
155
|
+
<CreateTicketDialog />
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
161
|
+
// Support Layout (with providers)
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export interface SupportLayoutProps {
|
|
165
|
+
children?: React.ReactNode;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const SupportLayout: React.FC<SupportLayoutProps> = () => {
|
|
169
|
+
return (
|
|
170
|
+
<div className="h-screen w-full overflow-hidden">
|
|
171
|
+
<SupportProvider>
|
|
172
|
+
<SupportLayoutProvider>
|
|
173
|
+
<SupportLayoutContent />
|
|
174
|
+
</SupportLayoutProvider>
|
|
175
|
+
</SupportProvider>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Create Ticket Dialog
|
|
4
|
+
* Dialog for creating new support tickets
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
Button,
|
|
17
|
+
Form,
|
|
18
|
+
FormField,
|
|
19
|
+
FormItem,
|
|
20
|
+
FormLabel,
|
|
21
|
+
FormControl,
|
|
22
|
+
FormMessage,
|
|
23
|
+
Input,
|
|
24
|
+
Textarea,
|
|
25
|
+
} from '@djangocfg/ui-nextjs';
|
|
26
|
+
import { useForm } from 'react-hook-form';
|
|
27
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { Plus, Loader2 } from 'lucide-react';
|
|
30
|
+
import { supportLogger } from '../../../utils/logger';
|
|
31
|
+
import { useSupportLayoutContext } from '../context';
|
|
32
|
+
import { useToast } from '@djangocfg/ui-nextjs';
|
|
33
|
+
import type { TicketFormData } from '../types';
|
|
34
|
+
|
|
35
|
+
const createTicketSchema = z.object({
|
|
36
|
+
subject: z.string().min(1, 'Subject is required').max(200, 'Subject too long'),
|
|
37
|
+
message: z.string().min(1, 'Message is required').max(5000, 'Message too long'),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const CreateTicketDialog: React.FC = () => {
|
|
41
|
+
const { uiState, createTicket, closeCreateDialog } = useSupportLayoutContext();
|
|
42
|
+
const { toast } = useToast();
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
44
|
+
|
|
45
|
+
const form = useForm<TicketFormData>({
|
|
46
|
+
resolver: zodResolver(createTicketSchema),
|
|
47
|
+
defaultValues: {
|
|
48
|
+
subject: '',
|
|
49
|
+
message: '',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const onSubmit = async (data: TicketFormData) => {
|
|
54
|
+
setIsSubmitting(true);
|
|
55
|
+
try {
|
|
56
|
+
await createTicket(data);
|
|
57
|
+
form.reset();
|
|
58
|
+
toast({
|
|
59
|
+
title: 'Success',
|
|
60
|
+
description: 'Support ticket created successfully',
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
supportLogger.error('Failed to create ticket:', error);
|
|
64
|
+
toast({
|
|
65
|
+
title: 'Error',
|
|
66
|
+
description: 'Failed to create ticket. Please try again.',
|
|
67
|
+
variant: 'destructive',
|
|
68
|
+
});
|
|
69
|
+
} finally {
|
|
70
|
+
setIsSubmitting(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleClose = () => {
|
|
75
|
+
form.reset();
|
|
76
|
+
closeCreateDialog();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Dialog open={uiState.isCreateDialogOpen} onOpenChange={(open) => !open && handleClose()}>
|
|
81
|
+
<DialogContent className="sm:max-w-[600px] animate-in fade-in slide-in-from-bottom-4 duration-300">
|
|
82
|
+
<DialogHeader>
|
|
83
|
+
<DialogTitle className="flex items-center gap-2">
|
|
84
|
+
<Plus className="h-5 w-5" />
|
|
85
|
+
Create Support Ticket
|
|
86
|
+
</DialogTitle>
|
|
87
|
+
<DialogDescription>
|
|
88
|
+
Describe your issue and we'll help you resolve it as quickly as possible.
|
|
89
|
+
</DialogDescription>
|
|
90
|
+
</DialogHeader>
|
|
91
|
+
|
|
92
|
+
<Form {...form}>
|
|
93
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
94
|
+
<FormField
|
|
95
|
+
control={form.control}
|
|
96
|
+
name="subject"
|
|
97
|
+
render={({ field }) => (
|
|
98
|
+
<FormItem>
|
|
99
|
+
<FormLabel>Subject</FormLabel>
|
|
100
|
+
<FormControl>
|
|
101
|
+
<Input placeholder="Brief description of your issue..." {...field} />
|
|
102
|
+
</FormControl>
|
|
103
|
+
<FormMessage />
|
|
104
|
+
</FormItem>
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<FormField
|
|
109
|
+
control={form.control}
|
|
110
|
+
name="message"
|
|
111
|
+
render={({ field }) => (
|
|
112
|
+
<FormItem>
|
|
113
|
+
<FormLabel>Message</FormLabel>
|
|
114
|
+
<FormControl>
|
|
115
|
+
<Textarea
|
|
116
|
+
placeholder="Describe your issue in detail. Include any error messages, steps to reproduce, or relevant information..."
|
|
117
|
+
className="min-h-[120px]"
|
|
118
|
+
{...field}
|
|
119
|
+
/>
|
|
120
|
+
</FormControl>
|
|
121
|
+
<FormMessage />
|
|
122
|
+
</FormItem>
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<div className="flex justify-end gap-3 pt-4">
|
|
127
|
+
<Button
|
|
128
|
+
type="button"
|
|
129
|
+
variant="outline"
|
|
130
|
+
onClick={handleClose}
|
|
131
|
+
disabled={isSubmitting}
|
|
132
|
+
>
|
|
133
|
+
Cancel
|
|
134
|
+
</Button>
|
|
135
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
136
|
+
{isSubmitting ? (
|
|
137
|
+
<>
|
|
138
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
139
|
+
Creating...
|
|
140
|
+
</>
|
|
141
|
+
) : (
|
|
142
|
+
<>
|
|
143
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
144
|
+
Create Ticket
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</Button>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
</Form>
|
|
151
|
+
</DialogContent>
|
|
152
|
+
</Dialog>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Input Component
|
|
3
|
+
* Input field for sending messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { Button, Textarea, useToast } from '@djangocfg/ui-nextjs';
|
|
10
|
+
import { Send } from 'lucide-react';
|
|
11
|
+
import { supportLogger } from '../../../utils/logger';
|
|
12
|
+
import { useSupportLayoutContext } from '../context';
|
|
13
|
+
|
|
14
|
+
export const MessageInput: React.FC = () => {
|
|
15
|
+
const { selectedTicket, sendMessage } = useSupportLayoutContext();
|
|
16
|
+
const { toast } = useToast();
|
|
17
|
+
const [message, setMessage] = useState('');
|
|
18
|
+
const [isSending, setIsSending] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
|
|
23
|
+
if (!message.trim() || !selectedTicket) return;
|
|
24
|
+
|
|
25
|
+
setIsSending(true);
|
|
26
|
+
try {
|
|
27
|
+
await sendMessage(message.trim());
|
|
28
|
+
setMessage('');
|
|
29
|
+
toast({
|
|
30
|
+
title: 'Success',
|
|
31
|
+
description: 'Message sent successfully',
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
supportLogger.error('Failed to send message:', error);
|
|
35
|
+
toast({
|
|
36
|
+
title: 'Error',
|
|
37
|
+
description: 'Failed to send message',
|
|
38
|
+
variant: 'destructive',
|
|
39
|
+
});
|
|
40
|
+
} finally {
|
|
41
|
+
setIsSending(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
46
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
handleSubmit(e);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!selectedTicket) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const canSendMessage = selectedTicket.status !== 'closed';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<form onSubmit={handleSubmit} className="p-4 border-t bg-background/50 backdrop-blur-sm flex-shrink-0">
|
|
60
|
+
<div className="flex gap-2">
|
|
61
|
+
<Textarea
|
|
62
|
+
value={message}
|
|
63
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
64
|
+
onKeyDown={handleKeyDown}
|
|
65
|
+
placeholder={
|
|
66
|
+
canSendMessage
|
|
67
|
+
? 'Type your message... (Shift+Enter for new line)'
|
|
68
|
+
: 'This ticket is closed'
|
|
69
|
+
}
|
|
70
|
+
className="min-h-[60px] max-h-[200px] transition-all duration-200
|
|
71
|
+
focus:ring-2 focus:ring-primary/20"
|
|
72
|
+
disabled={!canSendMessage || isSending}
|
|
73
|
+
/>
|
|
74
|
+
<Button
|
|
75
|
+
type="submit"
|
|
76
|
+
size="icon"
|
|
77
|
+
disabled={!message.trim() || !canSendMessage || isSending}
|
|
78
|
+
className="shrink-0 transition-all duration-200
|
|
79
|
+
hover:scale-110 active:scale-95 disabled:scale-100"
|
|
80
|
+
>
|
|
81
|
+
<Send className="h-4 w-4" />
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
{!canSendMessage && (
|
|
85
|
+
<p className="text-xs text-muted-foreground mt-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
86
|
+
This ticket is closed. You cannot send new messages.
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
</form>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|