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,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest'
5
+
6
+ module Testimonials
7
+ # Serves the self-contained browser widget. The JavaScript is plain ES (no
8
+ # framework, no build step) and styles itself inline, so it drops into any
9
+ # Rails app regardless of its CSS or JS setup. It is inlined into the page
10
+ # rather than served as a separate asset to avoid any dependency on the
11
+ # host's asset pipeline — and it lives under lib/ (not app/assets/) so a
12
+ # host that *does* run a pipeline never ingests it either.
13
+ module Widget
14
+ SOURCE = File.expand_path('widget.js', __dir__)
15
+ DASHBOARD_SOURCE = File.expand_path('dashboard.js', __dir__)
16
+
17
+ # Right-to-left scripts, so the form renders mirrored for those locales.
18
+ # Matched on the language subtag, so region variants ("ar-EG") count too.
19
+ RTL_LANGUAGES = %w[ar arc ckb dv fa ha he ks ku ps sd ug ur yi].freeze
20
+
21
+ class << self
22
+ def javascript
23
+ @javascript ||= File.read(SOURCE)
24
+ end
25
+
26
+ def dashboard_javascript
27
+ @dashboard_javascript ||= File.read(DASHBOARD_SOURCE)
28
+ end
29
+
30
+ # Content fingerprints for cache-busting script URLs: a changed file is
31
+ # a changed URL, so no browser can ever run stale widget code — Safari
32
+ # has been caught ignoring must-revalidate on same-URL scripts.
33
+ def fingerprint
34
+ @fingerprint ||= Digest::MD5.hexdigest(javascript)
35
+ end
36
+
37
+ def dashboard_fingerprint
38
+ @dashboard_fingerprint ||= Digest::MD5.hexdigest(dashboard_javascript)
39
+ end
40
+
41
+ # The two <script> tags the helper renders.
42
+ #
43
+ # The config rides in a `type="application/json"` block: it is *data*,
44
+ # not code, so the browser never executes it and Turbo never tries to
45
+ # re-run it on a soft visit — which means it needs no CSP nonce and the
46
+ # widget can re-read the *current* page's config on every `turbo:load`.
47
+ #
48
+ # The code is a same-origin `src` script served by the engine — NOT
49
+ # inlined. Under a nonce-based CSP, Turbo Drive body swaps re-run body
50
+ # scripts against the *original* page's CSP header, so a fresh inline
51
+ # nonce gets refused; a same-origin src is covered by `'self'` on every
52
+ # visit. `nonce:` is still stamped for hosts whose script-src has no
53
+ # 'self'; pass nil when the app has no nonce.
54
+ def snippet(locale:, authenticated:, auto_open: nil, mode: 'widget', existing: nil, nonce: nil)
55
+ json = config_json(locale:, authenticated:, auto_open:, mode:, existing:)
56
+ nonce_attr = nonce ? %( nonce="#{nonce}") : ''
57
+ src = "#{Testimonials.config.mount_path.chomp('/')}/widget.js?v=#{fingerprint}"
58
+
59
+ %(<script type="application/json" data-testimonials-config>#{json}</script>) +
60
+ %(<script src="#{src}" defer#{nonce_attr} data-testimonials-widget></script>)
61
+ end
62
+
63
+ def config_json(locale:, authenticated:, auto_open:, mode:, existing: nil)
64
+ config = Testimonials.config
65
+ payload = {
66
+ endpoints: {
67
+ testimonials: config.testimonials_endpoint,
68
+ nps: config.nps_endpoint,
69
+ events: config.events_endpoint
70
+ },
71
+ locale: locale.to_s,
72
+ rtl: rtl?(locale),
73
+ authenticated: authenticated ? true : false,
74
+ autoOpen: auto_open&.to_s,
75
+ mode: mode.to_s,
76
+ questions: Testimonials.questions,
77
+ consent: Testimonials.consent_text,
78
+ # The signed-in user's current review, if any: the widget opens it
79
+ # pre-filled for editing instead of starting a second one — rating,
80
+ # text, consent state, and a playable URL for an attached video.
81
+ existing: existing && {
82
+ rating: existing.rating,
83
+ body: existing.body,
84
+ consent: existing.consent_given ? true : false,
85
+ videoUrl: existing_video_url(existing)
86
+ },
87
+ video: {
88
+ enabled: config.video_enabled?,
89
+ maxSeconds: config.max_video_seconds.to_i,
90
+ maxSize: config.max_video_size.to_i
91
+ },
92
+ avatars: {
93
+ enabled: config.avatars_enabled?,
94
+ maxSize: config.max_avatar_size.to_i
95
+ },
96
+ nps: { enabled: config.nps ? true : false },
97
+ labels: labels
98
+ }
99
+ # Escape "</" so a value can't close the <script> block early.
100
+ payload.to_json.gsub('</', '<\/')
101
+ end
102
+
103
+ private
104
+
105
+ def existing_video_url(existing)
106
+ return unless existing.video_attached?
107
+
108
+ "#{Testimonials.config.mount_path.chomp('/')}/#{existing.id}/video"
109
+ end
110
+
111
+ # Every user-facing string in the widget, resolved through Rails I18n so
112
+ # the form follows the app's current locale. Each lookup carries an
113
+ # English default, so the widget stays fully worded even when a key is
114
+ # missing for the active locale.
115
+ def labels
116
+ app = Testimonials.app_name
117
+ {
118
+ enjoying: t(:enjoying, 'Enjoying %{app}?', app: app),
119
+ notNow: t(:not_now, 'Not now'),
120
+ rateAria: t(:rate_aria, 'Rate %{count} of 5'),
121
+ shareTitle: t(:share_title, 'Share your experience'),
122
+ updateTitle: t(:update_title, 'Update your review'),
123
+ promoterTitle: t(:promoter_title, 'Glad to hear it! Mind saying that publicly?'),
124
+ questionsTitle: t(:questions_title, 'Questions'),
125
+ message: t(:message, 'Your testimonial'),
126
+ messagePlaceholder: t(:message_placeholder, 'What would you tell a friend about %{app}?', app: app),
127
+ recordVideo: t(:record_video, 'Record a video'),
128
+ recordVideoHint: t(:record_video_hint, 'A short video says more than a page of text.'),
129
+ name: t(:name, 'Your name'),
130
+ email: t(:email, 'Your email'),
131
+ titleCompany: t(:title_company, 'Title, company'),
132
+ photo: t(:photo, 'Your photo'),
133
+ optional: t(:optional, 'optional'),
134
+ submit: t(:submit, 'Send'),
135
+ cancel: t(:cancel, 'Cancel'),
136
+ close: t(:close, 'Close'),
137
+ thanks: t(:thanks, 'Thank you so much!'),
138
+ videoCheckTitle: t(:video_check_title, 'Camera check'),
139
+ videoHint: t(:video_hint, 'Up to %{seconds} seconds. You can review it before sending.',
140
+ seconds: Testimonials.config.max_video_seconds.to_i),
141
+ startRecording: t(:start_recording, 'Start recording'),
142
+ stopRecording: t(:stop_recording, 'Finish'),
143
+ recordAgain: t(:record_again, 'Record again'),
144
+ useVideo: t(:use_video, 'Use this video'),
145
+ uploadInstead: t(:upload_instead, 'Upload a video file instead'),
146
+ videoAttached: t(:video_attached, 'Video attached'),
147
+ remove: t(:remove, 'Remove'),
148
+ npsQuestion: t(:nps_question, 'How likely are you to recommend %{app} to a friend or colleague?', app: app),
149
+ npsLow: t(:nps_low, 'Not at all likely'),
150
+ npsHigh: t(:nps_high, 'Extremely likely'),
151
+ npsCommentLabel: t(:nps_comment_label, "What's the main reason for your score?"),
152
+ errorBlank: t(:error_blank, 'Please write something or record a video.'),
153
+ errorContact: t(:error_contact, 'Please enter your name and email.'),
154
+ errorSave: t(:error_save, 'Could not send. Please try again.'),
155
+ errorVideoTooLarge: t(:error_video_too_large, 'The video is too large (max %{size} MB).',
156
+ size: Testimonials.config.max_video_size.to_i / (1024 * 1024)),
157
+ errorCamera: t(:error_camera, 'Could not access the camera. You can upload a video file instead.')
158
+ }
159
+ end
160
+
161
+ def t(key, default, **args)
162
+ I18n.t(key, scope: :testimonials, default: default, **args)
163
+ end
164
+
165
+ def rtl?(locale)
166
+ RTL_LANGUAGES.include?(locale.to_s.downcase.split(/[-_]/).first)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'testimonials/version'
4
+ require 'testimonials/configuration'
5
+ require 'testimonials/widget'
6
+ require 'testimonials/prompt_helper'
7
+ require 'testimonials/engine'
8
+
9
+ # Testimonials, reviews and NPS for Rails. An iOS-style in-app widget collects
10
+ # star ratings and testimonials (text or video) at moments your code chooses;
11
+ # a public page collects them from shareable links; NPS promoters are routed
12
+ # straight into the testimonial ask. Everything lands in your own database
13
+ # with a minimal dashboard to approve, feature, and pick best lines — and a read API
14
+ # to render approved testimonials wherever you like.
15
+ module Testimonials
16
+ class << self
17
+ def config
18
+ @config ||= Configuration.new
19
+ end
20
+
21
+ def configure
22
+ yield config
23
+ end
24
+
25
+ # Can this request see the widget and submit? Checked on the server for
26
+ # every public endpoint and by the helper before rendering.
27
+ def enabled?(request)
28
+ !!config.enabled.call(request)
29
+ end
30
+
31
+ # Can this request browse and triage submissions? Checked by every
32
+ # dashboard action.
33
+ def admin?(request)
34
+ !!config.authorize_admin.call(request)
35
+ end
36
+
37
+ def app_name
38
+ config.app_name.presence || rails_app_name
39
+ end
40
+
41
+ # The guiding questions for the current locale, with %{app} filled in.
42
+ # I18n leaves the token alone when no interpolation values are passed,
43
+ # so a plain gsub covers built-in, literal, and lambda-provided strings
44
+ # alike.
45
+ def questions
46
+ list = config.questions
47
+ list = list.call if list.respond_to?(:call)
48
+ list = I18n.t('testimonials.questions', default: []) if list.nil?
49
+ Array(list).map { |q| q.to_s.gsub('%{app}', app_name) }
50
+ end
51
+
52
+ def consent_text
53
+ config.consent_text.presence ||
54
+ I18n.t('testimonials.consent',
55
+ default: 'I give permission to use this testimonial across social channels ' \
56
+ 'and other marketing efforts.')
57
+ end
58
+
59
+ private
60
+
61
+ # The application's module name, verbatim ("EthicsPortal", "SupeRails") —
62
+ # no inflection games. Set config.app_name for anything fancier.
63
+ def rails_app_name
64
+ Rails.application.class.module_parent_name
65
+ rescue StandardError
66
+ 'this app'
67
+ end
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testimonials
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.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 collects customer testimonials — text and
28
+ video — and NPS scores from inside your app. An iOS-style "Enjoying this
29
+ app?" widget prompts users at moments your code chooses (with built-in
30
+ throttling), a public page collects testimonials from a shareable link,
31
+ and a minimal dashboard lets you approve, feature, and excerpt what comes
32
+ in. Display is headless: approved testimonials are served through your
33
+ models or an optional JSON API, and you render them with your own markup.
34
+ Framework-agnostic: no CSS or JS framework required.
35
+ email:
36
+ - yaroslav.shmarov@clickfunnels.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - MIT-LICENSE
43
+ - README.md
44
+ - Rakefile
45
+ - app/controllers/testimonials/api/base_controller.rb
46
+ - app/controllers/testimonials/api/stats_controller.rb
47
+ - app/controllers/testimonials/api/testimonials_controller.rb
48
+ - app/controllers/testimonials/application_controller.rb
49
+ - app/controllers/testimonials/collection_controller.rb
50
+ - app/controllers/testimonials/events_controller.rb
51
+ - app/controllers/testimonials/media_controller.rb
52
+ - app/controllers/testimonials/nps_controller.rb
53
+ - app/controllers/testimonials/nps_responses_controller.rb
54
+ - app/controllers/testimonials/testimonials_controller.rb
55
+ - app/controllers/testimonials/widgets_controller.rb
56
+ - app/helpers/testimonials/widget_helper.rb
57
+ - app/models/testimonials/application_record.rb
58
+ - app/models/testimonials/nps_response.rb
59
+ - app/models/testimonials/prompt_event.rb
60
+ - app/models/testimonials/testimonial.rb
61
+ - app/views/layouts/testimonials/application.html.erb
62
+ - app/views/layouts/testimonials/collection.html.erb
63
+ - app/views/testimonials/collection/show.html.erb
64
+ - app/views/testimonials/nps_responses/index.html.erb
65
+ - app/views/testimonials/testimonials/index.html.erb
66
+ - app/views/testimonials/testimonials/show.html.erb
67
+ - config/locales/testimonials.ar.yml
68
+ - config/locales/testimonials.bg.yml
69
+ - config/locales/testimonials.bn.yml
70
+ - config/locales/testimonials.de.yml
71
+ - config/locales/testimonials.el.yml
72
+ - config/locales/testimonials.en.yml
73
+ - config/locales/testimonials.es.yml
74
+ - config/locales/testimonials.fr.yml
75
+ - config/locales/testimonials.hi.yml
76
+ - config/locales/testimonials.hr.yml
77
+ - config/locales/testimonials.id.yml
78
+ - config/locales/testimonials.it.yml
79
+ - config/locales/testimonials.ja.yml
80
+ - config/locales/testimonials.ko.yml
81
+ - config/locales/testimonials.lb.yml
82
+ - config/locales/testimonials.nl.yml
83
+ - config/locales/testimonials.pl.yml
84
+ - config/locales/testimonials.pt.yml
85
+ - config/locales/testimonials.ro.yml
86
+ - config/locales/testimonials.ru.yml
87
+ - config/locales/testimonials.th.yml
88
+ - config/locales/testimonials.tr.yml
89
+ - config/locales/testimonials.uk.yml
90
+ - config/locales/testimonials.ur.yml
91
+ - config/locales/testimonials.vi.yml
92
+ - config/locales/testimonials.zh-CN.yml
93
+ - config/routes.rb
94
+ - lib/generators/testimonials/install/install_generator.rb
95
+ - lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt
96
+ - lib/generators/testimonials/install/templates/initializer.rb
97
+ - lib/testimonials.rb
98
+ - lib/testimonials/configuration.rb
99
+ - lib/testimonials/dashboard.js
100
+ - lib/testimonials/engine.rb
101
+ - lib/testimonials/prompt_helper.rb
102
+ - lib/testimonials/version.rb
103
+ - lib/testimonials/widget.js
104
+ - lib/testimonials/widget.rb
105
+ homepage: https://github.com/yshmarov/testimonials-engine
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ homepage_uri: https://github.com/yshmarov/testimonials-engine
110
+ source_code_uri: https://github.com/yshmarov/testimonials-engine
111
+ changelog_uri: https://github.com/yshmarov/testimonials-engine/blob/main/CHANGELOG.md
112
+ bug_tracker_uri: https://github.com/yshmarov/testimonials-engine/issues
113
+ rubygems_mfa_required: 'true'
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '3.2'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 4.0.6
129
+ specification_version: 4
130
+ summary: 'Testimonials, reviews and NPS for Rails: in-app collection widget, public
131
+ collection page, triage dashboard, and a read API.'
132
+ test_files: []