create-crm-tmp 1.1.2 → 2.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/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
2
|
+
|
|
1
3
|
interface MeetCancellationEmailTemplateProps {
|
|
2
4
|
contactName: string;
|
|
3
5
|
title: string;
|
|
@@ -50,7 +52,13 @@ export function MeetCancellationEmailTemplate({
|
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
return (
|
|
53
|
-
<div
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
58
|
+
lineHeight: '1.6',
|
|
59
|
+
color: '#333',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
54
62
|
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
|
|
55
63
|
<h1 style={{ color: '#EF4444', fontSize: '24px', marginBottom: '20px' }}>
|
|
56
64
|
Annulation de rendez-vous
|
|
@@ -93,7 +101,7 @@ export function MeetCancellationEmailTemplate({
|
|
|
93
101
|
<strong>Description :</strong>
|
|
94
102
|
<div
|
|
95
103
|
style={{ marginTop: '10px' }}
|
|
96
|
-
dangerouslySetInnerHTML={{ __html: description }}
|
|
104
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }}
|
|
97
105
|
/>
|
|
98
106
|
</div>
|
|
99
107
|
)}
|
|
@@ -111,7 +119,7 @@ export function MeetCancellationEmailTemplate({
|
|
|
111
119
|
borderTop: '1px solid #ddd',
|
|
112
120
|
fontSize: '14px',
|
|
113
121
|
}}
|
|
114
|
-
dangerouslySetInnerHTML={{ __html: signature }}
|
|
122
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
|
|
115
123
|
/>
|
|
116
124
|
)}
|
|
117
125
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
2
3
|
|
|
3
4
|
interface MeetConfirmationEmailTemplateProps {
|
|
4
5
|
contactName: string;
|
|
@@ -52,7 +53,13 @@ export function MeetConfirmationEmailTemplate({
|
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
return (
|
|
55
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
59
|
+
lineHeight: '1.6',
|
|
60
|
+
color: '#333',
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
56
63
|
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
|
|
57
64
|
<h1 style={{ color: '#1a1a1a', fontSize: '24px', marginBottom: '20px' }}>
|
|
58
65
|
Confirmation de rendez-vous
|
|
@@ -94,7 +101,7 @@ export function MeetConfirmationEmailTemplate({
|
|
|
94
101
|
<strong>Description :</strong>
|
|
95
102
|
<div
|
|
96
103
|
style={{ marginTop: '10px' }}
|
|
97
|
-
dangerouslySetInnerHTML={{ __html: description }}
|
|
104
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }}
|
|
98
105
|
/>
|
|
99
106
|
</div>
|
|
100
107
|
)}
|
|
@@ -147,7 +154,7 @@ export function MeetConfirmationEmailTemplate({
|
|
|
147
154
|
borderTop: '1px solid #ddd',
|
|
148
155
|
fontSize: '14px',
|
|
149
156
|
}}
|
|
150
|
-
dangerouslySetInnerHTML={{ __html: signature }}
|
|
157
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
|
|
151
158
|
/>
|
|
152
159
|
)}
|
|
153
160
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
2
3
|
|
|
3
4
|
interface MeetUpdateEmailTemplateProps {
|
|
4
5
|
contactName: string;
|
|
@@ -62,7 +63,13 @@ export function MeetUpdateEmailTemplate({
|
|
|
62
63
|
const isGoogleMeet = !!meetLink;
|
|
63
64
|
|
|
64
65
|
return (
|
|
65
|
-
<div
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
69
|
+
lineHeight: '1.6',
|
|
70
|
+
color: '#333',
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
66
73
|
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
|
|
67
74
|
<h1 style={{ color: '#1a1a1a', fontSize: '24px', marginBottom: '20px' }}>
|
|
68
75
|
Modification de rendez-vous
|
|
@@ -154,7 +161,7 @@ export function MeetUpdateEmailTemplate({
|
|
|
154
161
|
<strong>Description :</strong>
|
|
155
162
|
<div
|
|
156
163
|
style={{ marginTop: '10px' }}
|
|
157
|
-
dangerouslySetInnerHTML={{ __html: description }}
|
|
164
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }}
|
|
158
165
|
/>
|
|
159
166
|
</div>
|
|
160
167
|
)}
|
|
@@ -200,7 +207,7 @@ export function MeetUpdateEmailTemplate({
|
|
|
200
207
|
borderTop: '1px solid #ddd',
|
|
201
208
|
fontSize: '14px',
|
|
202
209
|
}}
|
|
203
|
-
dangerouslySetInnerHTML={{ __html: signature }}
|
|
210
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
|
|
204
211
|
/>
|
|
205
212
|
)}
|
|
206
213
|
</div>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
interface PageHeaderProps {
|
|
2
4
|
title: string;
|
|
3
5
|
description?: string;
|
|
@@ -6,22 +8,24 @@ interface PageHeaderProps {
|
|
|
6
8
|
|
|
7
9
|
export function PageHeader({ title, description, action }: Readonly<PageHeaderProps>) {
|
|
8
10
|
return (
|
|
9
|
-
<div className="border-b border-
|
|
10
|
-
<div>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<div className="
|
|
14
|
-
<
|
|
15
|
-
|
|
11
|
+
<div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8 lg:py-6">
|
|
12
|
+
<div className="flex items-start gap-3">
|
|
13
|
+
<div className="min-w-0 flex-1">
|
|
14
|
+
{action ? (
|
|
15
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
16
|
+
<div className="min-w-0 flex-1">
|
|
17
|
+
<h1 className="text-xl font-bold text-foreground sm:text-2xl">{title}</h1>
|
|
18
|
+
{description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
|
|
19
|
+
</div>
|
|
20
|
+
<div className="shrink-0">{action}</div>
|
|
16
21
|
</div>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)}
|
|
22
|
+
) : (
|
|
23
|
+
<>
|
|
24
|
+
<h1 className="text-xl font-bold text-foreground sm:text-2xl">{title}</h1>
|
|
25
|
+
{description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
|
|
26
|
+
</>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
25
29
|
</div>
|
|
26
30
|
</div>
|
|
27
31
|
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useSession } from '@/lib/auth-client';
|
|
6
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
7
|
+
import { PageLoader } from '@/components/skeleton';
|
|
8
|
+
|
|
9
|
+
const FALLBACK_ROUTES: { permission: string | string[]; href: string }[] = [
|
|
10
|
+
{ permission: 'dashboard.view', href: '/dashboard' },
|
|
11
|
+
{ permission: 'contacts.view_own', href: '/contacts' },
|
|
12
|
+
{ permission: 'contacts.view_all', href: '/contacts' },
|
|
13
|
+
{ permission: ['tasks.view_all', 'tasks.view_own'], href: '/agenda' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function getFirstAccessibleRoute(hasPermission: (p: string) => boolean): string {
|
|
17
|
+
for (const route of FALLBACK_ROUTES) {
|
|
18
|
+
const perms = Array.isArray(route.permission) ? route.permission : [route.permission];
|
|
19
|
+
if (perms.some((p) => hasPermission(p))) {
|
|
20
|
+
return route.href;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return '/contacts';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ProtectedPageProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
requiredPermission?: string | string[];
|
|
29
|
+
requireAll?: boolean;
|
|
30
|
+
redirectTo?: string;
|
|
31
|
+
fallback?: React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ProtectedPage({
|
|
35
|
+
children,
|
|
36
|
+
requiredPermission,
|
|
37
|
+
requireAll = false,
|
|
38
|
+
redirectTo,
|
|
39
|
+
fallback,
|
|
40
|
+
}: ProtectedPageProps) {
|
|
41
|
+
const router = useRouter();
|
|
42
|
+
const { data: session, isPending: isSessionPending } = useSession();
|
|
43
|
+
const { hasPermission, isLoading } = useUserRole();
|
|
44
|
+
|
|
45
|
+
const checkAccess = (perms: string[]) => {
|
|
46
|
+
if (requireAll) return perms.every((p) => hasPermission(p));
|
|
47
|
+
return perms.some((p) => hasPermission(p));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
// Sans session (déconnecté), rediriger vers la page de connexion pour éviter la boucle
|
|
52
|
+
if (!isSessionPending && !session?.user) {
|
|
53
|
+
const signinUrl = `/signin?callbackUrl=${encodeURIComponent(typeof globalThis.window !== 'undefined' ? globalThis.window.location.pathname : '/')}`;
|
|
54
|
+
router.push(signinUrl);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (isLoading || !requiredPermission) return;
|
|
58
|
+
|
|
59
|
+
const permissions = Array.isArray(requiredPermission)
|
|
60
|
+
? requiredPermission
|
|
61
|
+
: [requiredPermission];
|
|
62
|
+
|
|
63
|
+
if (!checkAccess(permissions)) {
|
|
64
|
+
router.push(redirectTo ?? getFirstAccessibleRoute(hasPermission));
|
|
65
|
+
}
|
|
66
|
+
}, [
|
|
67
|
+
requiredPermission,
|
|
68
|
+
requireAll,
|
|
69
|
+
redirectTo,
|
|
70
|
+
hasPermission,
|
|
71
|
+
isLoading,
|
|
72
|
+
isSessionPending,
|
|
73
|
+
session?.user,
|
|
74
|
+
router,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
if (isSessionPending || !session?.user || isLoading) {
|
|
78
|
+
return (
|
|
79
|
+
fallback || <PageLoader text="Vérification des permissions..." className="min-h-screen" />
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!requiredPermission) {
|
|
84
|
+
return <>{children}</>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const permissions = Array.isArray(requiredPermission) ? requiredPermission : [requiredPermission];
|
|
88
|
+
|
|
89
|
+
if (!checkAccess(permissions)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return <>{children}</>;
|
|
94
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
2
|
+
|
|
1
3
|
interface ResetPasswordEmailProps {
|
|
2
4
|
code: string;
|
|
3
5
|
signature?: string | null;
|
|
@@ -7,7 +9,7 @@ export function ResetPasswordEmailTemplate({ code, signature }: ResetPasswordEma
|
|
|
7
9
|
return (
|
|
8
10
|
<div
|
|
9
11
|
style={{
|
|
10
|
-
fontFamily: '
|
|
12
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
11
13
|
padding: '20px',
|
|
12
14
|
maxWidth: '600px',
|
|
13
15
|
margin: '0 auto',
|
|
@@ -71,7 +73,7 @@ export function ResetPasswordEmailTemplate({ code, signature }: ResetPasswordEma
|
|
|
71
73
|
fontSize: '14px',
|
|
72
74
|
lineHeight: '1.6',
|
|
73
75
|
}}
|
|
74
|
-
dangerouslySetInnerHTML={{ __html: signature }}
|
|
76
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
|
|
75
77
|
/>
|
|
76
78
|
)}
|
|
77
79
|
</div>
|
|
@@ -9,29 +9,22 @@ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
|
9
9
|
import { useSidebarContext } from '@/contexts/sidebar-context';
|
|
10
10
|
import { useViewAs } from '@/contexts/view-as-context';
|
|
11
11
|
import { ViewAsModal } from '@/components/view-as-modal';
|
|
12
|
-
import {
|
|
13
|
-
LayoutDashboard,
|
|
14
|
-
Users,
|
|
15
|
-
UserCog,
|
|
16
|
-
Settings,
|
|
17
|
-
Calendar as CalendarIcon,
|
|
18
|
-
FileText,
|
|
19
|
-
Eye,
|
|
20
|
-
Zap,
|
|
21
|
-
Columns3,
|
|
22
|
-
X,
|
|
23
|
-
} from 'lucide-react';
|
|
12
|
+
import { Eye, X, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
|
24
13
|
import { cn } from '@/lib/utils';
|
|
14
|
+
import { NAV_PAGES } from '@/config/nav-pages';
|
|
25
15
|
|
|
26
16
|
export function Sidebar() {
|
|
27
17
|
const pathname = usePathname();
|
|
28
18
|
const { data: session } = useSession();
|
|
29
19
|
const router = useRouter();
|
|
30
20
|
const { isOpen: isMobileMenuOpen, setIsOpen: setIsMobileMenuOpen } = useMobileMenuContext();
|
|
31
|
-
const {
|
|
21
|
+
const { isPinned, togglePin } = useSidebarContext();
|
|
32
22
|
const { viewAsUser, isViewingAsOther } = useViewAs();
|
|
33
23
|
const [showViewAsModal, setShowViewAsModal] = useState(false);
|
|
34
24
|
const [isMounted, setIsMounted] = useState(false);
|
|
25
|
+
/** Ouvert au survol uniquement (desktop, sans modifier le pin) */
|
|
26
|
+
const [expandedByHover, setExpandedByHover] = useState(false);
|
|
27
|
+
const isSidebarExpanded = isPinned || expandedByHover;
|
|
35
28
|
|
|
36
29
|
// Éviter l'erreur d'hydratation
|
|
37
30
|
useEffect(() => {
|
|
@@ -39,36 +32,15 @@ export function Sidebar() {
|
|
|
39
32
|
}, []);
|
|
40
33
|
|
|
41
34
|
// Obtenir le rôle de l'utilisateur via le hook personnalisé
|
|
42
|
-
const {
|
|
35
|
+
const { isRealAdmin, hasPermission } = useUserRole();
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{ name: 'Automatisations', href: '/automatisation', icon: Zap },
|
|
52
|
-
{ name: 'Templates', href: '/templates', icon: FileText },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Ajouter la gestion des droits d'accès seulement pour les admins
|
|
56
|
-
if (isAdmin) {
|
|
57
|
-
baseNav.push({
|
|
58
|
-
name: "Droits d'accès",
|
|
59
|
-
href: '/users',
|
|
60
|
-
icon: UserCog,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
baseNav.push({
|
|
65
|
-
name: 'Paramètres',
|
|
66
|
-
href: '/settings',
|
|
67
|
-
icon: Settings,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return baseNav;
|
|
71
|
-
}, [isAdmin]);
|
|
37
|
+
const dashboardNav = useMemo(
|
|
38
|
+
() =>
|
|
39
|
+
NAV_PAGES.filter(
|
|
40
|
+
(page) => !page.parentLabel && page.permissions.some((p) => hasPermission(p)),
|
|
41
|
+
),
|
|
42
|
+
[hasPermission],
|
|
43
|
+
);
|
|
72
44
|
|
|
73
45
|
const handleSignOut = async () => {
|
|
74
46
|
await signOut();
|
|
@@ -84,7 +56,7 @@ export function Sidebar() {
|
|
|
84
56
|
{/* Overlay for mobile */}
|
|
85
57
|
{isMobileMenuOpen && (
|
|
86
58
|
<div
|
|
87
|
-
className="fixed inset-0 z-40 bg-
|
|
59
|
+
className="fixed inset-0 z-40 bg-foreground/10 backdrop-blur-sm lg:hidden"
|
|
88
60
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
89
61
|
/>
|
|
90
62
|
)}
|
|
@@ -92,49 +64,39 @@ export function Sidebar() {
|
|
|
92
64
|
{/* Sidebar */}
|
|
93
65
|
<div
|
|
94
66
|
className={cn(
|
|
95
|
-
'fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-
|
|
67
|
+
'group fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground shadow-(--shadow-card) transition-all duration-300 ease-(--ease-standard) lg:relative lg:translate-x-0',
|
|
96
68
|
isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
|
97
|
-
|
|
69
|
+
!isSidebarExpanded ? 'w-64 lg:w-16' : 'w-64 lg:w-64',
|
|
98
70
|
)}
|
|
99
71
|
onMouseEnter={() => {
|
|
100
|
-
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
101
|
-
|
|
102
|
-
setIsCollapsed(false);
|
|
103
|
-
}
|
|
72
|
+
if (typeof globalThis.window !== 'undefined' && globalThis.window.innerWidth >= 1024 && !isPinned) {
|
|
73
|
+
setExpandedByHover(true);
|
|
104
74
|
}
|
|
105
75
|
}}
|
|
106
76
|
onMouseLeave={() => {
|
|
107
|
-
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
108
|
-
|
|
109
|
-
setIsCollapsed(true);
|
|
110
|
-
}
|
|
77
|
+
if (typeof globalThis.window !== 'undefined' && globalThis.window.innerWidth >= 1024) {
|
|
78
|
+
setExpandedByHover(false);
|
|
111
79
|
}
|
|
112
80
|
}}
|
|
113
81
|
>
|
|
114
|
-
{/* Bouton fermer - Mobile seulement */}
|
|
115
|
-
<div className="flex h-16 items-center justify-end border-b border-gray-200 px-4 lg:hidden">
|
|
116
|
-
<button
|
|
117
|
-
onClick={() => setIsMobileMenuOpen(false)}
|
|
118
|
-
className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
|
|
119
|
-
aria-label="Close menu"
|
|
120
|
-
>
|
|
121
|
-
<X className="h-5 w-5" />
|
|
122
|
-
</button>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
82
|
{/* Navigation principale */}
|
|
126
83
|
<nav className="flex-1 space-y-6 overflow-y-auto py-4">
|
|
127
84
|
{/* Section Dashboard */}
|
|
128
|
-
<div className={cn('px-3',
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
85
|
+
<div className={cn('px-3', !isSidebarExpanded && 'lg:px-2')}>
|
|
86
|
+
<div className="flex items-center justify-end">
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
89
|
+
className="cursor-pointer rounded-lg p-2 text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground lg:hidden"
|
|
90
|
+
aria-label="Close menu"
|
|
91
|
+
>
|
|
92
|
+
<X className="h-5 w-5" />
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
134
95
|
<div className="space-y-1">
|
|
135
96
|
{dashboardNav.map((item) => {
|
|
136
97
|
const isActive = pathname === item.href;
|
|
137
98
|
const Icon = item.icon;
|
|
99
|
+
const displayName = item.name;
|
|
138
100
|
return (
|
|
139
101
|
<Link
|
|
140
102
|
key={item.name}
|
|
@@ -142,16 +104,16 @@ export function Sidebar() {
|
|
|
142
104
|
onClick={handleLinkClick}
|
|
143
105
|
className={cn(
|
|
144
106
|
'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors',
|
|
145
|
-
|
|
107
|
+
!isSidebarExpanded ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
|
|
146
108
|
isActive
|
|
147
|
-
? 'bg-
|
|
148
|
-
: 'text-
|
|
109
|
+
? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
|
|
110
|
+
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
149
111
|
)}
|
|
150
|
-
title={
|
|
112
|
+
title={!isSidebarExpanded ? displayName : undefined}
|
|
151
113
|
>
|
|
152
114
|
<Icon className="h-5 w-5 shrink-0" />
|
|
153
|
-
{
|
|
154
|
-
<span className="whitespace-nowrap">{
|
|
115
|
+
{isSidebarExpanded && (
|
|
116
|
+
<span className="whitespace-nowrap">{displayName}</span>
|
|
155
117
|
)}
|
|
156
118
|
</Link>
|
|
157
119
|
);
|
|
@@ -164,18 +126,18 @@ export function Sidebar() {
|
|
|
164
126
|
{isRealAdmin && (
|
|
165
127
|
<div
|
|
166
128
|
className={cn(
|
|
167
|
-
'border-t border-
|
|
168
|
-
|
|
129
|
+
'border-t border-sidebar-border transition-all duration-300',
|
|
130
|
+
!isSidebarExpanded ? 'p-3 lg:p-2' : 'p-3',
|
|
169
131
|
)}
|
|
170
132
|
>
|
|
171
|
-
{
|
|
133
|
+
{isSidebarExpanded ? (
|
|
172
134
|
<button
|
|
173
135
|
onClick={() => setShowViewAsModal(true)}
|
|
174
136
|
className={cn(
|
|
175
137
|
'w-full cursor-pointer rounded-lg border-2 p-3 text-left transition-all',
|
|
176
138
|
isViewingAsOther
|
|
177
|
-
? 'border-
|
|
178
|
-
: 'border-
|
|
139
|
+
? 'border-sidebar-primary bg-sidebar-primary text-sidebar-primary-foreground hover:opacity-95'
|
|
140
|
+
: 'border-sidebar-border bg-sidebar text-sidebar-foreground hover:border-sidebar-ring hover:bg-sidebar-accent',
|
|
179
141
|
)}
|
|
180
142
|
aria-label="Changer de vue"
|
|
181
143
|
>
|
|
@@ -183,7 +145,9 @@ export function Sidebar() {
|
|
|
183
145
|
<div
|
|
184
146
|
className={cn(
|
|
185
147
|
'flex h-10 w-10 shrink-0 items-center justify-center rounded-full',
|
|
186
|
-
isViewingAsOther
|
|
148
|
+
isViewingAsOther
|
|
149
|
+
? 'bg-sidebar-primary-foreground/20 text-sidebar-primary-foreground'
|
|
150
|
+
: 'bg-sidebar-accent text-sidebar-accent-foreground',
|
|
187
151
|
)}
|
|
188
152
|
>
|
|
189
153
|
{!isMounted
|
|
@@ -196,7 +160,9 @@ export function Sidebar() {
|
|
|
196
160
|
<p
|
|
197
161
|
className={cn(
|
|
198
162
|
'text-xs font-medium',
|
|
199
|
-
isViewingAsOther
|
|
163
|
+
isViewingAsOther
|
|
164
|
+
? 'text-sidebar-primary-foreground/80'
|
|
165
|
+
: 'text-sidebar-foreground/70',
|
|
200
166
|
)}
|
|
201
167
|
>
|
|
202
168
|
{isViewingAsOther ? 'Vue:' : 'Ma vue'}
|
|
@@ -218,8 +184,8 @@ export function Sidebar() {
|
|
|
218
184
|
className={cn(
|
|
219
185
|
'w-full cursor-pointer rounded-lg p-2 transition-colors',
|
|
220
186
|
isViewingAsOther
|
|
221
|
-
? 'bg-
|
|
222
|
-
: 'text-
|
|
187
|
+
? 'bg-sidebar-primary text-sidebar-primary-foreground hover:opacity-95'
|
|
188
|
+
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent',
|
|
223
189
|
)}
|
|
224
190
|
title="Changer de vue"
|
|
225
191
|
aria-label="Changer de vue"
|
|
@@ -232,43 +198,75 @@ export function Sidebar() {
|
|
|
232
198
|
</div>
|
|
233
199
|
)}
|
|
234
200
|
|
|
201
|
+
{/* Bouton Réduire / Développer la navigation (desktop uniquement) */}
|
|
202
|
+
<div
|
|
203
|
+
className={cn(
|
|
204
|
+
'hidden border-t border-sidebar-border lg:block',
|
|
205
|
+
!isSidebarExpanded ? 'p-3 lg:p-2' : 'p-3',
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
208
|
+
<button
|
|
209
|
+
onClick={togglePin}
|
|
210
|
+
className={cn(
|
|
211
|
+
'flex w-full cursor-pointer items-center gap-3 rounded-lg py-2 text-sm font-medium text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
212
|
+
!isSidebarExpanded ? 'justify-center px-2' : 'px-3',
|
|
213
|
+
)}
|
|
214
|
+
title={isPinned ? 'Réduire la navigation' : 'Développer la navigation'}
|
|
215
|
+
aria-label={isPinned ? 'Réduire la navigation' : 'Développer la navigation'}
|
|
216
|
+
>
|
|
217
|
+
{isPinned ? (
|
|
218
|
+
<>
|
|
219
|
+
<PanelLeftClose className="h-5 w-5 shrink-0" />
|
|
220
|
+
<span className="whitespace-nowrap">Réduire la navigation</span>
|
|
221
|
+
</>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="flex gap-2">
|
|
224
|
+
<PanelLeftOpen className="h-5 w-5 shrink-0" />
|
|
225
|
+
<span className="hidden whitespace-nowrap group-hover:block">
|
|
226
|
+
Développer la navigation
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
235
233
|
{/* User Profile */}
|
|
236
234
|
<div
|
|
237
235
|
className={cn(
|
|
238
|
-
'border-t border-
|
|
239
|
-
|
|
236
|
+
'border-t border-sidebar-border transition-all duration-300',
|
|
237
|
+
!isSidebarExpanded ? 'p-4 lg:p-2' : 'p-4',
|
|
240
238
|
)}
|
|
241
239
|
>
|
|
242
|
-
{
|
|
240
|
+
{isSidebarExpanded ? (
|
|
243
241
|
<>
|
|
244
242
|
<div className="flex items-center gap-3">
|
|
245
|
-
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-
|
|
243
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-accent-foreground">
|
|
246
244
|
{!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
|
|
247
245
|
</div>
|
|
248
246
|
<div className="min-w-0 flex-1">
|
|
249
|
-
<p className="truncate text-sm font-medium text-
|
|
247
|
+
<p className="truncate text-sm font-medium text-sidebar-foreground">
|
|
250
248
|
{!isMounted ? 'Utilisateur' : session?.user?.name || 'Utilisateur'}
|
|
251
249
|
</p>
|
|
252
|
-
<p className="truncate text-xs text-
|
|
250
|
+
<p className="truncate text-xs text-sidebar-foreground/70">
|
|
253
251
|
{!isMounted ? '' : session?.user?.email}
|
|
254
252
|
</p>
|
|
255
253
|
</div>
|
|
256
254
|
</div>
|
|
257
255
|
<button
|
|
258
256
|
onClick={handleSignOut}
|
|
259
|
-
className="mt-3 w-full cursor-pointer rounded-lg bg-
|
|
257
|
+
className="mt-3 w-full cursor-pointer rounded-lg bg-sidebar-accent px-3 py-2 text-sm font-medium text-sidebar-accent-foreground transition-colors duration-200 hover:bg-sidebar-primary hover:text-sidebar-primary-foreground"
|
|
260
258
|
>
|
|
261
259
|
Déconnexion
|
|
262
260
|
</button>
|
|
263
261
|
</>
|
|
264
262
|
) : (
|
|
265
263
|
<div className="flex flex-col items-center gap-2">
|
|
266
|
-
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-
|
|
264
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-accent-foreground">
|
|
267
265
|
{!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
|
|
268
266
|
</div>
|
|
269
267
|
<button
|
|
270
268
|
onClick={handleSignOut}
|
|
271
|
-
className="cursor-pointer rounded-lg p-2 text-
|
|
269
|
+
className="cursor-pointer rounded-lg p-2 text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
272
270
|
title="Déconnexion"
|
|
273
271
|
aria-label="Déconnexion"
|
|
274
272
|
>
|