@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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +86 -0
- package/src/auth/README.md +962 -0
- package/src/auth/context/AuthContext.tsx +458 -0
- package/src/auth/context/index.ts +2 -0
- package/src/auth/context/types.ts +63 -0
- package/src/auth/hooks/index.ts +6 -0
- package/src/auth/hooks/useAuthForm.ts +329 -0
- package/src/auth/hooks/useAuthGuard.ts +23 -0
- package/src/auth/hooks/useAuthRedirect.ts +51 -0
- package/src/auth/hooks/useAutoAuth.ts +42 -0
- package/src/auth/hooks/useLocalStorage.ts +211 -0
- package/src/auth/hooks/useSessionStorage.ts +186 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/middlewares/index.ts +1 -0
- package/src/auth/middlewares/proxy.ts +24 -0
- package/src/auth/server.ts +6 -0
- package/src/auth/utils/errors.ts +34 -0
- package/src/auth/utils/index.ts +2 -0
- package/src/auth/utils/validation.ts +14 -0
- package/src/index.ts +15 -0
- package/src/layouts/AppLayout/AppLayout.tsx +123 -0
- package/src/layouts/AppLayout/README.md +204 -0
- package/src/layouts/AppLayout/SUMMARY.md +240 -0
- package/src/layouts/AppLayout/USAGE.md +312 -0
- package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
- package/src/layouts/AppLayout/components/Seo.tsx +87 -0
- package/src/layouts/AppLayout/components/index.ts +6 -0
- package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
- package/src/layouts/AppLayout/context/index.ts +5 -0
- package/src/layouts/AppLayout/hooks/index.ts +6 -0
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
- package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
- package/src/layouts/AppLayout/index.ts +31 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/index.ts +7 -0
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
- package/src/layouts/AppLayout/providers/index.ts +5 -0
- package/src/layouts/AppLayout/types/config.ts +40 -0
- package/src/layouts/AppLayout/types/index.ts +10 -0
- package/src/layouts/AppLayout/types/layout.ts +47 -0
- package/src/layouts/AppLayout/types/navigation.ts +41 -0
- package/src/layouts/AppLayout/types/routes.ts +45 -0
- package/src/layouts/AppLayout/utils/index.ts +5 -0
- package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
- package/src/layouts/PaymentsLayout/README.md +133 -0
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
- package/src/layouts/PaymentsLayout/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/events.ts +106 -0
- package/src/layouts/PaymentsLayout/index.ts +20 -0
- package/src/layouts/PaymentsLayout/types.ts +19 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
- package/src/layouts/ProfileLayout/components/index.ts +3 -0
- package/src/layouts/ProfileLayout/index.ts +3 -0
- package/src/layouts/SupportLayout/README.md +91 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
- package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
- package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
- package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
- package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
- package/src/layouts/SupportLayout/components/index.ts +6 -0
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
- package/src/layouts/SupportLayout/context/index.ts +2 -0
- package/src/layouts/SupportLayout/events.ts +31 -0
- package/src/layouts/SupportLayout/hooks/index.ts +2 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
- package/src/layouts/SupportLayout/index.ts +6 -0
- package/src/layouts/SupportLayout/types.ts +23 -0
- package/src/layouts/index.ts +9 -0
- package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
- package/src/snippets/AuthDialog/events.ts +21 -0
- package/src/snippets/AuthDialog/index.ts +3 -0
- package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
- package/src/snippets/Breadcrumbs.tsx +80 -0
- package/src/snippets/Chat/ChatUIContext.tsx +110 -0
- package/src/snippets/Chat/ChatWidget.tsx +476 -0
- package/src/snippets/Chat/README.md +122 -0
- package/src/snippets/Chat/components/MessageInput.tsx +124 -0
- package/src/snippets/Chat/components/MessageList.tsx +168 -0
- package/src/snippets/Chat/components/SessionList.tsx +192 -0
- package/src/snippets/Chat/components/index.ts +9 -0
- package/src/snippets/Chat/hooks/index.ts +6 -0
- package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
- package/src/snippets/Chat/index.tsx +44 -0
- package/src/snippets/Chat/types.ts +79 -0
- package/src/snippets/VideoPlayer/README.md +203 -0
- package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
- package/src/snippets/VideoPlayer/index.ts +8 -0
- package/src/snippets/VideoPlayer/types.ts +61 -0
- package/src/snippets/index.ts +10 -0
- package/src/styles/dashboard.css +41 -0
- package/src/styles/index.css +20 -0
- package/src/styles/sources.css +6 -0
- package/src/types/index.ts +1 -0
- package/src/types/pageConfig.ts +103 -0
- package/src/utils/index.ts +6 -0
- 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,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
|
+
|