@djangocfg/layouts 1.0.1

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/package.json +86 -0
  4. package/src/auth/README.md +962 -0
  5. package/src/auth/context/AuthContext.tsx +458 -0
  6. package/src/auth/context/index.ts +2 -0
  7. package/src/auth/context/types.ts +63 -0
  8. package/src/auth/hooks/index.ts +6 -0
  9. package/src/auth/hooks/useAuthForm.ts +329 -0
  10. package/src/auth/hooks/useAuthGuard.ts +23 -0
  11. package/src/auth/hooks/useAuthRedirect.ts +51 -0
  12. package/src/auth/hooks/useAutoAuth.ts +42 -0
  13. package/src/auth/hooks/useLocalStorage.ts +211 -0
  14. package/src/auth/hooks/useSessionStorage.ts +186 -0
  15. package/src/auth/index.ts +10 -0
  16. package/src/auth/middlewares/index.ts +1 -0
  17. package/src/auth/middlewares/proxy.ts +24 -0
  18. package/src/auth/server.ts +6 -0
  19. package/src/auth/utils/errors.ts +34 -0
  20. package/src/auth/utils/index.ts +2 -0
  21. package/src/auth/utils/validation.ts +14 -0
  22. package/src/index.ts +15 -0
  23. package/src/layouts/AppLayout/AppLayout.tsx +123 -0
  24. package/src/layouts/AppLayout/README.md +204 -0
  25. package/src/layouts/AppLayout/SUMMARY.md +240 -0
  26. package/src/layouts/AppLayout/USAGE.md +312 -0
  27. package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
  28. package/src/layouts/AppLayout/components/Seo.tsx +87 -0
  29. package/src/layouts/AppLayout/components/index.ts +6 -0
  30. package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
  31. package/src/layouts/AppLayout/context/index.ts +5 -0
  32. package/src/layouts/AppLayout/hooks/index.ts +6 -0
  33. package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
  34. package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
  35. package/src/layouts/AppLayout/index.ts +31 -0
  36. package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
  37. package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
  38. package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
  39. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
  40. package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
  41. package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
  42. package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
  43. package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
  44. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
  45. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
  46. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
  47. package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
  48. package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
  49. package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
  50. package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
  51. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
  52. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
  53. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
  54. package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
  55. package/src/layouts/AppLayout/layouts/index.ts +7 -0
  56. package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
  57. package/src/layouts/AppLayout/providers/index.ts +5 -0
  58. package/src/layouts/AppLayout/types/config.ts +40 -0
  59. package/src/layouts/AppLayout/types/index.ts +10 -0
  60. package/src/layouts/AppLayout/types/layout.ts +47 -0
  61. package/src/layouts/AppLayout/types/navigation.ts +41 -0
  62. package/src/layouts/AppLayout/types/routes.ts +45 -0
  63. package/src/layouts/AppLayout/utils/index.ts +5 -0
  64. package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
  65. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
  66. package/src/layouts/PaymentsLayout/README.md +133 -0
  67. package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
  68. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
  69. package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
  70. package/src/layouts/PaymentsLayout/components/index.ts +4 -0
  71. package/src/layouts/PaymentsLayout/events.ts +106 -0
  72. package/src/layouts/PaymentsLayout/index.ts +20 -0
  73. package/src/layouts/PaymentsLayout/types.ts +19 -0
  74. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
  75. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
  76. package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
  77. package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
  78. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
  79. package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
  80. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
  81. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
  82. package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
  83. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
  84. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
  85. package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
  86. package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
  87. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
  88. package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
  89. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
  90. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
  91. package/src/layouts/ProfileLayout/components/index.ts +3 -0
  92. package/src/layouts/ProfileLayout/index.ts +3 -0
  93. package/src/layouts/SupportLayout/README.md +91 -0
  94. package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
  95. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
  96. package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
  97. package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
  98. package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
  99. package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
  100. package/src/layouts/SupportLayout/components/index.ts +6 -0
  101. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
  102. package/src/layouts/SupportLayout/context/index.ts +2 -0
  103. package/src/layouts/SupportLayout/events.ts +31 -0
  104. package/src/layouts/SupportLayout/hooks/index.ts +2 -0
  105. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
  106. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
  107. package/src/layouts/SupportLayout/index.ts +6 -0
  108. package/src/layouts/SupportLayout/types.ts +23 -0
  109. package/src/layouts/index.ts +9 -0
  110. package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
  111. package/src/snippets/AuthDialog/events.ts +21 -0
  112. package/src/snippets/AuthDialog/index.ts +3 -0
  113. package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
  114. package/src/snippets/Breadcrumbs.tsx +80 -0
  115. package/src/snippets/Chat/ChatUIContext.tsx +110 -0
  116. package/src/snippets/Chat/ChatWidget.tsx +476 -0
  117. package/src/snippets/Chat/README.md +122 -0
  118. package/src/snippets/Chat/components/MessageInput.tsx +124 -0
  119. package/src/snippets/Chat/components/MessageList.tsx +168 -0
  120. package/src/snippets/Chat/components/SessionList.tsx +192 -0
  121. package/src/snippets/Chat/components/index.ts +9 -0
  122. package/src/snippets/Chat/hooks/index.ts +6 -0
  123. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
  124. package/src/snippets/Chat/index.tsx +44 -0
  125. package/src/snippets/Chat/types.ts +79 -0
  126. package/src/snippets/VideoPlayer/README.md +203 -0
  127. package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
  128. package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
  129. package/src/snippets/VideoPlayer/index.ts +8 -0
  130. package/src/snippets/VideoPlayer/types.ts +61 -0
  131. package/src/snippets/index.ts +10 -0
  132. package/src/styles/dashboard.css +41 -0
  133. package/src/styles/index.css +20 -0
  134. package/src/styles/sources.css +6 -0
  135. package/src/types/index.ts +1 -0
  136. package/src/types/pageConfig.ts +103 -0
  137. package/src/utils/index.ts +6 -0
  138. package/src/utils/logger.ts +57 -0
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+
3
+ import { Check, Upload, X } from 'lucide-react';
4
+ import React, { useState } from 'react';
5
+ import { toast } from 'sonner';
6
+
7
+ import { Avatar, AvatarFallback, Button } from '@djangocfg/ui/components';
8
+ import { useAccountsContext } from '@djangocfg/api/cfg/contexts';
9
+ import { useAuth } from '../../../auth';
10
+
11
+ export const AvatarSection = () => {
12
+ const { user } = useAuth();
13
+ const accounts = useAccountsContext();
14
+ const [avatarFile, setAvatarFile] = useState<File | null>(null);
15
+ const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
16
+ const [isUploading, setIsUploading] = useState(false);
17
+
18
+ const getInitials = (name: string) => {
19
+ if (!name) return 'UN';
20
+ return name
21
+ .split(' ')
22
+ .map((word) => word[0])
23
+ .join('')
24
+ .toUpperCase()
25
+ .slice(0, 2);
26
+ };
27
+
28
+ const handleAvatarChange = (event: React.ChangeEvent<HTMLInputElement>) => {
29
+ const file = event.target.files?.[0];
30
+ if (file) {
31
+ if (!file.type.startsWith('image/')) {
32
+ toast.error('Please select an image file');
33
+ return;
34
+ }
35
+ if (file.size > 5 * 1024 * 1024) {
36
+ toast.error('File size must be less than 5MB');
37
+ return;
38
+ }
39
+ setAvatarFile(file);
40
+ const reader = new FileReader();
41
+ reader.onload = (e) => setAvatarPreview(e.target?.result as string);
42
+ reader.readAsDataURL(file);
43
+ } else {
44
+ setAvatarFile(null);
45
+ setAvatarPreview(null);
46
+ }
47
+ };
48
+
49
+ const handleAvatarUpload = async () => {
50
+ if (!avatarFile) return;
51
+ setIsUploading(true);
52
+ try {
53
+ const formData = new FormData();
54
+ formData.append('avatar', avatarFile);
55
+ await accounts.uploadAvatar(formData as any);
56
+ toast.success('Avatar updated successfully');
57
+ setAvatarFile(null);
58
+ setAvatarPreview(null);
59
+ } catch (error) {
60
+ toast.error('Failed to upload avatar');
61
+ console.error('Avatar upload error:', error);
62
+ } finally {
63
+ setIsUploading(false);
64
+ }
65
+ };
66
+
67
+ const resetAvatar = () => {
68
+ setAvatarFile(null);
69
+ setAvatarPreview(null);
70
+ };
71
+
72
+ return (
73
+ <div className="flex flex-col items-center mb-4">
74
+ <div className="relative group">
75
+ <Avatar className="w-24 h-24 transition-transform group-hover:scale-105">
76
+ {avatarPreview ? (
77
+ <img
78
+ src={avatarPreview}
79
+ alt="Avatar preview"
80
+ className="w-full h-full object-cover rounded-full"
81
+ />
82
+ ) : user?.avatar ? (
83
+ <img
84
+ src={user.avatar}
85
+ alt="User avatar"
86
+ className="w-full h-full object-cover rounded-full"
87
+ />
88
+ ) : (
89
+ <AvatarFallback className="text-2xl font-semibold bg-gradient-to-br from-primary to-primary/80 text-primary-foreground">
90
+ {getInitials(user?.display_username || user?.email || '')}
91
+ </AvatarFallback>
92
+ )}
93
+ </Avatar>
94
+
95
+ {/* Upload overlay - appears on hover */}
96
+ <label className="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer">
97
+ <div className="p-3 rounded-full bg-primary/80 text-primary-foreground hover:bg-primary transition-colors">
98
+ <Upload className="w-5 h-5" />
99
+ </div>
100
+ <input
101
+ type="file"
102
+ accept="image/*"
103
+ onChange={handleAvatarChange}
104
+ className="hidden"
105
+ />
106
+ </label>
107
+
108
+ {/* Action buttons - appear when file is selected */}
109
+ {avatarFile && (
110
+ <div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 flex items-center space-x-1 bg-card border border-border rounded-full shadow-lg p-1">
111
+ <Button
112
+ size="sm"
113
+ onClick={handleAvatarUpload}
114
+ disabled={isUploading}
115
+ className="h-7 px-3 rounded-full"
116
+ >
117
+ {isUploading ? (
118
+ <div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white" />
119
+ ) : (
120
+ <Check className="w-3 h-3" />
121
+ )}
122
+ </Button>
123
+ <Button
124
+ size="sm"
125
+ variant="outline"
126
+ onClick={resetAvatar}
127
+ className="h-7 w-7 rounded-full p-0"
128
+ >
129
+ <X className="w-3 h-3" />
130
+ </Button>
131
+ </div>
132
+ )}
133
+ </div>
134
+
135
+ {/* File info - shows when file is selected */}
136
+ {avatarFile && (
137
+ <div className="mt-3 text-center">
138
+ <p className="text-xs text-muted-foreground">
139
+ {avatarFile.name} ({(avatarFile.size / 1024 / 1024).toFixed(2)} MB)
140
+ </p>
141
+ </div>
142
+ )}
143
+ </div>
144
+ );
145
+ };
146
+
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useForm } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { toast } from 'sonner';
7
+ import { profileLogger } from '../../../utils/logger';
8
+
9
+ import {
10
+ Button,
11
+ Form,
12
+ FormControl,
13
+ FormField,
14
+ FormItem,
15
+ FormLabel,
16
+ FormMessage,
17
+ Input,
18
+ Label,
19
+ PhoneInput,
20
+ } from '@djangocfg/ui/components';
21
+ import {
22
+ useAccountsContext,
23
+ PatchedUserProfileUpdateRequestSchema,
24
+ type PatchedUserProfileUpdateRequest
25
+ } from '@djangocfg/api/cfg/contexts';
26
+ import { useAuth } from '../../../auth';
27
+
28
+ export const ProfileForm = () => {
29
+ const { user } = useAuth();
30
+ const accounts = useAccountsContext();
31
+ const [isEditing, setIsEditing] = useState(false);
32
+ const [isSaving, setIsSaving] = useState(false);
33
+
34
+ const form = useForm<PatchedUserProfileUpdateRequest>({
35
+ resolver: zodResolver(PatchedUserProfileUpdateRequestSchema),
36
+ defaultValues: {
37
+ first_name: '',
38
+ last_name: '',
39
+ company: '',
40
+ position: '',
41
+ phone: '',
42
+ },
43
+ });
44
+
45
+ // Load user data
46
+ useEffect(() => {
47
+ if (user) {
48
+ form.reset({
49
+ first_name: user.first_name || '',
50
+ last_name: user.last_name || '',
51
+ company: user.company || '',
52
+ position: user.position || '',
53
+ phone: user.phone || '',
54
+ });
55
+ }
56
+ }, [user, form]);
57
+
58
+ const handleSubmit = async (data: PatchedUserProfileUpdateRequest) => {
59
+ setIsSaving(true);
60
+ try {
61
+ await accounts.partialUpdateProfile(data);
62
+ toast.success('Profile updated successfully');
63
+ setIsEditing(false);
64
+ } catch (error: any) {
65
+ profileLogger.error('Profile update error:', error);
66
+ if (error?.response?.data) {
67
+ const fieldErrors = error.response.data;
68
+ Object.entries(fieldErrors).forEach(([field, messages]) => {
69
+ if (Array.isArray(messages) && messages.length > 0) {
70
+ form.setError(field as keyof PatchedUserProfileUpdateRequest, {
71
+ type: 'server',
72
+ message: messages[0],
73
+ });
74
+ }
75
+ });
76
+ toast.error('Please fix the validation errors');
77
+ } else {
78
+ toast.error('Failed to update profile');
79
+ }
80
+ } finally {
81
+ setIsSaving(false);
82
+ }
83
+ };
84
+
85
+ const handleCancel = () => {
86
+ setIsEditing(false);
87
+ form.clearErrors();
88
+ if (user) {
89
+ form.reset({
90
+ first_name: user.first_name || '',
91
+ last_name: user.last_name || '',
92
+ company: user.company || '',
93
+ position: user.position || '',
94
+ phone: user.phone || '',
95
+ });
96
+ }
97
+ };
98
+
99
+ const onSubmit = form.handleSubmit(handleSubmit);
100
+
101
+ return (
102
+ <Form {...form}>
103
+ <form onSubmit={onSubmit} className="space-y-4">
104
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
105
+ <div className="space-y-2 md:col-span-2">
106
+ <Label htmlFor="email">Email</Label>
107
+ <Input id="email" value={user?.email || ''} disabled className="bg-muted" />
108
+ </div>
109
+
110
+ <FormField
111
+ control={form.control}
112
+ name="first_name"
113
+ render={({ field }) => (
114
+ <FormItem>
115
+ <FormLabel>First Name</FormLabel>
116
+ <FormControl>
117
+ <Input {...field} disabled={!isEditing} placeholder="Enter first name" />
118
+ </FormControl>
119
+ <FormMessage />
120
+ </FormItem>
121
+ )}
122
+ />
123
+
124
+ <FormField
125
+ control={form.control}
126
+ name="last_name"
127
+ render={({ field }) => (
128
+ <FormItem>
129
+ <FormLabel>Last Name</FormLabel>
130
+ <FormControl>
131
+ <Input {...field} disabled={!isEditing} placeholder="Enter last name" />
132
+ </FormControl>
133
+ <FormMessage />
134
+ </FormItem>
135
+ )}
136
+ />
137
+
138
+ <FormField
139
+ control={form.control}
140
+ name="company"
141
+ render={({ field }) => (
142
+ <FormItem>
143
+ <FormLabel>Company</FormLabel>
144
+ <FormControl>
145
+ <Input {...field} disabled={!isEditing} placeholder="Enter company name" />
146
+ </FormControl>
147
+ <FormMessage />
148
+ </FormItem>
149
+ )}
150
+ />
151
+
152
+ <FormField
153
+ control={form.control}
154
+ name="position"
155
+ render={({ field }) => (
156
+ <FormItem>
157
+ <FormLabel>Position</FormLabel>
158
+ <FormControl>
159
+ <Input {...field} disabled={!isEditing} placeholder="Enter position" />
160
+ </FormControl>
161
+ <FormMessage />
162
+ </FormItem>
163
+ )}
164
+ />
165
+
166
+ <FormField
167
+ control={form.control}
168
+ name="phone"
169
+ render={({ field }) => (
170
+ <FormItem className="md:col-span-2">
171
+ <FormLabel>Phone</FormLabel>
172
+ <FormControl>
173
+ <PhoneInput
174
+ value={field.value}
175
+ onChange={field.onChange}
176
+ disabled={!isEditing}
177
+ placeholder="Enter phone number"
178
+ defaultCountry="US"
179
+ />
180
+ </FormControl>
181
+ <FormMessage />
182
+ </FormItem>
183
+ )}
184
+ />
185
+ </div>
186
+
187
+ {/* Action Buttons */}
188
+ <div className="flex items-center justify-between pt-4">
189
+ {isEditing ? (
190
+ <div className="flex items-center gap-2">
191
+ <Button type="submit" disabled={isSaving}>
192
+ {isSaving ? 'Saving...' : 'Save Changes'}
193
+ </Button>
194
+ <Button type="button" variant="outline" onClick={handleCancel}>
195
+ Cancel
196
+ </Button>
197
+ </div>
198
+ ) : (
199
+ <Button type="button" onClick={() => setIsEditing(true)}>
200
+ Edit Profile
201
+ </Button>
202
+ )}
203
+ </div>
204
+ </form>
205
+ </Form>
206
+ );
207
+ };
208
+
@@ -0,0 +1,3 @@
1
+ export { AvatarSection } from './AvatarSection';
2
+ export { ProfileForm } from './ProfileForm';
3
+
@@ -0,0 +1,3 @@
1
+ export { ProfileLayout } from './ProfileLayout';
2
+ export type { } from './ProfileLayout';
3
+
@@ -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,178 @@
1
+ /**
2
+ * Support Layout
3
+ * Modern support layout with resizable panels for desktop and mobile-optimized view
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import { SupportProvider } from '@djangocfg/api/cfg/contexts';
10
+ import { SupportLayoutProvider, useSupportLayoutContext } from './context';
11
+ import {
12
+ TicketList,
13
+ MessageList,
14
+ MessageInput,
15
+ CreateTicketDialog,
16
+ } from './components';
17
+ import {
18
+ Button,
19
+ ResizablePanelGroup,
20
+ ResizablePanel,
21
+ ResizableHandle,
22
+ } from '@djangocfg/ui';
23
+ import { Plus, LifeBuoy, ArrowLeft } from 'lucide-react';
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────
26
+ // Support Layout Content (with context)
27
+ // ─────────────────────────────────────────────────────────────────────────
28
+
29
+ const SupportLayoutContent: React.FC = () => {
30
+ const { selectedTicket, selectTicket, openCreateDialog, getUnreadCount } =
31
+ useSupportLayoutContext();
32
+ const [isMobile, setIsMobile] = React.useState(false);
33
+
34
+ React.useEffect(() => {
35
+ const checkMobile = () => setIsMobile(window.innerWidth <= 768);
36
+ checkMobile();
37
+ window.addEventListener('resize', checkMobile);
38
+ return () => window.removeEventListener('resize', checkMobile);
39
+ }, []);
40
+
41
+ const unreadCount = getUnreadCount();
42
+
43
+ if (isMobile) {
44
+ // Mobile layout - single column with navigation
45
+ return (
46
+ <div className="h-screen flex flex-col overflow-hidden">
47
+ {/* Mobile Header */}
48
+ <div className="flex items-center justify-between p-4 border-b bg-background flex-shrink-0">
49
+ <div className="flex items-center gap-2">
50
+ {selectedTicket ? (
51
+ <Button
52
+ variant="ghost"
53
+ size="sm"
54
+ onClick={() => selectTicket(null)}
55
+ className="p-1"
56
+ >
57
+ <ArrowLeft className="h-5 w-5" />
58
+ </Button>
59
+ ) : (
60
+ <LifeBuoy className="h-6 w-6 text-primary" />
61
+ )}
62
+ <h1 className="text-xl font-semibold">
63
+ {selectedTicket ? selectedTicket.subject : 'Support'}
64
+ </h1>
65
+ {unreadCount > 0 && !selectedTicket && (
66
+ <div className="h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
67
+ {unreadCount}
68
+ </div>
69
+ )}
70
+ </div>
71
+ {!selectedTicket && (
72
+ <Button onClick={openCreateDialog} size="sm">
73
+ <Plus className="h-4 w-4 mr-2" />
74
+ New Ticket
75
+ </Button>
76
+ )}
77
+ </div>
78
+
79
+ {/* Mobile Content */}
80
+ <div className="flex-1 min-h-0 overflow-hidden">
81
+ {selectedTicket ? (
82
+ // Show messages when ticket is selected
83
+ <div className="h-full flex flex-col">
84
+ <div className="flex-1 min-h-0 overflow-hidden">
85
+ <MessageList />
86
+ </div>
87
+ <div className="flex-shrink-0">
88
+ <MessageInput />
89
+ </div>
90
+ </div>
91
+ ) : (
92
+ // Show ticket list when no ticket is selected
93
+ <TicketList />
94
+ )}
95
+ </div>
96
+
97
+ {/* Dialog */}
98
+ <CreateTicketDialog />
99
+ </div>
100
+ );
101
+ }
102
+
103
+ // Desktop layout - resizable panels
104
+ return (
105
+ <div className="h-screen flex flex-col overflow-hidden">
106
+ {/* Desktop Header */}
107
+ <div className="flex items-center justify-between p-6 border-b bg-background flex-shrink-0">
108
+ <div className="flex items-center gap-3">
109
+ <LifeBuoy className="h-7 w-7 text-primary" />
110
+ <div>
111
+ <h1 className="text-2xl font-bold">Support Center</h1>
112
+ <p className="text-sm text-muted-foreground">Get help from our support team</p>
113
+ </div>
114
+ {unreadCount > 0 && (
115
+ <div className="h-6 w-6 bg-red-500 text-white text-sm rounded-full flex items-center justify-center">
116
+ {unreadCount}
117
+ </div>
118
+ )}
119
+ </div>
120
+
121
+ <Button onClick={openCreateDialog}>
122
+ <Plus className="h-4 w-4 mr-2" />
123
+ New Ticket
124
+ </Button>
125
+ </div>
126
+
127
+ {/* Desktop Content */}
128
+ <div className="flex-1 min-h-0 overflow-hidden">
129
+ <ResizablePanelGroup direction="horizontal" className="h-full">
130
+ {/* Ticket List Panel */}
131
+ <ResizablePanel defaultSize={35} minSize={25} maxSize={50}>
132
+ <div className="h-full border-r overflow-hidden">
133
+ <TicketList />
134
+ </div>
135
+ </ResizablePanel>
136
+
137
+ <ResizableHandle withHandle className="hover:bg-accent transition-colors" />
138
+
139
+ {/* Messages Panel */}
140
+ <ResizablePanel defaultSize={65} minSize={50}>
141
+ <div className="h-full flex flex-col overflow-hidden">
142
+ <div className="flex-1 min-h-0 overflow-hidden">
143
+ <MessageList />
144
+ </div>
145
+ <div className="flex-shrink-0">
146
+ <MessageInput />
147
+ </div>
148
+ </div>
149
+ </ResizablePanel>
150
+ </ResizablePanelGroup>
151
+ </div>
152
+
153
+ {/* Dialog */}
154
+ <CreateTicketDialog />
155
+ </div>
156
+ );
157
+ };
158
+
159
+ // ─────────────────────────────────────────────────────────────────────────
160
+ // Support Layout (with providers)
161
+ // ─────────────────────────────────────────────────────────────────────────
162
+
163
+ export interface SupportLayoutProps {
164
+ children?: React.ReactNode;
165
+ }
166
+
167
+ export const SupportLayout: React.FC<SupportLayoutProps> = () => {
168
+ return (
169
+ <div className="h-screen w-full overflow-hidden">
170
+ <SupportProvider>
171
+ <SupportLayoutProvider>
172
+ <SupportLayoutContent />
173
+ </SupportLayoutProvider>
174
+ </SupportProvider>
175
+ </div>
176
+ );
177
+ };
178
+