hocmai-feedback-widget 0.2.3 → 0.2.4
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/LICENSE +21 -0
- package/README.md +84 -33
- package/dist/context/FeedbackContext.d.ts +6 -0
- package/dist/hocmai-feedback-widget.mjs +27 -20
- package/dist/hocmai-feedback-widget.mjs.map +1 -1
- package/package.json +3 -1
- package/src/components/FeedbackWidget.tsx +4 -3
- package/src/context/FeedbackContext.tsx +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HOCMAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
# hocmai-feedback-widget
|
|
2
2
|
|
|
3
|
-
Widget báo lỗi câu hỏi kèm chụp ảnh màn hình, tối ưu cho trang nhiều công thức **MathJax CHTML**.
|
|
3
|
+
> Widget báo lỗi câu hỏi kèm chụp ảnh màn hình, tối ưu cho trang nhiều công thức **MathJax CHTML**.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
Bộ chụp dùng [snapdom](https://github.com/zumerlab/snapdom) + nhúng font MathJax → chụp nhanh, đủ công thức ngay lần đầu, an toàn SSR.
|
|
11
|
+
|
|
12
|
+
## Mục lục
|
|
13
|
+
|
|
14
|
+
- [Tính năng](#tính-năng)
|
|
15
|
+
- [Cài đặt](#cài-đặt)
|
|
16
|
+
- [Sử dụng](#sử-dụng)
|
|
17
|
+
- [API](#api)
|
|
18
|
+
- [Lưu ý](#lưu-ý)
|
|
19
|
+
- [License](#license)
|
|
20
|
+
|
|
21
|
+
## Tính năng
|
|
22
|
+
|
|
23
|
+
- 📸 Chụp **toàn màn hình** hoặc **chọn vùng**, render đúng công thức MathJax CHTML.
|
|
24
|
+
- 📝 Mô tả lỗi bằng **textarea** hoặc **CKEditor** (tùy chọn).
|
|
25
|
+
- 🖼️ Tự upload ảnh và trả về URL (tối đa 3 ảnh).
|
|
26
|
+
- 🔌 API gọn: `FeedbackProvider` + hook `useFeedback`.
|
|
27
|
+
- 🧩 Core **không** phụ thuộc CKEditor — chỉ cài khi cần.
|
|
4
28
|
|
|
5
29
|
## Cài đặt
|
|
6
30
|
|
|
7
31
|
```bash
|
|
8
32
|
npm install hocmai-feedback-widget
|
|
9
|
-
# peer deps: react, react-dom
|
|
10
33
|
```
|
|
11
34
|
|
|
35
|
+
**Peer dependencies:** `react`, `react-dom` (>= 16.8).
|
|
36
|
+
|
|
12
37
|
## Sử dụng
|
|
13
38
|
|
|
14
39
|
### 1. Bọc app bằng `FeedbackProvider`
|
|
@@ -16,14 +41,17 @@ npm install hocmai-feedback-widget
|
|
|
16
41
|
```tsx
|
|
17
42
|
import { FeedbackProvider, type FeedbackData } from 'hocmai-feedback-widget';
|
|
18
43
|
|
|
19
|
-
<FeedbackProvider
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
<FeedbackProvider
|
|
45
|
+
onSubmit={async (data: FeedbackData) => {
|
|
46
|
+
// data.listImage: mảng URL ảnh đã upload
|
|
47
|
+
await api.post('/questionReport/studentReport', data);
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
22
50
|
<App />
|
|
23
|
-
</FeedbackProvider
|
|
51
|
+
</FeedbackProvider>;
|
|
24
52
|
```
|
|
25
53
|
|
|
26
|
-
`onSubmit` **không bắt buộc** ở Provider — có thể truyền theo từng lần ở `openFeedback` (xem
|
|
54
|
+
> `onSubmit` **không bắt buộc** ở Provider — có thể truyền theo từng lần ở `openFeedback` (xem mục 2).
|
|
27
55
|
|
|
28
56
|
### 2. Mở form từ component con bằng `useFeedback`
|
|
29
57
|
|
|
@@ -32,16 +60,16 @@ import { useFeedback } from 'hocmai-feedback-widget';
|
|
|
32
60
|
|
|
33
61
|
const { openFeedback } = useFeedback();
|
|
34
62
|
|
|
35
|
-
// Dùng onSubmit của Provider:
|
|
63
|
+
// a) Dùng onSubmit của Provider:
|
|
36
64
|
openFeedback(questionId, { questionIdChild });
|
|
37
65
|
|
|
38
|
-
// Hoặc truyền handler riêng cho lần gọi này (ghi đè Provider):
|
|
66
|
+
// b) Hoặc truyền handler riêng cho lần gọi này (ghi đè Provider):
|
|
39
67
|
openFeedback(questionId, { questionIdChild }, async (data) => {
|
|
40
68
|
await api.post('/questionReport/studentReport', data);
|
|
41
69
|
});
|
|
42
70
|
```
|
|
43
71
|
|
|
44
|
-
### 3.
|
|
72
|
+
### 3. Rich-text editor cho ô mô tả _(tùy chọn)_
|
|
45
73
|
|
|
46
74
|
Bật `isCkEditor` **và** truyền `descriptionEditor` (editor đóng gói sẵn ở entry phụ):
|
|
47
75
|
|
|
@@ -51,47 +79,69 @@ import CkEditor from 'hocmai-feedback-widget/ckeditor';
|
|
|
51
79
|
|
|
52
80
|
<FeedbackProvider onSubmit={handleSubmit} isCkEditor descriptionEditor={CkEditor}>
|
|
53
81
|
<App />
|
|
54
|
-
</FeedbackProvider
|
|
82
|
+
</FeedbackProvider>;
|
|
55
83
|
```
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
Cài thêm peer deps (chỉ khi dùng CKEditor):
|
|
58
86
|
|
|
59
87
|
```bash
|
|
60
|
-
yarn add @ckeditor/ckeditor5-react hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
|
|
88
|
+
yarn add @ckeditor/ckeditor5-react@5.0.0 hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
|
|
61
89
|
```
|
|
62
90
|
|
|
63
|
-
|
|
91
|
+
**Lưu ý**: `@ckeditor/ckeditor5-react` chỉ hỗ trợ version 5, `hocmai-ckeditor-custom` có thể dùng version 0.0.2 hoặc 0.0.7 trở lên.
|
|
92
|
+
|
|
93
|
+
### 4. Chụp màn hình trực tiếp _(tùy chọn)_
|
|
64
94
|
|
|
65
95
|
```ts
|
|
66
96
|
import { captureScreenshot } from 'hocmai-feedback-widget';
|
|
67
97
|
|
|
68
|
-
const png = await captureScreenshot();
|
|
69
|
-
const region = await captureScreenshot({ x, y, width, height });
|
|
98
|
+
const png = await captureScreenshot(); // toàn viewport
|
|
99
|
+
const region = await captureScreenshot({ x, y, width, height }); // 1 vùng (toạ độ viewport)
|
|
70
100
|
```
|
|
71
101
|
|
|
72
102
|
## API
|
|
73
103
|
|
|
74
|
-
|
|
104
|
+
### `FeedbackProvider`
|
|
105
|
+
|
|
106
|
+
| Prop | Kiểu | Bắt buộc | Mặc định | Mô tả |
|
|
107
|
+
| --- | --- | :---: | :---: | --- |
|
|
108
|
+
| `children` | `ReactNode` | ✓ | — | Nội dung app được bọc. |
|
|
109
|
+
| `onSubmit` | `(data: FeedbackData) => Promise<void> \| void` | — | — | Handler mặc định khi gửi báo lỗi. Có thể bỏ, truyền theo từng lần ở `openFeedback`. |
|
|
110
|
+
| `themeColor` | `string` | — | `#3b82f6` | Màu chủ đạo của nút/viền. |
|
|
111
|
+
| `isCkEditor` | `boolean` | — | `false` | Dùng rich-text editor cho ô mô tả (cần kèm `descriptionEditor`). |
|
|
112
|
+
| `descriptionEditor` | `ComponentType<CkEditorProps>` | — | — | Component editor cho ô mô tả, vd `import CkEditor from 'hocmai-feedback-widget/ckeditor'`. |
|
|
113
|
+
| `isEnableCaptureFull` | `boolean` | — | `true` | Hiện nút "Toàn màn hình". |
|
|
114
|
+
| `isEnableCaptureElement` | `boolean` | — | `true` | Hiện nút "Chọn vùng". |
|
|
115
|
+
| `uploadUrl` | `string` | — | endpoint LCMS | Nơi upload ảnh: nhận `{ base64 }`, trả `{ file_url }`. |
|
|
116
|
+
|
|
117
|
+
### `useFeedback()`
|
|
75
118
|
|
|
76
|
-
|
|
77
|
-
|------|------|-------|
|
|
78
|
-
| `onSubmit` | `(data: FeedbackData) => Promise<void> \| void` | Tùy chọn. Handler mặc định khi gửi. |
|
|
79
|
-
| `isCkEditor` | `boolean` | Dùng rich-text editor cho ô mô tả (cần `descriptionEditor`). |
|
|
80
|
-
| `descriptionEditor` | `ComponentType` | Editor truyền vào, vd `import CkEditor from 'hocmai-feedback-widget/ckeditor'`. |
|
|
81
|
-
| `themeColor` | `string` | Màu chủ đạo (mặc định `#3b82f6`). |
|
|
119
|
+
Trả về:
|
|
82
120
|
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
| Thành phần | Kiểu | Mô tả |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `openFeedback` | `(questionId, contextData?, onSubmit?) => void` | Mở form báo lỗi cho một câu hỏi. |
|
|
124
|
+
| `closeFeedback` | `() => void` | Đóng form. |
|
|
125
|
+
| `isOpen` | `boolean` | Form đang mở hay không. |
|
|
126
|
+
| `activeQuestionId` | `string \| number \| null` | ID câu hỏi đang mở form (`null` nếu đóng). |
|
|
85
127
|
|
|
86
|
-
|
|
128
|
+
Tham số của `openFeedback`:
|
|
129
|
+
|
|
130
|
+
| Tham số | Kiểu | Bắt buộc | Mô tả |
|
|
131
|
+
| --- | --- | :---: | --- |
|
|
132
|
+
| `questionId` | `string \| number` | ✓ | ID câu hỏi; tự điền vào ô "ID câu hỏi". |
|
|
133
|
+
| `contextData` | `object` | — | Dữ liệu kèm theo (trả lại trong `FeedbackData.contextData`). `contextData.questionIdChild` tự điền ô "ID câu hỏi con". |
|
|
134
|
+
| `onSubmit` | `(data: FeedbackData) => Promise<void> \| void` | — | Handler riêng cho lần gọi này, **ghi đè** `onSubmit` của Provider. |
|
|
135
|
+
|
|
136
|
+
### `FeedbackData`
|
|
87
137
|
|
|
88
138
|
```ts
|
|
89
|
-
{
|
|
139
|
+
interface FeedbackData {
|
|
90
140
|
idQuestion: string | number;
|
|
91
141
|
idChildQuestion?: string | number;
|
|
92
|
-
errorType: string;
|
|
142
|
+
errorType: string; // incorrect_content | display_error | missing_data | technical_issue | other
|
|
93
143
|
errorDescription: string; // text; là HTML nếu dùng CKEditor
|
|
94
|
-
listImage: string[];
|
|
144
|
+
listImage: string[]; // URL ảnh đã upload (tối đa 3)
|
|
95
145
|
deviceInfo: { os; osVersion; browser; browserVersion; deviceType };
|
|
96
146
|
contextData?: any;
|
|
97
147
|
}
|
|
@@ -99,11 +149,12 @@ const region = await captureScreenshot({ x, y, width, height }); // 1 vùng (to
|
|
|
99
149
|
|
|
100
150
|
## Lưu ý
|
|
101
151
|
|
|
102
|
-
- **
|
|
103
|
-
- **CKEditor
|
|
152
|
+
- **Thứ tự `onSubmit`:** handler theo lần gọi → handler ở Provider → nếu không có thì chỉ cảnh báo (không crash).
|
|
153
|
+
- **CKEditor:** `hocmai-ckeditor-custom` là build kiểu cũ → dùng `@ckeditor/ckeditor5-react` đời cũ (v5/v6), **không** dùng latest. Nên copy đúng version từ project gốc. Build này đã chứa core nên **không cần** cài `@ckeditor/ckeditor5-core`.
|
|
104
154
|
- Khi dùng CKEditor, `errorDescription` trả về là **HTML** — backend cần xử lý phù hợp.
|
|
105
|
-
- Core **không** phụ thuộc CKEditor: app không dùng `isCkEditor` vẫn build bình thường
|
|
155
|
+
- Core **không** phụ thuộc CKEditor: app không dùng `isCkEditor` vẫn build bình thường.
|
|
106
156
|
- An toàn **SSR** (không chạm `window`/`document` lúc import).
|
|
107
157
|
|
|
108
158
|
## License
|
|
109
|
-
|
|
159
|
+
|
|
160
|
+
[MIT](./LICENSE)
|
|
@@ -23,6 +23,12 @@ interface FeedbackProviderProps {
|
|
|
23
23
|
isCkEditor?: boolean;
|
|
24
24
|
/** Editor component for the description (from `hocmai-feedback-widget/ckeditor` or your own). */
|
|
25
25
|
descriptionEditor?: React.ComponentType<CkEditorProps>;
|
|
26
|
+
/** Show the "full screen" capture button. Default true. */
|
|
27
|
+
isEnableCaptureFull?: boolean;
|
|
28
|
+
/** Show the "select region" capture button. Default true. */
|
|
29
|
+
isEnableCaptureElement?: boolean;
|
|
30
|
+
/** Endpoint that accepts { base64 } and returns { file_url } for image uploads. */
|
|
31
|
+
uploadUrl?: string;
|
|
26
32
|
}
|
|
27
33
|
export declare const FeedbackProvider: React.FC<FeedbackProviderProps>;
|
|
28
34
|
export {};
|
|
@@ -7003,8 +7003,8 @@ const rd = [
|
|
|
7003
7003
|
alignItems: "center",
|
|
7004
7004
|
justifyContent: "center",
|
|
7005
7005
|
cursor: "zoom-out",
|
|
7006
|
-
animation: "fb-fade-in 0.
|
|
7007
|
-
|
|
7006
|
+
animation: "fb-fade-in 0.15s ease-out",
|
|
7007
|
+
willChange: "opacity"
|
|
7008
7008
|
},
|
|
7009
7009
|
onClick: () => k(null),
|
|
7010
7010
|
children: /* @__PURE__ */ _(
|
|
@@ -7013,6 +7013,7 @@ const rd = [
|
|
|
7013
7013
|
src: E,
|
|
7014
7014
|
alt: "Zoomed",
|
|
7015
7015
|
draggable: !1,
|
|
7016
|
+
decoding: "async",
|
|
7016
7017
|
onContextMenu: (I) => I.preventDefault(),
|
|
7017
7018
|
style: {
|
|
7018
7019
|
maxWidth: "90vw",
|
|
@@ -7066,7 +7067,7 @@ const rd = [
|
|
|
7066
7067
|
)
|
|
7067
7068
|
}
|
|
7068
7069
|
),
|
|
7069
|
-
L && /* @__PURE__ */ _("div", { "data-hsa-report-ui": "true", style: { position: "fixed", top: 20, right: 20, background: "rgba(0,0,0,0.8)", color: "white", padding: "8px 12px", borderRadius: "8px", zIndex: 1e4, fontWeight: 500,
|
|
7070
|
+
L && /* @__PURE__ */ _("div", { "data-hsa-report-ui": "true", style: { position: "fixed", top: 20, right: 20, background: "rgba(0,0,0,0.8)", color: "white", padding: "8px 12px", borderRadius: "8px", zIndex: 1e4, fontWeight: 500, fontSize: "0.8rem" }, children: "Đang chụp..." }),
|
|
7070
7071
|
X && !j && !E && /* @__PURE__ */ K("div", { className: "fb-widget-modal", "data-hsa-report-ui": "true", children: [
|
|
7071
7072
|
/* @__PURE__ */ K("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
|
|
7072
7073
|
/* @__PURE__ */ _("h2", { style: { margin: 0, fontSize: "1rem", fontWeight: 700, color: "#111827" }, children: "Báo lỗi" }),
|
|
@@ -7504,36 +7505,42 @@ const rd = [
|
|
|
7504
7505
|
onSubmit: t,
|
|
7505
7506
|
themeColor: r,
|
|
7506
7507
|
isCkEditor: n,
|
|
7507
|
-
descriptionEditor: o
|
|
7508
|
+
descriptionEditor: o,
|
|
7509
|
+
isEnableCaptureFull: i,
|
|
7510
|
+
isEnableCaptureElement: a,
|
|
7511
|
+
uploadUrl: l
|
|
7508
7512
|
}) => {
|
|
7509
|
-
const [
|
|
7510
|
-
|
|
7511
|
-
},
|
|
7512
|
-
|
|
7513
|
-
},
|
|
7514
|
-
const
|
|
7515
|
-
if (!
|
|
7513
|
+
const [s, u] = ie(!1), [c, d] = ie(null), [f, h] = ie(void 0), [m, p] = ie(void 0), g = (w, b, S) => {
|
|
7514
|
+
d(w), h(b), p(() => S), u(!0);
|
|
7515
|
+
}, y = () => {
|
|
7516
|
+
u(!1);
|
|
7517
|
+
}, x = async (w) => {
|
|
7518
|
+
const b = m ?? t;
|
|
7519
|
+
if (!b) {
|
|
7516
7520
|
console.warn(
|
|
7517
7521
|
"[feedback-widget] Bỏ qua gửi báo lỗi: chưa cung cấp onSubmit (ở FeedbackProvider hoặc openFeedback). Dữ liệu:",
|
|
7518
|
-
|
|
7522
|
+
w
|
|
7519
7523
|
);
|
|
7520
7524
|
return;
|
|
7521
7525
|
}
|
|
7522
|
-
await
|
|
7526
|
+
await b(w);
|
|
7523
7527
|
};
|
|
7524
|
-
return /* @__PURE__ */ K(Oo.Provider, { value: { openFeedback:
|
|
7528
|
+
return /* @__PURE__ */ K(Oo.Provider, { value: { openFeedback: g, closeFeedback: y, isOpen: s, activeQuestionId: c }, children: [
|
|
7525
7529
|
e,
|
|
7526
|
-
|
|
7530
|
+
c && /* @__PURE__ */ _(
|
|
7527
7531
|
od,
|
|
7528
7532
|
{
|
|
7529
|
-
questionId:
|
|
7530
|
-
contextData:
|
|
7531
|
-
onSubmit:
|
|
7533
|
+
questionId: c,
|
|
7534
|
+
contextData: f,
|
|
7535
|
+
onSubmit: x,
|
|
7532
7536
|
themeColor: r,
|
|
7533
7537
|
isCkEditor: n,
|
|
7534
7538
|
descriptionEditor: o,
|
|
7535
|
-
|
|
7536
|
-
|
|
7539
|
+
isEnableCaptureFull: i,
|
|
7540
|
+
isEnableCaptureElement: a,
|
|
7541
|
+
uploadUrl: l,
|
|
7542
|
+
isOpen: s,
|
|
7543
|
+
onClose: y,
|
|
7537
7544
|
triggerComponent: /* @__PURE__ */ _($t, {})
|
|
7538
7545
|
}
|
|
7539
7546
|
)
|