rails_onboarding 0.6.1
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/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- metadata +303 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Chart controller
|
|
4
|
+
// Handles chart animations and interactions
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["bar"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.animateBars()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
animateBars() {
|
|
13
|
+
if (!this.hasBarTarget) return
|
|
14
|
+
|
|
15
|
+
this.barTargets.forEach((bar, index) => {
|
|
16
|
+
const finalHeight = bar.style.height
|
|
17
|
+
bar.style.height = '0%'
|
|
18
|
+
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
bar.style.transition = 'height 0.5s ease-out'
|
|
21
|
+
bar.style.height = finalHeight
|
|
22
|
+
}, index * 100)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Filter controller
|
|
4
|
+
// Handles filtering and search functionality
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["searchInput", "statusSelect", "table", "row"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.filterRows()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
filter() {
|
|
13
|
+
this.filterRows()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
filterRows() {
|
|
17
|
+
if (!this.hasRowTarget) return
|
|
18
|
+
|
|
19
|
+
const searchTerm = this.hasSearchInputTarget ?
|
|
20
|
+
this.searchInputTarget.value.toLowerCase() : ''
|
|
21
|
+
|
|
22
|
+
const statusFilter = this.hasStatusSelectTarget ?
|
|
23
|
+
this.statusSelectTarget.value : ''
|
|
24
|
+
|
|
25
|
+
this.rowTargets.forEach(row => {
|
|
26
|
+
const rowText = row.textContent.toLowerCase()
|
|
27
|
+
const rowStatus = row.dataset.status || ''
|
|
28
|
+
|
|
29
|
+
const matchesSearch = searchTerm === '' || rowText.includes(searchTerm)
|
|
30
|
+
const matchesStatus = statusFilter === '' || rowStatus === statusFilter
|
|
31
|
+
|
|
32
|
+
if (matchesSearch && matchesStatus) {
|
|
33
|
+
row.style.display = ''
|
|
34
|
+
} else {
|
|
35
|
+
row.style.display = 'none'
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reset() {
|
|
41
|
+
if (this.hasSearchInputTarget) {
|
|
42
|
+
this.searchInputTarget.value = ''
|
|
43
|
+
}
|
|
44
|
+
if (this.hasStatusSelectTarget) {
|
|
45
|
+
this.statusSelectTarget.selectedIndex = 0
|
|
46
|
+
}
|
|
47
|
+
this.filterRows()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Flash message controller
|
|
4
|
+
// Handles auto-dismissing flash messages
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
connect() {
|
|
7
|
+
// Auto-dismiss after 5 seconds
|
|
8
|
+
setTimeout(() => {
|
|
9
|
+
this.dismiss()
|
|
10
|
+
}, 5000)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dismiss() {
|
|
14
|
+
this.element.style.transition = 'opacity 0.3s ease-out'
|
|
15
|
+
this.element.style.opacity = '0'
|
|
16
|
+
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
this.element.remove()
|
|
19
|
+
}, 300)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Flow editor controller
|
|
4
|
+
// Handles dynamic step management in flow editor
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["stepsContainer"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.stepIndex = this.getMaxStepIndex()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
addStep(event) {
|
|
13
|
+
event.preventDefault()
|
|
14
|
+
|
|
15
|
+
this.stepIndex++
|
|
16
|
+
const stepNumber = this.stepIndex + 1
|
|
17
|
+
|
|
18
|
+
const template = document.getElementById('step-template')
|
|
19
|
+
if (!template) return
|
|
20
|
+
|
|
21
|
+
const stepHtml = template.innerHTML
|
|
22
|
+
.replace(/__INDEX__/g, this.stepIndex)
|
|
23
|
+
.replace(/__NUMBER__/g, stepNumber)
|
|
24
|
+
|
|
25
|
+
const stepsList = document.getElementById('steps-list')
|
|
26
|
+
if (!stepsList) return
|
|
27
|
+
|
|
28
|
+
// Remove empty state message if it exists
|
|
29
|
+
const emptyMessage = stepsList.querySelector('.admin-text-muted')
|
|
30
|
+
if (emptyMessage) {
|
|
31
|
+
emptyMessage.remove()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stepsList.insertAdjacentHTML('beforeend', stepHtml)
|
|
35
|
+
this.updateStepNumbers()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
removeStep(event) {
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
|
|
41
|
+
if (!confirm('Are you sure you want to remove this step?')) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stepItem = event.target.closest('.admin-step-item')
|
|
46
|
+
if (!stepItem) return
|
|
47
|
+
|
|
48
|
+
stepItem.remove()
|
|
49
|
+
this.updateStepNumbers()
|
|
50
|
+
|
|
51
|
+
// Show empty message if no steps remain
|
|
52
|
+
const stepsList = document.getElementById('steps-list')
|
|
53
|
+
if (stepsList && stepsList.children.length === 0) {
|
|
54
|
+
stepsList.innerHTML = '<p class="admin-text-muted">No steps added yet. Click "Add Step" to get started.</p>'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
updateStepNumbers() {
|
|
59
|
+
const stepItems = document.querySelectorAll('.admin-step-item')
|
|
60
|
+
stepItems.forEach((item, index) => {
|
|
61
|
+
const stepNumber = item.querySelector('.admin-step-number')
|
|
62
|
+
if (stepNumber) {
|
|
63
|
+
stepNumber.textContent = index + 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const orderInput = item.querySelector('input[type="hidden"][name*="[order]"]')
|
|
67
|
+
if (orderInput) {
|
|
68
|
+
orderInput.value = index
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getMaxStepIndex() {
|
|
74
|
+
const stepItems = document.querySelectorAll('.admin-step-item')
|
|
75
|
+
let maxIndex = -1
|
|
76
|
+
|
|
77
|
+
stepItems.forEach(item => {
|
|
78
|
+
const index = parseInt(item.dataset.stepIndex || '0')
|
|
79
|
+
if (index > maxIndex) {
|
|
80
|
+
maxIndex = index
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return maxIndex
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rails Onboarding JavaScript Application
|
|
3
|
+
*
|
|
4
|
+
* This file provides flexible integration with different JavaScript bundling strategies:
|
|
5
|
+
* - Stimulus (via importmap or bundler)
|
|
6
|
+
* - Standalone (without Stimulus)
|
|
7
|
+
* - ESBuild/Webpack/Vite
|
|
8
|
+
*
|
|
9
|
+
* The code automatically detects the environment and adapts accordingly.
|
|
10
|
+
*
|
|
11
|
+
* Compatibility:
|
|
12
|
+
* - Works with Asset Pipeline (Sprockets)
|
|
13
|
+
* - Works with Propshaft
|
|
14
|
+
* - Works with importmap-rails
|
|
15
|
+
* - Works with esbuild, webpack, vite, etc.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
(function() {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
// Initialize Stimulus application if available
|
|
22
|
+
function initializeStimulusIfAvailable() {
|
|
23
|
+
// Check if Stimulus is already loaded
|
|
24
|
+
if (window.Stimulus) {
|
|
25
|
+
console.log('RailsOnboarding: Using existing Stimulus application');
|
|
26
|
+
return window.Stimulus;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if we can create a new Stimulus application
|
|
30
|
+
if (typeof Application !== 'undefined') {
|
|
31
|
+
const app = Application.start();
|
|
32
|
+
app.debug = false;
|
|
33
|
+
window.Stimulus = app;
|
|
34
|
+
console.log('RailsOnboarding: Created new Stimulus application');
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.debug('RailsOnboarding: Stimulus not available, using standalone mode');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Auto-initialize when DOM is ready
|
|
43
|
+
if (document.readyState === 'loading') {
|
|
44
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
45
|
+
initializeStimulusIfAvailable();
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
initializeStimulusIfAvailable();
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
// Global utilities for onboarding
|
|
53
|
+
window.RailsOnboarding = {
|
|
54
|
+
// Show a global tooltip
|
|
55
|
+
showTooltip: function(element, content, options = {}) {
|
|
56
|
+
const tooltip = new TooltipController()
|
|
57
|
+
tooltip.element = element
|
|
58
|
+
tooltip.featureValue = options.feature || ''
|
|
59
|
+
tooltip.positionValue = options.position || 'top'
|
|
60
|
+
tooltip.delayValue = options.delay || 0
|
|
61
|
+
tooltip.dismissibleValue = options.dismissible !== false
|
|
62
|
+
|
|
63
|
+
// Override content method
|
|
64
|
+
tooltip.getTooltipContent = () => content
|
|
65
|
+
|
|
66
|
+
tooltip.connect()
|
|
67
|
+
tooltip.forceShow()
|
|
68
|
+
|
|
69
|
+
return tooltip
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Hide all tooltips
|
|
73
|
+
hideAllTooltips: function() {
|
|
74
|
+
document.querySelectorAll('.onboarding-tooltip').forEach(tooltip => {
|
|
75
|
+
if (tooltip.parentNode) {
|
|
76
|
+
tooltip.parentNode.removeChild(tooltip)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Restart onboarding (if supported by backend)
|
|
82
|
+
restartOnboarding: function() {
|
|
83
|
+
if (confirm('Are you sure you want to restart the onboarding process?')) {
|
|
84
|
+
if (typeof Rails !== 'undefined' && Rails.ajax) {
|
|
85
|
+
Rails.ajax({
|
|
86
|
+
url: '/rails_onboarding/onboarding/restart',
|
|
87
|
+
type: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content
|
|
90
|
+
},
|
|
91
|
+
success: function() {
|
|
92
|
+
window.location.reload()
|
|
93
|
+
},
|
|
94
|
+
error: function() {
|
|
95
|
+
alert('Failed to restart onboarding. Please try again.')
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Skip to end of onboarding
|
|
103
|
+
skipToEnd: function() {
|
|
104
|
+
if (confirm('Are you sure you want to skip the remaining onboarding steps?')) {
|
|
105
|
+
const skipForm = document.createElement('form')
|
|
106
|
+
skipForm.method = 'POST'
|
|
107
|
+
skipForm.action = '/rails_onboarding/onboarding/complete'
|
|
108
|
+
|
|
109
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
110
|
+
if (csrfToken) {
|
|
111
|
+
const csrfInput = document.createElement('input')
|
|
112
|
+
csrfInput.type = 'hidden'
|
|
113
|
+
csrfInput.name = 'authenticity_token'
|
|
114
|
+
csrfInput.value = csrfToken
|
|
115
|
+
skipForm.appendChild(csrfInput)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
document.body.appendChild(skipForm)
|
|
119
|
+
skipForm.submit()
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Get current onboarding progress
|
|
124
|
+
getProgress: function() {
|
|
125
|
+
const progressController = document.querySelector('[data-controller*="progress"]')
|
|
126
|
+
if (progressController) {
|
|
127
|
+
return {
|
|
128
|
+
current: progressController.dataset.progressCurrentStepValue || 1,
|
|
129
|
+
total: progressController.dataset.progressTotalStepsValue || 4,
|
|
130
|
+
percentage: progressController.dataset.progressValue || 0
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Add custom step validation
|
|
137
|
+
addStepValidation: function(stepName, validationFunction) {
|
|
138
|
+
const onboardingController = document.querySelector('[data-controller*="onboarding"]')
|
|
139
|
+
if (onboardingController && onboardingController.stimulus) {
|
|
140
|
+
const controller = onboardingController.stimulus
|
|
141
|
+
if (!controller.customValidations) {
|
|
142
|
+
controller.customValidations = {}
|
|
143
|
+
}
|
|
144
|
+
controller.customValidations[stepName] = validationFunction
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Auto-initialize tooltips for elements with data-tooltip attribute
|
|
150
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
151
|
+
document.querySelectorAll('[data-tooltip]').forEach(element => {
|
|
152
|
+
const tooltipText = element.dataset.tooltip
|
|
153
|
+
const feature = element.dataset.tooltipFeature
|
|
154
|
+
const position = element.dataset.tooltipPosition || 'top'
|
|
155
|
+
const delay = parseInt(element.dataset.tooltipDelay) || 0
|
|
156
|
+
|
|
157
|
+
if (tooltipText && !element.dataset.controllerAdded) {
|
|
158
|
+
element.dataset.controller = (element.dataset.controller || '') + ' tooltip'
|
|
159
|
+
element.dataset.tooltipFeatureValue = feature || ''
|
|
160
|
+
element.dataset.tooltipPositionValue = position
|
|
161
|
+
element.dataset.tooltipDelayValue = delay
|
|
162
|
+
element.dataset.tooltipText = tooltipText
|
|
163
|
+
element.dataset.controllerAdded = 'true'
|
|
164
|
+
|
|
165
|
+
// Re-initialize Stimulus for this element
|
|
166
|
+
application.register("tooltip", TooltipController)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Handle onboarding completion celebration
|
|
172
|
+
document.addEventListener('onboarding:completed', function(event) {
|
|
173
|
+
// Show celebration animation or message
|
|
174
|
+
const celebration = document.createElement('div')
|
|
175
|
+
celebration.innerHTML = `
|
|
176
|
+
<div style="
|
|
177
|
+
position: fixed;
|
|
178
|
+
top: 50%;
|
|
179
|
+
left: 50%;
|
|
180
|
+
transform: translate(-50%, -50%);
|
|
181
|
+
background: white;
|
|
182
|
+
padding: 2rem;
|
|
183
|
+
border-radius: 1rem;
|
|
184
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
185
|
+
text-align: center;
|
|
186
|
+
z-index: 9999;
|
|
187
|
+
">
|
|
188
|
+
<div style="font-size: 3rem; margin-bottom: 1rem;">🎉</div>
|
|
189
|
+
<h2 style="margin-bottom: 1rem; color: #1f2937;">Congratulations!</h2>
|
|
190
|
+
<p style="color: #6b7280; margin-bottom: 1.5rem;">You've completed the onboarding process.</p>
|
|
191
|
+
<button onclick="this.parentElement.parentElement.remove()" style="
|
|
192
|
+
background: #6366f1;
|
|
193
|
+
color: white;
|
|
194
|
+
border: none;
|
|
195
|
+
padding: 0.75rem 1.5rem;
|
|
196
|
+
border-radius: 0.5rem;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
font-weight: 600;
|
|
199
|
+
">Get Started</button>
|
|
200
|
+
</div>
|
|
201
|
+
`
|
|
202
|
+
|
|
203
|
+
document.body.appendChild(celebration)
|
|
204
|
+
|
|
205
|
+
// Auto-remove after 5 seconds
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (celebration.parentNode) {
|
|
208
|
+
celebration.remove()
|
|
209
|
+
}
|
|
210
|
+
}, 5000)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Handle page transitions during onboarding
|
|
214
|
+
document.addEventListener('turbo:before-visit', function(event) {
|
|
215
|
+
// Hide tooltips before navigation
|
|
216
|
+
RailsOnboarding.hideAllTooltips()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Re-initialize onboarding after Turbo navigation
|
|
220
|
+
document.addEventListener('turbo:load', function() {
|
|
221
|
+
// Refresh any dynamic onboarding elements
|
|
222
|
+
const onboardingContainer = document.querySelector('.onboarding-container')
|
|
223
|
+
if (onboardingContainer) {
|
|
224
|
+
onboardingContainer.setAttribute('data-step-changed', 'true')
|
|
225
|
+
|
|
226
|
+
// Remove the attribute after animation
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
onboardingContainer.removeAttribute('data-step-changed')
|
|
229
|
+
}, 300)
|
|
230
|
+
}
|
|
231
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["confetti"]
|
|
5
|
+
// milestonesPath is supplied by the view. The engine is mounted at a
|
|
6
|
+
// host-chosen prefix, so a literal "/milestones" is wrong in most apps.
|
|
7
|
+
static values = { milestone: Object, milestonesPath: String }
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.renderContentIfEmpty()
|
|
11
|
+
this.showCelebration()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The celebration is normally rendered server-side by
|
|
15
|
+
// rails_onboarding/shared/_milestone_celebration. It can also be injected
|
|
16
|
+
// at runtime by milestone_dashboard_controller, which knows only the
|
|
17
|
+
// milestone's JSON - that injected element is an empty shell, and without
|
|
18
|
+
// this it showed as a full-screen backdrop containing nothing, with no
|
|
19
|
+
// dismiss control and no confetti container. Build the same structure the
|
|
20
|
+
// partial produces so the existing CSS applies either way.
|
|
21
|
+
renderContentIfEmpty() {
|
|
22
|
+
if (this.element.querySelector(".celebration-modal")) return
|
|
23
|
+
|
|
24
|
+
const milestone = this.hasMilestoneValue ? this.milestoneValue : {}
|
|
25
|
+
const el = (tag, className, text) => {
|
|
26
|
+
const node = document.createElement(tag)
|
|
27
|
+
if (className) node.className = className
|
|
28
|
+
if (text !== undefined && text !== null) node.textContent = text
|
|
29
|
+
return node
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const backdrop = el("div", "celebration-backdrop")
|
|
33
|
+
backdrop.dataset.action = "click->milestone-celebration#dismiss"
|
|
34
|
+
|
|
35
|
+
const iconContainer = el("div", "achievement-icon-container")
|
|
36
|
+
iconContainer.append(el("div", "achievement-icon-glow"),
|
|
37
|
+
el("div", "achievement-icon", milestone.icon))
|
|
38
|
+
|
|
39
|
+
const points = el("div", "points-earned")
|
|
40
|
+
points.append(el("span", "points-label", "You earned"),
|
|
41
|
+
el("span", "points-value", `+${milestone.points ?? 0}`),
|
|
42
|
+
el("span", "points-label", "points!"))
|
|
43
|
+
|
|
44
|
+
const announcement = el("div", "achievement-announcement")
|
|
45
|
+
announcement.append(iconContainer,
|
|
46
|
+
el("h2", "achievement-title", "Milestone Achieved!"),
|
|
47
|
+
el("h3", "milestone-name", milestone.title),
|
|
48
|
+
el("p", "milestone-description", milestone.description),
|
|
49
|
+
points)
|
|
50
|
+
|
|
51
|
+
const confetti = el("div", "confetti-container")
|
|
52
|
+
confetti.dataset.milestoneCelebrationTarget = "confetti"
|
|
53
|
+
|
|
54
|
+
const continueBtn = el("button", "onboarding-btn onboarding-btn-primary", "Continue")
|
|
55
|
+
continueBtn.type = "button"
|
|
56
|
+
continueBtn.dataset.action = "click->milestone-celebration#dismiss"
|
|
57
|
+
|
|
58
|
+
const actions = el("div", "celebration-actions")
|
|
59
|
+
actions.append(continueBtn)
|
|
60
|
+
|
|
61
|
+
// Only offer "View All" when the view told us where that lives.
|
|
62
|
+
if (this.hasMilestonesPathValue && this.milestonesPathValue) {
|
|
63
|
+
const allBtn = el("button", "onboarding-btn onboarding-btn-secondary", "View All Achievements")
|
|
64
|
+
allBtn.type = "button"
|
|
65
|
+
allBtn.dataset.action = "click->milestone-celebration#viewAllMilestones"
|
|
66
|
+
actions.append(allBtn)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const content = el("div", "celebration-content")
|
|
70
|
+
content.append(confetti, announcement, actions)
|
|
71
|
+
|
|
72
|
+
const modal = el("div", "celebration-modal")
|
|
73
|
+
modal.append(content)
|
|
74
|
+
|
|
75
|
+
this.element.append(backdrop, modal)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
showCelebration() {
|
|
79
|
+
// Show the modal with animation. .is-hidden/.is-fading (and the
|
|
80
|
+
// opacity transition) live in milestones.css, so this only toggles
|
|
81
|
+
// classes rather than writing display/opacity/transition inline.
|
|
82
|
+
this.element.classList.remove("is-hidden")
|
|
83
|
+
this.element.classList.add("is-fading")
|
|
84
|
+
|
|
85
|
+
// Trigger animations
|
|
86
|
+
requestAnimationFrame(() => {
|
|
87
|
+
this.element.classList.remove("is-fading")
|
|
88
|
+
|
|
89
|
+
// Start confetti animation
|
|
90
|
+
this.startConfetti()
|
|
91
|
+
|
|
92
|
+
// Auto-dismiss after 5 seconds if user doesn't interact
|
|
93
|
+
this.autoDismissTimer = setTimeout(() => {
|
|
94
|
+
this.dismiss()
|
|
95
|
+
}, 8000)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
startConfetti() {
|
|
100
|
+
// Fall back to a direct lookup: when renderContentIfEmpty() has just
|
|
101
|
+
// inserted the container, Stimulus may not have registered the target yet.
|
|
102
|
+
const confettiContainer = this.hasConfettiTarget
|
|
103
|
+
? this.confettiTarget
|
|
104
|
+
: this.element.querySelector(".confetti-container")
|
|
105
|
+
if (!confettiContainer) return
|
|
106
|
+
const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']
|
|
107
|
+
|
|
108
|
+
// Create confetti pieces
|
|
109
|
+
for (let i = 0; i < 50; i++) {
|
|
110
|
+
setTimeout(() => this.createConfettiPiece(confettiContainer, colors), i * 50)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
createConfettiPiece(container, colors) {
|
|
115
|
+
const confetti = document.createElement('div')
|
|
116
|
+
confetti.className = 'confetti-piece'
|
|
117
|
+
|
|
118
|
+
// Random properties
|
|
119
|
+
const color = colors[Math.floor(Math.random() * colors.length)]
|
|
120
|
+
const size = Math.random() * 8 + 4
|
|
121
|
+
const left = Math.random() * 100
|
|
122
|
+
const animationDuration = Math.random() * 2 + 2
|
|
123
|
+
const rotation = Math.random() * 360
|
|
124
|
+
|
|
125
|
+
confetti.style.cssText = `
|
|
126
|
+
position: absolute;
|
|
127
|
+
width: ${size}px;
|
|
128
|
+
height: ${size}px;
|
|
129
|
+
background-color: ${color};
|
|
130
|
+
left: ${left}%;
|
|
131
|
+
top: -10px;
|
|
132
|
+
border-radius: 50%;
|
|
133
|
+
animation: onboarding-confetti-fall ${animationDuration}s linear forwards;
|
|
134
|
+
transform: rotate(${rotation}deg);
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
container.appendChild(confetti)
|
|
139
|
+
|
|
140
|
+
// Remove confetti piece after animation
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (confetti.parentNode) {
|
|
143
|
+
confetti.parentNode.removeChild(confetti)
|
|
144
|
+
}
|
|
145
|
+
}, animationDuration * 1000)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dismiss() {
|
|
149
|
+
if (this.autoDismissTimer) {
|
|
150
|
+
clearTimeout(this.autoDismissTimer)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.element.classList.add("is-fading")
|
|
154
|
+
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
this.element.classList.add("is-hidden")
|
|
157
|
+
// Remove the element from DOM if it was dynamically added
|
|
158
|
+
if (this.element.dataset.temporary === "true") {
|
|
159
|
+
this.element.remove()
|
|
160
|
+
}
|
|
161
|
+
}, 300)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
viewAllMilestones() {
|
|
165
|
+
this.dismiss()
|
|
166
|
+
// Supplied by the view via the engine's route helper; a literal
|
|
167
|
+
// "/milestones" only resolves when the engine is mounted at root.
|
|
168
|
+
if (this.hasMilestonesPathValue && this.milestonesPathValue) {
|
|
169
|
+
window.location.href = this.milestonesPathValue
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Prevent dismissal when clicking inside the modal
|
|
174
|
+
preventDismiss(event) {
|
|
175
|
+
event.stopPropagation()
|
|
176
|
+
}
|
|
177
|
+
}
|