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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import useSWR from 'swr';
|
|
6
|
+
import { Search, Users, Building2, FileText, Loader2 } from 'lucide-react';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
import { NAV_PAGES } from '@/config/nav-pages';
|
|
9
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
10
|
+
|
|
11
|
+
interface ContactResult {
|
|
12
|
+
id: string;
|
|
13
|
+
firstName: string | null;
|
|
14
|
+
lastName: string | null;
|
|
15
|
+
email: string | null;
|
|
16
|
+
phone: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CompanyResult {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
email: string | null;
|
|
23
|
+
phone: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SearchResults {
|
|
27
|
+
contacts: ContactResult[];
|
|
28
|
+
companies: CompanyResult[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fetcher = async (url: string): Promise<SearchResults> => {
|
|
32
|
+
const params = new URL(url, globalThis.location?.origin);
|
|
33
|
+
const q = params.searchParams.get('q') || '';
|
|
34
|
+
const searchParam = encodeURIComponent(q);
|
|
35
|
+
|
|
36
|
+
const [contactsRes, companiesRes] = await Promise.all([
|
|
37
|
+
fetch(`/api/contacts?search=${searchParam}&limit=5`),
|
|
38
|
+
fetch(`/api/companies?search=${searchParam}&limit=5`),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const [contactsData, companiesData] = await Promise.all([
|
|
42
|
+
contactsRes.ok ? contactsRes.json() : { contacts: [] },
|
|
43
|
+
companiesRes.ok ? companiesRes.json() : { companies: [] },
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
contacts: contactsData.contacts ?? [],
|
|
48
|
+
companies: companiesData.companies ?? companiesData ?? [],
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function useDebounce(value: string, delay: number) {
|
|
53
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
56
|
+
return () => clearTimeout(timer);
|
|
57
|
+
}, [value, delay]);
|
|
58
|
+
return debouncedValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function GlobalSearch() {
|
|
62
|
+
const [query, setQuery] = useState('');
|
|
63
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
64
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
const { hasPermission } = useUserRole();
|
|
68
|
+
|
|
69
|
+
const debouncedQuery = useDebounce(query.trim(), 300);
|
|
70
|
+
const shouldFetchCRM = debouncedQuery.length >= 2;
|
|
71
|
+
|
|
72
|
+
const { data: crmResults, isLoading: crmLoading } = useSWR(
|
|
73
|
+
shouldFetchCRM ? `/api/_search?q=${debouncedQuery}` : null,
|
|
74
|
+
fetcher,
|
|
75
|
+
{ keepPreviousData: true, revalidateOnFocus: false },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const filteredPages = useMemo(() => {
|
|
79
|
+
const q = query.trim().toLowerCase();
|
|
80
|
+
if (q.length === 0) return [];
|
|
81
|
+
return NAV_PAGES.filter(
|
|
82
|
+
(page) =>
|
|
83
|
+
page.permissions.some((p) => hasPermission(p)) &&
|
|
84
|
+
`${page.parentLabel ?? ''} ${page.name}`.toLowerCase().includes(q),
|
|
85
|
+
);
|
|
86
|
+
}, [query, hasPermission]);
|
|
87
|
+
|
|
88
|
+
const flatOptions = useMemo(() => {
|
|
89
|
+
const opts: { href: string }[] = [];
|
|
90
|
+
filteredPages.forEach((p) => opts.push({ href: p.href }));
|
|
91
|
+
if (crmResults) {
|
|
92
|
+
crmResults.contacts.forEach((c) => opts.push({ href: `/contacts/${c.id}` }));
|
|
93
|
+
crmResults.companies.forEach((c) => opts.push({ href: `/contacts/companies/${c.id}` }));
|
|
94
|
+
}
|
|
95
|
+
return opts;
|
|
96
|
+
}, [filteredPages, crmResults]);
|
|
97
|
+
|
|
98
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
99
|
+
const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
100
|
+
const showDropdown = isOpen && query.trim().length > 0;
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
setFocusedIndex(0);
|
|
104
|
+
}, [flatOptions.length]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!showDropdown || focusedIndex < 0 || focusedIndex >= flatOptions.length) return;
|
|
108
|
+
optionRefs.current[focusedIndex]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
109
|
+
}, [showDropdown, focusedIndex, flatOptions.length]);
|
|
110
|
+
|
|
111
|
+
const hasResults =
|
|
112
|
+
filteredPages.length > 0 ||
|
|
113
|
+
(crmResults &&
|
|
114
|
+
(crmResults.contacts.length > 0 ||
|
|
115
|
+
crmResults.companies.length > 0));
|
|
116
|
+
|
|
117
|
+
const navigate = useCallback(
|
|
118
|
+
(href: string) => {
|
|
119
|
+
setIsOpen(false);
|
|
120
|
+
setQuery('');
|
|
121
|
+
router.push(href);
|
|
122
|
+
},
|
|
123
|
+
[router],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
128
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
129
|
+
setIsOpen(false);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
133
|
+
if (e.key === 'Escape') {
|
|
134
|
+
setIsOpen(false);
|
|
135
|
+
inputRef.current?.blur();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
139
|
+
document.addEventListener('keydown', handleEscape);
|
|
140
|
+
return () => {
|
|
141
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
142
|
+
document.removeEventListener('keydown', handleEscape);
|
|
143
|
+
};
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const handleKeyDown = useCallback(
|
|
147
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
148
|
+
if (!showDropdown || flatOptions.length === 0) return;
|
|
149
|
+
if (e.key === 'ArrowDown') {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
setFocusedIndex((i) => Math.min(i + 1, flatOptions.length - 1));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (e.key === 'ArrowUp') {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
setFocusedIndex((i) => Math.max(i - 1, 0));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (e.key === 'Enter') {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
navigate(flatOptions[focusedIndex].href);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
[showDropdown, flatOptions, focusedIndex, navigate],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
let optionIndex = 0;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div ref={containerRef} className="relative hidden w-full max-w-xl sm:block">
|
|
171
|
+
<div className="relative">
|
|
172
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
173
|
+
<input
|
|
174
|
+
ref={inputRef}
|
|
175
|
+
type="text"
|
|
176
|
+
value={query}
|
|
177
|
+
onChange={(e) => {
|
|
178
|
+
setQuery(e.target.value);
|
|
179
|
+
if (!isOpen) setIsOpen(true);
|
|
180
|
+
}}
|
|
181
|
+
onFocus={() => {
|
|
182
|
+
if (query.trim().length > 0) setIsOpen(true);
|
|
183
|
+
}}
|
|
184
|
+
onKeyDown={handleKeyDown}
|
|
185
|
+
placeholder="Rechercher Gold Blessing"
|
|
186
|
+
className="w-full rounded-lg border border-border bg-muted py-2 pr-4 pl-9 text-sm text-foreground transition-colors outline-none placeholder:text-muted-foreground focus:border-primary/40 focus:bg-background focus:ring-1 focus:ring-primary/40"
|
|
187
|
+
role="combobox"
|
|
188
|
+
aria-expanded={showDropdown}
|
|
189
|
+
aria-controls="global-search-results"
|
|
190
|
+
aria-activedescendant={
|
|
191
|
+
showDropdown && flatOptions.length > 0
|
|
192
|
+
? `global-search-option-${focusedIndex}`
|
|
193
|
+
: undefined
|
|
194
|
+
}
|
|
195
|
+
autoComplete="off"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{showDropdown && (
|
|
200
|
+
<div
|
|
201
|
+
id="global-search-results"
|
|
202
|
+
role="listbox"
|
|
203
|
+
className="absolute top-full left-0 z-50 mt-1 max-h-112 w-full overflow-y-auto rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)"
|
|
204
|
+
>
|
|
205
|
+
{/* Pages */}
|
|
206
|
+
{filteredPages.length > 0 && (
|
|
207
|
+
<div>
|
|
208
|
+
<div className="px-3 pt-3 pb-1">
|
|
209
|
+
<span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
|
210
|
+
Pages
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
{filteredPages.map((page) => {
|
|
214
|
+
const Icon = page.icon;
|
|
215
|
+
const idx = optionIndex++;
|
|
216
|
+
return (
|
|
217
|
+
<button
|
|
218
|
+
key={page.href}
|
|
219
|
+
ref={(el) => {
|
|
220
|
+
optionRefs.current[idx] = el;
|
|
221
|
+
}}
|
|
222
|
+
id={`global-search-option-${idx}`}
|
|
223
|
+
onClick={() => navigate(page.href)}
|
|
224
|
+
className={cn(
|
|
225
|
+
'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm text-popover-foreground transition-colors duration-200 hover:bg-accent',
|
|
226
|
+
focusedIndex === idx && 'bg-accent',
|
|
227
|
+
)}
|
|
228
|
+
role="option"
|
|
229
|
+
aria-selected={focusedIndex === idx}
|
|
230
|
+
>
|
|
231
|
+
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
232
|
+
<div className="min-w-0 flex-1">
|
|
233
|
+
<span className="block truncate font-medium text-popover-foreground">{page.name}</span>
|
|
234
|
+
{page.parentLabel && (
|
|
235
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
236
|
+
{page.parentLabel} {'>'} {page.name}
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</button>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* CRM loading */}
|
|
247
|
+
{shouldFetchCRM && crmLoading && !crmResults && (
|
|
248
|
+
<div className="flex items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground">
|
|
249
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
250
|
+
Recherche...
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* Contacts */}
|
|
255
|
+
{crmResults && crmResults.contacts.length > 0 && (
|
|
256
|
+
<div>
|
|
257
|
+
<div
|
|
258
|
+
className={cn(
|
|
259
|
+
'px-3 pt-3 pb-1',
|
|
260
|
+
filteredPages.length > 0 && 'border-t border-border',
|
|
261
|
+
)}
|
|
262
|
+
>
|
|
263
|
+
<span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
|
264
|
+
Contacts
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
{crmResults.contacts.map((contact) => {
|
|
268
|
+
const name =
|
|
269
|
+
[contact.firstName, contact.lastName].filter(Boolean).join(' ') ||
|
|
270
|
+
'Contact sans nom';
|
|
271
|
+
const idx = optionIndex++;
|
|
272
|
+
return (
|
|
273
|
+
<button
|
|
274
|
+
key={contact.id}
|
|
275
|
+
ref={(el) => {
|
|
276
|
+
optionRefs.current[idx] = el;
|
|
277
|
+
}}
|
|
278
|
+
id={`global-search-option-${idx}`}
|
|
279
|
+
onClick={() => navigate(`/contacts/${contact.id}`)}
|
|
280
|
+
className={cn(
|
|
281
|
+
'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-accent',
|
|
282
|
+
focusedIndex === idx && 'bg-accent',
|
|
283
|
+
)}
|
|
284
|
+
role="option"
|
|
285
|
+
aria-selected={focusedIndex === idx}
|
|
286
|
+
>
|
|
287
|
+
<Users className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
288
|
+
<div className="min-w-0 flex-1">
|
|
289
|
+
<span className="font-medium text-popover-foreground">{name}</span>
|
|
290
|
+
{(contact.email || contact.phone) && (
|
|
291
|
+
<span className="ml-2 truncate text-xs text-muted-foreground">
|
|
292
|
+
{contact.email || contact.phone}
|
|
293
|
+
</span>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
</button>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Entreprises */}
|
|
303
|
+
{crmResults && crmResults.companies.length > 0 && (
|
|
304
|
+
<div>
|
|
305
|
+
<div className="border-t border-border px-3 pt-3 pb-1">
|
|
306
|
+
<span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
|
307
|
+
Entreprises
|
|
308
|
+
</span>
|
|
309
|
+
</div>
|
|
310
|
+
{crmResults.companies.map((company) => {
|
|
311
|
+
const idx = optionIndex++;
|
|
312
|
+
return (
|
|
313
|
+
<button
|
|
314
|
+
key={company.id}
|
|
315
|
+
ref={(el) => {
|
|
316
|
+
optionRefs.current[idx] = el;
|
|
317
|
+
}}
|
|
318
|
+
id={`global-search-option-${idx}`}
|
|
319
|
+
onClick={() => navigate(`/contacts/companies/${company.id}`)}
|
|
320
|
+
className={cn(
|
|
321
|
+
'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-accent',
|
|
322
|
+
focusedIndex === idx && 'bg-accent',
|
|
323
|
+
)}
|
|
324
|
+
role="option"
|
|
325
|
+
aria-selected={focusedIndex === idx}
|
|
326
|
+
>
|
|
327
|
+
<Building2 className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
328
|
+
<div className="min-w-0 flex-1">
|
|
329
|
+
<span className="font-medium text-popover-foreground">{company.name}</span>
|
|
330
|
+
{company.email && (
|
|
331
|
+
<span className="ml-2 truncate text-xs text-muted-foreground">{company.email}</span>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
</button>
|
|
335
|
+
);
|
|
336
|
+
})}
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{/* Aucun résultat */}
|
|
341
|
+
{!crmLoading && !hasResults && query.trim().length > 0 && (
|
|
342
|
+
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
343
|
+
Aucun résultat pour "{query.trim()}"
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{/* Recherche trop courte pour le CRM */}
|
|
348
|
+
{query.trim().length === 1 && filteredPages.length === 0 && (
|
|
349
|
+
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
350
|
+
<FileText className="mx-auto mb-1 h-5 w-5 text-muted-foreground/70" />
|
|
351
|
+
Tapez au moins 2 caractères pour rechercher dans le CRM
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
@@ -7,7 +7,8 @@ import { useRouter } from 'next/navigation';
|
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
9
9
|
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
10
|
-
import {
|
|
10
|
+
import { GlobalSearch } from '@/components/global-search';
|
|
11
|
+
import { safeLocalStorageGet, safeLocalStorageSet } from '@/lib/local-storage';
|
|
11
12
|
|
|
12
13
|
interface Reminder {
|
|
13
14
|
id: string;
|
|
@@ -35,7 +36,7 @@ const TASK_TYPE_LABELS: Record<string, string> = {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const PRIORITY_COLORS: Record<string, string> = {
|
|
38
|
-
LOW: 'bg-
|
|
39
|
+
LOW: 'bg-muted text-muted-foreground',
|
|
39
40
|
MEDIUM: 'bg-yellow-100 text-yellow-700',
|
|
40
41
|
HIGH: 'bg-orange-100 text-orange-700',
|
|
41
42
|
URGENT: 'bg-red-100 text-red-700',
|
|
@@ -59,7 +60,6 @@ export function Header() {
|
|
|
59
60
|
const { data: session } = useSession();
|
|
60
61
|
const router = useRouter();
|
|
61
62
|
const { toggle: toggleMobileMenu } = useMobileMenuContext();
|
|
62
|
-
const { viewAsUser, isViewingAsOther } = useViewAs();
|
|
63
63
|
const [showRemindersDropdown, setShowRemindersDropdown] = useState(false);
|
|
64
64
|
const [showUserDropdown, setShowUserDropdown] = useState(false);
|
|
65
65
|
const [reminders, setReminders] = useState<Reminder[]>([]);
|
|
@@ -68,29 +68,20 @@ export function Header() {
|
|
|
68
68
|
const remindersRef = useRef<HTMLDivElement>(null);
|
|
69
69
|
const userRef = useRef<HTMLDivElement>(null);
|
|
70
70
|
|
|
71
|
-
const
|
|
72
|
-
const userName = isViewingAsOther ? viewAsUser?.name || 'Utilisateur' : realUserName;
|
|
71
|
+
const userName = session?.user?.name || 'Utilisateur';
|
|
73
72
|
const userEmail = session?.user?.email || '';
|
|
74
73
|
const userInitial = userName?.[0]?.toUpperCase() || 'U';
|
|
75
74
|
|
|
76
75
|
// Charger les rappels lus depuis localStorage
|
|
77
76
|
useEffect(() => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (stored) {
|
|
81
|
-
try {
|
|
82
|
-
setReadReminders(new Set(JSON.parse(stored)));
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error('Erreur lors du chargement des rappels lus:', e);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
77
|
+
const stored = safeLocalStorageGet<string[]>('read-reminders', []);
|
|
78
|
+
setReadReminders(new Set(stored));
|
|
88
79
|
}, []);
|
|
89
80
|
|
|
90
81
|
// Sauvegarder les rappels lus dans localStorage
|
|
91
82
|
useEffect(() => {
|
|
92
|
-
if (
|
|
93
|
-
|
|
83
|
+
if (readReminders.size > 0) {
|
|
84
|
+
safeLocalStorageSet('read-reminders', Array.from(readReminders));
|
|
94
85
|
}
|
|
95
86
|
}, [readReminders]);
|
|
96
87
|
|
|
@@ -150,37 +141,40 @@ export function Header() {
|
|
|
150
141
|
};
|
|
151
142
|
|
|
152
143
|
return (
|
|
153
|
-
<header className="sticky top-0 z-
|
|
154
|
-
<div className="flex items-center
|
|
155
|
-
{/* Left:
|
|
156
|
-
<div className="flex items-center gap-2 sm:gap-3">
|
|
157
|
-
{/* Bouton burger - visible uniquement sur mobile/tablette */}
|
|
158
|
-
<button
|
|
159
|
-
onClick={toggleMobileMenu}
|
|
160
|
-
className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 lg:hidden"
|
|
161
|
-
aria-label="Ouvrir le menu"
|
|
162
|
-
>
|
|
163
|
-
<Menu className="h-5 w-5" />
|
|
164
|
-
</button>
|
|
144
|
+
<header className="sticky top-0 z-40 border-b border-border bg-background/95 px-4 py-3 backdrop-blur-sm sm:px-6 lg:px-8">
|
|
145
|
+
<div className="flex items-center gap-2 sm:gap-4">
|
|
146
|
+
{/* Left: Logo + Greeting */}
|
|
147
|
+
<div className="flex shrink-0 items-center gap-2 sm:gap-3">
|
|
165
148
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
166
|
-
{/*
|
|
167
|
-
<
|
|
149
|
+
{/* Bouton burger pour mobile */}
|
|
150
|
+
<button
|
|
151
|
+
onClick={toggleMobileMenu}
|
|
152
|
+
className="cursor-pointer rounded-lg p-2 text-foreground/80 transition-colors duration-200 hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none lg:hidden"
|
|
153
|
+
aria-label="Ouvrir ou fermer le menu"
|
|
154
|
+
>
|
|
155
|
+
<Menu className="h-5 w-5" />
|
|
156
|
+
</button>
|
|
157
|
+
<span className="text-base font-bold text-foreground sm:text-lg">Gold Blessing</span>
|
|
168
158
|
</div>
|
|
169
|
-
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Center: Global Search */}
|
|
162
|
+
<div className="flex min-w-0 flex-1 justify-center">
|
|
163
|
+
<GlobalSearch />
|
|
170
164
|
</div>
|
|
171
165
|
|
|
172
166
|
{/* Right: Notifications + User Avatar */}
|
|
173
|
-
<div className="flex items-center gap-2 sm:gap-3">
|
|
167
|
+
<div className="flex shrink-0 items-center gap-2 sm:gap-3">
|
|
174
168
|
{/* Notifications Dropdown */}
|
|
175
169
|
<div className="relative" ref={remindersRef}>
|
|
176
170
|
<button
|
|
177
171
|
onClick={() => setShowRemindersDropdown(!showRemindersDropdown)}
|
|
178
|
-
className="relative cursor-pointer rounded-lg p-2 text-
|
|
172
|
+
className="relative cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
|
|
179
173
|
aria-label="Notifications"
|
|
180
174
|
>
|
|
181
175
|
<Bell className="h-5 w-5" />
|
|
182
176
|
{unreadCount > 0 && (
|
|
183
|
-
<span className="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-
|
|
177
|
+
<span className="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-semibold text-primary-foreground">
|
|
184
178
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
185
179
|
</span>
|
|
186
180
|
)}
|
|
@@ -188,14 +182,14 @@ export function Header() {
|
|
|
188
182
|
|
|
189
183
|
{/* Dropdown des rappels */}
|
|
190
184
|
{showRemindersDropdown && (
|
|
191
|
-
<div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-
|
|
192
|
-
<div className="border-b border-
|
|
185
|
+
<div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)">
|
|
186
|
+
<div className="border-b border-border px-4 py-3">
|
|
193
187
|
<div className="flex items-center justify-between">
|
|
194
|
-
<h3 className="text-sm font-semibold text-
|
|
188
|
+
<h3 className="text-sm font-semibold text-popover-foreground">Rappels</h3>
|
|
195
189
|
{unreadCount > 0 && (
|
|
196
190
|
<button
|
|
197
191
|
onClick={handleMarkAllAsRead}
|
|
198
|
-
className="cursor-pointer text-xs text-
|
|
192
|
+
className="cursor-pointer text-xs text-primary hover:text-primary/80 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
|
|
199
193
|
>
|
|
200
194
|
Tout marquer comme lu
|
|
201
195
|
</button>
|
|
@@ -204,11 +198,11 @@ export function Header() {
|
|
|
204
198
|
</div>
|
|
205
199
|
<div className="max-h-96 overflow-y-auto">
|
|
206
200
|
{loading ? (
|
|
207
|
-
<div className="px-4 py-8 text-center text-sm text-
|
|
201
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground">Chargement...</div>
|
|
208
202
|
) : reminders.length === 0 ? (
|
|
209
|
-
<div className="px-4 py-8 text-center text-sm text-
|
|
203
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground">Aucun rappel</div>
|
|
210
204
|
) : (
|
|
211
|
-
<div className="divide-y divide-
|
|
205
|
+
<div className="divide-y divide-border">
|
|
212
206
|
{reminders.map((reminder) => {
|
|
213
207
|
const isRead = readReminders.has(reminder.id);
|
|
214
208
|
const { date, time } = formatDateTime(reminder.scheduledAt);
|
|
@@ -218,17 +212,18 @@ export function Header() {
|
|
|
218
212
|
: null;
|
|
219
213
|
|
|
220
214
|
return (
|
|
221
|
-
<
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
222
217
|
key={reminder.id}
|
|
223
218
|
className={cn(
|
|
224
|
-
'px-4 py-3 transition-colors hover:bg-
|
|
225
|
-
!isRead && 'bg-
|
|
219
|
+
'w-full px-4 py-3 text-left transition-colors duration-200 hover:bg-accent',
|
|
220
|
+
!isRead && 'bg-accent/70',
|
|
226
221
|
)}
|
|
227
222
|
onClick={() => handleMarkAsRead(reminder.id)}
|
|
228
223
|
>
|
|
229
224
|
<div className="flex items-start gap-3">
|
|
230
225
|
<div className="mt-0.5 shrink-0">
|
|
231
|
-
<Calendar className="h-4 w-4 text-
|
|
226
|
+
<Calendar className="h-4 w-4 text-primary" />
|
|
232
227
|
</div>
|
|
233
228
|
<div className="min-w-0 flex-1">
|
|
234
229
|
<div className="flex items-start justify-between gap-2">
|
|
@@ -236,18 +231,18 @@ export function Header() {
|
|
|
236
231
|
<p
|
|
237
232
|
className={cn(
|
|
238
233
|
'text-sm font-medium',
|
|
239
|
-
!isRead ? 'text-
|
|
234
|
+
!isRead ? 'text-foreground' : 'text-muted-foreground',
|
|
240
235
|
)}
|
|
241
236
|
>
|
|
242
237
|
{reminder.title || TASK_TYPE_LABELS[reminder.type] || 'Tâche'}
|
|
243
238
|
</p>
|
|
244
239
|
{contactName && (
|
|
245
|
-
<p className="mt-0.5 text-xs text-
|
|
240
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{contactName}</p>
|
|
246
241
|
)}
|
|
247
242
|
<div className="mt-1 flex items-center gap-2">
|
|
248
|
-
<span className="text-xs text-
|
|
249
|
-
<span className="text-xs text-
|
|
250
|
-
<span className="text-xs text-
|
|
243
|
+
<span className="text-xs text-muted-foreground">{date}</span>
|
|
244
|
+
<span className="text-xs text-muted-foreground/70">•</span>
|
|
245
|
+
<span className="text-xs text-muted-foreground">{time}</span>
|
|
251
246
|
<span
|
|
252
247
|
className={cn(
|
|
253
248
|
'rounded-full px-1.5 py-0.5 text-[10px] font-medium',
|
|
@@ -266,21 +261,21 @@ export function Header() {
|
|
|
266
261
|
</div>
|
|
267
262
|
</div>
|
|
268
263
|
{!isRead && (
|
|
269
|
-
<div className="h-2 w-2 shrink-0 rounded-full bg-
|
|
264
|
+
<div className="h-2 w-2 shrink-0 rounded-full bg-primary" />
|
|
270
265
|
)}
|
|
271
266
|
</div>
|
|
272
267
|
{reminder.contact && (
|
|
273
268
|
<Link
|
|
274
269
|
href={`/contacts/${reminder.contact.id}`}
|
|
275
270
|
onClick={(e) => e.stopPropagation()}
|
|
276
|
-
className="mt-2 inline-block text-xs font-medium text-
|
|
271
|
+
className="mt-2 inline-block text-xs font-medium text-primary hover:text-primary/80"
|
|
277
272
|
>
|
|
278
273
|
Voir le contact
|
|
279
274
|
</Link>
|
|
280
275
|
)}
|
|
281
276
|
</div>
|
|
282
277
|
</div>
|
|
283
|
-
</
|
|
278
|
+
</button>
|
|
284
279
|
);
|
|
285
280
|
})}
|
|
286
281
|
</div>
|
|
@@ -294,26 +289,26 @@ export function Header() {
|
|
|
294
289
|
<div className="relative" ref={userRef}>
|
|
295
290
|
<button
|
|
296
291
|
onClick={() => setShowUserDropdown(!showUserDropdown)}
|
|
297
|
-
className="flex cursor-pointer items-center gap-1.5 sm:gap-2"
|
|
292
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-md transition-colors duration-200 hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none sm:gap-2"
|
|
298
293
|
aria-label="Menu utilisateur"
|
|
299
294
|
>
|
|
300
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-
|
|
295
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/15 text-xs font-semibold text-primary sm:h-9 sm:w-9 sm:text-sm">
|
|
301
296
|
{userInitial}
|
|
302
297
|
</div>
|
|
303
|
-
<ChevronDown className="hidden h-4 w-4 text-
|
|
298
|
+
<ChevronDown className="hidden h-4 w-4 text-muted-foreground transition-colors hover:text-foreground sm:block" />
|
|
304
299
|
</button>
|
|
305
300
|
|
|
306
301
|
{/* Dropdown utilisateur */}
|
|
307
302
|
{showUserDropdown && (
|
|
308
|
-
<div className="absolute right-0 mt-2 w-56 rounded-
|
|
309
|
-
<div className="border-b border-
|
|
310
|
-
<p className="text-sm font-medium text-
|
|
311
|
-
<p className="mt-0.5 text-xs text-
|
|
303
|
+
<div className="absolute right-0 mt-2 w-56 rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)">
|
|
304
|
+
<div className="border-b border-border px-4 py-3">
|
|
305
|
+
<p className="text-sm font-medium text-popover-foreground">{userName}</p>
|
|
306
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{userEmail}</p>
|
|
312
307
|
</div>
|
|
313
308
|
<div className="py-1">
|
|
314
309
|
<button
|
|
315
310
|
onClick={handleSignOut}
|
|
316
|
-
className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm text-
|
|
311
|
+
className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm text-popover-foreground transition-colors duration-200 hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
|
|
317
312
|
>
|
|
318
313
|
<LogOut className="h-4 w-4" />
|
|
319
314
|
<span>Déconnexion</span>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
2
|
+
|
|
1
3
|
interface InvitationEmailProps {
|
|
2
4
|
name: string;
|
|
3
5
|
invitationUrl: string;
|
|
@@ -8,7 +10,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
|
|
|
8
10
|
return (
|
|
9
11
|
<div
|
|
10
12
|
style={{
|
|
11
|
-
fontFamily: '
|
|
13
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
12
14
|
padding: '20px',
|
|
13
15
|
maxWidth: '600px',
|
|
14
16
|
margin: '0 auto',
|
|
@@ -71,7 +73,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
|
|
|
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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
|
+
import { Skeleton } from '@/components/skeleton';
|
|
5
|
+
|
|
6
|
+
export type { DefaultTemplateRef } from '@/components/editor';
|
|
7
|
+
|
|
8
|
+
export const LazyEditor = dynamic(() => import('@/components/editor').then((m) => m.Editor), {
|
|
9
|
+
ssr: false,
|
|
10
|
+
loading: () => <Skeleton className="h-64 rounded-lg" />,
|
|
11
|
+
});
|