data-primals-engine 1.0.10 → 1.1.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/client/README.md +8 -0
- package/client/index.html +31 -0
- package/client/index.js +25 -0
- package/client/package-lock.json +11664 -0
- package/client/package.json +70 -0
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
- package/client/public/doc/API.postman_collection.json +214 -0
- package/client/public/github.svg +1 -0
- package/client/public/profilCompany.jpg +0 -0
- package/client/public/profilDeveloper.jpg +0 -0
- package/client/public/profilEngineer.jpg +0 -0
- package/client/public/profilPersonal.jpg +0 -0
- package/client/public/profils .psd +0 -0
- package/client/public/react.svg +9 -0
- package/client/src/APIInfo.jsx +328 -0
- package/client/src/AddWidgetTypeModal.jsx +44 -0
- package/client/src/AddWidgetTypeModal.scss +87 -0
- package/client/src/App.css +42 -0
- package/client/src/App.jsx +586 -0
- package/client/src/App.scss +1466 -0
- package/client/src/AssistantChat.jsx +280 -0
- package/client/src/AssistantChat.scss +229 -0
- package/client/src/Button.jsx +12 -0
- package/client/src/Button.scss +197 -0
- package/client/src/CalculationBuilder.jsx +338 -0
- package/client/src/CalculationStepRow.jsx +199 -0
- package/client/src/CalendarConfigModal.jsx +49 -0
- package/client/src/ChartConfigModal.jsx +402 -0
- package/client/src/ConditionBuilder.jsx +622 -0
- package/client/src/ConditionBuilder.scss +672 -0
- package/client/src/ConditionBuilder2.jsx +757 -0
- package/client/src/ContentView.jsx +274 -0
- package/client/src/CronBuilder.jsx +126 -0
- package/client/src/CronBuilder.scss +128 -0
- package/client/src/CronPartBuilder.jsx +150 -0
- package/client/src/Dashboard.jsx +210 -0
- package/client/src/Dashboard.scss +445 -0
- package/client/src/DashboardChart.jsx +362 -0
- package/client/src/DashboardFlexViewItem.jsx +129 -0
- package/client/src/DashboardView.jsx +554 -0
- package/client/src/DataChart.jsx +349 -0
- package/client/src/DataEditor.jsx +556 -0
- package/client/src/DataLayout.jsx +709 -0
- package/client/src/DataTable.jsx +996 -0
- package/client/src/DataTable.scss +73 -0
- package/client/src/Dialog.jsx +90 -0
- package/client/src/Dialog.scss +117 -0
- package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
- package/client/src/DocumentationPageLayout.jsx +93 -0
- package/client/src/DocumentationPageLayout.scss +262 -0
- package/client/src/Draggable.jsx +222 -0
- package/client/src/Draggable.scss +49 -0
- package/client/src/ExportDialog.jsx +172 -0
- package/client/src/Field.jsx +1558 -0
- package/client/src/FlexBuilder.jsx +269 -0
- package/client/src/FlexBuilder.scss +226 -0
- package/client/src/FlexBuilderControls.jsx +170 -0
- package/client/src/FlexBuilderModal.jsx +75 -0
- package/client/src/FlexBuilderModal.scss +109 -0
- package/client/src/FlexBuilderPreview.jsx +48 -0
- package/client/src/FlexBuilderUtils.js +84 -0
- package/client/src/FlexDataRenderer.jsx +170 -0
- package/client/src/FlexDataRenderer.scss +80 -0
- package/client/src/FlexNode.jsx +290 -0
- package/client/src/FlexTreeUtils.js +129 -0
- package/client/src/HiddenableCell.jsx +46 -0
- package/client/src/HiddenableCell.scss +36 -0
- package/client/src/KPIDialog.jsx +39 -0
- package/client/src/KPIWidget.jsx +139 -0
- package/client/src/KanbanCard.jsx +92 -0
- package/client/src/KanbanColumn.jsx +29 -0
- package/client/src/KanbanConfigModal.jsx +115 -0
- package/client/src/KanbanView.jsx +113 -0
- package/client/src/KanbanView.scss +107 -0
- package/client/src/MessageRotator.jsx +77 -0
- package/client/src/MessageRotator.scss +14 -0
- package/client/src/ModelCreator.jsx +640 -0
- package/client/src/ModelCreator.scss +154 -0
- package/client/src/ModelCreatorField.jsx +810 -0
- package/client/src/ModelImporter.jsx +120 -0
- package/client/src/ModelList.jsx +205 -0
- package/client/src/Notification.jsx +137 -0
- package/client/src/Notification.scss +126 -0
- package/client/src/NotificationProvider.jsx +73 -0
- package/client/src/PackGallery.jsx +211 -0
- package/client/src/PackGallery.scss +156 -0
- package/client/src/Pagination.jsx +141 -0
- package/client/src/RTE.jsx +415 -0
- package/client/src/RTETrans.jsx +103 -0
- package/client/src/RelationField.jsx +247 -0
- package/client/src/RelationValue.jsx +230 -0
- package/client/src/RestoreConfirmationModal.jsx +183 -0
- package/client/src/RestoreDialog.jsx +134 -0
- package/client/src/RestoreDialog.scss +67 -0
- package/client/src/RichTextEditorModal.jsx +34 -0
- package/client/src/RichTextEditorModal.scss +54 -0
- package/client/src/TourSpotlight.jsx +234 -0
- package/client/src/TourSpotlight.scss +100 -0
- package/client/src/TutorialsMenu.jsx +83 -0
- package/client/src/TutorialsMenu.scss +217 -0
- package/client/src/TutorialsRewardModal.jsx +65 -0
- package/client/src/Utils.jsx +78 -0
- package/client/src/ViewSwitcher.jsx +54 -0
- package/client/src/ViewSwitcher.scss +74 -0
- package/client/src/Webpage.jsx +69 -0
- package/client/src/_variables.scss +21 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/constants.js +68 -0
- package/client/src/contexts/AuthContext.jsx +28 -0
- package/client/src/contexts/ModelContext.jsx +328 -0
- package/client/src/contexts/UIContext.jsx +52 -0
- package/client/src/core/data.js +35 -0
- package/client/src/entry-client.jsx +15 -0
- package/client/src/entry-server.jsx +13 -0
- package/client/src/filter.js +222 -0
- package/client/src/hooks/data.js +101 -0
- package/client/src/hooks/useDebounce.js +20 -0
- package/client/src/hooks/useDragAndDrop.js +59 -0
- package/client/src/hooks/useLocalStorage.js +100 -0
- package/client/src/hooks/useMount.js +42 -0
- package/client/src/hooks/useTutorials.jsx +227 -0
- package/client/src/hooks/useWindowSize.js +24 -0
- package/client/src/i18n.js +47 -0
- package/client/src/index.css +90 -0
- package/client/src/index.js +10 -0
- package/client/src/main.jsx +11 -0
- package/client/src/translations.js +15940 -0
- package/client/src/tutorials.js +112 -0
- package/client/vite.config.js +30 -0
- package/eslint.config.js +50 -0
- package/package.json +48 -27
- package/server.js +9 -0
- package/src/constants.js +12 -23
- package/src/core.js +9 -8
- package/src/data.js +81 -80
- package/src/defaultModels.js +114 -115
- package/src/email.js +8 -9
- package/src/engine.js +5 -5
- package/src/events.js +1 -2
- package/src/i18n.js +5390 -211
- package/src/middlewares/middleware-mongodb.js +94 -93
- package/src/migrate.js +24 -24
- package/src/modules/assistant.js +55 -55
- package/src/modules/bucket.js +10 -9
- package/src/modules/data.js +387 -357
- package/src/modules/file.js +3 -6
- package/src/modules/mongodb.js +6 -7
- package/src/modules/user.js +24 -47
- package/src/modules/workflow.js +68 -67
- package/src/openai.jobs.js +2 -2
- package/src/packs.js +30 -31
- package/src/providers.js +101 -3
- package/src/setenv.js +1 -1
- package/src/user.js +0 -17
- package/src/workers/crypto-worker.js +21 -20
- package/src/workers/import-export-worker.js +18 -18
- package/test/data.backup.integration.test.js +7 -12
- package/test/data.integration.test.js +27 -25
- package/test/globalSetup.js +2 -2
- package/test/import_export.integration.test.js +6 -6
- package/test/model.integration.test.js +21 -22
- package/test/workflow.integration.test.js +2 -5
- package/test/workflow.robustness.test.js +6 -13
- package/vitest.config.js +2 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/RestoreDialog.jsx
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { useSearchParams, useNavigate } from 'react-router-dom';
|
|
5
|
+
import { useTranslation, Trans } from 'react-i18next';
|
|
6
|
+
import Button from './Button'; // Assure-toi que le chemin est correct
|
|
7
|
+
import { FaCheckCircle, FaExclamationTriangle, FaSpinner } from 'react-icons/fa';
|
|
8
|
+
import './RestoreDialog.scss'; // Crée ce fichier pour le style du spinner
|
|
9
|
+
|
|
10
|
+
function RestoreDialog() {
|
|
11
|
+
const [searchParams] = useSearchParams();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [success, setSuccess] = useState(false);
|
|
18
|
+
const [username, setUsername] = useState(null); // Pour la redirection en cas de succès
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const token = searchParams.get('token');
|
|
22
|
+
const user = searchParams.get('username');
|
|
23
|
+
setUsername(user); // Stocke le username pour la redirection
|
|
24
|
+
|
|
25
|
+
if (!token || !user) {
|
|
26
|
+
setError(t('backup.restore.error.missingParams', "Paramètres 'token' ou 'username' manquants dans l'URL."));
|
|
27
|
+
setLoading(false);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const apiUrl = `/api/backup/restore?token=${encodeURIComponent(token)}&username=${encodeURIComponent(user)}`;
|
|
32
|
+
// Note: On utilise un chemin relatif /api/... car le serveur API est probablement servi
|
|
33
|
+
// sur le même domaine/port ou via un proxy configuré dans Vite (vite.config.js)
|
|
34
|
+
// Si l'API est sur un domaine différent (comme https://data.primals.net), utilise l'URL complète.
|
|
35
|
+
// const apiUrl = `https://data.primals.net/api/backup/restore?token=${encodeURIComponent(token)}&username=${encodeURIComponent(user)}`;
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
const restoreBackup = async () => {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
setSuccess(false);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(apiUrl, {
|
|
45
|
+
method: 'GET', // Ou 'POST' si l'API l'exige, mais GET semble plus probable ici
|
|
46
|
+
headers: {
|
|
47
|
+
'Accept': 'application/json',
|
|
48
|
+
// Ajoute d'autres headers si nécessaire (ex: Authorization si requis)
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
let errorMsg = t('backup.restore.error.generic', "Une erreur est survenue lors de la restauration.");
|
|
54
|
+
try {
|
|
55
|
+
// Essaye de lire un message d'erreur plus spécifique de l'API
|
|
56
|
+
const errorData = await response.json();
|
|
57
|
+
if (errorData && errorData.message) {
|
|
58
|
+
errorMsg = errorData.message;
|
|
59
|
+
} else {
|
|
60
|
+
errorMsg = `${errorMsg} (Status: ${response.status})`;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
errorMsg = `${errorMsg} (Status: ${response.status})`;
|
|
64
|
+
console.error("Could not parse error response:", e);
|
|
65
|
+
}
|
|
66
|
+
throw new Error(errorMsg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Si l'API renvoie des données JSON en cas de succès, tu peux les traiter ici
|
|
70
|
+
// const result = await response.json();
|
|
71
|
+
// console.log("Restore successful:", result);
|
|
72
|
+
|
|
73
|
+
setSuccess(true);
|
|
74
|
+
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Restore error:", err);
|
|
77
|
+
setError(err.message || t('backup.restore.error.network', "Erreur réseau ou serveur inaccessible."));
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
restoreBackup();
|
|
84
|
+
|
|
85
|
+
}, [searchParams, t]); // Dépendance à searchParams et t
|
|
86
|
+
|
|
87
|
+
const handleGoToData = () => {
|
|
88
|
+
if (username) {
|
|
89
|
+
navigate(`/user/${username}/`);
|
|
90
|
+
} else {
|
|
91
|
+
navigate('/'); // Fallback si username n'est pas défini
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleGoHome = () => {
|
|
96
|
+
navigate('/');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="restore-dialog-container">
|
|
102
|
+
<h2><Trans i18nKey="backup.restore.dialogTitle">Restauration de la sauvegarde</Trans></h2>
|
|
103
|
+
|
|
104
|
+
{loading && (
|
|
105
|
+
<div className="restore-status loading">
|
|
106
|
+
<FaSpinner className="spinner" />
|
|
107
|
+
<p><Trans i18nKey="backup.restore.restoring">Restauration en cours...</Trans></p>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{error && (
|
|
112
|
+
<div className="restore-status error msg msg-error">
|
|
113
|
+
<FaExclamationTriangle />
|
|
114
|
+
<p><Trans i18nKey="backup.restore.error.prefix">Erreur :</Trans> {error}</p>
|
|
115
|
+
<Button onClick={handleGoHome} className="btn-primary">
|
|
116
|
+
<Trans i18nKey="backup.restore.goHome">Retour à l'accueil</Trans>
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{success && (
|
|
122
|
+
<div className="restore-status success msg msg-success">
|
|
123
|
+
<FaCheckCircle />
|
|
124
|
+
<p><Trans i18nKey="backup.restore.success">La restauration de votre sauvegarde a été effectuée avec succès.</Trans></p>
|
|
125
|
+
<Button onClick={handleGoToData} className="btn-primary">
|
|
126
|
+
<Trans i18nKey="backup.restore.goToData">Accéder à mes données</Trans>
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default RestoreDialog;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
.restore-dialog-container {
|
|
3
|
+
padding: 2rem;
|
|
4
|
+
max-width: 600px;
|
|
5
|
+
margin: 4rem auto;
|
|
6
|
+
background-color: var(--bg-color-lighter, #f9f9f9);
|
|
7
|
+
border: 1px solid var(--border-color, #ddd);
|
|
8
|
+
border-radius: 8px;
|
|
9
|
+
text-align: center;
|
|
10
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
11
|
+
|
|
12
|
+
h2 {
|
|
13
|
+
margin-bottom: 1.5rem;
|
|
14
|
+
color: var(--text-color-strong, #333);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.restore-status {
|
|
18
|
+
padding: 1.5rem;
|
|
19
|
+
border-radius: 5px;
|
|
20
|
+
margin-top: 1rem;
|
|
21
|
+
|
|
22
|
+
svg {
|
|
23
|
+
font-size: 2rem;
|
|
24
|
+
margin-bottom: 0.5rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&.loading {
|
|
28
|
+
color: var(--text-color, #555);
|
|
29
|
+
.spinner {
|
|
30
|
+
animation: spin 1.5s linear infinite;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&.error {
|
|
35
|
+
// Utilise les styles de .msg-error si définis globalement
|
|
36
|
+
// Sinon, ajoute des styles spécifiques ici
|
|
37
|
+
// background-color: #ffebee;
|
|
38
|
+
// border: 1px solid #e57373;
|
|
39
|
+
// color: #c62828;
|
|
40
|
+
svg { color: var(--color-error, #d32f2f); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.success {
|
|
44
|
+
// Utilise les styles de .msg-success si définis globalement
|
|
45
|
+
// Sinon, ajoute des styles spécifiques ici
|
|
46
|
+
// background-color: #e8f5e9;
|
|
47
|
+
// border: 1px solid #81c784;
|
|
48
|
+
// color: #2e7d32;
|
|
49
|
+
svg { color: var(--color-success, #388e3c); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
p {
|
|
53
|
+
margin-bottom: 1rem;
|
|
54
|
+
font-size: 1.1rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
button {
|
|
58
|
+
margin-top: 1rem;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Animation du spinner
|
|
64
|
+
@keyframes spin {
|
|
65
|
+
0% { transform: rotate(0deg); }
|
|
66
|
+
100% { transform: rotate(360deg); }
|
|
67
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// client/src/RichTextEditorModal.jsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import {RTE} from "./RTE.jsx";
|
|
4
|
+
import {Dialog} from "./Dialog.jsx"; // Styles pour la modale plein écran
|
|
5
|
+
|
|
6
|
+
import './RichTextEditorModal.scss';
|
|
7
|
+
const RichTextEditorModal = ({ initialContent, title, onSave, onClose }) => {
|
|
8
|
+
const [content, setContent] = useState(initialContent || '');
|
|
9
|
+
|
|
10
|
+
const handleSave = () => {
|
|
11
|
+
onSave?.(content.value);
|
|
12
|
+
onClose?.();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Dialog isModal={true} onClose={onClose}
|
|
17
|
+
title={title || 'Éditeur de Texte Riche'} isClosable={true}>
|
|
18
|
+
<div className="flex actions">
|
|
19
|
+
<button onClick={handleSave} className="btn btn-primary">Sauvegarder & Fermer</button>
|
|
20
|
+
<button onClick={onClose} className="btn">Fermer</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="editor-container">
|
|
23
|
+
<RTE
|
|
24
|
+
onChange={(e) => setContent(e)}
|
|
25
|
+
value={content}
|
|
26
|
+
name={"rte"}
|
|
27
|
+
field={{name: 'rte'}}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</Dialog>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default RichTextEditorModal;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// client/src/RichTextEditorModal.scss
|
|
2
|
+
.richtext-editor-modal {
|
|
3
|
+
position: fixed;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
width: 100vw;
|
|
7
|
+
height: 80vh;
|
|
8
|
+
background-color: #fff;
|
|
9
|
+
z-index: 2000; // Doit être au-dessus de tout le reste
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
|
|
13
|
+
.editor-header {
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
align-items: center;
|
|
17
|
+
padding: 10px 20px;
|
|
18
|
+
border-bottom: 1px solid #e0e0e0;
|
|
19
|
+
background-color: #f8f9fa;
|
|
20
|
+
|
|
21
|
+
h2 {
|
|
22
|
+
margin: 0;
|
|
23
|
+
font-size: 1.2rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.editor-actions {
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: 10px;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.editor-container {
|
|
33
|
+
flex-grow: 1;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
overflow: hidden; // Empêche le double scroll
|
|
37
|
+
|
|
38
|
+
.ql-toolbar {
|
|
39
|
+
border-left: none;
|
|
40
|
+
border-right: none;
|
|
41
|
+
border-top: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ql-container {
|
|
45
|
+
flex-grow: 1;
|
|
46
|
+
border: none;
|
|
47
|
+
font-size: 16px;
|
|
48
|
+
overflow-y: auto; // Permet le scroll dans l'éditeur
|
|
49
|
+
}
|
|
50
|
+
.ql-editor {
|
|
51
|
+
padding: 2rem; // Un peu d'air
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/TourSpotlight.jsx
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import './TourSpotlight.scss';
|
|
5
|
+
import { useUI } from "./contexts/UIContext.jsx";
|
|
6
|
+
import useLocalStorage from "./hooks/useLocalStorage.js";
|
|
7
|
+
|
|
8
|
+
function debounce(func, wait) {
|
|
9
|
+
let timeout;
|
|
10
|
+
return function executedFunction(...args) {
|
|
11
|
+
const later = () => {
|
|
12
|
+
clearTimeout(timeout);
|
|
13
|
+
func(...args);
|
|
14
|
+
};
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
timeout = setTimeout(later, wait);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TourSpotlight({ name, steps = [], isOpen, onComplete, onClose, initialStepIndex = 0 }) {
|
|
21
|
+
const { tourStepIndex, setTourStepIndex, launchedTours, setLaunchedTours, currentTour, setCurrentTour, setCurrentTourSteps } = useUI();
|
|
22
|
+
const [targetRect, setTargetRect] = useState(null);
|
|
23
|
+
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
|
|
24
|
+
const tooltipRef = useRef(null);
|
|
25
|
+
const highlightRef = useRef(null);
|
|
26
|
+
|
|
27
|
+
const currentStep = steps[tourStepIndex];
|
|
28
|
+
|
|
29
|
+
// Réinitialiser l'index quand le tour s'ouvre
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
setTourStepIndex(initialStepIndex);
|
|
33
|
+
const rafId = requestAnimationFrame(() => {
|
|
34
|
+
calculateRectAndScroll();
|
|
35
|
+
});
|
|
36
|
+
return () => cancelAnimationFrame(rafId);
|
|
37
|
+
} else {
|
|
38
|
+
setTargetRect(null);
|
|
39
|
+
}
|
|
40
|
+
}, [isOpen, initialStepIndex]);
|
|
41
|
+
|
|
42
|
+
const calculateRectAndScroll = useCallback(() => {
|
|
43
|
+
if (!isOpen || !currentStep?.selector) {
|
|
44
|
+
setTargetRect(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const element = document.querySelector(currentStep.selector);
|
|
49
|
+
if (element) {
|
|
50
|
+
const rect = element.getBoundingClientRect();
|
|
51
|
+
if (!targetRect || rect.top !== targetRect.top || rect.left !== targetRect.left || rect.width !== targetRect.width || rect.height !== targetRect.height) {
|
|
52
|
+
setTargetRect(rect);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
console.warn(`[TourSpotlight] Élément cible non trouvé : ${currentStep.selector}`);
|
|
56
|
+
setTargetRect(null);
|
|
57
|
+
}
|
|
58
|
+
}, [isOpen, currentStep?.selector, targetRect]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!isOpen || !name) return;
|
|
62
|
+
|
|
63
|
+
if (!currentStep?.selector) {
|
|
64
|
+
console.warn(`[TourSpotlight] Élément cible non défini pour le tour ${name}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const element = document.querySelector(currentStep.selector);
|
|
69
|
+
if (element && currentTour !== name) {
|
|
70
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
71
|
+
}
|
|
72
|
+
const rafId = requestAnimationFrame(calculateRectAndScroll);
|
|
73
|
+
const debouncedCalculate = debounce(calculateRectAndScroll, 100);
|
|
74
|
+
window.addEventListener('resize', debouncedCalculate);
|
|
75
|
+
window.addEventListener('scroll', debouncedCalculate, true);
|
|
76
|
+
setCurrentTour(name);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
cancelAnimationFrame(rafId);
|
|
80
|
+
window.removeEventListener('resize', debouncedCalculate);
|
|
81
|
+
window.removeEventListener('scroll', debouncedCalculate, true);
|
|
82
|
+
};
|
|
83
|
+
}, [isOpen, tourStepIndex, currentStep?.selector, calculateRectAndScroll, name]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!targetRect || !tooltipRef.current) {
|
|
87
|
+
setTooltipPosition({ top: -9999, left: -9999 });
|
|
88
|
+
return;
|
|
89
|
+
};
|
|
90
|
+
const tooltipEl = tooltipRef.current;
|
|
91
|
+
const tooltipRect = tooltipEl.getBoundingClientRect();
|
|
92
|
+
const spacing = 15;
|
|
93
|
+
let pos = { top: 0, left: 0 };
|
|
94
|
+
const positionPreference = currentStep.position || 'bottom';
|
|
95
|
+
switch (positionPreference) {
|
|
96
|
+
case 'top':
|
|
97
|
+
pos = { top: targetRect.top - tooltipRect.height - spacing, left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 };
|
|
98
|
+
break;
|
|
99
|
+
case 'left':
|
|
100
|
+
pos = { top: targetRect.top + targetRect.height / 2 - tooltipRect.height / 2, left: targetRect.left - tooltipRect.width - spacing };
|
|
101
|
+
break;
|
|
102
|
+
case 'right':
|
|
103
|
+
pos = { top: targetRect.top + targetRect.height / 2 - tooltipRect.height / 2, left: targetRect.right + spacing };
|
|
104
|
+
break;
|
|
105
|
+
case 'bottom':
|
|
106
|
+
default:
|
|
107
|
+
pos = { top: targetRect.bottom + spacing, left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 };
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
pos.top = Math.max(spacing, pos.top);
|
|
111
|
+
pos.left = Math.max(spacing, pos.left);
|
|
112
|
+
pos.left = Math.min(window.innerWidth - tooltipRect.width - spacing, pos.left);
|
|
113
|
+
pos.top = Math.min(window.innerHeight - tooltipRect.height - spacing, pos.top);
|
|
114
|
+
setTooltipPosition(pos);
|
|
115
|
+
}, [targetRect, currentStep?.position, tourStepIndex]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (isOpen) {
|
|
119
|
+
if (tourStepIndex === steps.length-1 && steps.length !== 1) {
|
|
120
|
+
setLaunchedTours(tours => {
|
|
121
|
+
if(tours.includes(name))
|
|
122
|
+
return tours;
|
|
123
|
+
return [...tours, name];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}, [isOpen, launchedTours, name, setCurrentTour, tourStepIndex, steps]);
|
|
128
|
+
|
|
129
|
+
const handleComplete = useCallback(() => {
|
|
130
|
+
setCurrentTour(null);
|
|
131
|
+
setLaunchedTours(tours => {
|
|
132
|
+
if(tours.includes(name))
|
|
133
|
+
return tours;
|
|
134
|
+
return [...tours, name];
|
|
135
|
+
});
|
|
136
|
+
if (onComplete) {
|
|
137
|
+
onComplete(name);
|
|
138
|
+
} else{
|
|
139
|
+
onClose?.(name);
|
|
140
|
+
}
|
|
141
|
+
}, [name, onComplete, onClose, setCurrentTour]);
|
|
142
|
+
|
|
143
|
+
const handleNext = () => {
|
|
144
|
+
if (tourStepIndex < steps.length - 1) {
|
|
145
|
+
setTourStepIndex(prev => prev + 1);
|
|
146
|
+
} else {
|
|
147
|
+
handleComplete();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handlePrev = () => {
|
|
152
|
+
if (tourStepIndex > 0) {
|
|
153
|
+
setTourStepIndex(prev => prev - 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleClose = useCallback(() => {
|
|
158
|
+
setCurrentTour(null);
|
|
159
|
+
setLaunchedTours(tours => {
|
|
160
|
+
if( tours.includes(name) )
|
|
161
|
+
return [...tours];
|
|
162
|
+
return [...tours, name];
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (onClose) {
|
|
166
|
+
onClose(name);
|
|
167
|
+
}
|
|
168
|
+
}, [onClose, name, setCurrentTour, launchedTours]);
|
|
169
|
+
|
|
170
|
+
if (!isOpen || !currentTour || !currentStep || (launchedTours && launchedTours.includes(name))) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const highlightStyle = targetRect ? {
|
|
175
|
+
position: 'absolute',
|
|
176
|
+
top: `${targetRect.top}px`,
|
|
177
|
+
left: `${targetRect.left}px`,
|
|
178
|
+
width: `${targetRect.width}px`,
|
|
179
|
+
height: `${targetRect.height}px`,
|
|
180
|
+
borderRadius: '4px',
|
|
181
|
+
boxShadow: `0 0 0 9999px rgba(0, 0, 0, 0.75)`,
|
|
182
|
+
pointerEvents: 'none',
|
|
183
|
+
transition: 'top 0.3s ease-in-out, left 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out',
|
|
184
|
+
zIndex: 10000,
|
|
185
|
+
} : { display: 'none' };
|
|
186
|
+
|
|
187
|
+
const tooltipStyle = {
|
|
188
|
+
position: 'fixed',
|
|
189
|
+
top: `${tooltipPosition.top}px`,
|
|
190
|
+
left: `${tooltipPosition.left}px`,
|
|
191
|
+
zIndex: 10001,
|
|
192
|
+
transition: 'top 0.3s ease-in-out, left 0.3s ease-in-out',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div
|
|
197
|
+
className="tour-spotlight-container"
|
|
198
|
+
onClick={handleClose}
|
|
199
|
+
style={{
|
|
200
|
+
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999, pointerEvents: 'auto',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div ref={highlightRef} style={highlightStyle}></div>
|
|
204
|
+
{targetRect && (
|
|
205
|
+
<div
|
|
206
|
+
ref={tooltipRef}
|
|
207
|
+
className="tour-tooltip"
|
|
208
|
+
style={tooltipStyle}
|
|
209
|
+
onClick={(e) => e.stopPropagation()}
|
|
210
|
+
>
|
|
211
|
+
<div className="tour-tooltip-content">
|
|
212
|
+
{typeof currentStep.content === 'string' ? <p>{currentStep.content}</p> : currentStep.content}
|
|
213
|
+
</div>
|
|
214
|
+
<div className="tour-tooltip-footer">
|
|
215
|
+
<span className="tour-step-indicator">
|
|
216
|
+
Étape {tourStepIndex + 1} / {steps.length}
|
|
217
|
+
</span>
|
|
218
|
+
<div className="tour-buttons">
|
|
219
|
+
{tourStepIndex > 0 && (
|
|
220
|
+
<button onClick={handlePrev} className="tour-button tour-button-prev">Précédent</button>
|
|
221
|
+
)}
|
|
222
|
+
<button onClick={handleNext} className="tour-button tour-button-next">
|
|
223
|
+
{tourStepIndex < steps.length - 1 ? 'Suivant' : 'Terminer'}
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
<button onClick={handleClose} className="tour-button-close" aria-label="Fermer le tour">×</button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default TourSpotlight;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Variables (optionnel)
|
|
2
|
+
$tooltip-bg: white;
|
|
3
|
+
$tooltip-color: #333;
|
|
4
|
+
$tooltip-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
5
|
+
$button-bg: #3498db;
|
|
6
|
+
$button-color: white;
|
|
7
|
+
$overlay-bg: rgba(0, 0, 0, 0.75); // Couleur de l'overlay
|
|
8
|
+
|
|
9
|
+
.tour-spotlight-overlay {
|
|
10
|
+
// Le style de base est défini en inline pour la position/taille
|
|
11
|
+
// On peut ajouter des transitions ici si souhaité
|
|
12
|
+
// background-color: $overlay-bg; // Alternative si on n'utilise pas box-shadow pour l'overlay principal
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.tour-tooltip {
|
|
16
|
+
background-color: $tooltip-bg;
|
|
17
|
+
color: $tooltip-color;
|
|
18
|
+
padding: 15px 20px;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
box-shadow: $tooltip-shadow;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
transition: top 0.2s ease-out, left 0.2s ease-out; // Transition douce
|
|
23
|
+
width: 100%;
|
|
24
|
+
max-width: 440px;
|
|
25
|
+
.tour-tooltip-content {
|
|
26
|
+
margin-bottom: 15px;
|
|
27
|
+
line-height: 1.5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tour-tooltip-footer {
|
|
31
|
+
display: flex;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
align-items: center;
|
|
34
|
+
border-top: 1px solid #eee;
|
|
35
|
+
padding-top: 10px;
|
|
36
|
+
margin-top: 10px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.tour-step-indicator {
|
|
40
|
+
font-size: 0.85em;
|
|
41
|
+
color: #777;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.tour-buttons {
|
|
45
|
+
display: flex;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.tour-button {
|
|
50
|
+
background-color: $button-bg;
|
|
51
|
+
color: $button-color;
|
|
52
|
+
border: none;
|
|
53
|
+
padding: 8px 15px;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
font-size: 0.9em;
|
|
57
|
+
transition: background-color 0.2s ease;
|
|
58
|
+
|
|
59
|
+
&:hover {
|
|
60
|
+
background-color: darken($button-bg, 10%);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.tour-button-prev {
|
|
64
|
+
background-color: #777;
|
|
65
|
+
&:hover {
|
|
66
|
+
background-color: darken(#777, 10%);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.tour-button-close {
|
|
71
|
+
position: absolute;
|
|
72
|
+
top: 5px;
|
|
73
|
+
right: 8px;
|
|
74
|
+
background: none;
|
|
75
|
+
border: none;
|
|
76
|
+
font-size: 1.5em;
|
|
77
|
+
color: #aaa;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
padding: 5px;
|
|
80
|
+
line-height: 1;
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
color: #333;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Optionnel: Ajouter une petite flèche à l'info-bulle (plus complexe)
|
|
89
|
+
// .tour-tooltip::after {
|
|
90
|
+
// content: '';
|
|
91
|
+
// position: absolute;
|
|
92
|
+
// border-width: 8px;
|
|
93
|
+
// border-style: solid;
|
|
94
|
+
// /* Logique pour positionner la flèche en fonction de props.position */
|
|
95
|
+
// /* Exemple pour 'bottom' */
|
|
96
|
+
// bottom: 100%;
|
|
97
|
+
// left: 50%;
|
|
98
|
+
// transform: translateX(-50%);
|
|
99
|
+
// border-color: transparent transparent $tooltip-bg transparent;
|
|
100
|
+
// }
|