ideasbugs 0.5.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +114 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +339 -0
  5. data/Rakefile +19 -0
  6. data/app/controllers/ideasbugs/application_controller.rb +15 -0
  7. data/app/controllers/ideasbugs/feedbacks_controller.rb +156 -0
  8. data/app/controllers/ideasbugs/screenshots_controller.rb +27 -0
  9. data/app/helpers/ideasbugs/widget_helper.rb +18 -0
  10. data/app/models/ideasbugs/application_record.rb +7 -0
  11. data/app/models/ideasbugs/feedback.rb +30 -0
  12. data/app/views/ideasbugs/feedbacks/index.html.erb +78 -0
  13. data/app/views/ideasbugs/feedbacks/show.html.erb +63 -0
  14. data/app/views/layouts/ideasbugs/application.html.erb +76 -0
  15. data/config/locales/ideasbugs.ar.yml +24 -0
  16. data/config/locales/ideasbugs.bg.yml +24 -0
  17. data/config/locales/ideasbugs.bn.yml +24 -0
  18. data/config/locales/ideasbugs.de.yml +24 -0
  19. data/config/locales/ideasbugs.el.yml +24 -0
  20. data/config/locales/ideasbugs.en.yml +47 -0
  21. data/config/locales/ideasbugs.es.yml +24 -0
  22. data/config/locales/ideasbugs.fr.yml +24 -0
  23. data/config/locales/ideasbugs.hi.yml +24 -0
  24. data/config/locales/ideasbugs.hr.yml +24 -0
  25. data/config/locales/ideasbugs.id.yml +24 -0
  26. data/config/locales/ideasbugs.it.yml +24 -0
  27. data/config/locales/ideasbugs.ja.yml +24 -0
  28. data/config/locales/ideasbugs.ko.yml +24 -0
  29. data/config/locales/ideasbugs.lb.yml +24 -0
  30. data/config/locales/ideasbugs.nl.yml +24 -0
  31. data/config/locales/ideasbugs.pl.yml +24 -0
  32. data/config/locales/ideasbugs.pt.yml +24 -0
  33. data/config/locales/ideasbugs.ro.yml +24 -0
  34. data/config/locales/ideasbugs.ru.yml +24 -0
  35. data/config/locales/ideasbugs.th.yml +24 -0
  36. data/config/locales/ideasbugs.tr.yml +24 -0
  37. data/config/locales/ideasbugs.uk.yml +24 -0
  38. data/config/locales/ideasbugs.ur.yml +24 -0
  39. data/config/locales/ideasbugs.vi.yml +24 -0
  40. data/config/locales/ideasbugs.zh-CN.yml +24 -0
  41. data/config/routes.rb +12 -0
  42. data/lib/generators/ideasbugs/install/install_generator.rb +41 -0
  43. data/lib/generators/ideasbugs/install/templates/create_ideasbugs_feedbacks.rb.tt +21 -0
  44. data/lib/generators/ideasbugs/install/templates/initializer.rb +47 -0
  45. data/lib/ideasbugs/configuration.rb +93 -0
  46. data/lib/ideasbugs/engine.rb +13 -0
  47. data/lib/ideasbugs/version.rb +5 -0
  48. data/lib/ideasbugs/widget.js +387 -0
  49. data/lib/ideasbugs/widget.rb +109 -0
  50. data/lib/ideasbugs.rb +34 -0
  51. metadata +113 -0
@@ -0,0 +1,387 @@
1
+ /*
2
+ * ideasbugs widget — self-contained, no framework, no build step.
3
+ *
4
+ * Reads its config from the <script type="application/json"
5
+ * data-ideasbugs-config> the server renders — re-read on every render so
6
+ * a Turbo visit always reflects the current page's config.
7
+ *
8
+ * A floating button (or any host element carrying `data-ideasbugs-open`)
9
+ * opens a small modal form: type, optional section, message, optional
10
+ * screenshots. The form POSTs multipart form data to the mounted engine with
11
+ * the page's CSRF token. Esc or the backdrop closes it.
12
+ */
13
+ (function () {
14
+ "use strict";
15
+
16
+ var config = readConfig();
17
+ if (!config || window.__ideasbugsLoaded) return;
18
+ window.__ideasbugsLoaded = true;
19
+
20
+ var Z = 2147483000;
21
+ var overlay = null;
22
+ var lastFocused = null;
23
+
24
+ function ready(fn) {
25
+ if (document.readyState === "loading") {
26
+ document.addEventListener("DOMContentLoaded", fn);
27
+ } else {
28
+ fn();
29
+ }
30
+ }
31
+
32
+ ready(function () {
33
+ // Document-level listeners survive Turbo navigations; register them once.
34
+ document.addEventListener("keydown", handleKeydown);
35
+ document.addEventListener("click", handleOpenerClick, true);
36
+
37
+ // The button lives in <body>, which Turbo replaces on every visit, so
38
+ // re-run the per-page setup on each visit. render() also runs now for the
39
+ // initial (or non-Turbo) load.
40
+ render();
41
+ document.addEventListener("turbo:load", render);
42
+ });
43
+
44
+ function readConfig() {
45
+ var el = document.querySelector("script[data-ideasbugs-config]");
46
+ if (!el) return null;
47
+ try {
48
+ return JSON.parse(el.textContent);
49
+ } catch (e) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function render() {
55
+ // Re-read on each visit: the config block is data (not an executed
56
+ // script), so it reflects the page Turbo just rendered.
57
+ config = readConfig() || config;
58
+ injectStyles();
59
+ if (config.showButton !== false) buildButton();
60
+ }
61
+
62
+ function handleOpenerClick(event) {
63
+ var opener = event.target && event.target.closest
64
+ ? event.target.closest("[data-ideasbugs-open]")
65
+ : null;
66
+ if (!opener) return;
67
+ event.preventDefault();
68
+ event.stopPropagation();
69
+ openForm();
70
+ }
71
+
72
+ function handleKeydown(event) {
73
+ if (event.key === "Escape" && overlay) closeForm();
74
+ }
75
+
76
+ // --- floating button --------------------------------------------------------
77
+
78
+ function buildButton() {
79
+ if (document.getElementById("idb-button")) return;
80
+ var button = document.createElement("button");
81
+ button.id = "idb-button";
82
+ button.type = "button";
83
+ button.setAttribute("data-ideasbugs-open", "");
84
+ button.textContent = config.buttonLabel || config.labels.button;
85
+ document.body.appendChild(button);
86
+ }
87
+
88
+ // --- form -------------------------------------------------------------------
89
+
90
+ function openForm() {
91
+ if (overlay) return;
92
+ lastFocused = document.activeElement;
93
+
94
+ overlay = document.createElement("div");
95
+ overlay.id = "idb-overlay";
96
+ overlay.addEventListener("mousedown", function (event) {
97
+ if (event.target === overlay) closeForm();
98
+ });
99
+
100
+ var dialog = document.createElement("div");
101
+ dialog.id = "idb-dialog";
102
+ dialog.setAttribute("role", "dialog");
103
+ dialog.setAttribute("aria-modal", "true");
104
+ dialog.setAttribute("aria-labelledby", "idb-title");
105
+ if (config.rtl) dialog.setAttribute("dir", "rtl");
106
+
107
+ dialog.appendChild(header());
108
+ dialog.appendChild(form());
109
+ overlay.appendChild(dialog);
110
+ document.body.appendChild(overlay);
111
+
112
+ // Keep Tab (and Shift+Tab) cycling inside the dialog while it is open.
113
+ overlay.addEventListener("keydown", trapFocus);
114
+
115
+ var first = dialog.querySelector("select, textarea, input");
116
+ if (first) first.focus();
117
+ }
118
+
119
+ function closeForm() {
120
+ if (!overlay) return;
121
+ overlay.remove();
122
+ overlay = null;
123
+ if (lastFocused && lastFocused.focus) lastFocused.focus();
124
+ }
125
+
126
+ function trapFocus(event) {
127
+ if (event.key !== "Tab" || !overlay) return;
128
+ var focusable = overlay.querySelectorAll("button, select, textarea, input, a[href]");
129
+ if (!focusable.length) return;
130
+ var first = focusable[0];
131
+ var last = focusable[focusable.length - 1];
132
+ if (event.shiftKey && document.activeElement === first) {
133
+ event.preventDefault();
134
+ last.focus();
135
+ } else if (!event.shiftKey && document.activeElement === last) {
136
+ event.preventDefault();
137
+ first.focus();
138
+ }
139
+ }
140
+
141
+ function header() {
142
+ var head = document.createElement("div");
143
+ head.className = "idb-head";
144
+
145
+ var title = document.createElement("h2");
146
+ title.id = "idb-title";
147
+ title.textContent = config.labels.title;
148
+
149
+ var close = document.createElement("button");
150
+ close.type = "button";
151
+ close.className = "idb-x";
152
+ close.setAttribute("aria-label", config.labels.close);
153
+ close.textContent = "×";
154
+ close.addEventListener("click", closeForm);
155
+
156
+ head.appendChild(title);
157
+ head.appendChild(close);
158
+ return head;
159
+ }
160
+
161
+ function form() {
162
+ var form = document.createElement("form");
163
+ form.addEventListener("submit", function (event) {
164
+ event.preventDefault();
165
+ submit(form);
166
+ });
167
+
168
+ if (config.kinds.length > 1) form.appendChild(kindField());
169
+ if (config.sections.length > 0) form.appendChild(sectionField());
170
+ form.appendChild(messageField());
171
+ if (config.screenshots.enabled) form.appendChild(screenshotsField());
172
+
173
+ var error = document.createElement("p");
174
+ error.className = "idb-error";
175
+ error.hidden = true;
176
+ form.appendChild(error);
177
+
178
+ var actions = document.createElement("div");
179
+ actions.className = "idb-actions";
180
+
181
+ var cancel = document.createElement("button");
182
+ cancel.type = "button";
183
+ cancel.className = "idb-secondary";
184
+ cancel.textContent = config.labels.cancel;
185
+ cancel.addEventListener("click", closeForm);
186
+
187
+ var save = document.createElement("button");
188
+ save.type = "submit";
189
+ save.className = "idb-primary";
190
+ save.textContent = config.labels.submit;
191
+
192
+ actions.appendChild(cancel);
193
+ actions.appendChild(save);
194
+ form.appendChild(actions);
195
+ return form;
196
+ }
197
+
198
+ function field(labelText, control) {
199
+ var wrap = document.createElement("label");
200
+ wrap.className = "idb-field";
201
+ var caption = document.createElement("span");
202
+ caption.textContent = labelText;
203
+ wrap.appendChild(caption);
204
+ wrap.appendChild(control);
205
+ return wrap;
206
+ }
207
+
208
+ function kindField() {
209
+ var select = document.createElement("select");
210
+ select.name = "kind";
211
+ config.kinds.forEach(function (kind) {
212
+ var option = document.createElement("option");
213
+ option.value = kind.value;
214
+ option.textContent = kind.label;
215
+ select.appendChild(option);
216
+ });
217
+ return field(config.labels.kind, select);
218
+ }
219
+
220
+ function sectionField() {
221
+ var select = document.createElement("select");
222
+ select.name = "section";
223
+ var blank = document.createElement("option");
224
+ blank.value = "";
225
+ blank.textContent = config.labels.sectionAny;
226
+ select.appendChild(blank);
227
+ config.sections.forEach(function (section) {
228
+ var option = document.createElement("option");
229
+ option.value = section;
230
+ option.textContent = section;
231
+ select.appendChild(option);
232
+ });
233
+ return field(config.labels.section, select);
234
+ }
235
+
236
+ function messageField() {
237
+ var textarea = document.createElement("textarea");
238
+ textarea.name = "message";
239
+ textarea.rows = 5;
240
+ textarea.placeholder = config.labels.messagePlaceholder;
241
+ return field(config.labels.message, textarea);
242
+ }
243
+
244
+ function screenshotsField() {
245
+ var input = document.createElement("input");
246
+ input.type = "file";
247
+ input.name = "screenshots";
248
+ input.multiple = true;
249
+ input.accept = "image/*";
250
+ var wrap = field(config.labels.screenshots, input);
251
+ var hint = document.createElement("span");
252
+ hint.className = "idb-hint";
253
+ hint.textContent = config.labels.screenshotsHint;
254
+ wrap.appendChild(hint);
255
+ return wrap;
256
+ }
257
+
258
+ // --- submit -------------------------------------------------------------------
259
+
260
+ function submit(form) {
261
+ var message = form.querySelector("textarea[name=message]").value.trim();
262
+ if (!message) return showError(form, config.labels.errorBlank);
263
+
264
+ var files = fileList(form);
265
+ if (files.length > config.screenshots.max) {
266
+ return showError(form, config.labels.errorTooMany);
267
+ }
268
+ for (var i = 0; i < files.length; i++) {
269
+ if (files[i].size > config.screenshots.maxSize) {
270
+ return showError(form, config.labels.errorTooLarge);
271
+ }
272
+ }
273
+
274
+ var data = new FormData();
275
+ var kindSelect = form.querySelector("select[name=kind]");
276
+ data.append("feedback[kind]", kindSelect ? kindSelect.value : config.kinds[0].value);
277
+ var sectionSelect = form.querySelector("select[name=section]");
278
+ if (sectionSelect && sectionSelect.value) data.append("feedback[section]", sectionSelect.value);
279
+ data.append("feedback[message]", message);
280
+ data.append("feedback[page_url]", window.location.href);
281
+ files.forEach(function (file) {
282
+ data.append("feedback[screenshots][]", file);
283
+ });
284
+
285
+ var save = form.querySelector(".idb-primary");
286
+ save.disabled = true;
287
+
288
+ fetch(config.endpoint, {
289
+ method: "POST",
290
+ headers: csrfHeaders(),
291
+ body: data,
292
+ credentials: "same-origin"
293
+ })
294
+ .then(function (response) {
295
+ if (response.ok) return thanks();
296
+ return response
297
+ .json()
298
+ .catch(function () { return {}; })
299
+ .then(function (body) {
300
+ var messages = body && body.errors;
301
+ showError(form, (messages && messages[0]) || config.labels.errorSave);
302
+ });
303
+ })
304
+ .catch(function () {
305
+ showError(form, config.labels.errorSave);
306
+ })
307
+ .finally(function () {
308
+ save.disabled = false;
309
+ });
310
+ }
311
+
312
+ function fileList(form) {
313
+ var input = form.querySelector("input[type=file]");
314
+ return input ? Array.prototype.slice.call(input.files) : [];
315
+ }
316
+
317
+ function csrfHeaders() {
318
+ var meta = document.querySelector('meta[name="csrf-token"]');
319
+ return meta ? { "X-CSRF-Token": meta.content } : {};
320
+ }
321
+
322
+ function showError(form, text) {
323
+ var error = form.querySelector(".idb-error");
324
+ error.textContent = text;
325
+ error.hidden = false;
326
+ }
327
+
328
+ function thanks() {
329
+ if (!overlay) return;
330
+ var dialog = overlay.querySelector("#idb-dialog");
331
+ dialog.textContent = "";
332
+ var note = document.createElement("p");
333
+ note.className = "idb-thanks";
334
+ note.textContent = config.labels.thanks;
335
+ dialog.appendChild(note);
336
+ setTimeout(closeForm, 1600);
337
+ }
338
+
339
+ // --- styles -------------------------------------------------------------------
340
+
341
+ function injectStyles() {
342
+ if (document.getElementById("idb-styles")) return;
343
+ var css = [
344
+ "#idb-button{position:fixed;bottom:16px;right:16px;z-index:" + Z + ";",
345
+ "padding:10px 16px;border:0;border-radius:999px;cursor:pointer;",
346
+ "background:#2563eb;color:#fff;font:600 14px/1 system-ui,-apple-system,sans-serif;",
347
+ "box-shadow:0 4px 14px rgba(0,0,0,.25)}",
348
+ "#idb-button:hover{background:#1d4ed8}",
349
+ "#idb-overlay{position:fixed;inset:0;z-index:" + (Z + 1) + ";background:rgba(0,0,0,.45);",
350
+ "display:flex;align-items:center;justify-content:center;padding:16px}",
351
+ "#idb-dialog{width:100%;max-width:420px;max-height:90vh;overflow:auto;",
352
+ "background:#fff;color:#1c2024;border-radius:14px;padding:20px;",
353
+ "font:14px/1.5 system-ui,-apple-system,sans-serif;box-shadow:0 20px 60px rgba(0,0,0,.35)}",
354
+ "#idb-dialog .idb-head{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px}",
355
+ "#idb-dialog h2{margin:0;font-size:17px}",
356
+ "#idb-dialog .idb-x{border:0;background:none;font-size:22px;line-height:1;cursor:pointer;color:inherit;padding:2px 6px}",
357
+ "#idb-dialog .idb-field{display:block;margin-bottom:12px}",
358
+ "#idb-dialog .idb-field>span{display:block;margin-bottom:4px;font-weight:600}",
359
+ "#idb-dialog select,#idb-dialog textarea,#idb-dialog input[type=file]{width:100%;box-sizing:border-box;",
360
+ "padding:8px;border:1px solid #d1d5db;border-radius:8px;background:inherit;color:inherit;font:inherit}",
361
+ "#idb-dialog input[type=file]{padding:6px;color:#6b7280;font-size:13px}",
362
+ "#idb-dialog input[type=file]::file-selector-button{margin-inline-end:10px;padding:6px 12px;",
363
+ "border:1px solid #d1d5db;border-radius:6px;background:none;color:#1c2024;font:inherit;cursor:pointer}",
364
+ "#idb-dialog textarea{resize:vertical}",
365
+ "#idb-dialog .idb-hint{display:block;margin-top:4px;font-size:12px;color:#6b7280;font-weight:400}",
366
+ "#idb-dialog .idb-error{color:#dc2626;margin:0 0 12px}",
367
+ "#idb-dialog .idb-actions{display:flex;justify-content:flex-end;gap:8px}",
368
+ "#idb-dialog button{padding:8px 14px;border-radius:8px;cursor:pointer;font:inherit}",
369
+ "#idb-dialog .idb-secondary{border:1px solid #d1d5db;background:none;color:inherit}",
370
+ "#idb-dialog .idb-primary{border:0;background:#2563eb;color:#fff;font-weight:600}",
371
+ "#idb-dialog .idb-primary:disabled{opacity:.6;cursor:default}",
372
+ "#idb-dialog .idb-thanks{margin:8px 0;text-align:center;font-size:15px}",
373
+ "@media (prefers-color-scheme:dark){",
374
+ "#idb-dialog{background:#1a1f26;color:#e6e8ea}",
375
+ "#idb-dialog select,#idb-dialog textarea,#idb-dialog input[type=file]{border-color:#2a313a}",
376
+ "#idb-dialog input[type=file]{color:#9aa2ab}",
377
+ "#idb-dialog input[type=file]::file-selector-button{border-color:#2a313a;color:#e6e8ea}",
378
+ "#idb-dialog .idb-secondary{border-color:#2a313a}",
379
+ "#idb-dialog .idb-hint{color:#9aa2ab}",
380
+ "}"
381
+ ].join("");
382
+ var style = document.createElement("style");
383
+ style.id = "idb-styles";
384
+ style.textContent = css;
385
+ document.head.appendChild(style);
386
+ }
387
+ })();
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Ideasbugs
6
+ # Serves the self-contained browser widget. The JavaScript is plain ES (no
7
+ # framework, no build step) and styles itself inline, so it drops into any
8
+ # Rails app regardless of its CSS or JS setup. It is inlined into the page
9
+ # rather than served as a separate asset to avoid any dependency on the
10
+ # host's asset pipeline — and it lives under lib/ (not app/assets/) so a
11
+ # host that *does* run a pipeline never ingests it either: nothing lands in
12
+ # the host's asset namespace or precompiled output.
13
+ module Widget
14
+ SOURCE = File.expand_path('widget.js', __dir__)
15
+
16
+ # Right-to-left scripts, so the form renders mirrored for those locales.
17
+ # Matched on the language subtag, so region variants ("ar-EG") count too.
18
+ RTL_LANGUAGES = %w[ar arc ckb dv fa ha he ks ku ps sd ug ur yi].freeze
19
+
20
+ class << self
21
+ def javascript
22
+ @javascript ||= File.read(SOURCE)
23
+ end
24
+
25
+ # The two <script> tags the helper renders.
26
+ #
27
+ # The config rides in a `type="application/json"` block: it is *data*,
28
+ # not code, so the browser never executes it and Turbo never tries to
29
+ # re-run it on a soft visit — which means it needs no CSP nonce and the
30
+ # widget can re-read the *current* page's config on every `turbo:load`.
31
+ #
32
+ # `nonce:` stamps only the widget script (the code), so it runs under a
33
+ # nonce-based Content-Security-Policy; pass nil when the app has no nonce.
34
+ def snippet(endpoint:, locale:, nonce: nil)
35
+ config = {
36
+ endpoint: endpoint,
37
+ locale: locale.to_s,
38
+ kinds: kinds,
39
+ sections: Ideasbugs.config.sections.map(&:to_s),
40
+ screenshots: screenshots,
41
+ showButton: Ideasbugs.config.show_button ? true : false,
42
+ buttonLabel: Ideasbugs.config.button_label,
43
+ labels: labels,
44
+ rtl: rtl?(locale)
45
+ }
46
+ # Escape "</" so a value can't close the <script> block early.
47
+ json = config.to_json.gsub('</', '<\/')
48
+ nonce_attr = nonce ? %( nonce="#{nonce}") : ''
49
+
50
+ %(<script type="application/json" data-ideasbugs-config>#{json}</script>) +
51
+ %(<script data-ideasbugs-widget#{nonce_attr}>#{javascript}</script>)
52
+ end
53
+
54
+ private
55
+
56
+ def kinds
57
+ Ideasbugs.config.kinds.map do |kind|
58
+ { value: kind.to_s, label: t("kinds.#{kind}", kind.to_s.humanize) }
59
+ end
60
+ end
61
+
62
+ def screenshots
63
+ {
64
+ enabled: Ideasbugs.config.screenshots_enabled?,
65
+ max: Ideasbugs.config.max_screenshots,
66
+ maxSize: Ideasbugs.config.max_screenshot_size
67
+ }
68
+ end
69
+
70
+ # Every user-facing string in the widget, resolved through Rails I18n so
71
+ # the form follows the app's current locale. Each lookup carries an
72
+ # English default, so the widget stays fully worded even when the host is
73
+ # missing a key for the active locale.
74
+ def labels
75
+ {
76
+ button: t(:button, 'Feedback'),
77
+ title: t(:title, 'Send feedback'),
78
+ kind: t(:kind, 'Type'),
79
+ section: t(:section, 'Section'),
80
+ sectionAny: t(:section_any, 'General'),
81
+ message: t(:message, 'Your message'),
82
+ messagePlaceholder: t(:message_placeholder, "Tell us what's on your mind…"),
83
+ screenshots: t(:screenshots, 'Screenshots'),
84
+ screenshotsHint: t(:screenshots_hint, 'optional, up to %{count} files, %{size} MB each',
85
+ count: Ideasbugs.config.max_screenshots,
86
+ size: Ideasbugs.config.max_screenshot_size / (1024 * 1024)),
87
+ submit: t(:submit, 'Send feedback'),
88
+ cancel: t(:cancel, 'Cancel'),
89
+ close: t(:close, 'Close'),
90
+ thanks: t(:thanks, 'Thanks for your feedback!'),
91
+ errorBlank: t(:error_blank, 'Please enter a message.'),
92
+ errorSave: t(:error_save, 'Could not send feedback. Please try again.'),
93
+ errorTooMany: t(:error_too_many, 'Too many screenshots (max %{count}).',
94
+ count: Ideasbugs.config.max_screenshots),
95
+ errorTooLarge: t(:error_too_large, 'A screenshot is too large (max %{size} MB).',
96
+ size: Ideasbugs.config.max_screenshot_size / (1024 * 1024))
97
+ }
98
+ end
99
+
100
+ def t(key, default, **args)
101
+ I18n.t(key, scope: :ideasbugs, default: default, **args)
102
+ end
103
+
104
+ def rtl?(locale)
105
+ RTL_LANGUAGES.include?(locale.to_s.downcase.split(/[-_]/).first)
106
+ end
107
+ end
108
+ end
109
+ end
data/lib/ideasbugs.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ideasbugs/version'
4
+ require 'ideasbugs/configuration'
5
+ require 'ideasbugs/widget'
6
+ require 'ideasbugs/engine'
7
+
8
+ # In-app product feedback collection for Rails. A drop-in widget lets users
9
+ # send bug reports, feature requests, and general feedback (with screenshots)
10
+ # from any page; submissions land in your own database, with a minimal built-in
11
+ # dashboard to browse and triage them.
12
+ module Ideasbugs
13
+ class << self
14
+ def config
15
+ @config ||= Configuration.new
16
+ end
17
+
18
+ def configure
19
+ yield config
20
+ end
21
+
22
+ # Can this request send feedback? Checked on the server for the endpoint
23
+ # and by the helper before rendering the widget.
24
+ def enabled?(request)
25
+ !!config.enabled.call(request)
26
+ end
27
+
28
+ # Can this request browse and triage feedback? Checked by every dashboard
29
+ # action.
30
+ def admin?(request)
31
+ !!config.authorize_admin.call(request)
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ideasbugs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Yaroslav Shmarov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ description: |
27
+ A mountable Rails engine that adds a "Send feedback" widget to your app —
28
+ bug reports, feature requests, screenshots — and stores submissions in your
29
+ own database. Ships with a minimal built-in dashboard to browse and triage
30
+ what users send. Framework-agnostic: no CSS or JS framework required.
31
+ email:
32
+ - yaroslav.shmarov@clickfunnels.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - CHANGELOG.md
38
+ - MIT-LICENSE
39
+ - README.md
40
+ - Rakefile
41
+ - app/controllers/ideasbugs/application_controller.rb
42
+ - app/controllers/ideasbugs/feedbacks_controller.rb
43
+ - app/controllers/ideasbugs/screenshots_controller.rb
44
+ - app/helpers/ideasbugs/widget_helper.rb
45
+ - app/models/ideasbugs/application_record.rb
46
+ - app/models/ideasbugs/feedback.rb
47
+ - app/views/ideasbugs/feedbacks/index.html.erb
48
+ - app/views/ideasbugs/feedbacks/show.html.erb
49
+ - app/views/layouts/ideasbugs/application.html.erb
50
+ - config/locales/ideasbugs.ar.yml
51
+ - config/locales/ideasbugs.bg.yml
52
+ - config/locales/ideasbugs.bn.yml
53
+ - config/locales/ideasbugs.de.yml
54
+ - config/locales/ideasbugs.el.yml
55
+ - config/locales/ideasbugs.en.yml
56
+ - config/locales/ideasbugs.es.yml
57
+ - config/locales/ideasbugs.fr.yml
58
+ - config/locales/ideasbugs.hi.yml
59
+ - config/locales/ideasbugs.hr.yml
60
+ - config/locales/ideasbugs.id.yml
61
+ - config/locales/ideasbugs.it.yml
62
+ - config/locales/ideasbugs.ja.yml
63
+ - config/locales/ideasbugs.ko.yml
64
+ - config/locales/ideasbugs.lb.yml
65
+ - config/locales/ideasbugs.nl.yml
66
+ - config/locales/ideasbugs.pl.yml
67
+ - config/locales/ideasbugs.pt.yml
68
+ - config/locales/ideasbugs.ro.yml
69
+ - config/locales/ideasbugs.ru.yml
70
+ - config/locales/ideasbugs.th.yml
71
+ - config/locales/ideasbugs.tr.yml
72
+ - config/locales/ideasbugs.uk.yml
73
+ - config/locales/ideasbugs.ur.yml
74
+ - config/locales/ideasbugs.vi.yml
75
+ - config/locales/ideasbugs.zh-CN.yml
76
+ - config/routes.rb
77
+ - lib/generators/ideasbugs/install/install_generator.rb
78
+ - lib/generators/ideasbugs/install/templates/create_ideasbugs_feedbacks.rb.tt
79
+ - lib/generators/ideasbugs/install/templates/initializer.rb
80
+ - lib/ideasbugs.rb
81
+ - lib/ideasbugs/configuration.rb
82
+ - lib/ideasbugs/engine.rb
83
+ - lib/ideasbugs/version.rb
84
+ - lib/ideasbugs/widget.js
85
+ - lib/ideasbugs/widget.rb
86
+ homepage: https://github.com/yshmarov/ideasbugs
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/yshmarov/ideasbugs
91
+ source_code_uri: https://github.com/yshmarov/ideasbugs
92
+ changelog_uri: https://github.com/yshmarov/ideasbugs/blob/main/CHANGELOG.md
93
+ bug_tracker_uri: https://github.com/yshmarov/ideasbugs/issues
94
+ rubygems_mfa_required: 'true'
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '3.2'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 4.0.6
110
+ specification_version: 4
111
+ summary: 'In-app product feedback collection for Rails: a drop-in widget and a built-in
112
+ triage dashboard.'
113
+ test_files: []