diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0cbd1a8..829d351 100755
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,6 +5,9 @@ class ApplicationController < ActionController::Base
# Ensures that all non-GET requests include a valid authenticity token
protect_from_forgery with: :exception
+ # Redirect authenticated users to onboarding if not completed
+ before_action :require_onboarding_completion
+
# Restrict access to modern browsers only
# Requires browsers to support modern web standards:
# - WebP images for better compression
@@ -14,4 +17,29 @@ class ApplicationController < ActionController::Base
# - CSS nesting and :has() pseudo-class
# allow_browser versions: :modern
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
+
+ private
+
+ def require_onboarding_completion
+ # Skip onboarding check for these paths
+ return if skip_onboarding_check?
+
+ # Only apply to signed-in users
+ if user_signed_in? && current_user.needs_onboarding?
+ redirect_to onboarding_path unless request.path == onboarding_path
+ end
+ end
+
+ def skip_onboarding_check?
+ # Skip for devise controllers (login, signup, password reset, etc.)
+ devise_controller? ||
+ # Skip for onboarding controller itself
+ controller_name == "onboarding" ||
+ # Skip for API endpoints
+ controller_name.start_with?("api/") ||
+ # Skip for health checks
+ controller_name == "rails/health" ||
+ # Skip for home page (when not signed in)
+ (controller_name == "pages" && action_name == "home")
+ end
end
diff --git a/app/controllers/onboarding_controller.rb b/app/controllers/onboarding_controller.rb
new file mode 100644
index 0000000..8115bf8
--- /dev/null
+++ b/app/controllers/onboarding_controller.rb
@@ -0,0 +1,38 @@
+class OnboardingController < ApplicationController
+ before_action :authenticate_user!
+ before_action :redirect_if_onboarding_complete, except: [:complete]
+
+ def index
+ # Display the onboarding form
+ end
+
+ def complete
+ if onboarding_params_valid?
+ current_user.update!(onboarding_params)
+ current_user.complete_onboarding!
+
+ flash[:notice] = "Bienvenue sur AperoNight ! Votre profil a été configuré avec succès."
+ redirect_to dashboard_path
+ else
+ flash.now[:alert] = "Veuillez remplir tous les champs requis."
+ render :index
+ end
+ end
+
+ private
+
+ def onboarding_params
+ params.require(:user).permit(:first_name, :last_name, :company_name)
+ end
+
+ def onboarding_params_valid?
+ onboarding_params[:first_name].present? &&
+ onboarding_params[:last_name].present?
+ end
+
+ def redirect_if_onboarding_complete
+ if current_user&.onboarding_completed?
+ redirect_to dashboard_path
+ end
+ end
+end
diff --git a/app/helpers/onboarding_helper.rb b/app/helpers/onboarding_helper.rb
new file mode 100644
index 0000000..c01463d
--- /dev/null
+++ b/app/helpers/onboarding_helper.rb
@@ -0,0 +1,2 @@
+module OnboardingHelper
+end
diff --git a/app/javascript/controllers/toggle_section_controller.js b/app/javascript/controllers/toggle_section_controller.js
new file mode 100644
index 0000000..4eda2e5
--- /dev/null
+++ b/app/javascript/controllers/toggle_section_controller.js
@@ -0,0 +1,25 @@
+import { Controller } from "@hotwired/stimulus"
+
+// Connects to data-controller="toggle-section"
+export default class extends Controller {
+ static targets = ["section", "icon"]
+
+ connect() {
+ // Ensure the section starts hidden
+ this.sectionTarget.classList.add("hidden")
+ }
+
+ toggle() {
+ const isHidden = this.sectionTarget.classList.contains("hidden")
+
+ if (isHidden) {
+ // Show the section
+ this.sectionTarget.classList.remove("hidden")
+ this.iconTarget.classList.add("rotate-180")
+ } else {
+ // Hide the section
+ this.sectionTarget.classList.add("hidden")
+ this.iconTarget.classList.remove("rotate-180")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/models/user.rb b/app/models/user.rb
index 310964c..be9ca19 100755
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -29,6 +29,15 @@ class User < ApplicationRecord
validates :first_name, length: { minimum: 2, maximum: 50, allow_blank: true }
validates :company_name, length: { minimum: 2, maximum: 100, allow_blank: true }
+ # Onboarding methods
+ def needs_onboarding?
+ !onboarding_completed?
+ end
+
+ def complete_onboarding!
+ update!(onboarding_completed: true)
+ end
+
# Authorization methods
def can_manage_events?
# For now, all authenticated users can manage events
diff --git a/app/views/components/_header.html.erb b/app/views/components/_header.html.erb
index 75f9e21..f787bfa 100755
--- a/app/views/components/_header.html.erb
+++ b/app/views/components/_header.html.erb
@@ -30,17 +30,19 @@
-
+
<%= current_user.first_name || current_user.email %>
<%= current_user.email %>
- <%= link_to "Profile", edit_user_registration_path,
- class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
<%= link_to "Reservations", "#",
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
+
+ <%= link_to "Sécurité", edit_user_registration_path,
+ class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
+
<%= link_to "Sign out", destroy_user_session_path,
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
@@ -86,11 +88,11 @@
- <%= link_to t("header.profile"), edit_user_registration_path,
+ <%= link_to "Réservations", "#",
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
- <%= link_to t("header.reservations"), "#",
+ <%= link_to "Sécurité", edit_user_registration_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
- <%= link_to t("header.logout"), destroy_user_session_path,
+ <%= link_to "Déconnexion", destroy_user_session_path,
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
diff --git a/app/views/onboarding/index.html.erb b/app/views/onboarding/index.html.erb
new file mode 100644
index 0000000..22b8e67
--- /dev/null
+++ b/app/views/onboarding/index.html.erb
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
Bienvenue sur <%= Rails.application.config.app_name %> !
+
+ Configurons rapidement votre profil pour personnaliser votre expérience.
+
+
+
+
+
+ <%= form_with model: current_user, url: complete_onboarding_path, local: true, method: :post, class: "space-y-6" do |form| %>
+
+
+
+
+ Étape 1 sur 1
+ Configuration du profil
+
+
+
+
+
+
+
+
+
+
+ Informations personnelles
+
+
+
+
+
+ <%= form.label :first_name, "Prénom", class: "block text-sm font-medium text-gray-700 mb-2" %>
+ <%= form.text_field :first_name,
+ value: current_user.first_name,
+ class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
+ placeholder: "Votre prénom",
+ required: true %>
+
+
+
+
+ <%= form.label :last_name, "Nom", class: "block text-sm font-medium text-gray-700 mb-2" %>
+ <%= form.text_field :last_name,
+ value: current_user.last_name,
+ class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
+ placeholder: "Votre nom de famille",
+ required: true %>
+
+
+
+
+
+
+
+
+
+
+
+
Informations professionnelles
+
+
+ <%= form.label :company_name, "Nom de l'entreprise", class: "block text-sm font-medium text-gray-700 mb-2" %>
+ <%= form.text_field :company_name,
+ value: current_user.company_name,
+ class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
+ placeholder: "Nom de votre entreprise" %>
+
+ Cette information peut être utile si vous organisez des événements professionnels.
+
+
+
+
+
+
+
+
+
+
+ Vous pourrez modifier ces informations plus tard.
+
+ <%= form.submit "Finaliser mon profil",
+ class: "w-full px-8 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors cursor-pointer" %>
+
+
+
+ <% end %>
+
+
+
+
+
+ Après la configuration, vous pourrez :
+
+
+
+
+
Réserver des billets
+
+
+
+
Gérer vos commandes
+
+
+
+
Créer des événements
+
+
+
+
+
diff --git a/config/routes.rb b/config/routes.rb
index 4291f5d..04024c4 100755
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,6 +31,10 @@ Rails.application.routes.draw do
confirmation: "auth/confirmations" # Custom controller for confirmations
}
+ # === Onboarding ===
+ get "onboarding", to: "onboarding#index", as: "onboarding"
+ post "onboarding", to: "onboarding#complete", as: "complete_onboarding"
+
# === Pages ===
get "dashboard", to: "pages#dashboard", as: "dashboard"
diff --git a/db/migrate/20250908092220_add_onboarding_to_users.rb b/db/migrate/20250908092220_add_onboarding_to_users.rb
new file mode 100644
index 0000000..0861ddf
--- /dev/null
+++ b/db/migrate/20250908092220_add_onboarding_to_users.rb
@@ -0,0 +1,5 @@
+class AddOnboardingToUsers < ActiveRecord::Migration[8.0]
+ def change
+ add_column :users, :onboarding_completed, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index aafc021..4b736c6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
+ActiveRecord::Schema[8.0].define(version: 2025_09_08_092220) do
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "slug", null: false
@@ -94,6 +94,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
t.string "stripe_customer_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "onboarding_completed", default: false, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
diff --git a/test/controllers/application_controller_onboarding_test.rb b/test/controllers/application_controller_onboarding_test.rb
new file mode 100644
index 0000000..e6a3de6
--- /dev/null
+++ b/test/controllers/application_controller_onboarding_test.rb
@@ -0,0 +1,57 @@
+require "test_helper"
+
+class ApplicationControllerOnboardingTest < ActionDispatch::IntegrationTest
+ setup do
+ @user_without_onboarding = users(:one)
+ @user_without_onboarding.update!(onboarding_completed: false)
+
+ @user_with_onboarding = users(:two)
+ @user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe")
+ end
+
+ test "should redirect incomplete users to onboarding from dashboard" do
+ sign_in @user_without_onboarding
+ get dashboard_path
+ assert_redirected_to onboarding_path
+ end
+
+ test "should allow complete users to access dashboard" do
+ sign_in @user_with_onboarding
+ get dashboard_path
+ assert_response :success
+ end
+
+ test "should redirect incomplete users to onboarding from events" do
+ sign_in @user_without_onboarding
+ get events_path
+ assert_redirected_to onboarding_path
+ end
+
+ test "should allow complete users to access events" do
+ sign_in @user_with_onboarding
+ get events_path
+ assert_response :success
+ end
+
+ test "should not redirect from home page when not signed in" do
+ get root_path
+ assert_response :success
+ end
+
+ test "should redirect signed in incomplete users from home to onboarding" do
+ sign_in @user_without_onboarding
+ get root_path
+ assert_redirected_to dashboard_path # Home redirects to dashboard for signed in users
+ end
+
+ test "should not interfere with devise controllers" do
+ get new_user_session_path
+ assert_response :success
+ end
+
+ test "should not redirect when already on onboarding page" do
+ sign_in @user_without_onboarding
+ get onboarding_path
+ assert_response :success
+ end
+end
\ No newline at end of file
diff --git a/test/controllers/onboarding_controller_test.rb b/test/controllers/onboarding_controller_test.rb
new file mode 100644
index 0000000..6a878f1
--- /dev/null
+++ b/test/controllers/onboarding_controller_test.rb
@@ -0,0 +1,104 @@
+require "test_helper"
+
+class OnboardingControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @user_without_onboarding = users(:one)
+ @user_without_onboarding.update!(onboarding_completed: false)
+
+ @user_with_onboarding = users(:two)
+ @user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe")
+ end
+
+ test "should redirect to onboarding when user not signed in" do
+ get onboarding_path
+ assert_redirected_to new_user_session_path
+ end
+
+ test "should show onboarding page for incomplete user" do
+ sign_in @user_without_onboarding
+ get onboarding_path
+ assert_response :success
+ assert_select "h1", "Bienvenue sur AperoNight !"
+ assert_select "form"
+ end
+
+ test "should redirect completed user to dashboard" do
+ sign_in @user_with_onboarding
+ get onboarding_path
+ assert_redirected_to dashboard_path
+ end
+
+ test "should complete onboarding with valid data" do
+ sign_in @user_without_onboarding
+
+ assert_not @user_without_onboarding.onboarding_completed?
+
+ post complete_onboarding_path, params: {
+ user: {
+ first_name: "Jane",
+ last_name: "Smith",
+ company_name: "Test Company"
+ }
+ }
+
+ assert_redirected_to dashboard_path
+ follow_redirect!
+ assert_select ".notification", /Bienvenue sur AperoNight/
+
+ @user_without_onboarding.reload
+ assert @user_without_onboarding.onboarding_completed?
+ assert_equal "Jane", @user_without_onboarding.first_name
+ assert_equal "Smith", @user_without_onboarding.last_name
+ assert_equal "Test Company", @user_without_onboarding.company_name
+ end
+
+ test "should complete onboarding without optional company name" do
+ sign_in @user_without_onboarding
+
+ post complete_onboarding_path, params: {
+ user: {
+ first_name: "Jane",
+ last_name: "Smith",
+ company_name: ""
+ }
+ }
+
+ assert_redirected_to dashboard_path
+ @user_without_onboarding.reload
+ assert @user_without_onboarding.onboarding_completed?
+ end
+
+ test "should not complete onboarding without required fields" do
+ sign_in @user_without_onboarding
+
+ post complete_onboarding_path, params: {
+ user: {
+ first_name: "",
+ last_name: "Smith"
+ }
+ }
+
+ assert_response :success
+ assert_select ".notification", /Veuillez remplir tous les champs requis/
+
+ @user_without_onboarding.reload
+ assert_not @user_without_onboarding.onboarding_completed?
+ end
+
+ test "should not complete onboarding without last name" do
+ sign_in @user_without_onboarding
+
+ post complete_onboarding_path, params: {
+ user: {
+ first_name: "Jane",
+ last_name: ""
+ }
+ }
+
+ assert_response :success
+ assert_select ".notification", /Veuillez remplir tous les champs requis/
+
+ @user_without_onboarding.reload
+ assert_not @user_without_onboarding.onboarding_completed?
+ end
+end
diff --git a/test/controllers/orders_controller_test.rb b/test/controllers/orders_controller_test.rb
index f743b1e..aaa6a3c 100644
--- a/test/controllers/orders_controller_test.rb
+++ b/test/controllers/orders_controller_test.rb
@@ -5,7 +5,10 @@ class OrdersControllerTest < ActionDispatch::IntegrationTest
@user = User.create!(
email: "test@example.com",
password: "password123",
- password_confirmation: "password123"
+ password_confirmation: "password123",
+ onboarding_completed: true,
+ first_name: "Test",
+ last_name: "User"
)
@event = Event.create!(
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index ede4d0c..18b375a 100755
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -5,9 +5,11 @@ one:
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
last_name: Trump
first_name: Donald
+ onboarding_completed: true
two:
email: user2@example.com
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
last_name: Obama
first_name: Barack
+ onboarding_completed: true
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 1110eef..1669280 100755
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -63,4 +63,33 @@ class UserTest < ActiveSupport::TestCase
refute user.valid?, "User with last_name longer than 12 chars should be invalid"
assert_not_nil user.errors[:last_name], "No validation error for too long last_name"
end
+
+ # Test onboarding functionality
+ test "new users should need onboarding by default" do
+ user = User.new(email: "test@example.com", password: "password123")
+ assert user.needs_onboarding?, "New user should need onboarding"
+ assert_not user.onboarding_completed?, "New user should not have completed onboarding"
+ end
+
+ test "should complete onboarding" do
+ user = users(:one)
+ user.update!(onboarding_completed: false)
+
+ assert user.needs_onboarding?, "User should need onboarding initially"
+
+ user.complete_onboarding!
+
+ assert_not user.needs_onboarding?, "User should not need onboarding after completion"
+ assert user.onboarding_completed?, "User should have completed onboarding"
+ end
+
+ test "needs_onboarding? should return correct value" do
+ user = users(:one)
+
+ user.update!(onboarding_completed: false)
+ assert user.needs_onboarding?, "User with false onboarding_completed should need onboarding"
+
+ user.update!(onboarding_completed: true)
+ assert_not user.needs_onboarding?, "User with true onboarding_completed should not need onboarding"
+ end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index dd0c2bd..42e9b11 100755
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -17,6 +17,18 @@ module ActiveSupport
fixtures :all
# Add more helper methods to be used by all tests here...
+
+ # Helper to create users with completed onboarding by default for tests
+ def create_test_user(attributes = {})
+ User.create!({
+ email: "test#{rand(10000)}@example.com",
+ password: "password123",
+ password_confirmation: "password123",
+ first_name: "Test",
+ last_name: "User",
+ onboarding_completed: true
+ }.merge(attributes))
+ end
end
end