create-crm-tmp 1.0.0
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/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useSession, signOut } from '@/lib/auth-client';
|
|
6
|
+
import { useRouter } from 'next/navigation';
|
|
7
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
8
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
9
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
10
|
+
import { useSidebarContext } from '@/contexts/sidebar-context';
|
|
11
|
+
import { useViewAs } from '@/contexts/view-as-context';
|
|
12
|
+
import { ViewAsModal } from '@/components/view-as-modal';
|
|
13
|
+
import {
|
|
14
|
+
LayoutDashboard,
|
|
15
|
+
Users,
|
|
16
|
+
UserCog,
|
|
17
|
+
Settings,
|
|
18
|
+
Calendar as CalendarIcon,
|
|
19
|
+
FileText,
|
|
20
|
+
Eye,
|
|
21
|
+
Zap,
|
|
22
|
+
Columns3,
|
|
23
|
+
X,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
import { cn } from '@/lib/utils';
|
|
26
|
+
|
|
27
|
+
export function Sidebar() {
|
|
28
|
+
const pathname = usePathname();
|
|
29
|
+
const { data: session } = useSession();
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const { isOpen: isMobileMenuOpen, setIsOpen: setIsMobileMenuOpen } = useMobileMenuContext();
|
|
32
|
+
const { isCollapsed, isPinned, setIsCollapsed, togglePin } = useSidebarContext();
|
|
33
|
+
const { viewAsUser, isViewingAsOther } = useViewAs();
|
|
34
|
+
const [showViewAsModal, setShowViewAsModal] = useState(false);
|
|
35
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
36
|
+
|
|
37
|
+
// Éviter l'erreur d'hydratation
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setIsMounted(true);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
// Obtenir le rôle de l'utilisateur via le hook personnalisé
|
|
43
|
+
const { isAdmin, isRealAdmin } = useUserRole();
|
|
44
|
+
|
|
45
|
+
// Navigation principale (Dashboard section)
|
|
46
|
+
const dashboardNav = useMemo(() => {
|
|
47
|
+
const baseNav = [
|
|
48
|
+
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
|
49
|
+
{ name: 'Contacts', href: '/contacts', icon: Users },
|
|
50
|
+
{ name: 'Agenda', href: '/agenda', icon: CalendarIcon },
|
|
51
|
+
{ name: 'Closing', href: '/closing', icon: Columns3 },
|
|
52
|
+
{ name: 'Automatisations', href: '/automatisation', icon: Zap },
|
|
53
|
+
{ name: 'Templates', href: '/templates', icon: FileText },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Ajouter la gestion des droits d'accès seulement pour les admins
|
|
57
|
+
if (isAdmin) {
|
|
58
|
+
baseNav.push({
|
|
59
|
+
name: "Droits d'accès",
|
|
60
|
+
href: '/users',
|
|
61
|
+
icon: UserCog,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
baseNav.push({
|
|
66
|
+
name: 'Paramètres',
|
|
67
|
+
href: '/settings',
|
|
68
|
+
icon: Settings,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return baseNav;
|
|
72
|
+
}, [isAdmin]);
|
|
73
|
+
|
|
74
|
+
const handleSignOut = async () => {
|
|
75
|
+
await signOut();
|
|
76
|
+
router.push('/signin');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleLinkClick = () => {
|
|
80
|
+
setIsMobileMenuOpen(false);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
{/* Overlay for mobile */}
|
|
86
|
+
{isMobileMenuOpen && (
|
|
87
|
+
<div
|
|
88
|
+
className="fixed inset-0 z-40 bg-gray-500/20 backdrop-blur-sm lg:hidden"
|
|
89
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Sidebar */}
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
'fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-gray-200 bg-white transition-all duration-300 ease-in-out lg:relative lg:translate-x-0',
|
|
97
|
+
isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
|
98
|
+
isCollapsed && !isPinned ? 'w-64 lg:w-16' : 'w-64 lg:w-64',
|
|
99
|
+
)}
|
|
100
|
+
onMouseEnter={() => {
|
|
101
|
+
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
102
|
+
if (!isPinned && isCollapsed) {
|
|
103
|
+
setIsCollapsed(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}}
|
|
107
|
+
onMouseLeave={() => {
|
|
108
|
+
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
109
|
+
if (!isPinned && !isCollapsed) {
|
|
110
|
+
setIsCollapsed(true);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{/* Bouton fermer - Mobile seulement */}
|
|
116
|
+
<div className="flex h-16 items-center justify-end border-b border-gray-200 px-4 lg:hidden">
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
119
|
+
className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
|
|
120
|
+
aria-label="Close menu"
|
|
121
|
+
>
|
|
122
|
+
<X className="h-5 w-5" />
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Navigation principale */}
|
|
127
|
+
<nav className="flex-1 space-y-6 overflow-y-auto py-4">
|
|
128
|
+
{/* Section Dashboard */}
|
|
129
|
+
<div className={cn('px-3', isCollapsed && !isPinned && 'lg:px-2')}>
|
|
130
|
+
{(!isCollapsed || isPinned) && (
|
|
131
|
+
<h2 className="mb-2 px-3 text-xs font-semibold tracking-wider text-gray-500 uppercase">
|
|
132
|
+
CRM Template
|
|
133
|
+
</h2>
|
|
134
|
+
)}
|
|
135
|
+
<div className="space-y-1">
|
|
136
|
+
{dashboardNav.map((item) => {
|
|
137
|
+
const isActive = pathname === item.href;
|
|
138
|
+
const Icon = item.icon;
|
|
139
|
+
return (
|
|
140
|
+
<Link
|
|
141
|
+
key={item.name}
|
|
142
|
+
href={item.href}
|
|
143
|
+
onClick={handleLinkClick}
|
|
144
|
+
className={cn(
|
|
145
|
+
'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors',
|
|
146
|
+
isCollapsed && !isPinned ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
|
|
147
|
+
isActive
|
|
148
|
+
? 'bg-indigo-50 text-indigo-600'
|
|
149
|
+
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900',
|
|
150
|
+
)}
|
|
151
|
+
title={isCollapsed && !isPinned ? item.name : undefined}
|
|
152
|
+
>
|
|
153
|
+
<Icon className="h-5 w-5 shrink-0" />
|
|
154
|
+
{(!isCollapsed || isPinned) && (
|
|
155
|
+
<span className="whitespace-nowrap">{item.name}</span>
|
|
156
|
+
)}
|
|
157
|
+
</Link>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</nav>
|
|
163
|
+
|
|
164
|
+
{/* Vue active - pour les admins seulement */}
|
|
165
|
+
{isRealAdmin && (
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
'border-t border-gray-200 transition-all duration-300',
|
|
169
|
+
isCollapsed && !isPinned ? 'p-3 lg:p-2' : 'p-3',
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{!isCollapsed || isPinned ? (
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => setShowViewAsModal(true)}
|
|
175
|
+
className={cn(
|
|
176
|
+
'w-full cursor-pointer rounded-lg border-2 p-3 text-left transition-all',
|
|
177
|
+
isViewingAsOther
|
|
178
|
+
? 'border-indigo-600 bg-indigo-600 text-white hover:border-indigo-700 hover:bg-indigo-700'
|
|
179
|
+
: 'border-gray-300 bg-white text-gray-900 hover:border-indigo-300 hover:bg-indigo-50',
|
|
180
|
+
)}
|
|
181
|
+
aria-label="Changer de vue"
|
|
182
|
+
>
|
|
183
|
+
<div className="flex items-center gap-3">
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
'flex h-10 w-10 shrink-0 items-center justify-center rounded-full',
|
|
187
|
+
isViewingAsOther ? 'bg-white/20 text-white' : 'bg-indigo-100 text-indigo-600',
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
{!isMounted
|
|
191
|
+
? 'U'
|
|
192
|
+
: isViewingAsOther
|
|
193
|
+
? viewAsUser?.name?.[0]?.toUpperCase() || 'U'
|
|
194
|
+
: session?.user?.name?.[0]?.toUpperCase() || 'U'}
|
|
195
|
+
</div>
|
|
196
|
+
<div className="min-w-0 flex-1">
|
|
197
|
+
<p
|
|
198
|
+
className={cn(
|
|
199
|
+
'text-xs font-medium',
|
|
200
|
+
isViewingAsOther ? 'text-white/80' : 'text-gray-500',
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
203
|
+
{isViewingAsOther ? 'Vue:' : 'Ma vue'}
|
|
204
|
+
</p>
|
|
205
|
+
<p className={`truncate text-sm font-semibold`}>
|
|
206
|
+
{!isMounted
|
|
207
|
+
? 'Utilisateur'
|
|
208
|
+
: isViewingAsOther
|
|
209
|
+
? viewAsUser?.name
|
|
210
|
+
: session?.user?.name || 'Utilisateur'}
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
<Eye className="h-5 w-5 shrink-0" />
|
|
214
|
+
</div>
|
|
215
|
+
</button>
|
|
216
|
+
) : (
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => setShowViewAsModal(true)}
|
|
219
|
+
className={cn(
|
|
220
|
+
'w-full cursor-pointer rounded-lg p-2 transition-colors',
|
|
221
|
+
isViewingAsOther
|
|
222
|
+
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
|
223
|
+
: 'text-gray-500 hover:bg-gray-100',
|
|
224
|
+
)}
|
|
225
|
+
title="Changer de vue"
|
|
226
|
+
aria-label="Changer de vue"
|
|
227
|
+
>
|
|
228
|
+
<div className="flex items-center justify-center">
|
|
229
|
+
<Eye className="h-5 w-5" />
|
|
230
|
+
</div>
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* User Profile */}
|
|
237
|
+
<div
|
|
238
|
+
className={cn(
|
|
239
|
+
'border-t border-gray-200 transition-all duration-300',
|
|
240
|
+
isCollapsed && !isPinned ? 'p-4 lg:p-2' : 'p-4',
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{!isCollapsed || isPinned ? (
|
|
244
|
+
<>
|
|
245
|
+
<div className="flex items-center gap-3">
|
|
246
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-indigo-600">
|
|
247
|
+
{!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
|
|
248
|
+
</div>
|
|
249
|
+
<div className="min-w-0 flex-1">
|
|
250
|
+
<p className="truncate text-sm font-medium text-gray-900">
|
|
251
|
+
{!isMounted ? 'Utilisateur' : session?.user?.name || 'Utilisateur'}
|
|
252
|
+
</p>
|
|
253
|
+
<p className="truncate text-xs text-gray-500">
|
|
254
|
+
{!isMounted ? '' : session?.user?.email}
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<button
|
|
259
|
+
onClick={handleSignOut}
|
|
260
|
+
className="mt-3 w-full cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
|
261
|
+
>
|
|
262
|
+
Déconnexion
|
|
263
|
+
</button>
|
|
264
|
+
</>
|
|
265
|
+
) : (
|
|
266
|
+
<div className="flex flex-col items-center gap-2">
|
|
267
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-indigo-600">
|
|
268
|
+
{!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
|
|
269
|
+
</div>
|
|
270
|
+
<button
|
|
271
|
+
onClick={handleSignOut}
|
|
272
|
+
className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
|
|
273
|
+
title="Déconnexion"
|
|
274
|
+
aria-label="Déconnexion"
|
|
275
|
+
>
|
|
276
|
+
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
277
|
+
<path
|
|
278
|
+
strokeLinecap="round"
|
|
279
|
+
strokeLinejoin="round"
|
|
280
|
+
strokeWidth={2}
|
|
281
|
+
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
282
|
+
/>
|
|
283
|
+
</svg>
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Modal de changement de vue */}
|
|
291
|
+
<ViewAsModal isOpen={showViewAsModal} onClose={() => setShowViewAsModal(false)} />
|
|
292
|
+
</>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composants Skeleton pour les états de chargement
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
export function Skeleton({ className }: { className?: string }) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className={cn('animate-pulse rounded bg-gray-200', className)}
|
|
11
|
+
aria-label="Chargement..."
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ContactTableSkeleton() {
|
|
17
|
+
return (
|
|
18
|
+
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
|
19
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
20
|
+
<thead className="bg-gray-50">
|
|
21
|
+
<tr>
|
|
22
|
+
<th className="px-3 py-3 sm:px-6">
|
|
23
|
+
<Skeleton className="h-4 w-4" />
|
|
24
|
+
</th>
|
|
25
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
26
|
+
Contact
|
|
27
|
+
</th>
|
|
28
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
29
|
+
Téléphone
|
|
30
|
+
</th>
|
|
31
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
32
|
+
Email
|
|
33
|
+
</th>
|
|
34
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
35
|
+
Statut
|
|
36
|
+
</th>
|
|
37
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
38
|
+
Origine
|
|
39
|
+
</th>
|
|
40
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
41
|
+
COMMERCIAL
|
|
42
|
+
</th>
|
|
43
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
44
|
+
TÉLÉPRO
|
|
45
|
+
</th>
|
|
46
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
47
|
+
CRÉÉ LE
|
|
48
|
+
</th>
|
|
49
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
50
|
+
MODIFIÉ LE
|
|
51
|
+
</th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
55
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
56
|
+
<tr key={i} className="hover:bg-gray-50">
|
|
57
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
58
|
+
<Skeleton className="h-4 w-4" />
|
|
59
|
+
</td>
|
|
60
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
61
|
+
<div className="flex items-center">
|
|
62
|
+
<Skeleton className="h-10 w-10 rounded-full" />
|
|
63
|
+
<div className="ml-3 space-y-2 sm:ml-4">
|
|
64
|
+
<Skeleton className="h-4 w-32" />
|
|
65
|
+
<Skeleton className="h-3 w-24" />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</td>
|
|
69
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
70
|
+
<Skeleton className="h-4 w-28" />
|
|
71
|
+
</td>
|
|
72
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
73
|
+
<Skeleton className="h-4 w-40" />
|
|
74
|
+
</td>
|
|
75
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
76
|
+
<Skeleton className="h-6 w-20 rounded-full" />
|
|
77
|
+
</td>
|
|
78
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
79
|
+
<Skeleton className="h-4 w-28" />
|
|
80
|
+
</td>
|
|
81
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
82
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
83
|
+
</td>
|
|
84
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
85
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
86
|
+
</td>
|
|
87
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
88
|
+
<Skeleton className="h-4 w-32" />
|
|
89
|
+
</td>
|
|
90
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
91
|
+
<Skeleton className="h-4 w-32" />
|
|
92
|
+
</td>
|
|
93
|
+
</tr>
|
|
94
|
+
))}
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ContactCardsSkeleton() {
|
|
102
|
+
return (
|
|
103
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
104
|
+
{Array.from({ length: 9 }).map((_, i) => (
|
|
105
|
+
<div key={i} className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
106
|
+
{/* En-tête */}
|
|
107
|
+
<div className="mb-4 flex items-start justify-between">
|
|
108
|
+
<div className="flex items-center gap-3">
|
|
109
|
+
<Skeleton className="h-12 w-12 rounded-full" />
|
|
110
|
+
<div className="space-y-2">
|
|
111
|
+
<Skeleton className="h-5 w-32" />
|
|
112
|
+
<Skeleton className="h-3 w-24" />
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Informations de contact */}
|
|
118
|
+
<div className="mb-4 space-y-2">
|
|
119
|
+
<div className="flex items-center">
|
|
120
|
+
<Skeleton className="mr-2 h-4 w-4" />
|
|
121
|
+
<Skeleton className="h-4 w-28" />
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center">
|
|
124
|
+
<Skeleton className="mr-2 h-4 w-4" />
|
|
125
|
+
<Skeleton className="h-4 w-36" />
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center">
|
|
128
|
+
<Skeleton className="mr-2 h-4 w-4" />
|
|
129
|
+
<Skeleton className="h-4 w-32" />
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex items-center">
|
|
132
|
+
<Skeleton className="mr-2 h-4 w-4" />
|
|
133
|
+
<Skeleton className="h-4 w-24" />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Badge statut */}
|
|
138
|
+
<div className="mb-4">
|
|
139
|
+
<Skeleton className="h-6 w-24 rounded-full" />
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Pied de carte avec utilisateurs assignés */}
|
|
143
|
+
<div className="flex items-start justify-between border-t border-gray-100 pt-4">
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<div className="flex items-center gap-2">
|
|
146
|
+
<Skeleton className="h-7 w-7 rounded-full" />
|
|
147
|
+
<div className="space-y-1">
|
|
148
|
+
<Skeleton className="h-4 w-20" />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<Skeleton className="h-7 w-7 rounded-full" />
|
|
153
|
+
<div className="space-y-1">
|
|
154
|
+
<Skeleton className="h-4 w-20" />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function AgendaMonthSkeleton() {
|
|
166
|
+
return (
|
|
167
|
+
<div className="rounded-lg bg-white shadow">
|
|
168
|
+
<div className="grid grid-cols-7 border-b border-gray-200">
|
|
169
|
+
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
|
|
170
|
+
<div
|
|
171
|
+
key={day}
|
|
172
|
+
className="border-r border-gray-200 p-3 text-center text-sm font-semibold text-gray-700 last:border-r-0"
|
|
173
|
+
>
|
|
174
|
+
{day}
|
|
175
|
+
</div>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="grid grid-cols-7">
|
|
179
|
+
{Array.from({ length: 42 }).map((_, i) => (
|
|
180
|
+
<div
|
|
181
|
+
key={i}
|
|
182
|
+
className="min-h-[100px] border-r border-b border-gray-200 p-2 last:border-r-0"
|
|
183
|
+
>
|
|
184
|
+
<Skeleton className="mb-2 h-5 w-6" />
|
|
185
|
+
<div className="space-y-1">
|
|
186
|
+
<Skeleton className="h-5 w-full" />
|
|
187
|
+
<Skeleton className="h-5 w-3/4" />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function AgendaWeekSkeleton() {
|
|
197
|
+
const HOURS = Array.from({ length: 17 }, (_, i) => i + 6); // 6h à 22h
|
|
198
|
+
// Utiliser un pattern déterministe pour éviter les erreurs d'hydratation
|
|
199
|
+
// On affiche un skeleton toutes les 3 heures pour chaque jour
|
|
200
|
+
const shouldShowSkeleton = (dayIndex: number, hourIndex: number) => {
|
|
201
|
+
return (dayIndex + hourIndex) % 3 === 0;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="overflow-auto rounded-lg bg-white shadow">
|
|
206
|
+
<div className="grid grid-cols-8 border-b border-gray-200 bg-gray-50 text-xs font-medium text-gray-500">
|
|
207
|
+
<div className="px-3 py-2 text-right">(UTC+01:00) Hr</div>
|
|
208
|
+
{Array.from({ length: 7 }).map((_, i) => (
|
|
209
|
+
<div key={i} className="border-l border-gray-200 px-3 py-2 text-center">
|
|
210
|
+
<Skeleton className="mx-auto h-4 w-12" />
|
|
211
|
+
<Skeleton className="mx-auto mt-1 h-8 w-8 rounded-full" />
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="grid grid-cols-8 text-xs">
|
|
216
|
+
<div className="border-r border-gray-200 bg-gray-50">
|
|
217
|
+
{HOURS.map((hour) => (
|
|
218
|
+
<div
|
|
219
|
+
key={hour}
|
|
220
|
+
className="flex h-16 items-start justify-end border-b border-gray-200 pr-2"
|
|
221
|
+
>
|
|
222
|
+
<Skeleton className="h-3 w-10" />
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
{Array.from({ length: 7 }).map((_, dayIndex) => (
|
|
227
|
+
<div key={dayIndex} className="border-l border-gray-200">
|
|
228
|
+
{HOURS.map((hour, hourIndex) => (
|
|
229
|
+
<div key={hour} className="relative h-16 border-b border-gray-100 px-1.5 py-0.5">
|
|
230
|
+
{shouldShowSkeleton(dayIndex, hourIndex) && (
|
|
231
|
+
<Skeleton className="h-12 w-full rounded" />
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function AgendaDaySkeleton() {
|
|
243
|
+
return (
|
|
244
|
+
<div className="space-y-4">
|
|
245
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
246
|
+
<div key={i} className="rounded-lg border border-gray-200 bg-white p-4 shadow">
|
|
247
|
+
<div className="flex items-start justify-between">
|
|
248
|
+
<div className="flex-1">
|
|
249
|
+
<div className="flex items-center gap-2">
|
|
250
|
+
<Skeleton className="h-5 w-5 rounded-full" />
|
|
251
|
+
<div className="flex-1 space-y-2">
|
|
252
|
+
<div className="flex items-center gap-2">
|
|
253
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
254
|
+
<Skeleton className="h-4 w-20" />
|
|
255
|
+
</div>
|
|
256
|
+
<Skeleton className="h-5 w-48" />
|
|
257
|
+
<Skeleton className="h-4 w-32" />
|
|
258
|
+
<div className="mt-2 flex items-center gap-4">
|
|
259
|
+
<Skeleton className="h-4 w-20" />
|
|
260
|
+
<Skeleton className="h-4 w-24" />
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function UsersTableSkeleton() {
|
|
273
|
+
return (
|
|
274
|
+
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
|
275
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
276
|
+
<thead className="bg-gray-50">
|
|
277
|
+
<tr>
|
|
278
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
279
|
+
Utilisateur
|
|
280
|
+
</th>
|
|
281
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
282
|
+
Email
|
|
283
|
+
</th>
|
|
284
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
285
|
+
Rôle
|
|
286
|
+
</th>
|
|
287
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
288
|
+
Email vérifié
|
|
289
|
+
</th>
|
|
290
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
291
|
+
Compte
|
|
292
|
+
</th>
|
|
293
|
+
</tr>
|
|
294
|
+
</thead>
|
|
295
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
296
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
297
|
+
<tr key={i} className="hover:bg-gray-50">
|
|
298
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
299
|
+
<div className="flex items-center">
|
|
300
|
+
<Skeleton className="h-8 w-8 rounded-full sm:h-10 sm:w-10" />
|
|
301
|
+
<div className="ml-2 space-y-1 sm:ml-4">
|
|
302
|
+
<Skeleton className="h-4 w-32" />
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</td>
|
|
306
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
307
|
+
<Skeleton className="h-4 w-40" />
|
|
308
|
+
</td>
|
|
309
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
310
|
+
<Skeleton className="h-8 w-24 rounded-md" />
|
|
311
|
+
</td>
|
|
312
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
313
|
+
<Skeleton className="h-6 w-20 rounded-full" />
|
|
314
|
+
</td>
|
|
315
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
316
|
+
<div className="flex items-center gap-2">
|
|
317
|
+
<Skeleton className="h-5 w-9 rounded-full" />
|
|
318
|
+
<Skeleton className="h-4 w-12" />
|
|
319
|
+
</div>
|
|
320
|
+
</td>
|
|
321
|
+
</tr>
|
|
322
|
+
))}
|
|
323
|
+
</tbody>
|
|
324
|
+
</table>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function TemplatesPageSkeleton() {
|
|
330
|
+
return (
|
|
331
|
+
<div className="h-full">
|
|
332
|
+
{/* Header Skeleton */}
|
|
333
|
+
<div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8">
|
|
334
|
+
<div className="flex items-center justify-between">
|
|
335
|
+
<div className="space-y-2">
|
|
336
|
+
<Skeleton className="h-8 w-48" />
|
|
337
|
+
<Skeleton className="h-4 w-64" />
|
|
338
|
+
</div>
|
|
339
|
+
<Skeleton className="h-10 w-40 rounded-lg" />
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div className="p-4 sm:p-6 lg:p-8">
|
|
344
|
+
{/* Filtres Skeleton */}
|
|
345
|
+
<div className="mb-6 flex flex-wrap gap-2">
|
|
346
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
347
|
+
<Skeleton key={i} className="h-10 w-24 rounded-lg" />
|
|
348
|
+
))}
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Grille de templates Skeleton */}
|
|
352
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
353
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
354
|
+
<div
|
|
355
|
+
key={i}
|
|
356
|
+
className="rounded-lg border border-gray-200 bg-white p-4 shadow transition-shadow"
|
|
357
|
+
>
|
|
358
|
+
<div className="flex items-start justify-between">
|
|
359
|
+
<div className="flex-1">
|
|
360
|
+
<div className="mb-2 flex items-center gap-2">
|
|
361
|
+
<Skeleton className="h-5 w-5" />
|
|
362
|
+
<Skeleton className="h-6 w-32" />
|
|
363
|
+
</div>
|
|
364
|
+
<Skeleton className="mb-2 h-6 w-20 rounded-full" />
|
|
365
|
+
<Skeleton className="mb-1 h-4 w-full" />
|
|
366
|
+
<Skeleton className="mb-1 h-4 w-3/4" />
|
|
367
|
+
<Skeleton className="h-4 w-1/2" />
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="mt-4 flex items-center justify-end gap-2">
|
|
371
|
+
<Skeleton className="h-8 w-8 rounded-lg" />
|
|
372
|
+
<Skeleton className="h-8 w-8 rounded-lg" />
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
))}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|