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:
64
.editorconfig
Normal file
64
.editorconfig
Normal file
@@ -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
|
||||
1
Gemfile
1
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,52 +9,50 @@
|
||||
<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" %>
|
||||
<%= 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 %></div>
|
||||
<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" />
|
||||
<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"
|
||||
<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;"
|
||||
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
|
||||
},
|
||||
<%= 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>
|
||||
@@ -63,56 +61,77 @@
|
||||
<% 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" %>
|
||||
<%= 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">
|
||||
<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" />
|
||||
<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>
|
||||
</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" %>
|
||||
<%= 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>
|
||||
<% 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 "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" %>
|
||||
<%= 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" %>
|
||||
<%= 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>
|
||||
|
||||
@@ -56,7 +56,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
13
app/views/kaminari/_first_page.html.erb
Normal file
13
app/views/kaminari/_first_page.html.erb
Normal 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>
|
||||
12
app/views/kaminari/_gap.html.erb
Normal file
12
app/views/kaminari/_gap.html.erb
Normal 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>
|
||||
13
app/views/kaminari/_last_page.html.erb
Normal file
13
app/views/kaminari/_last_page.html.erb
Normal 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>
|
||||
13
app/views/kaminari/_next_page.html.erb
Normal file
13
app/views/kaminari/_next_page.html.erb
Normal 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>
|
||||
20
app/views/kaminari/_page.html.erb
Normal file
20
app/views/kaminari/_page.html.erb
Normal 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>
|
||||
27
app/views/kaminari/_paginator.html.erb
Normal file
27
app/views/kaminari/_paginator.html.erb
Normal 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 -%>
|
||||
13
app/views/kaminari/_prev_page.html.erb
Normal file
13
app/views/kaminari/_prev_page.html.erb
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
46
config/locales/fr.yml
Normal file
46
config/locales/fr.yml
Normal file
@@ -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 <b>1</b> %{entry_name}"
|
||||
other: "Affichage de <b>tous les %{count}</b> %{entry_name}"
|
||||
more_pages:
|
||||
display_entries: "Affichage de %{entry_name} <b>%{first} - %{last}</b> sur <b>%{total}</b> 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
db/schema.rb
generated
2
db/schema.rb
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
19
test.sh
Normal file
19
test.sh
Normal file
@@ -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
|
||||
4
test/fixtures/users.yml
vendored
4
test/fixtures/users.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user