feat(auth): enhance user registration with names and improve UI

- 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 <crush@charm.land>
This commit is contained in:
Kevin BATAILLE
2025-08-26 17:17:50 +02:00
parent 6b37c67b47
commit 884c6a8262
27 changed files with 462 additions and 160 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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, {

View File

@@ -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

View File

@@ -1,122 +1,141 @@
<header class="shadow-sm border-b border-neutral-200">
<div class="bg-gray-800">
<nav x-data="{ open: false }" class="bg-blue border-b border-purple-700">
<!-- Primary Navigation Menu -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<%= link_to Rails.application.config.app_name, "/", class: "text-xl font-bold text-white" %>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex items-center">
<%= link_to "Soirées et afterworks", parties_path, class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
<%= link_to "Concerts", "#", class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
</div>
<div class="bg-gray-800">
<nav x-data="{ open: false }" class="bg-blue border-b border-purple-700">
<!-- Primary Navigation Menu -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<%= link_to Rails.application.config.app_name, "/" , class: "text-xl font-bold text-white" %>
</div>
<!-- Authentication Links -->
<% if user_signed_in? %>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
<button class="bg-purple-700 text-white border border-purple-800 font-medium py-2 px-4 rounded-lg hover:bg-purple-800 transition-colors duration-200 focus-ring">
<div><%= current_user.email %></div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-purple-700 py-1 bg-purple-600">
<%= link_to "Mon profil", edit_user_registration_path, class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
<%= link_to "Mes réservations", "#", class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
<%= 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 w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
</div>
</div>
</div>
</div>
<% else %>
<!-- Login/Register Links -->
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-4 items-center">
<%= link_to "S'inscrire", new_user_registration_path, class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
<%= link_to "Se connecter", new_user_session_path, class: "bg-white text-purple-600 font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-purple-100 transition-all duration-200" %>
</div>
<% end %>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="p-2 rounded-md text-purple-200 hover:text-white hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex items-center">
<%= link_to "Soirées et afterworks" , parties_path,
class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
<%= link_to "Concerts" , "#" ,
class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
</div>
</div>
<!-- Authentication Links -->
<% if user_signed_in? %>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
<button
class="bg-purple-700 text-white border border-purple-800 font-medium py-2 px-4 rounded-lg hover:bg-purple-800 transition-colors duration-200 focus-ring">
<div>
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</div>
</button>
</div>
<div x-show="open" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0" style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-purple-700 py-1 bg-purple-600">
<%= link_to "Mon profil" , edit_user_registration_path,
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
<%= link_to "Mes réservations" , "#" ,
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
<%= 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 w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
</div>
</div>
</div>
</div>
<% else %>
<!-- Login/Register Links -->
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-4 items-center">
<%= link_to "Se connecter" , new_user_session_path,
class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
<%= link_to "S'inscrire" , new_user_registration_path,
class: "bg-white text-purple-600 font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-purple-100 transition-all duration-200"
%>
</div>
<% end %>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open"
class="p-2 rounded-md text-purple-200 hover:text-white hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1 bg-purple-600">
<%= link_to "Soirées et afterworks", "#", class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700" %>
<%= link_to "Concerts", "#", class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700" %>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-purple-700 bg-purple-600">
<% if user_signed_in? %>
<div class="px-4">
<div class="font-medium text-base text-white"><%= current_user.email %></div>
<div class="font-medium text-sm text-purple-200"><%= current_user.email %></div>
</div>
<div class="mt-3 space-y-1">
<%= link_to "Mon profil", edit_user_registration_path, class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700" %>
<%= link_to "Mes réservations", "#", class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700" %>
<%= 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-white hover:text-purple-200 hover:bg-purple-700" %>
</div>
<% else %>
<div class="mt-3 space-y-1">
<%= link_to "S'inscrire", new_user_registration_path, class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700" %>
<%= link_to "Se connecter", new_user_session_path, class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700" %>
</div>
<% end %>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1 bg-purple-600">
<%= link_to "Soirées et afterworks" , "#" ,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
%>
<%= link_to "Concerts" , "#" ,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
%>
</div>
</nav>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-purple-700 bg-purple-600">
<% if user_signed_in? %>
<div class="px-4">
<% if current_user.first_name %>
<div class="font-medium text-base text-white">
<%= current_user.first_name %>
</div>
<% else %>
<div class="font-medium text-base text-white">
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
</div>
<%# <div class="font-medium text-sm text-purple-200">
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
</div>
%>
<% end %>
</div>
<div class="mt-3 space-y-1">
<%= link_to "Mon profil" , edit_user_registration_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
%>
<%= link_to "Mes réservations" , "#" ,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
%>
<%= 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-white hover:text-purple-200 hover:bg-purple-700"
%>
</div>
<% else %>
<div class="mt-3 space-y-1">
<%= link_to "S'inscrire" , new_user_registration_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
%>
<%= link_to "Se connecter" , new_user_session_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
%>
</div>
<% end %>
</div>
</div>
</nav>
</div>
</header>

View File

@@ -56,7 +56,9 @@
</div>
</div>
<%= render "devise/shared/links" %>
<div class="mt-4">
<%= render "devise/shared/links" %>
</div>
</div>
</div>
</div>

View File

@@ -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
-%>
<li>
<%= 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 %>
</li>

View File

@@ -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
-%>
<li>
<span class="px-3 py-2 text-sm font-medium text-gray-400 bg-transparent">
<%= t('views.pagination.truncate').html_safe %>
</span>
</li>

View File

@@ -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
-%>
<li>
<%= 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 %>
</li>

View File

@@ -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
-%>
<li>
<%= 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 %>
</li>

View File

@@ -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
-%>
<li>
<% if page.current? %>
<span class="px-3 py-2 text-sm font-medium text-white bg-indigo-600 border border-indigo-600 rounded shadow-md" aria-current="page">
<%= page %>
</span>
<% 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 %>
</li>

View File

@@ -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 -%>
<nav class="flex justify-center mt-8 mb-4" role="navigation" aria-label="pager">
<ul class="flex flex-wrap items-center justify-center gap-2">
<%= first_page_tag unless current_page.first? %>
<%= prev_page_tag unless current_page.first? %>
<% each_page do |page| -%>
<% if page.display_tag? -%>
<%= page_tag page %>
<% elsif !page.was_truncated? -%>
<%= gap_tag %>
<% end -%>
<% end -%>
<% unless current_page.out_of_range? %>
<%= next_page_tag unless current_page.last? %>
<%= last_page_tag unless current_page.last? %>
<% end %>
</ul>
</nav>
<% end -%>

View File

@@ -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
-%>
<li>
<%= 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 %>
</li>

View File

@@ -1,25 +0,0 @@
<nav aria-label="Page navigation">
<ul class="pagination">
<% if paginator.prev_page %>
<li class="page-item">
<%= link_to 'Previous', url_for(page: paginator.prev_page), class: 'page-link' %>
</li>
<% else %>
<li class="page-item disabled"><span class="page-link">Previous</span></li>
<% end %>
<% paginator.page_range.each do |page| %>
<li class="page-item <%= 'active' if page == paginator.current_page %>">
<%= link_to page, url_for(page: page), class: 'page-link' %>
</li>
<% end %>
<% if paginator.next_page %>
<li class="page-item">
<%= link_to 'Next', url_for(page: paginator.next_page), class: 'page-link' %>
</li>
<% else %>
<li class="page-item disabled"><span class="page-link">Next</span></li>
<% end %>
</ul>
</nav>

View File

@@ -243,8 +243,8 @@
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-4xl font-bold text-neutral-900 mb-6">Prêt à vivre la nuit parisienne ?</h2>
<p class="text-xl text-neutral-700 mb-8">Rejoignez des milliers de party-goers qui utilisent Aperonight chaque semaine</p>
<button class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full text-lg transition-all duration-300 transform hover:scale-105 shadow-xl">
<%= link_to new_user_registration_path, class: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full text-lg transition-all duration-300 transform hover:scale-105 shadow-xl" do %>
S'inscrire gratuitement
</button>
<% end %>
</div>
</section>