@drawnagency/primitives 0.1.40 → 0.1.42
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/shell/BugReportFAB.d.ts +6 -0
- package/dist/components/shell/BugReportFAB.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/shell/BugReportFAB.tsx +345 -0
- package/src/components/shell/EditorShell.tsx +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BugReportFAB.d.ts","sourceRoot":"","sources":["../../../src/components/shell/BugReportFAB.tsx"],"names":[],"mappings":"AAMA,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AA0BD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,kDAsT7C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAsDjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAwoBP"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
import { EditorModal } from "./EditorModal";
|
|
4
|
+
import { useEditorContext } from "./EditorContext";
|
|
5
|
+
import { env } from "../../lib/env";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
siteId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Category = "Visual" | "Unexpected Behavior" | "Media / Media Library" | "Save / Publish" | "Feature Request" | "Other";
|
|
12
|
+
|
|
13
|
+
const CATEGORIES: Category[] = [
|
|
14
|
+
"Visual",
|
|
15
|
+
"Unexpected Behavior",
|
|
16
|
+
"Media / Media Library",
|
|
17
|
+
"Save / Publish",
|
|
18
|
+
"Feature Request",
|
|
19
|
+
"Other",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
interface CapturedContext {
|
|
23
|
+
url: string;
|
|
24
|
+
section_id: null;
|
|
25
|
+
user_agent: string;
|
|
26
|
+
viewport_width: number;
|
|
27
|
+
viewport_height: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ImagePreview {
|
|
31
|
+
dataUri: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function BugReportFAB({ siteId }: Props) {
|
|
36
|
+
const { isEditMode, historyState } = useEditorContext();
|
|
37
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
38
|
+
const [category, setCategory] = useState<Category>("Visual");
|
|
39
|
+
const [isCritical, setIsCritical] = useState(false);
|
|
40
|
+
const [description, setDescription] = useState("");
|
|
41
|
+
const [images, setImages] = useState<ImagePreview[]>([]);
|
|
42
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
44
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
45
|
+
const [descriptionError, setDescriptionError] = useState<string | null>(null);
|
|
46
|
+
const [toast, setToast] = useState<string | null>(null);
|
|
47
|
+
const capturedContextRef = useRef<CapturedContext | null>(null);
|
|
48
|
+
const dropZoneRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
|
|
50
|
+
const resetForm = useCallback(() => {
|
|
51
|
+
setCategory("Visual");
|
|
52
|
+
setIsCritical(false);
|
|
53
|
+
setDescription("");
|
|
54
|
+
setImages([]);
|
|
55
|
+
setSubmitError(null);
|
|
56
|
+
setDescriptionError(null);
|
|
57
|
+
capturedContextRef.current = null;
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const handleOpen = useCallback(() => {
|
|
61
|
+
capturedContextRef.current = {
|
|
62
|
+
url: window.location.pathname,
|
|
63
|
+
section_id: null,
|
|
64
|
+
user_agent: navigator.userAgent,
|
|
65
|
+
viewport_width: window.innerWidth,
|
|
66
|
+
viewport_height: window.innerHeight,
|
|
67
|
+
};
|
|
68
|
+
setIsOpen(true);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const handleClose = useCallback(() => {
|
|
72
|
+
setIsOpen(false);
|
|
73
|
+
resetForm();
|
|
74
|
+
}, [resetForm]);
|
|
75
|
+
|
|
76
|
+
const fileToDataUri = useCallback((file: File): Promise<string> => {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const reader = new FileReader();
|
|
79
|
+
reader.onload = () => resolve(reader.result as string);
|
|
80
|
+
reader.onerror = reject;
|
|
81
|
+
reader.readAsDataURL(file);
|
|
82
|
+
});
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const addImageFiles = useCallback(async (files: File[]) => {
|
|
86
|
+
const imageFiles = files.filter((f) => f.type.startsWith("image/"));
|
|
87
|
+
if (imageFiles.length === 0) return;
|
|
88
|
+
const newPreviews = await Promise.all(
|
|
89
|
+
imageFiles.map(async (file) => ({
|
|
90
|
+
dataUri: await fileToDataUri(file),
|
|
91
|
+
name: file.name,
|
|
92
|
+
})),
|
|
93
|
+
);
|
|
94
|
+
setImages((prev) => [...prev, ...newPreviews].slice(0, 5));
|
|
95
|
+
}, [fileToDataUri]);
|
|
96
|
+
|
|
97
|
+
const handleRemoveImage = useCallback((index: number) => {
|
|
98
|
+
setImages((prev) => prev.filter((_, i) => i !== index));
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
setIsDragOver(true);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
107
|
+
if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) {
|
|
108
|
+
setIsDragOver(false);
|
|
109
|
+
}
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
setIsDragOver(false);
|
|
115
|
+
const files = Array.from(e.dataTransfer.files);
|
|
116
|
+
await addImageFiles(files);
|
|
117
|
+
}, [addImageFiles]);
|
|
118
|
+
|
|
119
|
+
// Document-level paste listener when modal is open
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!isOpen) return;
|
|
122
|
+
|
|
123
|
+
const handlePaste = async (e: ClipboardEvent) => {
|
|
124
|
+
const items = e.clipboardData?.items;
|
|
125
|
+
if (!items) return;
|
|
126
|
+
const files: File[] = [];
|
|
127
|
+
for (const item of Array.from(items)) {
|
|
128
|
+
if (item.type.startsWith("image/")) {
|
|
129
|
+
const file = item.getAsFile();
|
|
130
|
+
if (file) files.push(file);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (files.length > 0) {
|
|
134
|
+
await addImageFiles(files);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
document.addEventListener("paste", handlePaste);
|
|
139
|
+
return () => document.removeEventListener("paste", handlePaste);
|
|
140
|
+
}, [isOpen, addImageFiles]);
|
|
141
|
+
|
|
142
|
+
const handleSubmit = useCallback(async () => {
|
|
143
|
+
if (!description.trim()) {
|
|
144
|
+
setDescriptionError("Please describe the issue.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
setDescriptionError(null);
|
|
148
|
+
setSubmitError(null);
|
|
149
|
+
setIsSubmitting(true);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const supabase = createClient(
|
|
153
|
+
env("SUPABASE_URL"),
|
|
154
|
+
env("SUPABASE_ANON_KEY"),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const { data: userData } = await supabase.auth.getUser();
|
|
158
|
+
const userId = userData?.user?.id ?? null;
|
|
159
|
+
|
|
160
|
+
const { error } = await supabase.from("bug_reports").insert({
|
|
161
|
+
site_id: siteId,
|
|
162
|
+
user_id: userId,
|
|
163
|
+
category,
|
|
164
|
+
is_critical: isCritical,
|
|
165
|
+
description: description.trim(),
|
|
166
|
+
images: images.map((img) => img.dataUri),
|
|
167
|
+
context: capturedContextRef.current,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (error) {
|
|
171
|
+
setSubmitError(error.message);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
handleClose();
|
|
176
|
+
setToast("Bug report submitted");
|
|
177
|
+
setTimeout(() => setToast(null), 3000);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
setSubmitError(err instanceof Error ? err.message : "Submission failed");
|
|
180
|
+
} finally {
|
|
181
|
+
setIsSubmitting(false);
|
|
182
|
+
}
|
|
183
|
+
}, [siteId, category, isCritical, description, images, handleClose]);
|
|
184
|
+
|
|
185
|
+
const visible = isEditMode && historyState === null;
|
|
186
|
+
|
|
187
|
+
if (!visible && !isOpen && !toast) return null;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
{/* FAB button */}
|
|
192
|
+
{visible && (
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
onClick={handleOpen}
|
|
196
|
+
aria-label="Report a bug"
|
|
197
|
+
className="cursor-pointer fixed bottom-16 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
198
|
+
>
|
|
199
|
+
{/* Bug / beetle SVG icon */}
|
|
200
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
201
|
+
<path d="M19 8h-1.26A6.003 6.003 0 0012 4a6.003 6.003 0 00-5.74 4H5a1 1 0 000 2h1.05A6.04 6.04 0 006 12H5a1 1 0 000 2h1c0 .34.03.67.08 1H5a1 1 0 000 2h1.26a6 6 0 0011.48 0H19a1 1 0 000-2h-1.08c.05-.33.08-.66.08-1h1a1 1 0 000-2h-1a6.04 6.04 0 00-.05-2H19a1 1 0 000-2zM12 6c1.7 0 3.16.88 4 2.19V8a1 1 0 10-2 0v.04A3.97 3.97 0 0012 8a3.97 3.97 0 00-2 .04V8a1 1 0 10-2 0v.19A4.69 4.69 0 018 6a4 4 0 014 0zm0 14a4 4 0 01-4-4v-4a4 4 0 018 0v4a4 4 0 01-4 4z"/>
|
|
202
|
+
</svg>
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Modal */}
|
|
207
|
+
<EditorModal
|
|
208
|
+
isOpen={isOpen}
|
|
209
|
+
onClose={handleClose}
|
|
210
|
+
title="Report a Bug"
|
|
211
|
+
>
|
|
212
|
+
<div className="space-y-4">
|
|
213
|
+
{/* Category */}
|
|
214
|
+
<div>
|
|
215
|
+
<label className="mb-1 block text-sm font-medium text-base-contrast" htmlFor="bug-category">
|
|
216
|
+
Category
|
|
217
|
+
</label>
|
|
218
|
+
<select
|
|
219
|
+
id="bug-category"
|
|
220
|
+
value={category}
|
|
221
|
+
onChange={(e) => setCategory(e.target.value as Category)}
|
|
222
|
+
className="w-full rounded-md border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast"
|
|
223
|
+
>
|
|
224
|
+
{CATEGORIES.map((cat) => (
|
|
225
|
+
<option key={cat} value={cat}>{cat}</option>
|
|
226
|
+
))}
|
|
227
|
+
</select>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* Critical toggle */}
|
|
231
|
+
<div className="flex items-center gap-3">
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
role="switch"
|
|
235
|
+
aria-checked={isCritical}
|
|
236
|
+
onClick={() => setIsCritical((prev) => !prev)}
|
|
237
|
+
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
|
|
238
|
+
isCritical
|
|
239
|
+
? "bg-red-600 focus-visible:ring-red-600"
|
|
240
|
+
: "bg-base-200 focus-visible:ring-base-200"
|
|
241
|
+
}`}
|
|
242
|
+
>
|
|
243
|
+
<span
|
|
244
|
+
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform ${
|
|
245
|
+
isCritical ? "translate-x-5" : "translate-x-0.5"
|
|
246
|
+
} mt-0.5`}
|
|
247
|
+
/>
|
|
248
|
+
</button>
|
|
249
|
+
<span className="text-sm text-base-contrast">Critical / Blocking</span>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Description */}
|
|
253
|
+
<div>
|
|
254
|
+
<label className="mb-1 block text-sm font-medium text-base-contrast" htmlFor="bug-description">
|
|
255
|
+
Description
|
|
256
|
+
</label>
|
|
257
|
+
<textarea
|
|
258
|
+
id="bug-description"
|
|
259
|
+
rows={4}
|
|
260
|
+
value={description}
|
|
261
|
+
onChange={(e) => {
|
|
262
|
+
setDescription(e.target.value);
|
|
263
|
+
if (descriptionError && e.target.value.trim()) {
|
|
264
|
+
setDescriptionError(null);
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
placeholder="Describe the issue..."
|
|
268
|
+
className="w-full rounded-md border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast placeholder:text-base-contrast/50 resize-none"
|
|
269
|
+
/>
|
|
270
|
+
{descriptionError && (
|
|
271
|
+
<p className="mt-1 text-xs text-red-600">{descriptionError}</p>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{/* Screenshots drop zone */}
|
|
276
|
+
<div>
|
|
277
|
+
<label className="mb-1 block text-sm font-medium text-base-contrast">
|
|
278
|
+
Screenshots
|
|
279
|
+
</label>
|
|
280
|
+
<div
|
|
281
|
+
ref={dropZoneRef}
|
|
282
|
+
onDragOver={handleDragOver}
|
|
283
|
+
onDragLeave={handleDragLeave}
|
|
284
|
+
onDrop={handleDrop}
|
|
285
|
+
className={`rounded-md border-2 border-dashed px-4 py-6 text-center transition-colors ${
|
|
286
|
+
isDragOver
|
|
287
|
+
? "border-primary bg-primary/5"
|
|
288
|
+
: "border-base-200 bg-base-accent/30"
|
|
289
|
+
}`}
|
|
290
|
+
>
|
|
291
|
+
<p className="text-sm text-base-contrast/60">
|
|
292
|
+
Drop images here or paste from clipboard
|
|
293
|
+
</p>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Thumbnail previews */}
|
|
297
|
+
{images.length > 0 && (
|
|
298
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
299
|
+
{images.map((img, index) => (
|
|
300
|
+
<div key={index} className="group relative h-16 w-16">
|
|
301
|
+
<img
|
|
302
|
+
src={img.dataUri}
|
|
303
|
+
alt={img.name}
|
|
304
|
+
className="h-16 w-16 rounded object-cover border border-base-200"
|
|
305
|
+
/>
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
onClick={() => handleRemoveImage(index)}
|
|
309
|
+
aria-label={`Remove screenshot ${img.name}`}
|
|
310
|
+
className="absolute -top-1.5 -right-1.5 hidden h-5 w-5 items-center justify-center rounded-full bg-red-600 text-white text-xs group-hover:flex"
|
|
311
|
+
>
|
|
312
|
+
×
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Submit error */}
|
|
321
|
+
{submitError && (
|
|
322
|
+
<p className="text-sm text-red-600">{submitError}</p>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{/* Submit button */}
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={handleSubmit}
|
|
329
|
+
disabled={isSubmitting}
|
|
330
|
+
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-contrast hover:opacity-90 transition-opacity disabled:opacity-60"
|
|
331
|
+
>
|
|
332
|
+
{isSubmitting ? "Submitting..." : "Submit"}
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
</EditorModal>
|
|
336
|
+
|
|
337
|
+
{/* Toast */}
|
|
338
|
+
{toast && (
|
|
339
|
+
<div className="fixed bottom-4 left-1/2 z-[60] -translate-x-1/2 rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-lg">
|
|
340
|
+
{toast}
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
@@ -21,6 +21,7 @@ import { ensureSectionsRegistered } from "../sections/register";
|
|
|
21
21
|
import { getSection, getAllSections } from "../../lib/registry";
|
|
22
22
|
|
|
23
23
|
ensureSectionsRegistered();
|
|
24
|
+
import { BugReportFAB } from "./BugReportFAB";
|
|
24
25
|
import { SectionWrapper } from "../editor/SectionWrapper";
|
|
25
26
|
import { SectionOrderingModal } from "../editor/SectionOrderingModal";
|
|
26
27
|
import { SectionLayout } from "../sections/SectionLayout";
|
|
@@ -633,6 +634,8 @@ export default function EditorShell({
|
|
|
633
634
|
onOrderingClick={() => setShowOrderingModal(true)}
|
|
634
635
|
/>
|
|
635
636
|
|
|
637
|
+
<BugReportFAB siteId={siteId} />
|
|
638
|
+
|
|
636
639
|
<HistoryOrEditorContent sections={sections}>
|
|
637
640
|
<EditorContent
|
|
638
641
|
sections={sections}
|