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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/MIT-LICENSE +20 -0
- data/README.md +175 -0
- data/Rakefile +11 -0
- data/app/controllers/testimonials/api/base_controller.rb +30 -0
- data/app/controllers/testimonials/api/stats_controller.rb +20 -0
- data/app/controllers/testimonials/api/testimonials_controller.rb +41 -0
- data/app/controllers/testimonials/application_controller.rb +50 -0
- data/app/controllers/testimonials/collection_controller.rb +18 -0
- data/app/controllers/testimonials/events_controller.rb +25 -0
- data/app/controllers/testimonials/media_controller.rb +48 -0
- data/app/controllers/testimonials/nps_controller.rb +71 -0
- data/app/controllers/testimonials/nps_responses_controller.rb +25 -0
- data/app/controllers/testimonials/testimonials_controller.rb +194 -0
- data/app/controllers/testimonials/widgets_controller.rb +36 -0
- data/app/helpers/testimonials/widget_helper.rb +83 -0
- data/app/models/testimonials/application_record.rb +7 -0
- data/app/models/testimonials/nps_response.rb +24 -0
- data/app/models/testimonials/prompt_event.rb +60 -0
- data/app/models/testimonials/testimonial.rb +53 -0
- data/app/views/layouts/testimonials/application.html.erb +118 -0
- data/app/views/layouts/testimonials/collection.html.erb +23 -0
- data/app/views/testimonials/collection/show.html.erb +12 -0
- data/app/views/testimonials/nps_responses/index.html.erb +67 -0
- data/app/views/testimonials/testimonials/index.html.erb +108 -0
- data/app/views/testimonials/testimonials/show.html.erb +105 -0
- data/config/locales/testimonials.ar.yml +101 -0
- data/config/locales/testimonials.bg.yml +101 -0
- data/config/locales/testimonials.bn.yml +101 -0
- data/config/locales/testimonials.de.yml +101 -0
- data/config/locales/testimonials.el.yml +101 -0
- data/config/locales/testimonials.en.yml +101 -0
- data/config/locales/testimonials.es.yml +101 -0
- data/config/locales/testimonials.fr.yml +101 -0
- data/config/locales/testimonials.hi.yml +101 -0
- data/config/locales/testimonials.hr.yml +101 -0
- data/config/locales/testimonials.id.yml +101 -0
- data/config/locales/testimonials.it.yml +101 -0
- data/config/locales/testimonials.ja.yml +101 -0
- data/config/locales/testimonials.ko.yml +101 -0
- data/config/locales/testimonials.lb.yml +101 -0
- data/config/locales/testimonials.nl.yml +101 -0
- data/config/locales/testimonials.pl.yml +101 -0
- data/config/locales/testimonials.pt.yml +101 -0
- data/config/locales/testimonials.ro.yml +101 -0
- data/config/locales/testimonials.ru.yml +101 -0
- data/config/locales/testimonials.th.yml +101 -0
- data/config/locales/testimonials.tr.yml +101 -0
- data/config/locales/testimonials.uk.yml +101 -0
- data/config/locales/testimonials.ur.yml +101 -0
- data/config/locales/testimonials.vi.yml +101 -0
- data/config/locales/testimonials.zh-CN.yml +101 -0
- data/config/routes.rb +29 -0
- data/lib/generators/testimonials/install/install_generator.rb +42 -0
- data/lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt +53 -0
- data/lib/generators/testimonials/install/templates/initializer.rb +70 -0
- data/lib/testimonials/configuration.rb +129 -0
- data/lib/testimonials/dashboard.js +33 -0
- data/lib/testimonials/engine.rb +16 -0
- data/lib/testimonials/prompt_helper.rb +15 -0
- data/lib/testimonials/version.rb +5 -0
- data/lib/testimonials/widget.js +1045 -0
- data/lib/testimonials/widget.rb +170 -0
- data/lib/testimonials.rb +69 -0
- 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
|