From 884c6a82626ba2d86551af75a60a531661dd0902 Mon Sep 17 00:00:00 2001 From: Kevin BATAILLE Date: Tue, 26 Aug 2025 17:17:50 +0200 Subject: [PATCH] feat(auth): enhance user registration with names and improve UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add first_name and last_name fields to User model with validations - Configure Devise registrations controller to accept name parameters - Update registration form with name fields and improved styling - Replace Twitter Bootstrap pagination with custom Tailwind components - Add French locale translations for pagination and models - Update header styling with responsive design improvements - Add EditorConfig for consistent code formatting - Fix logout controller URL handling and improve JavaScript - Update seed data and test fixtures with name attributes - Add comprehensive model tests for name validations - Add test.sh script for easier test execution 💘 Generated with Crush Co-Authored-By: Crush --- .editorconfig | 64 +++++ Gemfile | 1 + Gemfile.lock | 2 + .../registrations_controller.rb | 16 +- app/javascript/controllers/index.js | 2 + .../controllers/logout_controller.js | 14 +- app/models/user.rb | 4 + app/views/components/_header.html.erb | 247 ++++++++++-------- app/views/devise/registrations/new.html.erb | 4 +- app/views/kaminari/_first_page.html.erb | 13 + app/views/kaminari/_gap.html.erb | 12 + app/views/kaminari/_last_page.html.erb | 13 + app/views/kaminari/_next_page.html.erb | 13 + app/views/kaminari/_page.html.erb | 20 ++ app/views/kaminari/_paginator.html.erb | 27 ++ app/views/kaminari/_prev_page.html.erb | 13 + .../twitter_bootstrap/_paginator.html.erb | 25 -- app/views/pages/home.html.erb | 4 +- config/application.rb | 4 + config/locales/fr.yml | 46 ++++ config/routes.rb | 8 +- .../20250816145933_devise_create_users.rb | 3 + db/schema.rb | 2 + db/seeds.rb | 4 + test.sh | 19 ++ test/fixtures/users.yml | 4 + test/models/user_test.rb | 38 +++ 27 files changed, 462 insertions(+), 160 deletions(-) create mode 100644 .editorconfig create mode 100644 app/views/kaminari/_first_page.html.erb create mode 100644 app/views/kaminari/_gap.html.erb create mode 100644 app/views/kaminari/_last_page.html.erb create mode 100644 app/views/kaminari/_next_page.html.erb create mode 100644 app/views/kaminari/_page.html.erb create mode 100644 app/views/kaminari/_paginator.html.erb create mode 100644 app/views/kaminari/_prev_page.html.erb delete mode 100644 app/views/kaminari/twitter_bootstrap/_paginator.html.erb create mode 100644 config/locales/fr.yml create mode 100644 test.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5d840e3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,64 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to have these uncommented (set to true). +# If you want to support older versions of Ruby, set this to 1.9 +# ruby_version = 2.7 +# If you want to support older versions of JavaScript, set this to 5 +# javascript_version = 6 + +# Extend from global settings +[*.{rb,erb}] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,jsx}] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{json,json5,jsonc}] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{css,scss,less}] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{html,htm,erb}] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{md,markdown}] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/Gemfile b/Gemfile index 15c9858..e08976d 100644 --- a/Gemfile +++ b/Gemfile @@ -74,5 +74,6 @@ gem "devise", "~> 4.9" # Pagination gem gem "kaminari", "~> 1.2" +gem "kaminari-tailwind", "~> 0.1.0" # gem "net-pop", "~> 0.1.2" diff --git a/Gemfile.lock b/Gemfile.lock index 2c35911..f99db36 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -159,6 +159,7 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + kaminari-tailwind (0.1.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) @@ -399,6 +400,7 @@ DEPENDENCIES jsbundling-rails kamal kaminari (~> 1.2) + kaminari-tailwind (~> 0.1.0) minitest-reporters (~> 1.7) mysql2 (~> 0.5) propshaft diff --git a/app/controllers/authentications/registrations_controller.rb b/app/controllers/authentications/registrations_controller.rb index 5c54517..8d510e0 100644 --- a/app/controllers/authentications/registrations_controller.rb +++ b/app/controllers/authentications/registrations_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Authentications::RegistrationsController < Devise::RegistrationsController - # before_action :configure_sign_up_params, only: [:create] - # before_action :configure_account_update_params, only: [:update] + before_action :configure_sign_up_params, only: [ :create ] + before_action :configure_account_update_params, only: [ :update ] # GET /resource/sign_up # def new @@ -41,14 +41,14 @@ class Authentications::RegistrationsController < Devise::RegistrationsController # protected # If you have extra params to permit, append them to the sanitizer. - # def configure_sign_up_params - # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute]) - # end + def configure_sign_up_params + devise_parameter_sanitizer.permit(:sign_up, keys: [ :last_name, :first_name ]) + end # If you have extra params to permit, append them to the sanitizer. - # def configure_account_update_params - # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute]) - # end + def configure_account_update_params + devise_parameter_sanitizer.permit(:account_update, keys: [ :last_name, :first_name ]) + end # The path used after sign up. # def after_sign_up_path_for(resource) diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index c23dca2..2dd37f0 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -4,8 +4,10 @@ import { application } from "./application" +import LogoutController from "./logout_controller" import ShadcnTestController from "./shadcn_test_controller" import CounterController from "./counter_controller" +application.register("logout", LogoutController) application.register("shadcn-test", ShadcnTestController) application.register("counter", CounterController) diff --git a/app/javascript/controllers/logout_controller.js b/app/javascript/controllers/logout_controller.js index cedb53c..3819eaa 100644 --- a/app/javascript/controllers/logout_controller.js +++ b/app/javascript/controllers/logout_controller.js @@ -1,4 +1,3 @@ -// app/javascript/controllers/logout_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { @@ -7,14 +6,13 @@ export default class extends Controller { }; connect() { - // Optional: Add confirmation message - //console.log("Hello LogoutController, Stimulus!", this.element); - // this.element.dataset.confirm = "Êtes-vous sĂ»r de vouloir vous dĂ©connecter ?"; + // Display a message when the controller is mounted + console.log("LogoutController mounted", this.element); } signOut(event) { event.preventDefault(); - console.log("LogoutController#signOut mounted"); + console.log("User clicked on logout button."); // Ensure user wants to disconnect with a confirmation request // if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; } @@ -23,7 +21,11 @@ export default class extends Controller { const csrfToken = document.querySelector("[name='csrf-token']").content; // Define url to redirect user when action is valid - const url = this.hasUrlValue ? this.urlValue : this.element.href; + let url = this.hasUrlValue ? this.urlValue : this.element.href; + // Ensure the URL is using the correct path prefix + if (url && !url.includes('/auth/sign_out')) { + url = url.replace('/users/sign_out', '/auth/sign_out'); + } // Use fetch to send logout request fetch(url, { diff --git a/app/models/user.rb b/app/models/user.rb index bebc596..c2caaee 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,4 +22,8 @@ class User < ApplicationRecord # Relationships has_many :parties, dependent: :destroy has_many :tickets, dependent: :destroy + + # Validations + validates :last_name, length: { minimum: 3, maximum: 12, allow_blank: true } + validates :first_name, length: { minimum: 3, maximum: 12, allow_blank: true } end diff --git a/app/views/components/_header.html.erb b/app/views/components/_header.html.erb index 2792e54..f73cf6f 100644 --- a/app/views/components/_header.html.erb +++ b/app/views/components/_header.html.erb @@ -1,122 +1,141 @@
-
- +
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index fb15ab4..8e917b2 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -56,7 +56,9 @@ - <%= render "devise/shared/links" %> +
+ <%= render "devise/shared/links" %> +
diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb new file mode 100644 index 0000000..a5e6c2b --- /dev/null +++ b/app/views/kaminari/_first_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "First" page + - available local variables + url: url to the first page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, + class: "px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded hover:bg-gray-100 hover:text-gray-700 transition-colors duration-200 shadow-sm hover:shadow-md", + remote: remote %> +
  • diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb new file mode 100644 index 0000000..eed87ff --- /dev/null +++ b/app/views/kaminari/_gap.html.erb @@ -0,0 +1,12 @@ +<%# Non-link tag that stands for skipped pages... + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + + <%= t('views.pagination.truncate').html_safe %> + +
  • \ No newline at end of file diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb new file mode 100644 index 0000000..9d80a0a --- /dev/null +++ b/app/views/kaminari/_last_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Last" page + - available local variables + url: url to the last page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, + class: "px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded hover:bg-gray-100 hover:text-gray-700 transition-colors duration-200 shadow-sm hover:shadow-md", + remote: remote %> +
  • diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb new file mode 100644 index 0000000..212c8b5 --- /dev/null +++ b/app/views/kaminari/_next_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Next" page + - available local variables + url: url to the next page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, + class: "px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded hover:bg-gray-100 hover:text-gray-700 transition-colors duration-200 shadow-sm hover:shadow-md", + rel: 'next', remote: remote %> +
  • diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb new file mode 100644 index 0000000..97723c1 --- /dev/null +++ b/app/views/kaminari/_page.html.erb @@ -0,0 +1,20 @@ +<%# Link showing page number + - available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <% if page.current? %> + + <%= page %> + + <% else %> + <%= link_to page, url, + class: "px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded hover:bg-gray-100 hover:text-gray-700 transition-colors duration-200 shadow-sm hover:shadow-md", + remote: remote, rel: page.rel %> + <% end %> +
  • diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb new file mode 100644 index 0000000..6f667a0 --- /dev/null +++ b/app/views/kaminari/_paginator.html.erb @@ -0,0 +1,27 @@ +<%# The container tag + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside +-%> +<%= paginator.render do -%> + +<% end -%> \ No newline at end of file diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb new file mode 100644 index 0000000..aec0273 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Previous" page + - available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, + class: "px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded hover:bg-gray-100 hover:text-gray-700 transition-colors duration-200 shadow-sm hover:shadow-md", + rel: 'prev', remote: remote %> +
  • diff --git a/app/views/kaminari/twitter_bootstrap/_paginator.html.erb b/app/views/kaminari/twitter_bootstrap/_paginator.html.erb deleted file mode 100644 index c192aaf..0000000 --- a/app/views/kaminari/twitter_bootstrap/_paginator.html.erb +++ /dev/null @@ -1,25 +0,0 @@ - \ No newline at end of file diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index cc242e5..3bf7786 100644 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -243,8 +243,8 @@

    PrĂȘt Ă  vivre la nuit parisienne ?

    Rejoignez des milliers de party-goers qui utilisent Aperonight chaque semaine

    - + <% end %>
    diff --git a/config/application.rb b/config/application.rb index 1dfe061..a09e088 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,5 +23,9 @@ module Aperonight # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + + config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")] + # config.i18n.default_locale = :fr + end end diff --git a/config/locales/fr.yml b/config/locales/fr.yml new file mode 100644 index 0000000..fedcd6c --- /dev/null +++ b/config/locales/fr.yml @@ -0,0 +1,46 @@ +fr: + views: + pagination: + first: "« Premier" + last: "Dernier »" + previous: "‹ PrĂ©cĂ©dent" + next: "Suivant ›" + truncate: "…" + helpers: + page_entries_info: + one_page: + display_entries: + zero: "Aucun %{entry_name} trouvĂ©" + one: "Affichage de 1 %{entry_name}" + other: "Affichage de tous les %{count} %{entry_name}" + more_pages: + display_entries: "Affichage de %{entry_name} %{first} - %{last} sur %{total} au total" + activerecord: + models: + user: "Utilisateur" + party: "SoirĂ©e" + ticket: "Billet" + ticket_type: "Type de billet" + attributes: + user: + email: "Email" + password: "Mot de passe" + password_confirmation: "Confirmation du mot de passe" + remember_me: "Se souvenir de moi" + party: + name: "Nom" + description: "Description" + start_date: "Date de dĂ©but" + end_date: "Date de fin" + location: "Lieu" + capacity: "CapacitĂ©" + ticket: + user: "Utilisateur" + ticket_type: "Type de billet" + quantity: "QuantitĂ©" + price: "Prix" + ticket_type: + name: "Nom" + description: "Description" + price: "Prix" + available_quantity: "QuantitĂ© disponible" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 9a8caa9..5a6de5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,13 +20,13 @@ Rails.application.routes.draw do # Bind devise to user # devise_for :users devise_for :users, path: "auth", path_names: { - sign_up: "register", # Route for user registration - sign_in: "login", # Route for user login - sign_out: "logout", # Route for user logout + sign_in: "sign_in", # Route for user login + sign_out: "sign_out", # Route for user logout password: "reset-password", # Route for changing password confirmation: "verification", # Route for account confirmation unlock: "unblock", # Route for account unlock - registration: "register" # Route for user registration (redundant with sign_up) + # registration: "account", # Route for user account + sign_up: "signup" # Route for user registration }, controllers: { sessions: "authentications/sessions", # Custom controller for sessions diff --git a/db/migrate/20250816145933_devise_create_users.rb b/db/migrate/20250816145933_devise_create_users.rb index 74745e4..fb23be2 100644 --- a/db/migrate/20250816145933_devise_create_users.rb +++ b/db/migrate/20250816145933_devise_create_users.rb @@ -32,6 +32,9 @@ class DeviseCreateUsers < ActiveRecord::Migration[8.0] # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at + # Personnal informations + t.string :last_name, null: true # Nom + t.string :first_name, null: true # PrĂ©nom t.timestamps null: false end diff --git a/db/schema.rb b/db/schema.rb index 279035f..9f42cd7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -69,6 +69,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" + t.string "last_name" + t.string "first_name" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["email"], name: "index_users_on_email", unique: true diff --git a/db/seeds.rb b/db/seeds.rb index c79dabf..942d725 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -12,6 +12,8 @@ admin_user = User.find_or_create_by!(email: 'admin@example.com') do |u| u.password = 'password' u.password_confirmation = 'password' + u.last_name = nil + u.first_name = nil end # Create regular users for development @@ -21,6 +23,8 @@ missing_users_count.times do |i| User.find_or_create_by!(email: "user#{i + 1}@example.com") do |u| u.password = 'password' u.password_confirmation = 'password' + u.last_name = nil + u.first_name = nil end end diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..5717b7d --- /dev/null +++ b/test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Check if a directory/file argument is provided +if [ -n "$1" ]; then + # Get the test directory/file from the first argument + TEST_PATH="$1" + + # Check if the provided argument is a directory or file + if [ -d "$TEST_PATH" ] || [ -f "$TEST_PATH" ]; then + # Run Rails tests in the specified directory/file + bundle exec rails test "$TEST_PATH" + else + echo "Error: $TEST_PATH is not a valid directory or file" + exit 1 + fi +else + # Run Rails tests in the current directory + bundle exec rails test +fi \ No newline at end of file diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index eea2000..ede4d0c 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -3,7 +3,11 @@ one: email: user1@example.com encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %> + last_name: Trump + first_name: Donald two: email: user2@example.com encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %> + last_name: Obama + first_name: Barack diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 2149d74..48b362e 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -25,4 +25,42 @@ class UserTest < ActiveSupport::TestCase assert_equal :has_many, association.macro assert_equal :destroy, association.options[:dependent] end + + # Test first_name validations + test "should validate presence of first_name" do + user = User.new(last_name: "Doe") + refute user.valid?, "User with blank first_name should be invalid" + assert_not_nil user.errors[:first_name], "No validation error for blank first_name" + end + + test "should validate length of first_name" do + # Test minimum length + user = User.new(first_name: "A", last_name: "Doe") + refute user.valid?, "User with first_name shorter than 3 chars should be invalid" + assert_not_nil user.errors[:first_name], "No validation error for too short first_name" + + # Test maximum length + user = User.new(first_name: "A" * 13, last_name: "Doe") + refute user.valid?, "User with first_name longer than 12 chars should be invalid" + assert_not_nil user.errors[:first_name], "No validation error for too long first_name" + end + + # Test last_name validations + test "should validate presence of last_name" do + user = User.new(first_name: "John") + refute user.valid?, "User with blank last_name should be invalid" + assert_not_nil user.errors[:last_name], "No validation error for blank last_name" + end + + test "should validate length of last_name" do + # Test minimum length + user = User.new(first_name: "John", last_name: "Do") + refute user.valid?, "User with last_name shorter than 3 chars should be invalid" + assert_not_nil user.errors[:last_name], "No validation error for too short last_name" + + # Test maximum length + user = User.new(first_name: "John", last_name: "D" * 13) + 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 end \ No newline at end of file