feedback-vos 1.0.26 → 1.0.28

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/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { ChatTeardropDots, ArrowLeft, X, Trash, Camera, CircleNotch } from 'phosphor-react';
1
+ import { ChatTeardropDots, ArrowLeft, X, Paperclip, Trash, Camera, CircleNotch } from 'phosphor-react';
2
2
  import { Popover } from '@headlessui/react';
3
- import { useState } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
  import html2canvas from 'html2canvas';
6
6
 
@@ -130,8 +130,210 @@ function ScreenshotButton({
130
130
  }
131
131
  );
132
132
  }
133
+ var DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024;
134
+ var DEFAULT_MAX_TOTAL_SIZE = 20 * 1024 * 1024;
135
+ function FileUploadButton({
136
+ files,
137
+ onFilesChanged,
138
+ maxFileSize = DEFAULT_MAX_FILE_SIZE,
139
+ maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,
140
+ acceptedTypes = "image/*,.pdf,.doc,.docx,.txt",
141
+ language = "en"
142
+ }) {
143
+ const fileInputRef = useRef(null);
144
+ const [error, setError] = useState(null);
145
+ const translations2 = {
146
+ en: {
147
+ upload: "Upload file",
148
+ remove: "Remove file",
149
+ fileTooLarge: "File is too large",
150
+ totalSizeExceeded: "Total file size exceeded",
151
+ maxFileSize: "Max file size:",
152
+ maxTotalSize: "Max total size:",
153
+ files: "files"
154
+ },
155
+ nl: {
156
+ upload: "Bestand uploaden",
157
+ remove: "Bestand verwijderen",
158
+ fileTooLarge: "Bestand is te groot",
159
+ totalSizeExceeded: "Totale bestandsgrootte overschreden",
160
+ maxFileSize: "Max bestandsgrootte:",
161
+ maxTotalSize: "Max totale grootte:",
162
+ files: "bestanden"
163
+ }
164
+ };
165
+ const t = translations2[language];
166
+ function formatFileSize(bytes) {
167
+ if (bytes < 1024) return bytes + " B";
168
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
169
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
170
+ }
171
+ function handleFileSelect(e) {
172
+ setError(null);
173
+ const selectedFiles = Array.from(e.target.files || []);
174
+ if (selectedFiles.length === 0) return;
175
+ const newFiles = [];
176
+ let totalSize = files.reduce((sum, f) => sum + f.file.size, 0);
177
+ for (const file of selectedFiles) {
178
+ if (file.size > maxFileSize) {
179
+ setError(`${t.fileTooLarge} (${file.name}): ${formatFileSize(file.size)} > ${formatFileSize(maxFileSize)}`);
180
+ continue;
181
+ }
182
+ if (totalSize + file.size > maxTotalSize) {
183
+ setError(`${t.totalSizeExceeded} (${formatFileSize(maxTotalSize)})`);
184
+ continue;
185
+ }
186
+ let preview;
187
+ if (file.type.startsWith("image/")) {
188
+ preview = URL.createObjectURL(file);
189
+ }
190
+ newFiles.push({
191
+ file,
192
+ preview,
193
+ id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
194
+ });
195
+ totalSize += file.size;
196
+ }
197
+ if (newFiles.length > 0) {
198
+ onFilesChanged([...files, ...newFiles]);
199
+ }
200
+ if (fileInputRef.current) {
201
+ fileInputRef.current.value = "";
202
+ }
203
+ }
204
+ function handleRemoveFile(id) {
205
+ const fileToRemove = files.find((f) => f.id === id);
206
+ if (fileToRemove?.preview) {
207
+ URL.revokeObjectURL(fileToRemove.preview);
208
+ }
209
+ onFilesChanged(files.filter((f) => f.id !== id));
210
+ setError(null);
211
+ }
212
+ useEffect(() => {
213
+ return () => {
214
+ files.forEach((file) => {
215
+ if (file.preview) {
216
+ URL.revokeObjectURL(file.preview);
217
+ }
218
+ });
219
+ };
220
+ }, [files]);
221
+ function handleButtonClick() {
222
+ fileInputRef.current?.click();
223
+ }
224
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
225
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
226
+ /* @__PURE__ */ jsx(
227
+ "button",
228
+ {
229
+ type: "button",
230
+ className: "p-2 bg-zinc-800 rounded-md border-transparent hover:bg-zinc-700\n transitions-colors focus:outline-none focus:ring-2\n focus:ring-offset-2 focus:ring-offset-zinc-900 focus:ring-brand-500",
231
+ onClick: handleButtonClick,
232
+ title: t.upload,
233
+ children: /* @__PURE__ */ jsx(Paperclip, { weight: "bold", className: "w-6 h-6" })
234
+ }
235
+ ),
236
+ /* @__PURE__ */ jsx(
237
+ "input",
238
+ {
239
+ ref: fileInputRef,
240
+ type: "file",
241
+ multiple: true,
242
+ accept: acceptedTypes,
243
+ onChange: handleFileSelect,
244
+ className: "hidden"
245
+ }
246
+ ),
247
+ files.map((uploadedFile) => /* @__PURE__ */ jsxs(
248
+ "div",
249
+ {
250
+ className: "flex items-center gap-1 bg-zinc-800 rounded-md px-2 py-1 text-xs",
251
+ children: [
252
+ uploadedFile.preview ? /* @__PURE__ */ jsx(
253
+ "img",
254
+ {
255
+ src: uploadedFile.preview,
256
+ alt: uploadedFile.file.name,
257
+ className: "w-6 h-6 object-cover rounded"
258
+ }
259
+ ) : /* @__PURE__ */ jsx(Paperclip, { className: "w-4 h-4" }),
260
+ /* @__PURE__ */ jsx("span", { className: "text-zinc-300 max-w-[100px] truncate", title: uploadedFile.file.name, children: uploadedFile.file.name }),
261
+ /* @__PURE__ */ jsx(
262
+ "button",
263
+ {
264
+ type: "button",
265
+ onClick: () => handleRemoveFile(uploadedFile.id),
266
+ className: "text-zinc-400 hover:text-zinc-100 transition-colors",
267
+ title: t.remove,
268
+ children: /* @__PURE__ */ jsx(X, { weight: "bold", className: "w-4 h-4" })
269
+ }
270
+ )
271
+ ]
272
+ },
273
+ uploadedFile.id
274
+ ))
275
+ ] }),
276
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-400", children: error }),
277
+ files.length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-xs text-zinc-400", children: [
278
+ files.length,
279
+ " ",
280
+ t.files,
281
+ " (",
282
+ formatFileSize(files.reduce((sum, f) => sum + f.file.size, 0)),
283
+ " / ",
284
+ formatFileSize(maxTotalSize),
285
+ ")"
286
+ ] })
287
+ ] });
288
+ }
133
289
 
134
290
  // src/lib/integrations/github.ts
291
+ async function uploadFileToRepo(token, owner, repo, file, folderPath, defaultBranch) {
292
+ const base64Content = await new Promise((resolve, reject) => {
293
+ const reader = new FileReader();
294
+ reader.onload = () => {
295
+ const result = reader.result;
296
+ const base64 = result.includes(",") ? result.split(",")[1] : result;
297
+ resolve(base64);
298
+ };
299
+ reader.onerror = reject;
300
+ reader.readAsDataURL(file);
301
+ });
302
+ const timestamp = Date.now();
303
+ const randomId = Math.random().toString(36).substring(2, 9);
304
+ const fileExtension = file.name.split(".").pop() || "bin";
305
+ const filename = `feedback-${timestamp}-${randomId}.${fileExtension}`;
306
+ const path = `${folderPath}/${filename}`;
307
+ const uploadUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`;
308
+ const response = await fetch(uploadUrl, {
309
+ method: "PUT",
310
+ headers: {
311
+ "Authorization": `Bearer ${token}`,
312
+ "Accept": "application/vnd.github.v3+json",
313
+ "Content-Type": "application/json",
314
+ "User-Agent": "feedback-vos"
315
+ },
316
+ body: JSON.stringify({
317
+ message: `Add feedback file: ${filename}`,
318
+ content: base64Content,
319
+ branch: defaultBranch
320
+ })
321
+ });
322
+ if (!response.ok) {
323
+ const errorData = await response.json().catch(() => ({}));
324
+ const errorMessage = errorData.message || "Unknown error";
325
+ if (response.status === 409) {
326
+ throw new Error(`File already exists: ${filename}`);
327
+ } else if (response.status === 403) {
328
+ throw new Error(`Permission denied for file: ${filename}`);
329
+ } else if (response.status === 422) {
330
+ throw new Error(`File too large or invalid: ${filename} - ${errorMessage}`);
331
+ }
332
+ throw new Error(`Failed to upload file ${filename} (${response.status}): ${errorMessage}`);
333
+ }
334
+ const rawUrl = `https://github.com/${owner}/${repo}/blob/${defaultBranch}/${path}?raw=true`;
335
+ return rawUrl;
336
+ }
135
337
  async function uploadScreenshotToRepo(token, owner, repo, screenshot, screenshotPath) {
136
338
  const compressedScreenshot = await compressScreenshot(screenshot, 1920, 0.7);
137
339
  const base64Data = compressedScreenshot.split(",")[1];
@@ -306,7 +508,7 @@ async function verifyRepositoryAccess(token, owner, repo) {
306
508
  }
307
509
  async function sendToGitHub(config, data) {
308
510
  const { token, owner, repo, screenshotPath } = config;
309
- const { type, comment, screenshot } = data;
511
+ const { type, comment, screenshot, files } = data;
310
512
  if (!token || !owner || !repo) {
311
513
  throw new Error("GitHub configuration is incomplete. Please provide token, owner, and repo.");
312
514
  }
@@ -329,6 +531,55 @@ Please verify:
329
531
  Please enable Issues in repository Settings \u2192 General \u2192 Features \u2192 Issues`
330
532
  );
331
533
  }
534
+ const repoResponse = await fetch(
535
+ `https://api.github.com/repos/${owner}/${repo}`,
536
+ {
537
+ headers: {
538
+ "Authorization": `Bearer ${token}`,
539
+ "Accept": "application/vnd.github.v3+json",
540
+ "User-Agent": "feedback-vos"
541
+ }
542
+ }
543
+ );
544
+ let defaultBranch = "main";
545
+ if (repoResponse.ok) {
546
+ const repoData = await repoResponse.json();
547
+ defaultBranch = repoData.default_branch || "main";
548
+ }
549
+ const folderPath = screenshotPath || ".feedback-vos";
550
+ const folderCheckUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${folderPath}`;
551
+ try {
552
+ const folderCheck = await fetch(folderCheckUrl, {
553
+ headers: {
554
+ "Authorization": `Bearer ${token}`,
555
+ "Accept": "application/vnd.github.v3+json",
556
+ "User-Agent": "feedback-vos"
557
+ }
558
+ });
559
+ if (folderCheck.status === 404) {
560
+ const readmeContent = btoa("# Feedback Files\n\nThis folder contains files from user feedback.\n");
561
+ try {
562
+ await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${folderPath}/README.md`, {
563
+ method: "PUT",
564
+ headers: {
565
+ "Authorization": `Bearer ${token}`,
566
+ "Accept": "application/vnd.github.v3+json",
567
+ "Content-Type": "application/json",
568
+ "User-Agent": "feedback-vos"
569
+ },
570
+ body: JSON.stringify({
571
+ message: "Create feedback files folder",
572
+ content: readmeContent,
573
+ branch: defaultBranch
574
+ })
575
+ });
576
+ } catch (folderCreateError) {
577
+ console.warn("Could not create folder, proceeding with upload:", folderCreateError);
578
+ }
579
+ }
580
+ } catch (folderError) {
581
+ console.warn("Could not verify/create files folder:", folderError);
582
+ }
332
583
  const title = `[${type}] Feedback`;
333
584
  const ABSOLUTE_MAX_LENGTH = 65536;
334
585
  const BASE_BODY_LENGTH = 50;
@@ -343,7 +594,7 @@ ${limitedComment}`;
343
594
  if (screenshot) {
344
595
  try {
345
596
  console.log("Uploading screenshot to repository...");
346
- console.log("Screenshot path:", screenshotPath || ".feedback-vos");
597
+ console.log("Screenshot path:", folderPath);
347
598
  const screenshotUrl = await uploadScreenshotToRepo(token, owner, repo, screenshot, screenshotPath);
348
599
  console.log("Screenshot uploaded successfully, URL:", screenshotUrl);
349
600
  body += `
@@ -359,6 +610,44 @@ ${limitedComment}`;
359
610
  console.log("Body length after upload failure:", body.length);
360
611
  }
361
612
  }
613
+ if (files && files.length > 0) {
614
+ const uploadedFileUrls = [];
615
+ const failedFiles = [];
616
+ for (const uploadedFile of files) {
617
+ try {
618
+ console.log(`Uploading file: ${uploadedFile.file.name}`);
619
+ const fileUrl = await uploadFileToRepo(
620
+ token,
621
+ owner,
622
+ repo,
623
+ uploadedFile.file,
624
+ folderPath,
625
+ defaultBranch
626
+ );
627
+ uploadedFileUrls.push(fileUrl);
628
+ console.log(`File uploaded successfully: ${fileUrl}`);
629
+ } catch (error) {
630
+ console.error(`Failed to upload file ${uploadedFile.file.name}:`, error);
631
+ failedFiles.push(uploadedFile.file.name);
632
+ }
633
+ }
634
+ if (uploadedFileUrls.length > 0) {
635
+ body += `
636
+
637
+ **Uploaded Files:**
638
+ `;
639
+ uploadedFileUrls.forEach((url2, index) => {
640
+ const fileName = files[index]?.file.name || "Unknown";
641
+ body += `- [${fileName}](${url2})
642
+ `;
643
+ });
644
+ }
645
+ if (failedFiles.length > 0) {
646
+ body += `
647
+
648
+ **Failed to upload:** ${failedFiles.join(", ")}`;
649
+ }
650
+ }
362
651
  const SAFE_MAX_LENGTH = 65e3;
363
652
  console.log(`Issue body length before final check: ${body.length} characters`);
364
653
  if (body.length > SAFE_MAX_LENGTH) {
@@ -515,9 +804,19 @@ function FeedbackContentStep({
515
804
  const t = getTranslations(language);
516
805
  const feedbackTypes = getFeedbackTypes(language);
517
806
  const [screenshot, setScreenshot] = useState(null);
807
+ const [uploadedFiles, setUploadedFiles] = useState([]);
518
808
  const feedbackTypeData = feedbackTypes[feedbackType];
519
809
  const [comment, setComment] = useState("");
520
810
  const [isSendingFeedback, setIsSendingFeedback] = useState(false);
811
+ useEffect(() => {
812
+ return () => {
813
+ uploadedFiles.forEach((file) => {
814
+ if (file.preview) {
815
+ URL.revokeObjectURL(file.preview);
816
+ }
817
+ });
818
+ };
819
+ }, [uploadedFiles]);
521
820
  async function handleSubmitFeedback(e) {
522
821
  e.preventDefault();
523
822
  setIsSendingFeedback(true);
@@ -525,7 +824,8 @@ function FeedbackContentStep({
525
824
  const feedbackData = {
526
825
  type: feedbackType,
527
826
  comment,
528
- screenshot
827
+ screenshot,
828
+ files: uploadedFiles
529
829
  };
530
830
  if (integration === "github") {
531
831
  await sendToGitHub(githubConfig, feedbackData);
@@ -546,12 +846,12 @@ function FeedbackContentStep({
546
846
  "button",
547
847
  {
548
848
  type: "button",
549
- className: "absolute top-5 left-5 text-zinc-400 hover:text-zinc-100",
849
+ className: "absolute top-5 left-5 text-zinc-400 hover:text-zinc-100 z-10",
550
850
  onClick: onFeedbackRestartRequest,
551
851
  children: /* @__PURE__ */ jsx(ArrowLeft, { weight: "bold", className: "w-4 h-4" })
552
852
  }
553
853
  ),
554
- /* @__PURE__ */ jsxs("span", { className: "text-xl leading-6 flex items-center gap-2 mt-2", children: [
854
+ /* @__PURE__ */ jsxs("span", { className: "text-xl leading-6 flex items-center gap-2 mt-2 pl-10", children: [
555
855
  /* @__PURE__ */ jsx(
556
856
  "img",
557
857
  {
@@ -573,6 +873,14 @@ function FeedbackContentStep({
573
873
  onChange: (e) => setComment(e.target.value)
574
874
  }
575
875
  ),
876
+ /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
877
+ FileUploadButton,
878
+ {
879
+ files: uploadedFiles,
880
+ onFilesChanged: setUploadedFiles,
881
+ language
882
+ }
883
+ ) }),
576
884
  /* @__PURE__ */ jsxs("footer", { className: " flex gap-2 mt-2", children: [
577
885
  /* @__PURE__ */ jsx(
578
886
  ScreenshotButton,