ajaxter-chat 3.0.8 → 3.0.10

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.
Files changed (35) hide show
  1. package/dist/components/ChatScreen/index.d.ts +2 -0
  2. package/dist/components/ChatScreen/index.js +283 -34
  3. package/dist/components/ChatWidget.js +111 -15
  4. package/dist/components/HomeScreen/index.d.ts +2 -0
  5. package/dist/components/HomeScreen/index.js +2 -2
  6. package/dist/components/Tabs/BottomTabs.d.ts +0 -1
  7. package/dist/components/Tabs/BottomTabs.js +13 -20
  8. package/dist/components/TicketDetailScreen/index.d.ts +9 -0
  9. package/dist/components/TicketDetailScreen/index.js +46 -0
  10. package/dist/components/TicketFormScreen/index.d.ts +9 -0
  11. package/dist/components/TicketFormScreen/index.js +76 -0
  12. package/dist/components/TicketScreen/index.d.ts +2 -1
  13. package/dist/components/TicketScreen/index.js +8 -35
  14. package/dist/components/UserListScreen/index.d.ts +4 -0
  15. package/dist/components/UserListScreen/index.js +21 -3
  16. package/dist/types/index.d.ts +3 -1
  17. package/dist/utils/fileName.d.ts +2 -0
  18. package/dist/utils/fileName.js +7 -0
  19. package/dist/utils/messageSound.d.ts +4 -0
  20. package/dist/utils/messageSound.js +51 -0
  21. package/dist/utils/widgetSession.d.ts +13 -0
  22. package/dist/utils/widgetSession.js +24 -0
  23. package/package.json +1 -1
  24. package/src/components/ChatScreen/index.tsx +415 -58
  25. package/src/components/ChatWidget.tsx +140 -17
  26. package/src/components/HomeScreen/index.tsx +4 -2
  27. package/src/components/Tabs/BottomTabs.tsx +2 -22
  28. package/src/components/TicketDetailScreen/index.tsx +111 -0
  29. package/src/components/TicketFormScreen/index.tsx +151 -0
  30. package/src/components/TicketScreen/index.tsx +18 -58
  31. package/src/components/UserListScreen/index.tsx +51 -5
  32. package/src/types/index.ts +4 -0
  33. package/src/utils/fileName.ts +6 -0
  34. package/src/utils/messageSound.ts +47 -0
  35. package/src/utils/widgetSession.ts +34 -0
@@ -1,18 +1,22 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useEffect, useCallback } from 'react';
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
5
5
  import { loadLocalConfig } from '../config';
6
6
  import { mergeTheme } from '../utils/theme';
7
7
  import { useRemoteConfig } from '../hooks/useRemoteConfig';
8
8
  import { useChat } from '../hooks/useChat';
9
9
  import { useWebRTC } from '../hooks/useWebRTC';
10
+ import { saveSession, loadSession } from '../utils/widgetSession';
11
+ import { playMessageSound, getMessageSoundEnabled, setMessageSoundEnabled } from '../utils/messageSound';
10
12
 
11
13
  import { HomeScreen } from './HomeScreen';
12
14
  import { UserListScreen } from './UserListScreen';
13
15
  import { ChatScreen } from './ChatScreen';
14
16
  import { RecentChatsScreen } from './RecentChatsScreen';
15
17
  import { TicketScreen } from './TicketScreen';
18
+ import { TicketDetailScreen } from './TicketDetailScreen';
19
+ import { TicketFormScreen } from './TicketFormScreen';
16
20
  import { BlockListScreen } from './BlockList';
17
21
  import { CallScreen } from './CallScreen';
18
22
  import { MaintenanceView } from './MaintenanceView';
@@ -43,6 +47,9 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
43
47
  const [activeTab, setActiveTab] = useState<BottomTab>('home');
44
48
  const [screen, setScreen] = useState<Screen>('home');
45
49
  const [userListCtx, setUserListCtx] = useState<UserListContext>('support');
50
+ const [chatReturnCtx, setChatReturnCtx] = useState<UserListContext>('conversation');
51
+ const [viewingTicketId, setViewingTicketId] = useState<string | null>(null);
52
+ const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
46
53
 
47
54
  /* App state */
48
55
  const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
@@ -88,16 +95,81 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
88
95
  setIsOpen(true);
89
96
  };
90
97
 
98
+ const persistWidgetState = useCallback(() => {
99
+ const w = data?.widget;
100
+ if (!w) return;
101
+ saveSession(w.id, {
102
+ screen,
103
+ activeTab,
104
+ userListCtx,
105
+ activeUserUid: activeUser?.uid ?? null,
106
+ messages,
107
+ viewingTicketId,
108
+ chatReturnCtx,
109
+ });
110
+ }, [data?.widget, screen, activeTab, userListCtx, activeUser?.uid, messages, viewingTicketId, chatReturnCtx]);
111
+
91
112
  const closeDrawer = useCallback(() => {
113
+ persistWidgetState();
92
114
  setClosing(true);
93
115
  setTimeout(() => {
94
116
  setIsOpen(false);
95
117
  setClosing(false);
96
- setScreen('home');
97
- setActiveTab('home');
98
- clearChat();
99
118
  }, 300);
100
- }, [clearChat]);
119
+ }, [persistWidgetState]);
120
+
121
+ const restoredRef = useRef(false);
122
+ useEffect(() => {
123
+ if (!data?.widget || restoredRef.current) return;
124
+ const w = data.widget;
125
+ setMessageSoundEnabledState(getMessageSoundEnabled(w.id));
126
+ const p = loadSession(w.id);
127
+ if (p) {
128
+ setScreen(p.screen);
129
+ setActiveTab(p.activeTab);
130
+ setUserListCtx(p.userListCtx);
131
+ setViewingTicketId(p.viewingTicketId ?? null);
132
+ setChatReturnCtx(p.chatReturnCtx ?? 'conversation');
133
+ if (p.activeUserUid) {
134
+ const u = [...data.developers, ...data.users].find(x => x.uid === p.activeUserUid);
135
+ if (u) {
136
+ const hist = Array.isArray(p.messages) && p.messages.length
137
+ ? p.messages
138
+ : (data.sampleChats[u.uid] ?? []);
139
+ selectUser(u, hist);
140
+ }
141
+ }
142
+ }
143
+ restoredRef.current = true;
144
+ }, [data, selectUser]);
145
+
146
+ useEffect(() => {
147
+ if (!data?.widget) return;
148
+ persistWidgetState();
149
+ }, [data?.widget?.id, screen, activeTab, userListCtx, activeUser?.uid, messages, viewingTicketId, chatReturnCtx, persistWidgetState]);
150
+
151
+ const incomingSoundRef = useRef(0);
152
+ useEffect(() => {
153
+ incomingSoundRef.current = messages.length;
154
+ }, [activeUser?.uid]);
155
+
156
+ useEffect(() => {
157
+ if (!messageSoundEnabled || !activeUser || !data?.widget) return;
158
+ if (messages.length < incomingSoundRef.current) {
159
+ incomingSoundRef.current = messages.length;
160
+ return;
161
+ }
162
+ const added = messages.slice(incomingSoundRef.current);
163
+ incomingSoundRef.current = messages.length;
164
+ if (added.some(m => m.senderId !== 'me')) playMessageSound();
165
+ }, [messages, messageSoundEnabled, activeUser, data?.widget]);
166
+
167
+ const toggleMessageSound = useCallback((enabled: boolean) => {
168
+ const w = data?.widget;
169
+ if (!w) return;
170
+ setMessageSoundEnabled(w.id, enabled);
171
+ setMessageSoundEnabledState(enabled);
172
+ }, [data?.widget]);
101
173
 
102
174
  /* ── Navigation ──────────────────────────────────────────────────────── */
103
175
  const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
@@ -121,18 +193,34 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
121
193
  }
122
194
  }, [clearChat]);
123
195
 
124
- const handleSelectUser = useCallback((user: ChatUser) => {
125
- // Load history from sample chats if available
196
+ const listCtxForUser = useCallback((user: ChatUser, viewerIsDev: boolean): UserListContext => {
197
+ if (viewerIsDev) return user.type === 'user' ? 'support' : 'conversation';
198
+ return user.type === 'developer' ? 'support' : 'conversation';
199
+ }, []);
200
+
201
+ const handleSelectUser = useCallback((user: ChatUser, returnCtxOverride?: UserListContext) => {
202
+ setChatReturnCtx(returnCtxOverride ?? userListCtx);
126
203
  const history = data?.sampleChats[user.uid] ?? [];
127
204
  selectUser(user, history);
128
205
  setScreen('chat');
129
- // Update recent chats
130
206
  setRecentChats(prev => {
131
207
  const exists = prev.find(r => r.user.uid === user.uid);
132
208
  if (exists) return prev;
133
209
  return [{ id: `rc_${user.uid}`, user, lastMessage: '', lastTime: new Date().toISOString(), unread: 0, isPaused: false }, ...prev];
134
210
  });
135
- }, [data, selectUser]);
211
+ }, [data, selectUser, userListCtx]);
212
+
213
+ const handleBackFromChat = useCallback(() => {
214
+ clearChat();
215
+ setUserListCtx(chatReturnCtx);
216
+ setScreen('user-list');
217
+ }, [clearChat, chatReturnCtx]);
218
+
219
+ const handleOpenTicket = useCallback((id: string) => {
220
+ setViewingTicketId(id);
221
+ setScreen('ticket-detail');
222
+ setActiveTab('tickets');
223
+ }, []);
136
224
 
137
225
  const handleTabChange = useCallback((tab: BottomTab) => {
138
226
  setActiveTab(tab);
@@ -144,7 +232,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
144
232
  if (!activeUser) return;
145
233
  setBlockedUids(prev => [...prev, activeUser.uid]);
146
234
  clearChat();
147
- setScreen('home');
235
+ setScreen('block-list');
148
236
  setActiveTab('home');
149
237
  }, [activeUser, clearChat]);
150
238
 
@@ -161,7 +249,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
161
249
  updatedAt: new Date().toISOString(),
162
250
  assignedTo: null,
163
251
  };
164
- setTickets(prev => [t, ...prev]);
252
+ setTickets(prev => [...prev, t]);
253
+ setViewingTicketId(t.id);
254
+ setScreen('ticket-detail');
255
+ setActiveTab('tickets');
165
256
  }, []);
166
257
 
167
258
  /* ── Pause sync back into recent chats ──────────────────────────────── */
@@ -412,7 +503,12 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
412
503
  <div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
413
504
 
414
505
  {screen === 'home' && (
415
- <HomeScreen config={widgetConfig} onNavigate={handleCardClick} tickets={tickets} />
506
+ <HomeScreen
507
+ config={widgetConfig}
508
+ onNavigate={handleCardClick}
509
+ onOpenTicket={handleOpenTicket}
510
+ tickets={tickets}
511
+ />
416
512
  )}
417
513
 
418
514
  {screen === 'user-list' && (
@@ -423,6 +519,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
423
519
  viewerType={widgetConfig.viewerType ?? 'user'}
424
520
  onBack={() => setScreen('home')}
425
521
  onSelectUser={handleSelectUser}
522
+ onBlockList={userListCtx === 'conversation' ? () => setScreen('block-list') : undefined}
523
+ useHomeHeader={userListCtx === 'support' && widgetConfig.viewerType !== 'developer'}
426
524
  />
427
525
  )}
428
526
 
@@ -435,7 +533,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
435
533
  isReported={isReported}
436
534
  isBlocked={isBlocked}
437
535
  onSend={sendMessage}
438
- onBack={() => { clearChat(); setScreen('home'); setActiveTab('home'); }}
536
+ onBack={handleBackFromChat}
439
537
  onClose={closeDrawer}
440
538
  onTogglePause={handleTogglePause}
441
539
  onReport={reportChat}
@@ -444,6 +542,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
444
542
  onNavAction={handleNavFromMenu}
445
543
  otherDevelopers={otherDevelopers}
446
544
  onTransferToDeveloper={handleTransferToDeveloper}
545
+ messageSoundEnabled={messageSoundEnabled}
546
+ onToggleMessageSound={toggleMessageSound}
447
547
  />
448
548
  )}
449
549
 
@@ -463,7 +563,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
463
563
  <RecentChatsScreen
464
564
  chats={recentChats}
465
565
  config={widgetConfig}
466
- onSelectChat={handleSelectUser}
566
+ onSelectChat={u => handleSelectUser(u, listCtxForUser(u, viewerIsDev))}
467
567
  />
468
568
  )}
469
569
 
@@ -471,10 +571,32 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
471
571
  <TicketScreen
472
572
  tickets={tickets}
473
573
  config={widgetConfig}
474
- onRaiseTicket={handleRaiseTicket}
574
+ onNewTicket={() => setScreen('ticket-new')}
575
+ onSelectTicket={id => { setViewingTicketId(id); setScreen('ticket-detail'); }}
475
576
  />
476
577
  )}
477
578
 
579
+ {screen === 'ticket-new' && (
580
+ <TicketFormScreen
581
+ config={widgetConfig}
582
+ onSubmit={handleRaiseTicket}
583
+ onCancel={() => setScreen('tickets')}
584
+ />
585
+ )}
586
+
587
+ {screen === 'ticket-detail' && viewingTicketId && (
588
+ (() => {
589
+ const t = tickets.find(x => x.id === viewingTicketId);
590
+ return t ? (
591
+ <TicketDetailScreen
592
+ ticket={t}
593
+ config={widgetConfig}
594
+ onBack={() => { setViewingTicketId(null); setScreen('tickets'); }}
595
+ />
596
+ ) : null;
597
+ })()
598
+ )}
599
+
478
600
  {screen === 'block-list' && (
479
601
  <BlockListScreen
480
602
  blockedUsers={blockedUsers}
@@ -491,12 +613,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
491
613
  screen !== 'chat' &&
492
614
  screen !== 'call' &&
493
615
  screen !== 'user-list' &&
494
- screen !== 'block-list' && (
616
+ screen !== 'block-list' &&
617
+ screen !== 'ticket-detail' &&
618
+ screen !== 'ticket-new' && (
495
619
  <BottomTabs
496
620
  active={activeTab}
497
621
  onChange={handleTabChange}
498
622
  primaryColor={primaryColor}
499
- onBlockList={() => setScreen('block-list')}
500
623
  />
501
624
  )}
502
625
  </>
@@ -6,10 +6,12 @@ import { truncateWords } from '../../utils/chat';
6
6
  interface HomeScreenProps {
7
7
  config: WidgetConfig;
8
8
  onNavigate: (ctx: UserListContext | 'ticket') => void;
9
+ /** Open a specific pending ticket (full detail) */
10
+ onOpenTicket: (ticketId: string) => void;
9
11
  tickets: Ticket[];
10
12
  }
11
13
 
12
- export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tickets }) => {
14
+ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOpenTicket, tickets }) => {
13
15
  const [menuOpen, setMenuOpen] = useState(false);
14
16
  const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
15
17
  const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
@@ -110,7 +112,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
110
112
  <button
111
113
  key={t.id}
112
114
  type="button"
113
- onClick={() => onNavigate('ticket')}
115
+ onClick={() => onOpenTicket(t.id)}
114
116
  style={{
115
117
  width: '100%',
116
118
  textAlign: 'left',
@@ -5,10 +5,9 @@ interface BottomTabsProps {
5
5
  active: BottomTab;
6
6
  onChange: (tab: BottomTab) => void;
7
7
  primaryColor: string;
8
- onBlockList: () => void;
9
8
  }
10
9
 
11
- export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primaryColor, onBlockList }) => {
10
+ export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primaryColor }) => {
12
11
  const tabs: { key: BottomTab; label: string; Icon: React.FC<{ a: boolean; c: string }> }[] = [
13
12
  { key: 'home', label: 'Home', Icon: HomeIcon },
14
13
  { key: 'chats', label: 'Chats', Icon: ChatsIcon },
@@ -25,6 +24,7 @@ export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primar
25
24
  return (
26
25
  <button
27
26
  key={tab.key}
27
+ type="button"
28
28
  onClick={() => onChange(tab.key)}
29
29
  style={{
30
30
  flex: 1, padding: '10px 0 8px',
@@ -42,26 +42,6 @@ export const BottomTabs: React.FC<BottomTabsProps> = ({ active, onChange, primar
42
42
  </button>
43
43
  );
44
44
  })}
45
-
46
- {/* Block list icon */}
47
- <button
48
- onClick={onBlockList}
49
- style={{
50
- padding: '10px 14px 8px',
51
- display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
52
- background: 'transparent', border: 'none', cursor: 'pointer',
53
- fontSize: '10px', fontWeight: 500, color: '#9aa3af',
54
- borderTop: '2px solid transparent',
55
- transition: 'color 0.15s', fontFamily: 'inherit',
56
- }}
57
- title="Block List"
58
- >
59
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
60
- <circle cx="12" cy="12" r="10" stroke="#b0bec5" strokeWidth="1.8" />
61
- <line x1="4.93" y1="4.93" x2="19.07" y2="19.07" stroke="#b0bec5" strokeWidth="1.8" strokeLinecap="round" />
62
- </svg>
63
- Blocked
64
- </button>
65
45
  </div>
66
46
  );
67
47
  };
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { Ticket, WidgetConfig } from '../../types';
3
+
4
+ interface TicketDetailScreenProps {
5
+ ticket: Ticket;
6
+ config: WidgetConfig;
7
+ onBack: () => void;
8
+ }
9
+
10
+ const sm: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
11
+ open: { label: 'Open', bg: '', color: '' },
12
+ 'in-progress': { label: 'In Progress', bg: '#fef3c7', color: '#d97706' },
13
+ resolved: { label: 'Resolved', bg: '#f0fdf4', color: '#16a34a' },
14
+ closed: { label: 'Closed', bg: '#f3f4f6', color: '#6b7280' },
15
+ };
16
+
17
+ const pm: Record<Ticket['priority'], { label: string; color: string }> = {
18
+ low: { label: 'Low', color: '#6b7280' },
19
+ medium: { label: 'Medium', color: '#d97706' },
20
+ high: { label: 'High', color: '#ef4444' },
21
+ };
22
+
23
+ export const TicketDetailScreen: React.FC<TicketDetailScreenProps> = ({ ticket, config, onBack }) => {
24
+ const st = sm[ticket.status];
25
+ const pr = pm[ticket.priority];
26
+ const stBg = ticket.status === 'open' ? `${config.primaryColor}14` : st.bg;
27
+ const stColor = ticket.status === 'open' ? config.primaryColor : st.color;
28
+
29
+ return (
30
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }}>
31
+ <div
32
+ style={{
33
+ background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
34
+ padding: '14px 18px',
35
+ display: 'flex',
36
+ alignItems: 'center',
37
+ gap: 12,
38
+ flexShrink: 0,
39
+ }}
40
+ >
41
+ <button
42
+ type="button"
43
+ onClick={onBack}
44
+ style={{
45
+ background: 'rgba(255,255,255,0.22)',
46
+ border: 'none',
47
+ borderRadius: '50%',
48
+ width: 36,
49
+ height: 36,
50
+ display: 'flex',
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ cursor: 'pointer',
54
+ flexShrink: 0,
55
+ }}
56
+ >
57
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
58
+ <path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
59
+ </svg>
60
+ </button>
61
+ <div style={{ flex: 1, minWidth: 0 }}>
62
+ <div style={{ fontWeight: 800, fontSize: 16, color: '#fff', lineHeight: 1.25 }}>{ticket.title}</div>
63
+ <div style={{ fontSize: 12, color: 'rgba(255,255,255,0.85)', marginTop: 2 }}>#{ticket.id}</div>
64
+ </div>
65
+ </div>
66
+
67
+ <div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px' }}>
68
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
69
+ <span
70
+ style={{
71
+ fontSize: 11,
72
+ fontWeight: 700,
73
+ padding: '5px 12px',
74
+ borderRadius: 20,
75
+ backgroundColor: stBg,
76
+ color: stColor,
77
+ textTransform: 'uppercase',
78
+ letterSpacing: '0.04em',
79
+ }}
80
+ >
81
+ {st.label}
82
+ </span>
83
+ <span style={{ fontSize: 11, fontWeight: 700, padding: '5px 12px', borderRadius: 20, color: pr.color, background: `${pr.color}15` }}>
84
+ ● {pr.label} priority
85
+ </span>
86
+ </div>
87
+
88
+ <h3 style={{ margin: '0 0 8px', fontSize: 13, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
89
+ Description
90
+ </h3>
91
+ <p style={{ margin: '0 0 24px', fontSize: 15, color: '#1e293b', lineHeight: 1.65, whiteSpace: 'pre-wrap' }}>
92
+ {ticket.description || '—'}
93
+ </p>
94
+
95
+ <div style={{ fontSize: 13, color: '#94a3b8', display: 'grid', gap: 8 }}>
96
+ <div>
97
+ <strong style={{ color: '#64748b' }}>Created</strong> · {new Date(ticket.createdAt).toLocaleString()}
98
+ </div>
99
+ <div>
100
+ <strong style={{ color: '#64748b' }}>Updated</strong> · {new Date(ticket.updatedAt).toLocaleString()}
101
+ </div>
102
+ {ticket.assignedTo && (
103
+ <div>
104
+ <strong style={{ color: '#64748b' }}>Assigned</strong> · {ticket.assignedTo}
105
+ </div>
106
+ )}
107
+ </div>
108
+ </div>
109
+ </div>
110
+ );
111
+ };
@@ -0,0 +1,151 @@
1
+ import React, { useState } from 'react';
2
+ import { Ticket, WidgetConfig } from '../../types';
3
+
4
+ interface TicketFormScreenProps {
5
+ config: WidgetConfig;
6
+ onSubmit: (title: string, desc: string, priority: Ticket['priority']) => void;
7
+ onCancel: () => void;
8
+ }
9
+
10
+ function inputStyle(primaryColor: string): React.CSSProperties {
11
+ return {
12
+ width: '100%',
13
+ padding: '11px 14px',
14
+ borderRadius: 10,
15
+ border: '1.5px solid #e5e7eb',
16
+ outline: 'none',
17
+ fontSize: 14,
18
+ color: '#1a2332',
19
+ boxSizing: 'border-box',
20
+ fontFamily: 'inherit',
21
+ transition: 'border-color 0.2s',
22
+ };
23
+ }
24
+
25
+ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSubmit, onCancel }) => {
26
+ const [title, setTitle] = useState('');
27
+ const [desc, setDesc] = useState('');
28
+ const [priority, setPriority] = useState<Ticket['priority']>('medium');
29
+
30
+ const pm: Record<Ticket['priority'], { label: string; color: string }> = {
31
+ low: { label: 'Low', color: '#6b7280' },
32
+ medium: { label: 'Medium', color: '#d97706' },
33
+ high: { label: 'High', color: '#ef4444' },
34
+ };
35
+
36
+ const handleSubmit = () => {
37
+ if (!title.trim()) return;
38
+ onSubmit(title.trim(), desc.trim(), priority);
39
+ };
40
+
41
+ return (
42
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff', minHeight: 0 }}>
43
+ <div
44
+ style={{
45
+ background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
46
+ padding: '14px 18px',
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ gap: 12,
50
+ flexShrink: 0,
51
+ }}
52
+ >
53
+ <button
54
+ type="button"
55
+ onClick={onCancel}
56
+ style={{
57
+ background: 'rgba(255,255,255,0.22)',
58
+ border: 'none',
59
+ borderRadius: '50%',
60
+ width: 36,
61
+ height: 36,
62
+ display: 'flex',
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ cursor: 'pointer',
66
+ }}
67
+ >
68
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
69
+ <path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
70
+ </svg>
71
+ </button>
72
+ <div>
73
+ <h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }}>New ticket</h2>
74
+ <p style={{ margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }}>Describe your issue below</p>
75
+ </div>
76
+ </div>
77
+
78
+ <div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }}>
79
+ <input
80
+ placeholder="Title *"
81
+ value={title}
82
+ onChange={e => setTitle(e.target.value)}
83
+ style={inputStyle(config.primaryColor)}
84
+ onFocus={e => (e.target.style.borderColor = config.primaryColor)}
85
+ onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
86
+ />
87
+ <textarea
88
+ placeholder="Describe the issue…"
89
+ value={desc}
90
+ onChange={e => setDesc(e.target.value)}
91
+ rows={5}
92
+ style={{ ...inputStyle(config.primaryColor), resize: 'none', marginTop: 12 }}
93
+ onFocus={e => (e.target.style.borderColor = config.primaryColor)}
94
+ onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
95
+ />
96
+ <div style={{ display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }}>
97
+ {(['low', 'medium', 'high'] as Ticket['priority'][]).map(p => (
98
+ <button
99
+ key={p}
100
+ type="button"
101
+ onClick={() => setPriority(p)}
102
+ style={{
103
+ flex: 1,
104
+ padding: '8px',
105
+ border: `1.5px solid ${priority === p ? pm[p].color : '#e5e7eb'}`,
106
+ borderRadius: 8,
107
+ background: priority === p ? `${pm[p].color}15` : '#fff',
108
+ color: pm[p].color,
109
+ fontWeight: 700,
110
+ fontSize: 12,
111
+ cursor: 'pointer',
112
+ textTransform: 'capitalize',
113
+ }}
114
+ >
115
+ {pm[p].label}
116
+ </button>
117
+ ))}
118
+ </div>
119
+ </div>
120
+
121
+ <div
122
+ style={{
123
+ flexShrink: 0,
124
+ padding: '12px 18px 18px',
125
+ borderTop: '1px solid #eef0f5',
126
+ background: '#fff',
127
+ boxShadow: '0 -4px 20px rgba(15,23,42,0.06)',
128
+ }}
129
+ >
130
+ <button
131
+ type="button"
132
+ onClick={handleSubmit}
133
+ disabled={!title.trim()}
134
+ style={{
135
+ width: '100%',
136
+ padding: '14px',
137
+ borderRadius: 10,
138
+ border: 'none',
139
+ background: title.trim() ? config.primaryColor : '#e5e7eb',
140
+ color: title.trim() ? '#fff' : '#9ca3af',
141
+ fontWeight: 700,
142
+ fontSize: 15,
143
+ cursor: title.trim() ? 'pointer' : 'not-allowed',
144
+ }}
145
+ >
146
+ Submit ticket
147
+ </button>
148
+ </div>
149
+ </div>
150
+ );
151
+ };