product_tours 0.3.0 → 0.3.3
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 +4 -4
- data/AGENTS.md +19 -3
- data/CHANGELOG.md +39 -3
- data/README.md +23 -2
- data/app/controllers/product_tours/tours_controller.rb +9 -1
- data/lib/generators/product_tours/install/templates/initializer.rb +7 -0
- data/lib/product_tours/configuration.rb +2 -1
- data/lib/product_tours/dashboard.css +14 -0
- data/lib/product_tours/seeds.rb +163 -46
- data/lib/product_tours/version.rb +1 -1
- data/lib/product_tours/widget.js +4 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 397e2fbd273b707e9a16302852704ed8e5cc6daf410f3255fe3fa819047163b8
|
|
4
|
+
data.tar.gz: c040cac638d52a7484f4ef109732211e181f0b103bdbcd938f0d6c3c7b8d10e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b22e2391759e9a49d90728b9709650e5f0f2d1001fdf36d71e4c8d2c494e853007f70794b93c92762363502872f024970cf62660fa4639702aac82c854b2f9ab
|
|
7
|
+
data.tar.gz: e2ceef66890c00c8a5f3f9293f946e35c366215b08a584a52e49408cb858f245affe2eab62caece0f1b7286f5b5944da9358f3315777b95208e962e715b83b2f
|
data/AGENTS.md
CHANGED
|
@@ -81,7 +81,8 @@ YouTube, Vimeo, Loom, Tella, Voomly, a direct MP4/WebM URL, or an uploaded file
|
|
|
81
81
|
|
|
82
82
|
### Lifecycle events
|
|
83
83
|
|
|
84
|
-
Subscribe in an initializer
|
|
84
|
+
Subscribe in an initializer when your normal request middleware already sets
|
|
85
|
+
host identity:
|
|
85
86
|
|
|
86
87
|
```ruby
|
|
87
88
|
ActiveSupport::Notifications.subscribe("product_tours.completed") do |*, payload|
|
|
@@ -91,6 +92,20 @@ end
|
|
|
91
92
|
|
|
92
93
|
Names: `product_tours.viewed`, `product_tours.dismissed`, `product_tours.completed`, and `product_tours.unresolved_trigger` — the last one fires when a `data-product-tour` button names a key that does not resolve. Subscribe to it in development; it turns "my button does nothing" into a log line naming the key.
|
|
93
94
|
|
|
95
|
+
The public widget controller deliberately does not inherit the host's controller.
|
|
96
|
+
If that means a subscriber cannot see the host's current user/account context,
|
|
97
|
+
configure the request-aware lifecycle hook instead:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
config.on_event = lambda do |name, payload, request|
|
|
101
|
+
user = request.env["warden"]&.user
|
|
102
|
+
ProductTourEventJob.perform_later(name, payload, user&.id)
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It receives only `viewed`, `dismissed`, and `completed`, runs inline, and logs
|
|
107
|
+
exceptions without breaking the visitor flow. Keep it fast or enqueue a job.
|
|
108
|
+
|
|
94
109
|
### Do not
|
|
95
110
|
|
|
96
111
|
- **Do not copy the widget JavaScript into `app/javascript`, or add a `<script>` tag for it.** `product_tours_tag` renders what is needed and the engine serves the code. There is no build step and nothing for esbuild/importmap/Tailwind to know about.
|
|
@@ -101,7 +116,7 @@ Names: `product_tours.viewed`, `product_tours.dismissed`, `product_tours.complet
|
|
|
101
116
|
|
|
102
117
|
### Configuration
|
|
103
118
|
|
|
104
|
-
There are
|
|
119
|
+
There are seven options. That is the whole surface.
|
|
105
120
|
|
|
106
121
|
| Option | Default | What it does |
|
|
107
122
|
| --- | --- | --- |
|
|
@@ -109,6 +124,7 @@ There are five options. That is the whole surface.
|
|
|
109
124
|
| `enabled` | everyone | Per-request gate for the widget and its endpoints |
|
|
110
125
|
| `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits. Name your admin's and it adopts that layout, helpers, authentication and request context. Public endpoints never inherit it. |
|
|
111
126
|
| `admin_layout` | `product_tours/application` | Render the dashboard inside your admin shell |
|
|
127
|
+
| `on_event` | no-op | Handle lifecycle events with `(name, payload, request)` when host identity must be resolved from the raw request |
|
|
112
128
|
| `mount_path` | `"/product_tours"` | Keep in sync with `mount_product_tours at:` |
|
|
113
129
|
| `storage_service` | app default | Active Storage service for uploaded video (a `storage.yml` key) |
|
|
114
130
|
|
|
@@ -152,6 +168,6 @@ Conventions this codebase holds to — follow them rather than the first thing t
|
|
|
152
168
|
- **Uploaded media streams through the engine's gate**, never a public blob URL.
|
|
153
169
|
- **The CSP patch is additive.** It appends to existing sources and drops `'none'` rather than replacing a host's policy — do not let it start overwriting directives.
|
|
154
170
|
- **The dummy app pins `config.active_job.queue_adapter = :test`.** Do not remove it or let it drift back to the `:async` default. Attaching a video enqueues Active Storage's analysis job, and `:async` runs it on a background thread that checks out its own connection — writes no test transaction covers, landing in the middle of whatever runs next. That is a suite that fails order-dependently in a test which never created a row, and it is miserable to trace back.
|
|
155
|
-
- **`lib/product_tours/dashboard.css` is half shared.** Everything above the `GEM-SPECIFIC` banner is the design system all five gems in the family ship — the same tokens, the same `.page-head`/`.tabs`/`.filters`/`.card`/`.badge`/`button`, the same `.dashboard-shell` + `.record-row` + `.detail-panel` two-pane dashboard — identical in every repo apart from the `pt` prefix. Diff it against a sibling before changing it, and carry the change to the other four. Anything only this gem has goes below the banner. New dashboard markup reuses the shared class names rather than inventing a domain-specific one.
|
|
171
|
+
- **`lib/product_tours/dashboard.css` is half shared.** Everything above the `GEM-SPECIFIC` banner is the design system all five gems in the family ship — the same tokens, the same `.page-head`/`.tabs`/`.filters`/`.card`/`.badge`/`button`/`.status-switch`, the same `.dashboard-shell` + `.record-row` + `.detail-panel` two-pane dashboard — identical in every repo apart from the `pt` prefix. Diff it against a sibling before changing it, and carry the change to the other four. Anything only this gem has goes below the banner. New dashboard markup reuses the shared class names rather than inventing a domain-specific one.
|
|
156
172
|
- Every user-facing change bumps `lib/product_tours/version.rb` and adds a `CHANGELOG.md` entry (Keep a Changelog format) that says what it costs, not only what it adds.
|
|
157
173
|
- Commit messages are prose that explains the tradeoff — read `git log` before writing one.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.3.3] - 2026-08-08
|
|
6
|
+
|
|
7
|
+
- **The demo walkthrough is now a tour of Product Tours itself.** Its linked
|
|
8
|
+
steps explain host-owned triggers, post chaining, optional video, drafts,
|
|
9
|
+
locale-stable keys, lifecycle events, direct and uploaded video delivery, and
|
|
10
|
+
completion. The standalone buttons still exercise every provider, a draft,
|
|
11
|
+
a missing key, and an action URL, while making the purpose of each case clear.
|
|
12
|
+
- English, French, and Bulgarian demo posts carry the educational copy. Rich
|
|
13
|
+
descriptions are added when Action Text is installed; title-only installs
|
|
14
|
+
still get the guided sequence. The provider videos remain generic examples.
|
|
15
|
+
- `product_tours:seed_demo` preserves demo keys and refreshes their content, so
|
|
16
|
+
rerunning it overwrites edits to those gem-owned posts. Tutorials with other
|
|
17
|
+
keys are not touched.
|
|
18
|
+
|
|
19
|
+
## [0.3.2] - 2026-08-08
|
|
20
|
+
|
|
21
|
+
- **Video tutorials are comfortably watchable without entering full screen.**
|
|
22
|
+
Text-only dialogs remain compact at 440px, while a tutorial containing video
|
|
23
|
+
automatically expands to 760px on desktop. Once a video appears in an open
|
|
24
|
+
walkthrough, the dialog stays wide for its remaining steps so the layout does
|
|
25
|
+
not jump back and forth. Mobile dialogs remain full-screen. There is no new
|
|
26
|
+
configuration or API to adopt.
|
|
27
|
+
|
|
28
|
+
## [0.3.1] - 2026-08-08
|
|
29
|
+
|
|
30
|
+
- **Hosts can handle lifecycle events with their own request context.** Set
|
|
31
|
+
`config.on_event` to a callable accepting `(name, payload, request)` when a
|
|
32
|
+
`viewed`, `dismissed`, or `completed` signal must resolve a signed session,
|
|
33
|
+
user, account, or tenant that deliberately does not belong in the gem. The
|
|
34
|
+
existing `ActiveSupport::Notifications` events and their minimal payloads are
|
|
35
|
+
unchanged. The hook runs inline, so slow work should be enqueued; exceptions
|
|
36
|
+
are logged and never turn a working tutorial action into a visitor-facing
|
|
37
|
+
failure.
|
|
38
|
+
|
|
3
39
|
## 0.3.0
|
|
4
40
|
|
|
5
41
|
- **One design system across the family.** The stylesheet now opens with a
|
|
@@ -60,8 +96,6 @@
|
|
|
60
96
|
`remove_index :product_tours_posts, :locale`.
|
|
61
97
|
- A `BackboneTest` now fails the build on any of the above regressing.
|
|
62
98
|
|
|
63
|
-
## [Unreleased]
|
|
64
|
-
|
|
65
99
|
## [0.1.2] - 2026-08-04
|
|
66
100
|
|
|
67
101
|
- Added `AGENTS.md`: install and integration instructions written for coding
|
|
@@ -122,6 +156,8 @@
|
|
|
122
156
|
- Removed generic tutorial duplication now that translations provide the only
|
|
123
157
|
intentional content-copying workflow.
|
|
124
158
|
|
|
125
|
-
[Unreleased]: https://github.com/yshmarov/product_tours/compare/v0.
|
|
159
|
+
[Unreleased]: https://github.com/yshmarov/product_tours/compare/v0.3.2...HEAD
|
|
160
|
+
[0.3.2]: https://github.com/yshmarov/product_tours/compare/v0.3.1...v0.3.2
|
|
161
|
+
[0.3.1]: https://github.com/yshmarov/product_tours/compare/v0.3.0...v0.3.1
|
|
126
162
|
[0.1.1]: https://github.com/yshmarov/product_tours/compare/v0.1.0...v0.1.1
|
|
127
163
|
[0.1.0]: https://github.com/yshmarov/product_tours/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -92,8 +92,11 @@ the things it should not do. It ships inside the gem, so
|
|
|
92
92
|
|
|
93
93
|
<img src="docs/screenshots/03-mobile.jpg" alt="The product tutorial modal filling a mobile viewport with a video and primary action" width="390">
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
Text-only tutorials stay compact at 440px on desktop. A tutorial with video
|
|
96
|
+
automatically opens at up to 760px, and a walkthrough stays at that size after
|
|
97
|
+
its first video so the layout does not jump between steps. On screens up to
|
|
98
|
+
480px the modal becomes a full-screen sheet, respects safe areas, and follows
|
|
99
|
+
`visualViewport` while the mobile keyboard is open.
|
|
97
100
|
|
|
98
101
|
## Why a gem
|
|
99
102
|
|
|
@@ -228,6 +231,7 @@ Everything is optional — a development install works with zero config. In
|
|
|
228
231
|
| `authorize_admin` | development only | **Who can manage content at the mount path** |
|
|
229
232
|
| `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits — name your admin's and it adopts its layout, helpers and auth |
|
|
230
233
|
| `admin_layout` | gem layout | Just the shell, if you don't want the whole controller |
|
|
234
|
+
| `on_event` | no-op | Handle lifecycle events with the raw request and host-owned identity/account context |
|
|
231
235
|
| `storage_service` | app default | Named Active Storage service for uploaded videos |
|
|
232
236
|
| `mount_path` | `/product_tours` | Keep in sync only when mounting the engine manually |
|
|
233
237
|
|
|
@@ -236,6 +240,9 @@ ProductTours.configure do |config|
|
|
|
236
240
|
config.enabled = ->(request) { request.env["warden"]&.user.present? }
|
|
237
241
|
config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
|
|
238
242
|
config.admin_layout = "admin/application"
|
|
243
|
+
config.on_event = lambda do |name, payload, request|
|
|
244
|
+
AnalyticsJob.perform_later(name, payload, request.session[:user_id])
|
|
245
|
+
end
|
|
239
246
|
config.storage_service = :product_tours
|
|
240
247
|
end
|
|
241
248
|
```
|
|
@@ -302,6 +309,20 @@ end
|
|
|
302
309
|
Your subscriber can attach `Current.user` or account context. That identity does
|
|
303
310
|
not need to become product-tour configuration.
|
|
304
311
|
|
|
312
|
+
If the engine's public controller does not run your host controller callbacks,
|
|
313
|
+
use `on_event` instead. It receives the same event name and payload plus the raw
|
|
314
|
+
request, so it can safely resolve the host's signed session:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
config.on_event = lambda do |name, payload, request|
|
|
318
|
+
user = request.env["warden"]&.user
|
|
319
|
+
ProductTourEventJob.perform_later(name, payload, user&.id)
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The hook runs inline; enqueue slow work. Exceptions are logged and do not break
|
|
324
|
+
the visitor flow. `product_tours.unresolved_trigger` remains a notification only.
|
|
325
|
+
|
|
305
326
|
## Broken triggers fail loudly, not publicly
|
|
306
327
|
|
|
307
328
|
`data-product-tour="key"` is a contract between code and dashboard content.
|
|
@@ -32,12 +32,20 @@ module ProductTours
|
|
|
32
32
|
page_url: clean_page_url(params[:page_url]),
|
|
33
33
|
source: params[:source].to_s.presence
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
event_name = "product_tours.#{action}"
|
|
36
|
+
ActiveSupport::Notifications.instrument(event_name, payload)
|
|
37
|
+
notify_host(event_name, payload)
|
|
36
38
|
head :no_content
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
43
|
+
def notify_host(event_name, payload)
|
|
44
|
+
ProductTours.config.on_event.call(event_name, payload, request)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Rails.logger.error("product_tours: on_event hook raised #{e.class}: #{e.message}")
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
def post_payload(post)
|
|
42
50
|
video = post.resolved_video
|
|
43
51
|
video[:url] = media_path(post) if video&.dig(:kind) == 'upload'
|
|
@@ -21,6 +21,13 @@ ProductTours.configure do |config|
|
|
|
21
21
|
# Tutorials resolve in the page's current I18n.locale and fall back to
|
|
22
22
|
# I18n.default_locale when no translation exists.
|
|
23
23
|
|
|
24
|
+
# Optional lifecycle hook. Receives the event name, minimal payload, and raw
|
|
25
|
+
# request so the host can resolve its own user/account context. Runs inline;
|
|
26
|
+
# enqueue slow work. Hook errors are logged and never break the visitor flow.
|
|
27
|
+
# config.on_event = lambda do |name, payload, request|
|
|
28
|
+
# AnalyticsJob.perform_later(name, payload, request.session[:user_id])
|
|
29
|
+
# end
|
|
30
|
+
|
|
24
31
|
# Optional dedicated Active Storage service for uploaded videos.
|
|
25
32
|
# config.storage_service = :product_tours
|
|
26
33
|
|
|
@@ -7,7 +7,7 @@ module ProductTours
|
|
|
7
7
|
DEFAULT_ADMIN_LAYOUT = 'product_tours/application'
|
|
8
8
|
|
|
9
9
|
attr_accessor :enabled, :authorize_admin, :admin_layout, :mount_path,
|
|
10
|
-
:storage_service
|
|
10
|
+
:storage_service, :on_event
|
|
11
11
|
|
|
12
12
|
# The controller the DASHBOARD inherits from, as a String so it resolves
|
|
13
13
|
# lazily rather than at config time. Default: a plain
|
|
@@ -31,6 +31,7 @@ module ProductTours
|
|
|
31
31
|
@base_controller_class = 'ActionController::Base'
|
|
32
32
|
@mount_path = '/product_tours'
|
|
33
33
|
@storage_service = nil
|
|
34
|
+
@on_event = ->(_name, _payload, _request) {}
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def widget_endpoint = "#{mount_path.to_s.chomp('/')}/widget"
|
|
@@ -144,6 +144,19 @@
|
|
|
144
144
|
& .actions form { display: inline; }
|
|
145
145
|
& .form-actions { padding-top: 12px; }
|
|
146
146
|
|
|
147
|
+
/* Status switch: one pill holding every state a record can be in, the state it
|
|
148
|
+
is in now lit. Each other segment is a submit button, so moving a record
|
|
149
|
+
takes one click and no JavaScript; the current segment is a `type="button"`
|
|
150
|
+
so clicking it cannot re-file the record it is already filed under. Two
|
|
151
|
+
states or five, the pill sizes itself. A gem tints the lit segment with its
|
|
152
|
+
own status colour — see the `GEM-SPECIFIC` section. */
|
|
153
|
+
& .status-switch { display: inline-flex; flex: 0 0 auto; margin: 0; padding: 3px; border: 1px solid var(--pt-border); border-radius: 999px; background: var(--pt-bg); }
|
|
154
|
+
& .status-switch button { min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--pt-muted); font-size: 13px; font-weight: 700; white-space: nowrap; transition: background .16s ease, color .16s ease; }
|
|
155
|
+
& .status-switch button:hover { border: 0; background: color-mix(in srgb, var(--pt-muted) 12%, transparent); color: var(--pt-text); }
|
|
156
|
+
& .status-switch button.current { background: var(--pt-surface); color: var(--pt-text); box-shadow: 0 1px 2px rgba(15, 23, 42, .12); cursor: default; }
|
|
157
|
+
& .status-switch button.current:hover { background: var(--pt-surface); }
|
|
158
|
+
& .panel-head .status-switch { margin-left: auto; }
|
|
159
|
+
|
|
147
160
|
& .card { overflow: hidden; border: 1px solid var(--pt-border); border-radius: 12px; background: var(--pt-surface); }
|
|
148
161
|
& .card.pad { padding: 16px; overflow: visible; }
|
|
149
162
|
& table { width: 100%; border-collapse: collapse; }
|
|
@@ -266,6 +279,7 @@
|
|
|
266
279
|
& .dashboard-detail .detail-panel { min-height: calc(100vh - 64px); }
|
|
267
280
|
& .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
|
|
268
281
|
& .panel-head h1 { font-size: 18px; }
|
|
282
|
+
& .status-switch button { padding: 0 9px; font-size: 12px; }
|
|
269
283
|
& dl { grid-template-columns: 1fr; gap: 3px; }
|
|
270
284
|
& dd { margin-bottom: 8px; }
|
|
271
285
|
}
|
data/lib/product_tours/seeds.rb
CHANGED
|
@@ -7,107 +7,221 @@ module ProductTours
|
|
|
7
7
|
POSTS = [
|
|
8
8
|
{
|
|
9
9
|
key: 'demo_walkthrough_finish',
|
|
10
|
-
title: 'You
|
|
11
|
-
|
|
10
|
+
title: 'You completed the Product Tours tour',
|
|
11
|
+
description: '<p>Completing this step emits <code>product_tours.completed</code>. ' \
|
|
12
|
+
'Replace these demo posts with one short guide tied to a real customer success moment.</p>',
|
|
13
|
+
action_label: 'Finish tour'
|
|
12
14
|
},
|
|
13
15
|
{
|
|
14
16
|
key: 'demo_direct_video',
|
|
15
|
-
title: '
|
|
17
|
+
title: 'Keep video delivery flexible',
|
|
18
|
+
description: '<p>This placeholder uses a direct MP4. You can also attach a video with Active Storage; ' \
|
|
19
|
+
'uploaded media streams through the engine rather than a public blob URL.</p>',
|
|
16
20
|
video_url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
|
17
21
|
action_post_key: 'demo_walkthrough_finish'
|
|
18
22
|
},
|
|
19
23
|
{
|
|
20
24
|
key: 'demo_voomly',
|
|
21
|
-
title: '
|
|
25
|
+
title: 'Observe the lifecycle',
|
|
26
|
+
description: '<p>Subscribe to viewed, dismissed, completed, and unresolved-trigger notifications, ' \
|
|
27
|
+
'or use <code>config.on_event</code> when you need the raw request to resolve a user.</p>',
|
|
22
28
|
video_url: 'https://share.voomly.com/v/CxfDyNYKE0SBEBV-zVAUHo8rNwS2eLBr418z81gd6GlkKlLfJ',
|
|
23
29
|
action_post_key: 'demo_direct_video'
|
|
24
30
|
},
|
|
25
31
|
{
|
|
26
32
|
key: 'demo_tella',
|
|
27
|
-
title: '
|
|
33
|
+
title: 'Translate one stable key',
|
|
34
|
+
description: '<p>A tutorial keeps the same key in every locale. Product Tours resolves the visitor locale ' \
|
|
35
|
+
'and falls back to your default locale, so triggers never need translated keys.</p>',
|
|
28
36
|
video_url: 'https://www.tella.tv/video/add-your-logo-to-videos-39y0',
|
|
29
37
|
action_post_key: 'demo_voomly'
|
|
30
38
|
},
|
|
31
39
|
{
|
|
32
40
|
key: 'demo_loom',
|
|
33
|
-
title: '
|
|
41
|
+
title: 'Draft safely, then publish',
|
|
42
|
+
description: '<p>The seeded <code>demo_draft</code> post appears in the dashboard but cannot open for ' \
|
|
43
|
+
'visitors. Use drafts to review copy, actions, and video before a trigger goes live.</p>',
|
|
34
44
|
video_url: 'https://www.loom.com/share/e5b8c04bca094dd8a5507925ab887002',
|
|
35
45
|
action_post_key: 'demo_tella'
|
|
36
46
|
},
|
|
37
47
|
{
|
|
38
48
|
key: 'demo_vimeo',
|
|
39
|
-
title: '
|
|
49
|
+
title: 'Chain focused steps',
|
|
50
|
+
description: '<p>This Next button follows <code>action_post_key</code>. Each step is an ordinary tutorial, ' \
|
|
51
|
+
'so you can reuse it alone or connect it into a walkthrough without host-side JavaScript.</p>',
|
|
40
52
|
video_url: 'https://vimeo.com/76979871',
|
|
41
53
|
action_post_key: 'demo_loom'
|
|
42
54
|
},
|
|
43
55
|
{
|
|
44
56
|
key: 'demo_youtube',
|
|
45
|
-
title: '
|
|
57
|
+
title: 'Add video only when it helps',
|
|
58
|
+
description: '<p>This generic YouTube clip is only a provider example. Replace it with your own explanation, ' \
|
|
59
|
+
'or leave video blank for a fast text-only guide.</p>',
|
|
46
60
|
video_url: 'https://www.youtube.com/watch?v=M7lc1UVf-VE',
|
|
47
61
|
action_post_key: 'demo_vimeo'
|
|
48
62
|
},
|
|
49
63
|
{
|
|
50
64
|
key: 'demo_walkthrough_features',
|
|
51
|
-
title: '
|
|
65
|
+
title: 'Trigger a tour from your own UI',
|
|
66
|
+
description: '<p>Add <code>data-product-tour="demo_walkthrough_start"</code> to any button or link. ' \
|
|
67
|
+
'The single <code>product_tours_tag</code> in your layout handles every trigger on the page.</p>',
|
|
52
68
|
action_post_key: 'demo_youtube'
|
|
53
69
|
},
|
|
54
70
|
{
|
|
55
71
|
key: 'demo_walkthrough_start',
|
|
56
|
-
title: '
|
|
72
|
+
title: 'Welcome to Product Tours',
|
|
73
|
+
description: '<p>This is a real multi-step tour created by <code>product_tours:seed_demo</code>. ' \
|
|
74
|
+
'Continue to learn the public API by using the same experience your customers will see.</p>',
|
|
57
75
|
action_post_key: 'demo_walkthrough_features'
|
|
58
76
|
},
|
|
59
77
|
{
|
|
60
78
|
key: 'demo_getting_started',
|
|
61
|
-
title: '
|
|
62
|
-
|
|
79
|
+
title: 'Link to the next real action',
|
|
80
|
+
description: '<p>An action URL completes the tutorial and sends the visitor into your product. ' \
|
|
81
|
+
'Point it at the screen where they can immediately use what the guide taught.</p>',
|
|
82
|
+
action_label: 'Try the action URL',
|
|
63
83
|
action_url: '/settings'
|
|
64
84
|
},
|
|
65
85
|
{
|
|
66
86
|
key: 'demo_draft',
|
|
67
|
-
title: '
|
|
87
|
+
title: 'This draft stays invisible to visitors',
|
|
88
|
+
description: '<p>You can edit and preview this post in the dashboard, but its demo trigger will not resolve ' \
|
|
89
|
+
'until you publish it.</p>',
|
|
68
90
|
status: 'draft'
|
|
69
91
|
}
|
|
70
92
|
].freeze
|
|
71
93
|
|
|
72
94
|
BUTTONS = [
|
|
73
|
-
['demo_walkthrough_start', '
|
|
74
|
-
['demo_youtube', '
|
|
75
|
-
['demo_vimeo', '
|
|
76
|
-
['demo_loom', '
|
|
77
|
-
['demo_tella', '
|
|
78
|
-
['demo_voomly', '
|
|
79
|
-
['demo_direct_video', '
|
|
80
|
-
['demo_getting_started', '
|
|
81
|
-
['demo_draft', '
|
|
82
|
-
['demo_missing_post', '
|
|
95
|
+
['demo_walkthrough_start', 'Take the self-guided Product Tours tour'],
|
|
96
|
+
['demo_youtube', 'Preview a YouTube video step'],
|
|
97
|
+
['demo_vimeo', 'Preview a Vimeo video step'],
|
|
98
|
+
['demo_loom', 'Preview a Loom video step'],
|
|
99
|
+
['demo_tella', 'Preview a Tella video step'],
|
|
100
|
+
['demo_voomly', 'Preview a Voomly video step'],
|
|
101
|
+
['demo_direct_video', 'Preview a direct MP4 step'],
|
|
102
|
+
['demo_getting_started', 'Try a tour that opens a product page'],
|
|
103
|
+
['demo_draft', 'Confirm that draft tours stay hidden'],
|
|
104
|
+
['demo_missing_post', 'Confirm that missing keys are instrumented']
|
|
83
105
|
].freeze
|
|
84
106
|
|
|
85
107
|
TRANSLATIONS = {
|
|
86
108
|
'fr' => {
|
|
87
|
-
'demo_walkthrough_finish' => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
'
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
'demo_walkthrough_finish' => {
|
|
110
|
+
title: 'Vous avez terminé la visite de Product Tours',
|
|
111
|
+
description: '<p>Cette étape émet <code>product_tours.completed</code>. Remplacez ces exemples par ' \
|
|
112
|
+
'un guide court lié à un vrai moment de réussite client.</p>',
|
|
113
|
+
action_label: 'Terminer la visite'
|
|
114
|
+
},
|
|
115
|
+
'demo_walkthrough_features' => {
|
|
116
|
+
title: 'Déclenchez une visite depuis votre interface',
|
|
117
|
+
description: '<p>Ajoutez <code>data-product-tour="demo_walkthrough_start"</code> à un bouton ou un lien. ' \
|
|
118
|
+
'<code>product_tours_tag</code> gère tous les déclencheurs de la page.</p>'
|
|
119
|
+
},
|
|
120
|
+
'demo_walkthrough_start' => {
|
|
121
|
+
title: 'Bienvenue dans Product Tours',
|
|
122
|
+
description: '<p>Ceci est une vraie visite en plusieurs étapes créée par ' \
|
|
123
|
+
'<code>product_tours:seed_demo</code>. Continuez pour découvrir son API publique.</p>'
|
|
124
|
+
},
|
|
125
|
+
'demo_youtube' => {
|
|
126
|
+
title: 'Ajoutez une vidéo seulement si elle aide',
|
|
127
|
+
description: '<p>Cette vidéo YouTube générique illustre seulement le fournisseur. Remplacez-la par la ' \
|
|
128
|
+
'vôtre, ou laissez le champ vide pour un guide textuel rapide.</p>'
|
|
129
|
+
},
|
|
130
|
+
'demo_vimeo' => {
|
|
131
|
+
title: 'Enchaînez des étapes ciblées',
|
|
132
|
+
description: '<p>Le bouton Suivant utilise <code>action_post_key</code>. Reliez des guides sans écrire ' \
|
|
133
|
+
'de JavaScript dans votre application.</p>'
|
|
134
|
+
},
|
|
135
|
+
'demo_loom' => {
|
|
136
|
+
title: 'Préparez en brouillon, puis publiez',
|
|
137
|
+
description: '<p>Le post <code>demo_draft</code> est visible dans le tableau de bord, mais pas pour les ' \
|
|
138
|
+
'visiteurs. Vérifiez le contenu avant de publier.</p>'
|
|
139
|
+
},
|
|
140
|
+
'demo_tella' => {
|
|
141
|
+
title: 'Traduisez une clé stable',
|
|
142
|
+
description: '<p>Une visite garde la même clé dans chaque langue. Le déclencheur ne doit donc jamais ' \
|
|
143
|
+
'connaître la clé traduite.</p>'
|
|
144
|
+
},
|
|
145
|
+
'demo_voomly' => {
|
|
146
|
+
title: 'Observez le cycle de vie',
|
|
147
|
+
description: '<p>Abonnez-vous aux événements viewed, dismissed, completed et unresolved-trigger, ' \
|
|
148
|
+
'ou utilisez <code>config.on_event</code> avec la requête.</p>'
|
|
149
|
+
},
|
|
150
|
+
'demo_direct_video' => {
|
|
151
|
+
title: 'Gardez le choix de la diffusion vidéo',
|
|
152
|
+
description: '<p>Cet exemple utilise un MP4 direct. Active Storage permet aussi les fichiers envoyés, ' \
|
|
153
|
+
'diffusés par le moteur sans URL de blob publique.</p>'
|
|
154
|
+
},
|
|
155
|
+
'demo_getting_started' => {
|
|
156
|
+
title: "Ouvrez l'action réelle suivante",
|
|
157
|
+
description: '<p>Une URL d’action termine la visite et envoie le visiteur vers la page où il peut ' \
|
|
158
|
+
'appliquer ce qu’il vient d’apprendre.</p>',
|
|
159
|
+
action_label: "Essayer l'URL d'action"
|
|
160
|
+
},
|
|
161
|
+
'demo_draft' => {
|
|
162
|
+
title: 'Ce brouillon reste invisible aux visiteurs',
|
|
163
|
+
description: '<p>Modifiez-le dans le tableau de bord : son déclencheur ne fonctionnera qu’après sa ' \
|
|
164
|
+
'publication.</p>'
|
|
165
|
+
}
|
|
98
166
|
},
|
|
99
167
|
'bg' => {
|
|
100
|
-
'demo_walkthrough_finish' => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
'
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
'demo_walkthrough_finish' => {
|
|
169
|
+
title: 'Завършихте обиколката на Product Tours',
|
|
170
|
+
description: '<p>Тази стъпка излъчва <code>product_tours.completed</code>. Заменете примерите с кратък ' \
|
|
171
|
+
'урок, свързан с реален успех на клиента.</p>',
|
|
172
|
+
action_label: 'Край на обиколката'
|
|
173
|
+
},
|
|
174
|
+
'demo_walkthrough_features' => {
|
|
175
|
+
title: 'Стартирайте обиколка от вашия интерфейс',
|
|
176
|
+
description: '<p>Добавете <code>data-product-tour="demo_walkthrough_start"</code> към бутон или връзка. ' \
|
|
177
|
+
'<code>product_tours_tag</code> обслужва всички тригери на страницата.</p>'
|
|
178
|
+
},
|
|
179
|
+
'demo_walkthrough_start' => {
|
|
180
|
+
title: 'Добре дошли в Product Tours',
|
|
181
|
+
description: '<p>Това е истинска обиколка от няколко стъпки, създадена от ' \
|
|
182
|
+
'<code>product_tours:seed_demo</code>. Продължете, за да научите публичния API.</p>'
|
|
183
|
+
},
|
|
184
|
+
'demo_youtube' => {
|
|
185
|
+
title: 'Добавяйте видео само когато помага',
|
|
186
|
+
description: '<p>Този общ YouTube клип е само пример за доставчик. Заменете го със свой или оставете ' \
|
|
187
|
+
'полето празно за бърз текстов урок.</p>'
|
|
188
|
+
},
|
|
189
|
+
'demo_vimeo' => {
|
|
190
|
+
title: 'Свържете кратки, фокусирани стъпки',
|
|
191
|
+
description: '<p>Бутонът Напред използва <code>action_post_key</code>. Свързвайте уроци без JavaScript ' \
|
|
192
|
+
'в приложението домакин.</p>'
|
|
193
|
+
},
|
|
194
|
+
'demo_loom' => {
|
|
195
|
+
title: 'Подгответе чернова, после публикувайте',
|
|
196
|
+
description: '<p>Публикацията <code>demo_draft</code> се вижда в таблото, но не и от посетителите. ' \
|
|
197
|
+
'Прегледайте съдържанието преди публикуване.</p>'
|
|
198
|
+
},
|
|
199
|
+
'demo_tella' => {
|
|
200
|
+
title: 'Преведете един постоянен ключ',
|
|
201
|
+
description: '<p>Една обиколка използва същия ключ на всеки език. Тригерът никога не се нуждае от ' \
|
|
202
|
+
'преведен ключ.</p>'
|
|
203
|
+
},
|
|
204
|
+
'demo_voomly' => {
|
|
205
|
+
title: 'Наблюдавайте жизнения цикъл',
|
|
206
|
+
description: '<p>Абонирайте се за viewed, dismissed, completed и unresolved-trigger или използвайте ' \
|
|
207
|
+
'<code>config.on_event</code> с текущата заявка.</p>'
|
|
208
|
+
},
|
|
209
|
+
'demo_direct_video' => {
|
|
210
|
+
title: 'Изберете как да доставяте видеото',
|
|
211
|
+
description: '<p>Този пример използва директен MP4. Active Storage поддържа и качени файлове, които ' \
|
|
212
|
+
'се предават през двигателя без публичен blob URL.</p>'
|
|
213
|
+
},
|
|
214
|
+
'demo_getting_started' => {
|
|
215
|
+
title: 'Отворете следващото реално действие',
|
|
216
|
+
description: '<p>URL действието завършва урока и отвежда посетителя до мястото, където може веднага ' \
|
|
217
|
+
'да използва наученото.</p>',
|
|
218
|
+
action_label: 'Изпробвайте URL действието'
|
|
219
|
+
},
|
|
220
|
+
'demo_draft' => {
|
|
221
|
+
title: 'Тази чернова остава скрита за посетители',
|
|
222
|
+
description: '<p>Редактирайте я в таблото; нейният тригер няма да работи, докато не я ' \
|
|
223
|
+
'публикувате.</p>'
|
|
224
|
+
}
|
|
111
225
|
}
|
|
112
226
|
}.freeze
|
|
113
227
|
|
|
@@ -117,7 +231,10 @@ module ProductTours
|
|
|
117
231
|
key = attributes.fetch(:key)
|
|
118
232
|
localized = TRANSLATIONS.fetch(locale, {}).fetch(key, {})
|
|
119
233
|
post = ProductTours::Post.find_or_initialize_by(locale: locale, key: key)
|
|
120
|
-
|
|
234
|
+
seed_attributes = attributes.merge(localized).merge(status: attributes.fetch(:status, status))
|
|
235
|
+
description = seed_attributes.delete(:description)
|
|
236
|
+
post.assign_attributes(seed_attributes)
|
|
237
|
+
post.description = description if ProductTours::Post.description_supported?
|
|
121
238
|
post.save!
|
|
122
239
|
post
|
|
123
240
|
end
|
data/lib/product_tours/widget.js
CHANGED
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
|
|
91
91
|
function renderPost(post, source) {
|
|
92
92
|
if (!dialog) return;
|
|
93
|
+
var hasVideo = Boolean(post.video && post.video.url);
|
|
94
|
+
if (hasVideo) dialog.classList.add("pt-dialog-video");
|
|
93
95
|
var existing = dialog.querySelector(".pt-content");
|
|
94
96
|
if (existing) existing.remove();
|
|
95
97
|
currentPost = post;
|
|
@@ -108,7 +110,7 @@
|
|
|
108
110
|
head.appendChild(closeButton);
|
|
109
111
|
content.appendChild(head);
|
|
110
112
|
|
|
111
|
-
if (
|
|
113
|
+
if (hasVideo) content.appendChild(videoNode(post.video, post.title));
|
|
112
114
|
|
|
113
115
|
if (post.descriptionHtml) {
|
|
114
116
|
var description = element("div", "pt-description");
|
|
@@ -332,6 +334,7 @@
|
|
|
332
334
|
".pt-overlay{position:fixed;inset:0;z-index:2147482000;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:16px}",
|
|
333
335
|
".pt-overlay,.pt-overlay *{box-sizing:border-box}",
|
|
334
336
|
".pt-dialog{width:100%;max-width:440px;max-height:92vh;overflow:auto;overscroll-behavior:contain;background:#fff;color:#1c2024;border-radius:14px;padding:20px;font:14px/1.5 system-ui,-apple-system,sans-serif;box-shadow:0 20px 60px rgba(0,0,0,.35);outline:none}",
|
|
337
|
+
".pt-dialog-video{max-width:760px}",
|
|
335
338
|
".pt-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin:0 0 12px}",
|
|
336
339
|
".pt-title{margin:0;font-size:17px;line-height:1.5;font-weight:700;overflow-wrap:anywhere}",
|
|
337
340
|
".pt-close{border:0;background:none;font-size:22px;line-height:1;cursor:pointer;color:inherit;padding:2px 6px}",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: product_tours
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -131,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
131
|
- !ruby/object:Gem::Version
|
|
132
132
|
version: '0'
|
|
133
133
|
requirements: []
|
|
134
|
-
rubygems_version:
|
|
134
|
+
rubygems_version: 4.0.10
|
|
135
135
|
specification_version: 4
|
|
136
136
|
summary: Self-hosted product tours and video tutorials for Rails apps.
|
|
137
137
|
test_files: []
|