testimonials 0.1.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 (66) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +175 -0
  5. data/Rakefile +11 -0
  6. data/app/controllers/testimonials/api/base_controller.rb +30 -0
  7. data/app/controllers/testimonials/api/stats_controller.rb +20 -0
  8. data/app/controllers/testimonials/api/testimonials_controller.rb +41 -0
  9. data/app/controllers/testimonials/application_controller.rb +50 -0
  10. data/app/controllers/testimonials/collection_controller.rb +18 -0
  11. data/app/controllers/testimonials/events_controller.rb +25 -0
  12. data/app/controllers/testimonials/media_controller.rb +48 -0
  13. data/app/controllers/testimonials/nps_controller.rb +71 -0
  14. data/app/controllers/testimonials/nps_responses_controller.rb +25 -0
  15. data/app/controllers/testimonials/testimonials_controller.rb +194 -0
  16. data/app/controllers/testimonials/widgets_controller.rb +36 -0
  17. data/app/helpers/testimonials/widget_helper.rb +83 -0
  18. data/app/models/testimonials/application_record.rb +7 -0
  19. data/app/models/testimonials/nps_response.rb +24 -0
  20. data/app/models/testimonials/prompt_event.rb +60 -0
  21. data/app/models/testimonials/testimonial.rb +53 -0
  22. data/app/views/layouts/testimonials/application.html.erb +118 -0
  23. data/app/views/layouts/testimonials/collection.html.erb +23 -0
  24. data/app/views/testimonials/collection/show.html.erb +12 -0
  25. data/app/views/testimonials/nps_responses/index.html.erb +67 -0
  26. data/app/views/testimonials/testimonials/index.html.erb +108 -0
  27. data/app/views/testimonials/testimonials/show.html.erb +105 -0
  28. data/config/locales/testimonials.ar.yml +101 -0
  29. data/config/locales/testimonials.bg.yml +101 -0
  30. data/config/locales/testimonials.bn.yml +101 -0
  31. data/config/locales/testimonials.de.yml +101 -0
  32. data/config/locales/testimonials.el.yml +101 -0
  33. data/config/locales/testimonials.en.yml +101 -0
  34. data/config/locales/testimonials.es.yml +101 -0
  35. data/config/locales/testimonials.fr.yml +101 -0
  36. data/config/locales/testimonials.hi.yml +101 -0
  37. data/config/locales/testimonials.hr.yml +101 -0
  38. data/config/locales/testimonials.id.yml +101 -0
  39. data/config/locales/testimonials.it.yml +101 -0
  40. data/config/locales/testimonials.ja.yml +101 -0
  41. data/config/locales/testimonials.ko.yml +101 -0
  42. data/config/locales/testimonials.lb.yml +101 -0
  43. data/config/locales/testimonials.nl.yml +101 -0
  44. data/config/locales/testimonials.pl.yml +101 -0
  45. data/config/locales/testimonials.pt.yml +101 -0
  46. data/config/locales/testimonials.ro.yml +101 -0
  47. data/config/locales/testimonials.ru.yml +101 -0
  48. data/config/locales/testimonials.th.yml +101 -0
  49. data/config/locales/testimonials.tr.yml +101 -0
  50. data/config/locales/testimonials.uk.yml +101 -0
  51. data/config/locales/testimonials.ur.yml +101 -0
  52. data/config/locales/testimonials.vi.yml +101 -0
  53. data/config/locales/testimonials.zh-CN.yml +101 -0
  54. data/config/routes.rb +29 -0
  55. data/lib/generators/testimonials/install/install_generator.rb +42 -0
  56. data/lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt +53 -0
  57. data/lib/generators/testimonials/install/templates/initializer.rb +70 -0
  58. data/lib/testimonials/configuration.rb +129 -0
  59. data/lib/testimonials/dashboard.js +33 -0
  60. data/lib/testimonials/engine.rb +16 -0
  61. data/lib/testimonials/prompt_helper.rb +15 -0
  62. data/lib/testimonials/version.rb +5 -0
  63. data/lib/testimonials/widget.js +1045 -0
  64. data/lib/testimonials/widget.rb +170 -0
  65. data/lib/testimonials.rb +69 -0
  66. metadata +132 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ Testimonials.configure do |config|
4
+ # Shown in the widget ("Enjoying %{app}?") and interpolated into the
5
+ # default questions. Defaults to your Rails application name.
6
+ # config.app_name = "My App"
7
+
8
+ # Who sees the widget and can submit. Return false to hide and reject for
9
+ # this request. Defaults to everyone.
10
+ # config.enabled = ->(request) { true }
11
+
12
+ # Who can triage testimonials at the mount path. Defaults to development
13
+ # only — override before deploying.
14
+ # config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
15
+
16
+ # Attribute submissions to a user (optional). Return an object responding
17
+ # to #id, or nil. Receives the request.
18
+ # config.current_user = ->(request) { request.env["warden"]&.user }
19
+
20
+ # Attribution stored with a signed-in user's submission.
21
+ # config.user_display = ->(user) { { name: user.name, email: user.email } }
22
+
23
+ # Guiding prompts shown above the message field (not form fields).
24
+ # Default nil = the gem's built-in localized questions. Override with
25
+ # literal strings, or a lambda for host-side i18n. [] hides the section.
26
+ # config.questions = ["How has %{app} helped you?", "What's the best part?"]
27
+ # config.questions = -> { I18n.t("reviews.questions") }
28
+
29
+ # Video testimonials (requires Active Storage).
30
+ # config.video = true
31
+ # config.max_video_seconds = 120
32
+ # config.max_video_size = 50.megabytes
33
+
34
+ # Guest headshot upload on the public collection page (requires Active Storage).
35
+ # config.avatars = true
36
+ # config.max_avatar_size = 5.megabytes
37
+
38
+ # Auto-prompt throttling. Explicit opens (user clicked your link) bypass it.
39
+ # config.reprompt_after = 90.days
40
+ # config.max_prompts = 3
41
+
42
+ # Consent line stored verbatim with each submission. nil = localized default:
43
+ # "I give permission to use this testimonial across social channels and
44
+ # other marketing efforts."
45
+ # config.consent_text = "..."
46
+
47
+ # The standalone collection page at /testimonials/new — share it with customers
48
+ # outside the app. Set false to disable.
49
+ # config.public_collection = true
50
+
51
+ # Unauthenticated read access to GET /testimonials/api/testimonials and
52
+ # /testimonials/api/stats (approved + consented records only). Off = admins only.
53
+ # config.public_api = false
54
+
55
+ # NPS surveys. Promoters (9–10) are offered the testimonial form right away.
56
+ # config.nps = true
57
+ # config.nps_reprompt_after = 90.days
58
+
59
+ # Called with each saved Testimonial or NpsResponse — notify Slack, email…
60
+ # config.on_submit = ->(record) {}
61
+
62
+ # Called with each NPS response scored 0–6.
63
+ # config.on_detractor = ->(nps) {}
64
+
65
+ # Per-IP throttle for public endpoints (Rails 7.2+; ignored on 7.1).
66
+ # config.rate_limit = { to: 5, within: 1.minute }
67
+
68
+ # Keep in sync with the `mount` in config/routes.rb.
69
+ # config.mount_path = "/testimonials"
70
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Testimonials
4
+ # Host-tunable settings. Everything has a safe default, so a fresh install
5
+ # works with zero configuration; the hooks below let an app decide who gets
6
+ # prompted, who can triage, and how submissions are attributed.
7
+ class Configuration
8
+ # Shown in the widget ("Enjoying %{app}?") and interpolated into the
9
+ # default questions. nil resolves to the Rails application name.
10
+ attr_accessor :app_name
11
+
12
+ # Per-request gate for the widget and the submission endpoints. Return
13
+ # false to hide the widget and reject submissions for this request.
14
+ attr_accessor :enabled
15
+
16
+ # Per-request gate for the built-in dashboard. Defaults to development
17
+ # only — override it before deploying, e.g. with an admin check.
18
+ attr_accessor :authorize_admin
19
+
20
+ # Resolve the current user for attribution (optional). Return an object
21
+ # responding to #id, or nil. Receives the request.
22
+ attr_accessor :current_user
23
+
24
+ # Turn a resolved user into the attribution stored with a submission.
25
+ # Return a hash with :name, :email, and optionally :title_company.
26
+ # Receives whatever #current_user returned.
27
+ attr_accessor :user_display
28
+
29
+ # Guiding prompts shown above the message field and while recording —
30
+ # they fight blank-page paralysis, they are not form fields. nil uses the
31
+ # gem's built-in localized questions (`testimonials.questions`, with
32
+ # %{app} interpolated). Override with an array of literal strings, or a
33
+ # callable for host-side i18n: `-> { I18n.t("myapp.review_questions") }`.
34
+ # An empty array hides the section.
35
+ attr_accessor :questions
36
+
37
+ # Video testimonials (recording and upload). Requires Active Storage.
38
+ attr_accessor :video
39
+ attr_accessor :max_video_seconds, :max_video_size
40
+
41
+ # Headshot upload for guests on the public collection page. Requires
42
+ # Active Storage.
43
+ attr_accessor :avatars
44
+ attr_accessor :max_avatar_size
45
+
46
+ # Auto-prompt throttling. A user who dismissed the widget is not
47
+ # auto-prompted again within `reprompt_after`; a user auto-prompted
48
+ # `max_prompts` times without submitting is never auto-prompted again;
49
+ # a user who submitted a testimonial is done for good. Explicit opens
50
+ # (clicking your link) always work.
51
+ attr_accessor :reprompt_after, :max_prompts
52
+
53
+ # Consent line stored verbatim with each submission. nil uses the
54
+ # localized default.
55
+ attr_accessor :consent_text
56
+
57
+ # The standalone collection page at "#{mount_path}/new" — for links you
58
+ # send to customers outside the app. ON by default; set false to 404 it.
59
+ attr_accessor :public_collection
60
+
61
+ # Unauthenticated read access to the JSON API (approved + consented
62
+ # records only). OFF by default: the API then answers only for admins.
63
+ attr_accessor :public_api
64
+
65
+ # NPS surveys ("How likely are you to recommend…", 0–10). Promoters
66
+ # (9–10) are offered the testimonial form right after scoring.
67
+ attr_accessor :nps
68
+ attr_accessor :nps_reprompt_after
69
+
70
+ # Called with each saved Testimonials::Testimonial or
71
+ # Testimonials::NpsResponse — notify Slack, send an email. Runs inline
72
+ # after save; keep it fast or hand off to a job.
73
+ attr_accessor :on_submit
74
+
75
+ # Called with each NPS response scored 0–6. Route it into your feedback
76
+ # tool, e.g. create a FeedbackEngine::Feedback from nps.comment.
77
+ attr_accessor :on_detractor
78
+
79
+ # Per-IP throttle for the public endpoints, as keyword arguments for
80
+ # Rails' rate limiter (Rails 7.2+; ignored on 7.1). Read once when the
81
+ # controller loads — set it in an initializer. nil disables throttling.
82
+ attr_accessor :rate_limit
83
+
84
+ # Where the engine is mounted. The widget posts to paths under it, so
85
+ # keep this in sync with the `mount` line in your routes.
86
+ attr_accessor :mount_path
87
+
88
+ def initialize
89
+ @app_name = nil
90
+ @enabled = ->(_request) { true }
91
+ @authorize_admin = ->(_request) { Rails.env.development? }
92
+ @current_user = ->(_request) {}
93
+ @user_display = lambda { |user|
94
+ { name: user.try(:name), email: user.try(:email) }
95
+ }
96
+ @questions = nil
97
+ @video = true
98
+ @max_video_seconds = 120
99
+ @max_video_size = 50 * 1024 * 1024
100
+ @avatars = true
101
+ @max_avatar_size = 5 * 1024 * 1024
102
+ @reprompt_after = 90 * 24 * 60 * 60
103
+ @max_prompts = 3
104
+ @consent_text = nil
105
+ @public_collection = true
106
+ @public_api = false
107
+ @nps = true
108
+ @nps_reprompt_after = 90 * 24 * 60 * 60
109
+ @on_submit = ->(_record) {}
110
+ @on_detractor = ->(_nps_response) {}
111
+ @rate_limit = { to: 5, within: 60 }
112
+ @mount_path = '/testimonials'
113
+ end
114
+
115
+ def testimonials_endpoint = mount_path.chomp('/')
116
+ def nps_endpoint = "#{mount_path.chomp('/')}/nps"
117
+ def events_endpoint = "#{mount_path.chomp('/')}/events"
118
+
119
+ # Video and avatars need Active Storage — both the config switch and the
120
+ # host actually having it loaded.
121
+ def video_enabled?
122
+ video && defined?(::ActiveStorage) ? true : false
123
+ end
124
+
125
+ def avatars_enabled?
126
+ avatars && defined?(::ActiveStorage) ? true : false
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,33 @@
1
+ /*
2
+ * testimonials dashboard helpers — CSP-safe replacements for inline
3
+ * handlers, which strict script-src policies (nonce or 'self') block.
4
+ *
5
+ * forms with data-confirm ask before submitting; controls with
6
+ * data-autosubmit submit their form on change. Document-level listeners,
7
+ * registered once, so Turbo navigations into the dashboard are fine.
8
+ */
9
+ (function () {
10
+ "use strict";
11
+
12
+ if (window.__testimonialsDashboardLoaded) return;
13
+ window.__testimonialsDashboardLoaded = true;
14
+
15
+ document.addEventListener("submit", function (event) {
16
+ var form = event.target;
17
+ var message = form && form.dataset ? form.dataset.confirm : null;
18
+ if (message && !window.confirm(message)) {
19
+ event.preventDefault();
20
+ event.stopImmediatePropagation();
21
+ }
22
+ }, true);
23
+
24
+ document.addEventListener("change", function (event) {
25
+ var control = event.target;
26
+ if (!control || !control.dataset || !control.dataset.autosubmit || !control.form) return;
27
+ if (control.form.requestSubmit) {
28
+ control.form.requestSubmit();
29
+ } else {
30
+ control.form.submit();
31
+ }
32
+ });
33
+ })();
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Testimonials
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Testimonials
6
+
7
+ initializer 'testimonials.helpers' do
8
+ ActiveSupport.on_load(:action_view) do
9
+ include Testimonials::WidgetHelper
10
+ end
11
+ ActiveSupport.on_load(:action_controller) do
12
+ include Testimonials::PromptHelper
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Testimonials
4
+ # Included into the host's controllers. Call `testimonial_prompt!` at your
5
+ # app's success moments — subscription renewed, tenth invoice sent, big
6
+ # milestone hit. The widget auto-opens on the next rendered page *if* the
7
+ # throttle allows, so calling this liberally is safe: users who submitted,
8
+ # recently dismissed, or were already prompted max_prompts times are left
9
+ # alone.
10
+ module PromptHelper
11
+ def testimonial_prompt!(kind = :testimonial)
12
+ flash[:testimonials_prompt] = kind.to_s
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Testimonials
4
+ VERSION = '0.1.0'
5
+ end