@djangocfg/layouts 2.1.10 → 2.1.15
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 +53 -161
- package/package.json +6 -6
- package/src/components/RedirectPage/RedirectPage.tsx +1 -1
- package/src/index.ts +0 -6
- package/src/layouts/AppLayout/AppLayout.tsx +1 -1
- package/src/layouts/AppLayout/BaseApp.tsx +1 -1
- package/src/layouts/AuthLayout/AuthContext.tsx +1 -1
- package/src/layouts/AuthLayout/OAuthCallback.tsx +1 -1
- package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -1
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +1 -1
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -2
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +2 -2
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
- package/src/layouts/_components/UserMenu.tsx +1 -1
- package/src/layouts/index.ts +0 -2
- package/src/snippets/Analytics/useAnalytics.ts +1 -1
- package/src/snippets/index.ts +0 -3
- package/src/auth/README.md +0 -962
- package/src/auth/context/AccountsContext.tsx +0 -240
- package/src/auth/context/AuthContext.tsx +0 -604
- package/src/auth/context/index.ts +0 -4
- package/src/auth/context/types.ts +0 -68
- package/src/auth/hooks/index.ts +0 -17
- package/src/auth/hooks/useAuthForm.ts +0 -332
- package/src/auth/hooks/useAuthGuard.ts +0 -25
- package/src/auth/hooks/useAuthRedirect.ts +0 -51
- package/src/auth/hooks/useAutoAuth.ts +0 -49
- package/src/auth/hooks/useGithubAuth.ts +0 -184
- package/src/auth/hooks/useLocalStorage.ts +0 -214
- package/src/auth/hooks/useProfileCache.ts +0 -146
- package/src/auth/hooks/useSessionStorage.ts +0 -189
- package/src/auth/index.ts +0 -10
- package/src/auth/middlewares/index.ts +0 -1
- package/src/auth/middlewares/proxy.ts +0 -32
- package/src/auth/server.ts +0 -6
- package/src/auth/utils/errors.ts +0 -34
- package/src/auth/utils/index.ts +0 -2
- package/src/auth/utils/validation.ts +0 -14
- package/src/contexts/LeadsContext.tsx +0 -156
- package/src/contexts/NewsletterContext.tsx +0 -263
- package/src/contexts/SupportContext.tsx +0 -256
- package/src/contexts/index.ts +0 -59
- package/src/contexts/knowbase/ChatContext.tsx +0 -174
- package/src/contexts/knowbase/DocumentsContext.tsx +0 -304
- package/src/contexts/knowbase/SessionsContext.tsx +0 -174
- package/src/contexts/knowbase/index.ts +0 -61
- package/src/contexts/payments/BalancesContext.tsx +0 -65
- package/src/contexts/payments/CurrenciesContext.tsx +0 -66
- package/src/contexts/payments/OverviewContext.tsx +0 -174
- package/src/contexts/payments/PaymentsContext.tsx +0 -132
- package/src/contexts/payments/README.md +0 -201
- package/src/contexts/payments/RootPaymentsContext.tsx +0 -68
- package/src/contexts/payments/index.ts +0 -50
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -92
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -291
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -290
- package/src/layouts/PaymentsLayout/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/events.ts +0 -47
- package/src/layouts/PaymentsLayout/index.ts +0 -16
- package/src/layouts/PaymentsLayout/types.ts +0 -6
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -128
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -142
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -20
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -276
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -17
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -273
- package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -17
- package/src/layouts/SupportLayout/README.md +0 -91
- package/src/layouts/SupportLayout/SupportLayout.tsx +0 -179
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +0 -155
- package/src/layouts/SupportLayout/components/MessageInput.tsx +0 -92
- package/src/layouts/SupportLayout/components/MessageList.tsx +0 -314
- package/src/layouts/SupportLayout/components/TicketCard.tsx +0 -96
- package/src/layouts/SupportLayout/components/TicketList.tsx +0 -153
- package/src/layouts/SupportLayout/components/index.ts +0 -6
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +0 -263
- package/src/layouts/SupportLayout/context/index.ts +0 -2
- package/src/layouts/SupportLayout/events.ts +0 -33
- package/src/layouts/SupportLayout/hooks/index.ts +0 -2
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +0 -119
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +0 -92
- package/src/layouts/SupportLayout/index.ts +0 -8
- package/src/layouts/SupportLayout/types.ts +0 -21
- package/src/snippets/Chat/ChatUIContext.tsx +0 -110
- package/src/snippets/Chat/ChatWidget.tsx +0 -476
- package/src/snippets/Chat/README.md +0 -122
- package/src/snippets/Chat/components/MessageInput.tsx +0 -124
- package/src/snippets/Chat/components/MessageList.tsx +0 -169
- package/src/snippets/Chat/components/SessionList.tsx +0 -192
- package/src/snippets/Chat/components/index.ts +0 -9
- package/src/snippets/Chat/hooks/index.ts +0 -6
- package/src/snippets/Chat/hooks/useInfiniteSessions.ts +0 -82
- package/src/snippets/Chat/index.tsx +0 -45
- package/src/snippets/Chat/types.ts +0 -80
- package/src/snippets/ContactForm/ContactForm.tsx +0 -346
- package/src/snippets/ContactForm/ContactFormProvider.tsx +0 -153
- package/src/snippets/ContactForm/ContactInfo.tsx +0 -114
- package/src/snippets/ContactForm/ContactPage.tsx +0 -131
- package/src/snippets/ContactForm/dynamic.tsx +0 -55
- package/src/snippets/ContactForm/index.ts +0 -34
- package/src/snippets/ContactForm/types.ts +0 -110
|
@@ -1,155 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message List Component
|
|
3
|
-
* Displays messages in a ticket conversation with infinite scroll
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React, { useEffect, useRef, useCallback } from 'react';
|
|
9
|
-
import { ScrollArea, Skeleton, Avatar, AvatarFallback, AvatarImage, Card, CardContent, Button } from '@djangocfg/ui-nextjs';
|
|
10
|
-
import { MessageSquare, Loader2, User, Headphones } from 'lucide-react';
|
|
11
|
-
import { useSupportLayoutContext } from '../context';
|
|
12
|
-
import { useInfiniteMessages } from '../hooks';
|
|
13
|
-
import { useAuth } from '../../../auth';
|
|
14
|
-
import { CfgSupportTypes } from '@djangocfg/api';
|
|
15
|
-
|
|
16
|
-
type Message = CfgSupportTypes.Message;
|
|
17
|
-
|
|
18
|
-
const formatTime = (date: string | null | undefined): string => {
|
|
19
|
-
if (!date) return '';
|
|
20
|
-
return new Date(date).toLocaleTimeString('en-US', {
|
|
21
|
-
hour: '2-digit',
|
|
22
|
-
minute: '2-digit',
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const formatDate = (date: string | null | undefined): string => {
|
|
27
|
-
if (!date) return '';
|
|
28
|
-
return new Date(date).toLocaleDateString('en-US', {
|
|
29
|
-
year: 'numeric',
|
|
30
|
-
month: 'short',
|
|
31
|
-
day: 'numeric',
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
interface MessageBubbleProps {
|
|
36
|
-
message: Message;
|
|
37
|
-
isFromUser: boolean;
|
|
38
|
-
currentUser: any;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isFromUser, currentUser }) => {
|
|
42
|
-
const sender = message.sender;
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div
|
|
46
|
-
className={`flex gap-3 ${isFromUser ? 'justify-end' : 'justify-start'}
|
|
47
|
-
animate-in fade-in slide-in-from-bottom-2 duration-300`}
|
|
48
|
-
>
|
|
49
|
-
{/* Support Avatar (left side) */}
|
|
50
|
-
{!isFromUser && (
|
|
51
|
-
<Avatar className="h-8 w-8 shrink-0">
|
|
52
|
-
{sender?.avatar ? (
|
|
53
|
-
<AvatarImage src={sender.avatar} alt={sender.display_username || 'Support'} />
|
|
54
|
-
) : (
|
|
55
|
-
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
56
|
-
{sender?.is_staff ? (
|
|
57
|
-
<Headphones className="h-4 w-4" />
|
|
58
|
-
) : (
|
|
59
|
-
sender?.display_username?.charAt(0)?.toUpperCase() ||
|
|
60
|
-
sender?.initials ||
|
|
61
|
-
'S'
|
|
62
|
-
)}
|
|
63
|
-
</AvatarFallback>
|
|
64
|
-
)}
|
|
65
|
-
</Avatar>
|
|
66
|
-
)}
|
|
67
|
-
|
|
68
|
-
{/* Message Content */}
|
|
69
|
-
<div className={`flex flex-col gap-1 flex-1 max-w-[80%] ${
|
|
70
|
-
isFromUser ? 'items-end' : 'items-start'
|
|
71
|
-
}`}>
|
|
72
|
-
{/* Sender name (for support messages) */}
|
|
73
|
-
{!isFromUser && sender && (
|
|
74
|
-
<span className="text-xs text-muted-foreground px-1">
|
|
75
|
-
{sender.display_username || sender.email || 'Support Team'}
|
|
76
|
-
{sender.is_staff && ' (Staff)'}
|
|
77
|
-
</span>
|
|
78
|
-
)}
|
|
79
|
-
|
|
80
|
-
{/* Message Bubble */}
|
|
81
|
-
<Card
|
|
82
|
-
className={`${
|
|
83
|
-
isFromUser
|
|
84
|
-
? 'bg-primary text-primary-foreground'
|
|
85
|
-
: 'bg-muted'
|
|
86
|
-
} transition-all duration-200 hover:shadow-md`}
|
|
87
|
-
>
|
|
88
|
-
<CardContent className="p-3">
|
|
89
|
-
<p className="text-sm whitespace-pre-wrap break-words">{message.text}</p>
|
|
90
|
-
</CardContent>
|
|
91
|
-
</Card>
|
|
92
|
-
|
|
93
|
-
{/* Timestamp */}
|
|
94
|
-
<span className="text-xs text-muted-foreground px-1">
|
|
95
|
-
{formatTime(message.created_at)}
|
|
96
|
-
</span>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* User Avatar (right side) */}
|
|
100
|
-
{isFromUser && (
|
|
101
|
-
<Avatar className="h-8 w-8 shrink-0">
|
|
102
|
-
{currentUser?.avatar ? (
|
|
103
|
-
<AvatarImage src={currentUser.avatar} alt={currentUser.display_username || currentUser.email || 'You'} />
|
|
104
|
-
) : (
|
|
105
|
-
<AvatarFallback className="bg-primary/10 text-primary font-semibold">
|
|
106
|
-
{currentUser?.display_username?.charAt(0)?.toUpperCase() ||
|
|
107
|
-
currentUser?.email?.charAt(0)?.toUpperCase() ||
|
|
108
|
-
currentUser?.initials ||
|
|
109
|
-
<User className="h-4 w-4" />}
|
|
110
|
-
</AvatarFallback>
|
|
111
|
-
)}
|
|
112
|
-
</Avatar>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const MessageList: React.FC = () => {
|
|
119
|
-
const { selectedTicket } = useSupportLayoutContext();
|
|
120
|
-
const { user } = useAuth();
|
|
121
|
-
|
|
122
|
-
const {
|
|
123
|
-
messages,
|
|
124
|
-
isLoading,
|
|
125
|
-
isLoadingMore,
|
|
126
|
-
hasMore,
|
|
127
|
-
totalCount,
|
|
128
|
-
loadMore,
|
|
129
|
-
} = useInfiniteMessages(selectedTicket?.uuid || null);
|
|
130
|
-
|
|
131
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
132
|
-
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
133
|
-
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
134
|
-
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
135
|
-
const firstRender = useRef(true);
|
|
136
|
-
|
|
137
|
-
// Set up intersection observer for infinite scroll at the top
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (observerRef.current) {
|
|
140
|
-
observerRef.current.disconnect();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
observerRef.current = new IntersectionObserver(
|
|
144
|
-
(entries) => {
|
|
145
|
-
if (entries[0]?.isIntersecting && hasMore && !isLoadingMore) {
|
|
146
|
-
loadMore();
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
{ threshold: 0.1 }
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
if (loadMoreRef.current) {
|
|
153
|
-
observerRef.current.observe(loadMoreRef.current);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return () => {
|
|
157
|
-
if (observerRef.current) {
|
|
158
|
-
observerRef.current.disconnect();
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}, [hasMore, isLoadingMore, loadMore]);
|
|
162
|
-
|
|
163
|
-
// Auto-scroll to bottom on first load and new messages
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
if (firstRender.current && messages.length > 0) {
|
|
166
|
-
// Scroll to bottom on first render
|
|
167
|
-
const scrollContainer = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]');
|
|
168
|
-
if (scrollContainer) {
|
|
169
|
-
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
170
|
-
}
|
|
171
|
-
firstRender.current = false;
|
|
172
|
-
}
|
|
173
|
-
}, [messages]);
|
|
174
|
-
|
|
175
|
-
// Handle scroll position when loading older messages
|
|
176
|
-
const handleLoadMore = useCallback(() => {
|
|
177
|
-
const scrollContainer = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]');
|
|
178
|
-
const previousHeight = scrollContainer?.scrollHeight || 0;
|
|
179
|
-
|
|
180
|
-
loadMore();
|
|
181
|
-
|
|
182
|
-
// Restore scroll position after loading
|
|
183
|
-
setTimeout(() => {
|
|
184
|
-
if (scrollContainer) {
|
|
185
|
-
const newHeight = scrollContainer.scrollHeight;
|
|
186
|
-
scrollContainer.scrollTop = newHeight - previousHeight;
|
|
187
|
-
}
|
|
188
|
-
}, 100);
|
|
189
|
-
}, [loadMore]);
|
|
190
|
-
|
|
191
|
-
if (!selectedTicket) {
|
|
192
|
-
return (
|
|
193
|
-
<div className="flex flex-col items-center justify-center h-full p-8 text-center animate-in fade-in zoom-in-95 duration-300">
|
|
194
|
-
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
195
|
-
<h3 className="text-lg font-semibold mb-2">No ticket selected</h3>
|
|
196
|
-
<p className="text-sm text-muted-foreground max-w-sm">
|
|
197
|
-
Select a ticket from the list to view the conversation
|
|
198
|
-
</p>
|
|
199
|
-
</div>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (isLoading) {
|
|
204
|
-
return (
|
|
205
|
-
<div className="p-6 space-y-4">
|
|
206
|
-
{[1, 2, 3].map((i) => (
|
|
207
|
-
<div
|
|
208
|
-
key={i}
|
|
209
|
-
className="flex gap-3 animate-pulse"
|
|
210
|
-
style={{ animationDelay: `${i * 100}ms` }}
|
|
211
|
-
>
|
|
212
|
-
<Skeleton className="h-8 w-8 rounded-full" />
|
|
213
|
-
<Skeleton className="h-16 flex-1 max-w-[70%]" />
|
|
214
|
-
</div>
|
|
215
|
-
))}
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (!messages || messages.length === 0) {
|
|
221
|
-
return (
|
|
222
|
-
<div className="flex flex-col items-center justify-center h-full p-8 text-center animate-in fade-in zoom-in-95 duration-300">
|
|
223
|
-
<MessageSquare className="h-16 w-16 text-muted-foreground mb-4 animate-bounce" />
|
|
224
|
-
<h3 className="text-lg font-semibold mb-2">No messages yet</h3>
|
|
225
|
-
<p className="text-sm text-muted-foreground max-w-sm">
|
|
226
|
-
Start the conversation by sending a message below
|
|
227
|
-
</p>
|
|
228
|
-
</div>
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<ScrollArea className="h-full bg-muted/50" viewportRef={scrollAreaRef}>
|
|
234
|
-
<div className="p-6 space-y-4" ref={scrollRef}>
|
|
235
|
-
{/* Load more trigger at the top */}
|
|
236
|
-
<div ref={loadMoreRef} className="h-2" />
|
|
237
|
-
|
|
238
|
-
{/* Loading indicator at the top */}
|
|
239
|
-
{isLoadingMore && (
|
|
240
|
-
<div className="flex justify-center py-4">
|
|
241
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
242
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
243
|
-
<span className="text-sm">Loading older messages...</span>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
|
|
248
|
-
{/* Manual load button if needed */}
|
|
249
|
-
{hasMore && !isLoadingMore && (
|
|
250
|
-
<div className="flex justify-center pt-2 pb-4">
|
|
251
|
-
<Button
|
|
252
|
-
variant="outline"
|
|
253
|
-
size="sm"
|
|
254
|
-
onClick={handleLoadMore}
|
|
255
|
-
className="text-xs"
|
|
256
|
-
>
|
|
257
|
-
Load older messages ({totalCount > 0 ? `${messages.length}/${totalCount}` : ''})
|
|
258
|
-
</Button>
|
|
259
|
-
</div>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
{/* Date separator for first message group */}
|
|
263
|
-
{messages.length > 0 && (
|
|
264
|
-
<div className="flex items-center gap-3 my-4">
|
|
265
|
-
<div className="flex-1 h-px bg-border" />
|
|
266
|
-
<span className="text-xs text-muted-foreground">
|
|
267
|
-
{formatDate(messages[0]?.created_at)}
|
|
268
|
-
</span>
|
|
269
|
-
<div className="flex-1 h-px bg-border" />
|
|
270
|
-
</div>
|
|
271
|
-
)}
|
|
272
|
-
|
|
273
|
-
{/* Messages */}
|
|
274
|
-
{messages.map((message, index) => {
|
|
275
|
-
// Check if message is from the current user
|
|
276
|
-
// Convert IDs to strings for consistent comparison
|
|
277
|
-
// Also check is_from_author flag when sender is the ticket creator
|
|
278
|
-
const isFromUser =
|
|
279
|
-
(message.sender?.id && user?.id && String(message.sender.id) === String(user.id)) ||
|
|
280
|
-
(message.sender?.email && user?.email && message.sender.email === user.email) ||
|
|
281
|
-
(message.is_from_author && selectedTicket?.user && user?.id &&
|
|
282
|
-
String(selectedTicket.user) === String(user.id));
|
|
283
|
-
|
|
284
|
-
// Show date separator if date changes
|
|
285
|
-
const previousMessage = index > 0 ? messages[index - 1] : null;
|
|
286
|
-
const showDateSeparator = previousMessage &&
|
|
287
|
-
new Date(previousMessage.created_at || '').toDateString() !==
|
|
288
|
-
new Date(message.created_at || '').toDateString();
|
|
289
|
-
|
|
290
|
-
return (
|
|
291
|
-
<React.Fragment key={message.uuid}>
|
|
292
|
-
{showDateSeparator && (
|
|
293
|
-
<div className="flex items-center gap-3 my-4">
|
|
294
|
-
<div className="flex-1 h-px bg-border" />
|
|
295
|
-
<span className="text-xs text-muted-foreground">
|
|
296
|
-
{formatDate(message.created_at)}
|
|
297
|
-
</span>
|
|
298
|
-
<div className="flex-1 h-px bg-border" />
|
|
299
|
-
</div>
|
|
300
|
-
)}
|
|
301
|
-
<div style={{ animationDelay: `${Math.min(index, 10) * 50}ms` }}>
|
|
302
|
-
<MessageBubble
|
|
303
|
-
message={message}
|
|
304
|
-
isFromUser={!!isFromUser}
|
|
305
|
-
currentUser={user}
|
|
306
|
-
/>
|
|
307
|
-
</div>
|
|
308
|
-
</React.Fragment>
|
|
309
|
-
);
|
|
310
|
-
})}
|
|
311
|
-
</div>
|
|
312
|
-
</ScrollArea>
|
|
313
|
-
);
|
|
314
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ticket Card Component
|
|
3
|
-
* Card for displaying a single ticket
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import { Badge, Card, CardContent, cn } from '@djangocfg/ui-nextjs';
|
|
10
|
-
import { Clock, MessageSquare } from 'lucide-react';
|
|
11
|
-
import type { Ticket } from '@djangocfg/layouts/contexts';
|
|
12
|
-
|
|
13
|
-
interface TicketCardProps {
|
|
14
|
-
ticket: Ticket;
|
|
15
|
-
isSelected: boolean;
|
|
16
|
-
onClick: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const getStatusBadgeVariant = (
|
|
20
|
-
status: string
|
|
21
|
-
): 'default' | 'secondary' | 'destructive' | 'outline' => {
|
|
22
|
-
switch (status) {
|
|
23
|
-
case 'open':
|
|
24
|
-
return 'default';
|
|
25
|
-
case 'waiting_for_user':
|
|
26
|
-
return 'secondary';
|
|
27
|
-
case 'waiting_for_admin':
|
|
28
|
-
return 'outline';
|
|
29
|
-
case 'resolved':
|
|
30
|
-
return 'outline';
|
|
31
|
-
case 'closed':
|
|
32
|
-
return 'secondary';
|
|
33
|
-
default:
|
|
34
|
-
return 'default';
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const formatRelativeTime = (date: string | null | undefined): string => {
|
|
39
|
-
if (!date) return 'N/A';
|
|
40
|
-
|
|
41
|
-
const now = new Date();
|
|
42
|
-
const messageDate = new Date(date);
|
|
43
|
-
const diffInSeconds = Math.floor((now.getTime() - messageDate.getTime()) / 1000);
|
|
44
|
-
|
|
45
|
-
if (diffInSeconds < 60) return 'Just now';
|
|
46
|
-
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
47
|
-
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
48
|
-
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
49
|
-
|
|
50
|
-
return new Date(date).toLocaleDateString('en-US', {
|
|
51
|
-
year: 'numeric',
|
|
52
|
-
month: 'short',
|
|
53
|
-
day: 'numeric',
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const TicketCard: React.FC<TicketCardProps> = ({ ticket, isSelected, onClick }) => {
|
|
58
|
-
return (
|
|
59
|
-
<Card
|
|
60
|
-
className={cn(
|
|
61
|
-
'cursor-pointer transition-all duration-200 ease-out',
|
|
62
|
-
'hover:bg-accent/50 hover:shadow-md hover:scale-[1.02]',
|
|
63
|
-
'active:scale-[0.98]',
|
|
64
|
-
isSelected && 'bg-accent border-primary shadow-sm'
|
|
65
|
-
)}
|
|
66
|
-
onClick={onClick}
|
|
67
|
-
>
|
|
68
|
-
<CardContent className="p-4">
|
|
69
|
-
<div className="flex items-start justify-between mb-2">
|
|
70
|
-
<h3 className="font-semibold text-sm line-clamp-2 flex-1">{ticket.subject}</h3>
|
|
71
|
-
{(ticket.unanswered_messages_count || 0) > 0 && (
|
|
72
|
-
<Badge
|
|
73
|
-
variant="destructive"
|
|
74
|
-
className="ml-2 shrink-0 animate-pulse"
|
|
75
|
-
>
|
|
76
|
-
{ticket.unanswered_messages_count}
|
|
77
|
-
</Badge>
|
|
78
|
-
)}
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
82
|
-
<div className="flex items-center gap-3">
|
|
83
|
-
<Badge variant={getStatusBadgeVariant(ticket.status || 'open')} className="text-xs">
|
|
84
|
-
{ticket.status || 'open'}
|
|
85
|
-
</Badge>
|
|
86
|
-
<div className="flex items-center gap-1">
|
|
87
|
-
<Clock className="h-3 w-3" />
|
|
88
|
-
<span>{formatRelativeTime(ticket.created_at)}</span>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
</CardContent>
|
|
93
|
-
</Card>
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|