@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.
Files changed (107) hide show
  1. package/README.md +53 -161
  2. package/package.json +6 -6
  3. package/src/components/RedirectPage/RedirectPage.tsx +1 -1
  4. package/src/index.ts +0 -6
  5. package/src/layouts/AppLayout/AppLayout.tsx +1 -1
  6. package/src/layouts/AppLayout/BaseApp.tsx +1 -1
  7. package/src/layouts/AuthLayout/AuthContext.tsx +1 -1
  8. package/src/layouts/AuthLayout/OAuthCallback.tsx +1 -1
  9. package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -1
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  11. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +1 -1
  12. package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
  13. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -2
  14. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +2 -2
  15. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  16. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
  17. package/src/layouts/_components/UserMenu.tsx +1 -1
  18. package/src/layouts/index.ts +0 -2
  19. package/src/snippets/Analytics/useAnalytics.ts +1 -1
  20. package/src/snippets/index.ts +0 -3
  21. package/src/auth/README.md +0 -962
  22. package/src/auth/context/AccountsContext.tsx +0 -240
  23. package/src/auth/context/AuthContext.tsx +0 -604
  24. package/src/auth/context/index.ts +0 -4
  25. package/src/auth/context/types.ts +0 -68
  26. package/src/auth/hooks/index.ts +0 -17
  27. package/src/auth/hooks/useAuthForm.ts +0 -332
  28. package/src/auth/hooks/useAuthGuard.ts +0 -25
  29. package/src/auth/hooks/useAuthRedirect.ts +0 -51
  30. package/src/auth/hooks/useAutoAuth.ts +0 -49
  31. package/src/auth/hooks/useGithubAuth.ts +0 -184
  32. package/src/auth/hooks/useLocalStorage.ts +0 -214
  33. package/src/auth/hooks/useProfileCache.ts +0 -146
  34. package/src/auth/hooks/useSessionStorage.ts +0 -189
  35. package/src/auth/index.ts +0 -10
  36. package/src/auth/middlewares/index.ts +0 -1
  37. package/src/auth/middlewares/proxy.ts +0 -32
  38. package/src/auth/server.ts +0 -6
  39. package/src/auth/utils/errors.ts +0 -34
  40. package/src/auth/utils/index.ts +0 -2
  41. package/src/auth/utils/validation.ts +0 -14
  42. package/src/contexts/LeadsContext.tsx +0 -156
  43. package/src/contexts/NewsletterContext.tsx +0 -263
  44. package/src/contexts/SupportContext.tsx +0 -256
  45. package/src/contexts/index.ts +0 -59
  46. package/src/contexts/knowbase/ChatContext.tsx +0 -174
  47. package/src/contexts/knowbase/DocumentsContext.tsx +0 -304
  48. package/src/contexts/knowbase/SessionsContext.tsx +0 -174
  49. package/src/contexts/knowbase/index.ts +0 -61
  50. package/src/contexts/payments/BalancesContext.tsx +0 -65
  51. package/src/contexts/payments/CurrenciesContext.tsx +0 -66
  52. package/src/contexts/payments/OverviewContext.tsx +0 -174
  53. package/src/contexts/payments/PaymentsContext.tsx +0 -132
  54. package/src/contexts/payments/README.md +0 -201
  55. package/src/contexts/payments/RootPaymentsContext.tsx +0 -68
  56. package/src/contexts/payments/index.ts +0 -50
  57. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -92
  58. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -291
  59. package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -290
  60. package/src/layouts/PaymentsLayout/components/index.ts +0 -2
  61. package/src/layouts/PaymentsLayout/events.ts +0 -47
  62. package/src/layouts/PaymentsLayout/index.ts +0 -16
  63. package/src/layouts/PaymentsLayout/types.ts +0 -6
  64. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -128
  65. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -142
  66. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
  67. package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -20
  68. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -276
  69. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
  70. package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -17
  71. package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -273
  72. package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
  73. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -17
  74. package/src/layouts/SupportLayout/README.md +0 -91
  75. package/src/layouts/SupportLayout/SupportLayout.tsx +0 -179
  76. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +0 -155
  77. package/src/layouts/SupportLayout/components/MessageInput.tsx +0 -92
  78. package/src/layouts/SupportLayout/components/MessageList.tsx +0 -314
  79. package/src/layouts/SupportLayout/components/TicketCard.tsx +0 -96
  80. package/src/layouts/SupportLayout/components/TicketList.tsx +0 -153
  81. package/src/layouts/SupportLayout/components/index.ts +0 -6
  82. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +0 -263
  83. package/src/layouts/SupportLayout/context/index.ts +0 -2
  84. package/src/layouts/SupportLayout/events.ts +0 -33
  85. package/src/layouts/SupportLayout/hooks/index.ts +0 -2
  86. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +0 -119
  87. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +0 -92
  88. package/src/layouts/SupportLayout/index.ts +0 -8
  89. package/src/layouts/SupportLayout/types.ts +0 -21
  90. package/src/snippets/Chat/ChatUIContext.tsx +0 -110
  91. package/src/snippets/Chat/ChatWidget.tsx +0 -476
  92. package/src/snippets/Chat/README.md +0 -122
  93. package/src/snippets/Chat/components/MessageInput.tsx +0 -124
  94. package/src/snippets/Chat/components/MessageList.tsx +0 -169
  95. package/src/snippets/Chat/components/SessionList.tsx +0 -192
  96. package/src/snippets/Chat/components/index.ts +0 -9
  97. package/src/snippets/Chat/hooks/index.ts +0 -6
  98. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +0 -82
  99. package/src/snippets/Chat/index.tsx +0 -45
  100. package/src/snippets/Chat/types.ts +0 -80
  101. package/src/snippets/ContactForm/ContactForm.tsx +0 -346
  102. package/src/snippets/ContactForm/ContactFormProvider.tsx +0 -153
  103. package/src/snippets/ContactForm/ContactInfo.tsx +0 -114
  104. package/src/snippets/ContactForm/ContactPage.tsx +0 -131
  105. package/src/snippets/ContactForm/dynamic.tsx +0 -55
  106. package/src/snippets/ContactForm/index.ts +0 -34
  107. 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
-