allaw-ui 3.0.1 → 3.0.2
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/dist/components/molecules/fileUploader/FileUploader.d.ts +4 -1
- package/dist/components/molecules/fileUploader/FileUploader.js +45 -14
- package/dist/components/molecules/fileUploader/FileUploader.module.css +11 -0
- package/dist/components/molecules/fileUploader/FileUploader.stories.d.ts +33 -15
- package/dist/components/molecules/fileUploader/FileUploader.stories.js +57 -9
- package/dist/components/molecules/fileUploader/ImageCropperModal.d.ts +15 -0
- package/dist/components/molecules/fileUploader/ImageCropperModal.js +211 -0
- package/dist/components/molecules/fileUploader/ImageCropperModal.module.css +232 -0
- package/package.json +1 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { CropMetadata } from "./ImageCropperModal";
|
|
2
3
|
export interface FileUploaderProps {
|
|
3
4
|
acceptedExtensions: string[];
|
|
4
5
|
maxFileSizeMB: number;
|
|
5
6
|
enableDragAndDrop?: boolean;
|
|
7
|
+
enableCropping?: boolean;
|
|
8
|
+
cropShape?: "circle" | "square";
|
|
6
9
|
iconUrl?: string;
|
|
7
10
|
descriptionParts?: {
|
|
8
11
|
beforeLink: string;
|
|
@@ -10,7 +13,7 @@ export interface FileUploaderProps {
|
|
|
10
13
|
linkUrl: string;
|
|
11
14
|
afterLink: string;
|
|
12
15
|
};
|
|
13
|
-
onFileRead?: (file: File, fileContent: ArrayBuffer) => void;
|
|
16
|
+
onFileRead?: (file: File, fileContent: ArrayBuffer, cropMetadata?: CropMetadata) => void;
|
|
14
17
|
onFileRemove?: () => void;
|
|
15
18
|
uploadProgress?: number;
|
|
16
19
|
isLoading?: boolean;
|
|
@@ -35,11 +35,15 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
import React, { useState, useRef } from "react";
|
|
38
|
+
import Image from "next/image";
|
|
38
39
|
import styles from "./FileUploader.module.css";
|
|
40
|
+
import ImageCropperModal from "./ImageCropperModal";
|
|
39
41
|
var FileUploader = function (_a) {
|
|
40
|
-
var acceptedExtensions = _a.acceptedExtensions, maxFileSizeMB = _a.maxFileSizeMB, _b = _a.enableDragAndDrop, enableDragAndDrop = _b === void 0 ? true : _b, iconUrl = _a.iconUrl, descriptionParts = _a.descriptionParts, onFileRead = _a.onFileRead, onFileRemove = _a.onFileRemove,
|
|
41
|
-
var
|
|
42
|
-
var
|
|
42
|
+
var acceptedExtensions = _a.acceptedExtensions, maxFileSizeMB = _a.maxFileSizeMB, _b = _a.enableDragAndDrop, enableDragAndDrop = _b === void 0 ? true : _b, _c = _a.enableCropping, enableCropping = _c === void 0 ? false : _c, _d = _a.cropShape, cropShape = _d === void 0 ? "square" : _d, iconUrl = _a.iconUrl, descriptionParts = _a.descriptionParts, onFileRead = _a.onFileRead, onFileRemove = _a.onFileRemove, _e = _a.uploadProgress, uploadProgress = _e === void 0 ? 0 : _e, _f = _a.isLoading, isLoading = _f === void 0 ? false : _f, _g = _a.errorMessage, errorMessage = _g === void 0 ? null : _g, _h = _a.buttonLabel, buttonLabel = _h === void 0 ? "Choisir un fichier" : _h, _j = _a.acceptedLabel, acceptedLabel = _j === void 0 ? "Format accepté :" : _j, _k = _a.maxSizeLabel, maxSizeLabel = _k === void 0 ? "Taille maximale :" : _k, fileName = _a.fileName, fileSize = _a.fileSize;
|
|
43
|
+
var _l = useState(null), selectedFile = _l[0], setSelectedFile = _l[1];
|
|
44
|
+
var _m = useState(null), fileContent = _m[0], setFileContent = _m[1];
|
|
45
|
+
var _o = useState(false), isHovering = _o[0], setIsHovering = _o[1];
|
|
46
|
+
var _p = useState(false), showCropper = _p[0], setShowCropper = _p[1];
|
|
43
47
|
var fileInputRef = useRef(null);
|
|
44
48
|
var resetFileInput = function () {
|
|
45
49
|
if (fileInputRef.current) {
|
|
@@ -55,6 +59,12 @@ var FileUploader = function (_a) {
|
|
|
55
59
|
var isValidSize = file.size <= maxFileSizeMB * 1024 * 1024;
|
|
56
60
|
return isValidExtension && isValidSize;
|
|
57
61
|
};
|
|
62
|
+
var isImageFile = function (file) {
|
|
63
|
+
var _a;
|
|
64
|
+
var fileExtension = ".".concat((_a = file.name.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase());
|
|
65
|
+
var imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
|
|
66
|
+
return imageExtensions.includes(fileExtension);
|
|
67
|
+
};
|
|
58
68
|
var handleFileSelect = function (event) { return __awaiter(void 0, void 0, void 0, function () {
|
|
59
69
|
var file;
|
|
60
70
|
return __generator(this, function (_a) {
|
|
@@ -85,7 +95,7 @@ var FileUploader = function (_a) {
|
|
|
85
95
|
});
|
|
86
96
|
}); };
|
|
87
97
|
var processFile = function (file) { return __awaiter(void 0, void 0, void 0, function () {
|
|
88
|
-
var
|
|
98
|
+
var content, error_1;
|
|
89
99
|
return __generator(this, function (_a) {
|
|
90
100
|
switch (_a.label) {
|
|
91
101
|
case 0:
|
|
@@ -96,11 +106,20 @@ var FileUploader = function (_a) {
|
|
|
96
106
|
_a.label = 1;
|
|
97
107
|
case 1:
|
|
98
108
|
_a.trys.push([1, 3, , 4]);
|
|
99
|
-
setSelectedFile(file);
|
|
100
109
|
return [4 /*yield*/, file.arrayBuffer()];
|
|
101
110
|
case 2:
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
content = _a.sent();
|
|
112
|
+
setFileContent(content);
|
|
113
|
+
// Si c'est une image et que le cadrage est activé, afficher le composant de cadrage
|
|
114
|
+
if (enableCropping && isImageFile(file)) {
|
|
115
|
+
setSelectedFile(file);
|
|
116
|
+
setShowCropper(true);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Sinon, traiter normalement
|
|
120
|
+
setSelectedFile(file);
|
|
121
|
+
onFileRead === null || onFileRead === void 0 ? void 0 : onFileRead(file, content);
|
|
122
|
+
}
|
|
104
123
|
return [3 /*break*/, 4];
|
|
105
124
|
case 3:
|
|
106
125
|
error_1 = _a.sent();
|
|
@@ -111,8 +130,20 @@ var FileUploader = function (_a) {
|
|
|
111
130
|
}
|
|
112
131
|
});
|
|
113
132
|
}); };
|
|
133
|
+
var handleCropCancel = function () {
|
|
134
|
+
setShowCropper(false);
|
|
135
|
+
setSelectedFile(null);
|
|
136
|
+
resetFileInput();
|
|
137
|
+
};
|
|
138
|
+
var handleCropConfirm = function (cropMetadata) {
|
|
139
|
+
setShowCropper(false);
|
|
140
|
+
if (selectedFile && fileContent) {
|
|
141
|
+
onFileRead === null || onFileRead === void 0 ? void 0 : onFileRead(selectedFile, fileContent, cropMetadata);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
114
144
|
var handleFileDelete = function () {
|
|
115
145
|
setSelectedFile(null);
|
|
146
|
+
setFileContent(null);
|
|
116
147
|
resetFileInput();
|
|
117
148
|
onFileRemove === null || onFileRemove === void 0 ? void 0 : onFileRemove();
|
|
118
149
|
};
|
|
@@ -130,7 +161,7 @@ var FileUploader = function (_a) {
|
|
|
130
161
|
var displayFileSize = fileSize !== undefined ? fileSize : selectedFile ? selectedFile.size : 0;
|
|
131
162
|
var displayFileName = fileName || (selectedFile ? selectedFile.name : "");
|
|
132
163
|
return (React.createElement("div", { className: styles.upload_main_container },
|
|
133
|
-
enableDragAndDrop ? (React.createElement("div", { className: "".concat(styles.upload_container, " ").concat(isHovering ? styles.drag_over : ""), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop },
|
|
164
|
+
enableDragAndDrop ? (React.createElement("div", { className: "".concat(styles.upload_container, " ").concat(isHovering ? styles.drag_over : ""), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: function () { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } },
|
|
134
165
|
React.createElement("div", { className: styles.upload_content },
|
|
135
166
|
React.createElement("div", { className: styles.upload_icons },
|
|
136
167
|
React.createElement("div", { className: styles.upload_file_icon },
|
|
@@ -142,10 +173,9 @@ var FileUploader = function (_a) {
|
|
|
142
173
|
React.createElement("span", null,
|
|
143
174
|
"Glissez-d\u00E9posez votre fichier, ou",
|
|
144
175
|
" ",
|
|
145
|
-
React.createElement("
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
React.createElement("label", { htmlFor: "file-upload", className: styles.file_upload_label },
|
|
176
|
+
React.createElement("span", { className: styles.btn_tertiary }, buttonLabel),
|
|
177
|
+
React.createElement("input", { id: "file-upload", ref: fileInputRef, type: "file", accept: acceptedExtensions.join(","), style: { display: "none" }, onChange: handleFileSelect })))))) : (React.createElement("div", { className: styles.manual_upload_container },
|
|
178
|
+
React.createElement("button", { className: styles.file_upload_button, onClick: function () { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } },
|
|
149
179
|
React.createElement("span", { className: styles.btn_tertiary }, buttonLabel),
|
|
150
180
|
React.createElement("input", { id: "file-upload", ref: fileInputRef, type: "file", accept: acceptedExtensions.join(","), style: { display: "none" }, onChange: handleFileSelect })))),
|
|
151
181
|
React.createElement("div", { className: styles.limits },
|
|
@@ -171,7 +201,7 @@ var FileUploader = function (_a) {
|
|
|
171
201
|
React.createElement("span", { className: styles.error_message }, errorMessage)))),
|
|
172
202
|
(isLoading || selectedFile || fileName) && (React.createElement("div", { className: styles.uploaded_file_container },
|
|
173
203
|
React.createElement("div", { className: styles.uploaded_file_content },
|
|
174
|
-
React.createElement("div", { className: styles.uploaded_file_icon }, iconUrl ? (React.createElement(
|
|
204
|
+
React.createElement("div", { className: styles.uploaded_file_icon }, iconUrl ? (React.createElement(Image, { src: iconUrl, alt: "File icon", className: styles.file_icon, width: 20, height: 20 })) : (React.createElement("i", { className: "allaw-icon-file" }))),
|
|
175
205
|
React.createElement("div", { className: styles.uploaded_file_name_size },
|
|
176
206
|
React.createElement("div", { className: styles.uploaded_file_name },
|
|
177
207
|
React.createElement("span", { className: styles.file_name }, displayFileName || "Analyse en cours...")),
|
|
@@ -181,6 +211,7 @@ var FileUploader = function (_a) {
|
|
|
181
211
|
React.createElement("div", { className: styles.uploaded_file_delete, onClick: handleFileDelete },
|
|
182
212
|
React.createElement("i", { className: "allaw-icon-close" }))))),
|
|
183
213
|
React.createElement("div", { className: styles.uploaded_file_progress_bar }, uploadProgress > 0 && (React.createElement("div", { className: styles.progress_bar_container },
|
|
184
|
-
React.createElement("div", { className: styles.progress_bar_fill, style: { width: "".concat((uploadProgress / 10) * 100, "%") } }))))))
|
|
214
|
+
React.createElement("div", { className: styles.progress_bar_fill, style: { width: "".concat((uploadProgress / 10) * 100, "%") } })))))),
|
|
215
|
+
showCropper && selectedFile && (React.createElement(ImageCropperModal, { file: selectedFile, shape: cropShape, onCancel: handleCropCancel, onConfirm: handleCropConfirm }))));
|
|
185
216
|
};
|
|
186
217
|
export default FileUploader;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
background-color: #ffffff;
|
|
20
20
|
width: 100%;
|
|
21
21
|
transition: all 0.2s ease-in-out;
|
|
22
|
+
cursor: pointer;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
.manual_upload_container {
|
|
@@ -265,6 +266,16 @@
|
|
|
265
266
|
font-weight: 600;
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
.file_upload_button {
|
|
270
|
+
background: none;
|
|
271
|
+
border: none;
|
|
272
|
+
padding: 0;
|
|
273
|
+
margin: 0;
|
|
274
|
+
cursor: pointer;
|
|
275
|
+
font-family: inherit;
|
|
276
|
+
font-size: inherit;
|
|
277
|
+
}
|
|
278
|
+
|
|
268
279
|
@media (max-width: 768px) {
|
|
269
280
|
.upload_container {
|
|
270
281
|
padding: 2rem 1rem;
|
|
@@ -29,53 +29,69 @@ declare namespace _default {
|
|
|
29
29
|
let description_2: string;
|
|
30
30
|
export { description_2 as description };
|
|
31
31
|
}
|
|
32
|
-
namespace
|
|
32
|
+
namespace enableCropping {
|
|
33
33
|
let control_3: string;
|
|
34
34
|
export { control_3 as control };
|
|
35
35
|
let description_3: string;
|
|
36
36
|
export { description_3 as description };
|
|
37
37
|
}
|
|
38
|
-
namespace
|
|
39
|
-
|
|
38
|
+
namespace cropShape {
|
|
39
|
+
export namespace control_4 {
|
|
40
|
+
let type: string;
|
|
41
|
+
let options: string[];
|
|
42
|
+
}
|
|
40
43
|
export { control_4 as control };
|
|
41
44
|
let description_4: string;
|
|
42
45
|
export { description_4 as description };
|
|
43
46
|
}
|
|
44
|
-
namespace
|
|
47
|
+
namespace iconUrl {
|
|
45
48
|
let control_5: string;
|
|
46
49
|
export { control_5 as control };
|
|
47
50
|
let description_5: string;
|
|
48
51
|
export { description_5 as description };
|
|
49
52
|
}
|
|
50
|
-
namespace
|
|
53
|
+
namespace buttonLabel {
|
|
51
54
|
let control_6: string;
|
|
52
55
|
export { control_6 as control };
|
|
53
56
|
let description_6: string;
|
|
54
57
|
export { description_6 as description };
|
|
55
58
|
}
|
|
56
|
-
namespace
|
|
57
|
-
|
|
58
|
-
let type: string;
|
|
59
|
-
let min: number;
|
|
60
|
-
let max: number;
|
|
61
|
-
let step: number;
|
|
62
|
-
}
|
|
59
|
+
namespace acceptedLabel {
|
|
60
|
+
let control_7: string;
|
|
63
61
|
export { control_7 as control };
|
|
64
62
|
let description_7: string;
|
|
65
63
|
export { description_7 as description };
|
|
66
64
|
}
|
|
67
|
-
namespace
|
|
65
|
+
namespace maxSizeLabel {
|
|
68
66
|
let control_8: string;
|
|
69
67
|
export { control_8 as control };
|
|
70
68
|
let description_8: string;
|
|
71
69
|
export { description_8 as description };
|
|
72
70
|
}
|
|
73
|
-
namespace
|
|
74
|
-
|
|
71
|
+
namespace uploadProgress {
|
|
72
|
+
export namespace control_9 {
|
|
73
|
+
let type_1: string;
|
|
74
|
+
export { type_1 as type };
|
|
75
|
+
export let min: number;
|
|
76
|
+
export let max: number;
|
|
77
|
+
export let step: number;
|
|
78
|
+
}
|
|
75
79
|
export { control_9 as control };
|
|
76
80
|
let description_9: string;
|
|
77
81
|
export { description_9 as description };
|
|
78
82
|
}
|
|
83
|
+
namespace isLoading {
|
|
84
|
+
let control_10: string;
|
|
85
|
+
export { control_10 as control };
|
|
86
|
+
let description_10: string;
|
|
87
|
+
export { description_10 as description };
|
|
88
|
+
}
|
|
89
|
+
namespace errorMessage {
|
|
90
|
+
let control_11: string;
|
|
91
|
+
export { control_11 as control };
|
|
92
|
+
let description_11: string;
|
|
93
|
+
export { description_11 as description };
|
|
94
|
+
}
|
|
79
95
|
}
|
|
80
96
|
}
|
|
81
97
|
export default _default;
|
|
@@ -85,4 +101,6 @@ export const WithoutDragAndDrop: any;
|
|
|
85
101
|
export const WithError: any;
|
|
86
102
|
export const WithProgress: any;
|
|
87
103
|
export const MultipleExtensions: any;
|
|
104
|
+
export const WithSquareCropping: any;
|
|
105
|
+
export const WithCircleCropping: any;
|
|
88
106
|
import FileUploader from "./FileUploader";
|
|
@@ -77,6 +77,14 @@ export default {
|
|
|
77
77
|
control: "boolean",
|
|
78
78
|
description: "Active/désactive la zone de drag & drop",
|
|
79
79
|
},
|
|
80
|
+
enableCropping: {
|
|
81
|
+
control: "boolean",
|
|
82
|
+
description: "Active/désactive le cadrage pour les images",
|
|
83
|
+
},
|
|
84
|
+
cropShape: {
|
|
85
|
+
control: { type: "select", options: ["circle", "square"] },
|
|
86
|
+
description: "Forme du masque de cadrage (cercle ou carré)",
|
|
87
|
+
},
|
|
80
88
|
iconUrl: {
|
|
81
89
|
control: "text",
|
|
82
90
|
description: "URL de l'icône du type de fichier",
|
|
@@ -111,16 +119,21 @@ export default {
|
|
|
111
119
|
var Template = function (args) {
|
|
112
120
|
var _a = useState(null), file = _a[0], setFile = _a[1];
|
|
113
121
|
var _b = useState(null), fileContent = _b[0], setFileContent = _b[1];
|
|
114
|
-
var _c = useState(
|
|
115
|
-
var _d = useState(
|
|
116
|
-
var _e = useState(
|
|
117
|
-
var
|
|
122
|
+
var _c = useState(null), cropMetadata = _c[0], setCropMetadata = _c[1];
|
|
123
|
+
var _d = useState(0), progress = _d[0], setProgress = _d[1];
|
|
124
|
+
var _e = useState(false), loading = _e[0], setLoading = _e[1];
|
|
125
|
+
var _f = useState(null), error = _f[0], setError = _f[1];
|
|
126
|
+
var handleFileRead = function (file, content, cropMeta) { return __awaiter(void 0, void 0, void 0, function () {
|
|
118
127
|
return __generator(this, function (_a) {
|
|
119
128
|
switch (_a.label) {
|
|
120
129
|
case 0:
|
|
121
130
|
setLoading(true);
|
|
122
131
|
setFile(file);
|
|
123
132
|
setFileContent(content);
|
|
133
|
+
if (cropMeta) {
|
|
134
|
+
setCropMetadata(cropMeta);
|
|
135
|
+
console.log("Métadonnées de cadrage:", cropMeta);
|
|
136
|
+
}
|
|
124
137
|
// Simuler une progression
|
|
125
138
|
setProgress(2);
|
|
126
139
|
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 300); })];
|
|
@@ -144,21 +157,32 @@ var Template = function (args) {
|
|
|
144
157
|
var handleFileRemove = function () {
|
|
145
158
|
setFile(null);
|
|
146
159
|
setFileContent(null);
|
|
160
|
+
setCropMetadata(null);
|
|
147
161
|
setProgress(0);
|
|
148
162
|
setError(null);
|
|
149
163
|
console.log("Fichier supprimé");
|
|
150
164
|
};
|
|
151
|
-
return (React.createElement(
|
|
165
|
+
return (React.createElement("div", null,
|
|
166
|
+
React.createElement(FileUploader, __assign({}, args, { onFileRead: handleFileRead, onFileRemove: handleFileRemove, uploadProgress: progress, isLoading: loading, errorMessage: error })),
|
|
167
|
+
cropMetadata && (React.createElement("div", { style: {
|
|
168
|
+
marginTop: "20px",
|
|
169
|
+
padding: "10px",
|
|
170
|
+
backgroundColor: "#f5f5f5",
|
|
171
|
+
borderRadius: "8px",
|
|
172
|
+
} },
|
|
173
|
+
React.createElement("h4", { style: { margin: "0 0 10px 0", fontSize: "14px", color: "#456073" } }, "M\u00E9tadonn\u00E9es de cadrage:"),
|
|
174
|
+
React.createElement("code", { style: { display: "block", fontSize: "12px", color: "#333" } }, JSON.stringify(cropMetadata, null, 2))))));
|
|
152
175
|
};
|
|
153
176
|
export var Default = Template.bind({});
|
|
154
177
|
Default.args = {
|
|
155
|
-
acceptedExtensions: [".
|
|
178
|
+
acceptedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
|
|
156
179
|
maxFileSizeMB: 5,
|
|
157
180
|
enableDragAndDrop: true,
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
enableCropping: true,
|
|
182
|
+
cropShape: "circle",
|
|
183
|
+
buttonLabel: "Choisir une image",
|
|
184
|
+
acceptedLabel: "Formats acceptés :",
|
|
160
185
|
maxSizeLabel: "Taille maximale :",
|
|
161
|
-
iconUrl: pdfIcon,
|
|
162
186
|
};
|
|
163
187
|
export var WithDescription = Template.bind({});
|
|
164
188
|
WithDescription.args = __assign(__assign({}, Default.args), { descriptionParts: {
|
|
@@ -176,3 +200,27 @@ WithProgress.args = __assign(__assign({}, Default.args), { uploadProgress: 6, is
|
|
|
176
200
|
// Exemple avec des extensions multiples
|
|
177
201
|
export var MultipleExtensions = Template.bind({});
|
|
178
202
|
MultipleExtensions.args = __assign(__assign({}, Default.args), { acceptedExtensions: [".pdf", ".doc", ".docx"] });
|
|
203
|
+
// Exemple avec le cadrage d'image en carré
|
|
204
|
+
export var WithSquareCropping = Template.bind({});
|
|
205
|
+
WithSquareCropping.args = {
|
|
206
|
+
acceptedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
|
|
207
|
+
maxFileSizeMB: 5,
|
|
208
|
+
enableDragAndDrop: true,
|
|
209
|
+
enableCropping: true,
|
|
210
|
+
cropShape: "square",
|
|
211
|
+
buttonLabel: "Choisir une image",
|
|
212
|
+
acceptedLabel: "Formats acceptés :",
|
|
213
|
+
maxSizeLabel: "Taille maximale :",
|
|
214
|
+
};
|
|
215
|
+
// Exemple avec le cadrage d'image en cercle
|
|
216
|
+
export var WithCircleCropping = Template.bind({});
|
|
217
|
+
WithCircleCropping.args = {
|
|
218
|
+
acceptedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
|
|
219
|
+
maxFileSizeMB: 5,
|
|
220
|
+
enableDragAndDrop: true,
|
|
221
|
+
enableCropping: true,
|
|
222
|
+
cropShape: "circle",
|
|
223
|
+
buttonLabel: "Choisir une image",
|
|
224
|
+
acceptedLabel: "Formats acceptés :",
|
|
225
|
+
maxSizeLabel: "Taille maximale :",
|
|
226
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface CropMetadata {
|
|
3
|
+
zoom: number;
|
|
4
|
+
offsetX: number;
|
|
5
|
+
offsetY: number;
|
|
6
|
+
shape: "circle" | "square";
|
|
7
|
+
}
|
|
8
|
+
interface ImageCropperModalProps {
|
|
9
|
+
file: File;
|
|
10
|
+
shape: "circle" | "square";
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
onConfirm: (cropMetadata: CropMetadata) => void;
|
|
13
|
+
}
|
|
14
|
+
declare const ImageCropperModal: React.FC<ImageCropperModalProps>;
|
|
15
|
+
export default ImageCropperModal;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
import styles from "./ImageCropperModal.module.css";
|
|
4
|
+
var ImageCropperModal = function (_a) {
|
|
5
|
+
var file = _a.file, shape = _a.shape, onCancel = _a.onCancel, onConfirm = _a.onConfirm;
|
|
6
|
+
var _b = useState(1), zoom = _b[0], setZoom = _b[1]; // Facteur de zoom relatif (1-3)
|
|
7
|
+
var _c = useState(1), baseZoom = _c[0], setBaseZoom = _c[1]; // Zoom de base pour fit l'image
|
|
8
|
+
var _d = useState({
|
|
9
|
+
x: 0,
|
|
10
|
+
y: 0,
|
|
11
|
+
}), position = _d[0], setPosition = _d[1];
|
|
12
|
+
var _e = useState(false), isDragging = _e[0], setIsDragging = _e[1];
|
|
13
|
+
var _f = useState(null), imageSrc = _f[0], setImageSrc = _f[1];
|
|
14
|
+
var _g = useState({
|
|
15
|
+
x: 0,
|
|
16
|
+
y: 0,
|
|
17
|
+
}), dragStart = _g[0], setDragStart = _g[1];
|
|
18
|
+
var _h = useState(null), imageSize = _h[0], setImageSize = _h[1];
|
|
19
|
+
var imageRef = useRef(null);
|
|
20
|
+
var cropAreaRef = useRef(null);
|
|
21
|
+
var imageWrapperRef = useRef(null);
|
|
22
|
+
// Calcule le zoom de base pour que l'image "fit" parfaitement dans le cadre
|
|
23
|
+
var calculateFitZoom = function (imgWidth, imgHeight) {
|
|
24
|
+
if (!cropAreaRef.current)
|
|
25
|
+
return 1;
|
|
26
|
+
var cropRect = cropAreaRef.current.getBoundingClientRect();
|
|
27
|
+
var cropWidth = cropRect.width;
|
|
28
|
+
var cropHeight = cropRect.height;
|
|
29
|
+
// Calcul du zoom pour que l'image couvre complètement le cadre
|
|
30
|
+
// On utilise max pour assurer que l'image couvre tout le cadre, sans espace vide
|
|
31
|
+
var fitZoom = Math.max(cropWidth / imgWidth, cropHeight / imgHeight);
|
|
32
|
+
return fitZoom;
|
|
33
|
+
};
|
|
34
|
+
// Calcule les limites de déplacement pour éviter les zones vides
|
|
35
|
+
var calculateBoundaries = function () {
|
|
36
|
+
if (!cropAreaRef.current || !imageSize) {
|
|
37
|
+
return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
|
|
38
|
+
}
|
|
39
|
+
var cropRect = cropAreaRef.current.getBoundingClientRect();
|
|
40
|
+
var cropWidth = cropRect.width;
|
|
41
|
+
var cropHeight = cropRect.height;
|
|
42
|
+
// Taille de l'image après application des deux zooms
|
|
43
|
+
var totalZoom = baseZoom * zoom;
|
|
44
|
+
var scaledWidth = imageSize.width * totalZoom;
|
|
45
|
+
var scaledHeight = imageSize.height * totalZoom;
|
|
46
|
+
// Si l'image est plus petite que le crop à cause du zoom
|
|
47
|
+
// ce qui ne devrait pas arriver avec notre logique, mais au cas où
|
|
48
|
+
if (scaledWidth <= cropWidth || scaledHeight <= cropHeight) {
|
|
49
|
+
return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
|
|
50
|
+
}
|
|
51
|
+
// Calcule combien l'image peut être déplacée sans créer d'espace vide
|
|
52
|
+
var xLimit = Math.max(0, (scaledWidth - cropWidth) / 2);
|
|
53
|
+
var yLimit = Math.max(0, (scaledHeight - cropHeight) / 2);
|
|
54
|
+
return {
|
|
55
|
+
minX: -xLimit,
|
|
56
|
+
maxX: xLimit,
|
|
57
|
+
minY: -yLimit,
|
|
58
|
+
maxY: yLimit,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
// Centre l'image dans la zone de crop
|
|
62
|
+
var centerImage = function () {
|
|
63
|
+
setPosition({ x: 0, y: 0 });
|
|
64
|
+
};
|
|
65
|
+
useEffect(function () {
|
|
66
|
+
var reader = new FileReader();
|
|
67
|
+
reader.onload = function (e) {
|
|
68
|
+
if (e.target && typeof e.target.result === "string") {
|
|
69
|
+
setImageSrc(e.target.result);
|
|
70
|
+
// Charge l'image pour obtenir ses dimensions
|
|
71
|
+
var img_1 = new window.Image();
|
|
72
|
+
img_1.onload = function () {
|
|
73
|
+
setImageSize({ width: img_1.width, height: img_1.height });
|
|
74
|
+
// Attendre que les refs soient disponibles
|
|
75
|
+
setTimeout(function () {
|
|
76
|
+
var fitZoom = calculateFitZoom(img_1.width, img_1.height);
|
|
77
|
+
setBaseZoom(fitZoom);
|
|
78
|
+
setZoom(1); // Zoom initial toujours à 1 (relatif)
|
|
79
|
+
centerImage();
|
|
80
|
+
}, 50);
|
|
81
|
+
};
|
|
82
|
+
img_1.src = e.target.result;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
reader.readAsDataURL(file);
|
|
86
|
+
return function () {
|
|
87
|
+
reader.abort();
|
|
88
|
+
};
|
|
89
|
+
}, [file]);
|
|
90
|
+
// Recalcule les limites quand le zoom change
|
|
91
|
+
useEffect(function () {
|
|
92
|
+
// Quand zoom change, recalculer les limites et ajuster la position si nécessaire
|
|
93
|
+
if (imageSize && cropAreaRef.current) {
|
|
94
|
+
var boundaries = calculateBoundaries();
|
|
95
|
+
// Ajuste la position pour rester dans les limites
|
|
96
|
+
setPosition({
|
|
97
|
+
x: Math.max(boundaries.minX, Math.min(boundaries.maxX, position.x)),
|
|
98
|
+
y: Math.max(boundaries.minY, Math.min(boundaries.maxY, position.y)),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}, [zoom, imageSize]);
|
|
102
|
+
var handleMouseDown = function (e) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
setIsDragging(true);
|
|
105
|
+
setDragStart({
|
|
106
|
+
x: e.clientX - position.x,
|
|
107
|
+
y: e.clientY - position.y,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
var handleTouchStart = function (e) {
|
|
111
|
+
if (e.touches.length !== 1)
|
|
112
|
+
return;
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
setIsDragging(true);
|
|
115
|
+
setDragStart({
|
|
116
|
+
x: e.touches[0].clientX - position.x,
|
|
117
|
+
y: e.touches[0].clientY - position.y,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
var updatePosition = function (clientX, clientY) {
|
|
121
|
+
if (!isDragging)
|
|
122
|
+
return;
|
|
123
|
+
var newX = clientX - dragStart.x;
|
|
124
|
+
var newY = clientY - dragStart.y;
|
|
125
|
+
var boundaries = calculateBoundaries();
|
|
126
|
+
setPosition({
|
|
127
|
+
x: Math.max(boundaries.minX, Math.min(boundaries.maxX, newX)),
|
|
128
|
+
y: Math.max(boundaries.minY, Math.min(boundaries.maxY, newY)),
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
var handleMouseMove = function (e) {
|
|
132
|
+
if (!isDragging)
|
|
133
|
+
return;
|
|
134
|
+
updatePosition(e.clientX, e.clientY);
|
|
135
|
+
};
|
|
136
|
+
var handleTouchMove = function (e) {
|
|
137
|
+
if (!isDragging || e.touches.length !== 1)
|
|
138
|
+
return;
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
updatePosition(e.touches[0].clientX, e.touches[0].clientY);
|
|
141
|
+
};
|
|
142
|
+
var handleMouseUp = function () {
|
|
143
|
+
setIsDragging(false);
|
|
144
|
+
};
|
|
145
|
+
var handleTouchEnd = function () {
|
|
146
|
+
setIsDragging(false);
|
|
147
|
+
};
|
|
148
|
+
var handleZoomChange = function (e) {
|
|
149
|
+
var newZoom = parseFloat(e.target.value);
|
|
150
|
+
setZoom(Math.min(3, Math.max(1, newZoom)));
|
|
151
|
+
};
|
|
152
|
+
useEffect(function () {
|
|
153
|
+
if (isDragging) {
|
|
154
|
+
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
155
|
+
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
156
|
+
document.addEventListener("touchmove", handleDocumentTouchMove, {
|
|
157
|
+
passive: false,
|
|
158
|
+
});
|
|
159
|
+
document.addEventListener("touchend", handleDocumentTouchEnd);
|
|
160
|
+
}
|
|
161
|
+
return function () {
|
|
162
|
+
document.removeEventListener("mousemove", handleDocumentMouseMove);
|
|
163
|
+
document.removeEventListener("mouseup", handleDocumentMouseUp);
|
|
164
|
+
document.removeEventListener("touchmove", handleDocumentTouchMove);
|
|
165
|
+
document.removeEventListener("touchend", handleDocumentTouchEnd);
|
|
166
|
+
};
|
|
167
|
+
}, [isDragging, dragStart, zoom, imageSize]);
|
|
168
|
+
var handleDocumentMouseMove = function (e) {
|
|
169
|
+
updatePosition(e.clientX, e.clientY);
|
|
170
|
+
};
|
|
171
|
+
var handleDocumentTouchMove = function (e) {
|
|
172
|
+
if (e.touches.length !== 1)
|
|
173
|
+
return;
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
updatePosition(e.touches[0].clientX, e.touches[0].clientY);
|
|
176
|
+
};
|
|
177
|
+
var handleDocumentMouseUp = function () {
|
|
178
|
+
setIsDragging(false);
|
|
179
|
+
};
|
|
180
|
+
var handleDocumentTouchEnd = function () {
|
|
181
|
+
setIsDragging(false);
|
|
182
|
+
};
|
|
183
|
+
return (React.createElement("div", { className: styles.modal_overlay },
|
|
184
|
+
React.createElement("div", { className: styles.modal_content },
|
|
185
|
+
React.createElement("div", { className: styles.modal_header },
|
|
186
|
+
React.createElement("h3", { className: styles.modal_title }, "Recadrer l'image")),
|
|
187
|
+
React.createElement("div", { className: styles.crop_container },
|
|
188
|
+
React.createElement("div", { className: styles.image_wrapper, ref: imageWrapperRef }, imageSrc && (React.createElement("div", { ref: imageRef, className: styles.image_container, style: {
|
|
189
|
+
transform: "translate(".concat(position.x, "px, ").concat(position.y, "px) scale(").concat(baseZoom * zoom, ")"),
|
|
190
|
+
cursor: isDragging ? "grabbing" : "grab",
|
|
191
|
+
} },
|
|
192
|
+
React.createElement(Image, { src: imageSrc, alt: "Aper\u00E7u", className: styles.image, draggable: false, width: 1000, height: 1000, priority: true, unoptimized: true })))),
|
|
193
|
+
React.createElement("div", { ref: cropAreaRef, className: "".concat(styles.crop_area, " ").concat(styles["crop_area_".concat(shape)]), onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd })),
|
|
194
|
+
React.createElement("div", { className: styles.controls },
|
|
195
|
+
React.createElement("div", { className: styles.zoom_control },
|
|
196
|
+
React.createElement("span", { className: styles.zoom_label }, "Zoom:"),
|
|
197
|
+
React.createElement("button", { className: styles.zoom_button, onClick: function () { return setZoom(Math.max(1, zoom - 0.1)); }, disabled: zoom <= 1 }, "\u2013"),
|
|
198
|
+
React.createElement("input", { type: "range", min: "1", max: "3", step: "0.1", value: zoom, onChange: handleZoomChange, className: styles.zoom_slider }),
|
|
199
|
+
React.createElement("button", { className: styles.zoom_button, onClick: function () { return setZoom(Math.min(3, zoom + 0.1)); }, disabled: zoom >= 3 }, "+")),
|
|
200
|
+
React.createElement("div", { className: styles.action_buttons },
|
|
201
|
+
React.createElement("button", { className: styles.cancel_button, onClick: onCancel }, "Annuler"),
|
|
202
|
+
React.createElement("button", { className: styles.confirm_button, onClick: function () {
|
|
203
|
+
return onConfirm({
|
|
204
|
+
zoom: zoom,
|
|
205
|
+
offsetX: position.x,
|
|
206
|
+
offsetY: position.y,
|
|
207
|
+
shape: shape,
|
|
208
|
+
});
|
|
209
|
+
} }, "Valider"))))));
|
|
210
|
+
};
|
|
211
|
+
export default ImageCropperModal;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
.modal_overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
z-index: 9999;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.modal_content {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
background-color: transparent;
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
width: 90%;
|
|
20
|
+
max-width: 500px;
|
|
21
|
+
height: 80vh;
|
|
22
|
+
max-height: 600px;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.modal_header {
|
|
27
|
+
padding: 1rem;
|
|
28
|
+
background-color: white;
|
|
29
|
+
border-bottom: 1px solid var(--grey-venom, #e6edf5);
|
|
30
|
+
border-radius: 12px 12px 0 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.modal_title {
|
|
34
|
+
font-family: var(--font-open-sans, sans-serif);
|
|
35
|
+
font-size: 18px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
color: var(--noir, #171e25);
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.crop_container {
|
|
42
|
+
position: relative;
|
|
43
|
+
width: 100%;
|
|
44
|
+
flex: 1;
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
align-items: center;
|
|
48
|
+
padding: 1rem;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.crop_mask {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 0;
|
|
56
|
+
left: 0;
|
|
57
|
+
right: 0;
|
|
58
|
+
bottom: 0;
|
|
59
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
z-index: 10;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.crop_area {
|
|
65
|
+
position: relative;
|
|
66
|
+
width: 280px;
|
|
67
|
+
height: 280px;
|
|
68
|
+
background-color: transparent;
|
|
69
|
+
z-index: 20;
|
|
70
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.crop_area_square {
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.crop_area_circle {
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.crop_area::before {
|
|
82
|
+
content: "";
|
|
83
|
+
position: absolute;
|
|
84
|
+
inset: 0;
|
|
85
|
+
border: 2px solid rgba(255, 255, 255, 0.8);
|
|
86
|
+
border-radius: inherit;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
z-index: 25;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.image_wrapper {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: 0;
|
|
94
|
+
left: 0;
|
|
95
|
+
width: 100%;
|
|
96
|
+
height: 100%;
|
|
97
|
+
display: flex;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
align-items: center;
|
|
100
|
+
z-index: 5;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
pointer-events: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.image_container {
|
|
106
|
+
position: absolute;
|
|
107
|
+
transform-origin: center;
|
|
108
|
+
will-change: transform;
|
|
109
|
+
touch-action: none;
|
|
110
|
+
cursor: grab;
|
|
111
|
+
pointer-events: all;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.image {
|
|
115
|
+
width: auto !important;
|
|
116
|
+
height: auto !important;
|
|
117
|
+
max-width: none !important;
|
|
118
|
+
max-height: none !important;
|
|
119
|
+
user-select: none;
|
|
120
|
+
-webkit-user-drag: none;
|
|
121
|
+
display: block;
|
|
122
|
+
object-fit: cover !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.controls {
|
|
126
|
+
padding: 1rem;
|
|
127
|
+
background-color: white;
|
|
128
|
+
border-top: 1px solid var(--grey-venom, #e6edf5);
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
gap: 1rem;
|
|
132
|
+
border-radius: 0 0 12px 12px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.zoom_control {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
gap: 0.5rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.zoom_label {
|
|
143
|
+
font-family: var(--font-open-sans, sans-serif);
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
color: var(--mid-grey, #728ea7);
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.zoom_slider {
|
|
150
|
+
flex: 1;
|
|
151
|
+
-webkit-appearance: none;
|
|
152
|
+
appearance: none;
|
|
153
|
+
height: 4px;
|
|
154
|
+
background: var(--grey-venom, #e6edf5);
|
|
155
|
+
border-radius: 2px;
|
|
156
|
+
outline: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.zoom_slider::-webkit-slider-thumb {
|
|
160
|
+
-webkit-appearance: none;
|
|
161
|
+
appearance: none;
|
|
162
|
+
width: 18px;
|
|
163
|
+
height: 18px;
|
|
164
|
+
background: var(--bleu-allaw, #25beeb);
|
|
165
|
+
border-radius: 50%;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.zoom_slider::-moz-range-thumb {
|
|
170
|
+
width: 18px;
|
|
171
|
+
height: 18px;
|
|
172
|
+
background: var(--bleu-allaw, #25beeb);
|
|
173
|
+
border-radius: 50%;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
border: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.zoom_button {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
width: 30px;
|
|
183
|
+
height: 30px;
|
|
184
|
+
border-radius: 50%;
|
|
185
|
+
border: 1px solid var(--grey-venom, #e6edf5);
|
|
186
|
+
background-color: white;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
font-size: 16px;
|
|
189
|
+
color: var(--noir, #171e25);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.zoom_button:disabled {
|
|
193
|
+
opacity: 0.5;
|
|
194
|
+
cursor: not-allowed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.action_buttons {
|
|
198
|
+
display: flex;
|
|
199
|
+
justify-content: flex-end;
|
|
200
|
+
gap: 1rem;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.cancel_button,
|
|
204
|
+
.confirm_button {
|
|
205
|
+
padding: 0.5rem 1.5rem;
|
|
206
|
+
border-radius: 4px;
|
|
207
|
+
font-family: var(--font-open-sans, sans-serif);
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
font-weight: 600;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
transition: all 0.2s ease;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.cancel_button {
|
|
215
|
+
background-color: transparent;
|
|
216
|
+
border: 1px solid var(--grey-venom, #e6edf5);
|
|
217
|
+
color: var(--mid-grey, #728ea7);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.confirm_button {
|
|
221
|
+
background-color: var(--bleu-allaw, #25beeb);
|
|
222
|
+
border: 1px solid var(--bleu-allaw, #25beeb);
|
|
223
|
+
color: white;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cancel_button:hover {
|
|
227
|
+
background-color: #f8f9fb;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.confirm_button:hover {
|
|
231
|
+
background-color: #1da9d2;
|
|
232
|
+
}
|